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/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)
@@ -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