mkstats-client 0.2.1
Simple API clients for mkStats
Requires Python: >=3.10
Keywords: mkstats, analytics, api-client, exteraGram, plugin

# mkStats Python Client

Production-oriented Python clients for mkStats API.

## Package contents

- `mkstats_client`: client based on `requests`
- `mkstats_embed`: client based on Python stdlib (`urllib`)

Both clients support the same behavior:

- `device_fingerprint` payload support
- PoW challenge flow for `/handshake`
- automatic re-auth on `401 invalid_install_token`
- backoff on `429` with `Retry-After`
- buffered events with `queue_event()` and `flush_events()`

## Requirements

- Python `>= 3.10`
- mkStats backend API (`/api` or `/api/v1`)

## Installation

From PyPI:

```bash
pip install mkstats-client
```

Local development from this repository:

```bash
python -m venv .venv
.\.venv\Scripts\activate
pip install -e .\client
```

## Quick start (`requests` client)

```python
from mkstats_client import MkStatsClient, generate_user_hash, generate_device_fingerprint

api_url = "https://mkstats.mk69.su/api/v1"  # /api or /api/v1 both work
plugin_id = "my_plugin"
plugin_version = "1.0.0"
device_id = "stable-device-id"

user_hash = generate_user_hash(device_id, plugin_id)
device_fingerprint = generate_device_fingerprint(device_id)

client = MkStatsClient(
    api_url=api_url,
    plugin_id=plugin_id,
    plugin_version=plugin_version,
    user_hash=user_hash,
    device_fingerprint=device_fingerprint,
    client_name="exteraGram",
    client_version="12.3.1",
)

client.handshake()
client.send_ping()
client.track_event("feature_clicked")
```

## Quick start (`embed` client)

```python
from mkstats_embed import MkStatsCoreClient, generate_user_hash, generate_device_fingerprint

api_url = "https://mkstats.mk69.su/api/v1"
plugin_id = "my_plugin"
plugin_version = "1.0.0"
device_id = "stable-device-id"

user_hash = generate_user_hash(device_id, plugin_id)
device_fingerprint = generate_device_fingerprint(device_id)

client = MkStatsCoreClient(
    api_url=api_url,
    plugin_id=plugin_id,
    plugin_version=plugin_version,
    user_hash=user_hash,
    device_fingerprint=device_fingerprint,
    client_name="exteraGram",
    client_version="12.3.1",
)

client.handshake()
client.send_ping()
client.track_event("feature_clicked")
```

## API overview

Main methods available in both clients:

- `handshake() -> dict`: obtains `install_token`, solves PoW automatically if required
- `send_ping(install_token: str | None = None, timestamp: int | None = None) -> dict`
- `send_event(install_token: str | None, event: str, count: int = 1, timestamp: int | None = None) -> dict`
- `track_event(event: str, count: int = 1, timestamp: int | None = None) -> dict`
- `queue_event(event: str, count: int = 1) -> None`
- `flush_events(force: bool = False) -> bool`
- `heartbeat(interval_seconds: int = 1500, iterations: int | None = None, flush_buffered_events: bool = True) -> None`

Helpers:

- `generate_user_hash(device_id: str, plugin_id: str) -> str`
- `generate_device_fingerprint(device_id: str) -> str`
- `get_client_name() -> str`
- `get_exteragram_version() -> str`

## Failure behavior

- On `401`, clients reset token, run `handshake()`, and retry request once.
- On `409 metric_not_initialized` from `/event`, call `send_ping()` first.
- On `429`, clients enter backoff mode.
- While in backoff, network methods return empty dicts instead of raising.
- Event names are normalized to backend-safe format: first char `[A-Za-z_]`, then `[A-Za-z0-9_.:-]`, max 100 chars.

## Buffered events example

```python
client.queue_event("feature_opened")
client.queue_event("feature_opened")
client.queue_event("button_clicked")

ok = client.flush_events()
if not ok:
    # network/backoff issue: unsent events stay in internal queue
    pass
```

## Build and publish

From repository root:

```bash
python -m pip install -U build twine
python -m build .\client
python -m twine check .\client\dist\*
```

Upload to TestPyPI:

```bash
python -m twine upload --repository testpypi .\client\dist\*
```

Before each release, bump `version` in `client/pyproject.toml`.