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.
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/PKG-INFO +1 -1
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/config/endpoints.py +27 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/core.py +63 -33
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/__init__.py +3 -1
- dominus_sdk_python-2.9.0/dominus/namespaces/workflow.py +522 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/start.py +4 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/pyproject.toml +1 -1
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/README.md +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/__init__.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/config/__init__.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/errors.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/__init__.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/auth.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/cache.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/crypto.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/helpers/sse.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/admin.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/ai.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/auth.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/courier.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/db.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/ddl.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/fastapi.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/files.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/health.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/logs.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/open.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/__init__.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/session.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/types.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/portal.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/redis.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/secrets.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/secure.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/services/__init__.py +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
- {dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/setup.cfg +0 -0
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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=
|
|
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.
|
|
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
|
|
{dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/audio_capture.py
RENAMED
|
File without changes
|
{dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus/namespaces/oracle/oracle_websocket.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{dominus_sdk_python-2.8.0 → dominus_sdk_python-2.9.0}/dominus_sdk_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|