dominus-sdk-python 9.0.2__tar.gz → 9.0.4__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 (76) hide show
  1. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/PKG-INFO +3 -20
  2. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/README.md +2 -19
  3. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/__init__.py +1 -1
  4. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/errors.py +227 -58
  5. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/auth.py +1 -1
  6. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus_sdk_python.egg-info/PKG-INFO +3 -20
  7. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  8. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/pyproject.toml +1 -1
  9. dominus_sdk_python-9.0.4/tests/test_error_recovery_action.py +31 -0
  10. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_readme_quickstart_catalog.py +45 -27
  11. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/config/__init__.py +0 -0
  12. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/config/endpoints.py +0 -0
  13. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/__init__.py +0 -0
  14. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/cache.py +0 -0
  15. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/console_capture.py +0 -0
  16. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/core.py +0 -0
  17. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/crypto.py +0 -0
  18. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/sse.py +0 -0
  19. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/helpers/trace.py +0 -0
  20. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/__init__.py +0 -0
  21. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/admin.py +0 -0
  22. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/ai.py +0 -0
  23. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/artifacts.py +0 -0
  24. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/auth.py +0 -0
  25. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/authority.py +0 -0
  26. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/browser.py +0 -0
  27. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/coder.py +0 -0
  28. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/courier.py +0 -0
  29. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/db.py +0 -0
  30. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/ddl.py +0 -0
  31. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/deployer.py +0 -0
  32. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/fastapi.py +0 -0
  33. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/files.py +0 -0
  34. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/health.py +0 -0
  35. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/jobs.py +0 -0
  36. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/logs.py +0 -0
  37. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/platform.py +0 -0
  38. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/portal.py +0 -0
  39. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/processor.py +0 -0
  40. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/publisher.py +0 -0
  41. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/recipes.py +0 -0
  42. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/redis.py +0 -0
  43. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/secrets.py +0 -0
  44. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/secure.py +0 -0
  45. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/stash.py +0 -0
  46. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/sync.py +0 -0
  47. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/warden.py +0 -0
  48. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/namespaces/workflow.py +0 -0
  49. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/py.typed +0 -0
  50. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/services/__init__.py +0 -0
  51. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus/start.py +0 -0
  52. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  53. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus_sdk_python.egg-info/requires.txt +0 -0
  54. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  55. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/setup.cfg +0 -0
  56. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_auth.py +0 -0
  57. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_authority_public_vocabulary.py +0 -0
  58. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_browser_namespace.py +0 -0
  59. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_control_plane_namespaces.py +0 -0
  60. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_errors.py +0 -0
  61. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_flat_commands.py +0 -0
  62. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_health.py +0 -0
  63. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_logs.py +0 -0
  64. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_namespace_barrel_exports.py +0 -0
  65. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_platform_coder_namespaces.py +0 -0
  66. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_provisioning_parity.py +0 -0
  67. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_public_exports.py +0 -0
  68. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_publisher_namespace.py +0 -0
  69. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_recipes_namespace.py +0 -0
  70. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_recipes_stash_routing.py +0 -0
  71. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_secrets_presence_contract.py +0 -0
  72. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_stash_artifact_facade.py +0 -0
  73. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_stash_tables_namespace.py +0 -0
  74. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_transport_compat.py +0 -0
  75. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/tests/test_workflow_lifecycle.py +0 -0
  76. {dominus_sdk_python-9.0.2 → dominus_sdk_python-9.0.4}/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: 9.0.2
3
+ Version: 9.0.4
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
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.2`
51
+ - Current package version: `9.0.4`
52
52
 
53
53
  ## Install
54
54
 
@@ -67,13 +67,12 @@ for operators and advanced use; this path is the stable teaching surface.
67
67
  Set both values before the Python process imports the SDK singleton:
68
68
 
69
69
  ```bash
70
- export DOMINUS_TOKEN="your-psk-token"
70
+ export DOMINUS_USER_TOKEN="your-user-token"
71
71
  export DOMINUS_PROJECT="your-project-slug"
72
72
  ```
73
73
 
74
74
  ```python
75
75
  import asyncio
76
- import base64
77
76
 
78
77
  from dominus import dominus
79
78
 
@@ -83,22 +82,6 @@ async def main() -> None:
83
82
  workflow_recipe_ref="recipe://workflow-recipe-v1/hello@v1",
84
83
  )
85
84
 
