spanforge 2.0.0__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.
Files changed (101) hide show
  1. spanforge/__init__.py +695 -0
  2. spanforge/_batch_exporter.py +322 -0
  3. spanforge/_cli.py +3081 -0
  4. spanforge/_hooks.py +340 -0
  5. spanforge/_server.py +953 -0
  6. spanforge/_span.py +1015 -0
  7. spanforge/_store.py +287 -0
  8. spanforge/_stream.py +654 -0
  9. spanforge/_trace.py +334 -0
  10. spanforge/_tracer.py +253 -0
  11. spanforge/actor.py +141 -0
  12. spanforge/alerts.py +464 -0
  13. spanforge/auto.py +181 -0
  14. spanforge/baseline.py +336 -0
  15. spanforge/config.py +460 -0
  16. spanforge/consent.py +227 -0
  17. spanforge/consumer.py +379 -0
  18. spanforge/core/__init__.py +5 -0
  19. spanforge/core/compliance_mapping.py +1060 -0
  20. spanforge/cost.py +597 -0
  21. spanforge/debug.py +514 -0
  22. spanforge/drift.py +488 -0
  23. spanforge/egress.py +63 -0
  24. spanforge/eval.py +575 -0
  25. spanforge/event.py +1052 -0
  26. spanforge/exceptions.py +246 -0
  27. spanforge/explain.py +181 -0
  28. spanforge/export/__init__.py +50 -0
  29. spanforge/export/append_only.py +342 -0
  30. spanforge/export/cloud.py +349 -0
  31. spanforge/export/datadog.py +495 -0
  32. spanforge/export/grafana.py +331 -0
  33. spanforge/export/jsonl.py +198 -0
  34. spanforge/export/otel_bridge.py +291 -0
  35. spanforge/export/otlp.py +817 -0
  36. spanforge/export/otlp_bridge.py +231 -0
  37. spanforge/export/redis_backend.py +282 -0
  38. spanforge/export/webhook.py +302 -0
  39. spanforge/exporters/__init__.py +29 -0
  40. spanforge/exporters/console.py +271 -0
  41. spanforge/exporters/jsonl.py +144 -0
  42. spanforge/hitl.py +297 -0
  43. spanforge/inspect.py +429 -0
  44. spanforge/integrations/__init__.py +39 -0
  45. spanforge/integrations/_pricing.py +277 -0
  46. spanforge/integrations/anthropic.py +388 -0
  47. spanforge/integrations/bedrock.py +306 -0
  48. spanforge/integrations/crewai.py +251 -0
  49. spanforge/integrations/gemini.py +349 -0
  50. spanforge/integrations/groq.py +444 -0
  51. spanforge/integrations/langchain.py +349 -0
  52. spanforge/integrations/llamaindex.py +370 -0
  53. spanforge/integrations/ollama.py +286 -0
  54. spanforge/integrations/openai.py +370 -0
  55. spanforge/integrations/together.py +485 -0
  56. spanforge/metrics.py +393 -0
  57. spanforge/metrics_export.py +342 -0
  58. spanforge/migrate.py +278 -0
  59. spanforge/model_registry.py +282 -0
  60. spanforge/models.py +407 -0
  61. spanforge/namespaces/__init__.py +215 -0
  62. spanforge/namespaces/audit.py +253 -0
  63. spanforge/namespaces/cache.py +209 -0
  64. spanforge/namespaces/chain.py +74 -0
  65. spanforge/namespaces/confidence.py +69 -0
  66. spanforge/namespaces/consent.py +85 -0
  67. spanforge/namespaces/cost.py +175 -0
  68. spanforge/namespaces/decision.py +135 -0
  69. spanforge/namespaces/diff.py +146 -0
  70. spanforge/namespaces/drift.py +79 -0
  71. spanforge/namespaces/eval_.py +232 -0
  72. spanforge/namespaces/fence.py +180 -0
  73. spanforge/namespaces/guard.py +104 -0
  74. spanforge/namespaces/hitl.py +92 -0
  75. spanforge/namespaces/latency.py +69 -0
  76. spanforge/namespaces/prompt.py +185 -0
  77. spanforge/namespaces/redact.py +172 -0
  78. spanforge/namespaces/template.py +197 -0
  79. spanforge/namespaces/tool_call.py +76 -0
  80. spanforge/namespaces/trace.py +1006 -0
  81. spanforge/normalizer.py +183 -0
  82. spanforge/presidio_backend.py +149 -0
  83. spanforge/processor.py +258 -0
  84. spanforge/prompt_registry.py +415 -0
  85. spanforge/py.typed +0 -0
  86. spanforge/redact.py +780 -0
  87. spanforge/sampling.py +500 -0
  88. spanforge/schemas/v1.0/schema.json +170 -0
  89. spanforge/schemas/v2.0/schema.json +536 -0
  90. spanforge/signing.py +1152 -0
  91. spanforge/stream.py +559 -0
  92. spanforge/testing.py +376 -0
  93. spanforge/trace.py +199 -0
  94. spanforge/types.py +696 -0
  95. spanforge/ulid.py +304 -0
  96. spanforge/validate.py +383 -0
  97. spanforge-2.0.0.dist-info/METADATA +1777 -0
  98. spanforge-2.0.0.dist-info/RECORD +101 -0
  99. spanforge-2.0.0.dist-info/WHEEL +4 -0
  100. spanforge-2.0.0.dist-info/entry_points.txt +5 -0
  101. spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
