dominus-sdk-python 2.18.0__tar.gz → 3.0.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.18.0 → dominus_sdk_python-3.0.0}/PKG-INFO +9 -2
  2. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/README.md +8 -1
  3. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/__init__.py +3 -1
  4. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/core.py +36 -15
  5. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/artifacts.py +84 -60
  6. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/logs.py +35 -6
  7. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/workflow.py +43 -101
  8. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus_sdk_python.egg-info/PKG-INFO +9 -2
  9. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus_sdk_python.egg-info/SOURCES.txt +2 -0
  10. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/pyproject.toml +1 -1
  11. dominus_sdk_python-3.0.0/tests/test_logs.py +65 -0
  12. dominus_sdk_python-3.0.0/tests/test_public_exports.py +11 -0
  13. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/tests/test_workflow_lifecycle.py +29 -89
  14. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/tests/test_workflow_refs.py +12 -10
  15. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/config/__init__.py +0 -0
  16. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/config/endpoints.py +0 -0
  17. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/errors.py +0 -0
  18. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/__init__.py +0 -0
  19. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/auth.py +0 -0
  20. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/cache.py +0 -0
  21. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/console_capture.py +0 -0
  22. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/crypto.py +0 -0
  23. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/sse.py +0 -0
  24. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/helpers/trace.py +0 -0
  25. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/__init__.py +0 -0
  26. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/admin.py +0 -0
  27. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/ai.py +0 -0
  28. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/auth.py +0 -0
  29. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/courier.py +0 -0
  30. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/db.py +0 -0
  31. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/ddl.py +0 -0
  32. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/fastapi.py +0 -0
  33. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/files.py +0 -0
  34. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/health.py +0 -0
  35. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/jobs.py +0 -0
  36. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/open.py +0 -0
  37. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/__init__.py +0 -0
  38. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
  39. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  40. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/session.py +0 -0
  41. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/types.py +0 -0
  42. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
  43. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/portal.py +0 -0
  44. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/processor.py +0 -0
  45. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/redis.py +0 -0
  46. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/secrets.py +0 -0
  47. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/secure.py +0 -0
  48. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/namespaces/sync.py +0 -0
  49. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/services/__init__.py +0 -0
  50. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus/start.py +0 -0
  51. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  52. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  53. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  54. {dominus_sdk_python-2.18.0 → dominus_sdk_python-3.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.18.0
3
+ Version: 3.0.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.18.0
52
+ - Version: 3.0.0
53
53
 
54
54
  ## Quick Start
55
55
 
@@ -195,6 +195,13 @@ dominus.release_console() # stop capturing
195
195
  - [Development](docs/development.md) -- Setup, testing, adding APIs
196
196
  - [Workflow hard-cut release notes](docs/workflow-hard-cut-release.md) -- cutover notes and operational checks
197
197
 
198
+ ## Verification (local)
199
+
200
+ ```bash
201
+ pip install -e .
202
+ python -m pytest tests -q
203
+ ```
204
+
198
205
  ## Workflow Surfaces
199
206
 
200
207
  - `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`.
@@ -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.18.0
10
+ - Version: 3.0.0
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -153,6 +153,13 @@ dominus.release_console() # stop capturing
153
153
  - [Development](docs/development.md) -- Setup, testing, adding APIs
154
154
  - [Workflow hard-cut release notes](docs/workflow-hard-cut-release.md) -- cutover notes and operational checks
155
155
 
156
+ ## Verification (local)
157
+
158
+ ```bash
159
+ pip install -e .
160
+ python -m pytest tests -q
161
+ ```
162
+
156
163
  ## Workflow Surfaces
157
164
 
158
165
  - `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`.
@@ -154,6 +154,7 @@ from .helpers.cache import (
154
154
  from .helpers.core import (
155
155
  verify_jwt_locally,
156
156
  is_jwt_valid,
157
+ mint_selected_project_jwt,
157
158
  )
158
159
 
159
160
  # Export trace utilities for distributed tracing
@@ -180,7 +181,7 @@ from .errors import (
180
181
  TimeoutError as DominusTimeoutError,
181
182
  )
182
183
 
183
- __version__ = "2.18.0"
184
+ __version__ = "3.0.0"
184
185
  __all__ = [
185
186
  # Main SDK instance
186
187
  "dominus",
@@ -250,6 +251,7 @@ __all__ = [
250
251
  # JWT verification utilities
251
252
  "verify_jwt_locally",
252
253
  "is_jwt_valid",
254
+ "mint_selected_project_jwt",
253
255
  # Trace utilities
254
256
  "generate_trace_id",
255
257
  "get_current_trace_id",
@@ -521,29 +521,31 @@ async def _ensure_valid_jwt(psk_token: str, sovereign_url: str) -> str:
521
521
  return jwt
522
522
 
523
523
 
524
- async def delegate_jwt(
524
+ async def mint_selected_project_jwt(
525
525
  psk_token: str,
526
526
  target_project_id: str,
527
527
  target_environment: str,
528
528
  use_shared: bool = False
529
529
  ) -> str:
530
530
  """
531
- Mint a delegated JWT for a target project.
531
+ Mint a selected-project JWT for a target project.
532
532
 
533
- This is used by admin-level services (like MCP) to get JWTs
534
- that allow operations on behalf of other projects.
533
+ This is used by admin-level services to act within a selected project
534
+ context. The compatibility route name remains `/jwt/delegate`, but
535
+ the canonical contract is selected-project targeting, not a separate
536
+ delegated token family.
535
537
 
536
538
  Args:
537
539
  psk_token: Admin PSK token (DOMINUS_TOKEN for admin project)
538
540
  target_project_id: UUID of the target project
539
541
  target_environment: Environment (development, staging, production)
540
- use_shared: If True, includes shared_project_id in claims
542
+ use_shared: Deprecated compatibility flag; selected-project JWTs infer shared context from the target project
541
543
 
542
544
  Returns:
543
- Delegated JWT token string with system=delegated
545
+ Selected-project JWT string
544
546
 
545
547
  Raises:
546
- RuntimeError: If delegation fails
548
+ RuntimeError: If selected-project minting fails
547
549
  """
548
550
  from ..config.endpoints import get_gateway_url, get_proxy_config
549
551
  gateway_url = get_gateway_url()
@@ -557,13 +559,11 @@ async def delegate_jwt(
557
559
  "X-Parent-Span-Id": get_current_span_id(),
558
560
  }
559
561
 
560
- # Body format for /jwt/delegate endpoint
561
562
  body_json = {
562
563
  "target_project_id": target_project_id,
563
564
  "target_environment": target_environment
564
565
  }
565
- if use_shared:
566
- body_json["use_shared"] = True
566
+ _ = use_shared
567
567
 
568
568
  body_b64 = _b64_encode(body_json)
569
569
 
@@ -576,13 +576,13 @@ async def delegate_jwt(
576
576
  result = _b64_decode(response.text)
577
577
 
578
578
  if not result.get("success"):
579
- error_msg = result.get("error", "Unknown delegation error")
580
- raise RuntimeError(f"Delegation error: {error_msg}")
579
+ error_msg = result.get("error", "Unknown selected-project mint error")
580
+ raise RuntimeError(f"Selected-project mint error: {error_msg}")
581
581
 
582
582
  data = result.get("data", {})
583
583
  jwt = data.get("token") or data.get("access_token")
584
584
  if not jwt:
585
- raise RuntimeError("No JWT token in delegation response")
585
+ raise RuntimeError("No JWT token in selected-project mint response")
586
586
 
587
587
  return jwt
588
588
 
@@ -594,10 +594,31 @@ async def delegate_jwt(
594
594
  error_detail = error_body.get("error") or str(error_body)
595
595
  except Exception:
596
596
  error_detail = e.response.text[:200] if e.response.text else str(e)
597
- raise RuntimeError(f"Delegation failed: {error_detail}") from e
597
+ raise RuntimeError(f"Selected-project JWT mint failed: {error_detail}") from e
598
598
 
599
599
  except Exception as e:
600
- raise RuntimeError(f"Delegation failed: {e}") from e
600
+ raise RuntimeError(f"Selected-project JWT mint failed: {e}") from e
601
+
602
+
603
+ async def delegate_jwt(
604
+ psk_token: str,
605
+ target_project_id: str,
606
+ target_environment: str,
607
+ use_shared: bool = False
608
+ ) -> str:
609
+ """
610
+ Compatibility alias for :func:`mint_selected_project_jwt`.
611
+
612
+ .. deprecated::
613
+ Prefer :func:`mint_selected_project_jwt` for new code; "delegated" naming is
614
+ legacy terminology only.
615
+ """
616
+ return await mint_selected_project_jwt(
617
+ psk_token=psk_token,
618
+ target_project_id=target_project_id,
619
+ target_environment=target_environment,
620
+ use_shared=use_shared,
621
+ )
601
622
 
602
623
 
603
624
  def verify_token_format(token: str) -> bool:
@@ -4,10 +4,14 @@ Artifacts Namespace - Temporary artifact storage.
4
4
  Provides auto-tiered artifact storage (Redis < 1MB, B2 >= 1MB)
5
5
  with TTL-based expiration. Routes through gateway to artifact-worker.
6
6
 
7
+ The artifact worker returns HTTP 410 for legacy paths (/store, /retrieve, /list, /delete).
8
+ This SDK only calls /api/artifact/v2/*; convenience ``store``/``retrieve``/``delete`` map
9
+ those shapes to V2 compatibility addressing (see ``store_v2`` for canonical addressed writes).
10
+
7
11
  Usage:
8
12
  from dominus import dominus
9
13
 
10
- # Store
14
+ # Prefer store_v2 / addressed refs for new code; legacy-shaped store maps to V2 internally
11
15
  result = await dominus.artifacts.store(data="base64data...", ttl_seconds=7200)
12
16
 
13
17
  # Retrieve
@@ -271,6 +275,21 @@ class ArtifactsNamespace:
271
275
  def __init__(self, client: "Dominus"):
272
276
  self._client = client
273
277
 
278
+ async def _legacy_project_context(self) -> tuple[str, str]:
279
+ """project_id and env from the cached service JWT (for V1-shaped -> V2 compat calls)."""
280
+ from ..helpers.core import _ensure_valid_jwt
281
+ from ..start import _BASE_URL, _TOKEN
282
+
283
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
284
+ claims = await self._client.validate_jwt(jwt)
285
+ pid = _safe_string(claims.get("project_id"))
286
+ env = _safe_string(claims.get("env")) or "production"
287
+ if not pid:
288
+ raise RuntimeError(
289
+ "dominus.artifacts legacy methods require project_id on the service JWT",
290
+ )
291
+ return pid, env
292
+
274
293
  async def _api(
275
294
  self,
276
295
  endpoint: str,
@@ -293,19 +312,27 @@ class ArtifactsNamespace:
293
312
  category: Optional[str] = None,
294
313
  ) -> Dict[str, Any]:
295
314
  """
296
- List artifacts for the current project/environment.
315
+ List artifact heads (V2 / legacy kind) for the service JWT project.
297
316
 
298
- Args:
299
- limit: Max items to return (default 100, max 500)
300
- category: Optional category filter
301
-
302
- Returns:
303
- {artifacts: [...], count: int, total_size_bytes: int}
317
+ Category filtering is client-side when rows include ``category``.
304
318
  """
305
- body: Dict[str, Any] = {"limit": limit}
319
+ project_id, environment = await self._legacy_project_context()
320
+ lim = max(1, min(int(limit or 100), 500))
321
+ raw = await self.list_v2(
322
+ lim,
323
+ group="dominus",
324
+ owner=f"project:{project_id}",
325
+ environment=environment,
326
+ kind="legacy",
327
+ )
328
+ artifacts = list(raw.get("artifacts") or [])
306
329
  if category:
307
- body["category"] = category
308
- return await self._api("/api/artifact/list", body=body)
330
+ artifacts = [a for a in artifacts if (a or {}).get("category") == category]
331
+ count = raw.get("count", len(artifacts))
332
+ total = raw.get("total_size_bytes")
333
+ if total is None:
334
+ total = sum(int((a or {}).get("size_bytes") or 0) for a in artifacts)
335
+ return {"artifacts": artifacts, "count": count, "total_size_bytes": total}
309
336
 
310
337
  async def get_health(self) -> Dict[str, Any]:
311
338
  """Check artifact worker health."""
@@ -320,52 +347,56 @@ class ArtifactsNamespace:
320
347
  content_type: Optional[str] = None,
321
348
  ) -> Dict[str, Any]:
322
349
  """
323
- Store an artifact.
324
-
325
- Args:
326
- data: Base64-encoded data
327
- key: Optional caller-provided key (UUID generated if not provided)
328
- ttl_seconds: Time-to-live in seconds (default 3600)
329
- category: Optional category for organization
330
-
331
- Returns:
332
- {key, ref, storage_type, size_bytes, expires_at}
350
+ Legacy-shaped store mapped to V2 (dominus / project:{id} / legacy / encoded key).
333
351
  """
334
- body: Dict[str, Any] = {
335
- "data": data,
336
- "ttl_seconds": ttl_seconds,
352
+ import uuid
353
+
354
+ project_id, environment = await self._legacy_project_context()
355
+ use_key = key or str(uuid.uuid4())
356
+ v2 = await self.store_v2(
357
+ group="dominus",
358
+ owner=f"project:{project_id}",
359
+ environment=environment,
360
+ kind="legacy",
361
+ artifact_key=quote(use_key, safe=""),
362
+ data=data,
363
+ ttl_seconds=ttl_seconds,
364
+ category=category,
365
+ content_type=content_type,
366
+ )
367
+ disp = build_display_artifact_ref(use_key)
368
+ return {
369
+ "key": use_key,
370
+ "ref": v2.get("compatibility_ref") or v2.get("head_ref") or disp,
371
+ "storage_type": v2.get("storage_type") or "redis",
372
+ "size_bytes": v2.get("size_bytes", 0),
373
+ "expires_at": v2.get("expires_at") or "",
337
374
  }
338
- if key:
339
- body["key"] = key
340
- if category:
341
- body["category"] = category
342
- if content_type:
343
- body["content_type"] = content_type
344
- return await self._api("/api/artifact/store", body=body)
345
375
 
346
376
  async def retrieve(self, key: str) -> Dict[str, Any]:
347
377
  """
348
- Retrieve an artifact by key.
349
-
350
- Args:
351
- key: Artifact key
352
-
353
- Returns:
354
- {key, data, storage_type, size_bytes, expires_at}
378
+ Retrieve by key via V2 display ref + target_project_id (compat with legacy data).
355
379
  """
356
- return await self._api("/api/artifact/retrieve", body={"key": key})
380
+ project_id, _environment = await self._legacy_project_context()
381
+ return await self._api(
382
+ "/api/artifact/v2/retrieve",
383
+ body={
384
+ "ref": f"{DISPLAY_REF_PREFIX}{key}",
385
+ "target_project_id": project_id,
386
+ },
387
+ )
357
388
 
358
389
  async def delete(self, key: str) -> Dict[str, Any]:
359
- """
360
- Delete an artifact by key.
361
-
362
- Args:
363
- key: Artifact key
364
-
365
- Returns:
366
- {deleted: bool, key: str}
367
- """
368
- return await self._api("/api/artifact/delete", body={"key": key})
390
+ """Delete legacy compatibility head via V2 selector."""
391
+ project_id, environment = await self._legacy_project_context()
392
+ await self.delete_v2(
393
+ group="dominus",
394
+ owner=f"project:{project_id}",
395
+ environment=environment,
396
+ kind="legacy",
397
+ artifact_key=quote(key, safe=""),
398
+ )
399
+ return {"deleted": True, "key": key}
369
400
 
370
401
  async def cleanup(
371
402
  self,
@@ -373,19 +404,12 @@ class ArtifactsNamespace:
373
404
  limit: int = 100,
374
405
  ) -> Dict[str, Any]:
375
406
  """
376
- Cleanup expired artifacts (admin only).
377
-
378
- Args:
379
- force: Skip throttle check
380
- limit: Max items to clean (default 100)
407
+ HTTP /cleanup is not exposed on the worker; opportunistic cleanup runs after v2 store.
381
408
 
382
- Returns:
383
- {cleaned: int, skipped: bool}
409
+ Returns a skipped sentinel for API compatibility.
384
410
  """
385
- body: Dict[str, Any] = {"limit": limit}
386
- if force:
387
- body["force"] = True
388
- return await self._api("/api/artifact/cleanup", body=body)
411
+ del force, limit
412
+ return {"cleaned": 0, "skipped": True}
389
413
 
390
414
  def _selector_body(self, options: Dict[str, Any]) -> Dict[str, Any]:
391
415
  body: Dict[str, Any] = {}
@@ -11,6 +11,7 @@ import sys
11
11
  import time
12
12
  from datetime import datetime, timezone
13
13
  from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
14
+ from urllib.parse import urlencode
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from ..start import Dominus
@@ -321,6 +322,13 @@ class LogsNamespace:
321
322
  minutes: int = 10,
322
323
  limit: int = 100,
323
324
  trace_id: Optional[str] = None,
325
+ since: Optional[str] = None,
326
+ company: Optional[str] = None,
327
+ subject: Optional[str] = None,
328
+ run_id: Optional[str] = None,
329
+ deploy_id: Optional[str] = None,
330
+ installation_id: Optional[str] = None,
331
+ artifact_ref: Optional[str] = None,
324
332
  ) -> List[Dict[str, Any]]:
325
333
  """Tail recent logs from the Logs Worker.
326
334
 
@@ -330,18 +338,39 @@ class LogsNamespace:
330
338
  minutes: Time window — 1, 10, or 60 (default: 10)
331
339
  limit: Maximum results, 1-1000 (default: 100)
332
340
  trace_id: Optional trace_id filter to return only events from a specific trace
341
+ since: Optional ISO timestamp lower bound
342
+ company: Optional canonical company slug filter
343
+ subject: Optional domain subject filter
344
+ run_id: Optional workflow or Authority run id filter
345
+ deploy_id: Optional deploy authority id filter
346
+ installation_id: Optional managed-client installation id filter
347
+ artifact_ref: Optional artifact ref filter
333
348
 
334
349
  Returns:
335
350
  List of log event dicts
336
351
  """
337
- params = []
352
+ params: Dict[str, Any] = {}
338
353
  if minutes != 10:
339
- params.append(f"minutes={minutes}")
354
+ params["minutes"] = minutes
340
355
  if limit != 100:
341
- params.append(f"limit={limit}")
356
+ params["limit"] = limit
342
357
  if trace_id:
343
- params.append(f"trace_id={trace_id}")
344
- qs = "&".join(params)
358
+ params["trace_id"] = trace_id
359
+ if since:
360
+ params["since"] = since
361
+ if company:
362
+ params["company"] = company
363
+ if subject:
364
+ params["subject"] = subject
365
+ if run_id:
366
+ params["run_id"] = run_id
367
+ if deploy_id:
368
+ params["deploy_id"] = deploy_id
369
+ if installation_id:
370
+ params["installation_id"] = installation_id
371
+ if artifact_ref:
372
+ params["artifact_ref"] = artifact_ref
373
+ qs = urlencode(params)
345
374
  endpoint = f"/api/logs/tail?{qs}" if qs else "/api/logs/tail"
346
375
 
347
376
  try:
@@ -372,7 +401,7 @@ class LogsNamespace:
372
401
  Returns:
373
402
  List of log event dicts
374
403
  """
375
- return await self.tail(minutes=minutes, limit=limit)
404
+ return await self.tail(minutes=minutes, limit=limit, **kwargs)
376
405
 
377
406
  async def batch(
378
407
  self,
@@ -29,10 +29,15 @@ Usage:
29
29
  templates = await dominus.workflow.list_templates()
30
30
  await dominus.workflow.copy_template(template_id)
31
31
 
32
- # Saved-workflow lifecycle
33
- run = await dominus.workflow.create_run(workflow_id=workflow_id, context={"key": "value"})
34
- await dominus.workflow.validate_run(run["run_id"])
35
- result = await dominus.workflow.start_run(run["run_id"], mode="async")
32
+ # Canonical saved-workflow launch (Authority POST /api/authority/runs/ensure)
33
+ execution = await dominus.workflow.ensure(
34
+ "my-workflow-id",
35
+ subject="PCM47474562",
36
+ company="acme",
37
+ inputs={"key": "value"},
38
+ mode="async",
39
+ )
40
+ # Legacy create_run / validate_run / start_run against /api/workflow/runs are removed.
36
41
  """
37
42
  import base64
38
43
  import json
@@ -55,6 +60,12 @@ class WorkflowNamespace:
55
60
  def __init__(self, client: "Dominus"):
56
61
  self._client = client
57
62
 
63
+ def _reject_legacy_runs_facade(self, method: str) -> None:
64
+ raise RuntimeError(
65
+ f"dominus.workflow.{method} was removed: /api/workflow/runs is retired. "
66
+ "Use ensure() with Authority fields (subject, company, inputs, context, ...) or ensure_instance()."
67
+ )
68
+
58
69
  @staticmethod
59
70
  def _is_workflow_ref(value: Optional[str]) -> bool:
60
71
  return str(value or "").strip().startswith("wf://")
@@ -758,32 +769,8 @@ class WorkflowNamespace:
758
769
  use_shared: Optional[bool] = None,
759
770
  shared_project_id: Optional[str] = None,
760
771
  ) -> Dict[str, Any]:
761
- """
762
- Create a saved-workflow run through workflow-manager's lifecycle facade.
763
-
764
- Args:
765
- workflow_id: Workflow UUID
766
- workflow_ref: Stable workflow_ref for the saved workflow
767
- """
768
- body: Dict[str, Any] = self._workflow_identifier_body(
769
- workflow_id=workflow_id,
770
- workflow_ref=workflow_ref,
771
- )
772
- if run_id:
773
- body["run_id"] = run_id
774
- if metadata:
775
- body["metadata"] = metadata
776
- if context:
777
- body["context"] = context
778
- if use_shared is not None:
779
- body["use_shared"] = use_shared
780
- if shared_project_id:
781
- body["shared_project_id"] = shared_project_id
782
-
783
- return await self._api(
784
- endpoint="/api/workflow/runs",
785
- body=body,
786
- )
772
+ del workflow_id, workflow_ref, run_id, metadata, context, use_shared, shared_project_id
773
+ self._reject_legacy_runs_facade("create_run")
787
774
 
788
775
  async def ensure(
789
776
  self,
@@ -1190,18 +1177,12 @@ class WorkflowNamespace:
1190
1177
  )
1191
1178
 
1192
1179
  async def get_run(self, run_id: str) -> Dict[str, Any]:
1193
- """Get a saved-workflow run."""
1194
- return await self._api(
1195
- endpoint=f"/api/workflow/runs/{run_id}",
1196
- method="GET",
1197
- )
1180
+ del run_id
1181
+ self._reject_legacy_runs_facade("get_run")
1198
1182
 
1199
1183
  async def get_manifest(self, run_id: str) -> Dict[str, Any]:
1200
- """Get the artifact manifest for a saved-workflow run."""
1201
- return await self._api(
1202
- endpoint=f"/api/workflow/runs/{run_id}/manifest",
1203
- method="GET",
1204
- )
1184
+ del run_id
1185
+ self._reject_legacy_runs_facade("get_manifest")
1205
1186
 
1206
1187
  async def register_artifact(
1207
1188
  self,
@@ -1223,43 +1204,26 @@ class WorkflowNamespace:
1223
1204
  """
1224
1205
  Register a source artifact with a saved-workflow run.
1225
1206
  """
1226
- body: Dict[str, Any] = {
1227
- "artifact_key": artifact_key,
1228
- }
1229
- if artifact_id:
1230
- body["artifact_id"] = artifact_id
1231
- if artifact_ref:
1232
- body["artifact_ref"] = artifact_ref
1233
- if source_artifact_key:
1234
- body["source_artifact_key"] = source_artifact_key
1235
- if source_namespace:
1236
- body["source_namespace"] = source_namespace
1237
- if source_conversation_id:
1238
- body["source_conversation_id"] = source_conversation_id
1239
- if source_project:
1240
- body["source_project"] = source_project
1241
- if source_env:
1242
- body["source_env"] = source_env
1243
- if source_project_id:
1244
- body["source_project_id"] = source_project_id
1245
- if source_tenant:
1246
- body["source_tenant"] = source_tenant
1247
- if metadata:
1248
- body["metadata"] = metadata
1249
- if metadata_trusted is not None:
1250
- body["metadata_trusted"] = metadata_trusted
1251
-
1252
- return await self._api(
1253
- endpoint=f"/api/workflow/runs/{run_id}/artifacts",
1254
- body=body,
1207
+ del (
1208
+ run_id,
1209
+ artifact_key,
1210
+ artifact_id,
1211
+ artifact_ref,
1212
+ source_artifact_key,
1213
+ source_namespace,
1214
+ source_conversation_id,
1215
+ source_project,
1216
+ source_env,
1217
+ source_project_id,
1218
+ source_tenant,
1219
+ metadata,
1220
+ metadata_trusted,
1255
1221
  )
1222
+ self._reject_legacy_runs_facade("register_artifact")
1256
1223
 
1257
1224
  async def validate_run(self, run_id: str) -> Dict[str, Any]:
1258
- """Validate that a saved-workflow run is ready to start."""
1259
- return await self._api(
1260
- endpoint=f"/api/workflow/runs/{run_id}/validate",
1261
- body={},
1262
- )
1225
+ del run_id
1226
+ self._reject_legacy_runs_facade("validate_run")
1263
1227
 
1264
1228
  async def start_run(
1265
1229
  self,
@@ -1270,18 +1234,8 @@ class WorkflowNamespace:
1270
1234
  save_events: Optional[bool] = None,
1271
1235
  webhook_url: Optional[str] = None,
1272
1236
  ) -> Dict[str, Any]:
1273
- """Start a saved-workflow run after validation."""
1274
- return await self._api(
1275
- endpoint=f"/api/workflow/runs/{run_id}/start",
1276
- body={
1277
- "config": self._build_start_config(
1278
- mode=mode,
1279
- timeout_seconds=timeout_seconds,
1280
- save_events=save_events,
1281
- webhook_url=webhook_url,
1282
- )
1283
- },
1284
- )
1237
+ del run_id, mode, timeout_seconds, save_events, webhook_url
1238
+ self._reject_legacy_runs_facade("start_run")
1285
1239
 
1286
1240
  async def stream_start_run(
1287
1241
  self,
@@ -1291,21 +1245,9 @@ class WorkflowNamespace:
1291
1245
  save_events: Optional[bool] = None,
1292
1246
  on_chunk: Optional[Any] = None,
1293
1247
  ):
1294
- """Start a saved-workflow run with SSE streaming."""
1295
- async for chunk in self._client._stream_request(
1296
- endpoint=f"/api/workflow/runs/{run_id}/start",
1297
- body={
1298
- "config": self._build_start_config(
1299
- mode="streaming",
1300
- timeout_seconds=timeout_seconds,
1301
- save_events=save_events,
1302
- )
1303
- },
1304
- on_chunk=on_chunk,
1305
- timeout=float(timeout_seconds or 600),
1306
- use_gateway=True,
1307
- ):
1308
- yield chunk
1248
+ del run_id, timeout_seconds, save_events, on_chunk
1249
+ self._reject_legacy_runs_facade("stream_start_run")
1250
+ yield # pragma: no cover
1309
1251
 
1310
1252
  async def status(self, execution_id: str) -> Dict[str, Any]:
1311
1253
  """Get saved-workflow execution status."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.18.0
3
+ Version: 3.0.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.18.0
52
+ - Version: 3.0.0
53
53
 
54
54
  ## Quick Start
55
55
 
@@ -195,6 +195,13 @@ dominus.release_console() # stop capturing
195
195
  - [Development](docs/development.md) -- Setup, testing, adding APIs
196
196
  - [Workflow hard-cut release notes](docs/workflow-hard-cut-release.md) -- cutover notes and operational checks
197
197
 
198
+ ## Verification (local)
199
+
200
+ ```bash
201
+ pip install -e .
202
+ python -m pytest tests -q
203
+ ```
204
+
198
205
  ## Workflow Surfaces
199
206
 
200
207
  - `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`.
@@ -46,5 +46,7 @@ dominus_sdk_python.egg-info/SOURCES.txt
46
46
  dominus_sdk_python.egg-info/dependency_links.txt
47
47
  dominus_sdk_python.egg-info/requires.txt
48
48
  dominus_sdk_python.egg-info/top_level.txt
49
+ tests/test_logs.py
50
+ tests/test_public_exports.py
49
51
  tests/test_workflow_lifecycle.py
50
52
  tests/test_workflow_refs.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "2.18.0"
7
+ version = "3.0.0"
8
8
  description = "Python SDK for the Dominus Orchestrator Platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -0,0 +1,65 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ if str(ROOT) not in sys.path:
9
+ sys.path.insert(0, str(ROOT))
10
+
11
+ from dominus.namespaces.logs import LogsNamespace # noqa: E402
12
+
13
+
14
+ class FakeClient:
15
+ def __init__(self):
16
+ self.calls = []
17
+
18
+ async def _request(
19
+ self,
20
+ endpoint,
21
+ method="POST",
22
+ body=None,
23
+ user_token=None,
24
+ use_gateway=False,
25
+ timeout=30.0,
26
+ ):
27
+ self.calls.append(
28
+ {
29
+ "endpoint": endpoint,
30
+ "method": method,
31
+ "body": body,
32
+ "user_token": user_token,
33
+ "use_gateway": use_gateway,
34
+ "timeout": timeout,
35
+ }
36
+ )
37
+ return {"events": [{"event_id": "log-1"}]}
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_logs_tail_forwards_business_filters():
42
+ client = FakeClient()
43
+ namespace = LogsNamespace(client)
44
+
45
+ events = await namespace.tail(
46
+ limit=25,
47
+ since="2026-04-11T08:33:00Z",
48
+ company="carebridge-summit",
49
+ subject="PCM47474562",
50
+ run_id="run-123",
51
+ deploy_id="deploy-123",
52
+ installation_id="inst-123",
53
+ artifact_ref="ar://carebridge/summit/production/company/report_snapshot_current",
54
+ )
55
+
56
+ assert len(events) == 1
57
+ assert client.calls[0]["endpoint"] == (
58
+ "/api/logs/tail?"
59
+ "limit=25&since=2026-04-11T08%3A33%3A00Z&company=carebridge-summit"
60
+ "&subject=PCM47474562&run_id=run-123&deploy_id=deploy-123"
61
+ "&installation_id=inst-123&artifact_ref="
62
+ "ar%3A%2F%2Fcarebridge%2Fsummit%2Fproduction%2Fcompany%2Freport_snapshot_current"
63
+ )
64
+ assert client.calls[0]["method"] == "GET"
65
+ assert client.calls[0]["use_gateway"] is True
@@ -0,0 +1,11 @@
1
+ from dominus import mint_selected_project_jwt
2
+
3
+
4
+ def test_top_level_exports_drop_delegate_alias():
5
+ assert callable(mint_selected_project_jwt)
6
+ try:
7
+ from dominus import delegate_jwt # type: ignore[attr-defined]
8
+ except ImportError:
9
+ delegate_jwt = None
10
+
11
+ assert delegate_jwt is None
@@ -167,104 +167,44 @@ async def test_ai_workflow_two_phase_lifecycle_routes_match_node_surface():
167
167
 
168
168
 
169
169
  @pytest.mark.asyncio
170
- async def test_saved_workflow_facade_lifecycle_contract_supports_processor_shape():
170
+ async def test_saved_workflow_facade_legacy_chain_raises():
171
171
  client = FakeClient(stream_chunks=[{"_event": "workflow_start"}])
172
172
  namespace = WorkflowNamespace(client)
173
173
 
174
- await namespace.create_run(
175
- "wf-123",
176
- run_id="report-run-1",
177
- context={
178
- "report_ref": {
179
- "report_id": "report-1",
180
- "accession": "acc-1",
181
- "session_number": "2",
182
- }
183
- },
184
- metadata={"source": "processor"},
185
- use_shared=True,
186
- shared_project_id="fdcd5800-c735-4a45-90be-51b621bd4127",
187
- )
188
- await namespace.register_artifact(
189
- "report-run-1",
190
- "report_snapshot_current",
191
- artifact_ref="ar://carebridge-summit/production/company/carebridge%2Fcarebridge-summit%2Fproduction%2Freport-workflow%2Freport%2Freport-1%2Freport_snapshot_current.json",
192
- source_artifact_key="carebridge/carebridge-summit/production/report-workflow/report/report-1/report_snapshot_current.json",
193
- source_namespace="company",
194
- source_project="carebridge-summit",
195
- source_env="production",
196
- source_project_id="project-123",
197
- source_tenant="tenant-a",
198
- metadata={
199
- "report_ref": {
200
- "report_id": "report-1",
201
- "accession": "acc-1",
202
- "session_number": "2",
203
- },
204
- "freshness": {
205
- "state": "fresh",
206
- "age_seconds": 12,
207
- },
208
- },
209
- metadata_trusted=True,
210
- )
211
- await namespace.validate_run("report-run-1")
212
- await namespace.start_run("report-run-1", timeout_seconds=120)
213
- streamed = [chunk async for chunk in namespace.stream_start_run("report-run-1", timeout_seconds=90)]
174
+ with pytest.raises(RuntimeError, match="removed"):
175
+ await namespace.create_run(
176
+ "wf-123",
177
+ run_id="report-run-1",
178
+ context={"report_ref": {"report_id": "report-1"}},
179
+ )
180
+ with pytest.raises(RuntimeError, match="removed"):
181
+ await namespace.register_artifact("report-run-1", "k", artifact_id="x")
182
+ with pytest.raises(RuntimeError, match="removed"):
183
+ await namespace.validate_run("report-run-1")
184
+ with pytest.raises(RuntimeError, match="removed"):
185
+ await namespace.start_run("report-run-1", timeout_seconds=120)
186
+ with pytest.raises(RuntimeError, match="removed"):
187
+ _ = [c async for c in namespace.stream_start_run("report-run-1", timeout_seconds=90)]
188
+
189
+ assert client.calls == []
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_saved_workflow_execution_poll_endpoints_unchanged():
194
+ client = FakeClient(stream_chunks=[])
195
+ namespace = WorkflowNamespace(client)
196
+
214
197
  await namespace.messages("exec-456", count=15)
215
198
  await namespace.events("exec-456", from_id="evt-2", count=20)
216
199
  await namespace.status("exec-456")
217
200
  await namespace.output("exec-456")
218
201
  await namespace.cancel("exec-456")
219
202
 
220
- create_call = client.calls[0]
221
- assert create_call["endpoint"] == "/api/workflow/runs"
222
- assert create_call["body"] == {
223
- "workflow_id": "wf-123",
224
- "run_id": "report-run-1",
225
- "metadata": {"source": "processor"},
226
- "context": {
227
- "report_ref": {
228
- "report_id": "report-1",
229
- "accession": "acc-1",
230
- "session_number": "2",
231
- }
232
- },
233
- "use_shared": True,
234
- "shared_project_id": "fdcd5800-c735-4a45-90be-51b621bd4127",
235
- }
236
-
237
- register_call = client.calls[1]
238
- assert register_call["endpoint"] == "/api/workflow/runs/report-run-1/artifacts"
239
- assert register_call["body"] == {
240
- "artifact_key": "report_snapshot_current",
241
- "artifact_ref": "ar://carebridge-summit/production/company/carebridge%2Fcarebridge-summit%2Fproduction%2Freport-workflow%2Freport%2Freport-1%2Freport_snapshot_current.json",
242
- "source_artifact_key": "carebridge/carebridge-summit/production/report-workflow/report/report-1/report_snapshot_current.json",
243
- "source_namespace": "company",
244
- "source_project": "carebridge-summit",
245
- "source_env": "production",
246
- "source_project_id": "project-123",
247
- "source_tenant": "tenant-a",
248
- "metadata": {
249
- "report_ref": {
250
- "report_id": "report-1",
251
- "accession": "acc-1",
252
- "session_number": "2",
253
- },
254
- "freshness": {
255
- "state": "fresh",
256
- "age_seconds": 12,
257
- },
258
- },
259
- "metadata_trusted": True,
260
- }
261
-
262
- assert client.calls[-5]["endpoint"] == "/api/workflow/executions/exec-456/messages?count=15"
263
- assert client.calls[-4]["endpoint"] == "/api/workflow/executions/exec-456/events?from_id=evt-2&count=20"
264
- assert client.calls[-3]["endpoint"] == "/api/workflow/executions/exec-456/status"
265
- assert client.calls[-2]["endpoint"] == "/api/workflow/executions/exec-456/output"
266
- assert client.calls[-1]["endpoint"] == "/api/workflow/executions/exec-456/cancel"
267
- assert streamed == [{"_event": "workflow_start"}]
203
+ assert client.calls[0]["endpoint"] == "/api/workflow/executions/exec-456/messages?count=15"
204
+ assert client.calls[1]["endpoint"] == "/api/workflow/executions/exec-456/events?from_id=evt-2&count=20"
205
+ assert client.calls[2]["endpoint"] == "/api/workflow/executions/exec-456/status"
206
+ assert client.calls[3]["endpoint"] == "/api/workflow/executions/exec-456/output"
207
+ assert client.calls[4]["endpoint"] == "/api/workflow/executions/exec-456/cancel"
268
208
 
269
209
 
270
210
  @pytest.mark.asyncio
@@ -53,22 +53,24 @@ async def test_workflow_get_accepts_workflow_ref():
53
53
 
54
54
 
55
55
  @pytest.mark.asyncio
56
- async def test_workflow_create_run_accepts_workflow_ref():
56
+ async def test_workflow_ensure_accepts_workflow_ref():
57
57
  client = FakeClient()
58
58
  namespace = WorkflowNamespace(client)
59
59
 
60
- await namespace.create_run(
61
- workflow_ref="wf://carebridge-platform/production/shared/patient-intake",
62
- run_id="run-123",
60
+ await namespace.ensure(
61
+ "wf://carebridge-platform/production/shared/patient-intake",
62
+ subject="sub-1",
63
+ company="co-1",
64
+ instance_id="run-123",
63
65
  context={"report_ref": {"report_id": "r-1"}},
66
+ mode="async",
64
67
  )
65
68
 
66
- assert client.calls[0]["endpoint"] == "/api/workflow/runs"
67
- assert client.calls[0]["body"] == {
68
- "workflow_ref": "wf://carebridge-platform/production/shared/patient-intake",
69
- "run_id": "run-123",
70
- "context": {"report_ref": {"report_id": "r-1"}},
71
- }
69
+ assert client.calls[0]["endpoint"] == "/api/authority/runs/ensure"
70
+ body = client.calls[0]["body"]
71
+ assert body["workflow_ref"] == "wf://carebridge-platform/production/shared/patient-intake"
72
+ assert body["instance_id"] == "run-123"
73
+ assert body["context"] == {"report_ref": {"report_id": "r-1"}}
72
74
 
73
75
 
74
76
  @pytest.mark.asyncio