trodo-python 1.0.0__py3-none-any.whl

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/user_context.py ADDED
@@ -0,0 +1,224 @@
1
+ """UserContext — user-bound proxy for all Trodo SDK operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import traceback
6
+ from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING
7
+
8
+ from .managers.people_manager import PeopleManager
9
+ from .managers.group_manager import GroupManager, GroupProfile
10
+ from .types import (
11
+ ApiResult,
12
+ IdentifyResult,
13
+ ResetResult,
14
+ WalletAddressResult,
15
+ EventPayload,
16
+ )
17
+ from .session.server_session import now_iso
18
+
19
+ if TYPE_CHECKING:
20
+ from .api.http_client import HttpClient
21
+ from .session.session_manager import SessionManager
22
+ from .queue.event_queue import EventQueue
23
+ from .queue.batch_flusher import BatchFlusher
24
+ from .auto.auto_event_manager import AutoEventManager
25
+ from .types import ServerSession
26
+
27
+
28
+ class UserContext:
29
+ def __init__(
30
+ self,
31
+ distinct_id: str,
32
+ site_id: str,
33
+ http_client: "HttpClient",
34
+ session_manager: "SessionManager",
35
+ event_queue: Optional["EventQueue"],
36
+ batch_flusher: Optional["BatchFlusher"],
37
+ auto_event_manager: "AutoEventManager",
38
+ session_id: Optional[str] = None,
39
+ ) -> None:
40
+ self._distinct_id = distinct_id
41
+ self._site_id = site_id
42
+ self._http = http_client
43
+ self._session_manager = session_manager
44
+ self._event_queue = event_queue
45
+ self._batch_flusher = batch_flusher
46
+ self._auto_event_manager = auto_event_manager
47
+ self._session_id_override = session_id
48
+ self._session: Optional["ServerSession"] = None
49
+
50
+ # Eagerly initialise session
51
+ self._session = self._session_manager.get_or_create(
52
+ distinct_id, site_id, session_id
53
+ )
54
+
55
+ self.people = PeopleManager(
56
+ http_client, site_id, lambda: self._get_distinct_id()
57
+ )
58
+ self._group_manager = GroupManager(
59
+ http_client, site_id, lambda: self._get_distinct_id()
60
+ )
61
+
62
+ def _get_distinct_id(self) -> str:
63
+ if self._session:
64
+ return self._session.distinct_id
65
+ return self._distinct_id
66
+
67
+ def _get_session(self) -> "ServerSession":
68
+ if not self._session:
69
+ self._session = self._session_manager.get_or_create(
70
+ self._distinct_id, self._site_id, self._session_id_override
71
+ )
72
+ return self._session
73
+
74
+ def _send_event(
75
+ self,
76
+ event_name: str,
77
+ properties: Optional[Dict[str, Any]] = None,
78
+ category: str = "custom",
79
+ ) -> None:
80
+ session = self._get_session()
81
+ self._session_manager.ensure_confirmed(session, self._http)
82
+
83
+ event = EventPayload(
84
+ event_type="custom",
85
+ event_name=event_name,
86
+ event_category=category,
87
+ session_id=session.session_id,
88
+ user_id=session.distinct_id,
89
+ custom_properties=properties or {},
90
+ )
91
+
92
+ if self._event_queue and self._batch_flusher:
93
+ should_flush = self._event_queue.enqueue(event)
94
+ if should_flush:
95
+ self._batch_flusher.flush()
96
+ else:
97
+ self._http.post_event(event)
98
+
99
+ # --------------------------------------------------------------------------
100
+ # Event Tracking
101
+ # --------------------------------------------------------------------------
102
+
103
+ def track(
104
+ self,
105
+ event_name: str,
106
+ properties: Optional[Dict[str, Any]] = None,
107
+ category: str = "custom",
108
+ ) -> None:
109
+ self._send_event(event_name, properties, category)
110
+
111
+ def track_event(
112
+ self,
113
+ event_name: str,
114
+ properties: Optional[Dict[str, Any]] = None,
115
+ category: str = "custom",
116
+ ) -> None:
117
+ """Alias for track() — matches frontend SDK naming."""
118
+ self._send_event(event_name, properties, category)
119
+
120
+ # --------------------------------------------------------------------------
121
+ # Identity
122
+ # --------------------------------------------------------------------------
123
+
124
+ def identify(self, identify_id: str) -> IdentifyResult:
125
+ session = self._get_session()
126
+ self._session_manager.ensure_confirmed(session, self._http)
127
+
128
+ result = self._http.post_identify({
129
+ "siteId": self._site_id,
130
+ "sessionId": session.session_id,
131
+ "userId": session.distinct_id,
132
+ "distinctId": session.distinct_id,
133
+ "identifyId": identify_id,
134
+ })
135
+
136
+ new_distinct_id = result.get("newDistinctId") or result.get("distinctId")
137
+ if new_distinct_id and new_distinct_id != session.distinct_id:
138
+ session.distinct_id = new_distinct_id
139
+
140
+ return result
141
+
142
+ def wallet_address(self, wallet_addr: str) -> WalletAddressResult:
143
+ session = self._get_session()
144
+ self._session_manager.ensure_confirmed(session, self._http)
145
+
146
+ return self._http.post_wallet_address({
147
+ "siteId": self._site_id,
148
+ "sessionId": session.session_id,
149
+ "userId": session.distinct_id,
150
+ "distinctId": session.distinct_id,
151
+ "walletAddress": wallet_addr,
152
+ })
153
+
154
+ def reset(self) -> ResetResult:
155
+ session = self._get_session()
156
+ result = self._http.post_reset({
157
+ "siteId": self._site_id,
158
+ "sessionId": session.session_id,
159
+ "userId": session.distinct_id,
160
+ "distinctId": session.distinct_id,
161
+ })
162
+ self._session_manager.invalidate(self._distinct_id)
163
+ self._session = None
164
+ return result
165
+
166
+ # --------------------------------------------------------------------------
167
+ # People — accessible via user.people.set(...) etc.
168
+ # --------------------------------------------------------------------------
169
+
170
+ # --------------------------------------------------------------------------
171
+ # Groups
172
+ # --------------------------------------------------------------------------
173
+
174
+ def set_group(self, group_key: str, group_id: Union[str, List[str]]) -> ApiResult:
175
+ self._get_session()
176
+ return self._group_manager.set_group(group_key, group_id)
177
+
178
+ def add_group(self, group_key: str, group_id: str) -> ApiResult:
179
+ self._get_session()
180
+ return self._group_manager.add_group(group_key, group_id)
181
+
182
+ def remove_group(self, group_key: str, group_id: str) -> ApiResult:
183
+ self._get_session()
184
+ return self._group_manager.remove_group(group_key, group_id)
185
+
186
+ def get_group(self, group_key: str, group_id: str) -> GroupProfile:
187
+ return self._group_manager.get_group(group_key, group_id)
188
+
189
+ # --------------------------------------------------------------------------
190
+ # Error capture
191
+ # --------------------------------------------------------------------------
192
+
193
+ def capture_error(
194
+ self,
195
+ error: Exception,
196
+ severity: str = "error",
197
+ ) -> None:
198
+ session = self._get_session()
199
+ self._auto_event_manager.track_error(
200
+ error,
201
+ error_type=type(error).__name__,
202
+ severity=severity,
203
+ distinct_id=session.distinct_id,
204
+ )
205
+
206
+ # --------------------------------------------------------------------------
207
+ # Auto events (per-context toggle)
208
+ # --------------------------------------------------------------------------
209
+
210
+ def enable_auto_events(self) -> None:
211
+ self._auto_event_manager.enable()
212
+
213
+ def disable_auto_events(self) -> None:
214
+ self._auto_event_manager.disable()
215
+
216
+ # --------------------------------------------------------------------------
217
+ # Session info
218
+ # --------------------------------------------------------------------------
219
+
220
+ def get_session_id(self) -> Optional[str]:
221
+ return self._session.session_id if self._session else None
222
+
223
+ def get_current_distinct_id(self) -> str:
224
+ return self._get_distinct_id()
@@ -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,23 @@
1
+ trodo/__init__.py,sha256=YF8iWrzi3W3I44Q6eQ5GVSZLwJAKifiDsRuM5jbUp4s,3633
2
+ trodo/client.py,sha256=YjLAb-CAs-lYJ_JdD1WdjvByWheDMonUrr6Z5wL1SJc,7251
3
+ trodo/types.py,sha256=OOVdmihZMu9f2zxcSJKzUWXc4-a7gKT4txBUn0K9CGo,2517
4
+ trodo/user_context.py,sha256=K_6Wwb3J9jyKfV60fZKhAUd6HS_Ia-pb92eSVtNjGeY,8083
5
+ trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ trodo/api/async_client.py,sha256=-DEB0ZGtsf3eybe6MDA9RyNd2A1J1sNswDWj9GeyCdw,3504
7
+ trodo/api/endpoints.py,sha256=SrAQfPfm3dq4W8it3g0GYiHUcESwpmpp-sH0R7SQe7I,818
8
+ trodo/api/http_client.py,sha256=0Ij51cHAVMc708ai0MlKJl5VFqnBIJ3EdbsNMogYg68,3206
9
+ trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ trodo/auto/auto_event_manager.py,sha256=uuSUPMfix7Mw9Tqmdgk682tGSmYwlEptmm0yk6BGyi0,4381
11
+ trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ trodo/managers/group_manager.py,sha256=dQGVLFkQVYieFRIU91g8ew93D2eVNMLu4tFn867_6cA,3648
13
+ trodo/managers/people_manager.py,sha256=EsjruZRd-l9zOZylU0m9PPbaPlofkMk2hYaRKjLwElA,2959
14
+ trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ trodo/queue/batch_flusher.py,sha256=cTVhVtiil2G_V9-eF9kHuYw7rrjkCwDOhecTFCHTfSQ,1350
16
+ trodo/queue/event_queue.py,sha256=MOf993yFfOAEUKZ2l294T9_mxRMLvu4cd9PBb6jl-J8,904
17
+ trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ trodo/session/server_session.py,sha256=CE9bxJ6vCoqMs23BQpb8Kaw1K0f4eoa2L4aeuoozvpU,2044
19
+ trodo/session/session_manager.py,sha256=JV1sZfCdA8BaGw30-DZyTJtMj93FFzGiuhInkboTCh8,2599
20
+ trodo_python-1.0.0.dist-info/METADATA,sha256=qtLcOqaQkHXPjHWUs2wUU5QtCypoJLKClM9xdUgWWQo,6847
21
+ trodo_python-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ trodo_python-1.0.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
23
+ trodo_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ trodo