trodo-python 1.0.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.
- trodo_python-1.0.0/PKG-INFO +227 -0
- trodo_python-1.0.0/README.md +200 -0
- trodo_python-1.0.0/pyproject.toml +43 -0
- trodo_python-1.0.0/setup.cfg +4 -0
- trodo_python-1.0.0/trodo/__init__.py +134 -0
- trodo_python-1.0.0/trodo/api/__init__.py +0 -0
- trodo_python-1.0.0/trodo/api/async_client.py +96 -0
- trodo_python-1.0.0/trodo/api/endpoints.py +20 -0
- trodo_python-1.0.0/trodo/api/http_client.py +87 -0
- trodo_python-1.0.0/trodo/auto/__init__.py +0 -0
- trodo_python-1.0.0/trodo/auto/auto_event_manager.py +134 -0
- trodo_python-1.0.0/trodo/client.py +195 -0
- trodo_python-1.0.0/trodo/managers/__init__.py +0 -0
- trodo_python-1.0.0/trodo/managers/group_manager.py +106 -0
- trodo_python-1.0.0/trodo/managers/people_manager.py +77 -0
- trodo_python-1.0.0/trodo/queue/__init__.py +0 -0
- trodo_python-1.0.0/trodo/queue/batch_flusher.py +52 -0
- trodo_python-1.0.0/trodo/queue/event_queue.py +32 -0
- trodo_python-1.0.0/trodo/session/__init__.py +0 -0
- trodo_python-1.0.0/trodo/session/server_session.py +74 -0
- trodo_python-1.0.0/trodo/session/session_manager.py +74 -0
- trodo_python-1.0.0/trodo/types.py +79 -0
- trodo_python-1.0.0/trodo/user_context.py +224 -0
- trodo_python-1.0.0/trodo_python.egg-info/PKG-INFO +227 -0
- trodo_python-1.0.0/trodo_python.egg-info/SOURCES.txt +26 -0
- trodo_python-1.0.0/trodo_python.egg-info/dependency_links.txt +1 -0
- trodo_python-1.0.0/trodo_python.egg-info/requires.txt +10 -0
- trodo_python-1.0.0/trodo_python.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trodo-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Trodo Analytics SDK for Python — server-side event tracking
|
|
5
|
+
License: ISC
|
|
6
|
+
Keywords: analytics,tracking,trodo,server-side
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.28.0
|
|
20
|
+
Provides-Extra: async
|
|
21
|
+
Requires-Dist: httpx>=0.27.0; extra == "async"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
|
+
Requires-Dist: responses>=0.25.0; extra == "dev"
|
|
26
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# trodo-python
|
|
29
|
+
|
|
30
|
+
Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, and manage people/groups — all merging seamlessly with your frontend Trodo data under the same `site_id`.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install trodo-python
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For async support (optional):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install trodo-python[async]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.8+.
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import trodo
|
|
50
|
+
|
|
51
|
+
# Initialize once at app startup
|
|
52
|
+
trodo.init(
|
|
53
|
+
site_id='your-site-id',
|
|
54
|
+
debug=False, # optional: log API calls
|
|
55
|
+
auto_events=True, # optional: hook sys.excepthook
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Get a user context
|
|
59
|
+
user = trodo.for_user('user-123')
|
|
60
|
+
|
|
61
|
+
# Track a custom event
|
|
62
|
+
user.track('purchase_completed', {'amount': 99.99, 'plan': 'pro'})
|
|
63
|
+
|
|
64
|
+
# Identify the user (merges with frontend events under the same identity)
|
|
65
|
+
user.identify('user@example.com') # distinct_id becomes id_user@example.com
|
|
66
|
+
|
|
67
|
+
# Update people profile
|
|
68
|
+
user.people.set({'plan': 'pro', 'company': 'Acme'})
|
|
69
|
+
|
|
70
|
+
# Track a server-side error
|
|
71
|
+
user.capture_error(Exception('payment failed'))
|
|
72
|
+
|
|
73
|
+
# Flush queued events before process exit
|
|
74
|
+
trodo.shutdown()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Flask / FastAPI Example
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Flask
|
|
81
|
+
from flask import Flask, request
|
|
82
|
+
import trodo
|
|
83
|
+
|
|
84
|
+
app = Flask(__name__)
|
|
85
|
+
trodo.init(site_id='your-site-id')
|
|
86
|
+
|
|
87
|
+
@app.route('/purchase', methods=['POST'])
|
|
88
|
+
def purchase():
|
|
89
|
+
user_id = request.json['user_id']
|
|
90
|
+
user = trodo.for_user(user_id)
|
|
91
|
+
user.track('purchase_completed', {'amount': request.json['amount']})
|
|
92
|
+
return {'ok': True}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Cross-SDK Identity Merging
|
|
96
|
+
|
|
97
|
+
Frontend and backend events merge when both sides call `identify()` with the same value:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Python SDK
|
|
101
|
+
user.identify('user@example.com') # → id_user@example.com
|
|
102
|
+
|
|
103
|
+
# Browser SDK (same value)
|
|
104
|
+
# Trodo.identify('user@example.com') → id_user@example.com
|
|
105
|
+
|
|
106
|
+
# Both event streams now appear together in the Trodo dashboard
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `trodo.init(config)`
|
|
112
|
+
|
|
113
|
+
| Parameter | Type | Default | Description |
|
|
114
|
+
|-----------|------|---------|-------------|
|
|
115
|
+
| `site_id` | `str` | required | Your Trodo site ID |
|
|
116
|
+
| `api_base` | `str` | `https://sdkapi.trodo.ai` | API base URL |
|
|
117
|
+
| `debug` | `bool` | `False` | Log API requests/responses |
|
|
118
|
+
| `auto_events` | `bool` | `False` | Hook `sys.excepthook` + `threading.excepthook` |
|
|
119
|
+
| `retries` | `int` | `3` | HTTP retry attempts on 5xx errors |
|
|
120
|
+
| `timeout` | `int` | `10` | HTTP timeout in seconds |
|
|
121
|
+
| `batch_enabled` | `bool` | `False` | Queue events and flush in bulk |
|
|
122
|
+
| `batch_size` | `int` | `50` | Max events per batch flush |
|
|
123
|
+
| `batch_flush_interval` | `float` | `5.0` | Flush interval in seconds |
|
|
124
|
+
| `on_error` | `callable` | `None` | Callback for SDK errors |
|
|
125
|
+
|
|
126
|
+
### `trodo.for_user(distinct_id, session_id=None)`
|
|
127
|
+
|
|
128
|
+
Returns a user-bound context. All subsequent calls use this user's session.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
user = trodo.for_user(
|
|
132
|
+
'user-123',
|
|
133
|
+
session_id=request.cookies.get('trodo_session'), # optional: correlate with browser session
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### User Context Methods
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
user.track(event_name, properties=None) # Track custom event
|
|
141
|
+
user.track_event(event_name, properties=None) # Alias for track()
|
|
142
|
+
user.identify(identify_id) # Merge identity
|
|
143
|
+
user.wallet_address(address) # Set crypto wallet address
|
|
144
|
+
user.reset() # Clear session context
|
|
145
|
+
user.capture_error(exception) # Track server_error event
|
|
146
|
+
|
|
147
|
+
# People profile
|
|
148
|
+
user.people.set(properties)
|
|
149
|
+
user.people.set_once(properties)
|
|
150
|
+
user.people.unset(keys)
|
|
151
|
+
user.people.increment(properties)
|
|
152
|
+
user.people.append(properties)
|
|
153
|
+
user.people.union(properties)
|
|
154
|
+
user.people.remove(properties)
|
|
155
|
+
user.people.track_charge(amount, properties=None)
|
|
156
|
+
user.people.clear_charges()
|
|
157
|
+
user.people.delete_user()
|
|
158
|
+
|
|
159
|
+
# Groups
|
|
160
|
+
user.set_group(group_key, group_id)
|
|
161
|
+
user.add_group(group_key, group_id)
|
|
162
|
+
user.remove_group(group_key, group_id)
|
|
163
|
+
group = user.get_group(group_key, group_id)
|
|
164
|
+
group.set(properties)
|
|
165
|
+
group.set_once(properties)
|
|
166
|
+
group.union(properties)
|
|
167
|
+
group.remove(properties)
|
|
168
|
+
group.unset(keys)
|
|
169
|
+
group.increment(properties)
|
|
170
|
+
group.append(properties)
|
|
171
|
+
group.delete()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Direct Call Pattern (for pipelines/scripts)
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
trodo.track('user-123', 'event_name', {'key': 'value'})
|
|
178
|
+
trodo.identify('user-123', 'identify_id')
|
|
179
|
+
trodo.people_set('user-123', {'plan': 'pro'})
|
|
180
|
+
trodo.set_group('user-123', 'company', 'acme')
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Global Methods
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
trodo.enable_auto_events() # Enable sys.excepthook hooks
|
|
187
|
+
trodo.disable_auto_events() # Disable hooks
|
|
188
|
+
trodo.flush() # Flush pending batch queue
|
|
189
|
+
trodo.shutdown() # Flush + stop background timers
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Auto Events
|
|
193
|
+
|
|
194
|
+
When `auto_events=True`, the SDK wraps Python's exception hooks and sends `server_error` events to Trodo:
|
|
195
|
+
|
|
196
|
+
- `sys.excepthook` — unhandled exceptions in the main thread
|
|
197
|
+
- `threading.excepthook` — unhandled exceptions in threads
|
|
198
|
+
|
|
199
|
+
These events use `distinct_id: 'server_global'` in the dashboard.
|
|
200
|
+
|
|
201
|
+
Per-user error capture: `user.capture_error(e)` uses the user's own `distinct_id`.
|
|
202
|
+
|
|
203
|
+
## Batching
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
trodo.init(
|
|
207
|
+
site_id='your-site-id',
|
|
208
|
+
batch_enabled=True,
|
|
209
|
+
batch_size=100,
|
|
210
|
+
batch_flush_interval=3.0,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Events are queued and flushed every 3s or when 100 events accumulate
|
|
214
|
+
user.track('page_view')
|
|
215
|
+
|
|
216
|
+
# Always flush before process exit
|
|
217
|
+
import atexit
|
|
218
|
+
atexit.register(trodo.shutdown)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Thread Safety
|
|
222
|
+
|
|
223
|
+
The SDK is thread-safe. `SessionManager`, `EventQueue`, and `BatchFlusher` all use `threading.Lock` internally. Safe to use in multi-threaded Flask/Django/FastAPI applications.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
ISC
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# trodo-python
|
|
2
|
+
|
|
3
|
+
Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, and manage people/groups — all merging seamlessly with your frontend Trodo data under the same `site_id`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install trodo-python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For async support (optional):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install trodo-python[async]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.8+.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import trodo
|
|
23
|
+
|
|
24
|
+
# Initialize once at app startup
|
|
25
|
+
trodo.init(
|
|
26
|
+
site_id='your-site-id',
|
|
27
|
+
debug=False, # optional: log API calls
|
|
28
|
+
auto_events=True, # optional: hook sys.excepthook
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Get a user context
|
|
32
|
+
user = trodo.for_user('user-123')
|
|
33
|
+
|
|
34
|
+
# Track a custom event
|
|
35
|
+
user.track('purchase_completed', {'amount': 99.99, 'plan': 'pro'})
|
|
36
|
+
|
|
37
|
+
# Identify the user (merges with frontend events under the same identity)
|
|
38
|
+
user.identify('user@example.com') # distinct_id becomes id_user@example.com
|
|
39
|
+
|
|
40
|
+
# Update people profile
|
|
41
|
+
user.people.set({'plan': 'pro', 'company': 'Acme'})
|
|
42
|
+
|
|
43
|
+
# Track a server-side error
|
|
44
|
+
user.capture_error(Exception('payment failed'))
|
|
45
|
+
|
|
46
|
+
# Flush queued events before process exit
|
|
47
|
+
trodo.shutdown()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Flask / FastAPI Example
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# Flask
|
|
54
|
+
from flask import Flask, request
|
|
55
|
+
import trodo
|
|
56
|
+
|
|
57
|
+
app = Flask(__name__)
|
|
58
|
+
trodo.init(site_id='your-site-id')
|
|
59
|
+
|
|
60
|
+
@app.route('/purchase', methods=['POST'])
|
|
61
|
+
def purchase():
|
|
62
|
+
user_id = request.json['user_id']
|
|
63
|
+
user = trodo.for_user(user_id)
|
|
64
|
+
user.track('purchase_completed', {'amount': request.json['amount']})
|
|
65
|
+
return {'ok': True}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Cross-SDK Identity Merging
|
|
69
|
+
|
|
70
|
+
Frontend and backend events merge when both sides call `identify()` with the same value:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Python SDK
|
|
74
|
+
user.identify('user@example.com') # → id_user@example.com
|
|
75
|
+
|
|
76
|
+
# Browser SDK (same value)
|
|
77
|
+
# Trodo.identify('user@example.com') → id_user@example.com
|
|
78
|
+
|
|
79
|
+
# Both event streams now appear together in the Trodo dashboard
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### `trodo.init(config)`
|
|
85
|
+
|
|
86
|
+
| Parameter | Type | Default | Description |
|
|
87
|
+
|-----------|------|---------|-------------|
|
|
88
|
+
| `site_id` | `str` | required | Your Trodo site ID |
|
|
89
|
+
| `api_base` | `str` | `https://sdkapi.trodo.ai` | API base URL |
|
|
90
|
+
| `debug` | `bool` | `False` | Log API requests/responses |
|
|
91
|
+
| `auto_events` | `bool` | `False` | Hook `sys.excepthook` + `threading.excepthook` |
|
|
92
|
+
| `retries` | `int` | `3` | HTTP retry attempts on 5xx errors |
|
|
93
|
+
| `timeout` | `int` | `10` | HTTP timeout in seconds |
|
|
94
|
+
| `batch_enabled` | `bool` | `False` | Queue events and flush in bulk |
|
|
95
|
+
| `batch_size` | `int` | `50` | Max events per batch flush |
|
|
96
|
+
| `batch_flush_interval` | `float` | `5.0` | Flush interval in seconds |
|
|
97
|
+
| `on_error` | `callable` | `None` | Callback for SDK errors |
|
|
98
|
+
|
|
99
|
+
### `trodo.for_user(distinct_id, session_id=None)`
|
|
100
|
+
|
|
101
|
+
Returns a user-bound context. All subsequent calls use this user's session.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
user = trodo.for_user(
|
|
105
|
+
'user-123',
|
|
106
|
+
session_id=request.cookies.get('trodo_session'), # optional: correlate with browser session
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### User Context Methods
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
user.track(event_name, properties=None) # Track custom event
|
|
114
|
+
user.track_event(event_name, properties=None) # Alias for track()
|
|
115
|
+
user.identify(identify_id) # Merge identity
|
|
116
|
+
user.wallet_address(address) # Set crypto wallet address
|
|
117
|
+
user.reset() # Clear session context
|
|
118
|
+
user.capture_error(exception) # Track server_error event
|
|
119
|
+
|
|
120
|
+
# People profile
|
|
121
|
+
user.people.set(properties)
|
|
122
|
+
user.people.set_once(properties)
|
|
123
|
+
user.people.unset(keys)
|
|
124
|
+
user.people.increment(properties)
|
|
125
|
+
user.people.append(properties)
|
|
126
|
+
user.people.union(properties)
|
|
127
|
+
user.people.remove(properties)
|
|
128
|
+
user.people.track_charge(amount, properties=None)
|
|
129
|
+
user.people.clear_charges()
|
|
130
|
+
user.people.delete_user()
|
|
131
|
+
|
|
132
|
+
# Groups
|
|
133
|
+
user.set_group(group_key, group_id)
|
|
134
|
+
user.add_group(group_key, group_id)
|
|
135
|
+
user.remove_group(group_key, group_id)
|
|
136
|
+
group = user.get_group(group_key, group_id)
|
|
137
|
+
group.set(properties)
|
|
138
|
+
group.set_once(properties)
|
|
139
|
+
group.union(properties)
|
|
140
|
+
group.remove(properties)
|
|
141
|
+
group.unset(keys)
|
|
142
|
+
group.increment(properties)
|
|
143
|
+
group.append(properties)
|
|
144
|
+
group.delete()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Direct Call Pattern (for pipelines/scripts)
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
trodo.track('user-123', 'event_name', {'key': 'value'})
|
|
151
|
+
trodo.identify('user-123', 'identify_id')
|
|
152
|
+
trodo.people_set('user-123', {'plan': 'pro'})
|
|
153
|
+
trodo.set_group('user-123', 'company', 'acme')
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Global Methods
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
trodo.enable_auto_events() # Enable sys.excepthook hooks
|
|
160
|
+
trodo.disable_auto_events() # Disable hooks
|
|
161
|
+
trodo.flush() # Flush pending batch queue
|
|
162
|
+
trodo.shutdown() # Flush + stop background timers
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Auto Events
|
|
166
|
+
|
|
167
|
+
When `auto_events=True`, the SDK wraps Python's exception hooks and sends `server_error` events to Trodo:
|
|
168
|
+
|
|
169
|
+
- `sys.excepthook` — unhandled exceptions in the main thread
|
|
170
|
+
- `threading.excepthook` — unhandled exceptions in threads
|
|
171
|
+
|
|
172
|
+
These events use `distinct_id: 'server_global'` in the dashboard.
|
|
173
|
+
|
|
174
|
+
Per-user error capture: `user.capture_error(e)` uses the user's own `distinct_id`.
|
|
175
|
+
|
|
176
|
+
## Batching
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
trodo.init(
|
|
180
|
+
site_id='your-site-id',
|
|
181
|
+
batch_enabled=True,
|
|
182
|
+
batch_size=100,
|
|
183
|
+
batch_flush_interval=3.0,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Events are queued and flushed every 3s or when 100 events accumulate
|
|
187
|
+
user.track('page_view')
|
|
188
|
+
|
|
189
|
+
# Always flush before process exit
|
|
190
|
+
import atexit
|
|
191
|
+
atexit.register(trodo.shutdown)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Thread Safety
|
|
195
|
+
|
|
196
|
+
The SDK is thread-safe. `SessionManager`, `EventQueue`, and `BatchFlusher` all use `threading.Lock` internally. Safe to use in multi-threaded Flask/Django/FastAPI applications.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
ISC
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trodo-python"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Trodo Analytics SDK for Python — server-side event tracking"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "ISC" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.28.0",
|
|
14
|
+
]
|
|
15
|
+
keywords = ["analytics", "tracking", "trodo", "server-side"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"License :: OSI Approved :: ISC License (ISCL)",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
async = ["httpx>=0.27.0"]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0",
|
|
33
|
+
"pytest-cov",
|
|
34
|
+
"responses>=0.25.0",
|
|
35
|
+
"httpx>=0.27.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["trodo*"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trodo-python — Trodo Analytics SDK for Python
|
|
3
|
+
|
|
4
|
+
Usage (module-level singleton):
|
|
5
|
+
import trodo
|
|
6
|
+
trodo.init(site_id='your-site-id')
|
|
7
|
+
user = trodo.for_user('user-123')
|
|
8
|
+
user.track('purchase_completed', {'amount': 99.99})
|
|
9
|
+
|
|
10
|
+
Usage (class):
|
|
11
|
+
from trodo import TrodoClient
|
|
12
|
+
client = TrodoClient(site_id='your-site-id')
|
|
13
|
+
user = client.for_user('user-123')
|
|
14
|
+
user.track('purchase_completed', {'amount': 99.99})
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional, Union
|
|
20
|
+
|
|
21
|
+
from .client import TrodoClient
|
|
22
|
+
from .user_context import UserContext
|
|
23
|
+
from .managers.group_manager import GroupProfile
|
|
24
|
+
from .types import ApiResult, IdentifyResult, ResetResult, WalletAddressResult
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"TrodoClient",
|
|
28
|
+
"UserContext",
|
|
29
|
+
"GroupProfile",
|
|
30
|
+
"init",
|
|
31
|
+
"for_user",
|
|
32
|
+
"track",
|
|
33
|
+
"identify",
|
|
34
|
+
"wallet_address",
|
|
35
|
+
"reset",
|
|
36
|
+
"enable_auto_events",
|
|
37
|
+
"disable_auto_events",
|
|
38
|
+
"flush",
|
|
39
|
+
"shutdown",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# Singleton convenience API
|
|
44
|
+
# ============================================================================
|
|
45
|
+
|
|
46
|
+
_client: Optional[TrodoClient] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_client() -> TrodoClient:
|
|
50
|
+
if _client is None:
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"trodo-python: Call trodo.init(site_id=...) before using the SDK."
|
|
53
|
+
)
|
|
54
|
+
return _client
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def init(
|
|
58
|
+
site_id: str,
|
|
59
|
+
api_base: str = "https://sdkapi.trodo.ai",
|
|
60
|
+
timeout: int = 10,
|
|
61
|
+
retries: int = 2,
|
|
62
|
+
batch_enabled: bool = False,
|
|
63
|
+
batch_size: int = 50,
|
|
64
|
+
batch_flush_interval: float = 5.0,
|
|
65
|
+
auto_events: bool = False,
|
|
66
|
+
on_error: Optional[Any] = None,
|
|
67
|
+
debug: bool = False,
|
|
68
|
+
) -> TrodoClient:
|
|
69
|
+
"""Initialise the singleton SDK instance."""
|
|
70
|
+
global _client
|
|
71
|
+
_client = TrodoClient(
|
|
72
|
+
site_id=site_id,
|
|
73
|
+
api_base=api_base,
|
|
74
|
+
timeout=timeout,
|
|
75
|
+
retries=retries,
|
|
76
|
+
batch_enabled=batch_enabled,
|
|
77
|
+
batch_size=batch_size,
|
|
78
|
+
batch_flush_interval=batch_flush_interval,
|
|
79
|
+
auto_events=auto_events,
|
|
80
|
+
on_error=on_error,
|
|
81
|
+
debug=debug,
|
|
82
|
+
)
|
|
83
|
+
return _client
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def for_user(
|
|
87
|
+
distinct_id: str,
|
|
88
|
+
session_id: Optional[str] = None,
|
|
89
|
+
) -> UserContext:
|
|
90
|
+
"""Return a UserContext bound to the given distinctId."""
|
|
91
|
+
return _get_client().for_user(distinct_id, session_id)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def track(
|
|
95
|
+
distinct_id: str,
|
|
96
|
+
event_name: str,
|
|
97
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
98
|
+
category: str = "custom",
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Track an event for a user (direct-call pattern)."""
|
|
101
|
+
_get_client().track(distinct_id, event_name, properties, category)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def identify(distinct_id: str, identify_id: str) -> IdentifyResult:
|
|
105
|
+
"""Alias a user's distinctId to an external identifier."""
|
|
106
|
+
return _get_client().identify(distinct_id, identify_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def wallet_address(distinct_id: str, wallet_addr: str) -> WalletAddressResult:
|
|
110
|
+
"""Associate a wallet address with a user."""
|
|
111
|
+
return _get_client().wallet_address(distinct_id, wallet_addr)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset(distinct_id: str) -> ResetResult:
|
|
115
|
+
"""Reset a user's session."""
|
|
116
|
+
return _get_client().reset(distinct_id)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def enable_auto_events() -> None:
|
|
120
|
+
_get_client().enable_auto_events()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def disable_auto_events() -> None:
|
|
124
|
+
_get_client().disable_auto_events()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def flush() -> None:
|
|
128
|
+
"""Flush any queued batch events."""
|
|
129
|
+
_get_client().flush()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def shutdown() -> None:
|
|
133
|
+
"""Flush, stop timers, and disable auto events."""
|
|
134
|
+
_get_client().shutdown()
|
|
File without changes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Async HTTP client using httpx (optional dependency)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from ..types import ApiResult, EventPayload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncHttpClient:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_base: str,
|
|
16
|
+
site_id: str,
|
|
17
|
+
timeout: int = 10,
|
|
18
|
+
retries: int = 2,
|
|
19
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
20
|
+
debug: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
try:
|
|
23
|
+
import httpx # noqa: F401
|
|
24
|
+
except ImportError:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"trodo-python async support requires httpx: pip install trodo-python[async]"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self.api_base = api_base.rstrip("/")
|
|
30
|
+
self.site_id = site_id
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
self.retries = retries
|
|
33
|
+
self.on_error = on_error
|
|
34
|
+
self.debug = debug
|
|
35
|
+
|
|
36
|
+
def _log(self, *args: Any) -> None:
|
|
37
|
+
if self.debug:
|
|
38
|
+
sys.stderr.write(f"[trodo-python-async] {' '.join(str(a) for a in args)}\n")
|
|
39
|
+
|
|
40
|
+
async def _request(
|
|
41
|
+
self, path: str, body: Dict[str, Any], attempt: int = 0
|
|
42
|
+
) -> ApiResult:
|
|
43
|
+
import httpx
|
|
44
|
+
|
|
45
|
+
url = f"{self.api_base}{path}"
|
|
46
|
+
self._log(f"POST {url}")
|
|
47
|
+
headers = {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"X-Trodo-Site-Id": self.site_id,
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
53
|
+
resp = await client.post(url, json=body, headers=headers)
|
|
54
|
+
if resp.status_code >= 500 and attempt < self.retries:
|
|
55
|
+
delay = 2 ** attempt
|
|
56
|
+
self._log(f"Retry {attempt + 1} after {delay}s")
|
|
57
|
+
await asyncio.sleep(delay)
|
|
58
|
+
return await self._request(path, body, attempt + 1)
|
|
59
|
+
try:
|
|
60
|
+
return resp.json()
|
|
61
|
+
except Exception:
|
|
62
|
+
return {}
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
if attempt < self.retries:
|
|
65
|
+
await asyncio.sleep(2 ** attempt)
|
|
66
|
+
return await self._request(path, body, attempt + 1)
|
|
67
|
+
self._log(f"Error: {exc}")
|
|
68
|
+
if self.on_error:
|
|
69
|
+
self.on_error(exc)
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
async def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
|
|
73
|
+
return await self._request("/api/sdk/track", {"sessionData": session_data})
|
|
74
|
+
|
|
75
|
+
async def post_event(self, event: EventPayload) -> ApiResult:
|
|
76
|
+
return await self._request("/api/events", event.to_dict())
|
|
77
|
+
|
|
78
|
+
async def post_bulk_events(self, events: list) -> ApiResult:
|
|
79
|
+
return await self._request(
|
|
80
|
+
"/api/events/bulk", {"events": [e.to_dict() for e in events]}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def post_identify(self, payload: Dict[str, Any]) -> ApiResult:
|
|
84
|
+
return await self._request("/api/sdk/identify", payload)
|
|
85
|
+
|
|
86
|
+
async def post_wallet_address(self, payload: Dict[str, Any]) -> ApiResult:
|
|
87
|
+
return await self._request("/api/sdk/wallet-address", payload)
|
|
88
|
+
|
|
89
|
+
async def post_reset(self, payload: Dict[str, Any]) -> ApiResult:
|
|
90
|
+
return await self._request("/api/sdk/reset", payload)
|
|
91
|
+
|
|
92
|
+
async def post_people(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
93
|
+
return await self._request(path, payload)
|
|
94
|
+
|
|
95
|
+
async def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
96
|
+
return await self._request(path, payload)
|