cohorly 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cohorly-0.1.0/PKG-INFO +184 -0
- cohorly-0.1.0/README.md +164 -0
- cohorly-0.1.0/pyproject.toml +36 -0
- cohorly-0.1.0/setup.cfg +4 -0
- cohorly-0.1.0/src/cohorly/__init__.py +462 -0
- cohorly-0.1.0/src/cohorly/py.typed +0 -0
- cohorly-0.1.0/src/cohorly.egg-info/PKG-INFO +184 -0
- cohorly-0.1.0/src/cohorly.egg-info/SOURCES.txt +12 -0
- cohorly-0.1.0/src/cohorly.egg-info/dependency_links.txt +1 -0
- cohorly-0.1.0/src/cohorly.egg-info/requires.txt +3 -0
- cohorly-0.1.0/src/cohorly.egg-info/top_level.txt +1 -0
- cohorly-0.1.0/tests/test_buffered_consumer.py +246 -0
- cohorly-0.1.0/tests/test_client.py +165 -0
- cohorly-0.1.0/tests/test_consumer.py +97 -0
cohorly-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cohorly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Cohorly server-side Python SDK (Mixpanel-style product analytics, self-hosted)
|
|
5
|
+
Author: Cohorly
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: analytics,cohorly,mixpanel,tracking,events
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# Cohorly Python SDK
|
|
22
|
+
|
|
23
|
+
The official server-side Python SDK for [Cohorly](../../README.md), a
|
|
24
|
+
self-hosted Mixpanel-style product analytics platform. The API mirrors
|
|
25
|
+
`mixpanel-python`, so migrating existing code is mostly a matter of swapping
|
|
26
|
+
the import and pointing at your Cohorly server.
|
|
27
|
+
|
|
28
|
+
- Zero runtime dependencies (stdlib `urllib` only)
|
|
29
|
+
- Python 3.8+, fully typed (`py.typed`)
|
|
30
|
+
- Synchronous `Consumer` and batching `BufferedConsumer` with the Cohorly
|
|
31
|
+
retry contract (exponential backoff, `Retry-After`, bounded queue)
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install cohorly
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or from this repo:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ./sdks/python
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from cohorly import Cohorly
|
|
49
|
+
|
|
50
|
+
ch = Cohorly("YOUR_PROJECT_TOKEN", api_host="http://localhost:4000")
|
|
51
|
+
|
|
52
|
+
# Track an event
|
|
53
|
+
ch.track("user-1", "Signed Up", {"plan": "pro", "source": "landing"})
|
|
54
|
+
|
|
55
|
+
# Link an alias to an existing distinct_id
|
|
56
|
+
ch.alias("user-1", "anon-7f3a")
|
|
57
|
+
|
|
58
|
+
# Update a user profile
|
|
59
|
+
ch.people_set("user-1", {"$first_name": "Ada", "plan": "pro"})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The project token comes from your Cohorly dashboard (Settings -> Projects).
|
|
63
|
+
`api_host` is the base URL of your Cohorly server (default
|
|
64
|
+
`http://localhost:4000`).
|
|
65
|
+
|
|
66
|
+
## Tracking events
|
|
67
|
+
|
|
68
|
+
`track(distinct_id, event_name, properties=None, meta=None)` stamps these
|
|
69
|
+
default properties before sending:
|
|
70
|
+
|
|
71
|
+
| Property | Value |
|
|
72
|
+
| -------------- | --------------------------------------- |
|
|
73
|
+
| `distinct_id` | the id you pass |
|
|
74
|
+
| `time` | current unix time in **milliseconds** |
|
|
75
|
+
| `$insert_id` | random uuid4 hex (server-side dedup) |
|
|
76
|
+
| `$lib` | `"python"` |
|
|
77
|
+
| `$lib_version` | SDK version |
|
|
78
|
+
| `token` | your project token (stripped by server) |
|
|
79
|
+
|
|
80
|
+
Your `properties` merge over the defaults, so you may supply a custom `time`
|
|
81
|
+
or `$insert_id` (e.g. for idempotent re-sends):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
ch.track("user-1", "Order Completed", {
|
|
85
|
+
"amount": 42.5,
|
|
86
|
+
"$insert_id": f"order-{order.id}", # dedup key
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Historical imports
|
|
91
|
+
|
|
92
|
+
Use `import_data` to record events with an explicit timestamp (unix
|
|
93
|
+
milliseconds - Cohorly's convention throughout):
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
ch.import_data("user-1", "Legacy Signup", 1600000000000, {"source": "csv"})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Unlike Mixpanel there is no separate import endpoint, API secret, or 5-day
|
|
100
|
+
cutoff - it is the same `/track` pipeline.
|
|
101
|
+
|
|
102
|
+
## User profiles (people)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
ch.people_set("user-1", {"plan": "pro"}) # set/overwrite
|
|
106
|
+
ch.people_set_once("user-1", {"created": "..."}) # only if unset
|
|
107
|
+
ch.people_increment("user-1", {"logins": 1}) # numeric add
|
|
108
|
+
ch.people_unset("user-1", ["plan"]) # remove properties
|
|
109
|
+
ch.people_delete("user-1") # delete the profile
|
|
110
|
+
ch.people_update({"distinct_id": "user-1", "$set": {"x": 1}}) # raw op
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
These map to the Cohorly `/engage` operations `$set`, `$set_once`, `$add`,
|
|
114
|
+
`$unset`, `$delete`.
|
|
115
|
+
|
|
116
|
+
## Consumers
|
|
117
|
+
|
|
118
|
+
By default every call sends immediately via a synchronous `Consumer`. For
|
|
119
|
+
higher throughput use `BufferedConsumer`, which batches messages (default 50
|
|
120
|
+
per request, server max 500) and implements the Cohorly retry contract:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from cohorly import Cohorly, BufferedConsumer
|
|
124
|
+
|
|
125
|
+
consumer = BufferedConsumer(max_size=50, api_host="http://localhost:4000")
|
|
126
|
+
ch = Cohorly("YOUR_PROJECT_TOKEN", consumer=consumer)
|
|
127
|
+
|
|
128
|
+
for user in users:
|
|
129
|
+
ch.track(user.id, "Backfill Event", {"batch": True})
|
|
130
|
+
|
|
131
|
+
consumer.flush() # IMPORTANT: drain remaining messages before exit
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Retry behavior (BufferedConsumer):
|
|
135
|
+
|
|
136
|
+
- **429 / 5xx / network error** - the queue is kept and retried with
|
|
137
|
+
exponential backoff: base 2s, doubling per consecutive failure, capped at
|
|
138
|
+
10 minutes, +/-20% jitter. A `Retry-After` header is honored when present.
|
|
139
|
+
- **413** - the flush batch size is halved (floor 1) and retried.
|
|
140
|
+
- **400** - the rejected batch is dropped and `CohorlyException` is raised.
|
|
141
|
+
- **401** (invalid token) - the queue is kept; backoff at the maximum delay.
|
|
142
|
+
- The in-memory queue is capped at 1000 messages per endpoint; the oldest
|
|
143
|
+
message is dropped on overflow.
|
|
144
|
+
|
|
145
|
+
Cohorly batch rejections are atomic (nothing partially inserted), so retrying
|
|
146
|
+
the same payload is always safe.
|
|
147
|
+
|
|
148
|
+
The synchronous `Consumer(api_host, request_timeout=10, retry_limit=4)`
|
|
149
|
+
retries 429/5xx/network errors inline with the same backoff schedule up to
|
|
150
|
+
`retry_limit` times, then raises `CohorlyException`.
|
|
151
|
+
|
|
152
|
+
Because the token travels with each message, several `Cohorly` instances with
|
|
153
|
+
different project tokens can share one consumer.
|
|
154
|
+
|
|
155
|
+
## Error handling
|
|
156
|
+
|
|
157
|
+
Delivery failures raise `cohorly.CohorlyException`:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from cohorly import Cohorly, CohorlyException
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
ch.track("user-1", "event")
|
|
164
|
+
except CohorlyException as exc:
|
|
165
|
+
log.warning("cohorly delivery failed: %s", exc)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Serialization
|
|
169
|
+
|
|
170
|
+
Messages are JSON. `datetime`/`date` values are serialized to ISO-8601 by the
|
|
171
|
+
default `DatetimeSerializer`; pass your own `json.JSONEncoder` subclass via
|
|
172
|
+
`Cohorly(..., serializer=MyEncoder)` for custom types.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd sdks/python
|
|
178
|
+
python3 -m venv .venv
|
|
179
|
+
.venv/bin/pip install pytest
|
|
180
|
+
.venv/bin/pytest
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Tests run against the source tree (no install needed) and use a mocked
|
|
184
|
+
transport - no network required.
|
cohorly-0.1.0/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Cohorly Python SDK
|
|
2
|
+
|
|
3
|
+
The official server-side Python SDK for [Cohorly](../../README.md), a
|
|
4
|
+
self-hosted Mixpanel-style product analytics platform. The API mirrors
|
|
5
|
+
`mixpanel-python`, so migrating existing code is mostly a matter of swapping
|
|
6
|
+
the import and pointing at your Cohorly server.
|
|
7
|
+
|
|
8
|
+
- Zero runtime dependencies (stdlib `urllib` only)
|
|
9
|
+
- Python 3.8+, fully typed (`py.typed`)
|
|
10
|
+
- Synchronous `Consumer` and batching `BufferedConsumer` with the Cohorly
|
|
11
|
+
retry contract (exponential backoff, `Retry-After`, bounded queue)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install cohorly
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or from this repo:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ./sdks/python
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from cohorly import Cohorly
|
|
29
|
+
|
|
30
|
+
ch = Cohorly("YOUR_PROJECT_TOKEN", api_host="http://localhost:4000")
|
|
31
|
+
|
|
32
|
+
# Track an event
|
|
33
|
+
ch.track("user-1", "Signed Up", {"plan": "pro", "source": "landing"})
|
|
34
|
+
|
|
35
|
+
# Link an alias to an existing distinct_id
|
|
36
|
+
ch.alias("user-1", "anon-7f3a")
|
|
37
|
+
|
|
38
|
+
# Update a user profile
|
|
39
|
+
ch.people_set("user-1", {"$first_name": "Ada", "plan": "pro"})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The project token comes from your Cohorly dashboard (Settings -> Projects).
|
|
43
|
+
`api_host` is the base URL of your Cohorly server (default
|
|
44
|
+
`http://localhost:4000`).
|
|
45
|
+
|
|
46
|
+
## Tracking events
|
|
47
|
+
|
|
48
|
+
`track(distinct_id, event_name, properties=None, meta=None)` stamps these
|
|
49
|
+
default properties before sending:
|
|
50
|
+
|
|
51
|
+
| Property | Value |
|
|
52
|
+
| -------------- | --------------------------------------- |
|
|
53
|
+
| `distinct_id` | the id you pass |
|
|
54
|
+
| `time` | current unix time in **milliseconds** |
|
|
55
|
+
| `$insert_id` | random uuid4 hex (server-side dedup) |
|
|
56
|
+
| `$lib` | `"python"` |
|
|
57
|
+
| `$lib_version` | SDK version |
|
|
58
|
+
| `token` | your project token (stripped by server) |
|
|
59
|
+
|
|
60
|
+
Your `properties` merge over the defaults, so you may supply a custom `time`
|
|
61
|
+
or `$insert_id` (e.g. for idempotent re-sends):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
ch.track("user-1", "Order Completed", {
|
|
65
|
+
"amount": 42.5,
|
|
66
|
+
"$insert_id": f"order-{order.id}", # dedup key
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Historical imports
|
|
71
|
+
|
|
72
|
+
Use `import_data` to record events with an explicit timestamp (unix
|
|
73
|
+
milliseconds - Cohorly's convention throughout):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
ch.import_data("user-1", "Legacy Signup", 1600000000000, {"source": "csv"})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Unlike Mixpanel there is no separate import endpoint, API secret, or 5-day
|
|
80
|
+
cutoff - it is the same `/track` pipeline.
|
|
81
|
+
|
|
82
|
+
## User profiles (people)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
ch.people_set("user-1", {"plan": "pro"}) # set/overwrite
|
|
86
|
+
ch.people_set_once("user-1", {"created": "..."}) # only if unset
|
|
87
|
+
ch.people_increment("user-1", {"logins": 1}) # numeric add
|
|
88
|
+
ch.people_unset("user-1", ["plan"]) # remove properties
|
|
89
|
+
ch.people_delete("user-1") # delete the profile
|
|
90
|
+
ch.people_update({"distinct_id": "user-1", "$set": {"x": 1}}) # raw op
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
These map to the Cohorly `/engage` operations `$set`, `$set_once`, `$add`,
|
|
94
|
+
`$unset`, `$delete`.
|
|
95
|
+
|
|
96
|
+
## Consumers
|
|
97
|
+
|
|
98
|
+
By default every call sends immediately via a synchronous `Consumer`. For
|
|
99
|
+
higher throughput use `BufferedConsumer`, which batches messages (default 50
|
|
100
|
+
per request, server max 500) and implements the Cohorly retry contract:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from cohorly import Cohorly, BufferedConsumer
|
|
104
|
+
|
|
105
|
+
consumer = BufferedConsumer(max_size=50, api_host="http://localhost:4000")
|
|
106
|
+
ch = Cohorly("YOUR_PROJECT_TOKEN", consumer=consumer)
|
|
107
|
+
|
|
108
|
+
for user in users:
|
|
109
|
+
ch.track(user.id, "Backfill Event", {"batch": True})
|
|
110
|
+
|
|
111
|
+
consumer.flush() # IMPORTANT: drain remaining messages before exit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Retry behavior (BufferedConsumer):
|
|
115
|
+
|
|
116
|
+
- **429 / 5xx / network error** - the queue is kept and retried with
|
|
117
|
+
exponential backoff: base 2s, doubling per consecutive failure, capped at
|
|
118
|
+
10 minutes, +/-20% jitter. A `Retry-After` header is honored when present.
|
|
119
|
+
- **413** - the flush batch size is halved (floor 1) and retried.
|
|
120
|
+
- **400** - the rejected batch is dropped and `CohorlyException` is raised.
|
|
121
|
+
- **401** (invalid token) - the queue is kept; backoff at the maximum delay.
|
|
122
|
+
- The in-memory queue is capped at 1000 messages per endpoint; the oldest
|
|
123
|
+
message is dropped on overflow.
|
|
124
|
+
|
|
125
|
+
Cohorly batch rejections are atomic (nothing partially inserted), so retrying
|
|
126
|
+
the same payload is always safe.
|
|
127
|
+
|
|
128
|
+
The synchronous `Consumer(api_host, request_timeout=10, retry_limit=4)`
|
|
129
|
+
retries 429/5xx/network errors inline with the same backoff schedule up to
|
|
130
|
+
`retry_limit` times, then raises `CohorlyException`.
|
|
131
|
+
|
|
132
|
+
Because the token travels with each message, several `Cohorly` instances with
|
|
133
|
+
different project tokens can share one consumer.
|
|
134
|
+
|
|
135
|
+
## Error handling
|
|
136
|
+
|
|
137
|
+
Delivery failures raise `cohorly.CohorlyException`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from cohorly import Cohorly, CohorlyException
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
ch.track("user-1", "event")
|
|
144
|
+
except CohorlyException as exc:
|
|
145
|
+
log.warning("cohorly delivery failed: %s", exc)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Serialization
|
|
149
|
+
|
|
150
|
+
Messages are JSON. `datetime`/`date` values are serialized to ISO-8601 by the
|
|
151
|
+
default `DatetimeSerializer`; pass your own `json.JSONEncoder` subclass via
|
|
152
|
+
`Cohorly(..., serializer=MyEncoder)` for custom types.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cd sdks/python
|
|
158
|
+
python3 -m venv .venv
|
|
159
|
+
.venv/bin/pip install pytest
|
|
160
|
+
.venv/bin/pytest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Tests run against the source tree (no install needed) and use a mocked
|
|
164
|
+
transport - no network required.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cohorly"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Cohorly server-side Python SDK (Mixpanel-style product analytics, self-hosted)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Cohorly" }]
|
|
13
|
+
keywords = ["analytics", "cohorly", "mixpanel", "tracking", "events"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=7"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
cohorly = ["py.typed"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
cohorly-0.1.0/setup.cfg
ADDED