trodo-python 2.23.3__py3-none-any.whl → 2.23.4__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 CHANGED
@@ -41,7 +41,7 @@ Downstream microservice (join the caller's run instead of making a new one):
41
41
 
42
42
  from __future__ import annotations
43
43
 
44
- __version__ = "2.21.0"
44
+ __version__ = "2.23.4"
45
45
 
46
46
  from typing import Any, Callable, Dict, List, Optional, Union
47
47
 
trodo/api/_retry.py ADDED
@@ -0,0 +1,30 @@
1
+ """When to retry a request, and after how long. Mirrors the Node SDK."""
2
+ import random
3
+ from typing import Optional
4
+
5
+
6
+ def retry_delay(status: int, retry_after: Optional[str], attempt: int) -> Optional[float]:
7
+ """Seconds to wait before retrying, or None when not to retry.
8
+
9
+ Retries a 5xx, and a 429 that says when (Retry-After): a rate limit is
10
+ temporary, an exhausted allowance (429 without Retry-After) is not. A
11
+ server that says Retry-After is honoured (capped at 30 s). Otherwise
12
+ exponential backoff with jitter, so a burst of clients hit by the same
13
+ outage does not retry in lockstep and knock it over again.
14
+ """
15
+ retryable = status >= 500 or (status == 429 and retry_after is not None)
16
+ if not retryable:
17
+ return None
18
+ try:
19
+ seconds = float(retry_after) if retry_after is not None else None
20
+ except ValueError:
21
+ seconds = None
22
+ if seconds is not None and seconds >= 0:
23
+ return min(seconds, 30.0) + random.random()
24
+ return jittered_backoff(attempt)
25
+
26
+
27
+ def jittered_backoff(attempt: int) -> float:
28
+ """2**attempt seconds, somewhere in its upper half."""
29
+ base = float(2 ** attempt)
30
+ return base / 2 + random.random() * (base / 2)
trodo/api/async_client.py CHANGED
@@ -7,6 +7,7 @@ import asyncio
7
7
  from typing import Any, Callable, Dict, Optional
8
8
 
9
9
  from ..types import ApiResult, EventPayload
10
+ from ._retry import jittered_backoff, retry_delay
10
11
 
11
12
 
12
13
  class AsyncHttpClient:
@@ -51,9 +52,12 @@ class AsyncHttpClient:
51
52
  try:
52
53
  async with httpx.AsyncClient(timeout=self.timeout) as client:
53
54
  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")
55
+ delay = (
56
+ retry_delay(resp.status_code, resp.headers.get('retry-after'), attempt)
57
+ if attempt < self.retries else None
58
+ )
59
+ if delay is not None:
60
+ self._log(f"Retry {attempt + 1} after {delay:.1f}s (status {resp.status_code})")
57
61
  await asyncio.sleep(delay)
58
62
  return await self._request(path, body, attempt + 1)
59
63
  try:
@@ -62,7 +66,7 @@ class AsyncHttpClient:
62
66
  return {}
63
67
  except Exception as exc:
64
68
  if attempt < self.retries:
65
- await asyncio.sleep(2 ** attempt)
69
+ await asyncio.sleep(jittered_backoff(attempt))
66
70
  return await self._request(path, body, attempt + 1)
67
71
  self._log(f"Error: {exc}")
68
72
  if self.on_error:
trodo/api/http_client.py CHANGED
@@ -10,6 +10,7 @@ import requests
10
10
 
11
11
  from ..types import ApiResult, EventPayload
12
12
  from . import endpoints
13
+ from ._retry import jittered_backoff, retry_delay
13
14
 
14
15
 
15
16
  class HttpClient:
@@ -43,9 +44,12 @@ class HttpClient:
43
44
  self._log(f"POST {url}")
44
45
  try:
45
46
  resp = self._session.post(url, json=body, timeout=self.timeout)
46
- if resp.status_code >= 500 and attempt < self.retries:
47
- delay = 2 ** attempt
48
- self._log(f"Retry {attempt + 1} after {delay}s (status {resp.status_code})")
47
+ delay = (
48
+ retry_delay(resp.status_code, resp.headers.get('retry-after'), attempt)
49
+ if attempt < self.retries else None
50
+ )
51
+ if delay is not None:
52
+ self._log(f"Retry {attempt + 1} after {delay:.1f}s (status {resp.status_code})")
49
53
  time.sleep(delay)
50
54
  return self._request(path, body, attempt + 1)
51
55
  if resp.status_code >= 400:
@@ -72,8 +76,8 @@ class HttpClient:
72
76
  return {}
73
77
  except Exception as exc:
74
78
  if attempt < self.retries:
75
- delay = 2 ** attempt
76
- self._log(f"Retry {attempt + 1} after {delay}s (network error)")
79
+ delay = jittered_backoff(attempt)
80
+ self._log(f"Retry {attempt + 1} after {delay:.1f}s (network error)")
77
81
  time.sleep(delay)
78
82
  return self._request(path, body, attempt + 1)
79
83
  self._log(f"Error: {exc}")
@@ -91,9 +95,12 @@ class HttpClient:
91
95
  self._log(f"GET {url}")
92
96
  try:
93
97
  resp = self._session.get(url, params=params or {}, timeout=self.timeout)
94
- if resp.status_code >= 500 and attempt < self.retries:
95
- delay = 2 ** attempt
96
- self._log(f"Retry {attempt + 1} after {delay}s (status {resp.status_code})")
98
+ delay = (
99
+ retry_delay(resp.status_code, resp.headers.get('retry-after'), attempt)
100
+ if attempt < self.retries else None
101
+ )
102
+ if delay is not None:
103
+ self._log(f"Retry {attempt + 1} after {delay:.1f}s (status {resp.status_code})")
97
104
  time.sleep(delay)
98
105
  return self._get(path, params, attempt + 1)
99
106
  try:
@@ -108,8 +115,8 @@ class HttpClient:
108
115
  return data
109
116
  except Exception as exc:
110
117
  if attempt < self.retries:
111
- delay = 2 ** attempt
112
- self._log(f"Retry {attempt + 1} after {delay}s (network error)")
118
+ delay = jittered_backoff(attempt)
119
+ self._log(f"Retry {attempt + 1} after {delay:.1f}s (network error)")
113
120
  time.sleep(delay)
114
121
  return self._get(path, params, attempt + 1)
115
122
  self._log(f"Error: {exc}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.23.3
3
+ Version: 2.23.4
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -1,11 +1,12 @@
1
- trodo/__init__.py,sha256=yv0gxvU4OpZhqck-U2YY0RZE7qRWDhD4ig4zenbMPI0,26928
1
+ trodo/__init__.py,sha256=SbEfXeTk-1B_rMcbO2kCU66Ock4p7pmiEBLQGttMaP4,26928
2
2
  trodo/client.py,sha256=9UYaHZtWPMYdlG2xWVf77PQJ9DfFgN-zobTh60i8_Cc,22453
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=uHCI2WYoOI3cNwdIUEuZdX4VYSu14pzm348ksn6hn_c,8195
5
5
  trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- trodo/api/async_client.py,sha256=rZN4aJ2QiKyrHBK260bApCUB9JaMWU6BQtzoSJZh7xk,3408
6
+ trodo/api/_retry.py,sha256=mHhuzfe3QuYa6j9BqRYVClvnYH0o3WkXXngmSwqWfgY,1214
7
+ trodo/api/async_client.py,sha256=qF97NbvoYbNhem8uXtPh14EG4JVAhEAzUCYn7busaaA,3609
7
8
  trodo/api/endpoints.py,sha256=H0Da456mf2IUNZH5bsln69IIUoUhdF4bncPo-WDLZ9I,1433
8
- trodo/api/http_client.py,sha256=RRsXVDqTp3H8OrOqM-7tsE3_WKHYCuRkf9icvyowVQE,9577
9
+ trodo/api/http_client.py,sha256=wzvxUpY2bR8BvebqbmqeHizVBmw41TmY3WQWVeAzABg,9881
9
10
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
11
12
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,7 +37,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
36
37
  trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
37
38
  trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
38
39
  trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
39
- trodo_python-2.23.3.dist-info/METADATA,sha256=Jim05wcQ2OduKmnK4NYi19Hp3fFf2FrD8E575KKzShc,25308
40
- trodo_python-2.23.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
41
- trodo_python-2.23.3.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
42
- trodo_python-2.23.3.dist-info/RECORD,,
40
+ trodo_python-2.23.4.dist-info/METADATA,sha256=VQP23ftatdvKhd55L_qv-XX60zq7OuTjVKI0c6Kw9dI,25308
41
+ trodo_python-2.23.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
42
+ trodo_python-2.23.4.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
43
+ trodo_python-2.23.4.dist-info/RECORD,,