86
- await dominus.artifacts.store_v2(
87
- group="your-group",
88
- owner="your-project-slug",
89
- environment="production",
90
- kind="blob",
91
- artifact_key="hello",
92
- data=base64.b64encode(b"hello dominus").decode("ascii"),
93
- )
94
- artifact = await dominus.artifacts.retrieve_v2(
95
- group="your-group",
96
- owner="your-project-slug",
97
- environment="production",
98
- kind="blob",
99
- artifact_key="hello",
100
- )
101
-
102
85
  stash_result = await dominus.stash.upsert(
103
86
  kind="config",
104
87
  scope={"env": "production"},
@@ -15,7 +15,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
15
15
  - Gateway-scoped client mode for MCP and other user-JWT sessions
16
16
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
17
17
  - Local helpers for JWT verification, trace propagation, retries, and console capture
18
- - Current package version: `9.0.2`
18
+ - Current package version: `9.0.4`
19
19
 
20
20
  ## Install
21
21
 
@@ -34,13 +34,12 @@ for operators and advanced use; this path is the stable teaching surface.
34
34
  Set both values before the Python process imports the SDK singleton:
35
35
 
36
36
  ```bash
37
- export DOMINUS_TOKEN="your-psk-token"
37
+ export DOMINUS_USER_TOKEN="your-user-token"
38
38
  export DOMINUS_PROJECT="your-project-slug"
39
39
  ```
40
40
 
41
41
  ```python
42
42
  import asyncio
43
- import base64
44
43
 
45
44
  from dominus import dominus
46
45
 
@@ -50,22 +49,6 @@ async def main() -> None:
50
49
  workflow_recipe_ref="recipe://workflow-recipe-v1/hello@v1",
51
50
  )
52
51
 
53
- await dominus.artifacts.store_v2(
54
- group="your-group",
55
- owner="your-project-slug",
56
- environment="production",
57
- kind="blob",
58
- artifact_key="hello",
59
- data=base64.b64encode(b"hello dominus").decode("ascii"),
60
- )
61
- artifact = await dominus.artifacts.retrieve_v2(
62
- group="your-group",
63
- owner="your-project-slug",
64
- environment="production",
65
- kind="blob",
66
- artifact_key="hello",
67
- )
68
-
69
52
  stash_result = await dominus.stash.upsert(
70
53
  kind="config",
71
54
  scope={"env": "production"},
@@ -178,7 +178,7 @@ from .errors import (
178
178
  TimeoutError as DominusTimeoutError,
179
179
  )
180
180
 
181
- __version__ = "9.0.2"
181
+ __version__ = "9.0.4"
182
182
  __all__ = [
183
183
  # Main SDK instance
184
184
  "dominus",
@@ -4,6 +4,45 @@ Dominus SDK Error Classes
4
4
  Custom exceptions for the Dominus SDK with structured error information.
5
5
  """
6
6
  from typing import Any, Dict, Optional
7
+ from uuid import uuid4
8
+
9
+
10
+ CANONICAL_ERROR_KINDS = (
11
+ "unauthorized",
12
+ "forbidden",
13
+ "not_found",
14
+ "validation_failed",
15
+ "conflict",
16
+ "rate_limited",
17
+ "upstream_unavailable",
18
+ "internal",
19
+ "timeout",
20
+ "canceled",
21
+ "unsupported_version",
22
+ )
23
+
24
+ RECOVERY_ACTIONS = ("retry", "reauth", "escalate", "fix_config")
25
+
26
+
27
+ def recovery_action_for_kind(kind: str, retryable: Optional[bool] = None) -> str:
28
+ if kind == "unauthorized":
29
+ return "reauth"
30
+ if kind in {"rate_limited", "timeout"}:
31
+ return "retry"
32
+ if kind == "upstream_unavailable":
33
+ return "retry" if retryable is not False else "escalate"
34
+ if kind == "internal":
35
+ return "escalate"
36
+ if kind in {
37
+ "validation_failed",
38
+ "unsupported_version",
39
+ "not_found",
40
+ "forbidden",
41
+ "conflict",
42
+ "canceled",
43
+ }:
44
+ return "fix_config"
45
+ return "escalate"
7
46
 
8
47
 
9
48
  def _first_string(*values: Any) -> Optional[str]:
@@ -59,6 +98,38 @@ def _normalize_kernel_details(
59
98
  return normalized
60
99
 
61
100
 
101
+ def _kind_from_details(status_code: Optional[int], details: Dict[str, Any]) -> str:
102
+ explicit = _first_string(details.get("kind"))
103
+ if explicit in CANONICAL_ERROR_KINDS:
104
+ return explicit
105
+ code = (_first_string(details.get("code")) or "").lower()
106
+ if "unsupported_version" in code:
107
+ return "unsupported_version"
108
+ if "timeout" in code or status_code in {408, 504}:
109
+ return "timeout"
110
+ if "cancel" in code or status_code == 499:
111
+ return "canceled"
112
+ if status_code == 401:
113
+ return "unauthorized"
114
+ if status_code == 403:
115
+ return "forbidden"
116
+ if status_code == 404:
117
+ return "not_found"
118
+ if status_code in {400, 422}:
119
+ return "validation_failed"
120
+ if status_code == 409:
121
+ return "conflict"
122
+ if status_code == 429:
123
+ return "rate_limited"
124
+ if status_code and status_code >= 500:
125
+ return "upstream_unavailable"
126
+ return "internal"
127
+
128
+
129
+ def _default_retryable(kind: str) -> bool:
130
+ return kind in {"rate_limited", "upstream_unavailable", "timeout"}
131
+
132
+
62
133
  class DominusError(Exception):
63
134
  """
64
135
  Base exception for all Dominus SDK errors.
@@ -70,17 +141,21 @@ class DominusError(Exception):
70
141
  message: Human-readable error message
71
142
  status_code: HTTP status code (if applicable)
72
143
  code: Stable machine-readable error code
144
+ kind: Canonical error kind from the closed-sum taxonomy (errors.spec.json)
73
145
  category: Error category from the kernel contract
74
146
  details: Additional error details from backend
75
147
  endpoint: The endpoint that was called (if applicable)
76
148
  """
77
149
 
150
+ kind: str = "internal"
151
+
78
152
  def __init__(
79
153
  self,
80
154
  message: str,
81
155
  status_code: Optional[int] = None,
82
156
  details: Optional[Dict[str, Any]] = None,
83
- endpoint: Optional[str] = None
157
+ endpoint: Optional[str] = None,
158
+ kind: Optional[str] = None,
84
159
  ):
85
160
  self.message = message
86
161
  self.status_code = status_code
@@ -91,6 +166,11 @@ class DominusError(Exception):
91
166
  self.trace_id = _first_string(self.details.get("trace_id"), self.details.get("traceId"))
92
167
  self.request_id = _first_string(self.details.get("request_id"), self.details.get("requestId"))
93
168
  self.retryable = _first_bool(self.details.get("retryable"))
169
+ self.kind = kind or _kind_from_details(status_code, self.details)
170
+ self.correlation_id = _first_string(self.details.get("correlation_id"), self.details.get("correlationId"))
171
+ self.cause = self.details.get("cause")
172
+ self.recovery_action = recovery_action_for_kind(self.kind, self.retryable)
173
+ self.details.setdefault("recovery_action", self.recovery_action)
94
174
  super().__init__(self.message)
95
175
 
96
176
  def __str__(self) -> str:
@@ -107,6 +187,7 @@ class DominusError(Exception):
107
187
  return (
108
188
  f"DominusError(message={self.message!r}, "
109
189
  f"status_code={self.status_code}, "
190
+ f"kind={self.kind!r}, "
110
191
  f"code={self.code!r}, "
111
192
  f"category={self.category!r}, "
112
193
  f"details={self.details!r}, "
@@ -114,21 +195,40 @@ class DominusError(Exception):
114
195
  )
115
196
 
116
197
  def to_dict(self) -> Dict[str, Any]:
198
+ request_id = self.request_id or str(uuid4())
199
+ self.request_id = request_id
200
+ self.details.setdefault("request_id", request_id)
201
+ http_status = self.status_code or 500
202
+ retryable = self.retryable if self.retryable is not None else _default_retryable(self.kind)
117
203
  return {
118
204
  "message": self.message,
119
205
  "status_code": self.status_code,
120
206
  "code": self.code,
207
+ "kind": self.kind,
121
208
  "category": self.category,
122
209
  "details": self.details,
123
210
  "endpoint": self.endpoint,
124
211
  "trace_id": self.trace_id,
125
- "request_id": self.request_id,
126
- "retryable": self.retryable,
212
+ "request_id": request_id,
213
+ "correlation_id": self.correlation_id,
214
+ "retryable": retryable,
215
+ "recovery_action": self.recovery_action,
216
+ "http_status": http_status,
217
+ "envelope_version": "1",
218
+ **({"cause": self.cause} if self.cause is not None else {}),
127
219
  }
128
220
 
129
221
 
130
- class AuthenticationError(DominusError):
222
+ # ---------------------------------------------------------------------------
223
+ # Per-kind canonical subclasses (Seams fork 6, ADR 0010).
224
+ # Retired kind codes: dominus-cli/docs/deprecation-table.json (M-1).
225
+ # One derived class per canonical `kind` from errors.spec.json.
226
+ # Existing class names are preserved as aliases (v1); deprecated in v2.
227
+ # ---------------------------------------------------------------------------
228
+
229
+ class UnauthorizedError(DominusError):
131
230
  """Raised when authentication fails (invalid token, expired JWT, etc.)."""
231
+ kind = "unauthorized"
132
232
 
133
233
  def __init__(
134
234
  self,
@@ -137,11 +237,12 @@ class AuthenticationError(DominusError):
137
237
  details: Optional[Dict[str, Any]] = None,
138
238
  endpoint: Optional[str] = None
139
239
  ):
140
- super().__init__(message, status_code, details, endpoint)
240
+ super().__init__(message, status_code, details, endpoint, kind="unauthorized")
141
241
 
142
242
 
143
- class AuthorizationError(DominusError):
243
+ class ForbiddenError(DominusError):
144
244
  """Raised when authorization fails (insufficient permissions)."""
245
+ kind = "forbidden"
145
246
 
146
247
  def __init__(
147
248
  self,
@@ -150,11 +251,12 @@ class AuthorizationError(DominusError):
150
251
  details: Optional[Dict[str, Any]] = None,
151
252
  endpoint: Optional[str] = None
152
253
  ):
153
- super().__init__(message, status_code, details, endpoint)
254
+ super().__init__(message, status_code, details, endpoint, kind="forbidden")
154
255
 
155
256
 
156
257
  class NotFoundError(DominusError):
157
258
  """Raised when a requested resource is not found."""
259
+ kind = "not_found"
158
260
 
159
261
  def __init__(
160
262
  self,
@@ -163,11 +265,12 @@ class NotFoundError(DominusError):
163
265
  details: Optional[Dict[str, Any]] = None,
164
266
  endpoint: Optional[str] = None
165
267
  ):
166
- super().__init__(message, status_code, details, endpoint)
268
+ super().__init__(message, status_code, details, endpoint, kind="not_found")
167
269
 
168
270
 
169
- class ValidationError(DominusError):
271
+ class ValidationFailedError(DominusError):
170
272
  """Raised when request validation fails."""
273
+ kind = "validation_failed"
171
274
 
172
275
  def __init__(
173
276
  self,
@@ -176,11 +279,12 @@ class ValidationError(DominusError):
176
279
  details: Optional[Dict[str, Any]] = None,
177
280
  endpoint: Optional[str] = None
178
281
  ):
179
- super().__init__(message, status_code, details, endpoint)
282
+ super().__init__(message, status_code, details, endpoint, kind="validation_failed")
180
283
 
181
284
 
182
285
  class ConflictError(DominusError):
183
286
  """Raised when there's a conflict (duplicate key, version mismatch, etc.)."""
287
+ kind = "conflict"
184
288
 
185
289
  def __init__(
186
290
  self,
@@ -189,87 +293,134 @@ class ConflictError(DominusError):
189
293
  details: Optional[Dict[str, Any]] = None,
190
294
  endpoint: Optional[str] = None
191
295
  ):
192
- super().__init__(message, status_code, details, endpoint)
296
+ super().__init__(message, status_code, details, endpoint, kind="conflict")
193
297
 
194
298
 
195
- class ServiceError(DominusError):
196
- """Raised when a backend service error occurs."""
299
+ class RateLimitedError(DominusError):
300
+ """Raised when a request is rate-limited (429)."""
301
+ kind = "rate_limited"
197
302
 
198
303
  def __init__(
199
304
  self,
200
- message: str = "Service error",
201
- status_code: int = 500,
305
+ message: str = "Rate limit exceeded",
306
+ status_code: int = 429,
202
307
  details: Optional[Dict[str, Any]] = None,
203
308
  endpoint: Optional[str] = None
204
309
  ):
205
- super().__init__(message, status_code, details, endpoint)
310
+ super().__init__(message, status_code, details, endpoint, kind="rate_limited")
206
311
 
207
312
 
208
- class StashBookmarkArtifactNotFound(NotFoundError):
209
- """Raised when a stash artifact bookmark target cannot be found."""
210
-
211
-
212
- class StashBookmarkInvalidVersionRef(ValidationError):
213
- """Raised when a stash artifact bookmark version_ref is invalid."""
214
-
215
-
216
- class StashBookmarkDuplicateName(ConflictError):
217
- """Raised when a stash artifact bookmark name is already in use."""
218
-
219
-
220
- class StashBookmarkUpstreamRejected(ServiceError):
221
- """Raised when the artifact backend rejects a stash bookmark operation."""
222
-
223
-
224
- class StashWatchInvalidWebhookUrl(ValidationError):
225
- """Raised when a stash artifact watcher webhook_url is invalid."""
313
+ class UpstreamUnavailableError(DominusError):
314
+ """Raised when a backend service is unavailable (5xx)."""
315
+ kind = "upstream_unavailable"
226
316
 
317
+ def __init__(
318
+ self,
319
+ message: str = "Service unavailable",
320
+ status_code: int = 500,
321
+ details: Optional[Dict[str, Any]] = None,
322
+ endpoint: Optional[str] = None
323
+ ):
324
+ super().__init__(message, status_code, details, endpoint, kind="upstream_unavailable")
227
325
 
228
- class StashWatchUpstreamRejected(ServiceError):
229
- """Raised when the artifact backend rejects a stash watch operation."""
230
326
 
327
+ class InternalError(DominusError):
328
+ """Raised for unexpected internal errors."""
329
+ kind = "internal"
231
330
 
232
- class StashWatchNotFound(NotFoundError):
233
- """Raised when a stash artifact watcher cannot be found."""
331
+ def __init__(
332
+ self,
333
+ message: str = "Internal error",
334
+ status_code: int = 500,
335
+ details: Optional[Dict[str, Any]] = None,
336
+ endpoint: Optional[str] = None
337
+ ):
338
+ super().__init__(message, status_code, details, endpoint, kind="internal")
234
339
 
235
340
 
236
- class ConnectionError(DominusError):
237
- """Raised when connection to the backend fails."""
341
+ class TimeoutError(DominusError):
342
+ """Raised when a request times out."""
343
+ kind = "timeout"
238
344
 
239
345
  def __init__(
240
346
  self,
241
- message: str = "Connection failed",
347
+ message: str = "Request timed out",
242
348
  status_code: Optional[int] = None,
243
349
  details: Optional[Dict[str, Any]] = None,
244
350
  endpoint: Optional[str] = None
245
351
  ):
246
- super().__init__(message, status_code, details, endpoint)
352
+ super().__init__(message, status_code, details, endpoint, kind="timeout")
247
353
 
248
354
 
249
- class TimeoutError(DominusError):
250
- """Raised when a request times out."""
355
+ class CanceledError(DominusError):
356
+ """Raised when an operation is canceled."""
357
+ kind = "canceled"
251
358
 
252
359
  def __init__(
253
360
  self,
254
- message: str = "Request timed out",
361
+ message: str = "Operation canceled",
255
362
  status_code: Optional[int] = None,
256
363
  details: Optional[Dict[str, Any]] = None,
257
364
  endpoint: Optional[str] = None
258
365
  ):
259
- super().__init__(message, status_code, details, endpoint)
366
+ super().__init__(message, status_code, details, endpoint, kind="canceled")
260
367
 
261
368
 
262
- class SecureTableError(DominusError):
263
- """Raised when accessing a secure table without providing a reason."""
369
+ class UnsupportedVersionError(DominusError):
370
+ """Raised when an unsupported API or schema version is requested."""
371
+ kind = "unsupported_version"
264
372
 
265
373
  def __init__(
266
374
  self,
267
- message: str = "Access to secure table requires 'reason' parameter",
268
- status_code: int = 403,
375
+ message: str = "Unsupported version",
376
+ status_code: int = 400,
269
377
  details: Optional[Dict[str, Any]] = None,
270
378
  endpoint: Optional[str] = None
271
379
  ):
272
- super().__init__(message, status_code, details, endpoint)
380
+ super().__init__(message, status_code, details, endpoint, kind="unsupported_version")
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # v1 aliases — existing public class names preserved.
385
+ # Every existing name maps to exactly one canonical per-kind subclass.
386
+ # Deprecation warnings fire in v2; removal in v3 (Seams fork 6, ADR 0010).
387
+ # ---------------------------------------------------------------------------
388
+
389
+ AuthenticationError = UnauthorizedError
390
+ AuthorizationError = ForbiddenError
391
+ ValidationError = ValidationFailedError
392
+ ServiceError = UpstreamUnavailableError
393
+ ConnectionError = UpstreamUnavailableError
394
+ SecureTableError = ForbiddenError
395
+
396
+
397
+ # Stash subclass aliases — map to the canonical per-kind base.
398
+ class StashBookmarkArtifactNotFound(NotFoundError):
399
+ """Raised when a stash artifact bookmark target cannot be found."""
400
+
401
+
402
+ class StashBookmarkInvalidVersionRef(ValidationFailedError):
403
+ """Raised when a stash artifact bookmark version_ref is invalid."""
404
+
405
+
406
+ class StashBookmarkDuplicateName(ConflictError):
407
+ """Raised when a stash artifact bookmark name is already in use."""
408
+
409
+
410
+ class StashBookmarkUpstreamRejected(UpstreamUnavailableError):
411
+ """Raised when the artifact backend rejects a stash bookmark operation."""
412
+
413
+
414
+ class StashWatchInvalidWebhookUrl(ValidationFailedError):
415
+ """Raised when a stash artifact watcher webhook_url is invalid."""
416
+
417
+
418
+ class StashWatchUpstreamRejected(UpstreamUnavailableError):
419
+ """Raised when the artifact backend rejects a stash watch operation."""
420
+
421
+
422
+ class StashWatchNotFound(NotFoundError):
423
+ """Raised when a stash artifact watcher cannot be found."""
273
424
 
274
425
 
275
426
  STASH_ERROR_CLASSES = {
@@ -303,16 +454,34 @@ def raise_for_status(
303
454
  """
304
455
  code = _first_string((details or {}).get("code"))
305
456
  error_classes = {
306
- 400: ValidationError,
307
- 401: AuthenticationError,
308
- 403: AuthorizationError,
457
+ 400: ValidationFailedError,
458
+ 401: UnauthorizedError,
459
+ 403: ForbiddenError,
309
460
  404: NotFoundError,
310
461
  409: ConflictError,
311
- 500: ServiceError,
312
- 502: ServiceError,
313
- 503: ServiceError,
462
+ 429: RateLimitedError,
463
+ 500: UpstreamUnavailableError,
464
+ 502: UpstreamUnavailableError,
465
+ 503: UpstreamUnavailableError,
314
466
  504: TimeoutError,
315
467
  }
316
468
 
317
- error_class = STASH_ERROR_CLASSES.get(code) or error_classes.get(status_code, DominusError)
469
+ kind_classes = {
470
+ kind: error_class for kind, error_class in (
471
+ ("unauthorized", UnauthorizedError),
472
+ ("forbidden", ForbiddenError),
473
+ ("not_found", NotFoundError),
474
+ ("validation_failed", ValidationFailedError),
475
+ ("conflict", ConflictError),
476
+ ("rate_limited", RateLimitedError),
477
+ ("upstream_unavailable", UpstreamUnavailableError),
478
+ ("internal", InternalError),
479
+ ("timeout", TimeoutError),
480
+ ("canceled", CanceledError),
481
+ ("unsupported_version", UnsupportedVersionError),
482
+ )
483
+ }
484
+
485
+ kind = _first_string((details or {}).get("kind"))
486
+ error_class = STASH_ERROR_CLASSES.get(code) or kind_classes.get(kind) or error_classes.get(status_code, DominusError)
318
487
  raise error_class(message, status_code, details, endpoint)
@@ -19,5 +19,5 @@ def _resolve_token() -> Optional[str]:
19
19
  Returns:
20
20
  Token string or None if not set
21
21
  """
22
- return os.getenv("DOMINUS_TOKEN")
22
+ return os.getenv("DOMINUS_USER_TOKEN") or os.getenv("DOMINUS_TOKEN")
23
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 9.0.2
3
+ Version: 9.0.4
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
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.2`
51
+ - Current package version: `9.0.4`
52
52
 
53
53
  ## Install
54
54
 
@@ -67,13 +67,12 @@ for operators and advanced use; this path is the stable teaching surface.
67
67
  Set both values before the Python process imports the SDK singleton:
68
68
 
69
69
  ```bash
70
- export DOMINUS_TOKEN="your-psk-token"
70
+ export DOMINUS_USER_TOKEN="your-user-token"
71
71
  export DOMINUS_PROJECT="your-project-slug"
72
72
  ```
73
73
 
74
74
  ```python
75
75
  import asyncio
76
- import base64
77
76
 
78
77
  from dominus import dominus
79
78
 
@@ -83,22 +82,6 @@ async def main() -> None:
83
82
  workflow_recipe_ref="recipe://workflow-recipe-v1/hello@v1",
84
83
  )
85
84
 
86
- await dominus.artifacts.store_v2(
87
- group="your-group",
88
- owner="your-project-slug",
89
- environment="production",
90
- kind="blob",
91
- artifact_key="hello",
92
- data=base64.b64encode(b"hello dominus").decode("ascii"),
93
- )
94
- artifact = await dominus.artifacts.retrieve_v2(
95
- group="your-group",
96
- owner="your-project-slug",
97
- environment="production",
98
- kind="blob",
99
- artifact_key="hello",
100
- )
101
-
102
85
  stash_result = await dominus.stash.upsert(
103
86
  kind="config",
104
87
  scope={"env": "production"},
@@ -53,6 +53,7 @@ tests/test_auth.py
53
53
  tests/test_authority_public_vocabulary.py
54
54
  tests/test_browser_namespace.py
55
55
  tests/test_control_plane_namespaces.py
56
+ tests/test_error_recovery_action.py
56
57
  tests/test_errors.py
57
58
  tests/test_flat_commands.py
58
59
  tests/test_health.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "9.0.2"
7
+ version = "9.0.4"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -0,0 +1,31 @@
1
+ import pytest
2
+
3
+ from dominus.errors import CANONICAL_ERROR_KINDS, DominusError, raise_for_status, recovery_action_for_kind
4
+
5
+
6
+ @pytest.mark.parametrize("kind", CANONICAL_ERROR_KINDS)
7
+ def test_recovery_action_for_kind_is_closed_sum(kind: str) -> None:
8
+ retryable = False if kind == "upstream_unavailable" else None
9
+ action = recovery_action_for_kind(kind, retryable)
10
+ assert action in {"retry", "reauth", "escalate", "fix_config"}
11
+
12
+
13
+ def test_dominus_error_recovery_action_accessor() -> None:
14
+ error = DominusError(
15
+ "Auth failed",
16
+ status_code=401,
17
+ details={"code": "kernel.auth.token_invalid", "kind": "unauthorized", "request_id": "req-auth"},
18
+ )
19
+ assert error.recovery_action == "reauth"
20
+ assert error.to_dict()["recovery_action"] == "reauth"
21
+ assert error.details["recovery_action"] == "reauth"
22
+
23
+
24
+ def test_raise_for_status_surfaces_recovery_action() -> None:
25
+ with pytest.raises(DominusError) as exc:
26
+ raise_for_status(
27
+ 404,
28
+ "Missing",
29
+ {"code": "kernel.resource.resource_not_found", "kind": "not_found"},
30
+ )
31
+ assert exc.value.recovery_action == "fix_config"
@@ -15,8 +15,6 @@ README = ROOT / "README.md"
15
15
  SDK_PYTHON_CATALOG = frozenset(
16
16
  {
17
17
  "workflow.ensure",
18
- "artifacts.store_v2",
19
- "artifacts.retrieve_v2",
20
18
  "stash.upsert",
21
19
  "stash.get",
22
20
  "authority.get_run_timeline",
@@ -24,7 +22,7 @@ SDK_PYTHON_CATALOG = frozenset(
24
22
  }
25
23
  )
26
24
 
27
- FORBIDDEN = re.compile(r"summit|carebridge|PCM47474562|report-cycle", re.IGNORECASE)
25
+ FORBIDDEN = re.compile(r"summit|carebridge|PCM47474562|report-cycle|artifact", re.IGNORECASE)
28
26
 
29
27
 
30
28
  def _quick_start_section(readme: str) -> str:
@@ -48,11 +46,11 @@ def test_quick_start_scope_precedes_import_and_methods_match_catalog() -> None:
48
46
  python_index, body = _fenced(section, "python")
49
47
 
50
48
  assert shell_index < python_index
51
- assert 'export DOMINUS_TOKEN="your-psk-token"' in shell
49
+ assert 'export DOMINUS_USER_TOKEN="your-user-token"' in shell
52
50
  assert 'export DOMINUS_PROJECT="your-project-slug"' in shell
53
51
  assert "os.environ" not in body
54
52
  assert "from dominus import dominus" in body
55
- assert not FORBIDDEN.search(body), "Quick Start still has CareBridge-shaped nouns"
53
+ assert not FORBIDDEN.search(body), "Quick Start still has forbidden catalog nouns"
56
54
 
57
55
  found = set(re.findall(r"\bdominus\.([a-z0-9_]+)\.([a-z0-9_]+)\b", body))
58
56
  identifiers = {f"{namespace}.{method}" for namespace, method in found}
@@ -64,9 +62,12 @@ def test_quick_start_executes_in_fresh_process_with_live_transport_shapes() -> N
64
62
  r'''
65
63
  import asyncio
66
64
  import base64
65
+ import json
67
66
  import sys
68
67
  from pathlib import Path
69
68
 
69
+ import httpx
70
+
70
71
  readme = Path(sys.argv[1]).read_text(encoding="utf-8")
71
72
  section = readme[readme.index("## Quick Start"):]
72
73
  marker = "```python\n"
@@ -74,48 +75,65 @@ def test_quick_start_executes_in_fresh_process_with_live_transport_shapes() -> N
74
75
  body = section[start:section.index("```", start)]
75
76
 
76
77
  from dominus import dominus
78
+ import dominus.helpers.core as core
77
79
 
78
80
  asyncio.run(dominus._ensure_ready())
79
81
  calls = []
80
82
 
81
- async def fake_request(**kwargs):
82
- calls.append(kwargs)
83
- endpoint = kwargs["endpoint"]
84
- if endpoint == "/api/authority/runs/ensure":
85
- return {"run_id": "run-1"}
86
- if endpoint == "/svc/stash/items/upsert":
87
- return {"item": {"id": "stash-1"}, "scope_origin": "own"}
88
- if endpoint == "/svc/stash/items/get":
89
- return {"item": {"id": "stash-1"}}
90
- return {}
91
-
92
- dominus._request = fake_request
83
+ async def fake_jwt(*_args, **_kwargs):
84
+ return "header.payload.signature"
85
+
86
+ core._ensure_valid_jwt = fake_jwt
87
+
88
+ def handle(request):
89
+ body = json.loads(base64.b64decode(request.content)) if request.content else {}
90
+ calls.append(
91
+ {
92
+ "endpoint": request.url.path,
93
+ "body": body,
94
+ "headers": dict(request.headers),
95
+ }
96
+ )
97
+ if request.url.path == "/svc/authority/runs/ensure":
98
+ payload = {"run_id": "run-1"}
99
+ elif request.url.path == "/svc/stash/items/upsert":
100
+ payload = {"item": {"id": "stash-1"}, "scope_origin": "own"}
101
+ elif request.url.path == "/svc/stash/items/get":
102
+ payload = {"item": {"id": "stash-1"}}
103
+ else:
104
+ payload = {}
105
+ return httpx.Response(200, json=payload)
106
+
107
+ transport = httpx.MockTransport(handle)
108
+ real_async_client = httpx.AsyncClient
109
+
110
+ def mock_async_client(*args, **kwargs):
111
+ kwargs["transport"] = transport
112
+ return real_async_client(*args, **kwargs)
113
+
114
+ httpx.AsyncClient = mock_async_client
93
115
  exec(compile(body, "README.md#quick-start", "exec"), {"__name__": "__main__"})
94
116
 
95
117
  endpoints = [call["endpoint"] for call in calls]
96
118
  assert endpoints == [
97
- "/api/authority/runs/ensure",
98
- "/api/artifact/v2/store",
99
- "/api/artifact/v2/retrieve",
119
+ "/svc/authority/runs/ensure",
100
120
  "/svc/stash/items/upsert",
101
121
  "/svc/stash/items/get",
102
- "/api/authority/runs/run-1/timeline",
103
- "/api/authority/runs/run-1/verdict",
122
+ "/svc/authority/runs/run-1/timeline",
123
+ "/svc/authority/runs/run-1/verdict",
104
124
  ], endpoints
105
125
  assert calls[0]["body"]["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/hello@v1"
106
- assert calls[1]["body"]["data"] == base64.b64encode(b"hello dominus").decode("ascii")
107
- assert base64.b64decode(calls[1]["body"]["data"], validate=True) == b"hello dominus"
108
- assert calls[3]["body"] == {
126
+ assert calls[1]["body"] == {
109
127
  "kind": "config",
110
128
  "scope": {"env": "production"},
111
129
  "value": {"greeting": "hello"},
112
130
  "item_key": "hello",
113
131
  }
114
- assert calls[4]["body"] == {"id": "stash-1"}
132
+ assert calls[2]["body"] == {"id": "stash-1"}
115
133
  '''
116
134
  )
117
135
  env = os.environ.copy()
118
- env["DOMINUS_TOKEN"] = "a" * 64
136
+ env["DOMINUS_USER_TOKEN"] = "a" * 64
119
137
  env["DOMINUS_PROJECT"] = "your-project-slug"
120
138
  result = subprocess.run(
121
139
  [sys.executable, "-c", child, str(README)],