trodo-python 3.0.0__py3-none-any.whl → 3.0.2__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/_version.py CHANGED
@@ -1 +1 @@
1
- SDK_VERSION = "3.0.0"
1
+ SDK_VERSION = "3.0.2"
trodo/delivery/sender.py CHANGED
@@ -14,12 +14,18 @@ and PostHog have converged on (1 Oct 2026 survey):
14
14
  the traces being debugged right now survive; drops are counted and
15
15
  reported to the server with the next batch.
16
16
  - retries while nobody waits: 429 pauses the whole sender until
17
- Retry-After (a client over its limit delivers late, not never); 5xx, 408
18
- and network errors back off (1 s → 60 s) for up to 10 minutes per item,
19
- then it is dropped as ``gave_up``; 413 splits the batch, a single
17
+ Retry-After (a client over its limit delivers late, not never). A rate
18
+ 429 never counts toward giving up (3.0.2): the server answered, it is
19
+ only pacing. 5xx, 408, network errors and an exhausted allowance back off
20
+ (1 s → 60 s) for up to 10 minutes per item, then it is dropped as
21
+ ``gave_up``. Once a 429 names the team's ceiling (units per minute), a
22
+ batch carries at most that many units, so each one is admissible; 413 splits the batch, a single
20
23
  oversized item is dropped as ``too_large``; any other 4xx is a bad item:
21
24
  dropped as ``rejected``, reported once.
22
- - exit: ``atexit`` flushes (bounded by ``exit_flush_s``).
25
+ - exit: ``atexit`` flushes (bounded by ``exit_flush_s``) and says on stderr
26
+ how many traces were still undelivered, and why, if any were.
27
+ - the server holds over-rate items itself (202 ``queued``) and stores them
28
+ at the team's rate; the sender says so on stderr once per 10 minutes.
23
29
  - ``immediate`` mode for platforms that freeze between requests (Lambda,
24
30
  Cloud Functions, Cloud Run, Vercel): the item is sent now, with short
25
31
  retries, on the caller's thread. ``auto`` picks it from the platform's
@@ -124,6 +130,9 @@ class Sender:
124
130
  self._stats = {"sent": 0, "dropped": 0, "dropped_by_reason": {}}
125
131
  self._unreported = {"count": 0, "by_reason": {}}
126
132
  self._warned_drop = False
133
+ self._unit_cap: Optional[int] = None # the team's ceiling, once a 429 names it
134
+ self._last_rate: Optional[Dict[str, Any]] = None
135
+ self._warned_paced_at = 0.0
127
136
  self._thread: Optional[threading.Thread] = None
128
137
  if self.mode == "background" and register_atexit:
129
138
  atexit.register(self._atexit)
@@ -174,11 +183,13 @@ class Sender:
174
183
  with self._lock:
175
184
  self._stopped = True
176
185
  left = len(self._queue)
186
+ spans_left = sum(_units_of(q.item) for q in self._queue)
177
187
  self._queue.clear()
178
188
  self._queue_bytes = 0
179
189
  self._wake.notify_all()
180
190
  if left:
181
191
  self._drop(left, "shutdown", f"{left} items were still queued at shutdown")
192
+ self._warn_undelivered(left, spans_left)
182
193
 
183
194
  def stats(self) -> Dict[str, Any]:
184
195
  with self._lock:
@@ -229,21 +240,24 @@ class Sender:
229
240
  def _take_batch_locked(self) -> List[_Queued]:
230
241
  out: List[_Queued] = []
231
242
  nbytes = 0
243
+ units = 0
244
+ cap = self._unit_cap
232
245
  while self._queue and len(out) < self._o["flush_at"]:
233
246
  nxt = self._queue[0]
234
247
  if out and nbytes + nxt.nbytes > self._o["max_batch_bytes"]:
235
248
  break
249
+ u = _units_of(nxt.item)
250
+ if out and cap and units + u > cap:
251
+ break
236
252
  out.append(self._queue.popleft())
237
253
  nbytes += nxt.nbytes
254
+ units += u
238
255
  self._queue_bytes -= nbytes
239
256
  return out
240
257
 
241
258
  def _send_batch(self, batch: List[_Queued]) -> None:
242
- t = self._now()
243
259
  for q in batch:
244
260
  q.attempts += 1
