spanforge 1.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 (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
@@ -0,0 +1,144 @@
1
+ """spanforge.exporters.jsonl — Synchronous JSONL file exporter.
2
+
3
+ Appends one canonical JSON line per event to a file on disk. Zero external
4
+ dependencies (stdlib only).
5
+
6
+ Usage::
7
+
8
+ from spanforge import configure
9
+ configure(exporter="jsonl", endpoint="./events.jsonl")
10
+
11
+ # Now all tracer.span() / agent_run() / agent_step() calls write to
12
+ # events.jsonl automatically.
13
+
14
+ You can also instantiate directly for testing::
15
+
16
+ from spanforge.exporters.jsonl import SyncJSONLExporter
17
+ from spanforge.event import Event, EventType, Tags
18
+
19
+ exporter = SyncJSONLExporter("/tmp/test.jsonl") # noqa: S108 # NOSONAR
20
+ exporter.export(my_event)
21
+ exporter.close()
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import sys
27
+ import threading
28
+ from pathlib import Path
29
+ from typing import IO, TYPE_CHECKING, Literal, Union
30
+
31
+ if TYPE_CHECKING:
32
+ from spanforge.event import Event
33
+
34
+ __all__ = ["SyncJSONLExporter"]
35
+
36
+ _PathLike = Union[str, Path]
37
+
38
+
39
+ class SyncJSONLExporter:
40
+ """Synchronous exporter that appends events as newline-delimited JSON.
41
+
42
+ Thread-safe: a :class:`threading.Lock` serialises concurrent writes so
43
+ the output file is never corrupted when multiple threads share one
44
+ instance.
45
+
46
+ Args:
47
+ path: File path, :class:`pathlib.Path`, or ``"-"`` for stdout.
48
+ mode: File open mode — ``"a"`` (append, default) or ``"w"``
49
+ (overwrite / truncate on first write).
50
+ encoding: File encoding (default ``"utf-8"``).
51
+
52
+ Raises:
53
+ OSError: If the file cannot be opened or written.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ path: _PathLike | str = "spanforge_events.jsonl",
59
+ mode: str = "a",
60
+ encoding: str = "utf-8",
61
+ ) -> None:
62
+ if mode not in ("a", "w"):
63
+ raise ValueError("mode must be 'a' or 'w'")
64
+ self._path_str = str(path)
65
+ self._mode = mode
66
+ self._encoding = encoding
67
+ self._file: IO[str] | None = None
68
+ self._lock = threading.Lock()
69
+ self._closed = False
70
+
71
+ # ------------------------------------------------------------------
72
+ # Internal file management
73
+ # ------------------------------------------------------------------
74
+
75
+ def _ensure_open(self) -> IO[str]:
76
+ """Open the file lazily on first write."""
77
+ if self._file is not None and not self._file.closed:
78
+ return self._file
79
+ if self._path_str == "-":
80
+ self._file = sys.stdout
81
+ return self._file
82
+ # Create parent directories if they don't exist.
83
+ p = Path(self._path_str)
84
+ p.parent.mkdir(parents=True, exist_ok=True)
85
+ self._file = p.open(self._mode, encoding=self._encoding)
86
+ # After first open, always append.
87
+ self._mode = "a"
88
+ return self._file
89
+
90
+ # ------------------------------------------------------------------
91
+ # Public interface
92
+ # ------------------------------------------------------------------
93
+
94
+ def export(self, event: Event) -> None:
95
+ """Write *event* as a single JSON line.
96
+
97
+ Args:
98
+ event: A fully-formed :class:`~spanforge.event.Event` instance.
99
+
100
+ Raises:
101
+ RuntimeError: If :meth:`close` has already been called.
102
+ OSError: If the file write fails.
103
+ """
104
+ if self._closed:
105
+ raise RuntimeError("SyncJSONLExporter is closed")
106
+ line = event.to_json() + "\n"
107
+ with self._lock:
108
+ fh = self._ensure_open()
109
+ fh.write(line)
110
+ fh.flush()
111
+
112
+ def flush(self) -> None:
113
+ """Flush any buffered data to disk."""
114
+ with self._lock:
115
+ if self._file is not None and not self._file.closed:
116
+ self._file.flush()
117
+
118
+ def close(self) -> None:
119
+ """Flush and close the output file. Safe to call multiple times."""
120
+ with self._lock:
121
+ if not self._closed:
122
+ if self._file is not None and self._file is not sys.stdout:
123
+ try:
124
+ self._file.flush()
125
+ self._file.close()
126
+ except OSError:
127
+ pass
128
+ self._file = None
129
+ self._closed = True
130
+
131
+ # ------------------------------------------------------------------
132
+ # Context manager
133
+ # ------------------------------------------------------------------
134
+
135
+ def __enter__(self) -> SyncJSONLExporter:
136
+ return self
137
+
138
+ def __exit__(self, *_: object) -> Literal[False]:
139
+ self.close()
140
+ return False
141
+
142
+ def __repr__(self) -> str:
143
+ state = "closed" if self._closed else "open"
144
+ return f"SyncJSONLExporter(path={self._path_str!r}, state={state})"
@@ -0,0 +1,142 @@
1
+ """spanforge.exporters.sqlite — Synchronous SQLite exporter.
2
+
3
+ Persists events to a local SQLite database. Zero external dependencies
4
+ (stdlib ``sqlite3`` only). Suitable for development, staging, and solo
5
+ deployments where durable single-file storage is needed without standing up
6
+ Redis, Kafka, or a cloud collector.
7
+
8
+ Usage::
9
+
10
+ from spanforge import configure
11
+ configure(exporter="sqlite", endpoint="./spanforge.db")
12
+
13
+ # Events are now durable across process restarts.
14
+ # Query them with any SQLite client:
15
+ # sqlite3 spanforge.db "SELECT event_type, source, ts FROM events ORDER BY ts DESC LIMIT 10;"
16
+
17
+ You can also instantiate directly::
18
+
19
+ from spanforge.exporters.sqlite import SyncSQLiteExporter
20
+ exporter = SyncSQLiteExporter("./spanforge.db")
21
+ exporter.export(my_event)
22
+ exporter.close()
23
+
24
+ Schema
25
+ ------
26
+ Table ``events``:
27
+
28
+ * ``id`` INTEGER PRIMARY KEY AUTOINCREMENT
29
+ * ``event_id`` TEXT NOT NULL — ULID from :attr:`~spanforge.event.Event.event_id`
30
+ * ``event_type`` TEXT NOT NULL — e.g. ``"trace.span.completed"``
31
+ * ``source`` TEXT NOT NULL
32
+ * ``org_id`` TEXT
33
+ * ``trace_id`` TEXT
34
+ * ``span_id`` TEXT
35
+ * ``ts`` TEXT NOT NULL — ISO-8601 UTC timestamp
36
+ * ``payload`` TEXT NOT NULL — full canonical JSON of the event
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import sqlite3
42
+ import threading
43
+ from typing import TYPE_CHECKING
44
+
45
+ if TYPE_CHECKING:
46
+ from pathlib import Path
47
+
48
+ from spanforge.event import Event
49
+
50
+ __all__ = ["SyncSQLiteExporter"]
51
+
52
+ _CREATE_TABLE = """
53
+ CREATE TABLE IF NOT EXISTS events (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ event_id TEXT NOT NULL,
56
+ event_type TEXT NOT NULL,
57
+ source TEXT NOT NULL,
58
+ org_id TEXT,
59
+ trace_id TEXT,
60
+ span_id TEXT,
61
+ ts TEXT NOT NULL,
62
+ payload TEXT NOT NULL
63
+ );
64
+ CREATE INDEX IF NOT EXISTS idx_events_trace_id ON events (trace_id);
65
+ CREATE INDEX IF NOT EXISTS idx_events_event_type ON events (event_type);
66
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts);
67
+ """
68
+
69
+ _INSERT = """
70
+ INSERT INTO events (event_id, event_type, source, org_id, trace_id, span_id, ts, payload)
71
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
72
+ """
73
+
74
+
75
+ class SyncSQLiteExporter:
76
+ """Synchronous exporter that writes events to a SQLite database.
77
+
78
+ Thread-safe: a :class:`threading.Lock` serialises all writes.
79
+
80
+ Args:
81
+ path: Filesystem path for the SQLite database file. Defaults to
82
+ ``"spanforge_events.db"``. Use ``":memory:"`` for tests.
83
+
84
+ Raises:
85
+ sqlite3.Error: If the database cannot be opened or the schema
86
+ cannot be initialised.
87
+ """
88
+
89
+ def __init__(self, path: str | Path = "spanforge_events.db") -> None:
90
+ self._path = str(path)
91
+ self._lock = threading.Lock()
92
+ self._closed = False
93
+ self._conn: sqlite3.Connection = sqlite3.connect(
94
+ self._path,
95
+ check_same_thread=False,
96
+ )
97
+ self._conn.executescript(_CREATE_TABLE)
98
+ self._conn.commit()
99
+
100
+ # ------------------------------------------------------------------
101
+ # Public interface
102
+ # ------------------------------------------------------------------
103
+
104
+ def export(self, event: Event) -> None:
105
+ """Persist *event* to the SQLite database.
106
+
107
+ Args:
108
+ event: A fully-formed :class:`~spanforge.event.Event` instance.
109
+
110
+ Raises:
111
+ RuntimeError: If :meth:`close` has already been called.
112
+ sqlite3.Error: If the INSERT fails.
113
+ """
114
+ if self._closed:
115
+ raise RuntimeError("SyncSQLiteExporter is closed")
116
+ ts = str(getattr(event, "timestamp", "") or "")
117
+ row = (
118
+ str(event.event_id),
119
+ str(event.event_type.value if hasattr(event.event_type, "value") else event.event_type),
120
+ str(getattr(event, "source", "") or ""),
121
+ str(getattr(event, "org_id", "") or "") or None,
122
+ str(getattr(event, "trace_id", "") or "") or None,
123
+ str(getattr(event, "span_id", "") or "") or None,
124
+ ts,
125
+ event.to_json(),
126
+ )
127
+ with self._lock:
128
+ self._conn.execute(_INSERT, row)
129
+ self._conn.commit()
130
+
131
+ def flush(self) -> None:
132
+ """Flush — commits are immediate per-write, so this is a no-op."""
133
+
134
+ def close(self) -> None:
135
+ """Close the database connection. Safe to call multiple times."""
136
+ with self._lock:
137
+ if not self._closed:
138
+ self._closed = True
139
+ try:
140
+ self._conn.close()
141
+ except sqlite3.Error:
142
+ pass