dominus-sdk-python 4.5.0__tar.gz → 4.6.1__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-4.5.0 → dominus_sdk_python-4.6.1}/PKG-INFO +1 -1
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/__init__.py +3 -1
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/__init__.py +2 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/browser.py +24 -3
- dominus_sdk_python-4.6.1/dominus/namespaces/recipes.py +165 -0
- dominus_sdk_python-4.6.1/dominus/namespaces/stash.py +275 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/start.py +8 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/SOURCES.txt +2 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/pyproject.toml +1 -1
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_browser_namespace.py +6 -2
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_public_exports.py +3 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/README.md +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/config/__init__.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/config/endpoints.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/errors.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/__init__.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/auth.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/cache.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/console_capture.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/core.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/crypto.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/sse.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/trace.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/admin.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/ai.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/artifacts.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/auth.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/authority.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/courier.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/db.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/ddl.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/deployer.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/fastapi.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/files.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/health.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/jobs.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/logs.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/portal.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/processor.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/redis.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/secrets.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/secure.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/sync.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/warden.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/workflow.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/services/__init__.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/requires.txt +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/top_level.txt +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/setup.cfg +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_auth.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_authority_public_vocabulary.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_control_plane_namespaces.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_errors.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_flat_commands.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_health.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_logs.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_provisioning_parity.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_transport_compat.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_workflow_lifecycle.py +0 -0
- {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_workflow_refs.py +0 -0
|
@@ -112,6 +112,7 @@ from .namespaces.processor import ProcessorNamespace
|
|
|
112
112
|
from .namespaces.sync import SyncNamespace
|
|
113
113
|
from .namespaces.authority import AuthorityNamespace
|
|
114
114
|
from .namespaces.browser import BrowserNamespace
|
|
115
|
+
from .namespaces.recipes import RecipesNamespace
|
|
115
116
|
from .namespaces.deployer import DeployerNamespace
|
|
116
117
|
from .namespaces.warden import WardenNamespace
|
|
117
118
|
|
|
@@ -164,7 +165,7 @@ from .errors import (
|
|
|
164
165
|
TimeoutError as DominusTimeoutError,
|
|
165
166
|
)
|
|
166
167
|
|
|
167
|
-
__version__ = "4.
|
|
168
|
+
__version__ = "4.6.1"
|
|
168
169
|
__all__ = [
|
|
169
170
|
# Main SDK instance
|
|
170
171
|
"dominus",
|
|
@@ -215,6 +216,7 @@ __all__ = [
|
|
|
215
216
|
"SyncNamespace",
|
|
216
217
|
"AuthorityNamespace",
|
|
217
218
|
"BrowserNamespace",
|
|
219
|
+
"RecipesNamespace",
|
|
218
220
|
"DeployerNamespace",
|
|
219
221
|
"WardenNamespace",
|
|
220
222
|
# AI namespace for agent-runtime operations
|
|
@@ -16,6 +16,7 @@ from .processor import ProcessorNamespace
|
|
|
16
16
|
from .sync import SyncNamespace
|
|
17
17
|
from .authority import AuthorityNamespace
|
|
18
18
|
from .browser import BrowserNamespace
|
|
19
|
+
from .recipes import RecipesNamespace
|
|
19
20
|
from .deployer import DeployerNamespace
|
|
20
21
|
from .warden import WardenNamespace
|
|
21
22
|
from .ai import (
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
"SyncNamespace",
|
|
45
46
|
"AuthorityNamespace",
|
|
46
47
|
"BrowserNamespace",
|
|
48
|
+
"RecipesNamespace",
|
|
47
49
|
"DeployerNamespace",
|
|
48
50
|
"WardenNamespace",
|
|
49
51
|
"AiNamespace",
|
|
@@ -77,7 +77,7 @@ class BrowserNamespace:
|
|
|
77
77
|
async def ensure_run(
|
|
78
78
|
self,
|
|
79
79
|
*,
|
|
80
|
-
target: Dict[str, Any],
|
|
80
|
+
target: Optional[Dict[str, Any]] = None,
|
|
81
81
|
idempotency_key: Optional[str] = None,
|
|
82
82
|
run_kind: Optional[str] = None,
|
|
83
83
|
route_label: Optional[str] = None,
|
|
@@ -92,11 +92,29 @@ class BrowserNamespace:
|
|
|
92
92
|
org_id: Optional[str] = None,
|
|
93
93
|
app_slug: Optional[str] = None,
|
|
94
94
|
env: Optional[str] = None,
|
|
95
|
+
recipe_ref: Optional[str] = None,
|
|
96
|
+
recipe: Optional[Any] = None,
|
|
97
|
+
recipe_inputs: Optional[Dict[str, Any]] = None,
|
|
95
98
|
timeout: float = 30.0,
|
|
96
99
|
) -> Dict[str, Any]:
|
|
97
100
|
"""
|
|
98
101
|
Ensure a browser run. ``POST /api/browser/runs/ensure``.
|
|
99
102
|
|
|
103
|
+
``target`` shorthand, ``recipe_ref``, or inline ``recipe`` must be
|
|
104
|
+
present. When ``recipe_ref`` is set, the worker resolves the
|
|
105
|
+
recipe through the recipe-worker, parses it, resolves variables, and
|
|
106
|
+
runs it through the recipe executor — supporting multi-step flows
|
|
107
|
+
including clicks, fills, assertions, extracts, and stash-backed
|
|
108
|
+
credential fills.
|
|
109
|
+
|
|
110
|
+
``recipe`` may be an inline YAML string or JSON object for one-off
|
|
111
|
+
runs. Stable/shared recipes should be published and referenced via
|
|
112
|
+
``recipe_ref``. Target/assertion shorthand runs are synthesized into a
|
|
113
|
+
``browser-recipe-v1`` recipe internally; there is no separate runner path.
|
|
114
|
+
|
|
115
|
+
``recipe_inputs`` supplies scalar values consumed by ``recipe-input://``
|
|
116
|
+
variable refs declared in the recipe's vars map.
|
|
117
|
+
|
|
100
118
|
Browser run metadata remains browser-worker runtime state. Artifact V2
|
|
101
119
|
is used for sanitized `browser-run` result payloads and, when opted-in
|
|
102
120
|
via ``capture_policy["storage_state"] = "always"`` plus
|
|
@@ -109,8 +127,8 @@ class BrowserNamespace:
|
|
|
109
127
|
lookup, returns no storage_state on first call) or an explicit
|
|
110
128
|
``session_artifact_ref`` (hard-error if the ref does not exist).
|
|
111
129
|
"""
|
|
112
|
-
if not target:
|
|
113
|
-
raise ValueError("target is required")
|
|
130
|
+
if not target and not recipe_ref and recipe is None:
|
|
131
|
+
raise ValueError("target, recipe_ref, or recipe is required")
|
|
114
132
|
body = _compact({
|
|
115
133
|
"idempotency_key": idempotency_key,
|
|
116
134
|
"run_kind": run_kind,
|
|
@@ -127,6 +145,9 @@ class BrowserNamespace:
|
|
|
127
145
|
"org_id": org_id,
|
|
128
146
|
"app_slug": app_slug,
|
|
129
147
|
"env": env,
|
|
148
|
+
"recipe_ref": recipe_ref,
|
|
149
|
+
"recipe": recipe,
|
|
150
|
+
"recipe_inputs": recipe_inputs,
|
|
130
151
|
})
|
|
131
152
|
return await self._post("/api/browser/runs/ensure", body, timeout=timeout)
|
|
132
153
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipes Namespace - kernel-tier recipe protocol.
|
|
3
|
+
|
|
4
|
+
Routes through ``/svc/recipe/*`` on the Dominus gateway. The recipe worker
|
|
5
|
+
owns the type registry, JSON-Schema-based publish validation, three-tier
|
|
6
|
+
resolution (project -> group -> platform), and B2-backed durable storage.
|
|
7
|
+
|
|
8
|
+
Recipe types currently registered:
|
|
9
|
+
- browser-recipe (owned by dominus-browser-worker; body version is browser-recipe-v1)
|
|
10
|
+
|
|
11
|
+
Refs follow ``recipe://{type}/{name}[@v{N}][?tier={tier}]``.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..start import Dominus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
|
+
return {k: v for k, v in payload.items() if v is not None and v != ""}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecipesNamespace:
|
|
26
|
+
"""
|
|
27
|
+
Recipe worker namespace.
|
|
28
|
+
|
|
29
|
+
Usage::
|
|
30
|
+
|
|
31
|
+
types = await dominus.recipes.list_types()
|
|
32
|
+
await dominus.recipes.publish(
|
|
33
|
+
type="browser-recipe",
|
|
34
|
+
tier="platform",
|
|
35
|
+
name="health-check",
|
|
36
|
+
body="version: browser-recipe-v1\\nsteps:\\n - kind: goto\\n url: https://x/\\n",
|
|
37
|
+
)
|
|
38
|
+
recipe = await dominus.recipes.get(type="browser-recipe", name="health-check")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, client: "Dominus"):
|
|
42
|
+
self._client = client
|
|
43
|
+
|
|
44
|
+
async def _post(self, endpoint: str, body: Optional[Dict[str, Any]] = None, *, timeout: float = 30.0) -> Dict[str, Any]:
|
|
45
|
+
return await self._client._request(
|
|
46
|
+
endpoint=endpoint,
|
|
47
|
+
method="POST",
|
|
48
|
+
body=body or {},
|
|
49
|
+
use_gateway=True,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def _get(self, endpoint: str, *, timeout: float = 30.0) -> Dict[str, Any]:
|
|
54
|
+
return await self._client._request(
|
|
55
|
+
endpoint=endpoint,
|
|
56
|
+
method="GET",
|
|
57
|
+
use_gateway=True,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def list_types(self, *, owning_worker: Optional[str] = None, timeout: float = 15.0) -> Dict[str, Any]:
|
|
62
|
+
"""List registered recipe types. ``GET /api/recipe/types``."""
|
|
63
|
+
query = f"?owning_worker={owning_worker}" if owning_worker else ""
|
|
64
|
+
return await self._get(f"/api/recipe/types{query}", timeout=timeout)
|
|
65
|
+
|
|
66
|
+
async def get_type(self, name: str, *, timeout: float = 15.0) -> Dict[str, Any]:
|
|
67
|
+
"""Fetch a registered recipe type's schema. ``GET /api/recipe/types/{name}``."""
|
|
68
|
+
return await self._get(f"/api/recipe/types/{name}", timeout=timeout)
|
|
69
|
+
|
|
70
|
+
async def publish(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
type: str,
|
|
74
|
+
tier: str,
|
|
75
|
+
name: str,
|
|
76
|
+
body: str,
|
|
77
|
+
description: Optional[str] = None,
|
|
78
|
+
timeout: float = 30.0,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
"""Publish a recipe. ``POST /api/recipe/recipes/publish``."""
|
|
81
|
+
return await self._post(
|
|
82
|
+
"/api/recipe/recipes/publish",
|
|
83
|
+
_compact({
|
|
84
|
+
"type": type,
|
|
85
|
+
"tier": tier,
|
|
86
|
+
"name": name,
|
|
87
|
+
"body": body,
|
|
88
|
+
"description": description,
|
|
89
|
+
}),
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def validate(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
type: str,
|
|
97
|
+
body: str,
|
|
98
|
+
timeout: float = 15.0,
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
"""Validate a recipe body against its type schema. ``POST /api/recipe/recipes/validate``."""
|
|
101
|
+
return await self._post(
|
|
102
|
+
"/api/recipe/recipes/validate",
|
|
103
|
+
{"type": type, "body": body},
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def list(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
type: Optional[str] = None,
|
|
111
|
+
tier: Optional[str] = None,
|
|
112
|
+
name_prefix: Optional[str] = None,
|
|
113
|
+
timeout: float = 15.0,
|
|
114
|
+
) -> Dict[str, Any]:
|
|
115
|
+
"""List recipes filtered by type/tier/name. ``GET /api/recipe/recipes/list``."""
|
|
116
|
+
params: List[str] = []
|
|
117
|
+
if type:
|
|
118
|
+
params.append(f"type={type}")
|
|
119
|
+
if tier:
|
|
120
|
+
params.append(f"tier={tier}")
|
|
121
|
+
if name_prefix:
|
|
122
|
+
params.append(f"name_prefix={name_prefix}")
|
|
123
|
+
query = ("?" + "&".join(params)) if params else ""
|
|
124
|
+
return await self._get(f"/api/recipe/recipes/list{query}", timeout=timeout)
|
|
125
|
+
|
|
126
|
+
async def get(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
type: str,
|
|
130
|
+
name: str,
|
|
131
|
+
version: Optional[int] = None,
|
|
132
|
+
tier: Optional[str] = None,
|
|
133
|
+
timeout: float = 15.0,
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
"""Resolve a recipe through the three-tier chain. ``GET /api/recipe/recipes/{type}/{name}[@v{N}]``."""
|
|
136
|
+
path = f"/api/recipe/recipes/{type}/{name}"
|
|
137
|
+
if version is not None:
|
|
138
|
+
path += f"@v{version}"
|
|
139
|
+
if tier:
|
|
140
|
+
path += f"?tier={tier}"
|
|
141
|
+
return await self._get(path, timeout=timeout)
|
|
142
|
+
|
|
143
|
+
async def register_type(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
name: str,
|
|
147
|
+
owning_worker: str,
|
|
148
|
+
schema_version: int,
|
|
149
|
+
json_schema: Dict[str, Any],
|
|
150
|
+
timeout: float = 15.0,
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Register a recipe type. ``POST /api/recipe/types/register``.
|
|
153
|
+
|
|
154
|
+
Owning workers do this on cold start. Normal callers should not need it.
|
|
155
|
+
"""
|
|
156
|
+
return await self._post(
|
|
157
|
+
"/api/recipe/types/register",
|
|
158
|
+
{
|
|
159
|
+
"name": name,
|
|
160
|
+
"owning_worker": owning_worker,
|
|
161
|
+
"schema_version": schema_version,
|
|
162
|
+
"json_schema": json_schema,
|
|
163
|
+
},
|
|
164
|
+
timeout=timeout,
|
|
165
|
+
)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stash Namespace - per-scope durable items (credentials + configs).
|
|
3
|
+
|
|
4
|
+
Routes through ``/svc/stash/*`` on the Dominus gateway. The stash worker
|
|
5
|
+
stores items in each project's ``stash.*`` schema and transparently falls
|
|
6
|
+
back to a designated shared project on read.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..start import Dominus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StashNamespace:
|
|
15
|
+
"""
|
|
16
|
+
Stash worker namespace.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
item = await dominus.stash.resolve(
|
|
20
|
+
kind="credential.vendor.fuji_synapse",
|
|
21
|
+
scope={"user_id": user_id, "organization_code": "piedmont"},
|
|
22
|
+
purpose="extraction",
|
|
23
|
+
)
|
|
24
|
+
await dominus.stash.mark_verified(id=item["id"], purpose="extraction")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: "Dominus"):
|
|
28
|
+
self._client = client
|
|
29
|
+
|
|
30
|
+
async def upsert(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
kind: str,
|
|
34
|
+
scope: Dict[str, Any],
|
|
35
|
+
value: Dict[str, Any],
|
|
36
|
+
id: Optional[str] = None,
|
|
37
|
+
target: Optional[str] = None,
|
|
38
|
+
item_key: Optional[str] = None,
|
|
39
|
+
role: Optional[str] = None,
|
|
40
|
+
priority: Optional[int] = None,
|
|
41
|
+
is_sensitive: Optional[bool] = None,
|
|
42
|
+
rotation_interval_days: Optional[int] = None,
|
|
43
|
+
expires_at: Optional[str] = None,
|
|
44
|
+
purpose: Optional[str] = None,
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""Insert or update a stash item."""
|
|
47
|
+
body: Dict[str, Any] = {"kind": kind, "scope": scope, "value": value}
|
|
48
|
+
if id is not None:
|
|
49
|
+
body["id"] = id
|
|
50
|
+
if target is not None:
|
|
51
|
+
body["target"] = target
|
|
52
|
+
if item_key is not None:
|
|
53
|
+
body["item_key"] = item_key
|
|
54
|
+
if role is not None:
|
|
55
|
+
body["role"] = role
|
|
56
|
+
if priority is not None:
|
|
57
|
+
body["priority"] = priority
|
|
58
|
+
if is_sensitive is not None:
|
|
59
|
+
body["is_sensitive"] = is_sensitive
|
|
60
|
+
if rotation_interval_days is not None:
|
|
61
|
+
body["rotation_interval_days"] = rotation_interval_days
|
|
62
|
+
if expires_at is not None:
|
|
63
|
+
body["expires_at"] = expires_at
|
|
64
|
+
if purpose is not None:
|
|
65
|
+
body["purpose"] = purpose
|
|
66
|
+
return await self._client._request(
|
|
67
|
+
endpoint="/svc/stash/items/upsert",
|
|
68
|
+
body=body,
|
|
69
|
+
use_gateway=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def get(
|
|
73
|
+
self,
|
|
74
|
+
id: str,
|
|
75
|
+
*,
|
|
76
|
+
purpose: Optional[str] = None,
|
|
77
|
+
include_shared: Optional[bool] = None,
|
|
78
|
+
) -> Optional[Dict[str, Any]]:
|
|
79
|
+
"""Fetch a single item by id; decrypts sensitive values."""
|
|
80
|
+
body: Dict[str, Any] = {"id": id}
|
|
81
|
+
if purpose is not None:
|
|
82
|
+
body["purpose"] = purpose
|
|
83
|
+
if include_shared is not None:
|
|
84
|
+
body["include_shared"] = include_shared
|
|
85
|
+
result = await self._client._request(
|
|
86
|
+
endpoint="/svc/stash/items/get",
|
|
87
|
+
body=body,
|
|
88
|
+
use_gateway=True,
|
|
89
|
+
)
|
|
90
|
+
return result.get("item") if isinstance(result, dict) else None
|
|
91
|
+
|
|
92
|
+
async def resolve(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
kind: str,
|
|
96
|
+
scope: Dict[str, Any],
|
|
97
|
+
purpose: str,
|
|
98
|
+
fallback_roles: Optional[List[str]] = None,
|
|
99
|
+
health_status_in: Optional[List[str]] = None,
|
|
100
|
+
machine_id: Optional[str] = None,
|
|
101
|
+
include_shared: Optional[bool] = None,
|
|
102
|
+
) -> Optional[Dict[str, Any]]:
|
|
103
|
+
"""Resolve the best-priority match by (kind, scope) with fallback walk."""
|
|
104
|
+
body: Dict[str, Any] = {"kind": kind, "scope": scope, "purpose": purpose}
|
|
105
|
+
if fallback_roles is not None:
|
|
106
|
+
body["fallback_roles"] = fallback_roles
|
|
107
|
+
if health_status_in is not None:
|
|
108
|
+
body["health_status_in"] = health_status_in
|
|
109
|
+
if machine_id is not None:
|
|
110
|
+
body["machine_id"] = machine_id
|
|
111
|
+
if include_shared is not None:
|
|
112
|
+
body["include_shared"] = include_shared
|
|
113
|
+
result = await self._client._request(
|
|
114
|
+
endpoint="/svc/stash/items/resolve",
|
|
115
|
+
body=body,
|
|
116
|
+
use_gateway=True,
|
|
117
|
+
)
|
|
118
|
+
return result.get("item") if isinstance(result, dict) else None
|
|
119
|
+
|
|
120
|
+
async def list(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
kind: Optional[str] = None,
|
|
124
|
+
scope: Optional[Dict[str, Any]] = None,
|
|
125
|
+
role: Optional[str] = None,
|
|
126
|
+
health_status: Optional[Any] = None,
|
|
127
|
+
scope_origin: Optional[str] = None,
|
|
128
|
+
include_deleted: Optional[bool] = None,
|
|
129
|
+
limit: Optional[int] = None,
|
|
130
|
+
offset: Optional[int] = None,
|
|
131
|
+
) -> Dict[str, Any]:
|
|
132
|
+
"""List items by (kind, partial scope, role, health). Metadata only."""
|
|
133
|
+
body: Dict[str, Any] = {}
|
|
134
|
+
if kind is not None:
|
|
135
|
+
body["kind"] = kind
|
|
136
|
+
if scope is not None:
|
|
137
|
+
body["scope"] = scope
|
|
138
|
+
if role is not None:
|
|
139
|
+
body["role"] = role
|
|
140
|
+
if health_status is not None:
|
|
141
|
+
body["health_status"] = health_status
|
|
142
|
+
if scope_origin is not None:
|
|
143
|
+
body["scope_origin"] = scope_origin
|
|
144
|
+
if include_deleted is not None:
|
|
145
|
+
body["include_deleted"] = include_deleted
|
|
146
|
+
if limit is not None:
|
|
147
|
+
body["limit"] = limit
|
|
148
|
+
if offset is not None:
|
|
149
|
+
body["offset"] = offset
|
|
150
|
+
return await self._client._request(
|
|
151
|
+
endpoint="/svc/stash/items/list",
|
|
152
|
+
body=body,
|
|
153
|
+
use_gateway=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def mark_verified(
|
|
157
|
+
self, *, id: str, purpose: str, detail: Optional[str] = None
|
|
158
|
+
) -> Optional[Dict[str, Any]]:
|
|
159
|
+
"""Mark an item as verified after a successful use; resets failure_count."""
|
|
160
|
+
body: Dict[str, Any] = {"id": id, "purpose": purpose}
|
|
161
|
+
if detail is not None:
|
|
162
|
+
body["detail"] = detail
|
|
163
|
+
result = await self._client._request(
|
|
164
|
+
endpoint="/svc/stash/items/mark-verified",
|
|
165
|
+
body=body,
|
|
166
|
+
use_gateway=True,
|
|
167
|
+
)
|
|
168
|
+
return result.get("item") if isinstance(result, dict) else None
|
|
169
|
+
|
|
170
|
+
async def mark_failed(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
id: str,
|
|
174
|
+
purpose: str,
|
|
175
|
+
failure_reason: Optional[str] = None,
|
|
176
|
+
detail: Optional[str] = None,
|
|
177
|
+
) -> Optional[Dict[str, Any]]:
|
|
178
|
+
"""Mark an item as failed; increments failure_count, sets last_failed_at."""
|
|
179
|
+
body: Dict[str, Any] = {"id": id, "purpose": purpose}
|
|
180
|
+
if failure_reason is not None:
|
|
181
|
+
body["failure_reason"] = failure_reason
|
|
182
|
+
if detail is not None:
|
|
183
|
+
body["detail"] = detail
|
|
184
|
+
result = await self._client._request(
|
|
185
|
+
endpoint="/svc/stash/items/mark-failed",
|
|
186
|
+
body=body,
|
|
187
|
+
use_gateway=True,
|
|
188
|
+
)
|
|
189
|
+
return result.get("item") if isinstance(result, dict) else None
|
|
190
|
+
|
|
191
|
+
async def delete(self, id: str, *, purpose: Optional[str] = None) -> bool:
|
|
192
|
+
"""Soft-delete an item (sets deleted_at)."""
|
|
193
|
+
body: Dict[str, Any] = {"id": id}
|
|
194
|
+
if purpose is not None:
|
|
195
|
+
body["purpose"] = purpose
|
|
196
|
+
result = await self._client._request(
|
|
197
|
+
endpoint="/svc/stash/items/delete",
|
|
198
|
+
body=body,
|
|
199
|
+
use_gateway=True,
|
|
200
|
+
)
|
|
201
|
+
return bool(result.get("deleted")) if isinstance(result, dict) else False
|
|
202
|
+
|
|
203
|
+
async def register_kind(
|
|
204
|
+
self,
|
|
205
|
+
*,
|
|
206
|
+
kind: str,
|
|
207
|
+
is_sensitive: Optional[bool] = None,
|
|
208
|
+
value_schema: Optional[Dict[str, Any]] = None,
|
|
209
|
+
default_rotation_days: Optional[int] = None,
|
|
210
|
+
verification_probe_ref: Optional[str] = None,
|
|
211
|
+
description: Optional[str] = None,
|
|
212
|
+
) -> Dict[str, Any]:
|
|
213
|
+
"""Register or update a kind in stash.kind_registry. Admin-scoped."""
|
|
214
|
+
body: Dict[str, Any] = {"kind": kind}
|
|
215
|
+
if is_sensitive is not None:
|
|
216
|
+
body["is_sensitive"] = is_sensitive
|
|
217
|
+
if value_schema is not None:
|
|
218
|
+
body["value_schema"] = value_schema
|
|
219
|
+
if default_rotation_days is not None:
|
|
220
|
+
body["default_rotation_days"] = default_rotation_days
|
|
221
|
+
if verification_probe_ref is not None:
|
|
222
|
+
body["verification_probe_ref"] = verification_probe_ref
|
|
223
|
+
if description is not None:
|
|
224
|
+
body["description"] = description
|
|
225
|
+
result = await self._client._request(
|
|
226
|
+
endpoint="/svc/stash/kinds/register",
|
|
227
|
+
body=body,
|
|
228
|
+
use_gateway=True,
|
|
229
|
+
)
|
|
230
|
+
return result.get("kind", {}) if isinstance(result, dict) else {}
|
|
231
|
+
|
|
232
|
+
async def list_kinds(self) -> List[Dict[str, Any]]:
|
|
233
|
+
"""List registered kinds (own + shared merged)."""
|
|
234
|
+
result = await self._client._request(
|
|
235
|
+
endpoint="/svc/stash/kinds/list",
|
|
236
|
+
body={},
|
|
237
|
+
use_gateway=True,
|
|
238
|
+
)
|
|
239
|
+
return result.get("kinds", []) if isinstance(result, dict) else []
|
|
240
|
+
|
|
241
|
+
async def query_audit(
|
|
242
|
+
self,
|
|
243
|
+
*,
|
|
244
|
+
item_id: Optional[str] = None,
|
|
245
|
+
kind: Optional[str] = None,
|
|
246
|
+
caller_id: Optional[str] = None,
|
|
247
|
+
action: Optional[str] = None,
|
|
248
|
+
occurred_after: Optional[str] = None,
|
|
249
|
+
occurred_before: Optional[str] = None,
|
|
250
|
+
limit: Optional[int] = None,
|
|
251
|
+
offset: Optional[int] = None,
|
|
252
|
+
) -> Dict[str, Any]:
|
|
253
|
+
"""Query the append-only audit log. Hashes only — never plaintext."""
|
|
254
|
+
body: Dict[str, Any] = {}
|
|
255
|
+
if item_id is not None:
|
|
256
|
+
body["item_id"] = item_id
|
|
257
|
+
if kind is not None:
|
|
258
|
+
body["kind"] = kind
|
|
259
|
+
if caller_id is not None:
|
|
260
|
+
body["caller_id"] = caller_id
|
|
261
|
+
if action is not None:
|
|
262
|
+
body["action"] = action
|
|
263
|
+
if occurred_after is not None:
|
|
264
|
+
body["occurred_after"] = occurred_after
|
|
265
|
+
if occurred_before is not None:
|
|
266
|
+
body["occurred_before"] = occurred_before
|
|
267
|
+
if limit is not None:
|
|
268
|
+
body["limit"] = limit
|
|
269
|
+
if offset is not None:
|
|
270
|
+
body["offset"] = offset
|
|
271
|
+
return await self._client._request(
|
|
272
|
+
endpoint="/svc/stash/audit/query",
|
|
273
|
+
body=body,
|
|
274
|
+
use_gateway=True,
|
|
275
|
+
)
|
|
@@ -182,6 +182,14 @@ class Dominus:
|
|
|
182
182
|
self.deployer = DeployerNamespace(self)
|
|
183
183
|
self.warden = WardenNamespace(self)
|
|
184
184
|
|
|
185
|
+
# Stash worker (per-scope durable items: credentials + configs)
|
|
186
|
+
from .namespaces.stash import StashNamespace
|
|
187
|
+
self.stash = StashNamespace(self)
|
|
188
|
+
|
|
189
|
+
# Recipe worker (kernel-tier; hosts browser-recipe-v1 today, workflow + envoy + others later)
|
|
190
|
+
from .namespaces.recipes import RecipesNamespace
|
|
191
|
+
self.recipes = RecipesNamespace(self)
|
|
192
|
+
|
|
185
193
|
# Cache for JWT public key
|
|
186
194
|
self._public_key_cache = None
|
|
187
195
|
|
{dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/SOURCES.txt
RENAMED
|
@@ -31,9 +31,11 @@ dominus/namespaces/jobs.py
|
|
|
31
31
|
dominus/namespaces/logs.py
|
|
32
32
|
dominus/namespaces/portal.py
|
|
33
33
|
dominus/namespaces/processor.py
|
|
34
|
+
dominus/namespaces/recipes.py
|
|
34
35
|
dominus/namespaces/redis.py
|
|
35
36
|
dominus/namespaces/secrets.py
|
|
36
37
|
dominus/namespaces/secure.py
|
|
38
|
+
dominus/namespaces/stash.py
|
|
37
39
|
dominus/namespaces/sync.py
|
|
38
40
|
dominus/namespaces/warden.py
|
|
39
41
|
dominus/namespaces/workflow.py
|
|
@@ -59,6 +59,8 @@ async def test_browser_namespace_uses_gateway_routed_api_browser_paths(monkeypat
|
|
|
59
59
|
],
|
|
60
60
|
authority={"run_id": "auth-run-1", "artifact_observations": True},
|
|
61
61
|
metadata={"route": "dashboard"},
|
|
62
|
+
recipe={"version": "browser-recipe-v1", "steps": [{"kind": "goto", "url": "https://summit.example/dashboard"}]},
|
|
63
|
+
recipe_inputs={"tenant": "summit"},
|
|
62
64
|
)
|
|
63
65
|
await sdk.browser.start_run("br_123")
|
|
64
66
|
await sdk.browser.get_run_status("br_123")
|
|
@@ -99,9 +101,11 @@ async def test_browser_namespace_uses_gateway_routed_api_browser_paths(monkeypat
|
|
|
99
101
|
"run_id": "auth-run-1",
|
|
100
102
|
"artifact_observations": True,
|
|
101
103
|
}
|
|
104
|
+
assert ensure_body["recipe"]["version"] == "browser-recipe-v1"
|
|
105
|
+
assert ensure_body["recipe_inputs"] == {"tenant": "summit"}
|
|
102
106
|
|
|
103
107
|
|
|
104
108
|
@pytest.mark.asyncio
|
|
105
|
-
async def
|
|
106
|
-
with pytest.raises(ValueError, match="target is required"):
|
|
109
|
+
async def test_browser_ensure_run_requires_target_or_recipe(sdk):
|
|
110
|
+
with pytest.raises(ValueError, match="target, recipe_ref, or recipe is required"):
|
|
107
111
|
await sdk.browser.ensure_run(target={})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dominus import (
|
|
2
2
|
BrowserNamespace,
|
|
3
3
|
DeployerNamespace,
|
|
4
|
+
RecipesNamespace,
|
|
4
5
|
WardenNamespace,
|
|
5
6
|
gateway_circuit_breaker,
|
|
6
7
|
mint_selected_scope_jwt,
|
|
@@ -31,9 +32,11 @@ def test_top_level_exports_drop_delegate_alias():
|
|
|
31
32
|
assert DeployerNamespace is not None
|
|
32
33
|
assert WardenNamespace is not None
|
|
33
34
|
assert BrowserNamespace is not None
|
|
35
|
+
assert RecipesNamespace is not None
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def test_singleton_exposes_browser_namespace():
|
|
37
39
|
from dominus import dominus
|
|
38
40
|
|
|
39
41
|
assert callable(dominus.browser.get_health)
|
|
42
|
+
assert callable(dominus.recipes.list_types)
|
|
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
|
|
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-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_authority_public_vocabulary.py
RENAMED
|
File without changes
|
{dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_control_plane_namespaces.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
|