trodo-python 2.22.0__tar.gz → 2.23.1__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 (65) hide show
  1. {trodo_python-2.22.0 → trodo_python-2.23.1}/PKG-INFO +1 -1
  2. {trodo_python-2.22.0 → trodo_python-2.23.1}/pyproject.toml +1 -1
  3. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_instrumentor_resolution.py +79 -0
  4. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_prompts.py +24 -0
  5. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/auto_instrument.py +49 -1
  6. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/prompts/compile.py +38 -2
  7. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/prompts/template.py +16 -1
  8. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo_python.egg-info/PKG-INFO +1 -1
  9. {trodo_python-2.22.0 → trodo_python-2.23.1}/README.md +0 -0
  10. {trodo_python-2.22.0 → trodo_python-2.23.1}/setup.cfg +0 -0
  11. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_anon_distinct_id.py +0 -0
  12. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_auto_instrument_fixes.py +0 -0
  13. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_cross_process_session.py +0 -0
  14. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_datasets.py +0 -0
  15. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_end_run.py +0 -0
  16. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_error_enrichment.py +0 -0
  17. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_experiments.py +0 -0
  18. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_get_prompt_version_types.py +0 -0
  19. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_llm_usage_cost.py +0 -0
  20. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_multi_agent_shape.py +0 -0
  21. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_processor_methods.py +0 -0
  22. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_prompt_trace.py +0 -0
  23. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_register_otel.py +0 -0
  24. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_start_run.py +0 -0
  25. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_user_traits.py +0 -0
  26. {trodo_python-2.22.0 → trodo_python-2.23.1}/tests/test_wrap_agent_unchanged.py +0 -0
  27. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/__init__.py +0 -0
  28. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/api/__init__.py +0 -0
  29. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/api/async_client.py +0 -0
  30. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/api/endpoints.py +0 -0
  31. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/api/http_client.py +0 -0
  32. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/auto/__init__.py +0 -0
  33. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/auto/auto_event_manager.py +0 -0
  34. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/client.py +0 -0
  35. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/__init__.py +0 -0
  36. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/dataset_manager.py +0 -0
  37. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/experiment_manager.py +0 -0
  38. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/group_manager.py +0 -0
  39. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/people_manager.py +0 -0
  40. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/prompt_manager.py +0 -0
  41. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/managers/user_manager.py +0 -0
  42. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/__init__.py +0 -0
  43. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/context.py +0 -0
  44. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/helpers.py +0 -0
  45. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/processor.py +0 -0
  46. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/prompt_trace.py +0 -0
  47. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/register.py +0 -0
  48. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/transport.py +0 -0
  49. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/otel/wrap_agent.py +0 -0
  50. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/prompts/__init__.py +0 -0
  51. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/prompts/types.py +0 -0
  52. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/queue/__init__.py +0 -0
  53. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/queue/batch_flusher.py +0 -0
  54. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/queue/event_queue.py +0 -0
  55. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/session/__init__.py +0 -0
  56. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/session/server_session.py +0 -0
  57. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/session/session_manager.py +0 -0
  58. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/types.py +0 -0
  59. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/user_context.py +0 -0
  60. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/util/__init__.py +0 -0
  61. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo/util/lru.py +0 -0
  62. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo_python.egg-info/SOURCES.txt +0 -0
  63. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo_python.egg-info/dependency_links.txt +0 -0
  64. {trodo_python-2.22.0 → trodo_python-2.23.1}/trodo_python.egg-info/requires.txt +0 -0
  65. {trodo_python-2.22.0 → trodo_python-2.23.1}/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.22.0
3
+ Version: 2.23.1
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.22.0"
7
+ version = "2.23.1"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -107,3 +107,82 @@ def test_a_missing_module_still_raises_importerror(_clean):
107
107
  """The caller relies on ImportError to mean "skip this framework"."""
108
108
  with pytest.raises(ImportError):
109
109
  _instrument('module_that_does_not_exist_xyz', 'AnyInstrumentor')
