dominus-sdk-python 8.0.0__tar.gz → 9.0.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 (74) hide show
  1. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/__init__.py +10 -2
  3. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/__init__.py +9 -1
  4. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/secrets.py +109 -3
  5. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  6. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  7. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/pyproject.toml +1 -1
  8. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_namespace_barrel_exports.py +21 -0
  9. dominus_sdk_python-9.0.0/tests/test_secrets_presence_contract.py +143 -0
  10. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/README.md +0 -0
  11. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/config/__init__.py +0 -0
  12. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/config/endpoints.py +0 -0
  13. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/errors.py +0 -0
  14. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/__init__.py +0 -0
  15. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/auth.py +0 -0
  16. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/cache.py +0 -0
  17. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/console_capture.py +0 -0
  18. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/core.py +0 -0
  19. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/crypto.py +0 -0
  20. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/sse.py +0 -0
  21. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/helpers/trace.py +0 -0
  22. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/admin.py +0 -0
  23. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/ai.py +0 -0
  24. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/artifacts.py +0 -0
  25. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/auth.py +0 -0
  26. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/authority.py +0 -0
  27. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/browser.py +0 -0
  28. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/coder.py +0 -0
  29. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/courier.py +0 -0
  30. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/db.py +0 -0
  31. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/ddl.py +0 -0
  32. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/deployer.py +0 -0
  33. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/fastapi.py +0 -0
  34. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/files.py +0 -0
  35. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/health.py +0 -0
  36. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/jobs.py +0 -0
  37. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/logs.py +0 -0
  38. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/platform.py +0 -0
  39. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/portal.py +0 -0
  40. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/processor.py +0 -0
  41. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/publisher.py +0 -0
  42. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/recipes.py +0 -0
  43. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/redis.py +0 -0
  44. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/secure.py +0 -0
  45. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/stash.py +0 -0
  46. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/sync.py +0 -0
  47. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/warden.py +0 -0
  48. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/namespaces/workflow.py +0 -0
  49. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/py.typed +0 -0
  50. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/services/__init__.py +0 -0
  51. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus/start.py +0 -0
  52. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  53. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  54. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  55. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/setup.cfg +0 -0
  56. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_auth.py +0 -0
  57. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_authority_public_vocabulary.py +0 -0
  58. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_browser_namespace.py +0 -0
  59. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_control_plane_namespaces.py +0 -0
  60. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_errors.py +0 -0
  61. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_flat_commands.py +0 -0
  62. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_health.py +0 -0
  63. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_logs.py +0 -0
  64. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_platform_coder_namespaces.py +0 -0
  65. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_provisioning_parity.py +0 -0
  66. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_public_exports.py +0 -0
  67. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_publisher_namespace.py +0 -0
  68. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_recipes_namespace.py +0 -0
  69. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_recipes_stash_routing.py +0 -0
  70. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_stash_artifact_facade.py +0 -0
  71. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_stash_tables_namespace.py +0 -0
  72. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_transport_compat.py +0 -0
  73. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.0}/tests/test_workflow_lifecycle.py +0 -0
  74. {dominus_sdk_python-8.0.0 → dominus_sdk_python-9.0.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: 8.0.0
3
+ Version: 9.0.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
@@ -82,7 +82,12 @@ from .namespaces.files import FilesNamespace
82
82
  from .namespaces.portal import PortalNamespace
83
83
  from .namespaces.admin import AdminNamespace
84
84
  from .namespaces.secure import SecureNamespace, SecureAccessContext
85
- from .namespaces.secrets import SecretsNamespace
85
+ from .namespaces.secrets import (
86
+ SecretsNamespace,
87
+ SecretEnvironment,
88
+ SecretPresenceCell,
89
+ SecretPresenceGrid,
90
+ )
86
91
  from .namespaces.redis import RedisNamespace
87
92
  from .namespaces.logs import LogsNamespace
88
93
  from .namespaces.courier import CourierNamespace
@@ -173,7 +178,7 @@ from .errors import (
173
178
  TimeoutError as DominusTimeoutError,
174
179
  )
175
180
 
176
- __version__ = "8.0.0"
181
+ __version__ = "9.0.0"
177
182
  __all__ = [
178
183
  # Main SDK instance
179
184
  "dominus",
@@ -197,6 +202,9 @@ __all__ = [
197
202
  "SecureNamespace",
198
203
  "SecureAccessContext",
199
204
  "SecretsNamespace",
205
+ "SecretEnvironment",
206
+ "SecretPresenceCell",
207
+ "SecretPresenceGrid",
200
208
  "RedisNamespace",
201
209
  "LogsNamespace",
202
210
  "CourierNamespace",
@@ -1,5 +1,10 @@
1
1
  """Dominus SDK namespace exports."""
2
- from .secrets import SecretsNamespace
2
+ from .secrets import (
3
+ SecretsNamespace,
4
+ SecretEnvironment,
5
+ SecretPresenceCell,
6
+ SecretPresenceGrid,
7
+ )
3
8
  from .db import DbNamespace
4
9
  from .redis import RedisNamespace
5
10
  from .files import FilesNamespace
@@ -33,6 +38,9 @@ from .ai import (
33
38
 
34
39
  __all__ = [
35
40
  "SecretsNamespace",
41
+ "SecretEnvironment",
42
+ "SecretPresenceCell",
43
+ "SecretPresenceGrid",
36
44
  "DbNamespace",
37
45
  "RedisNamespace",
38
46
  "FilesNamespace",
@@ -3,12 +3,115 @@ Secrets Namespace - Warden secrets management.
3
3
 
4
4
  Provides CRUD operations for secrets stored in the Warden service.
5
5
  """
6
- from typing import Any, Dict, List, Optional, TYPE_CHECKING
6
+ from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, TypedDict
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from ..start import Dominus
10
10
 
11
11
 
12
+ SecretEnvironment = Literal["development", "staging", "production"]
13
+ _SECRET_ENVIRONMENTS = ("development", "staging", "production")
14
+
15
+
16
+ class SecretPresenceCell(TypedDict):
17
+ present: bool
18
+ preview: Optional[str]
19
+
20
+
21
+ class SecretPresenceGrid(TypedDict):
22
+ secrets: Dict[str, Dict[SecretEnvironment, SecretPresenceCell]]
23
+ keys: List[str]
24
+ count: int
25
+ environment_counts: Dict[SecretEnvironment, int]
26
+
27
+
28
+ def _invalid_presence_response() -> None:
29
+ raise ValueError("Invalid secret presence response")
30
+
31
+
32
+ def _safe_preview(value: str) -> str:
33
+ if value == "":
34
+ return ""
35
+ if len(value) <= 8:
36
+ return "<redacted>"
37
+ return f"{value[:4]}...{value[-4:]}"
38
+
39
+
40
+ def _is_canonical_preview(value: str) -> bool:
41
+ return value in ("", "<redacted>") or (len(value) == 11 and value[4:7] == "...")
42
+
43
+
44
+ def _normalize_presence_grid(value: Any) -> SecretPresenceGrid:
45
+ if not isinstance(value, dict) or not isinstance(value.get("secrets"), dict):
46
+ _invalid_presence_response()
47
+ keys = value.get("keys")
48
+ count = value.get("count")
49
+ if not isinstance(keys, list) or not all(isinstance(key, str) for key in keys):
50
+ _invalid_presence_response()
51
+ if type(count) is not int or count < 0:
52
+ _invalid_presence_response()
53
+
54
+ unique_sorted = sorted(set(keys))
55
+ if keys != unique_sorted or sorted(value["secrets"].keys()) != keys or count != len(keys):
56
+ _invalid_presence_response()
57
+
58
+ canonical_mode = "environment_counts" in value
59
+ counts: Dict[SecretEnvironment, int] = {
60
+ "development": 0,
61
+ "staging": 0,
62
+ "production": 0,
63
+ }
64
+ secrets: Dict[str, Dict[SecretEnvironment, SecretPresenceCell]] = {}
65
+
66
+ for key in keys:
67
+ raw_row = value["secrets"][key]
68
+ if not isinstance(raw_row, dict) or not set(raw_row).issubset(_SECRET_ENVIRONMENTS):
69
+ _invalid_presence_response()
70
+ row: Dict[SecretEnvironment, SecretPresenceCell] = {}
71
+ for environment in _SECRET_ENVIRONMENTS:
72
+ has_cell = environment in raw_row
73
+ raw_cell = raw_row.get(environment)
74
+ if canonical_mode:
75
+ if not has_cell or not isinstance(raw_cell, dict) or set(raw_cell) != {"present", "preview"}:
76
+ _invalid_presence_response()
77
+ if type(raw_cell["present"]) is not bool:
78
+ _invalid_presence_response()
79
+ if raw_cell["present"]:
80
+ if not isinstance(raw_cell["preview"], str) or not _is_canonical_preview(raw_cell["preview"]):
81
+ _invalid_presence_response()
82
+ elif raw_cell["preview"] is not None:
83
+ _invalid_presence_response()
84
+ cell: SecretPresenceCell = {
85
+ "present": raw_cell["present"],
86
+ "preview": raw_cell["preview"],
87
+ }
88
+ elif not has_cell or raw_cell is None:
89
+ cell = {"present": False, "preview": None}
90
+ elif isinstance(raw_cell, str):
91
+ cell = {"present": True, "preview": _safe_preview(raw_cell)}
92
+ else:
93
+ _invalid_presence_response()
94
+ row[environment] = cell
95
+ if cell["present"]:
96
+ counts[environment] += 1
97
+ secrets[key] = row
98
+
99
+ if canonical_mode:
100
+ response_counts = value["environment_counts"]
101
+ if not isinstance(response_counts, dict) or set(response_counts) != set(_SECRET_ENVIRONMENTS):
102
+ _invalid_presence_response()
103
+ if any(type(response_counts[environment]) is not int or response_counts[environment] != counts[environment]
104
+ for environment in _SECRET_ENVIRONMENTS):
105
+ _invalid_presence_response()
106
+
107
+ return {
108
+ "secrets": secrets,
109
+ "keys": list(keys),
110
+ "count": len(keys),
111
+ "environment_counts": counts,
112
+ }
113
+
114
+
12
115
  class SecretsNamespace:
13
116
  """
14
117
  Secrets management namespace.
@@ -193,9 +296,11 @@ class SecretsNamespace:
193
296
 
194
297
  async def list_secrets_all_environments(
195
298
  self, infisical_project_id: str
196
- ) -> Dict[str, Any]:
299
+ ) -> SecretPresenceGrid:
197
300
  """Warden grid: secrets across environments for an Infisical project id."""
198
- return await self._client._request(
301
+ if not infisical_project_id.strip():
302
+ raise ValueError("Infisical project id is required")
303
+ response = await self._client._request(
199
304
  endpoint="/api/warden/secrets",
200
305
  body={
201
306
  "action": "list-all-environments",
@@ -203,3 +308,4 @@ class SecretsNamespace:
203
308
  },
204
309
  use_gateway=True,
205
310
  )
311
+ return _normalize_presence_grid(response)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 8.0.0
3
+ Version: 9.0.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
@@ -64,6 +64,7 @@ tests/test_public_exports.py
64
64
  tests/test_publisher_namespace.py
65
65
  tests/test_recipes_namespace.py
66
66
  tests/test_recipes_stash_routing.py
67
+ tests/test_secrets_presence_contract.py
67
68
  tests/test_stash_artifact_facade.py
68
69
  tests/test_stash_tables_namespace.py
69
70
  tests/test_transport_compat.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "8.0.0"
7
+ version = "9.0.0"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -39,3 +39,24 @@ def test_stash_tables_namespace_is_exported_from_both_barrels():
39
39
 
40
40
  def test_package_includes_declared_pep561_marker():
41
41
  assert Path(dominus.__file__).with_name("py.typed").is_file()
42
+
43
+
44
+ def test_secret_presence_types_are_identical_across_both_public_barrels():
45
+ from dominus import SecretEnvironment, SecretPresenceCell, SecretPresenceGrid
46
+ from dominus.namespaces import (
47
+ SecretEnvironment as NamespaceEnvironment,
48
+ SecretPresenceCell as NamespaceCell,
49
+ SecretPresenceGrid as NamespaceGrid,
50
+ )
51
+ from dominus.namespaces.secrets import (
52
+ SecretEnvironment as DefinedEnvironment,
53
+ SecretPresenceCell as DefinedCell,
54
+ SecretPresenceGrid as DefinedGrid,
55
+ )
56
+
57
+ assert SecretEnvironment is NamespaceEnvironment is DefinedEnvironment
58
+ assert SecretPresenceCell is NamespaceCell is DefinedCell
59
+ assert SecretPresenceGrid is NamespaceGrid is DefinedGrid
60
+ for name in ("SecretEnvironment", "SecretPresenceCell", "SecretPresenceGrid"):
61
+ assert name in dominus.__all__
62
+ assert name in dominus.namespaces.__all__
@@ -0,0 +1,143 @@
1
+ import json
2
+ from typing import Literal, get_args, get_type_hints
3
+
4
+ import pytest
5
+
6
+ from dominus.namespaces.secrets import SecretsNamespace
7
+
8
+
9
+ RAW_SENTINEL = "SENTINEL-DO-NOT-LEAK"
10
+ LEGACY = {
11
+ "secrets": {
12
+ "EMPTY": {"development": "", "staging": None, "production": None},
13
+ "LONG": {"development": RAW_SENTINEL, "staging": None, "production": "tiny"},
14
+ "STAGE": {"development": None, "staging": "12345678", "production": None},
15
+ },
16
+ "keys": ["EMPTY", "LONG", "STAGE"],
17
+ "count": 3,
18
+ }
19
+ OMITTED = {
20
+ "secrets": {
21
+ "EMPTY": {"development": ""},
22
+ "LONG": {"development": RAW_SENTINEL, "production": "tiny"},
23
+ "STAGE": {"staging": "12345678"},
24
+ },
25
+ "keys": ["EMPTY", "LONG", "STAGE"],
26
+ "count": 3,
27
+ }
28
+ EXPECTED = {
29
+ "secrets": {
30
+ "EMPTY": {
31
+ "development": {"present": True, "preview": ""},
32
+ "staging": {"present": False, "preview": None},
33
+ "production": {"present": False, "preview": None},
34
+ },
35
+ "LONG": {
36
+ "development": {"present": True, "preview": "SENT...LEAK"},
37
+ "staging": {"present": False, "preview": None},
38
+ "production": {"present": True, "preview": "<redacted>"},
39
+ },
40
+ "STAGE": {
41
+ "development": {"present": False, "preview": None},
42
+ "staging": {"present": True, "preview": "<redacted>"},
43
+ "production": {"present": False, "preview": None},
44
+ },
45
+ },
46
+ "keys": ["EMPTY", "LONG", "STAGE"],
47
+ "count": 3,
48
+ "environment_counts": {"development": 2, "staging": 1, "production": 1},
49
+ }
50
+
51
+
52
+ class Client:
53
+ def __init__(self, response=None, error=None):
54
+ self.response = response
55
+ self.error = error
56
+ self.calls = []
57
+
58
+ async def _request(self, **kwargs):
59
+ self.calls.append(kwargs)
60
+ if self.error is not None:
61
+ raise self.error
62
+ return self.response
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_physical_id_wire_and_legacy_safe_rollback_normalization():
67
+ for response in (LEGACY, EXPECTED, LEGACY):
68
+ client = Client(response)
69
+ result = await SecretsNamespace(client).list_secrets_all_environments("physical-123")
70
+ assert client.calls == [{
71
+ "endpoint": "/api/warden/secrets",
72
+ "body": {"action": "list-all-environments", "infisical_project_id": "physical-123"},
73
+ "use_gateway": True,
74
+ }]
75
+ assert result == EXPECTED
76
+ assert RAW_SENTINEL not in json.dumps(result)
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_omitted_legacy_properties_equal_explicit_nulls():
81
+ assert await SecretsNamespace(Client(OMITTED)).list_secrets_all_environments("physical-123") == EXPECTED
82
+
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_blank_physical_id_is_rejected_locally():
86
+ client = Client(LEGACY)
87
+ with pytest.raises(ValueError, match="Infisical project id is required"):
88
+ await SecretsNamespace(client).list_secrets_all_environments(" ")
89
+ assert client.calls == []
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_upstream_failure_propagates_unchanged():
94
+ failure = RuntimeError("upstream unavailable")
95
+ with pytest.raises(RuntimeError) as caught:
96
+ await SecretsNamespace(Client(error=failure)).list_secrets_all_environments("physical-123")
97
+ assert caught.value is failure
98
+
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_malformed_responses_fail_without_raw_cell_in_error_or_logs(caplog):
102
+ malformed = [
103
+ {"secrets": {"BAD": {"development": value, "staging": None, "production": None}}, "keys": ["BAD"], "count": 1}
104
+ for value in (1, True, [], {})
105
+ ] + [
106
+ {**EXPECTED, "secrets": {"BAD": {"development": {"present": False, "preview": "bad"}, "staging": {"present": False, "preview": None}, "production": {"present": False, "preview": None}}}, "keys": ["BAD"], "count": 1, "environment_counts": {"development": 0, "staging": 0, "production": 0}},
107
+ {**EXPECTED, "secrets": {"BAD": {"development": {"present": True, "preview": None}, "staging": {"present": False, "preview": None}, "production": {"present": False, "preview": None}}}, "keys": ["BAD"], "count": 1, "environment_counts": {"development": 1, "staging": 0, "production": 0}},
108
+ {"secrets": {"BAD": {"development": RAW_SENTINEL, "staging": None, "production": None, "preview": "unknown"}}, "keys": ["BAD"], "count": 1},
109
+ {**LEGACY, "keys": ["LONG", "EMPTY", "STAGE"]},
110
+ {**LEGACY, "keys": ["EMPTY", "EMPTY", "STAGE"]},
111
+ {**LEGACY, "count": 2},
112
+ {**EXPECTED, "environment_counts": {"development": 99, "staging": 1, "production": 1}},
113
+ {"secrets": LEGACY["secrets"], "keys": LEGACY["keys"]},
114
+ {"success": False, "error": RAW_SENTINEL},
115
+ ]
116
+ for response in malformed:
117
+ with pytest.raises(ValueError) as caught:
118
+ await SecretsNamespace(Client(response)).list_secrets_all_environments("physical-123")
119
+ assert RAW_SENTINEL not in str(caught.value)
120
+ assert RAW_SENTINEL not in caplog.text
121
+
122
+
123
+ def test_public_types_and_return_annotation_are_exact():
124
+ from dominus import SecretEnvironment, SecretPresenceCell, SecretPresenceGrid
125
+ from dominus.namespaces import (
126
+ SecretEnvironment as NamespaceEnvironment,
127
+ SecretPresenceCell as NamespaceCell,
128
+ SecretPresenceGrid as NamespaceGrid,
129
+ )
130
+ from dominus.namespaces.secrets import (
131
+ SecretEnvironment as DefinedEnvironment,
132
+ SecretPresenceCell as DefinedCell,
133
+ SecretPresenceGrid as DefinedGrid,
134
+ )
135
+
136
+ assert SecretEnvironment is NamespaceEnvironment is DefinedEnvironment
137
+ assert SecretPresenceCell is NamespaceCell is DefinedCell
138
+ assert SecretPresenceGrid is NamespaceGrid is DefinedGrid
139
+ assert get_args(SecretEnvironment) == ("development", "staging", "production")
140
+ assert SecretEnvironment == Literal["development", "staging", "production"]
141
+ assert SecretPresenceCell.__required_keys__ == frozenset({"present", "preview"})
142
+ assert SecretPresenceGrid.__required_keys__ == frozenset({"secrets", "keys", "count", "environment_counts"})
143
+ assert get_type_hints(SecretsNamespace.list_secrets_all_environments)["return"] is SecretPresenceGrid