unique_toolkit 2026.34.0.dev2__py3-none-any.whl → 2026.34.0.dev4__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.
@@ -1,3 +1,19 @@
1
+ from unique_toolkit.monitoring.tracing import (
2
+ TraceContextMiddleware,
3
+ TraceExporter,
4
+ TracingSettings,
5
+ configure_tracing,
6
+ inject_trace_headers,
7
+ )
8
+
9
+ __all__ = [
10
+ "TraceContextMiddleware",
11
+ "TraceExporter",
12
+ "TracingSettings",
13
+ "configure_tracing",
14
+ "inject_trace_headers",
15
+ ]
16
+
1
17
  _MONITORING_AVAILABLE = False
2
18
  try:
3
19
  from prometheus_client import ( # pyright: ignore[reportMissingImports]
@@ -27,7 +43,7 @@ if _MONITORING_AVAILABLE:
27
43
  track_execution,
28
44
  )
29
45
 
30
- __all__ = [
46
+ __all__ += [
31
47
  "REGISTRY",
32
48
  "MetricNamespace",
33
49
  "MetricsMiddleware",
@@ -0,0 +1,201 @@
1
+ """OpenTelemetry tracing bootstrap for toolkit consumers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable, Iterable, MutableMapping
6
+ from enum import StrEnum
7
+ from typing import TYPE_CHECKING, ClassVar, Self, TypeAlias, cast
8
+
9
+ from pydantic import AliasChoices, Field
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+ if TYPE_CHECKING:
13
+ from opentelemetry.trace.propagation.tracecontext import (
14
+ TraceContextTextMapPropagator,
15
+ )
16
+
17
+ # Keep ASGI aliases local; tracing should not depend on asgiref solely for typing.
18
+ ASGIScope: TypeAlias = MutableMapping[str, object]
19
+ ASGIMessage: TypeAlias = MutableMapping[str, object]
20
+ ASGIReceive: TypeAlias = Callable[[], Awaitable[ASGIMessage]]
21
+ ASGISend: TypeAlias = Callable[[ASGIMessage], Awaitable[None]]
22
+ ASGIApp: TypeAlias = Callable[[ASGIScope, ASGIReceive, ASGISend], Awaitable[None]]
23
+
24
+ _OTEL_EXTRA_MESSAGE = (
25
+ "OpenTelemetry tracing requires the unique_toolkit[otel] extra. "
26
+ "Install it with: uv add 'unique-toolkit[otel]'"
27
+ )
28
+
29
+
30
+ class TraceExporter(StrEnum):
31
+ """Trace exporters supported by the toolkit bootstrap."""
32
+
33
+ NONE = "none"
34
+ CONSOLE = "console"
35
+ OTLP = "otlp"
36
+
37
+
38
+ class TracingSettings(BaseSettings):
39
+ """Environment settings for optional OpenTelemetry tracing."""
40
+
41
+ model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
42
+ env_prefix="OTEL_",
43
+ env_ignore_empty=True,
44
+ extra="ignore",
45
+ )
46
+
47
+ traces_exporter: TraceExporter | None = None
48
+ service_name: str | None = None
49
+ service_version: str | None = Field(
50
+ default=None,
51
+ validation_alias=AliasChoices("OTEL_SERVICE_VERSION", "VERSION"),
52
+ )
53
+ otlp_endpoint: str | None = Field(
54
+ default=None,
55
+ validation_alias=AliasChoices(
56
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
57
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
58
+ ),
59
+ )
60
+ span_processor: TraceExporter | None = None
61
+ enabled: bool | None = Field(
62
+ default=None,
63
+ validation_alias="ENABLE_OPENTELEMETRY",
64
+ )
65
+
66
+ @property
67
+ def exporter(self) -> TraceExporter | None:
68
+ """Resolve standard OTel settings with Node service compatibility aliases."""
69
+ if self.traces_exporter:
70
+ return self.traces_exporter
71
+ if self.otlp_endpoint:
72
+ return TraceExporter.OTLP
73
+ if self.enabled is False:
74
+ return None
75
+ if self.enabled is True:
76
+ return self.span_processor or TraceExporter.OTLP
77
+ return None
78
+
79
+ @classmethod
80
+ def try_load(cls) -> Self | None:
81
+ """Load env settings, or ``None`` when tracing cannot be enabled."""
82
+ settings = cls()
83
+ if settings.exporter in {None, TraceExporter.NONE}:
84
+ return None
85
+ return settings
86
+
87
+
88
+ def _missing_otel_extra() -> ImportError:
89
+ """Create a consistent error for an unavailable optional tracing dependency."""
90
+ return ImportError(_OTEL_EXTRA_MESSAGE)
91
+
92
+
93
+ def _trace_context_propagator() -> TraceContextTextMapPropagator:
94
+ """Return a W3C trace-context propagator without forwarding baggage."""
95
+ try:
96
+ from opentelemetry.trace.propagation.tracecontext import (
97
+ TraceContextTextMapPropagator,
98
+ )
99
+ except ImportError as error:
100
+ raise _missing_otel_extra() from error
101
+ return TraceContextTextMapPropagator()
102
+
103
+
104
+ class TraceContextMiddleware:
105
+ """ASGI middleware that continues W3C trace context with one server span."""
106
+
107
+ def __init__(self, app: ASGIApp, span_name: str = "HTTP request") -> None:
108
+ self.app: ASGIApp = app
109
+ self._span_name: str = span_name
110
+
111
+ async def __call__(
112
+ self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
113
+ ) -> None:
114
+ if scope["type"] != "http":
115
+ return await self.app(scope, receive, send)
116
+
117
+ try:
118
+ from opentelemetry import trace
119
+ from opentelemetry.trace import SpanKind
120
+ except ImportError as error:
121
+ raise _missing_otel_extra() from error
122
+
123
+ headers: dict[str, str] = {}
124
+ raw_headers = cast(Iterable[tuple[bytes, bytes]], scope.get("headers", ()))
125
+ for name, value in raw_headers:
126
+ key = name.decode("latin-1").lower()
127
+ if key not in {"traceparent", "tracestate"}:
128
+ continue
129
+ header_value = value.decode("latin-1")
130
+ if key == "tracestate" and key in headers:
131
+ headers[key] = f"{headers[key]},{header_value}"
132
+ elif key not in headers:
133
+ headers[key] = header_value
134
+ parent_context = _trace_context_propagator().extract(headers)
135
+ with trace.get_tracer(__name__).start_as_current_span(
136
+ self._span_name,
137
+ context=parent_context,
138
+ kind=SpanKind.SERVER,
139
+ ):
140
+ return await self.app(scope, receive, send)
141
+
142
+
143
+ def inject_trace_headers(headers: MutableMapping[str, str]) -> None:
144
+ """Inject the active W3C trace context into outgoing request headers."""
145
+ _trace_context_propagator().inject(headers)
146
+
147
+
148
+ def configure_tracing(
149
+ *,
150
+ service_name: str | None = None,
151
+ service_version: str | None = None,
152
+ ) -> bool:
153
+ """Install an OpenTelemetry trace provider from standard and Node-compatible env.
154
+
155
+ ``OTEL_TRACES_EXPORTER`` takes precedence. When it is unset,
156
+ ``ENABLE_OPENTELEMETRY`` and ``OTEL_SPAN_PROCESSOR`` mirror the Node service
157
+ tracing configuration. Prometheus metrics and application logging remain
158
+ separate, so ``OTEL_METRICS_READER`` and ``OTEL_LOGS_PROCESSOR`` are ignored.
159
+ """
160
+ settings = TracingSettings.try_load()
161
+ if settings is None:
162
+ return False
163
+ exporter = settings.exporter
164
+ assert exporter is not None # try_load guarantees a concrete exporter
165
+
166
+ try:
167
+ from opentelemetry import trace
168
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
169
+ OTLPSpanExporter,
170
+ )
171
+ from opentelemetry.sdk.resources import Resource
172
+ from opentelemetry.sdk.trace import TracerProvider
173
+ from opentelemetry.sdk.trace.export import (
174
+ BatchSpanProcessor,
175
+ ConsoleSpanExporter,
176
+ SimpleSpanProcessor,
177
+ )
178
+ from opentelemetry.trace import ProxyTracerProvider
179
+ except ImportError as error:
180
+ raise _missing_otel_extra() from error
181
+
182
+ if not isinstance(trace.get_tracer_provider(), ProxyTracerProvider):
183
+ return True
184
+
185
+ name = service_name if service_name is not None else settings.service_name
186
+ version = (
187
+ service_version if service_version is not None else settings.service_version
188
+ )
189
+ attributes = {
190
+ key: value
191
+ for key, value in (("service.name", name), ("service.version", version))
192
+ if value is not None
193
+ }
194
+
195
+ provider = TracerProvider(resource=Resource.create(attributes))
196
+ if exporter is TraceExporter.CONSOLE:
197
+ provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
198
+ else:
199
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
200
+ trace.set_tracer_provider(provider)
201
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.34.0.dev2
3
+ Version: 2026.34.0.dev4
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.34.0.dev0,<2026.34.0rc0
19
+ Requires-Dist: unique-sdk>=2026.34.0.dev1,<2026.34.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -42,10 +42,13 @@ Requires-Dist: langchain>=1.0.0,<2 ; extra == 'langchain'
42
42
  Requires-Dist: langchain-openai>=0.3.28 ; extra == 'langchain'
43
43
  Requires-Dist: langchain-core>=1.0.0,<2 ; extra == 'langchain'
44
44
  Requires-Dist: prometheus-client>=0.24.1,<1 ; extra == 'monitoring'
45
+ Requires-Dist: opentelemetry-sdk>=1.27,<2 ; extra == 'otel'
46
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27,<2 ; extra == 'otel'
45
47
  Requires-Python: >=3.12
46
48
  Provides-Extra: fastapi
47
49
  Provides-Extra: langchain
48
50
  Provides-Extra: monitoring
51
+ Provides-Extra: otel
49
52
  Description-Content-Type: text/markdown
50
53
 
51
54
  # Unique Toolkit
@@ -402,10 +402,11 @@ unique_toolkit/language_model/service.py,sha256=hDoJBBgG_wWs8WuC_c8tnmK8cNu8o5Eh
402
402
  unique_toolkit/language_model/settings.py,sha256=7szohExI-qmTfUf2Zp5AB5TzrsZy1UekAGyXx_UaPMc,1524
403
403
  unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQm3WB4yZt75BXcqx0DVkI,2775
404
404
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
405
- unique_toolkit/monitoring/__init__.py,sha256=z1gkOCLxGcg936MFuLSeWFa0tREcBFSUbV09NxilBw4,1065
405
+ unique_toolkit/monitoring/__init__.py,sha256=auxvvOBwYrCD3TqEX0sIupDYx9nbCt2u4XN0Sy55qKI,1376
406
406
  unique_toolkit/monitoring/gunicorn.py,sha256=urTFFS5zYHKoxb1TEQHcLqb2byi-aYgFRHGwgYRzYYI,844
407
407
  unique_toolkit/monitoring/middleware.py,sha256=MNsUuKgt74mSnUwttXTB_AxX546Pz2zKoAP9OewVrwQ,4531
408
408
  unique_toolkit/monitoring/registry.py,sha256=De2Bx5QBBRJQY2MM3xtntiJ2JU2FoKkVT7rNMG7ewgA,5874
409
+ unique_toolkit/monitoring/tracing.py,sha256=86yUIF8HrHVrkegOBOM7GqpbmVrYP07AZ4HtP-3X9QY,7128
409
410
  unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
410
411
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
411
412
  unique_toolkit/services/chat_service.py,sha256=ofhNHCW7kz6xXN4pUTBdDEYovZ7PXhUglPDgez8Qov0,99142
@@ -419,6 +420,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
419
420
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
420
421
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
421
422
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
422
- unique_toolkit-2026.34.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
- unique_toolkit-2026.34.0.dev2.dist-info/METADATA,sha256=sSnbBxt3xMhYRWjORpXENMgfFSyVIc6ibCmenVbxoco,7494
424
- unique_toolkit-2026.34.0.dev2.dist-info/RECORD,,
423
+ unique_toolkit-2026.34.0.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
424
+ unique_toolkit-2026.34.0.dev4.dist-info/METADATA,sha256=Rm135S4RxNJaS3bLkzg-PWh3tfyJBB2IyjCVUUgACT0,7656
425
+ unique_toolkit-2026.34.0.dev4.dist-info/RECORD,,