dominus-sdk-python 6.1.3__tar.gz → 6.2.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 (69) hide show
  1. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/__init__.py +1 -1
  3. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/authority.py +2 -0
  4. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/recipes.py +109 -3
  5. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  6. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  7. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/pyproject.toml +1 -1
  8. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_authority_public_vocabulary.py +2 -1
  9. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_recipes_namespace.py +2 -2
  10. dominus_sdk_python-6.2.0/tests/test_recipes_stash_routing.py +229 -0
  11. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/README.md +0 -0
  12. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/config/__init__.py +0 -0
  13. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/config/endpoints.py +0 -0
  14. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/errors.py +0 -0
  15. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/__init__.py +0 -0
  16. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/auth.py +0 -0
  17. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/cache.py +0 -0
  18. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/console_capture.py +0 -0
  19. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/core.py +0 -0
  20. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/crypto.py +0 -0
  21. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/sse.py +0 -0
  22. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/helpers/trace.py +0 -0
  23. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/__init__.py +0 -0
  24. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/admin.py +0 -0
  25. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/ai.py +0 -0
  26. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/artifacts.py +0 -0
  27. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/auth.py +0 -0
  28. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/browser.py +0 -0
  29. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/coder.py +0 -0
  30. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/courier.py +0 -0
  31. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/db.py +0 -0
  32. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/ddl.py +0 -0
  33. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/deployer.py +0 -0
  34. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/fastapi.py +0 -0
  35. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/files.py +0 -0
  36. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/health.py +0 -0
  37. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/jobs.py +0 -0
  38. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/logs.py +0 -0
  39. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/platform.py +0 -0
  40. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/portal.py +0 -0
  41. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/processor.py +0 -0
  42. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/publisher.py +0 -0
  43. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/redis.py +0 -0
  44. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/secrets.py +0 -0
  45. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/secure.py +0 -0
  46. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/stash.py +0 -0
  47. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/sync.py +0 -0
  48. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/warden.py +0 -0
  49. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/namespaces/workflow.py +0 -0
  50. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/services/__init__.py +0 -0
  51. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus/start.py +0 -0
  52. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  53. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  54. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  55. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/setup.cfg +0 -0
  56. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_auth.py +0 -0
  57. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_browser_namespace.py +0 -0
  58. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_control_plane_namespaces.py +0 -0
  59. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_errors.py +0 -0
  60. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_flat_commands.py +0 -0
  61. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_health.py +0 -0
  62. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_logs.py +0 -0
  63. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_platform_coder_namespaces.py +0 -0
  64. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_provisioning_parity.py +0 -0
  65. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_public_exports.py +0 -0
  66. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_publisher_namespace.py +0 -0
  67. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_transport_compat.py +0 -0
  68. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_workflow_lifecycle.py +0 -0
  69. {dominus_sdk_python-6.1.3 → dominus_sdk_python-6.2.0}/tests/test_workflow_refs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 6.1.3
3
+ Version: 6.2.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -165,7 +165,7 @@ from .errors import (
165
165
  TimeoutError as DominusTimeoutError,
166
166
  )
167
167
 
168
- __version__ = "6.1.3"
168
+ __version__ = "6.2.0"
169
169
  __all__ = [
170
170
  # Main SDK instance
171
171
  "dominus",
@@ -1155,6 +1155,7 @@ class AuthorityNamespace:
1155
1155
  until: Optional[str] = None,
1156
1156
  window_hours: Optional[int] = None,
1157
1157
  limit: Optional[int] = None,
1158
+ full: bool = False,
1158
1159
  timeout: Optional[float] = None,
1159
1160
  ) -> Dict[str, Any]:
1160
1161
  """
@@ -1172,6 +1173,7 @@ class AuthorityNamespace:
1172
1173
  "until": until,
1173
1174
  "window_hours": window_hours,
1174
1175
  "limit": limit,
1176
+ "full": 1 if full else None,
1175
1177
  })
1176
1178
  return await self._get(
1177
1179
  f"/api/authority/dossiers/deploy/{quote(deploy_id, safe='')}{qs}",
@@ -12,16 +12,76 @@ Refs follow ``recipe://{type}/{name}[@v{N}][?tier={tier}]``.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import json
15
16
  from typing import Any, Dict, List, Optional, TYPE_CHECKING
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from ..start import Dominus
19
20
 
20
21
 
22
+ STASH_BACKED_RECIPE_TYPES = frozenset({
23
+ "envoy-bootstrap-v1",
24
+ "envoy-release-publish-v1",
25
+ "envoy-policy-v1",
26
+ "pacs-extraction-recipe-v1",
27
+ "pacs-vendor-probe-v1",
28
+ "browser-recipe",
29
+ })
30
+
31
+
21
32
  def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
22
33
  return {k: v for k, v in payload.items() if v is not None and v != ""}
23
34
 
24
35
 
36
+ def _recipe_tier_to_stash_scope(tier: Optional[str]) -> str:
37
+ if tier == "platform":
38
+ return "platform"
39
+ if tier == "group":
40
+ return "group"
41
+ return "self"
42
+
43
+
44
+ def _resolve_recipe_stash_env(client: "Dominus", env: Optional[str]) -> str:
45
+ return env or getattr(client, "_gateway_env", None) or "production"
46
+
47
+
48
+ def _recipe_body_to_text(value: Any) -> str:
49
+ if isinstance(value, str):
50
+ return value
51
+ if value is None:
52
+ raise ValueError("body is required")
53
+ return json.dumps(value, separators=(",", ":"))
54
+
55
+
56
+ def _build_recipe_payload_from_stash(
57
+ *,
58
+ type: str,
59
+ name: str,
60
+ tier: Optional[str],
61
+ data: Dict[str, Any],
62
+ ) -> Dict[str, Any]:
63
+ version = data.get("version")
64
+ try:
65
+ version_int = int(version) if version is not None else None
66
+ except (TypeError, ValueError):
67
+ version_int = None
68
+ return {
69
+ "recipe": {
70
+ "metadata": {
71
+ "type": type,
72
+ "name": name,
73
+ "tier": data.get("resolved_tier") or tier,
74
+ "version": version_int,
75
+ "schema_version": 1,
76
+ "sha256": data.get("value_hash"),
77
+ "head_ref": data.get("head_ref"),
78
+ "snapshot_ref": data.get("snapshot_ref"),
79
+ },
80
+ "body": data.get("value"),
81
+ },
82
+ }
83
+
84
+
25
85
  class RecipesNamespace:
26
86
  """
27
87
  Recipe worker namespace.
@@ -86,16 +146,41 @@ class RecipesNamespace:
86
146
  body: str,
87
147
  description: Optional[str] = None,
88
148
  actor_context: Optional[Dict[str, str]] = None,
149
+ env: Optional[str] = None,
89
150
  timeout: float = 30.0,
90
151
  ) -> Dict[str, Any]:
91
- """Publish a recipe. ``POST /api/recipe/recipes/publish``."""
152
+ """Publish a recipe. Stash-backed recipe types accept optional ``env``."""
153
+ body_text = _recipe_body_to_text(body)
154
+ if type in STASH_BACKED_RECIPE_TYPES:
155
+ result = await self._client._request(
156
+ endpoint="/svc/stash/put",
157
+ method="POST",
158
+ body={
159
+ "env": _resolve_recipe_stash_env(self._client, env),
160
+ "kind": type,
161
+ "scope": _recipe_tier_to_stash_scope(tier),
162
+ "key": name,
163
+ "value": body_text,
164
+ "versioned": True,
165
+ "purpose": description or "sdk-recipe-publish",
166
+ },
167
+ use_gateway=True,
168
+ actor=actor_context,
169
+ timeout=timeout,
170
+ )
171
+ return _build_recipe_payload_from_stash(
172
+ type=type,
173
+ name=name,
174
+ tier=tier,
175
+ data=result,
176
+ )
92
177
  return await self._post(
93
178
  "/api/recipe/recipes/publish",
94
179
  _compact({
95
180
  "type": type,
96
181
  "tier": tier,
97
182
  "name": name,
98
- "body": body,
183
+ "body": body_text,
99
184
  "description": description,
100
185
  }),
101
186
  actor_context=actor_context,
@@ -167,9 +252,30 @@ class RecipesNamespace:
167
252
  name: str,
168
253
  version: Optional[int] = None,
169
254
  tier: Optional[str] = None,
255
+ env: Optional[str] = None,
170
256
  timeout: float = 15.0,
171
257
  ) -> Dict[str, Any]:
172
- """Resolve a recipe through the three-tier chain. ``GET /api/recipe/recipes/{type}/{name}[@v{N}]``."""
258
+ """Resolve a recipe. Stash-backed recipe types accept optional ``env``."""
259
+ if type in STASH_BACKED_RECIPE_TYPES:
260
+ result = await self._client._request(
261
+ endpoint="/svc/stash/get",
262
+ method="POST",
263
+ body={
264
+ "env": _resolve_recipe_stash_env(self._client, env),
265
+ "kind": type,
266
+ "scope": _recipe_tier_to_stash_scope(tier),
267
+ "key": name,
268
+ "version": version if version is not None else "head",
269
+ },
270
+ use_gateway=True,
271
+ timeout=timeout,
272
+ )
273
+ return _build_recipe_payload_from_stash(
274
+ type=type,
275
+ name=name,
276
+ tier=tier,
277
+ data=result,
278
+ )
173
279
  path = f"/api/recipe/recipes/{type}/{name}"
174
280
  if version is not None:
175
281
  path += f"@v{version}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 6.1.3
3
+ Version: 6.2.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -61,6 +61,7 @@ tests/test_provisioning_parity.py
61
61
  tests/test_public_exports.py
62
62
  tests/test_publisher_namespace.py
63
63
  tests/test_recipes_namespace.py
64
+ tests/test_recipes_stash_routing.py
64
65
  tests/test_transport_compat.py
65
66
  tests/test_workflow_lifecycle.py
66
67
  tests/test_workflow_refs.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "6.1.3"
7
+ version = "6.2.0"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -217,6 +217,7 @@ async def test_authority_scope_methods_use_canonical_context_wire_names():
217
217
  until="2026-04-11T09:33:00Z",
218
218
  window_hours=24,
219
219
  limit=150,
220
+ full=True,
220
221
  )
221
222
  await namespace.query_timelines(
222
223
  subject="PCM47474562",
@@ -314,7 +315,7 @@ async def test_authority_scope_methods_use_canonical_context_wire_names():
314
315
  )
315
316
  assert client.calls[7]["endpoint"] == (
316
317
  "/api/authority/dossiers/deploy/deploy-1?"
317
- "since=2026-04-11T08%3A33%3A00Z&until=2026-04-11T09%3A33%3A00Z&window_hours=24&limit=150"
318
+ "since=2026-04-11T08%3A33%3A00Z&until=2026-04-11T09%3A33%3A00Z&window_hours=24&limit=150&full=1"
318
319
  )