245
- if q.first_tried_at is None:
246
- q.first_tried_at = t
247
261
  try:
248
262
  status, retry_after, body = self._http.post_raw(
249
263
  endpoints.BATCH, self._envelope(batch), timeout=self._o["request_timeout_s"], gzip_body=True,
@@ -257,8 +271,9 @@ class Sender:
257
271
  self._settle(batch, body)
258
272
  return
259
273
  if status == 429:
274
+ paced = self._note_rate(body if isinstance(body, dict) else {}, retry_after)
260
275
  self._pause(parse_retry_after_s(retry_after, self._now()))
261
- self._requeue(batch, 0.0)
276
+ self._requeue(batch, 0.0, paced=paced)
262
277
  return
263
278
  if status == 413:
264
279
  self._split(batch)
@@ -273,16 +288,23 @@ class Sender:
273
288
 
274
289
  def _settle(self, batch: List[_Queued], body: Dict[str, Any]) -> None:
275
290
  keep: List[_Queued] = []
291
+ paced: List[_Queued] = []
276
292
  rejected = 0
293
+ queued = 0
277
294
  first_error = None
278
295
  results = body["results"]
279
296
  for i, q in enumerate(batch):
280
297
  r = results[i] if i < len(results) and isinstance(results[i], dict) else {"status": 500}
281
298
  st = int(r.get("status", 500))
282
299
  if 200 <= st < 300:
300
+ if r.get("queued"):
301
+ queued += 1
283
302
  with self._lock:
284
303
  self._stats["sent"] += 1
285
304
  continue
305
+ if st == 429 and self._note_rate(r, None):
306
+ paced.append(q)
307
+ continue
286
308
  if st == 429 or st >= 500 or st == 408:
287
309
  keep.append(q)
288
310
  continue
@@ -293,17 +315,60 @@ class Sender:
293
315
  first_error = first_error or f"HTTP {st}: {r.get('error', '')}"
294
316
  if rejected:
295
317
  self._drop(rejected, "rejected", f"{rejected} item(s) rejected by the server ({first_error})")
318
+ if queued:
319
+ self._warn_paced(body.get("paced") if isinstance(body.get("paced"), dict) else {})
296
320
  ra = body.get("retry_after_ms")
297
321
  if ra:
298
322
  self._pause(float(ra) / 1000.0)
323
+ if paced:
324
+ self._requeue(paced, 0.0, paced=True)
299
325
  if keep:
300
326
  self._requeue(keep, 0.0 if ra else None)
301
327
 
302
- def _requeue(self, batch: List[_Queued], wait_s: Optional[float]) -> None:
328
+ def _note_rate(self, body: Dict[str, Any], retry_after: Optional[str]) -> bool:
329
+ """Is this 429 the rate ceiling (pacing: retry, never give up) rather
330
+ than an exhausted allowance? Learns the team's ceiling from it."""
331
+ ceiling = body.get("ceiling_units_min")
332
+ rate = bool(retry_after) or body.get("retry_after_seconds") is not None or body.get("deferred") or ceiling is not None
333
+ if not rate:
334
+ return False
335
+ try:
336
+ c = int(ceiling) if ceiling is not None else 0
337
+ except (TypeError, ValueError):
338
+ c = 0
339
+ with self._lock:
340
+ if c > 0:
341
+ self._unit_cap = c
342
+ self._last_rate = {"ceiling": c or None, "reason": body.get("reason"), "message": body.get("message")}
343
+ return True
344
+
345
+ def _warn_paced(self, paced: Dict[str, Any]) -> None:
346
+ t = self._now()
347
+ with self._lock:
348
+ if t - self._warned_paced_at < 600:
349
+ return
350
+ self._warned_paced_at = t
351
+ c = paced.get("ceiling_units_min")
352
+ limit = f" ({c} units/min)" if c else ""
353
+ sys.stderr.write(
354
+ f"[trodo-python] This team is sending faster than its rate limit{limit}. Trodo is holding the extra traces "
355
+ "and storing them at that rate, so they will appear late. Contact Trodo for a higher limit.\n"
356
+ )
357
+
358
+ def _requeue(self, batch: List[_Queued], wait_s: Optional[float], paced: bool = False) -> None:
303
359
  t = self._now()
304
360
  alive: List[_Queued] = []
305
361
  for q in batch:
306
- if q.first_tried_at is not None and t - q.first_tried_at > self._o["max_retry_s"]:
362
+ if paced:
363
+ # The server answered and is only pacing: the give-up clock
364
+ # (for outages and refusals) starts over.
365
+ q.first_tried_at = None
366
+ q.attempts = 0
367
+ alive.append(q)
368
+ elif q.first_tried_at is None:
369
+ q.first_tried_at = t
370
+ alive.append(q)
371
+ elif t - q.first_tried_at > self._o["max_retry_s"]:
307
372
  self._drop(1, "gave_up", f"an item was retried for {int(self._o['max_retry_s'])} s without being accepted")
308
373
  else:
309
374
  alive.append(q)
@@ -314,7 +379,7 @@ class Sender:
314
379
  self._queue.appendleft(q)
315
380
  self._queue_bytes += sum(q.nbytes for q in alive)
316
381
  shed = self._shed_locked()
317
- attempts = max(q.attempts for q in alive)
382
+ attempts = max(1, max(q.attempts for q in alive))
318
383
  backoff = wait_s if wait_s is not None else min(_MAX_BACKOFF_S, _MIN_BACKOFF_S * 2 ** (attempts - 1)) * (0.5 + random.random() / 2)
319
384
  self._not_before = t + backoff
320
385
  if shed:
@@ -342,6 +407,7 @@ class Sender:
342
407
  one attempt, the status read — a refusal keeps the item like the batch
343
408
  path would, instead of the old blocking request that dropped it."""
344
409
  keep: List[_Queued] = []
410
+ paced: List[_Queued] = []
345
411
  wait: Optional[float] = None
346
412
  for q in batch:
347
413
  path, body = _legacy_route(q.item)
@@ -353,14 +419,18 @@ class Sender:
353
419
  with self._lock:
354
420
  self._stats["sent"] += 1
355
421
  continue
422
+ if status == 429 and self._note_rate(resp if isinstance(resp, dict) else {}, retry_after):
423
+ paced.append(q)
424
+ wait = parse_retry_after_s(retry_after, self._now()) or wait
425
+ continue
356
426
  if status is None or status in _TRANSIENT:
357
427
  keep.append(q)
358
- if status == 429:
359
- wait = parse_retry_after_s(retry_after, self._now()) or wait
360
428
  continue
361
429
  self._drop(1, "too_large" if status == 413 else "rejected", f"POST {path} -> HTTP {status}: {json.dumps(resp)[:200]}")
362
430
  if wait is not None:
363
431
  self._pause(wait)
432
+ if paced:
433
+ self._requeue(paced, 0.0, paced=True)
364
434
  if keep:
365
435
  self._requeue(keep, 0.0 if wait is not None else None)
366
436
 
@@ -472,6 +542,27 @@ class Sender:
472
542
  self.flush(self._o["exit_flush_s"])
473
543
  except Exception:
474
544
  pass
545
+ with self._lock:
546
+ left = len(self._queue) + self._in_flight
547
+ units = sum(_units_of(q.item) for q in self._queue)
548
+ if left:
549
+ self._warn_undelivered(left, units)
550
+
551
+ def _warn_undelivered(self, items: int, units: int) -> None:
552
+ """Always on stderr, whatever was said before: data is about to be lost."""
553
+ with self._lock:
554
+ rate = self._last_rate if self._paused_until and self._paused_until > self._now() else None
555
+ why = ""
556
+ if rate and rate.get("message"):
557
+ why = f" Trodo said: {rate['message']}"
558
+ elif rate:
559
+ c = rate.get("ceiling")
560
+ why = f" The team is over its rate limit{f' ({c} units/min)' if c else ''} and Trodo could not hold more."
561
+ sys.stderr.write(
562
+ f"[trodo-python] {items} trace item(s), {units} units, were NOT delivered before the process exited "
563
+ f"(waited {self._o['exit_flush_s']:g} s).{why} Call trodo.flush() before exiting (it waits until they are "
564
+ "delivered), or raise delivery_options exit_flush_s.\n"
565
+ )
475
566
 
476
567
  def _log(self, msg: str) -> None:
477
568
  if self._debug:
@@ -489,6 +580,14 @@ def _legacy_route(item: Item):
489
580
  return endpoints.SPANS_APPEND, {"spans": item["spans"]}
490
581
 
491
582
 
583
+ def _units_of(item: Any) -> int:
584
+ """What an item costs against the rate ceiling: one per span, one for the run."""
585
+ if not isinstance(item, dict):
586
+ return 0
587
+ spans = item.get("spans")
588
+ return (len(spans) if isinstance(spans, list) else 0) + (1 if isinstance(item.get("run"), dict) else 0)
589
+
590
+
492
591
  def _size_of(item: Any) -> int:
493
592
  try:
494
593
  return len(json.dumps(item, default=str).encode("utf-8"))
trodo/otel/processor.py CHANGED
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import threading
11
11
  from dataclasses import dataclass, asdict
12
+ import json
12
13
  from typing import Any, Dict, List, Optional, Union
13
14
 
14
15
  # How long end_run waits for the run's queued spans to send before sending the end.
@@ -97,6 +98,50 @@ class TrodoSpan:
97
98
  return {k: v for k, v in asdict(self).items() if v is not None}
98
99
 
99
100
 
101
+ # A run's spans travel in pieces of at most this many bytes (and at most
102
+ # max_spans_per_run spans). The sender drops any single item over its 4 MB
103
+ # request limit, so before this a run of a few hundred LLM spans with long
104
+ # prompts was dropped whole, run row included.
105
+ _CHUNK_BYTES = 1_000_000
106
+
107
+
108
+ def _size(d: Dict[str, Any]) -> int:
109
+ try:
110
+ return len(json.dumps(d, default=str))
111
+ except Exception:
112
+ return 0
113
+
114
+
115
+ def _pieces(dicts: List[Dict[str, Any]], limit: int = _CHUNK_BYTES) -> List[List[Dict[str, Any]]]:
116
+ """Split span dicts into consecutive pieces of at most ``limit`` bytes (one span always fits a piece)."""
117
+ out: List[List[Dict[str, Any]]] = []
118
+ cur: List[Dict[str, Any]] = []
119
+ size = 0
120
+ for d in dicts:
121
+ n = _size(d)
122
+ if cur and size + n > limit:
123
+ out.append(cur)
124
+ cur, size = [], 0
125
+ cur.append(d)
126
+ size += n
127
+ if cur:
128
+ out.append(cur)
129
+ return out
130
+
131
+
132
+ class _SentSpan:
133
+ """What the run's totals need of a span already sent ahead: no payloads."""
134
+
135
+ __slots__ = ("input_tokens", "output_tokens", "cost", "kind", "status")
136
+
137
+ def __init__(self, span: TrodoSpan) -> None:
138
+ self.input_tokens = span.input_tokens
139
+ self.output_tokens = span.output_tokens
140
+ self.cost = span.cost
141
+ self.kind = span.kind
142
+ self.status = span.status
143
+
144
+
100
145
  class TrodoSpanProcessor:
101
146
  """Buffers spans per run_id; flushes to /runs/ingest when the run ends.
102
147
 
@@ -118,6 +163,11 @@ class TrodoSpanProcessor:
118
163
  self._flush_interval = flush_interval_s
119
164
  self._max_spans = max_spans_per_run
120
165
  self._pending: Dict[str, List[TrodoSpan]] = {}
166
+ # A run past max_spans_per_run sends each full chunk ahead (the server
167
+ # stores spans before their run arrives) and keeps only what its totals
168
+ # need. Before this, every span after the 1,000th was dropped.
169
+ self._sent_ahead: Dict[str, List[_SentSpan]] = {}
170
+ self._bucket_bytes: Dict[str, int] = {}
121
171
  self._lock = threading.Lock()
122
172
  self._shutdown = False
123
173
  # run_ids that are "joined" (owned by a remote service) — their spans
@@ -133,22 +183,43 @@ class TrodoSpanProcessor:
133
183
  with self._lock:
134
184
  self._joined_runs.discard(run_id)
135
185
  self._pending.pop(run_id, None)
186
+ self._sent_ahead.pop(run_id, None)
187
+ self._bucket_bytes.pop(run_id, None)
136
188
 
137
189
  def enqueue_span(self, span: TrodoSpan) -> None:
138
190
  """Buffer a completed span under its run_id.
139
191
 
140
192
  Joined runs flush immediately (append_spans) instead of batching.
141
193
  """
194
+ chunk: List[TrodoSpan] = []
195
+ d = span.to_dict()
196
+ n = _size(d)
142
197
  with self._lock:
143
198
  joined = span.run_id in self._joined_runs
144
- if joined:
145
- pass
146
- else:
199
+ if not joined:
147
200
  bucket = self._pending.setdefault(span.run_id, [])
148
- if len(bucket) < self._max_spans:
149
- bucket.append(span)
201
+ held = self._bucket_bytes.get(span.run_id, 0)
202
+ # Full by bytes: what is held goes ahead and this span starts the next piece.
203
+ if bucket and held + n > _CHUNK_BYTES:
204
+ chunk = bucket
205
+ bucket = []
206
+ self._pending[span.run_id] = bucket
207
+ held = 0
208
+ bucket.append(span)
209
+ held += n
210
+ # Full by count: everything held goes ahead, this span included.
211
+ if len(bucket) >= self._max_spans:
212
+ chunk = chunk + bucket
213
+ self._pending[span.run_id] = []
214
+ held = 0
215
+ self._bucket_bytes[span.run_id] = held
216
+ if chunk:
217
+ self._sent_ahead.setdefault(span.run_id, []).extend(_SentSpan(x) for x in chunk)
150
218
  if joined:
151
- self._send_spans(span.run_id, [span.to_dict()])
219
+ self._send_spans(span.run_id, [d])
220
+ elif chunk:
221
+ for piece in _pieces([x.to_dict() for x in chunk]):
222
+ self._send_spans(span.run_id, piece)
152
223
 
153
224
  def _send_spans(self, run_id: str, spans: List[Dict[str, Any]]) -> None:
154
225
  if self._sender is not None:
@@ -161,15 +232,22 @@ class TrodoSpanProcessor:
161
232
 
162
233
  def get_pending(self, run_id: str) -> List[TrodoSpan]:
163
234
  with self._lock:
164
- return list(self._pending.get(run_id, []))
235
+ # The run's totals cover every span, including those sent ahead.
236
+ return [*self._sent_ahead.get(run_id, []), *self._pending.get(run_id, [])]
165
237
 
166
238
  def ingest_run(self, run: TrodoRun) -> None:
167
239
  """Flush a run + all its buffered spans in one call."""
168
240
  with self._lock:
169
241
  spans = self._pending.pop(run.run_id, [])
242
+ self._sent_ahead.pop(run.run_id, None)
243
+ self._bucket_bytes.pop(run.run_id, None)
170
244
  payload: Dict[str, Any] = {"run": run.to_dict()}
171
245
  if spans:
172
- payload["spans"] = [s.to_dict() for s in spans]
246
+ # All but the last piece go ahead; the run travels with the last.
247
+ pieces = _pieces([s.to_dict() for s in spans])
248
+ for piece in pieces[:-1]:
249
+ self._send_spans(run.run_id, piece)
250
+ payload["spans"] = pieces[-1]
173
251
  if self._sender is not None:
174
252
  # Queued: the agent's caller never waits on the network (2.24).
175
253
  self._sender.enqueue(payload)
@@ -201,8 +279,10 @@ class TrodoSpanProcessor:
201
279
  try:
202
280
  with self._lock:
203
281
  buffered = self._pending.pop(run_id, None) or []
204
- if buffered:
205
- self._send_spans(run_id, [s.to_dict() for s in buffered])
282
+ self._sent_ahead.pop(run_id, None)
283
+ self._bucket_bytes.pop(run_id, None)
284
+ for piece in _pieces([s.to_dict() for s in buffered]):
285
+ self._send_spans(run_id, piece)
206
286
  if self._sender is not None:
207
287
  self._sender.flush(END_RUN_FLUSH_S)
208
288
  except Exception:
@@ -216,17 +296,18 @@ class TrodoSpanProcessor:
216
296
  """Stream spans for a long-running or joined run without finalising."""
217
297
  if not spans:
218
298
  return
219
- self._send_spans(run_id, [s.to_dict() for s in spans])
299
+ for piece in _pieces([s.to_dict() for s in spans]):
300
+ self._send_spans(run_id, piece)
220
301
 
221
302
  def force_flush(self) -> None:
222
303
  """Flush any buffered spans that don't have a run finalise yet."""
223
304
  with self._lock:
224
305
  pending = list(self._pending.items())
225
306
  self._pending.clear()
307
+ self._bucket_bytes.clear()
226
308
  for run_id, spans in pending:
227
- if not spans:
228
- continue
229
- self._send_spans(run_id, [s.to_dict() for s in spans])
309
+ for piece in _pieces([s.to_dict() for s in spans]):
310
+ self._send_spans(run_id, piece)
230
311
 
231
312
  def shutdown(self) -> None:
232
313
  self._shutdown = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 3.0.0
3
+ Version: 3.0.2
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,5 +1,5 @@
1
1
  trodo/__init__.py,sha256=x7zE63FK8CFJ6ATUvsXxeTkvZUaLKXqAJr99I6nnyZc,27099
2
- trodo/_version.py,sha256=AsSlr9XWZaS6NVC4bCgB5xuBv1s5UrEYJlMsyJQfpMw,22
2
+ trodo/_version.py,sha256=GmU1Hng0hfRPQUBIrIvzRCIVKBsPLckerj1Erc-Dt1g,22
3
3
  trodo/client.py,sha256=eaZtHMtOI-VLgfoqM_KVWMuP_I5Ivs7VFnR0oU96Yqk,23743
4
4
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
5
5
  trodo/user_context.py,sha256=uHCI2WYoOI3cNwdIUEuZdX4VYSu14pzm348ksn6hn_c,8195
@@ -12,7 +12,7 @@ trodo/api/management.py,sha256=uwhr0ZI1pvguz5-itW1KcgRj0TXM2n4KDa-hGIaBdTA,4023
12
12
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
14
14
  trodo/delivery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- trodo/delivery/sender.py,sha256=urO7m6mRMnSEW7_cNAmJ4A6jtDNxuihZHOxCx1jQScI,21348
15
+ trodo/delivery/sender.py,sha256=kp9kNvS_nJZpRKGD05zXcytwuxCOYD8A27rNJySGqR0,26119
16
16
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  trodo/managers/dataset_manager.py,sha256=HTFszK-wKmwjW-aw7xJWr5EKvDpJ_om496MLDr3gelg,13920
18
18
  trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
@@ -23,7 +23,7 @@ trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
23
23
  trodo/otel/auto_instrument.py,sha256=M5YVpFoPqn9bQohDFM7yyADrVGJPqFBToJLnBO7nOSA,29029
24
24
  trodo/otel/context.py,sha256=Jd0aTc0Q-1dM5kXXinhZD8YtnpdKgvneNiODyboVGKY,1418
25
25
  trodo/otel/helpers.py,sha256=XOMWcgZHaq5SQbkFxDaXPE4CDFjn01xjJmJ1vIxvwpw,20730
26
- trodo/otel/processor.py,sha256=GiMQR3uKHq_sbqXU00MdyarStaBzrZLmPgU1YnlzZb4,8965
26
+ trodo/otel/processor.py,sha256=ezaesBaxgv4-Mx8aHyp3ur41rvHwnQnjCNlPTe8rhjU,12343
27
27
  trodo/otel/prompt_trace.py,sha256=-kywRBU0Q2gWntS9HU-P5i7Kc5U-IJU7UVLjkdC2jw4,13593
28
28
  trodo/otel/register.py,sha256=7X3AfFQ5YvHGOUmdq8J38LQGvOJFPD907yT3uAp90lI,9607
29
29
  trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
@@ -40,7 +40,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
40
40
  trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
41
41
  trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
42
42
  trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
43
- trodo_python-3.0.0.dist-info/METADATA,sha256=Uq0WsyxJPpE-A8CBpSrafzEvniEf2a__kDIEE9055Ls,25307
44
- trodo_python-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
45
- trodo_python-3.0.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
46
- trodo_python-3.0.0.dist-info/RECORD,,
43
+ trodo_python-3.0.2.dist-info/METADATA,sha256=o047XtyjWl3fOEhJOR12qlWtDqlBelbYvAsbY3wT4xc,25307
44
+ trodo_python-3.0.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
45
+ trodo_python-3.0.2.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
46
+ trodo_python-3.0.2.dist-info/RECORD,,