trodo-python 2.10.2__tar.gz → 2.10.4__tar.gz

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.
Files changed (47) hide show
  1. {trodo_python-2.10.2 → trodo_python-2.10.4}/PKG-INFO +1 -1
  2. {trodo_python-2.10.2 → trodo_python-2.10.4}/pyproject.toml +1 -1
  3. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/__init__.py +1 -1
  4. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/client.py +9 -1
  5. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/auto_instrument.py +22 -2
  6. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/register.py +44 -2
  7. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/wrap_agent.py +95 -4
  8. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/PKG-INFO +1 -1
  9. {trodo_python-2.10.2 → trodo_python-2.10.4}/README.md +0 -0
  10. {trodo_python-2.10.2 → trodo_python-2.10.4}/setup.cfg +0 -0
  11. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_anon_distinct_id 2.py +0 -0
  12. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_anon_distinct_id.py +0 -0
  13. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_auto_instrument_fixes.py +0 -0
  14. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_cross_process_session.py +0 -0
  15. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_end_run.py +0 -0
  16. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_error_enrichment.py +0 -0
  17. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_llm_usage_cost.py +0 -0
  18. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_processor_methods.py +0 -0
  19. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_register_otel.py +0 -0
  20. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_start_run.py +0 -0
  21. {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_wrap_agent_unchanged.py +0 -0
  22. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/__init__.py +0 -0
  23. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/async_client.py +0 -0
  24. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/endpoints.py +0 -0
  25. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/http_client.py +0 -0
  26. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/auto/__init__.py +0 -0
  27. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/auto/auto_event_manager.py +0 -0
  28. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/__init__.py +0 -0
  29. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/group_manager.py +0 -0
  30. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/people_manager.py +0 -0
  31. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/__init__.py +0 -0
  32. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/context.py +0 -0
  33. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/helpers.py +0 -0
  34. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/processor.py +0 -0
  35. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/transport.py +0 -0
  36. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/__init__.py +0 -0
  37. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/batch_flusher.py +0 -0
  38. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/event_queue.py +0 -0
  39. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/__init__.py +0 -0
  40. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/server_session.py +0 -0
  41. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/session_manager.py +0 -0
  42. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/types.py +0 -0
  43. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/user_context.py +0 -0
  44. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/SOURCES.txt +0 -0
  45. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/dependency_links.txt +0 -0
  46. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/requires.txt +0 -0
  47. {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.2
3
+ Version: 2.10.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trodo-python"
7
- version = "2.10.2"
7
+ version = "2.10.4"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- __version__ = "2.10.2"
43
+ __version__ = "2.10.4"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -23,7 +23,11 @@ from .otel.wrap_agent import (
23
23
  current_span_id as _current_span_id,
24
24
  )
25
25
  from .otel.auto_instrument import enable_auto_instrument
26
- from .otel.register import register_otel as _register_otel
26
+ from .otel.register import (
27
+ register_otel as _register_otel,
28
+ flush_otlp_pipeline as _flush_otlp_pipeline,
29
+ shutdown_otlp_pipeline as _shutdown_otlp_pipeline,
30
+ )
27
31
  from .otel.helpers import (
28
32
  tool as tool_decorator,
29
33
  track_llm_call as track_llm_call_fn,
@@ -233,6 +237,9 @@ class TrodoClient:
233
237
  if self._batch_flusher:
234
238
  self._batch_flusher.flush()
235
239
  self._span_processor.force_flush()
240
+ # 'otlp' mode: drain the OTel BatchSpanProcessor too — it batches for
241
+ # up to 5s, so without this a short-lived process drops its spans.
242
+ _flush_otlp_pipeline()
236
243
 
237
244
  def shutdown(self) -> None:
238
245
  self._auto_event_manager.disable()
@@ -240,6 +247,7 @@ class TrodoClient:
240
247
  self._batch_flusher.stop()
241
248
  self._batch_flusher.flush()
242
249
  self._span_processor.shutdown()
250
+ _shutdown_otlp_pipeline()
243
251
 
244
252
  # --------------------------------------------------------------------------
245
253
  # Agent Runs — Lemma-style seamless wrapping
@@ -9,12 +9,25 @@ lazily and skip anything that isn't installed.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import sys
12
13
  from datetime import datetime, timezone
13
14
  from typing import Any, Callable, Dict, Iterable, List, Optional
14
15
 
15
16
  from .context import get_active_context
16
17
  from .processor import TrodoSpan, TrodoSpanProcessor
17
18
 
19
+ # Always-on, one-shot warnings. Silent skips (missing opentelemetry-sdk) are the
20
+ # #1 cause of "I installed Trodo but see no LLM spans" — surface them even
21
+ # without debug, matching the Node SDK's warnOnce behaviour.
22
+ _WARNED_ONCE: set = set()
23
+
24
+
25
+ def _warn_once(key: str, msg: str) -> None:
26
+ if key in _WARNED_ONCE:
27
+ return
28
+ _WARNED_ONCE.add(key)
29
+ sys.stderr.write(f"[trodo-python] {msg}\n")
30
+
18
31
 
19
32
  def _hr_to_iso(nanos: Optional[int]) -> Optional[str]:
20
33
  if nanos is None:
@@ -341,14 +354,21 @@ def enable_auto_instrument(
341
354
  """Register OTel auto-instrumentations for installed packages.
342
355
 
343
356
  Returns the list of framework ids that were actually instrumented.
344
- Skipped silently if opentelemetry-sdk or the per-framework
345
- instrumentation package isn't installed.
357
+ Emits a one-shot stderr warning if opentelemetry-sdk isn't installed (the
358
+ #1 silent-failure mode); per-framework instrumentation packages that aren't
359
+ installed are skipped silently.
346
360
  """
347
361
  disabled = set(disable or [])
348
362
  try:
349
363
  from opentelemetry import trace # type: ignore
350
364
  from opentelemetry.sdk.trace import TracerProvider # type: ignore
351
365
  except Exception:
366
+ _warn_once(
367
+ "otel-sdk-missing",
368
+ "auto-instrument: opentelemetry-sdk is not installed — no LLM spans "
369
+ "will be auto-captured. Install: pip install opentelemetry-sdk "
370
+ "opentelemetry-api (plus opentelemetry-instrumentation-<provider> per framework).",
371
+ )
352
372
  return []
353
373
 
354
374
  provider = trace.get_tracer_provider()
@@ -36,6 +36,33 @@ class RegisterOtelResult:
36
36
 
37
37
  _registered: Optional[RegisterOtelResult] = None
38
38
 
39
+ # Flush/shutdown handle for the 'otlp'-mode pipeline. Without this, the
40
+ # BatchSpanProcessor created in _setup_otlp_mode (default 5s schedule) is
41
+ # unreachable — trodo.flush()/shutdown() only drained the events batcher and
42
+ # TrodoSpanProcessor, so short-lived processes silently dropped every
43
+ # OTLP-exported span. TrodoClient.flush()/shutdown() call these.
44
+ _otlp_processor: Optional[Any] = None
45
+
46
+
47
+ def flush_otlp_pipeline() -> None:
48
+ """Force-flush the 'otlp'-mode export pipeline. No-op in 'trodo' mode."""
49
+ if _otlp_processor is not None:
50
+ try:
51
+ _otlp_processor.force_flush()
52
+ except Exception:
53
+ pass
54
+
55
+
56
+ def shutdown_otlp_pipeline() -> None:
57
+ """Shut down the 'otlp'-mode export pipeline (flushes first). No-op in 'trodo' mode."""
58
+ global _otlp_processor
59
+ if _otlp_processor is not None:
60
+ proc, _otlp_processor = _otlp_processor, None
61
+ try:
62
+ proc.shutdown()
63
+ except Exception:
64
+ pass
65
+
39
66
 
40
67
  def register_otel(
41
68
  *,
@@ -86,8 +113,9 @@ def register_otel(
86
113
 
87
114
  def _reset_for_tests() -> None:
88
115
  """Reset internal state — for tests only. Not exported from __init__.py."""
89
- global _registered
116
+ global _registered, _otlp_processor
90
117
  _registered = None
118
+ _otlp_processor = None
91
119
  _reset_transport_for_tests()
92
120
 
93
121
 
@@ -131,6 +159,11 @@ def _setup_otlp_mode(
131
159
  headers={"Authorization": f"Bearer {site_id}"},
132
160
  )
133
161
  batch_processor = BatchSpanProcessor(exporter)
162
+ # Expose flush/shutdown so trodo.flush()/shutdown() can drain the batch —
163
+ # BatchSpanProcessor holds spans for up to 5s by default, so without this
164
+ # a short-lived process exits before the exporter ever sends them.
165
+ global _otlp_processor
166
+ _otlp_processor = batch_processor
134
167
 
135
168
  # Coexistence: if a non-default TracerProvider is already registered,
136
169
  # attach our exporter to it instead of replacing it.
@@ -162,7 +195,16 @@ def _setup_otlp_mode(
162
195
  active = _build_instrumentations(disable)
163
196
 
164
197
  # Activate the OTel transport so wrap_agent / span dispatch via tracer.
165
- set_otel_transport(otel_trace.get_tracer("trodo-agent"))
198
+ # Pass the use_span/Status/StatusCode helpers (same as the coexistence
199
+ # branch above) — without use_span, wrap_agent's root span is never made
200
+ # current, so trodo.span() children and auto-instrumented spans can't
201
+ # join its trace, and error status can never be stamped.
202
+ set_otel_transport(
203
+ otel_trace.get_tracer("trodo-agent"),
204
+ use_span=use_span,
205
+ status_cls=Status,
206
+ status_code=StatusCode,
207
+ )
166
208
  return active
167
209
 
168
210
 
@@ -253,8 +253,14 @@ class RunHandle:
253
253
  def set_output(self, value: Any) -> None:
254
254
  self.output = _prepare_value(value)
255
255
 
256
- def set_metadata(self, **kwargs: Any) -> None:
257
- self.metadata.update(kwargs)
256
+ def set_metadata(self, metadata: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
257
+ # Accept a dict (Node parity: setMetadata({...})) and/or keyword args
258
+ # (set_metadata(key=value)). Previously kwargs-only, which broke code
259
+ # copied from the Node SDK.
260
+ if metadata:
261
+ self.metadata.update(metadata)
262
+ if kwargs:
263
+ self.metadata.update(kwargs)
258
264
 
259
265
  def set_error_summary(self, summary: str, *, type: Optional[str] = None) -> None:
260
266
  """Mark the run as errored without raising — e.g. a soft failure the
@@ -636,10 +642,12 @@ class wrap_agent:
636
642
  assert self._otel_span is not None
637
643
  otel_span = self._otel_span
638
644
  try:
645
+ # OTel attribute values must be primitives — dicts/lists are
646
+ # rejected (with a warning) and silently dropped. Serialize.
639
647
  if self.handle.input is not None:
640
- otel_span.set_attribute("ai.prompt", self.handle.input)
648
+ otel_span.set_attribute("ai.prompt", _serialize_attr(self.handle.input))
641
649
  if self.handle.output is not None:
642
- otel_span.set_attribute("ai.response.text", self.handle.output)
650
+ otel_span.set_attribute("ai.response.text", _serialize_attr(self.handle.output))
643
651
  for k, v in self.handle.metadata.items():
644
652
  otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
645
653
  if exc is not None:
@@ -787,8 +795,17 @@ class span:
787
795
  self._active: Optional[ActiveSpanContext] = None
788
796
  self._span_id: str = ""
789
797
  self.handle: Optional[SpanHandle] = None
798
+ # 'otlp' transport state — populated only when register_otel(mode='otlp').
799
+ self._otel_span: Optional[Any] = None
800
+ self._otel_cm: Optional[Any] = None
790
801
 
791
802
  def __enter__(self) -> SpanHandle:
803
+ # Dispatch: 'otlp' transport routes through the OTel tracer so this
804
+ # span joins the wrap_agent root's trace (mirrors Node withSpanOtel).
805
+ # Without this branch, otlp-mode children hit the "no active run"
806
+ # no-op below and were silently dropped.
807
+ if get_transport_mode() == "otlp" and get_otel_tracer() is not None:
808
+ return self._enter_otel()
792
809
  self._active = get_active_context()
793
810
  self._span_id = str(uuid.uuid4())
794
811
  self.handle = SpanHandle(self._span_id, self._name)
@@ -813,6 +830,8 @@ class span:
813
830
  return self.handle
814
831
 
815
832
  def __exit__(self, exc_type, exc, tb) -> None:
833
+ if self._otel_span is not None:
834
+ return self._exit_otel(exc_type, exc, tb)
816
835
  if self._active is None or self.handle is None:
817
836
  return None
818
837
  ended_iso = _now_iso()
@@ -854,3 +873,75 @@ class span:
854
873
  if self._ctx_mgr is not None:
855
874
  self._ctx_mgr.__exit__(exc_type, exc, tb)
856
875
  return None
876
+
877
+ # ----------------------------------------------------------------------
878
+ # 'otlp' transport — OTel tracer path (mirrors Node withSpanOtel).
879
+ # ----------------------------------------------------------------------
880
+
881
+ def _enter_otel(self) -> SpanHandle:
882
+ tracer = get_otel_tracer()
883
+ # start_span parents to the CURRENT OTel context — the wrap_agent root
884
+ # (made current via use_span in _enter_otel of wrap_agent).
885
+ otel_span = tracer.start_span(self._name)
886
+ use_span_helper, _, _ = get_otel_helpers()
887
+ if use_span_helper is not None:
888
+ self._otel_cm = use_span_helper(otel_span, end_on_exit=False)
889
+ self._otel_cm.__enter__()
890
+ self._otel_span = otel_span
891
+ self._span_id = str(uuid.uuid4())
892
+ self.handle = SpanHandle(self._span_id, self._name)
893
+ if self._input is not None:
894
+ self.handle.input = self._input
895
+ if self._attributes:
896
+ self.handle.attributes.update(self._attributes)
897
+ return self.handle
898
+
899
+ def _exit_otel(self, exc_type, exc, tb) -> None:
900
+ otel_span = self._otel_span
901
+ h = self.handle
902
+ try:
903
+ # Stamp Trodo kind via gen_ai.* / retrieval.* convention so the
904
+ # backend's kind inference picks it up (same keys as trodo-node).
905
+ if self._kind == "tool":
906
+ otel_span.set_attribute("gen_ai.tool.name", h.tool_name or self._name)
907
+ if self._kind == "retrieval":
908
+ otel_span.set_attribute("retrieval.query", self._name)
909
+ if h.input is not None:
910
+ otel_span.set_attribute("input.value", _serialize_attr(h.input))
911
+ if h.output is not None:
912
+ otel_span.set_attribute("output.value", _serialize_attr(h.output))
913
+ if h.model:
914
+ otel_span.set_attribute("gen_ai.request.model", h.model)
915
+ if h.provider:
916
+ otel_span.set_attribute("gen_ai.system", h.provider)
917
+ if h.input_tokens is not None:
918
+ otel_span.set_attribute("gen_ai.usage.input_tokens", h.input_tokens)
919
+ if h.output_tokens is not None:
920
+ otel_span.set_attribute("gen_ai.usage.output_tokens", h.output_tokens)
921
+ if h.cost is not None:
922
+ otel_span.set_attribute("gen_ai.usage.cost", h.cost)
923
+ if h.temperature is not None:
924
+ otel_span.set_attribute("gen_ai.request.temperature", h.temperature)
925
+ if h.tool_name:
926
+ otel_span.set_attribute("gen_ai.tool.name", h.tool_name)
927
+ for k, v in (h.attributes or {}).items():
928
+ otel_span.set_attribute(f"trodo.span.{k}", _serialize_attr(v))
929
+ einfo = _resolve_error(h, exc_type, exc, tb)
930
+ if einfo["status"] == "error":
931
+ if exc is not None and hasattr(otel_span, "record_exception"):
932
+ otel_span.record_exception(exc)
933
+ _, status_cls, status_code = get_otel_helpers()
934
+ if status_cls is not None and status_code is not None:
935
+ otel_span.set_status(
936
+ status_cls(status_code.ERROR, einfo["error_message"] or "")
937
+ )
938
+ finally:
939
+ otel_span.end()
940
+ if self._otel_cm is not None:
941
+ try:
942
+ self._otel_cm.__exit__(exc_type, exc, tb)
943
+ except Exception:
944
+ pass
945
+ self._otel_span = None
946
+ self._otel_cm = None
947
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.2
3
+ Version: 2.10.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
File without changes
File without changes