Metadata-Version: 2.4
Name: cohorly
Version: 0.1.0
Summary: Official Cohorly server-side Python SDK (Mixpanel-style product analytics, self-hosted)
Author: Cohorly
License: Apache-2.0
Keywords: analytics,cohorly,mixpanel,tracking,events
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# Cohorly Python SDK

The official server-side Python SDK for [Cohorly](../../README.md), a
self-hosted Mixpanel-style product analytics platform. The API mirrors
`mixpanel-python`, so migrating existing code is mostly a matter of swapping
the import and pointing at your Cohorly server.

- Zero runtime dependencies (stdlib `urllib` only)
- Python 3.8+, fully typed (`py.typed`)
- Synchronous `Consumer` and batching `BufferedConsumer` with the Cohorly
  retry contract (exponential backoff, `Retry-After`, bounded queue)

## Installation

```bash
pip install cohorly
```

Or from this repo:

```bash
pip install ./sdks/python
```

## Quickstart

```python
from cohorly import Cohorly

ch = Cohorly("YOUR_PROJECT_TOKEN", api_host="http://localhost:4000")

# Track an event
ch.track("user-1", "Signed Up", {"plan": "pro", "source": "landing"})

# Link an alias to an existing distinct_id
ch.alias("user-1", "anon-7f3a")

# Update a user profile
ch.people_set("user-1", {"$first_name": "Ada", "plan": "pro"})
```

The project token comes from your Cohorly dashboard (Settings -> Projects).
`api_host` is the base URL of your Cohorly server (default
`http://localhost:4000`).

## Tracking events

`track(distinct_id, event_name, properties=None, meta=None)` stamps these
default properties before sending:

| Property       | Value                                   |
| -------------- | --------------------------------------- |
| `distinct_id`  | the id you pass                         |
| `time`         | current unix time in **milliseconds**   |
| `$insert_id`   | random uuid4 hex (server-side dedup)    |
| `$lib`         | `"python"`                              |
| `$lib_version` | SDK version                             |
| `token`        | your project token (stripped by server) |

Your `properties` merge over the defaults, so you may supply a custom `time`
or `$insert_id` (e.g. for idempotent re-sends):

```python
ch.track("user-1", "Order Completed", {
    "amount": 42.5,
    "$insert_id": f"order-{order.id}",  # dedup key
})
```

### Historical imports

Use `import_data` to record events with an explicit timestamp (unix
milliseconds - Cohorly's convention throughout):

```python
ch.import_data("user-1", "Legacy Signup", 1600000000000, {"source": "csv"})
```

Unlike Mixpanel there is no separate import endpoint, API secret, or 5-day
cutoff - it is the same `/track` pipeline.

## User profiles (people)

```python
ch.people_set("user-1", {"plan": "pro"})          # set/overwrite
ch.people_set_once("user-1", {"created": "..."})  # only if unset
ch.people_increment("user-1", {"logins": 1})      # numeric add
ch.people_unset("user-1", ["plan"])               # remove properties
ch.people_delete("user-1")                        # delete the profile
ch.people_update({"distinct_id": "user-1", "$set": {"x": 1}})  # raw op
```

These map to the Cohorly `/engage` operations `$set`, `$set_once`, `$add`,
`$unset`, `$delete`.

## Consumers

By default every call sends immediately via a synchronous `Consumer`. For
higher throughput use `BufferedConsumer`, which batches messages (default 50
per request, server max 500) and implements the Cohorly retry contract:

```python
from cohorly import Cohorly, BufferedConsumer

consumer = BufferedConsumer(max_size=50, api_host="http://localhost:4000")
ch = Cohorly("YOUR_PROJECT_TOKEN", consumer=consumer)

for user in users:
    ch.track(user.id, "Backfill Event", {"batch": True})

consumer.flush()  # IMPORTANT: drain remaining messages before exit
```

Retry behavior (BufferedConsumer):

- **429 / 5xx / network error** - the queue is kept and retried with
  exponential backoff: base 2s, doubling per consecutive failure, capped at
  10 minutes, +/-20% jitter. A `Retry-After` header is honored when present.
- **413** - the flush batch size is halved (floor 1) and retried.
- **400** - the rejected batch is dropped and `CohorlyException` is raised.
- **401** (invalid token) - the queue is kept; backoff at the maximum delay.
- The in-memory queue is capped at 1000 messages per endpoint; the oldest
  message is dropped on overflow.

Cohorly batch rejections are atomic (nothing partially inserted), so retrying
the same payload is always safe.

The synchronous `Consumer(api_host, request_timeout=10, retry_limit=4)`
retries 429/5xx/network errors inline with the same backoff schedule up to
`retry_limit` times, then raises `CohorlyException`.

Because the token travels with each message, several `Cohorly` instances with
different project tokens can share one consumer.

## Error handling

Delivery failures raise `cohorly.CohorlyException`:

```python
from cohorly import Cohorly, CohorlyException

try:
    ch.track("user-1", "event")
except CohorlyException as exc:
    log.warning("cohorly delivery failed: %s", exc)
```

## Serialization

Messages are JSON. `datetime`/`date` values are serialized to ISO-8601 by the
default `DatetimeSerializer`; pass your own `json.JSONEncoder` subclass via
`Cohorly(..., serializer=MyEncoder)` for custom types.

## Development

```bash
cd sdks/python
python3 -m venv .venv
.venv/bin/pip install pytest
.venv/bin/pytest
```

Tests run against the source tree (no install needed) and use a mocked
transport - no network required.