spanforge/consumer.py ADDED
@@ -0,0 +1,379 @@
1
+ """Consumer registration API for spanforge.
2
+
3
+ Provides a lightweight registry that downstream tools, services, and libraries
4
+ can use to declare which event namespaces and schema versions they depend on.
5
+ This enables proactive compatibility checking between producers and consumers
6
+ before runtime failures occur.
7
+
8
+ Typical usage::
9
+
10
+ from spanforge.consumer import register_consumer, assert_compatible
11
+
12
+ # Register your tool's schema requirements.
13
+ register_consumer(
14
+ tool_name="my-analytics-pipeline",
15
+ namespaces=["trace", "eval"],
16
+ schema_version="1.0",
17
+ )
18
+
19
+ # Later — verify all registered consumers are compatible with the current schema.
20
+ assert_compatible() # raises IncompatibleSchemaError if any consumer is incompatible
21
+
22
+ See :class:`ConsumerRegistry` for the full registry API.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ import threading
29
+ from dataclasses import dataclass, field
30
+ from typing import TYPE_CHECKING
31
+
32
+ if TYPE_CHECKING:
33
+ from collections.abc import Sequence
34
+
35
+ __all__ = [
36
+ "ConsumerRecord",
37
+ "ConsumerRegistry",
38
+ "IncompatibleSchemaError",
39
+ "assert_compatible",
40
+ "get_registry",
41
+ "register_consumer",
42
+ ]
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Sentinel — current schema version understood by the library
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _CURRENT_SCHEMA_VERSION = "2.0"
49
+
50
+ # Accepted schema version patterns (semver-like, e.g. "1.0", "1.1", "2.0")
51
+ _VERSION_RE = re.compile(r"^\d+\.\d+$")
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Errors
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ class IncompatibleSchemaError(Exception):
60
+ """Raised when a consumer requires a schema version incompatible with the installed one.
61
+
62
+ Not compatible with the currently installed library version.
63
+
64
+ Attributes:
65
+ incompatible: List of ``(tool_name, required_version)`` pairs that
66
+ are incompatible with the installed schema version.
67
+ """
68
+
69
+ def __init__(self, incompatible: Sequence[tuple[str, str]]) -> None:
70
+ self.incompatible = list(incompatible)
71
+ pairs = ", ".join(f"{t!r} ({v})" for t, v in self.incompatible)
72
+ super().__init__(
73
+ f"Incompatible schema consumers: {pairs}. "
74
+ f"Installed schema version: {_CURRENT_SCHEMA_VERSION}"
75
+ )
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Data models
80
+ # ---------------------------------------------------------------------------
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ConsumerRecord:
85
+ """A record of a registered consumer's schema requirements.
86
+
87
+ Attributes:
88
+ tool_name: Human-readable name of the consuming tool or service.
89
+ namespaces: Event namespaces the consumer depends on, e.g.
90
+ ``["trace", "eval"]``.
91
+ schema_version: Minimum schema version required. Must be in
92
+ ``MAJOR.MINOR`` format (e.g. ``"1.0"``).
93
+ contact: Optional contact info (e.g. email, team name, Slack
94
+ channel) for compatibility issue escalation.
95
+ metadata: Optional freeform metadata for tooling.
96
+ """
97
+
98
+ tool_name: str
99
+ namespaces: tuple[str, ...]
100
+ schema_version: str
101
+ contact: str | None = None
102
+ metadata: dict[str, str] = field(default_factory=dict)
103
+
104
+ def __repr__(self) -> str: # pragma: no cover
105
+ return (
106
+ f"ConsumerRecord(tool_name={self.tool_name!r}, "
107
+ f"namespaces={list(self.namespaces)!r}, "
108
+ f"schema_version={self.schema_version!r})"
109
+ )
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Registry
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class ConsumerRegistry:
118
+ """Thread-safe registry of downstream consumer schema requirements.
119
+
120
+ Consumers register themselves with :meth:`register` declaring which
121
+ namespaces and schema version they depend on. Operators can then call
122
+ :meth:`assert_compatible` to validate all consumers before deploying a
123
+ new schema version.
124
+
125
+ Example::
126
+
127
+ registry = ConsumerRegistry()
128
+ registry.register("my-tool", namespaces=["trace"], schema_version="1.0")
129
+ registry.assert_compatible()
130
+ """
131
+
132
+ def __init__(self) -> None:
133
+ self._lock = threading.Lock()
134
+ self._records: list[ConsumerRecord] = []
135
+
136
+ # ------------------------------------------------------------------
137
+ # Registration
138
+ # ------------------------------------------------------------------
139
+
140
+ def register(
141
+ self,
142
+ tool_name: str,
143
+ *,
144
+ namespaces: Sequence[str],
145
+ schema_version: str,
146
+ contact: str | None = None,
147
+ metadata: dict[str, str] | None = None,
148
+ ) -> ConsumerRecord:
149
+ """Register a consumer's schema requirements.
150
+
151
+ Args:
152
+ tool_name: Name of the consuming tool or service.
153
+ namespaces: Event namespaces required (e.g. ``["trace", "eval"]``).
154
+ schema_version: Minimum schema version required (``"MAJOR.MINOR"``).
155
+ contact: Optional contact info for compatibility escalations.
156
+ metadata: Optional freeform metadata dict.
157
+
158
+ Returns:
159
+ The created :class:`ConsumerRecord`.
160
+
161
+ Raises:
162
+ ValueError: If *tool_name* is empty, *namespaces* is empty, or
163
+ *schema_version* is not in ``MAJOR.MINOR`` format.
164
+ """
165
+ if not tool_name or not tool_name.strip():
166
+ raise ValueError("tool_name must be a non-empty string")
167
+ if not namespaces:
168
+ raise ValueError("namespaces must contain at least one entry")
169
+ if not _VERSION_RE.match(schema_version):
170
+ raise ValueError(
171
+ f"schema_version must be in MAJOR.MINOR format (got {schema_version!r})"
172
+ )
173
+
174
+ record = ConsumerRecord(
175
+ tool_name=tool_name.strip(),
176
+ namespaces=tuple(str(ns).strip() for ns in namespaces),
177
+ schema_version=schema_version,
178
+ contact=contact,
179
+ metadata=dict(metadata) if metadata else {},
180
+ )
181
+ with self._lock:
182
+ self._records.append(record)
183
+ return record
184
+
185
+ # ------------------------------------------------------------------
186
+ # Querying
187
+ # ------------------------------------------------------------------
188
+
189
+ def all(self) -> list[ConsumerRecord]:
190
+ """Return a snapshot of all registered consumer records.
191
+
192
+ Returns:
193
+ List of all :class:`ConsumerRecord` instances.
194
+ """
195
+ with self._lock:
196
+ return list(self._records)
197
+
198
+ def by_namespace(self, namespace: str) -> list[ConsumerRecord]:
199
+ """Return all consumers that depend on *namespace*.
200
+
201
+ Args:
202
+ namespace: The namespace string to filter by (e.g. ``"trace"``).
203
+
204
+ Returns:
205
+ Filtered list of :class:`ConsumerRecord` instances.
206
+ """
207
+ with self._lock:
208
+ return [r for r in self._records if namespace in r.namespaces]
209
+
210
+ def by_tool(self, tool_name: str) -> ConsumerRecord | None:
211
+ """Return the first record registered under *tool_name*, or ``None``.
212
+
213
+ Args:
214
+ tool_name: The tool name to look up.
215
+
216
+ Returns:
217
+ The :class:`ConsumerRecord` or ``None`` if not found.
218
+ """
219
+ with self._lock:
220
+ for r in self._records:
221
+ if r.tool_name == tool_name:
222
+ return r
223
+ return None
224
+
225
+ # ------------------------------------------------------------------
226
+ # Compatibility checking
227
+ # ------------------------------------------------------------------
228
+
229
+ def check_compatible(
230
+ self,
231
+ installed_version: str = _CURRENT_SCHEMA_VERSION,
232
+ ) -> list[tuple[str, str]]:
233
+ """Check all consumers against *installed_version*.
234
+
235
+ A consumer is *compatible* if its ``schema_version`` major matches and
236
+ minor is less than or equal to the installed minor version. That is:
237
+
238
+ * Major version bump → always incompatible (breaking changes).
239
+ * Minor version bump → backwards-compatible (new events only).
240
+
241
+ Args:
242
+ installed_version: Schema version to check against. Defaults to
243
+ the current library schema version.
244
+
245
+ Returns:
246
+ List of ``(tool_name, required_version)`` pairs that are
247
+ incompatible. Empty list means everything is compatible.
248
+ """
249
+ try:
250
+ inst_major, inst_minor = _parse_version(installed_version)
251
+ except ValueError as exc:
252
+ raise ValueError(
253
+ f"installed_version must be MAJOR.MINOR format: {exc}"
254
+ ) from exc
255
+
256
+ incompatible: list[tuple[str, str]] = []
257
+ with self._lock:
258
+ for record in self._records:
259
+ req_major, req_minor = _parse_version(record.schema_version)
260
+ if req_major != inst_major or req_minor > inst_minor:
261
+ incompatible.append((record.tool_name, record.schema_version))
262
+ return incompatible
263
+
264
+ def assert_compatible(
265
+ self,
266
+ installed_version: str = _CURRENT_SCHEMA_VERSION,
267
+ ) -> None:
268
+ """Assert that all consumers are compatible with *installed_version*.
269
+
270
+ Args:
271
+ installed_version: Schema version to check against. Defaults to
272
+ the current library schema version.
273
+
274
+ Raises:
275
+ IncompatibleSchemaError: If any registered consumer is incompatible.
276
+ """
277
+ incompatible = self.check_compatible(installed_version)
278
+ if incompatible:
279
+ raise IncompatibleSchemaError(incompatible)
280
+
281
+ def clear(self) -> None:
282
+ """Remove all records from the registry (useful in tests).
283
+
284
+ .. warning::
285
+ Not safe to call from production code while other threads may be
286
+ registering consumers.
287
+ """
288
+ with self._lock:
289
+ self._records.clear()
290
+
291
+ def __len__(self) -> int:
292
+ with self._lock:
293
+ return len(self._records)
294
+
295
+ def __repr__(self) -> str: # pragma: no cover
296
+ return f"ConsumerRegistry(consumers={len(self)})"
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # Module-level singleton and helpers
301
+ # ---------------------------------------------------------------------------
302
+
303
+ _GLOBAL_REGISTRY = ConsumerRegistry()
304
+
305
+
306
+ def get_registry() -> ConsumerRegistry:
307
+ """Return the module-level :class:`ConsumerRegistry` singleton.
308
+
309
+ Returns:
310
+ The global :class:`ConsumerRegistry` instance.
311
+ """
312
+ return _GLOBAL_REGISTRY
313
+
314
+
315
+ def register_consumer(
316
+ tool_name: str,
317
+ *,
318
+ namespaces: Sequence[str],
319
+ schema_version: str,
320
+ contact: str | None = None,
321
+ metadata: dict[str, str] | None = None,
322
+ ) -> ConsumerRecord:
323
+ """Register a consumer in the global registry.
324
+
325
+ Convenience wrapper around :meth:`ConsumerRegistry.register` that operates
326
+ on the global singleton registry.
327
+
328
+ Args:
329
+ tool_name: Name of the consuming tool or service.
330
+ namespaces: Event namespaces required (e.g. ``["trace", "eval"]``).
331
+ schema_version: Minimum schema version required (``"MAJOR.MINOR"``).
332
+ contact: Optional contact info for compatibility escalations.
333
+ metadata: Optional freeform metadata dict.
334
+
335
+ Returns:
336
+ The created :class:`ConsumerRecord`.
337
+
338
+ Raises:
339
+ ValueError: See :meth:`ConsumerRegistry.register`.
340
+ """
341
+ return _GLOBAL_REGISTRY.register(
342
+ tool_name,
343
+ namespaces=namespaces,
344
+ schema_version=schema_version,
345
+ contact=contact,
346
+ metadata=metadata,
347
+ )
348
+
349
+
350
+ def assert_compatible(
351
+ installed_version: str = _CURRENT_SCHEMA_VERSION,
352
+ ) -> None:
353
+ """Assert all globally registered consumers are compatible with *installed_version*.
354
+
355
+ Convenience wrapper around :meth:`ConsumerRegistry.assert_compatible` that
356
+ operates on the global singleton registry.
357
+
358
+ Args:
359
+ installed_version: Schema version to check against. Defaults to the
360
+ current library schema version.
361
+
362
+ Raises:
363
+ IncompatibleSchemaError: If any registered consumer is incompatible.
364
+ """
365
+ _GLOBAL_REGISTRY.assert_compatible(installed_version)
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # Internal helpers
370
+ # ---------------------------------------------------------------------------
371
+
372
+
373
+ def _parse_version(version: str) -> tuple[int, int]:
374
+ """Parse a ``"MAJOR.MINOR"`` version string into ``(int, int)``."""
375
+ parts = version.split(".", 1)
376
+ try:
377
+ return int(parts[0]), int(parts[1])
378
+ except (IndexError, ValueError) as exc:
379
+ raise ValueError(f"Not a valid MAJOR.MINOR version: {version!r}") from exc
@@ -0,0 +1,5 @@
1
+ """spanforge.core — AI compliance platform core package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []