dominus-sdk-python 2.8.0__tar.gz → 2.9.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 (44) hide show
  1. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/config/endpoints.py +27 -0
  3. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/core.py +63 -33
  4. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/__init__.py +3 -1
  5. dominus_sdk_python-2.9.0/dominus/namespaces/workflow.py +522 -0
  6. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/start.py +4 -0
  7. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  8. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  9. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/pyproject.toml +1 -1
  10. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/README.md +0 -0
  11. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/__init__.py +0 -0
  12. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/config/__init__.py +0 -0
  13. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/errors.py +0 -0
  14. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/__init__.py +0 -0
  15. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/auth.py +0 -0
  16. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/cache.py +0 -0
  17. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/crypto.py +0 -0
  18. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/sse.py +0 -0
  19. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/admin.py +0 -0
  20. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/ai.py +0 -0
  21. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/auth.py +0 -0
  22. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/courier.py +0 -0
  23. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/db.py +0 -0
  24. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/ddl.py +0 -0
  25. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/fastapi.py +0 -0
  26. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/files.py +0 -0
  27. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/health.py +0 -0
  28. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/logs.py +0 -0
  29. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/open.py +0 -0
  30. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/__init__.py +0 -0
  31. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
  32. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  33. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/session.py +0 -0
  34. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/types.py +0 -0
  35. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
  36. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/portal.py +0 -0
  37. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/redis.py +0 -0
  38. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/secrets.py +0 -0
  39. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/secure.py +0 -0
  40. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/services/__init__.py +0 -0
  41. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  42. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  43. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  44. {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -30,6 +30,33 @@ ARCHITECT_URL = BASE_URL
30
30
  ORCHESTRATOR_URL = BASE_URL
31
31
  WARDEN_URL = BASE_URL
32
32
 
33
+ # Proxy configuration (optional)
34
+ # DOMINUS_* variants take precedence over standard HTTP_PROXY/HTTPS_PROXY
35
+ HTTP_PROXY = os.environ.get("DOMINUS_HTTP_PROXY") or os.environ.get("HTTP_PROXY")
36
+ HTTPS_PROXY = os.environ.get("DOMINUS_HTTPS_PROXY") or os.environ.get("HTTPS_PROXY")
37
+
38
+
39
+ def get_proxy_config() -> dict | None:
40
+ """
41
+ Get proxy configuration for httpx clients.
42
+
43
+ Returns a dict suitable for httpx's `proxies` parameter, or None if no proxy is configured.
44
+
45
+ Environment variables (in order of precedence):
46
+ - DOMINUS_HTTP_PROXY / DOMINUS_HTTPS_PROXY (SDK-specific)
47
+ - HTTP_PROXY / HTTPS_PROXY (standard)
48
+
49
+ Returns:
50
+ Dict mapping protocol to proxy URL, or None if no proxies configured.
51
+ Example: {"http://": "http://proxy:8080", "https://": "http://proxy:8080"}
52
+ """
53
+ proxies = {}
54
+ if HTTP_PROXY:
55
+ proxies["http://"] = HTTP_PROXY
56
+ if HTTPS_PROXY:
57
+ proxies["https://"] = HTTPS_PROXY
58
+ return proxies if proxies else None
59
+
33
60
 
34
61
  def get_gateway_url() -> str:
35
62
  """
@@ -16,6 +16,10 @@ from .cache import dominus_cache, sovereign_circuit_breaker, exponential_backoff
16
16
  # Max retries for HTTP requests (reduced from implicit to explicit)
17
17
  MAX_RETRIES = 3
18
18
 
19
+ # Mutex for JWT refresh to prevent race conditions
20
+ # When multiple concurrent coroutines need a new JWT, only one will mint
21
+ _jwt_refresh_lock = asyncio.Lock()
22
+
19
23
  # Type alias
20
24
  DominusResponse = dict[str, Any]
21
25
 
@@ -103,11 +107,12 @@ async def _fetch_jwks() -> dict:
103
107
  if _cached_jwks and time.time() - _jwks_cache_time < _JWKS_CACHE_TTL:
104
108
  return _cached_jwks
105
109
 
106
- from ..config.endpoints import get_gateway_url
110
+ from ..config.endpoints import get_gateway_url, get_proxy_config
107
111
  gateway_url = get_gateway_url()
112
+ proxy_config = get_proxy_config()
108
113
 
109
114
  try:
110
- async with httpx.AsyncClient(timeout=10.0) as client:
115
+ async with httpx.AsyncClient(timeout=10.0, proxies=proxy_config) as client:
111
116
  response = await client.get(f"{gateway_url}/jwt/jwks")
112
117
  response.raise_for_status()
113
118
 
@@ -329,8 +334,9 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
329
334
  Raises:
330
335
  RuntimeError: If circuit is open or auth fails after retries
331
336
  """
332
- from ..config.endpoints import get_gateway_url
337
+ from ..config.endpoints import get_gateway_url, get_proxy_config
333
338
  gateway_url = get_gateway_url()
339
+ proxy_config = get_proxy_config()
334
340
 
335
341
  # Circuit breaker check
336
342
  if not sovereign_circuit_breaker.can_execute():
@@ -357,7 +363,7 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
357
363
 
358
364
  for attempt in range(JWT_MINT_RETRIES):
359
365
  try:
360
- async with httpx.AsyncClient(base_url=gateway_url, headers=headers, timeout=30.0) as client:
366
+ async with httpx.AsyncClient(base_url=gateway_url, headers=headers, timeout=30.0, proxies=proxy_config) as client:
361
367
  response = await client.post("/jwt/mint", content=body_b64)
362
368
 
363
369
  # Check for retryable status codes before raise_for_status
@@ -455,43 +461,56 @@ def _get_architect_url(psk_token: str = None, sovereign_url: str = None, environ
455
461
  async def _ensure_valid_jwt(psk_token: str, sovereign_url: str) -> str:
456
462
  """
457
463
  Ensure we have a valid JWT, fetching and caching if needed.
458
-
464
+
465
+ Thread-safe via asyncio.Lock to prevent duplicate mints when
466
+ multiple concurrent coroutines need a new JWT.
467
+
459
468
  Cache key: "jwt:self:service"
460
469
  Cache TTL: 14 minutes (JWT lifetime is 15 minutes)
461
470
  Refresh when <60 seconds remain.
462
-
471
+
463
472
  Args:
464
473
  psk_token: PSK token (DOMINUS_TOKEN)
465
474
  sovereign_url: Sovereign base URL
466
-
475
+
467
476
  Returns:
468
477
  Valid JWT token string
469
478
  """
470
479
  cache_key = "jwt:self:service"
471
-
472
- # Check cache
480
+
481
+ # Fast path: check cache without lock
473
482
  cached_jwt = dominus_cache.get(cache_key)
474
483
  if cached_jwt:
475
484
  try:
476
- # Decode payload to check expiry
477
485
  payload = _decode_jwt_payload(cached_jwt)
478
486
  exp = payload.get("exp", 0)
479
487
  current_time = int(time.time())
480
-
481
- # Refresh if <60 seconds remain
482
488
  if exp - current_time > 60:
483
489
  return cached_jwt
484
490
  except Exception:
485
- # If decode fails, fetch new JWT
486
491
  pass
487
-
488
- # Fetch new JWT
489
- jwt = await _get_service_jwt(psk_token, sovereign_url)
490
-
491
- # Cache for 14 minutes (840 seconds)
492
- dominus_cache.set(cache_key, jwt, ttl=840)
493
-
494
- return jwt
492
+
493
+ # Slow path: acquire lock and refresh
494
+ async with _jwt_refresh_lock:
495
+ # Double-check cache (another coroutine may have refreshed while we waited)
496
+ cached_jwt = dominus_cache.get(cache_key)
497
+ if cached_jwt:
498
+ try:
499
+ payload = _decode_jwt_payload(cached_jwt)
500
+ exp = payload.get("exp", 0)
501
+ current_time = int(time.time())
502
+ if exp - current_time > 60:
503
+ return cached_jwt
504
+ except Exception:
505
+ pass
506
+
507
+ # Fetch new JWT
508
+ jwt = await _get_service_jwt(psk_token, sovereign_url)
509
+
510
+ # Cache for 14 minutes (840 seconds)
511
+ dominus_cache.set(cache_key, jwt, ttl=840)
512
+
513
+ return jwt
495
514
 
496
515
 
497
516
  def verify_token_format(token: str) -> bool:
@@ -524,12 +543,16 @@ async def health_check_all(base_url: str) -> dict:
524
543
  Returns:
525
544
  Health status dict with service results
526
545
  """
546
+ from ..config.endpoints import get_proxy_config
547
+ proxy_config = get_proxy_config()
548
+
527
549
  results = {}
528
550
 
529
551
  # Check orchestrator via /api/health
552
+ # Timeout: 15s to handle cold starts on Cloud Run
530
553
  try:
531
554
  start = time.time()
532
- async with httpx.AsyncClient(base_url=base_url, timeout=5.0) as client:
555
+ async with httpx.AsyncClient(base_url=base_url, timeout=15.0, proxies=proxy_config) as client:
533
556
  response = await client.get("/api/health")
534
557
  response.raise_for_status()
535
558
 
@@ -587,32 +610,35 @@ async def execute_with_retry(
587
610
  ) -> DominusResponse:
588
611
  """
589
612
  Execute HTTP request with retry logic.
590
-
613
+
591
614
  Args:
592
615
  route_info: (method, path, requires_auth, cacheable)
593
616
  base_url: Base URL for API
594
617
  token: Auth token (if needed)
595
618
  kwargs: Request parameters
596
-
619
+
597
620
  Returns:
598
621
  Response dict
599
622
  """
623
+ from ..config.endpoints import get_proxy_config
624
+ proxy_config = get_proxy_config()
625
+
600
626
  method, path, requires_auth, cacheable = route_info
601
-
627
+
602
628
  # Check cache first
603
629
  if cacheable and kwargs:
604
630
  cache_key = f"{path}:{str(sorted(kwargs.items()))}"
605
631
  cached = dominus_cache.get(cache_key)
606
632
  if cached:
607
633
  return cached
608
-
634
+
609
635
  # Validate token
610
636
  if requires_auth and not token:
611
637
  raise RuntimeError(
612
638
  "DOMINUS_TOKEN not set. "
613
639
  "Set the environment variable: export DOMINUS_TOKEN=your_token"
614
640
  )
615
-
641
+
616
642
  # Retry loop with exponential backoff and jitter
617
643
  for attempt in range(MAX_RETRIES):
618
644
  try:
@@ -620,7 +646,7 @@ async def execute_with_retry(
620
646
  headers = {}
621
647
  if requires_auth:
622
648
  headers["Authorization"] = f"Bearer {_b64_token(token)}"
623
-
649
+
624
650
  # Prepare body (encode if auth required and kwargs provided)
625
651
  # For auth routes: send raw base64 string as text/plain (matches middleware expectation)
626
652
  # For non-auth routes: send JSON as normal
@@ -630,9 +656,9 @@ async def execute_with_retry(
630
656
  body = body_b64
631
657
  else:
632
658
  body = kwargs if kwargs else {}
633
-
659
+
634
660
  # Make request
635
- async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client:
661
+ async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0, proxies=proxy_config) as client:
636
662
  if method == "GET":
637
663
  response = await client.get(path, params=kwargs if kwargs else None)
638
664
  else:
@@ -720,6 +746,9 @@ async def execute_bridge_call(
720
746
  Returns:
721
747
  Response data (the "data" field from successful response)
722
748
  """
749
+ from ..config.endpoints import get_proxy_config
750
+ proxy_config = get_proxy_config()
751
+
723
752
  # Check cache first
724
753
  if cacheable and params:
725
754
  cache_key = f"bridge:{method}:{str(sorted(params.items()))}"
@@ -749,7 +778,7 @@ async def execute_bridge_call(
749
778
  # Retry loop with exponential backoff and jitter
750
779
  for attempt in range(MAX_RETRIES):
751
780
  try:
752
- async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0) as client:
781
+ async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=30.0, proxies=proxy_config) as client:
753
782
  response = await client.post(endpoint, content=body_b64)
754
783
 
755
784
  response.raise_for_status()
@@ -878,8 +907,9 @@ async def _execute_auth_call(
878
907
  Returns:
879
908
  Response data
880
909
  """
881
- from ..config.endpoints import get_gateway_url
910
+ from ..config.endpoints import get_gateway_url, get_proxy_config
882
911
  gateway_url = get_gateway_url()
912
+ proxy_config = get_proxy_config()
883
913
 
884
914
  headers = {
885
915
  "Content-Type": "text/plain"
@@ -901,7 +931,7 @@ async def _execute_auth_call(
901
931
  else:
902
932
  endpoint = "/jwt/mint"
903
933
 
904
- async with httpx.AsyncClient(base_url=gateway_url, headers=headers, timeout=30.0) as client:
934
+ async with httpx.AsyncClient(base_url=gateway_url, headers=headers, timeout=30.0, proxies=proxy_config) as client:
905
935
  # JWKS uses GET (public endpoint), other auth endpoints use POST
906
936
  if method == "auth.jwks":
907
937
  response = await client.get(endpoint)
@@ -1,4 +1,4 @@
1
- """Dominus SDK Namespaces v2.7"""
1
+ """Dominus SDK Namespaces v2.8"""
2
2
  from .secrets import SecretsNamespace
3
3
  from .db import DbNamespace
4
4
  from .redis import RedisNamespace
@@ -10,6 +10,7 @@ from .open import OpenNamespace
10
10
  from .health import HealthNamespace
11
11
  from .portal import PortalNamespace
12
12
  from .courier import CourierNamespace
13
+ from .workflow import WorkflowNamespace
13
14
  from .ai import (
14
15
  AiNamespace,
15
16
  RagSubNamespace,
@@ -30,6 +31,7 @@ __all__ = [
30
31
  "HealthNamespace",
31
32
  "PortalNamespace",
32
33
  "CourierNamespace",
34
+ "WorkflowNamespace",
33
35
  "AiNamespace",
34
36
  "RagSubNamespace",
35
37
  "ArtifactsSubNamespace",
@@ -0,0 +1,522 @@
1
+ """
2
+ Workflow Namespace - Workflow management operations.
3
+
4
+ Provides CRUD operations for workflows, categories/pipelines, and templates.
5
+ Routes through gateway to dominus-workflow-manager service.
6
+
7
+ Usage:
8
+ from dominus import dominus
9
+
10
+ # Workflow CRUD
11
+ workflow = await dominus.workflow.save(
12
+ name="My Workflow",
13
+ yaml_content="name: My Workflow\nnodes: []",
14
+ description="A test workflow"
15
+ )
16
+ workflows = await dominus.workflow.list()
17
+ workflow = await dominus.workflow.get(workflow_id, include_content=True)
18
+ await dominus.workflow.delete(workflow_id)
19
+
20
+ # Categories (execution pipelines)
21
+ category = await dominus.workflow.create_category(
22
+ name="Intake Pipeline",
23
+ description="Patient intake workflow sequence"
24
+ )
25
+ await dominus.workflow.add_to_category(category_id, workflow_id)
26
+ categories = await dominus.workflow.list_categories()
27
+
28
+ # Templates
29
+ templates = await dominus.workflow.list_templates()
30
+ await dominus.workflow.copy_template(template_id)
31
+
32
+ # Execution
33
+ result = await dominus.workflow.execute(workflow_id, context={"key": "value"})
34
+ result = await dominus.workflow.execute_async(workflow_id, callback_url="...")
35
+ result = await dominus.workflow.execute_category(category_id)
36
+ """
37
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
38
+
39
+ if TYPE_CHECKING:
40
+ from ..start import Dominus
41
+
42
+
43
+ class WorkflowNamespace:
44
+ """
45
+ Workflow management namespace.
46
+
47
+ Provides operations for workflow CRUD, categories/pipelines, templates,
48
+ and execution via the dominus-workflow-manager service.
49
+ """
50
+
51
+ def __init__(self, client: "Dominus"):
52
+ self._client = client
53
+
54
+ async def _api(
55
+ self,
56
+ endpoint: str,
57
+ method: str = "POST",
58
+ body: Optional[Dict[str, Any]] = None
59
+ ) -> Dict[str, Any]:
60
+ """Make gateway-routed API request to workflow-manager."""
61
+ return await self._client._request(
62
+ endpoint=endpoint,
63
+ method=method,
64
+ body=body,
65
+ use_gateway=True
66
+ )
67
+
68
+ # ========================================
69
+ # Workflow CRUD
70
+ # ========================================
71
+
72
+ async def save(
73
+ self,
74
+ name: str,
75
+ yaml_content: str,
76
+ workflow_id: Optional[str] = None,
77
+ tenant_slug: Optional[str] = None,
78
+ description: Optional[str] = None,
79
+ category_id: Optional[str] = None,
80
+ tags: Optional[List[str]] = None,
81
+ is_template: bool = False,
82
+ ) -> Dict[str, Any]:
83
+ """
84
+ Save a workflow (create or update).
85
+
86
+ Args:
87
+ name: Workflow display name
88
+ yaml_content: YAML workflow definition
89
+ workflow_id: Optional ID for updates (omit for create)
90
+ tenant_slug: Optional tenant scope
91
+ description: Optional description
92
+ category_id: Optional category to assign
93
+ tags: Optional list of tags
94
+ is_template: Whether this is a template
95
+
96
+ Returns:
97
+ Dict with workflow metadata
98
+ """
99
+ body: Dict[str, Any] = {
100
+ "name": name,
101
+ "yaml_content": yaml_content,
102
+ }
103
+ if workflow_id:
104
+ body["workflow_id"] = workflow_id
105
+ if tenant_slug:
106
+ body["tenant_slug"] = tenant_slug
107
+ if description:
108
+ body["description"] = description
109
+ if category_id:
110
+ body["category_id"] = category_id
111
+ if tags:
112
+ body["tags"] = tags
113
+ if is_template:
114
+ body["is_template"] = is_template
115
+
116
+ return await self._api(
117
+ endpoint="/api/workflow/workflows",
118
+ body=body
119
+ )
120
+
121
+ async def get(
122
+ self,
123
+ workflow_id: str,
124
+ include_content: bool = False
125
+ ) -> Dict[str, Any]:
126
+ """
127
+ Get a workflow by ID.
128
+
129
+ Args:
130
+ workflow_id: Workflow UUID
131
+ include_content: Whether to include YAML content
132
+
133
+ Returns:
134
+ Dict with workflow metadata and optionally content
135
+ """
136
+ endpoint = f"/api/workflow/workflows/{workflow_id}"
137
+ if include_content:
138
+ endpoint += "?include_content=true"
139
+ return await self._api(endpoint=endpoint, method="GET")
140
+
141
+ async def list(
142
+ self,
143
+ tenant_slug: Optional[str] = None,
144
+ category_id: Optional[str] = None,
145
+ tags: Optional[List[str]] = None,
146
+ is_template: Optional[bool] = None,
147
+ search: Optional[str] = None,
148
+ limit: int = 100,
149
+ offset: int = 0,
150
+ ) -> List[Dict[str, Any]]:
151
+ """
152
+ List workflows.
153
+
154
+ Args:
155
+ tenant_slug: Filter by tenant
156
+ category_id: Filter by category
157
+ tags: Filter by tags (any match)
158
+ is_template: Filter by template status
159
+ search: Search in name/description
160
+ limit: Max results
161
+ offset: Pagination offset
162
+
163
+ Returns:
164
+ List of workflow metadata dicts
165
+ """
166
+ body: Dict[str, Any] = {"limit": limit, "offset": offset}
167
+ if tenant_slug:
168
+ body["tenant_slug"] = tenant_slug
169
+ if category_id:
170
+ body["category_id"] = category_id
171
+ if tags:
172
+ body["tags"] = tags
173
+ if is_template is not None:
174
+ body["is_template"] = is_template
175
+ if search:
176
+ body["search"] = search
177
+
178
+ result = await self._api(
179
+ endpoint="/api/workflow/workflows/list",
180
+ body=body
181
+ )
182
+ return result.get("workflows", result) if isinstance(result, dict) else result
183
+
184
+ async def delete(self, workflow_id: str) -> Dict[str, Any]:
185
+ """
186
+ Delete a workflow.
187
+
188
+ Args:
189
+ workflow_id: Workflow UUID
190
+
191
+ Returns:
192
+ Dict with deletion status
193
+ """
194
+ return await self._api(
195
+ endpoint=f"/api/workflow/workflows/{workflow_id}",
196
+ method="DELETE"
197
+ )
198
+
199
+ # ========================================
200
+ # Categories (Execution Pipelines)
201
+ # ========================================
202
+
203
+ async def create_category(
204
+ self,
205
+ name: str,
206
+ tenant_slug: Optional[str] = None,
207
+ description: Optional[str] = None,
208
+ ) -> Dict[str, Any]:
209
+ """
210
+ Create a category (execution pipeline).
211
+
212
+ Args:
213
+ name: Category display name
214
+ tenant_slug: Optional tenant scope
215
+ description: Optional description
216
+
217
+ Returns:
218
+ Dict with category metadata
219
+ """
220
+ body: Dict[str, Any] = {"name": name}
221
+ if tenant_slug:
222
+ body["tenant_slug"] = tenant_slug
223
+ if description:
224
+ body["description"] = description
225
+
226
+ return await self._api(
227
+ endpoint="/api/workflow/categories",
228
+ body=body
229
+ )
230
+
231
+ async def get_category(
232
+ self,
233
+ category_id: str,
234
+ include_workflows: bool = False
235
+ ) -> Dict[str, Any]:
236
+ """
237
+ Get a category by ID.
238
+
239
+ Args:
240
+ category_id: Category UUID
241
+ include_workflows: Whether to include workflow list
242
+
243
+ Returns:
244
+ Dict with category metadata and optionally workflows
245
+ """
246
+ endpoint = f"/api/workflow/categories/{category_id}"
247
+ if include_workflows:
248
+ endpoint += "?include_workflows=true"
249
+ return await self._api(endpoint=endpoint, method="GET")
250
+
251
+ async def list_categories(
252
+ self,
253
+ tenant_slug: Optional[str] = None,
254
+ limit: int = 100,
255
+ offset: int = 0,
256
+ ) -> List[Dict[str, Any]]:
257
+ """
258
+ List categories.
259
+
260
+ Args:
261
+ tenant_slug: Filter by tenant
262
+ limit: Max results
263
+ offset: Pagination offset
264
+
265
+ Returns:
266
+ List of category metadata dicts
267
+ """
268
+ body: Dict[str, Any] = {"limit": limit, "offset": offset}
269
+ if tenant_slug:
270
+ body["tenant_slug"] = tenant_slug
271
+
272
+ result = await self._api(
273
+ endpoint="/api/workflow/categories/list",
274
+ body=body
275
+ )
276
+ return result.get("categories", result) if isinstance(result, dict) else result
277
+
278
+ async def delete_category(self, category_id: str) -> Dict[str, Any]:
279
+ """
280
+ Delete a category.
281
+
282
+ Args:
283
+ category_id: Category UUID
284
+
285
+ Returns:
286
+ Dict with deletion status
287
+ """
288
+ return await self._api(
289
+ endpoint=f"/api/workflow/categories/{category_id}",
290
+ method="DELETE"
291
+ )
292
+
293
+ async def add_to_category(
294
+ self,
295
+ category_id: str,
296
+ workflow_id: str,
297
+ position: Optional[int] = None,
298
+ ) -> Dict[str, Any]:
299
+ """
300
+ Add a workflow to a category.
301
+
302
+ Args:
303
+ category_id: Category UUID
304
+ workflow_id: Workflow UUID
305
+ position: Optional position in execution order
306
+
307
+ Returns:
308
+ Dict with operation status
309
+ """
310
+ body: Dict[str, Any] = {"workflow_id": workflow_id}
311
+ if position is not None:
312
+ body["position"] = position
313
+
314
+ return await self._api(
315
+ endpoint=f"/api/workflow/categories/{category_id}/workflows",
316
+ body=body
317
+ )
318
+
319
+ async def remove_from_category(
320
+ self,
321
+ category_id: str,
322
+ workflow_id: str,
323
+ ) -> Dict[str, Any]:
324
+ """
325
+ Remove a workflow from a category.
326
+
327
+ Args:
328
+ category_id: Category UUID
329
+ workflow_id: Workflow UUID
330
+
331
+ Returns:
332
+ Dict with operation status
333
+ """
334
+ return await self._api(
335
+ endpoint=f"/api/workflow/categories/{category_id}/workflows/{workflow_id}",
336
+ method="DELETE"
337
+ )
338
+
339
+ async def reorder_category(
340
+ self,
341
+ category_id: str,
342
+ workflow_order: List[str],
343
+ ) -> Dict[str, Any]:
344
+ """
345
+ Reorder workflows in a category.
346
+
347
+ Args:
348
+ category_id: Category UUID
349
+ workflow_order: List of workflow IDs in desired order
350
+
351
+ Returns:
352
+ Dict with operation status
353
+ """
354
+ return await self._api(
355
+ endpoint=f"/api/workflow/categories/{category_id}/reorder",
356
+ body={"workflow_order": workflow_order}
357
+ )
358
+
359
+ # ========================================
360
+ # Templates
361
+ # ========================================
362
+
363
+ async def list_templates(
364
+ self,
365
+ limit: int = 100,
366
+ offset: int = 0,
367
+ ) -> List[Dict[str, Any]]:
368
+ """
369
+ List available templates.
370
+
371
+ Args:
372
+ limit: Max results
373
+ offset: Pagination offset
374
+
375
+ Returns:
376
+ List of template metadata dicts
377
+ """
378
+ result = await self._api(
379
+ endpoint="/api/workflow/templates",
380
+ method="GET"
381
+ )
382
+ return result.get("templates", result) if isinstance(result, dict) else result
383
+
384
+ async def get_template(
385
+ self,
386
+ template_id: str,
387
+ include_content: bool = False
388
+ ) -> Dict[str, Any]:
389
+ """
390
+ Get a template by ID.
391
+
392
+ Args:
393
+ template_id: Template UUID
394
+ include_content: Whether to include YAML content
395
+
396
+ Returns:
397
+ Dict with template metadata and optionally content
398
+ """
399
+ endpoint = f"/api/workflow/templates/{template_id}"
400
+ if include_content:
401
+ endpoint += "?include_content=true"
402
+ return await self._api(endpoint=endpoint, method="GET")
403
+
404
+ async def copy_template(
405
+ self,
406
+ template_id: str,
407
+ name: Optional[str] = None,
408
+ tenant_slug: Optional[str] = None,
409
+ ) -> Dict[str, Any]:
410
+ """
411
+ Copy a template to create a new workflow.
412
+
413
+ Args:
414
+ template_id: Template UUID to copy
415
+ name: Optional name for the new workflow
416
+ tenant_slug: Optional tenant scope
417
+
418
+ Returns:
419
+ Dict with new workflow metadata
420
+ """
421
+ body: Dict[str, Any] = {}
422
+ if name:
423
+ body["name"] = name
424
+ if tenant_slug:
425
+ body["tenant_slug"] = tenant_slug
426
+
427
+ return await self._api(
428
+ endpoint=f"/api/workflow/templates/{template_id}/copy",
429
+ body=body
430
+ )
431
+
432
+ # ========================================
433
+ # Execution
434
+ # ========================================
435
+
436
+ async def execute(
437
+ self,
438
+ workflow_id: str,
439
+ context: Optional[Dict[str, Any]] = None,
440
+ ) -> Dict[str, Any]:
441
+ """
442
+ Execute a workflow synchronously.
443
+
444
+ Args:
445
+ workflow_id: Workflow UUID
446
+ context: Optional initial context/variables
447
+
448
+ Returns:
449
+ Dict with execution result
450
+ """
451
+ body: Dict[str, Any] = {
452
+ "workflow_id": workflow_id,
453
+ "async_mode": False,
454
+ }
455
+ if context:
456
+ body["context"] = context
457
+
458
+ return await self._api(
459
+ endpoint="/api/workflow/execute/workflow",
460
+ body=body
461
+ )
462
+
463
+ async def execute_async(
464
+ self,
465
+ workflow_id: str,
466
+ context: Optional[Dict[str, Any]] = None,
467
+ callback_url: Optional[str] = None,
468
+ ) -> Dict[str, Any]:
469
+ """
470
+ Execute a workflow asynchronously.
471
+
472
+ Args:
473
+ workflow_id: Workflow UUID
474
+ context: Optional initial context/variables
475
+ callback_url: URL to POST results to when complete
476
+
477
+ Returns:
478
+ Dict with execution_id for tracking
479
+ """
480
+ body: Dict[str, Any] = {
481
+ "workflow_id": workflow_id,
482
+ "async_mode": True,
483
+ }
484
+ if context:
485
+ body["context"] = context
486
+ if callback_url:
487
+ body["callback_url"] = callback_url
488
+
489
+ return await self._api(
490
+ endpoint="/api/workflow/execute/workflow",
491
+ body=body
492
+ )
493
+
494
+ async def execute_category(
495
+ self,
496
+ category_id: str,
497
+ context: Optional[Dict[str, Any]] = None,
498
+ callback_url: Optional[str] = None,
499
+ ) -> Dict[str, Any]:
500
+ """
501
+ Execute a category/pipeline (all workflows in sequence).
502
+
503
+ Categories always execute asynchronously.
504
+
505
+ Args:
506
+ category_id: Category UUID
507
+ context: Optional initial context/variables
508
+ callback_url: URL to POST results to when complete
509
+
510
+ Returns:
511
+ Dict with execution_id for tracking
512
+ """
513
+ body: Dict[str, Any] = {"category_id": category_id}
514
+ if context:
515
+ body["context"] = context
516
+ if callback_url:
517
+ body["callback_url"] = callback_url
518
+
519
+ return await self._api(
520
+ endpoint="/api/workflow/execute/category",
521
+ body=body
522
+ )
@@ -355,6 +355,10 @@ class Dominus:
355
355
  from .namespaces.ai import AiNamespace
356
356
  self.ai = AiNamespace(self)
357
357
 
358
+ # Workflow namespace (workflow-manager service)
359
+ from .namespaces.workflow import WorkflowNamespace
360
+ self.workflow = WorkflowNamespace(self)
361
+
358
362
  # Cache for JWT public key
359
363
  self._public_key_cache = None
360
364
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -27,6 +27,7 @@ dominus/namespaces/portal.py
27
27
  dominus/namespaces/redis.py
28
28
  dominus/namespaces/secrets.py
29
29
  dominus/namespaces/secure.py
30
+ dominus/namespaces/workflow.py
30
31
  dominus/namespaces/oracle/__init__.py
31
32
  dominus/namespaces/oracle/audio_capture.py
32
33
  dominus/namespaces/oracle/oracle_websocket.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "2.8.0"
7
+ version = "2.9.0"
8
8
  description = "Python SDK for the Dominus Orchestrator Platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}