319
320
  assert client.calls[8]["body"] == {
320
321
  "subject": "PCM47474562",
@@ -61,7 +61,7 @@ async def test_recipes_namespace_passes_actor_context_on_publish(monkeypatch, sd
61
61
 
62
62
  actor = {"type": "user", "id": "user-1"}
63
63
  await sdk.recipes.publish(
64
- type="browser-recipe",
64
+ type="legacy-recipe-type",
65
65
  tier="project",
66
66
  name="cms-login",
67
67
  body="version: browser-recipe-v1\nsteps: []\n",
@@ -73,7 +73,7 @@ async def test_recipes_namespace_passes_actor_context_on_publish(monkeypatch, sd
73
73
  "endpoint": "/api/recipe/recipes/publish",
74
74
  "method": "POST",
75
75
  "body": {
76
- "type": "browser-recipe",
76
+ "type": "legacy-recipe-type",
77
77
  "tier": "project",
78
78
  "name": "cms-login",
79
79
  "body": "version: browser-recipe-v1\nsteps: []\n",
@@ -0,0 +1,229 @@
1
+ import pytest
2
+
3
+ import dominus.start as start_module
4
+
5
+
6
+ @pytest.fixture()
7
+ def sdk(monkeypatch):
8
+ monkeypatch.setattr(start_module, "_VALIDATION_ERROR", None)
9
+ monkeypatch.setattr(start_module, "_TOKEN", "a" * 64)
10
+ monkeypatch.setattr(start_module, "_VALIDATED", False)
11
+ monkeypatch.setattr(start_module, "_BASE_URL", "https://gateway.example")
12
+ monkeypatch.setattr(start_module, "_GATEWAY_URL", "https://gateway.example")
13
+ return start_module.Dominus()
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_publish_stash_backed_type_routes_to_stash(monkeypatch, sdk):
18
+ calls = []
19
+
20
+ async def fake_request(**kwargs):
21
+ calls.append(kwargs)
22
+ return {
23
+ "version": 1,
24
+ "value": "channel: prod\n",
25
+ "value_hash": "abc123",
26
+ "resolved_tier": "platform",
27
+ "head_ref": "stash://envoy-bootstrap-v1/envoy-prod@head",
28
+ "snapshot_ref": "stash://envoy-bootstrap-v1/envoy-prod@v1",
29
+ }
30
+
31
+ monkeypatch.setattr(sdk, "_request", fake_request)
32
+
33
+ result = await sdk.recipes.publish(
34
+ type="envoy-bootstrap-v1",
35
+ tier="platform",
36
+ name="envoy-prod",
37
+ body="channel: prod\n",
38
+ description="publish bootstrap",
39
+ env="staging",
40
+ )
41
+
42
+ assert len(calls) == 1
43
+ assert calls[0]["endpoint"] == "/svc/stash/put"
44
+ assert calls[0]["method"] == "POST"
45
+ assert calls[0]["use_gateway"] is True
46
+ assert calls[0]["timeout"] == 30.0
47
+ assert calls[0]["body"] == {
48
+ "env": "staging",
49
+ "kind": "envoy-bootstrap-v1",
50
+ "scope": "platform",
51
+ "key": "envoy-prod",
52
+ "value": "channel: prod\n",
53
+ "versioned": True,
54
+ "purpose": "publish bootstrap",
55
+ }
56
+ assert result == {
57
+ "recipe": {
58
+ "metadata": {
59
+ "type": "envoy-bootstrap-v1",
60
+ "name": "envoy-prod",
61
+ "tier": "platform",
62
+ "version": 1,
63
+ "schema_version": 1,
64
+ "sha256": "abc123",
65
+ "head_ref": "stash://envoy-bootstrap-v1/envoy-prod@head",
66
+ "snapshot_ref": "stash://envoy-bootstrap-v1/envoy-prod@v1",
67
+ },
68
+ "body": "channel: prod\n",
69
+ }
70
+ }
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_publish_non_stash_backed_type_keeps_legacy_path(monkeypatch, sdk):
75
+ calls = []
76
+
77
+ async def fake_request(**kwargs):
78
+ calls.append(kwargs)
79
+ return {"ok": True, "recipe": {"version": 7}}
80
+
81
+ monkeypatch.setattr(sdk, "_request", fake_request)
82
+
83
+ result = await sdk.recipes.publish(
84
+ type="some-unmigrated-type",
85
+ tier="group",
86
+ name="legacy-recipe",
87
+ body="steps: []\n",
88
+ env="staging",
89
+ )
90
+
91
+ assert result["recipe"]["version"] == 7
92
+ assert calls == [
93
+ {
94
+ "endpoint": "/api/recipe/recipes/publish",
95
+ "method": "POST",
96
+ "body": {
97
+ "type": "some-unmigrated-type",
98
+ "tier": "group",
99
+ "name": "legacy-recipe",
100
+ "body": "steps: []\n",
101
+ },
102
+ "use_gateway": True,
103
+ "timeout": 30.0,
104
+ }
105
+ ]
106
+
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_get_stash_backed_type_routes_to_stash(monkeypatch, sdk):
110
+ calls = []
111
+
112
+ async def fake_request(**kwargs):
113
+ calls.append(kwargs)
114
+ return {
115
+ "version": "2",
116
+ "value": "operation: fetch\n",
117
+ "value_hash": "def456",
118
+ "resolved_tier": "group",
119
+ "head_ref": "stash://pacs-extraction-recipe-v1/fuji-x@head",
120
+ "snapshot_ref": "stash://pacs-extraction-recipe-v1/fuji-x@v2",
121
+ }
122
+
123
+ monkeypatch.setattr(sdk, "_request", fake_request)
124
+
125
+ result = await sdk.recipes.get(
126
+ type="pacs-extraction-recipe-v1",
127
+ name="fuji-x",
128
+ version=2,
129
+ tier="group",
130
+ env="development",
131
+ )
132
+
133
+ assert len(calls) == 1
134
+ assert calls[0] == {
135
+ "endpoint": "/svc/stash/get",
136
+ "method": "POST",
137
+ "body": {
138
+ "env": "development",
139
+ "kind": "pacs-extraction-recipe-v1",
140
+ "scope": "group",
141
+ "key": "fuji-x",
142
+ "version": 2,
143
+ },
144
+ "use_gateway": True,
145
+ "timeout": 15.0,
146
+ }
147
+ assert result == {
148
+ "recipe": {
149
+ "metadata": {
150
+ "type": "pacs-extraction-recipe-v1",
151
+ "name": "fuji-x",
152
+ "tier": "group",
153
+ "version": 2,
154
+ "schema_version": 1,
155
+ "sha256": "def456",
156
+ "head_ref": "stash://pacs-extraction-recipe-v1/fuji-x@head",
157
+ "snapshot_ref": "stash://pacs-extraction-recipe-v1/fuji-x@v2",
158
+ },
159
+ "body": "operation: fetch\n",
160
+ }
161
+ }
162
+
163
+
164
+ @pytest.mark.asyncio
165
+ async def test_get_non_stash_backed_type_keeps_legacy_path(monkeypatch, sdk):
166
+ calls = []
167
+
168
+ async def fake_request(**kwargs):
169
+ calls.append(kwargs)
170
+ return {"recipe": {"metadata": {"name": "y"}}}
171
+
172
+ monkeypatch.setattr(sdk, "_request", fake_request)
173
+
174
+ result = await sdk.recipes.get(type="other", name="y", env="staging")
175
+
176
+ assert result["recipe"]["metadata"]["name"] == "y"
177
+ assert calls == [
178
+ {
179
+ "endpoint": "/api/recipe/recipes/other/y",
180
+ "method": "GET",
181
+ "use_gateway": True,
182
+ "timeout": 15.0,
183
+ }
184
+ ]
185
+
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_recipe_body_to_text_validation(monkeypatch, sdk):
189
+ calls = []
190
+
191
+ async def fake_request(**kwargs):
192
+ calls.append(kwargs)
193
+ return {
194
+ "version": len(calls),
195
+ "value": kwargs["body"]["value"],
196
+ "value_hash": f"hash-{len(calls)}",
197
+ "resolved_tier": "self",
198
+ }
199
+
200
+ monkeypatch.setattr(sdk, "_request", fake_request)
201
+
202
+ string_result = await sdk.recipes.publish(
203
+ type="browser-recipe",
204
+ tier="project",
205
+ name="string-body",
206
+ body="steps: []\n",
207
+ )
208
+ dict_result = await sdk.recipes.publish(
209
+ type="browser-recipe",
210
+ tier="project",
211
+ name="dict-body",
212
+ body={"steps": []},
213
+ )
214
+
215
+ assert calls[0]["body"]["env"] == "production"
216
+ assert calls[0]["body"]["value"] == "steps: []\n"
217
+ assert string_result["recipe"]["body"] == "steps: []\n"
218
+ assert calls[1]["body"]["value"] == '{"steps":[]}'
219
+ assert dict_result["recipe"]["body"] == '{"steps":[]}'
220
+
221
+ with pytest.raises(ValueError, match="body is required"):
222
+ await sdk.recipes.publish(
223
+ type="browser-recipe",
224
+ tier="project",
225
+ name="missing-body",
226
+ body=None,
227
+ )
228
+
229
+ assert len(calls) == 2