110
+
111
+
112
+ # ── Version conflicts must not be reported as active ────────────────────────
113
+ #
114
+ # An instrumentor whose target library is the wrong VERSION does not raise:
115
+ # OpenTelemetry logs a DependencyConflict and returns without patching. We then
116
+ # appended it to `active` and reported auto-capture that was not happening.
117
+ # Found live: cohere 7.0.9 against the instrumentor's ">=4.2.7, <6" was listed
118
+ # as active while producing no spans — the same lie as Node listing `openai`
119
+ # for a version its instrumentation cannot patch.
120
+
121
+ @pytest.fixture
122
+ def fake_conflict_checker():
123
+ """Stand in for opentelemetry.instrumentation.dependencies.
124
+
125
+ OpenTelemetry is an OPTIONAL dependency of this SDK, so the suite must not
126
+ require it to be installed — injecting the module is how these tests stay
127
+ runnable in a bare environment.
128
+ """
129
+ made = []
130
+
131
+ def install(result):
132
+ for mod in ('opentelemetry', 'opentelemetry.instrumentation',
133
+ 'opentelemetry.instrumentation.dependencies'):
134
+ if mod not in sys.modules:
135
+ sys.modules[mod] = types.ModuleType(mod)
136
+ made.append(mod)
137
+ sys.modules['opentelemetry.instrumentation.dependencies'].get_dependency_conflicts = (
138
+ lambda deps: result
139
+ )
140
+ yield install
141
+ for m in made:
142
+ sys.modules.pop(m, None)
143
+
144
+
145
+ def test_a_version_conflict_is_raised_not_swallowed(_clean, fake_conflict_checker):
146
+ name = 'fake_instr_conflict'
147
+ _clean.append(name)
148
+
149
+ class Conflicted(_Recorder):
150
+ instrumented = False
151
+
152
+ def instrumentation_dependencies(self):
153
+ return ('somelib >=1, <2',)
154
+
155
+ _fake_module(name, ConflictedInstrumentor=Conflicted)
156
+ fake_conflict_checker('DependencyConflict: requested "somelib >=1, <2" but found "somelib 3.0"')
157
+ from trodo.otel.auto_instrument import _IncompatibleInstrumentation
158
+
159
+ with pytest.raises(_IncompatibleInstrumentation, match='somelib 3.0'):
160
+ _instrument(name, 'ConflictedInstrumentor')
161
+ assert Conflicted.instrumented is False # and it never patched
162
+
163
+
164
+ def test_no_conflict_instruments_normally(_clean, fake_conflict_checker):
165
+ name = 'fake_instr_ok'
166
+ _clean.append(name)
167
+
168
+ class Fine(_Recorder):
169
+ instrumented = False
170
+
171
+ def instrumentation_dependencies(self):
172
+ return ('somelib >=1',)
173
+
174
+ _fake_module(name, FineInstrumentor=Fine)
175
+ fake_conflict_checker(None)
176
+ _instrument(name, 'FineInstrumentor')
177
+ assert Fine.instrumented is True
178
+
179
+
180
+ def test_unknowable_conflict_state_fails_open(_clean):
181
+ """No dependency helper, or an instrumentor that declares nothing, must not
182
+ be condemned — falsely reporting a working setup as broken is worse."""
183
+ name = 'fake_instr_nodeps'
184
+ _clean.append(name)
185
+ cls = type('NoDepsInstrumentor', (_Recorder,), {'instrumented': False})
186
+ _fake_module(name, NoDepsInstrumentor=cls)
187
+ _instrument(name, 'NoDepsInstrumentor')
188
+ assert cls.instrumented is True
@@ -408,3 +408,27 @@ def test_list_returns_empty_on_error():
408
408
  return {"__error": True}
409
409
 
410
410
  assert PromptManager(Err()).list() == []
