trodo-python 1.0.0__py3-none-any.whl → 1.2.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/__init__.py +177 -134
- trodo/api/async_client.py +96 -96
- trodo/api/endpoints.py +21 -20
- trodo/api/http_client.py +90 -87
- trodo/auto/auto_event_manager.py +134 -134
- trodo/client.py +318 -195
- trodo/managers/group_manager.py +106 -106
- trodo/managers/people_manager.py +77 -77
- trodo/queue/batch_flusher.py +52 -52
- trodo/queue/event_queue.py +32 -32
- trodo/session/server_session.py +74 -74
- trodo/session/session_manager.py +74 -74
- trodo/types.py +154 -79
- trodo/user_context.py +224 -224
- trodo_python-1.2.0.dist-info/METADATA +358 -0
- trodo_python-1.2.0.dist-info/RECORD +23 -0
- trodo_python-1.0.0.dist-info/METADATA +0 -227
- trodo_python-1.0.0.dist-info/RECORD +0 -23
- {trodo_python-1.0.0.dist-info → trodo_python-1.2.0.dist-info}/WHEEL +0 -0
- {trodo_python-1.0.0.dist-info → trodo_python-1.2.0.dist-info}/top_level.txt +0 -0
trodo/api/http_client.py
CHANGED
|
@@ -1,87 +1,90 @@
|
|
|
1
|
-
"""Synchronous HTTP client using requests."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
import time
|
|
7
|
-
from typing import Any, Callable, Dict, Optional
|
|
8
|
-
|
|
9
|
-
import requests
|
|
10
|
-
|
|
11
|
-
from ..types import ApiResult, EventPayload
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class HttpClient:
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
api_base: str,
|
|
18
|
-
site_id: str,
|
|
19
|
-
timeout: int = 10,
|
|
20
|
-
retries: int = 2,
|
|
21
|
-
on_error: Optional[Callable[[Exception], None]] = None,
|
|
22
|
-
debug: bool = False,
|
|
23
|
-
) -> None:
|
|
24
|
-
self.api_base = api_base.rstrip("/")
|
|
25
|
-
self.site_id = site_id
|
|
26
|
-
self.timeout = timeout
|
|
27
|
-
self.retries = retries
|
|
28
|
-
self.on_error = on_error
|
|
29
|
-
self.debug = debug
|
|
30
|
-
self._session = requests.Session()
|
|
31
|
-
self._session.headers.update({
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
"X-Trodo-Site-Id": self.site_id,
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
def _log(self, *args: Any) -> None:
|
|
37
|
-
if self.debug:
|
|
38
|
-
sys.stderr.write(f"[trodo-python] {' '.join(str(a) for a in args)}\n")
|
|
39
|
-
|
|
40
|
-
def _request(self, path: str, body: Dict[str, Any], attempt: int = 0) -> ApiResult:
|
|
41
|
-
url = f"{self.api_base}{path}"
|
|
42
|
-
self._log(f"POST {url}")
|
|
43
|
-
try:
|
|
44
|
-
resp = self._session.post(url, json=body, timeout=self.timeout)
|
|
45
|
-
if resp.status_code >= 500 and attempt < self.retries:
|
|
46
|
-
delay = 2 ** attempt
|
|
47
|
-
self._log(f"Retry {attempt + 1} after {delay}s (status {resp.status_code})")
|
|
48
|
-
time.sleep(delay)
|
|
49
|
-
return self._request(path, body, attempt + 1)
|
|
50
|
-
try:
|
|
51
|
-
return resp.json()
|
|
52
|
-
except Exception:
|
|
53
|
-
return {}
|
|
54
|
-
except Exception as exc:
|
|
55
|
-
if attempt < self.retries:
|
|
56
|
-
delay = 2 ** attempt
|
|
57
|
-
self._log(f"Retry {attempt + 1} after {delay}s (network error)")
|
|
58
|
-
time.sleep(delay)
|
|
59
|
-
return self._request(path, body, attempt + 1)
|
|
60
|
-
self._log(f"Error: {exc}")
|
|
61
|
-
if self.on_error:
|
|
62
|
-
self.on_error(exc)
|
|
63
|
-
return {}
|
|
64
|
-
|
|
65
|
-
def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
|
|
66
|
-
return self._request("/api/sdk/track", {"sessionData": session_data})
|
|
67
|
-
|
|
68
|
-
def post_event(self, event: EventPayload) -> ApiResult:
|
|
69
|
-
return self._request("/api/events", event.to_dict())
|
|
70
|
-
|
|
71
|
-
def post_bulk_events(self, events: list) -> ApiResult:
|
|
72
|
-
return self._request("/api/events/bulk", {"events": [e.to_dict() for e in events]})
|
|
73
|
-
|
|
74
|
-
def post_identify(self, payload: Dict[str, Any]) -> ApiResult:
|
|
75
|
-
return self._request("/api/sdk/identify", payload)
|
|
76
|
-
|
|
77
|
-
def post_wallet_address(self, payload: Dict[str, Any]) -> ApiResult:
|
|
78
|
-
return self._request("/api/sdk/wallet-address", payload)
|
|
79
|
-
|
|
80
|
-
def post_reset(self, payload: Dict[str, Any]) -> ApiResult:
|
|
81
|
-
return self._request("/api/sdk/reset", payload)
|
|
82
|
-
|
|
83
|
-
def post_people(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
84
|
-
return self._request(path, payload)
|
|
85
|
-
|
|
86
|
-
def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
87
|
-
return self._request(path, payload)
|
|
1
|
+
"""Synchronous HTTP client using requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from ..types import ApiResult, EventPayload
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HttpClient:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_base: str,
|
|
18
|
+
site_id: str,
|
|
19
|
+
timeout: int = 10,
|
|
20
|
+
retries: int = 2,
|
|
21
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
22
|
+
debug: bool = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.api_base = api_base.rstrip("/")
|
|
25
|
+
self.site_id = site_id
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
self.retries = retries
|
|
28
|
+
self.on_error = on_error
|
|
29
|
+
self.debug = debug
|
|
30
|
+
self._session = requests.Session()
|
|
31
|
+
self._session.headers.update({
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"X-Trodo-Site-Id": self.site_id,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
def _log(self, *args: Any) -> None:
|
|
37
|
+
if self.debug:
|
|
38
|
+
sys.stderr.write(f"[trodo-python] {' '.join(str(a) for a in args)}\n")
|
|
39
|
+
|
|
40
|
+
def _request(self, path: str, body: Dict[str, Any], attempt: int = 0) -> ApiResult:
|
|
41
|
+
url = f"{self.api_base}{path}"
|
|
42
|
+
self._log(f"POST {url}")
|
|
43
|
+
try:
|
|
44
|
+
resp = self._session.post(url, json=body, timeout=self.timeout)
|
|
45
|
+
if resp.status_code >= 500 and attempt < self.retries:
|
|
46
|
+
delay = 2 ** attempt
|
|
47
|
+
self._log(f"Retry {attempt + 1} after {delay}s (status {resp.status_code})")
|
|
48
|
+
time.sleep(delay)
|
|
49
|
+
return self._request(path, body, attempt + 1)
|
|
50
|
+
try:
|
|
51
|
+
return resp.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
return {}
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
if attempt < self.retries:
|
|
56
|
+
delay = 2 ** attempt
|
|
57
|
+
self._log(f"Retry {attempt + 1} after {delay}s (network error)")
|
|
58
|
+
time.sleep(delay)
|
|
59
|
+
return self._request(path, body, attempt + 1)
|
|
60
|
+
self._log(f"Error: {exc}")
|
|
61
|
+
if self.on_error:
|
|
62
|
+
self.on_error(exc)
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
|
|
66
|
+
return self._request("/api/sdk/track", {"sessionData": session_data})
|
|
67
|
+
|
|
68
|
+
def post_event(self, event: EventPayload) -> ApiResult:
|
|
69
|
+
return self._request("/api/events", event.to_dict())
|
|
70
|
+
|
|
71
|
+
def post_bulk_events(self, events: list) -> ApiResult:
|
|
72
|
+
return self._request("/api/events/bulk", {"events": [e.to_dict() for e in events]})
|
|
73
|
+
|
|
74
|
+
def post_identify(self, payload: Dict[str, Any]) -> ApiResult:
|
|
75
|
+
return self._request("/api/sdk/identify", payload)
|
|
76
|
+
|
|
77
|
+
def post_wallet_address(self, payload: Dict[str, Any]) -> ApiResult:
|
|
78
|
+
return self._request("/api/sdk/wallet-address", payload)
|
|
79
|
+
|
|
80
|
+
def post_reset(self, payload: Dict[str, Any]) -> ApiResult:
|
|
81
|
+
return self._request("/api/sdk/reset", payload)
|
|
82
|
+
|
|
83
|
+
def post_people(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
84
|
+
return self._request(path, payload)
|
|
85
|
+
|
|
86
|
+
def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
|
|
87
|
+
return self._request(path, payload)
|
|
88
|
+
|
|
89
|
+
def post_agent_event(self, payload: Dict[str, Any]) -> ApiResult:
|
|
90
|
+
return self._request("/api/sdk/track-agent", payload)
|
trodo/auto/auto_event_manager.py
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
"""Auto event manager — captures unhandled Python exceptions as server_error events."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
import threading
|
|
7
|
-
import traceback
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from typing import Any, Optional, TYPE_CHECKING
|
|
10
|
-
|
|
11
|
-
from ..types import EventPayload
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from ..session.session_manager import SessionManager
|
|
15
|
-
from ..api.http_client import HttpClient
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _now_iso() -> str:
|
|
19
|
-
return datetime.now(timezone.utc).isoformat()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AutoEventManager:
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
site_id: str,
|
|
26
|
-
http_client: "HttpClient",
|
|
27
|
-
session_manager: "SessionManager",
|
|
28
|
-
) -> None:
|
|
29
|
-
self._site_id = site_id
|
|
30
|
-
self._http = http_client
|
|
31
|
-
self._session_manager = session_manager
|
|
32
|
-
self._enabled = False
|
|
33
|
-
self._prev_excepthook: Any = None
|
|
34
|
-
self._prev_thread_excepthook: Any = None
|
|
35
|
-
|
|
36
|
-
def enable(self) -> None:
|
|
37
|
-
if self._enabled:
|
|
38
|
-
return
|
|
39
|
-
self._enabled = True
|
|
40
|
-
|
|
41
|
-
# Wrap sys.excepthook
|
|
42
|
-
self._prev_excepthook = sys.excepthook
|
|
43
|
-
|
|
44
|
-
def _excepthook(exc_type, exc_value, exc_tb):
|
|
45
|
-
self._track_error(
|
|
46
|
-
exc_type.__name__ if exc_type else "Exception",
|
|
47
|
-
str(exc_value),
|
|
48
|
-
"".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
49
|
-
)
|
|
50
|
-
if self._prev_excepthook:
|
|
51
|
-
self._prev_excepthook(exc_type, exc_value, exc_tb)
|
|
52
|
-
|
|
53
|
-
sys.excepthook = _excepthook
|
|
54
|
-
|
|
55
|
-
# Wrap threading.excepthook (Python 3.8+)
|
|
56
|
-
if hasattr(threading, "excepthook"):
|
|
57
|
-
self._prev_thread_excepthook = threading.excepthook
|
|
58
|
-
|
|
59
|
-
def _thread_excepthook(args):
|
|
60
|
-
self._track_error(
|
|
61
|
-
args.exc_type.__name__ if args.exc_type else "ThreadException",
|
|
62
|
-
str(args.exc_value),
|
|
63
|
-
"".join(
|
|
64
|
-
traceback.format_exception(
|
|
65
|
-
args.exc_type, args.exc_value, args.exc_tb
|
|
66
|
-
)
|
|
67
|
-
),
|
|
68
|
-
)
|
|
69
|
-
if self._prev_thread_excepthook:
|
|
70
|
-
self._prev_thread_excepthook(args)
|
|
71
|
-
|
|
72
|
-
threading.excepthook = _thread_excepthook
|
|
73
|
-
|
|
74
|
-
def disable(self) -> None:
|
|
75
|
-
if not self._enabled:
|
|
76
|
-
return
|
|
77
|
-
self._enabled = False
|
|
78
|
-
|
|
79
|
-
if self._prev_excepthook is not None:
|
|
80
|
-
sys.excepthook = self._prev_excepthook
|
|
81
|
-
self._prev_excepthook = None
|
|
82
|
-
|
|
83
|
-
if hasattr(threading, "excepthook") and self._prev_thread_excepthook is not None:
|
|
84
|
-
threading.excepthook = self._prev_thread_excepthook
|
|
85
|
-
self._prev_thread_excepthook = None
|
|
86
|
-
|
|
87
|
-
def is_enabled(self) -> bool:
|
|
88
|
-
return self._enabled
|
|
89
|
-
|
|
90
|
-
def track_error(
|
|
91
|
-
self,
|
|
92
|
-
error: Exception,
|
|
93
|
-
error_type: Optional[str] = None,
|
|
94
|
-
severity: str = "error",
|
|
95
|
-
distinct_id: str = "server_global",
|
|
96
|
-
) -> None:
|
|
97
|
-
self._track_error(
|
|
98
|
-
error_type or type(error).__name__,
|
|
99
|
-
str(error),
|
|
100
|
-
traceback.format_exc() or repr(error),
|
|
101
|
-
severity=severity,
|
|
102
|
-
distinct_id=distinct_id,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def _track_error(
|
|
106
|
-
self,
|
|
107
|
-
error_type: str,
|
|
108
|
-
message: str,
|
|
109
|
-
stack: str,
|
|
110
|
-
severity: str = "critical",
|
|
111
|
-
distinct_id: str = "server_global",
|
|
112
|
-
) -> None:
|
|
113
|
-
try:
|
|
114
|
-
session = self._session_manager.get_or_create(distinct_id, self._site_id)
|
|
115
|
-
self._session_manager.ensure_confirmed(session, self._http)
|
|
116
|
-
|
|
117
|
-
event = EventPayload(
|
|
118
|
-
event_type="auto",
|
|
119
|
-
event_name="server_error",
|
|
120
|
-
event_category="error",
|
|
121
|
-
session_id=session.session_id,
|
|
122
|
-
user_id=session.distinct_id,
|
|
123
|
-
custom_properties={
|
|
124
|
-
"error_type": error_type,
|
|
125
|
-
"message": message,
|
|
126
|
-
"stack": stack,
|
|
127
|
-
"runtime": "python",
|
|
128
|
-
"severity": severity,
|
|
129
|
-
"timestamp": _now_iso(),
|
|
130
|
-
},
|
|
131
|
-
)
|
|
132
|
-
self._http.post_event(event)
|
|
133
|
-
except Exception:
|
|
134
|
-
pass # Never let error tracking crash the process
|
|
1
|
+
"""Auto event manager — captures unhandled Python exceptions as server_error events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import traceback
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ..types import EventPayload
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..session.session_manager import SessionManager
|
|
15
|
+
from ..api.http_client import HttpClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _now_iso() -> str:
|
|
19
|
+
return datetime.now(timezone.utc).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AutoEventManager:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
site_id: str,
|
|
26
|
+
http_client: "HttpClient",
|
|
27
|
+
session_manager: "SessionManager",
|
|
28
|
+
) -> None:
|
|
29
|
+
self._site_id = site_id
|
|
30
|
+
self._http = http_client
|
|
31
|
+
self._session_manager = session_manager
|
|
32
|
+
self._enabled = False
|
|
33
|
+
self._prev_excepthook: Any = None
|
|
34
|
+
self._prev_thread_excepthook: Any = None
|
|
35
|
+
|
|
36
|
+
def enable(self) -> None:
|
|
37
|
+
if self._enabled:
|
|
38
|
+
return
|
|
39
|
+
self._enabled = True
|
|
40
|
+
|
|
41
|
+
# Wrap sys.excepthook
|
|
42
|
+
self._prev_excepthook = sys.excepthook
|
|
43
|
+
|
|
44
|
+
def _excepthook(exc_type, exc_value, exc_tb):
|
|
45
|
+
self._track_error(
|
|
46
|
+
exc_type.__name__ if exc_type else "Exception",
|
|
47
|
+
str(exc_value),
|
|
48
|
+
"".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
49
|
+
)
|
|
50
|
+
if self._prev_excepthook:
|
|
51
|
+
self._prev_excepthook(exc_type, exc_value, exc_tb)
|
|
52
|
+
|
|
53
|
+
sys.excepthook = _excepthook
|
|
54
|
+
|
|
55
|
+
# Wrap threading.excepthook (Python 3.8+)
|
|
56
|
+
if hasattr(threading, "excepthook"):
|
|
57
|
+
self._prev_thread_excepthook = threading.excepthook
|
|
58
|
+
|
|
59
|
+
def _thread_excepthook(args):
|
|
60
|
+
self._track_error(
|
|
61
|
+
args.exc_type.__name__ if args.exc_type else "ThreadException",
|
|
62
|
+
str(args.exc_value),
|
|
63
|
+
"".join(
|
|
64
|
+
traceback.format_exception(
|
|
65
|
+
args.exc_type, args.exc_value, args.exc_tb
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
if self._prev_thread_excepthook:
|
|
70
|
+
self._prev_thread_excepthook(args)
|
|
71
|
+
|
|
72
|
+
threading.excepthook = _thread_excepthook
|
|
73
|
+
|
|
74
|
+
def disable(self) -> None:
|
|
75
|
+
if not self._enabled:
|
|
76
|
+
return
|
|
77
|
+
self._enabled = False
|
|
78
|
+
|
|
79
|
+
if self._prev_excepthook is not None:
|
|
80
|
+
sys.excepthook = self._prev_excepthook
|
|
81
|
+
self._prev_excepthook = None
|
|
82
|
+
|
|
83
|
+
if hasattr(threading, "excepthook") and self._prev_thread_excepthook is not None:
|
|
84
|
+
threading.excepthook = self._prev_thread_excepthook
|
|
85
|
+
self._prev_thread_excepthook = None
|
|
86
|
+
|
|
87
|
+
def is_enabled(self) -> bool:
|
|
88
|
+
return self._enabled
|
|
89
|
+
|
|
90
|
+
def track_error(
|
|
91
|
+
self,
|
|
92
|
+
error: Exception,
|
|
93
|
+
error_type: Optional[str] = None,
|
|
94
|
+
severity: str = "error",
|
|
95
|
+
distinct_id: str = "server_global",
|
|
96
|
+
) -> None:
|
|
97
|
+
self._track_error(
|
|
98
|
+
error_type or type(error).__name__,
|
|
99
|
+
str(error),
|
|
100
|
+
traceback.format_exc() or repr(error),
|
|
101
|
+
severity=severity,
|
|
102
|
+
distinct_id=distinct_id,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _track_error(
|
|
106
|
+
self,
|
|
107
|
+
error_type: str,
|
|
108
|
+
message: str,
|
|
109
|
+
stack: str,
|
|
110
|
+
severity: str = "critical",
|
|
111
|
+
distinct_id: str = "server_global",
|
|
112
|
+
) -> None:
|
|
113
|
+
try:
|
|
114
|
+
session = self._session_manager.get_or_create(distinct_id, self._site_id)
|
|
115
|
+
self._session_manager.ensure_confirmed(session, self._http)
|
|
116
|
+
|
|
117
|
+
event = EventPayload(
|
|
118
|
+
event_type="auto",
|
|
119
|
+
event_name="server_error",
|
|
120
|
+
event_category="error",
|
|
121
|
+
session_id=session.session_id,
|
|
122
|
+
user_id=session.distinct_id,
|
|
123
|
+
custom_properties={
|
|
124
|
+
"error_type": error_type,
|
|
125
|
+
"message": message,
|
|
126
|
+
"stack": stack,
|
|
127
|
+
"runtime": "python",
|
|
128
|
+
"severity": severity,
|
|
129
|
+
"timestamp": _now_iso(),
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
self._http.post_event(event)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass # Never let error tracking crash the process
|