trodo-python 2.10.3__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.3 → trodo_python-2.10.4}/PKG-INFO +1 -1
  2. {trodo_python-2.10.3 → trodo_python-2.10.4}/pyproject.toml +1 -1
  3. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/__init__.py +1 -1
  4. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/client.py +9 -1
  5. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/register.py +44 -2
  6. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/wrap_agent.py +87 -2
  7. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo_python.egg-info/PKG-INFO +1 -1
  8. {trodo_python-2.10.3 → trodo_python-2.10.4}/README.md +0 -0
  9. {trodo_python-2.10.3 → trodo_python-2.10.4}/setup.cfg +0 -0
  10. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_anon_distinct_id 2.py +0 -0
  11. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_anon_distinct_id.py +0 -0
  12. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_auto_instrument_fixes.py +0 -0
  13. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_cross_process_session.py +0 -0
  14. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_end_run.py +0 -0
  15. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_error_enrichment.py +0 -0
  16. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_llm_usage_cost.py +0 -0
  17. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_processor_methods.py +0 -0
  18. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_register_otel.py +0 -0
  19. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_start_run.py +0 -0
  20. {trodo_python-2.10.3 → trodo_python-2.10.4}/tests/test_wrap_agent_unchanged.py +0 -0
  21. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/api/__init__.py +0 -0
  22. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/api/async_client.py +0 -0
  23. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/api/endpoints.py +0 -0
  24. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/api/http_client.py +0 -0
  25. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/auto/__init__.py +0 -0
  26. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/auto/auto_event_manager.py +0 -0
  27. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/managers/__init__.py +0 -0
  28. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/managers/group_manager.py +0 -0
  29. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/managers/people_manager.py +0 -0
  30. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/__init__.py +0 -0
  31. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/auto_instrument.py +0 -0
  32. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/context.py +0 -0
  33. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/helpers.py +0 -0
  34. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/processor.py +0 -0
  35. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/otel/transport.py +0 -0
  36. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/queue/__init__.py +0 -0
  37. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/queue/batch_flusher.py +0 -0
  38. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/queue/event_queue.py +0 -0
  39. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/session/__init__.py +0 -0
  40. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/session/server_session.py +0 -0
  41. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/session/session_manager.py +0 -0
  42. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/types.py +0 -0
  43. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo/user_context.py +0 -0
  44. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo_python.egg-info/SOURCES.txt +0 -0
  45. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo_python.egg-info/dependency_links.txt +0 -0
  46. {trodo_python-2.10.3 → trodo_python-2.10.4}/trodo_python.egg-info/requires.txt +0 -0
  47. {trodo_python-2.10.3 → 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.3
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.3"
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.3"
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
@@ -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
 
@@ -642,10 +642,12 @@ class wrap_agent:
642
642
  assert self._otel_span is not None
643
643
  otel_span = self._otel_span
644
644
  try:
645
+ # OTel attribute values must be primitives — dicts/lists are
646
+ # rejected (with a warning) and silently dropped. Serialize.
645
647
  if self.handle.input is not None:
646
- otel_span.set_attribute("ai.prompt", self.handle.input)
648
+ otel_span.set_attribute("ai.prompt", _serialize_attr(self.handle.input))
647
649
  if self.handle.output is not None:
648
- otel_span.set_attribute("ai.response.text", self.handle.output)
650
+ otel_span.set_attribute("ai.response.text", _serialize_attr(self.handle.output))
649
651
  for k, v in self.handle.metadata.items():
650
652
  otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
651
653
  if exc is not None:
@@ -793,8 +795,17 @@ class span:
793
795
  self._active: Optional[ActiveSpanContext] = None
794
796
  self._span_id: str = ""
795
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
796
801
 
797
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()
798
809
  self._active = get_active_context()
799
810
  self._span_id = str(uuid.uuid4())
800
811
  self.handle = SpanHandle(self._span_id, self._name)
@@ -819,6 +830,8 @@ class span:
819
830
  return self.handle
820
831
 
821
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)
822
835
  if self._active is None or self.handle is None:
823
836
  return None
824
837
  ended_iso = _now_iso()
@@ -860,3 +873,75 @@ class span:
860
873
  if self._ctx_mgr is not None:
861
874
  self._ctx_mgr.__exit__(exc_type, exc, tb)
862
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.3
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