411
+
412
+
413
+ # ── Shared vectors: COMPILE layer ═══════════════════════════════════════════
414
+ #
415
+ # Declared-variable semantics (defaults, explicit null, per-type coercion) are
416
+ # where the three engines actually drifted -- THIS engine substituted the
417
+ # default for an explicit ``None`` and coerced string-typed values with
418
+ # ``str()`` instead of JS ``String()``, so the same call rendered different
419
+ # text in Python than in the playground. The backend and Node suites run the
420
+ # SAME cases from the same file.
421
+
422
+ with open(_VECTORS_PATH, encoding="utf-8") as _f:
423
+ _COMPILE_CASES = json.load(_f).get("compileCases", [])
424
+
425
+
426
+ @pytest.mark.parametrize("case", _COMPILE_CASES, ids=lambda c: c["name"])
427
+ def test_compile_vectors(case):
428
+ prompt = {
429
+ "messages": [{"role": "user", "content": [{"type": "text", "text": case["template"]}]}],
430
+ "model": {}, "tools": [], "response_format": None,
431
+ "variables": case["variables"],
432
+ }
433
+ out = compile_prompt(prompt, case["values"])
434
+ assert out.messages[0]["content"][0]["text"] == case["expect"]
@@ -438,7 +438,44 @@ def _instrument(module_name: str, *preferred: str) -> None:
438
438
  )
439
439
  if cls is None:
440
440
  raise ImportError(f"no Instrumentor class exported by {module_name}")
441
- cls().instrument()
441
+
442
+ instr = cls()
443
+
444
+ # An instrumentor whose target library is the wrong VERSION does not raise:
445
+ # OpenTelemetry logs a DependencyConflict and returns without patching
446
+ # anything. We would then append it to `active` and report auto-capture that
447
+ # is not happening -- the same lie as listing a framework whose
448
+ # instrumentation cannot patch the installed SDK. Ask first, and let the
449
+ # caller report it honestly.
450
+ conflict = _dependency_conflict(instr)
451
+ if conflict is not None:
452
+ raise _IncompatibleInstrumentation(str(conflict))
453
+
454
+ instr.instrument()
455
+
456
+
457
+ class _IncompatibleInstrumentation(ImportError):
458
+ """The instrumentor loaded, but its target library is an unsupported version."""
459
+
460
+
461
+ def _dependency_conflict(instr: Any) -> Any:
462
+ """The version conflict OpenTelemetry would hit, or None.
463
+
464
+ Returns None when we cannot tell -- an older OTel without the helper, or an
465
+ instrumentor that declares nothing. Falsely reporting a working setup as
466
+ broken is worse than the silence this replaces.
467
+ """
468
+ try:
469
+ from opentelemetry.instrumentation.dependencies import ( # type: ignore
470
+ get_dependency_conflicts,
471
+ )
472
+
473
+ deps = instr.instrumentation_dependencies()
474
+ if not deps:
475
+ return None
476
+ return get_dependency_conflicts(deps)
477
+ except Exception: # noqa: BLE001
478
+ return None
442
479
 
443
480
 
444
481
  _INSTRUMENTORS: List[tuple[str, Callable[[], Any]]] = []
