dominus-sdk-python 2.15.0__tar.gz → 2.16.0__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 (54) hide show
  1. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/PKG-INFO +26 -6
  2. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/README.md +25 -5
  3. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/__init__.py +53 -2
  4. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/config/endpoints.py +2 -2
  5. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/cache.py +1 -1
  6. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/core.py +39 -18
  7. dominus_sdk_python-2.16.0/dominus/helpers/trace.py +97 -0
  8. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/ai.py +284 -20
  9. dominus_sdk_python-2.16.0/dominus/namespaces/artifacts.py +615 -0
  10. dominus_sdk_python-2.16.0/dominus/namespaces/courier.py +247 -0
  11. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/logs.py +13 -0
  12. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/redis.py +16 -16
  13. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/workflow.py +337 -69
  14. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/start.py +17 -5
  15. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus_sdk_python.egg-info/PKG-INFO +26 -6
  16. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus_sdk_python.egg-info/SOURCES.txt +4 -1
  17. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/pyproject.toml +1 -1
  18. dominus_sdk_python-2.16.0/tests/test_workflow_lifecycle.py +316 -0
  19. dominus_sdk_python-2.16.0/tests/test_workflow_refs.py +218 -0
  20. dominus_sdk_python-2.15.0/dominus/namespaces/artifacts.py +0 -150
  21. dominus_sdk_python-2.15.0/dominus/namespaces/courier.py +0 -251
  22. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/config/__init__.py +0 -0
  23. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/errors.py +0 -0
  24. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/__init__.py +0 -0
  25. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/auth.py +0 -0
  26. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/console_capture.py +0 -0
  27. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/crypto.py +0 -0
  28. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/helpers/sse.py +0 -0
  29. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/__init__.py +0 -0
  30. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/admin.py +0 -0
  31. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/auth.py +0 -0
  32. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/db.py +0 -0
  33. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/ddl.py +0 -0
  34. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/fastapi.py +0 -0
  35. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/files.py +0 -0
  36. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/health.py +0 -0
  37. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/jobs.py +0 -0
  38. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/open.py +0 -0
  39. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/__init__.py +0 -0
  40. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
  41. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  42. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/session.py +0 -0
  43. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/types.py +0 -0
  44. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
  45. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/portal.py +0 -0
  46. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/processor.py +0 -0
  47. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/secrets.py +0 -0
  48. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/secure.py +0 -0
  49. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/namespaces/sync.py +0 -0
  50. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus/services/__init__.py +0 -0
  51. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  52. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  53. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  54. {dominus_sdk_python-2.15.0 → dominus_sdk_python-2.16.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.15.0
3
+ Version: 2.16.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -49,7 +49,7 @@ Async Python SDK for CareBridge Dominus platform services. Routes calls through
49
49
  - Server-side, asyncio-first Python SDK (3.9+)
50
50
  - Namespace API with root-level shortcuts for common operations
51
51
  - Targets production Cloudflare Workers (gateway, JWT, logs) and Cloud Run (orchestrator)
52
- - Version: 2.12.0
52
+ - Version: 2.16.0
53
53
 
54
54
  ## Quick Start
55
55
 
@@ -89,8 +89,20 @@ async for chunk in dominus.ai.stream_agent(
89
89
  ):
90
90
  print(chunk.get("content", ""), end="", flush=True)
91
91
 
92
- # Workflow execution
93
- result = await dominus.workflow.execute(workflow_id, context={"key": "value"})
92
+ # Saved workflow lifecycle through workflow-manager
93
+ run = await dominus.workflow.create_run(
94
+ workflow_id="wf_saved_123",
95
+ context={"report_ref": {"report_id": "r-1", "accession": "a-1", "session_number": "2"}},
96
+ )
97
+ await dominus.workflow.validate_run(run["run_id"])
98
+ result = await dominus.workflow.start_run(run["run_id"])
99
+
100
+ # Raw orchestration lifecycle with inline workflow definition
101
+ raw_run = await dominus.ai.workflow.create_run(
102
+ workflow_definition={"workflow": {"name": "raw"}, "agents": {}, "artifacts": {}},
103
+ )
104
+ await dominus.ai.workflow.validate_run(raw_run["run_id"])
105
+ raw_result = await dominus.ai.workflow.start_run(raw_run["run_id"])
94
106
  ```
95
107
 
96
108
  ## Architecture (SDK View)
@@ -116,7 +128,7 @@ DOMINUS_TOKEN (PSK) --> Gateway /jwt/mint --> JWT cached (14min TTL)
116
128
  | `secrets` | Warden | Secrets CRUD |
117
129
  | `db` | Scribe | Database CRUD with optional secure-table audit |
118
130
  | `secure` / `db_secure` | Scribe | Audit-logged data access (requires reason/actor) |
119
- | `redis` | Whisperer | Cache, locks, counters, hashes |
131
+ | `redis` | Redis Worker | Cache, locks, counters, hashes |
120
132
  | `files` | Archivist | Object storage with compliance mode |
121
133
  | `auth` | Guardian | Users, roles, scopes, tenants, clients, pages, nav, subtypes, secure tables (147 methods) |
122
134
  | `ddl` | Smith | Schema DDL, migrations, tenant provisioning, schema builder |
@@ -127,7 +139,7 @@ DOMINUS_TOKEN (PSK) --> Gateway /jwt/mint --> JWT cached (14min TTL)
127
139
  | `health` | Orchestrator | Health/ping/warmup |
128
140
  | `admin` | Admin | Reseed/reset admin category |
129
141
  | `ai` | Agent Runtime | Agent execution, LLM completions, RAG, artifacts, results, orchestration, STT/TTS |
130
- | `workflow` | Workflow Manager | Workflow CRUD, categories/pipelines, templates, execution |
142
+ | `workflow` | Workflow Manager | Workflow CRUD, pipelines/templates, saved-workflow run lifecycle, native pipeline runner |
131
143
  | `oracle` / `stt` | Oracle | Real-time streaming STT with VAD (WebSocket, requires `oracle` extras) |
132
144
  | `fastapi` | Local | FastAPI route auth decorators (`@jwt`, `@psk`, `@scopes`) |
133
145
 
@@ -173,6 +185,14 @@ dominus.release_console() # stop capturing
173
185
  - [Architecture](docs/architecture.md) -- Request flow, resilience, JWT verification
174
186
  - [Services Reference](docs/services.md) -- Complete namespace and method inventory
175
187
  - [Development](docs/development.md) -- Setup, testing, adding APIs
188
+ - [Workflow hard-cut release notes](docs/workflow-hard-cut-release.md) -- cutover notes and operational checks
189
+
190
+ ## Workflow Surfaces
191
+
192
+ - `dominus.workflow.*` is the saved-workflow facade through `dominus-workflow-manager`. Use it for stored workflow IDs and the artifact-aware run lifecycle: `create_run -> register_artifact -> validate_run -> start_run`.
193
+ - `dominus.ai.workflow.*` is the raw orchestration surface. It requires inline `workflow_definition` data. It does not support saved-workflow IDs directly.
194
+ - `dominus.workflow.execute_pipeline()` runs stored pipelines through workflow-manager's native orchestration-backed runner.
195
+ - `dominus.artifacts.*` now supports addressed V2 artifact refs alongside legacy helpers. Prefer `store_v2()`, `head_v2()`, `compare_v2()`, bookmark helpers, and watcher helpers with canonical `ar://{group}/{owner}/{environment}/{kind}/{artifact_key}` refs for new code; `target_project_id` / `use_shared` remain compatibility-only targeting concepts.
176
196
 
177
197
  ## License
178
198
 
@@ -7,7 +7,7 @@ Async Python SDK for CareBridge Dominus platform services. Routes calls through
7
7
  - Server-side, asyncio-first Python SDK (3.9+)
8
8
  - Namespace API with root-level shortcuts for common operations
9
9
  - Targets production Cloudflare Workers (gateway, JWT, logs) and Cloud Run (orchestrator)
10
- - Version: 2.12.0
10
+ - Version: 2.16.0
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -47,8 +47,20 @@ async for chunk in dominus.ai.stream_agent(
47
47
  ):
48
48
  print(chunk.get("content", ""), end="", flush=True)
49
49
 
50
- # Workflow execution
51
- result = await dominus.workflow.execute(workflow_id, context={"key": "value"})
50
+ # Saved workflow lifecycle through workflow-manager
51
+ run = await dominus.workflow.create_run(
52
+ workflow_id="wf_saved_123",
53
+ context={"report_ref": {"report_id": "r-1", "accession": "a-1", "session_number": "2"}},
54
+ )
55
+ await dominus.workflow.validate_run(run["run_id"])
56
+ result = await dominus.workflow.start_run(run["run_id"])
57
+
58
+ # Raw orchestration lifecycle with inline workflow definition
59
+ raw_run = await dominus.ai.workflow.create_run(
60
+ workflow_definition={"workflow": {"name": "raw"}, "agents": {}, "artifacts": {}},
61
+ )
62
+ await dominus.ai.workflow.validate_run(raw_run["run_id"])
63
+ raw_result = await dominus.ai.workflow.start_run(raw_run["run_id"])
52
64
  ```
53
65
 
54
66
  ## Architecture (SDK View)
@@ -74,7 +86,7 @@ DOMINUS_TOKEN (PSK) --> Gateway /jwt/mint --> JWT cached (14min TTL)
74
86
  | `secrets` | Warden | Secrets CRUD |
75
87
  | `db` | Scribe | Database CRUD with optional secure-table audit |
76
88
  | `secure` / `db_secure` | Scribe | Audit-logged data access (requires reason/actor) |
77
- | `redis` | Whisperer | Cache, locks, counters, hashes |
89
+ | `redis` | Redis Worker | Cache, locks, counters, hashes |
78
90
  | `files` | Archivist | Object storage with compliance mode |
79
91
  | `auth` | Guardian | Users, roles, scopes, tenants, clients, pages, nav, subtypes, secure tables (147 methods) |
80
92
  | `ddl` | Smith | Schema DDL, migrations, tenant provisioning, schema builder |
@@ -85,7 +97,7 @@ DOMINUS_TOKEN (PSK) --> Gateway /jwt/mint --> JWT cached (14min TTL)
85
97
  | `health` | Orchestrator | Health/ping/warmup |
86
98
  | `admin` | Admin | Reseed/reset admin category |
87
99
  | `ai` | Agent Runtime | Agent execution, LLM completions, RAG, artifacts, results, orchestration, STT/TTS |
88
- | `workflow` | Workflow Manager | Workflow CRUD, categories/pipelines, templates, execution |
100
+ | `workflow` | Workflow Manager | Workflow CRUD, pipelines/templates, saved-workflow run lifecycle, native pipeline runner |
89
101
  | `oracle` / `stt` | Oracle | Real-time streaming STT with VAD (WebSocket, requires `oracle` extras) |
90
102
  | `fastapi` | Local | FastAPI route auth decorators (`@jwt`, `@psk`, `@scopes`) |
91
103
 
@@ -131,6 +143,14 @@ dominus.release_console() # stop capturing
131
143
  - [Architecture](docs/architecture.md) -- Request flow, resilience, JWT verification
132
144
  - [Services Reference](docs/services.md) -- Complete namespace and method inventory
133
145
  - [Development](docs/development.md) -- Setup, testing, adding APIs
146
+ - [Workflow hard-cut release notes](docs/workflow-hard-cut-release.md) -- cutover notes and operational checks
147
+
148
+ ## Workflow Surfaces
149
+
150
+ - `dominus.workflow.*` is the saved-workflow facade through `dominus-workflow-manager`. Use it for stored workflow IDs and the artifact-aware run lifecycle: `create_run -> register_artifact -> validate_run -> start_run`.
151
+ - `dominus.ai.workflow.*` is the raw orchestration surface. It requires inline `workflow_definition` data. It does not support saved-workflow IDs directly.
152
+ - `dominus.workflow.execute_pipeline()` runs stored pipelines through workflow-manager's native orchestration-backed runner.
153
+ - `dominus.artifacts.*` now supports addressed V2 artifact refs alongside legacy helpers. Prefer `store_v2()`, `head_v2()`, `compare_v2()`, bookmark helpers, and watcher helpers with canonical `ar://{group}/{owner}/{environment}/{kind}/{artifact_key}` refs for new code; `target_project_id` / `use_shared` remain compatibility-only targeting concepts.
134
154
 
135
155
  ## License
136
156
 
@@ -101,7 +101,25 @@ from .namespaces.health import HealthNamespace
101
101
  from .namespaces.open import OpenNamespace
102
102
 
103
103
  # Export new namespaces (Node.js SDK parity)
104
- from .namespaces.artifacts import ArtifactsNamespace
104
+ from .namespaces.artifacts import (
105
+ ARTIFACT_REF_PREFIX,
106
+ DISPLAY_REF_PREFIX,
107
+ ARTIFACT_ENVIRONMENTS,
108
+ ArtifactsNamespace,
109
+ build_artifact_ref,
110
+ build_v2_artifact_ref,
111
+ build_pinned_artifact_ref,
112
+ build_legacy_artifact_ref,
113
+ build_display_artifact_ref,
114
+ parse_artifact_ref,
115
+ try_parse_artifact_ref,
116
+ validate_artifact_address,
117
+ is_artifact_ref,
118
+ is_pinned_artifact_ref,
119
+ is_head_artifact_ref,
120
+ is_legacy_artifact_ref,
121
+ is_display_artifact_ref,
122
+ )
105
123
  from .namespaces.jobs import JobsNamespace
106
124
  from .namespaces.processor import ProcessorNamespace
107
125
  from .namespaces.sync import SyncNamespace
@@ -138,6 +156,16 @@ from .helpers.core import (
138
156
  is_jwt_valid,
139
157
  )
140
158
 
159
+ # Export trace utilities for distributed tracing
160
+ from .helpers.trace import (
161
+ generate_trace_id,
162
+ get_current_trace_id,
163
+ get_current_span_id,
164
+ start_trace,
165
+ end_trace,
166
+ with_trace,
167
+ )
168
+
141
169
  # Export error classes
142
170
  from .errors import (
143
171
  DominusError,
@@ -152,7 +180,7 @@ from .errors import (
152
180
  TimeoutError as DominusTimeoutError,
153
181
  )
154
182
 
155
- __version__ = "2.15.0"
183
+ __version__ = "2.16.0"
156
184
  __all__ = [
157
185
  # Main SDK instance
158
186
  "dominus",
@@ -182,7 +210,23 @@ __all__ = [
182
210
  "HealthNamespace",
183
211
  "OpenNamespace",
184
212
  # New namespaces (Node.js SDK parity)
213
+ "ARTIFACT_REF_PREFIX",
214
+ "DISPLAY_REF_PREFIX",
215
+ "ARTIFACT_ENVIRONMENTS",
185
216
  "ArtifactsNamespace",
217
+ "build_artifact_ref",
218
+ "build_v2_artifact_ref",
219
+ "build_pinned_artifact_ref",
220
+ "build_legacy_artifact_ref",
221
+ "build_display_artifact_ref",
222
+ "parse_artifact_ref",
223
+ "try_parse_artifact_ref",
224
+ "validate_artifact_address",
225
+ "is_artifact_ref",
226
+ "is_pinned_artifact_ref",
227
+ "is_head_artifact_ref",
228
+ "is_legacy_artifact_ref",
229
+ "is_display_artifact_ref",
186
230
  "JobsNamespace",
187
231
  "ProcessorNamespace",
188
232
  "SyncNamespace",
@@ -206,6 +250,13 @@ __all__ = [
206
250
  # JWT verification utilities
207
251
  "verify_jwt_locally",
208
252
  "is_jwt_valid",
253
+ # Trace utilities
254
+ "generate_trace_id",
255
+ "get_current_trace_id",
256
+ "get_current_span_id",
257
+ "start_trace",
258
+ "end_trace",
259
+ "with_trace",
209
260
  # Error classes
210
261
  "DominusError",
211
262
  "AuthenticationError",
@@ -37,7 +37,7 @@ LOGS_URL = os.environ.get("DOMINUS_LOGS_URL", _DEFAULT_LOGS_URL)
37
37
  BASE_URL = os.environ.get("DOMINUS_BASE_URL", _DEFAULT_BASE_URL)
38
38
 
39
39
  # Legacy aliases (all point to orchestrator now) - DEPRECATED
40
- SOVEREIGN_URL = BASE_URL
40
+ SOVEREIGN_URL = BASE_URL # Deprecated: use BASE_URL or get_gateway_url()
41
41
  ARCHITECT_URL = BASE_URL
42
42
  ORCHESTRATOR_URL = BASE_URL
43
43
  WARDEN_URL = BASE_URL
@@ -107,7 +107,7 @@ def get_base_url() -> str:
107
107
 
108
108
  # DEPRECATED - use get_base_url()
109
109
  def get_sovereign_url(environment: str = None) -> str:
110
- """Deprecated: Use get_base_url() instead."""
110
+ """Deprecated: Use get_base_url() instead. 'Sovereign' is the legacy name for the gateway."""
111
111
  return BASE_URL
112
112
 
113
113
 
@@ -197,4 +197,4 @@ orchestrator_circuit_breaker = CircuitBreaker(
197
197
  )
198
198
 
199
199
  # Backward compatibility alias
200
- sovereign_circuit_breaker = orchestrator_circuit_breaker
200
+ gateway_circuit_breaker = orchestrator_circuit_breaker
@@ -11,7 +11,7 @@ import json
11
11
  import time
12
12
  from typing import Any, Optional
13
13
 
14
- from .cache import dominus_cache, sovereign_circuit_breaker, exponential_backoff_with_jitter
14
+ from .cache import dominus_cache, gateway_circuit_breaker, exponential_backoff_with_jitter
15
15
 
16
16
  # Max retries for HTTP requests (reduced from implicit to explicit)
17
17
  MAX_RETRIES = 3
@@ -112,8 +112,13 @@ async def _fetch_jwks() -> dict:
112
112
  proxy_config = get_proxy_config()
113
113
 
114
114
  try:
115
+ from .trace import get_current_trace_id, get_current_span_id
116
+ jwks_headers = {
117
+ "X-Trace-Id": get_current_trace_id(),
118
+ "X-Parent-Span-Id": get_current_span_id(),
119
+ }
115
120
  async with httpx.AsyncClient(timeout=10.0, proxy=proxy_config) as client:
116
- response = await client.get(f"{gateway_url}/jwt/jwks")
121
+ response = await client.get(f"{gateway_url}/jwt/jwks", headers=jwks_headers)
117
122
  response.raise_for_status()
118
123
 
119
124
  # Response is base64 encoded
@@ -339,18 +344,21 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
339
344
  proxy_config = get_proxy_config()
340
345
 
341
346
  # Circuit breaker check
342
- if not sovereign_circuit_breaker.can_execute():
347
+ if not gateway_circuit_breaker.can_execute():
343
348
  raise RuntimeError(
344
349
  f"Circuit breaker OPEN for auth - "
345
350
  f"too many recent failures. Will retry after recovery timeout."
346
351
  )
347
352
 
348
- if sovereign_circuit_breaker.state == sovereign_circuit_breaker.HALF_OPEN:
349
- sovereign_circuit_breaker.record_half_open_call()
353
+ if gateway_circuit_breaker.state == gateway_circuit_breaker.HALF_OPEN:
354
+ gateway_circuit_breaker.record_half_open_call()
350
355
 
356
+ from .trace import get_current_trace_id, get_current_span_id
351
357
  headers = {
352
358
  "Authorization": f"Bearer {psk_token}",
353
- "Content-Type": "text/plain"
359
+ "Content-Type": "text/plain",
360
+ "X-Trace-Id": get_current_trace_id(),
361
+ "X-Parent-Span-Id": get_current_span_id(),
354
362
  }
355
363
 
356
364
  # Body format for gateway /jwt/mint endpoint
@@ -384,17 +392,17 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
384
392
 
385
393
  if not result.get("success"):
386
394
  error_msg = result.get("error", "Unknown auth error")
387
- sovereign_circuit_breaker.record_failure()
395
+ gateway_circuit_breaker.record_failure()
388
396
  raise RuntimeError(f"Auth error: {error_msg}")
389
397
 
390
398
  data = result.get("data", {})
391
399
  jwt = data.get("access_token") or data.get("token")
392
400
  if not jwt:
393
- sovereign_circuit_breaker.record_failure()
401
+ gateway_circuit_breaker.record_failure()
394
402
  raise RuntimeError("No JWT token in auth response")
395
403
 
396
404
  # Success - record it
397
- sovereign_circuit_breaker.record_success()
405
+ gateway_circuit_breaker.record_success()
398
406
  return jwt
399
407
 
400
408
  except httpx.TimeoutException as e:
@@ -407,7 +415,7 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
407
415
  )
408
416
  await asyncio.sleep(delay)
409
417
  continue
410
- sovereign_circuit_breaker.record_failure()
418
+ gateway_circuit_breaker.record_failure()
411
419
  raise RuntimeError(f"Failed to get JWT: {e}") from e
412
420
 
413
421
  except httpx.NetworkError as e:
@@ -420,21 +428,21 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
420
428
  )
421
429
  await asyncio.sleep(delay)
422
430
  continue
423
- sovereign_circuit_breaker.record_failure()
431
+ gateway_circuit_breaker.record_failure()
424
432
  raise RuntimeError(f"Failed to get JWT: {e}") from e
425
433
 
426
434
  except httpx.HTTPStatusError as e:
427
435
  last_error = e
428
436
  # 4xx errors (except 401 which is retried above) should not be retried
429
437
  if 400 <= e.response.status_code < 500 and e.response.status_code != 401:
430
- sovereign_circuit_breaker.record_failure()
438
+ gateway_circuit_breaker.record_failure()
431
439
  raise RuntimeError(f"Failed to get JWT: {e}") from e
432
440
  # For other errors, we've already handled retries in the status check above
433
- sovereign_circuit_breaker.record_failure()
441
+ gateway_circuit_breaker.record_failure()
434
442
  raise RuntimeError(f"Failed to get JWT: {e}") from e
435
443
 
436
444
  # Should not reach here, but just in case
437
- sovereign_circuit_breaker.record_failure()
445
+ gateway_circuit_breaker.record_failure()
438
446
  raise RuntimeError(f"Failed to get JWT after {JWT_MINT_RETRIES} retries: {last_error}")
439
447
 
440
448
 
@@ -541,9 +549,12 @@ async def delegate_jwt(
541
549
  gateway_url = get_gateway_url()
542
550
  proxy_config = get_proxy_config()
543
551
 
552
+ from .trace import get_current_trace_id, get_current_span_id
544
553
  headers = {
545
554
  "Authorization": f"Bearer {psk_token}",
546
- "Content-Type": "text/plain"
555
+ "Content-Type": "text/plain",
556
+ "X-Trace-Id": get_current_trace_id(),
557
+ "X-Parent-Span-Id": get_current_span_id(),
547
558
  }
548
559
 
549
560
  # Body format for /jwt/delegate endpoint
@@ -704,7 +715,11 @@ async def execute_with_retry(
704
715
  for attempt in range(MAX_RETRIES):
705
716
  try:
706
717
  # Prepare headers
707
- headers = {}
718
+ from .trace import get_current_trace_id, get_current_span_id
719
+ headers = {
720
+ "X-Trace-Id": get_current_trace_id(),
721
+ "X-Parent-Span-Id": get_current_span_id(),
722
+ }
708
723
  if requires_auth:
709
724
  headers["Authorization"] = f"Bearer {_b64_token(token)}"
710
725
 
@@ -827,9 +842,12 @@ async def execute_bridge_call(
827
842
  jwt = await _ensure_valid_jwt(token, base_url)
828
843
 
829
844
  # Prepare Bridge API request with JWT
845
+ from .trace import get_current_trace_id, get_current_span_id
830
846
  headers = {
831
847
  "Authorization": f"Bearer {jwt}",
832
- "Content-Type": "text/plain"
848
+ "Content-Type": "text/plain",
849
+ "X-Trace-Id": get_current_trace_id(),
850
+ "X-Parent-Span-Id": get_current_span_id(),
833
851
  }
834
852
 
835
853
  # Body format: {"method": "...", "params": {...}}
@@ -972,8 +990,11 @@ async def _execute_auth_call(
972
990
  gateway_url = get_gateway_url()
973
991
  proxy_config = get_proxy_config()
974
992
 
993
+ from .trace import get_current_trace_id, get_current_span_id
975
994
  headers = {
976
- "Content-Type": "text/plain"
995
+ "Content-Type": "text/plain",
996
+ "X-Trace-Id": get_current_trace_id(),
997
+ "X-Parent-Span-Id": get_current_span_id(),
977
998
  }
978
999
 
979
1000
  # Add PSK header only if token provided (auth.jwks is public)
@@ -0,0 +1,97 @@
1
+ """
2
+ Distributed Tracing Module for Dominus Python SDK.
3
+
4
+ Provides contextvars-based trace context propagation.
5
+ Every SDK HTTP request automatically includes X-Trace-Id and X-Parent-Span-Id headers.
6
+
7
+ Usage patterns:
8
+
9
+ # Auto mode (default) — each request gets a unique trace_id
10
+ await dominus.db.query("users")
11
+
12
+ # Manual grouping — all requests within scope share one trace_id
13
+ async with with_trace() as trace_id:
14
+ await dominus.db.query("users")
15
+ await dominus.logs.info("queried users")
16
+
17
+ # Imperative scope
18
+ trace_id = start_trace()
19
+ await dominus.db.query("users")
20
+ end_trace()
21
+ """
22
+
23
+ import uuid
24
+ from contextvars import ContextVar
25
+ from contextlib import asynccontextmanager
26
+ from typing import Optional
27
+
28
+
29
+ # ContextVar for trace ID — safe for concurrent async tasks
30
+ _active_trace_id: ContextVar[Optional[str]] = ContextVar("_active_trace_id", default=None)
31
+
32
+
33
+ def generate_trace_id() -> str:
34
+ """Generate a new trace ID (UUID v4)."""
35
+ return str(uuid.uuid4())
36
+
37
+
38
+ def get_current_trace_id() -> str:
39
+ """Get the current trace ID from context.
40
+
41
+ - If inside a ``with_trace()`` or ``start_trace()`` scope, returns the scoped trace_id.
42
+ - Otherwise, generates a new unique trace_id per call (auto mode).
43
+ """
44
+ return _active_trace_id.get() or generate_trace_id()
45
+
46
+
47
+ def get_current_span_id() -> str:
48
+ """Generate a per-call span ID for parent-child tracking.
49
+
50
+ Each SDK HTTP call is a sub-span. The span_id generated here
51
+ is sent as X-Parent-Span-Id so the callee service knows its parent span.
52
+ """
53
+ return str(uuid.uuid4())
54
+
55
+
56
+ def start_trace() -> str:
57
+ """Start a manual trace scope.
58
+
59
+ Sets the trace_id in the current context. All SDK requests
60
+ in this context will share the same trace_id.
61
+
62
+ Returns:
63
+ The new trace_id
64
+ """
65
+ trace_id = generate_trace_id()
66
+ _active_trace_id.set(trace_id)
67
+ return trace_id
68
+
69
+
70
+ def end_trace() -> None:
71
+ """End the current manual trace scope.
72
+
73
+ Clears the trace_id from context so subsequent calls
74
+ revert to auto-mode (unique trace_id per call).
75
+ """
76
+ _active_trace_id.set(None)
77
+
78
+
79
+ @asynccontextmanager
80
+ async def with_trace():
81
+ """Async context manager for trace grouping.
82
+
83
+ All SDK requests within the ``async with`` block share one trace_id.
84
+ Safe for concurrent async tasks — uses contextvars.
85
+
86
+ Usage::
87
+
88
+ async with with_trace() as trace_id:
89
+ await dominus.db.query("users")
90
+ await dominus.logs.info("queried users")
91
+ """
92
+ trace_id = generate_trace_id()
93
+ token = _active_trace_id.set(trace_id)
94
+ try:
95
+ yield trace_id
96
+ finally:
97
+ _active_trace_id.reset(token)