@@ -556,6 +593,17 @@ def enable_auto_instrument(
556
593
  try:
557
594
  register()
558
595
  active.append(name)
596
+ except _IncompatibleInstrumentation as e:
597
+ # Loud, because this is the case where everything LOOKS installed:
598
+ # the package is present, the import worked, and no spans will ever
599
+ # appear. Silence here is what costs people an afternoon.
600
+ _warn_once(
601
+ f"version-{name}",
602
+ f"auto-instrument: {name} will NOT be auto-captured and is not "
603
+ f"reported as active -- {e}. Wrap the call with trodo.llm(...) "
604
+ f"to capture it, or pin the library to a supported version.",
605
+ )
606
+ continue
559
607
  except Exception:
560
608
  continue
561
609
  return active
@@ -36,6 +36,36 @@ def _q(value: Any) -> str:
36
36
  except (TypeError, ValueError):
37
37
  return repr(value)
38
38
 
39
+ def _js_string(value: Any) -> str:
40
+ """JavaScript ``String()`` semantics, byte-for-byte.
41
+
42
+ A non-string passed for a string-typed variable is a caller type mismatch,
43
+ but all three engines must agree on what it renders as -- the backend
44
+ reference (which powers the playground) and the Node SDK both go through
45
+ JS ``String()``, so ``True`` must render ``true``, ``3.0`` must render
46
+ ``3``, a list joins with commas, and a dict renders the infamous
47
+ ``[object Object]``. The JSON rendering callers actually want lives on the
48
+ ``json`` variable type, where all three engines already emit compact JSON.
49
+ """
50
+ if value is None:
51
+ return ""
52
+ if isinstance(value, bool):
53
+ return "true" if value else "false"
54
+ if isinstance(value, float) and value.is_integer() and abs(value) < 1e21:
55
+ return str(int(value))
56
+ if isinstance(value, (int, float)):
57
+ return str(value)
58
+ if isinstance(value, str):
59
+ return value
60
+ if isinstance(value, (list, tuple)):
61
+ # Array.prototype.toString: elements joined by ',', null/undefined
62
+ # rendering empty, recursively.
63
+ return ",".join(_js_string(x) for x in value)
64
+ if isinstance(value, dict):
65
+ return "[object Object]"
66
+ return str(value)
67
+
68
+
39
69
  def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
40
70
  """Coerce a caller value to the declared type.
41
71
 
@@ -71,7 +101,7 @@ def _coerce(value: Any, type_: str, name: str, errors: List[str]) -> Any:
71
101
  return value
72
102
 
73
103
  if type_ == "string":
74
- return value if isinstance(value, str) else str(value)
104
+ return value if isinstance(value, str) else _js_string(value)
75
105
 
76
106
  if type_ == "messages":
77
107
  if not isinstance(value, list):
@@ -119,7 +149,13 @@ def build_scope(
119
149
  # empty. That IS the point of declaring a default -- there is no third
120
150
  # case where the caller has to have supplied something, and adding one
121
151
  # would turn an empty render into a crash for no gain.
122
- if not has or value is None:
152
+ #
153
+ # The default applies to an ABSENT key only. An explicit ``None`` is a
154
+ # value -- "render this empty" -- exactly as ``null`` is in the backend
155
+ # reference engine and the Node SDK. This engine used to substitute the
156
+ # default for an explicit ``None``, so the same call rendered different
157
+ # text in Python than everywhere else, including the playground.
158
+ if not has:
123
159
  value = default if has_default else ([] if type_ == "messages" else "")
124
160
 
125
161
  scope[name] = _coerce(value, type_, name, errors)
@@ -159,6 +159,21 @@ def _lookup(path: str, scopes: Sequence[Any]) -> Tuple[bool, Any]:
159
159
  return False, None
160
160
 
161
161
 
162
+ def _jsonable(value):
163
+ """Normalise for JSON.stringify parity: JS has one number type, so a float
164
+ that is a whole number must serialise as ``1`` and not ``1.0`` -- at any
165
+ depth. Everything else passes through untouched."""
166
+ if isinstance(value, bool):
167
+ return value
168
+ if isinstance(value, float) and value.is_integer() and abs(value) < 1e21:
169
+ return int(value)
170
+ if isinstance(value, dict):
171
+ return {k: _jsonable(v) for k, v in value.items()}
172
+ if isinstance(value, (list, tuple)):
173
+ return [_jsonable(v) for v in value]
174
+ return value
175
+
176
+
162
177
  def _stringify(value: Any) -> str:
163
178
  if value is None:
164
179
  return ""
@@ -173,7 +188,7 @@ def _stringify(value: Any) -> str:
173
188
  return str(int(value))
174
189
  return str(value)
175
190
  try:
176
- return json.dumps(value, separators=(",", ":"))
191
+ return json.dumps(_jsonable(value), separators=(",", ":"), ensure_ascii=False)
177
192
  except (TypeError, ValueError):
178
193
  return str(value)
179
194
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.22.0
3
+ Version: 2.23.1
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