dominus-sdk-python 2.0.0__py3-none-any.whl
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/__init__.py +47 -0
- dominus/config/__init__.py +24 -0
- dominus/config/endpoints.py +43 -0
- dominus/errors.py +201 -0
- dominus/helpers/__init__.py +2 -0
- dominus/helpers/auth.py +49 -0
- dominus/helpers/cache.py +192 -0
- dominus/helpers/core.py +603 -0
- dominus/helpers/crypto.py +118 -0
- dominus/namespaces/__init__.py +26 -0
- dominus/namespaces/_deprecated_crossover.py +10 -0
- dominus/namespaces/_deprecated_sql.py +1341 -0
- dominus/namespaces/auth.py +1025 -0
- dominus/namespaces/courier.py +251 -0
- dominus/namespaces/db.py +267 -0
- dominus/namespaces/ddl.py +590 -0
- dominus/namespaces/files.py +299 -0
- dominus/namespaces/health.py +59 -0
- dominus/namespaces/logs.py +367 -0
- dominus/namespaces/open.py +72 -0
- dominus/namespaces/portal.py +437 -0
- dominus/namespaces/redis.py +357 -0
- dominus/namespaces/secrets.py +126 -0
- dominus/services/__init__.py +2 -0
- dominus/services/_deprecated_architect.py +323 -0
- dominus/services/_deprecated_sovereign.py +93 -0
- dominus/start.py +942 -0
- dominus_sdk_python-2.0.0.dist-info/METADATA +381 -0
- dominus_sdk_python-2.0.0.dist-info/RECORD +31 -0
- dominus_sdk_python-2.0.0.dist-info/WHEEL +5 -0
- dominus_sdk_python-2.0.0.dist-info/top_level.txt +1 -0
dominus/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CB Dominus SDK - Ultra-flat async SDK for CareBridge Services
|
|
3
|
+
|
|
4
|
+
Ultra-Flat API:
|
|
5
|
+
from dominus import dominus
|
|
6
|
+
|
|
7
|
+
# Secrets (root level)
|
|
8
|
+
value = await dominus.get("DB_URL")
|
|
9
|
+
await dominus.upsert("KEY", "value")
|
|
10
|
+
|
|
11
|
+
# Auth (root level)
|
|
12
|
+
await dominus.add_user(username="john", password="secret", role_id="...")
|
|
13
|
+
await dominus.add_scope(slug="read", display_name="Read", tenant_category_id=1)
|
|
14
|
+
result = await dominus.verify_user_password(username="john", password="secret")
|
|
15
|
+
|
|
16
|
+
# SQL data - app schema (root level)
|
|
17
|
+
tables = await dominus.list_tables()
|
|
18
|
+
rows = await dominus.query_table("users")
|
|
19
|
+
await dominus.insert_row("users", {"name": "John"})
|
|
20
|
+
|
|
21
|
+
# SQL data - secure schema (secure namespace)
|
|
22
|
+
rows = await dominus.secure.query_table("patients")
|
|
23
|
+
await dominus.secure.insert_row("patients", {"mrn": "12345"})
|
|
24
|
+
|
|
25
|
+
# Schema DDL - app schema (root level)
|
|
26
|
+
await dominus.add_table("users", [{"name": "id", "type": "UUID"}])
|
|
27
|
+
|
|
28
|
+
# Schema DDL - secure schema (secure namespace)
|
|
29
|
+
await dominus.secure.add_table("patients", [...])
|
|
30
|
+
|
|
31
|
+
# Open DSN
|
|
32
|
+
dsn = await dominus.open.dsn()
|
|
33
|
+
|
|
34
|
+
# Health
|
|
35
|
+
status = await dominus.health.check()
|
|
36
|
+
|
|
37
|
+
Backward Compatible String API:
|
|
38
|
+
result = await dominus("secrets.get", key="DB_URL")
|
|
39
|
+
"""
|
|
40
|
+
from .start import dominus
|
|
41
|
+
from .helpers.core import DominusResponse
|
|
42
|
+
|
|
43
|
+
__version__ = "0.3.0"
|
|
44
|
+
__all__ = [
|
|
45
|
+
"dominus",
|
|
46
|
+
"DominusResponse",
|
|
47
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""SDK Configuration"""
|
|
2
|
+
from .endpoints import (
|
|
3
|
+
SOVEREIGN_URL,
|
|
4
|
+
ARCHITECT_URL,
|
|
5
|
+
ORCHESTRATOR_URL,
|
|
6
|
+
WARDEN_URL,
|
|
7
|
+
get_service_url,
|
|
8
|
+
get_architect_url,
|
|
9
|
+
get_sovereign_url,
|
|
10
|
+
PROJECT_NUMBER,
|
|
11
|
+
REGION,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SOVEREIGN_URL",
|
|
16
|
+
"ARCHITECT_URL",
|
|
17
|
+
"ORCHESTRATOR_URL",
|
|
18
|
+
"WARDEN_URL",
|
|
19
|
+
"get_service_url",
|
|
20
|
+
"get_architect_url",
|
|
21
|
+
"get_sovereign_url",
|
|
22
|
+
"PROJECT_NUMBER",
|
|
23
|
+
"REGION",
|
|
24
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dominus Orchestrator Endpoints
|
|
3
|
+
|
|
4
|
+
Single backend URL for all services. The SDK now targets dominus-orchestrator
|
|
5
|
+
which consolidates all service functionality.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from dominus.config.endpoints import BASE_URL
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Cloud Run configuration
|
|
12
|
+
PROJECT_NUMBER = "775398158805"
|
|
13
|
+
REGION = "us-east4"
|
|
14
|
+
|
|
15
|
+
# Single orchestrator URL - all services consolidated here
|
|
16
|
+
BASE_URL = f"https://dominus-orchestrator-production-{PROJECT_NUMBER}.{REGION}.run.app"
|
|
17
|
+
|
|
18
|
+
# Legacy aliases (all point to orchestrator now) - DEPRECATED
|
|
19
|
+
SOVEREIGN_URL = BASE_URL
|
|
20
|
+
ARCHITECT_URL = BASE_URL
|
|
21
|
+
ORCHESTRATOR_URL = BASE_URL
|
|
22
|
+
WARDEN_URL = BASE_URL
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_base_url() -> str:
|
|
26
|
+
"""Get the dominus-orchestrator base URL."""
|
|
27
|
+
return BASE_URL
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# DEPRECATED - use get_base_url()
|
|
31
|
+
def get_sovereign_url(environment: str = None) -> str:
|
|
32
|
+
"""Deprecated: Use get_base_url() instead."""
|
|
33
|
+
return BASE_URL
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_architect_url(environment: str = None) -> str:
|
|
37
|
+
"""Deprecated: Use get_base_url() instead."""
|
|
38
|
+
return BASE_URL
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_service_url(service: str, environment: str = None) -> str:
|
|
42
|
+
"""Deprecated: Use get_base_url() instead. All services are now consolidated."""
|
|
43
|
+
return BASE_URL
|
dominus/errors.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dominus SDK Error Classes
|
|
3
|
+
|
|
4
|
+
Custom exceptions for the Dominus SDK with structured error information.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DominusError(Exception):
|
|
10
|
+
"""
|
|
11
|
+
Base exception for all Dominus SDK errors.
|
|
12
|
+
|
|
13
|
+
Provides structured error information including HTTP status codes,
|
|
14
|
+
error messages, and optional details from the backend.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
message: Human-readable error message
|
|
18
|
+
status_code: HTTP status code (if applicable)
|
|
19
|
+
details: Additional error details from backend
|
|
20
|
+
endpoint: The endpoint that was called (if applicable)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
message: str,
|
|
26
|
+
status_code: Optional[int] = None,
|
|
27
|
+
details: Optional[Dict[str, Any]] = None,
|
|
28
|
+
endpoint: Optional[str] = None
|
|
29
|
+
):
|
|
30
|
+
self.message = message
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.details = details or {}
|
|
33
|
+
self.endpoint = endpoint
|
|
34
|
+
super().__init__(self.message)
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
parts = [self.message]
|
|
38
|
+
if self.status_code:
|
|
39
|
+
parts.append(f"(status {self.status_code})")
|
|
40
|
+
if self.endpoint:
|
|
41
|
+
parts.append(f"at {self.endpoint}")
|
|
42
|
+
return " ".join(parts)
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return (
|
|
46
|
+
f"DominusError(message={self.message!r}, "
|
|
47
|
+
f"status_code={self.status_code}, "
|
|
48
|
+
f"details={self.details!r}, "
|
|
49
|
+
f"endpoint={self.endpoint!r})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthenticationError(DominusError):
|
|
54
|
+
"""Raised when authentication fails (invalid token, expired JWT, etc.)."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
message: str = "Authentication failed",
|
|
59
|
+
status_code: int = 401,
|
|
60
|
+
details: Optional[Dict[str, Any]] = None,
|
|
61
|
+
endpoint: Optional[str] = None
|
|
62
|
+
):
|
|
63
|
+
super().__init__(message, status_code, details, endpoint)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AuthorizationError(DominusError):
|
|
67
|
+
"""Raised when authorization fails (insufficient permissions)."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
message: str = "Permission denied",
|
|
72
|
+
status_code: int = 403,
|
|
73
|
+
details: Optional[Dict[str, Any]] = None,
|
|
74
|
+
endpoint: Optional[str] = None
|
|
75
|
+
):
|
|
76
|
+
super().__init__(message, status_code, details, endpoint)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NotFoundError(DominusError):
|
|
80
|
+
"""Raised when a requested resource is not found."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
message: str = "Resource not found",
|
|
85
|
+
status_code: int = 404,
|
|
86
|
+
details: Optional[Dict[str, Any]] = None,
|
|
87
|
+
endpoint: Optional[str] = None
|
|
88
|
+
):
|
|
89
|
+
super().__init__(message, status_code, details, endpoint)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ValidationError(DominusError):
|
|
93
|
+
"""Raised when request validation fails."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
message: str = "Validation failed",
|
|
98
|
+
status_code: int = 400,
|
|
99
|
+
details: Optional[Dict[str, Any]] = None,
|
|
100
|
+
endpoint: Optional[str] = None
|
|
101
|
+
):
|
|
102
|
+
super().__init__(message, status_code, details, endpoint)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ConflictError(DominusError):
|
|
106
|
+
"""Raised when there's a conflict (duplicate key, version mismatch, etc.)."""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
message: str = "Conflict",
|
|
111
|
+
status_code: int = 409,
|
|
112
|
+
details: Optional[Dict[str, Any]] = None,
|
|
113
|
+
endpoint: Optional[str] = None
|
|
114
|
+
):
|
|
115
|
+
super().__init__(message, status_code, details, endpoint)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ServiceError(DominusError):
|
|
119
|
+
"""Raised when a backend service error occurs."""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
message: str = "Service error",
|
|
124
|
+
status_code: int = 500,
|
|
125
|
+
details: Optional[Dict[str, Any]] = None,
|
|
126
|
+
endpoint: Optional[str] = None
|
|
127
|
+
):
|
|
128
|
+
super().__init__(message, status_code, details, endpoint)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ConnectionError(DominusError):
|
|
132
|
+
"""Raised when connection to the backend fails."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
message: str = "Connection failed",
|
|
137
|
+
status_code: Optional[int] = None,
|
|
138
|
+
details: Optional[Dict[str, Any]] = None,
|
|
139
|
+
endpoint: Optional[str] = None
|
|
140
|
+
):
|
|
141
|
+
super().__init__(message, status_code, details, endpoint)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TimeoutError(DominusError):
|
|
145
|
+
"""Raised when a request times out."""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
message: str = "Request timed out",
|
|
150
|
+
status_code: Optional[int] = None,
|
|
151
|
+
details: Optional[Dict[str, Any]] = None,
|
|
152
|
+
endpoint: Optional[str] = None
|
|
153
|
+
):
|
|
154
|
+
super().__init__(message, status_code, details, endpoint)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SecureTableError(DominusError):
|
|
158
|
+
"""Raised when accessing a secure table without providing a reason."""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
message: str = "Access to secure table requires 'reason' parameter",
|
|
163
|
+
status_code: int = 403,
|
|
164
|
+
details: Optional[Dict[str, Any]] = None,
|
|
165
|
+
endpoint: Optional[str] = None
|
|
166
|
+
):
|
|
167
|
+
super().__init__(message, status_code, details, endpoint)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def raise_for_status(
|
|
171
|
+
status_code: int,
|
|
172
|
+
message: str,
|
|
173
|
+
details: Optional[Dict[str, Any]] = None,
|
|
174
|
+
endpoint: Optional[str] = None
|
|
175
|
+
) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Raise appropriate DominusError subclass based on status code.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
status_code: HTTP status code
|
|
181
|
+
message: Error message
|
|
182
|
+
details: Optional error details
|
|
183
|
+
endpoint: Optional endpoint that was called
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
Appropriate DominusError subclass
|
|
187
|
+
"""
|
|
188
|
+
error_classes = {
|
|
189
|
+
400: ValidationError,
|
|
190
|
+
401: AuthenticationError,
|
|
191
|
+
403: AuthorizationError,
|
|
192
|
+
404: NotFoundError,
|
|
193
|
+
409: ConflictError,
|
|
194
|
+
500: ServiceError,
|
|
195
|
+
502: ServiceError,
|
|
196
|
+
503: ServiceError,
|
|
197
|
+
504: TimeoutError,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
error_class = error_classes.get(status_code, DominusError)
|
|
201
|
+
raise error_class(message, status_code, details, endpoint)
|
dominus/helpers/auth.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Token and URL resolution helpers.
|
|
3
|
+
|
|
4
|
+
Handles resolution of authentication token and service URLs
|
|
5
|
+
with support for environment variables and hardcoded fallbacks.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _resolve_token(hardcoded_token: Optional[str] = None) -> Optional[str]:
|
|
12
|
+
"""
|
|
13
|
+
Resolve auth token.
|
|
14
|
+
|
|
15
|
+
Priority:
|
|
16
|
+
1. Environment variable: DOMINUS_TOKEN
|
|
17
|
+
2. Hardcoded fallback (passed as parameter)
|
|
18
|
+
|
|
19
|
+
Note: When fetching from Infisical via Sovereign, the secret is stored as
|
|
20
|
+
PROVISION_DOMINUS_TOKEN but should be set as DOMINUS_TOKEN in environment.
|
|
21
|
+
The PROVISION_ prefix is dropped when setting environment variables.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
hardcoded_token: Optional hardcoded token for development
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Token string or None
|
|
28
|
+
"""
|
|
29
|
+
token = os.getenv("DOMINUS_TOKEN")
|
|
30
|
+
if token:
|
|
31
|
+
return token
|
|
32
|
+
if hardcoded_token:
|
|
33
|
+
return hardcoded_token
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_sovereign_url(environment: str = None) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Resolve Sovereign base URL from SDK flat file config.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
environment: Environment override (development, staging, production)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Sovereign base URL string
|
|
46
|
+
"""
|
|
47
|
+
from ..config.endpoints import get_sovereign_url
|
|
48
|
+
return get_sovereign_url(environment)
|
|
49
|
+
|
dominus/helpers/cache.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal cache with automatic encryption and circuit breaker.
|
|
3
|
+
|
|
4
|
+
NOT exposed to SDK users - internal use only.
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import base64
|
|
9
|
+
import random
|
|
10
|
+
from typing import Dict, Tuple, Optional, Any
|
|
11
|
+
from cryptography.fernet import Fernet
|
|
12
|
+
from hashlib import sha256
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CircuitBreaker:
|
|
16
|
+
"""
|
|
17
|
+
Simple circuit breaker to prevent runaway retries.
|
|
18
|
+
|
|
19
|
+
States:
|
|
20
|
+
- CLOSED: Normal operation, requests pass through
|
|
21
|
+
- OPEN: Too many failures, requests blocked
|
|
22
|
+
- HALF_OPEN: Testing if service recovered
|
|
23
|
+
|
|
24
|
+
Prevents CPU/quota exhaustion from retry storms.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
CLOSED = "closed"
|
|
28
|
+
OPEN = "open"
|
|
29
|
+
HALF_OPEN = "half_open"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
failure_threshold: int = 5,
|
|
34
|
+
recovery_timeout: float = 30.0,
|
|
35
|
+
half_open_max_calls: int = 1
|
|
36
|
+
):
|
|
37
|
+
self._failure_count = 0
|
|
38
|
+
self._failure_threshold = failure_threshold
|
|
39
|
+
self._recovery_timeout = recovery_timeout
|
|
40
|
+
self._half_open_max_calls = half_open_max_calls
|
|
41
|
+
self._state = self.CLOSED
|
|
42
|
+
self._last_failure_time: float = 0
|
|
43
|
+
self._half_open_calls = 0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def state(self) -> str:
|
|
47
|
+
"""Get current state, transitioning OPEN→HALF_OPEN if timeout elapsed."""
|
|
48
|
+
if self._state == self.OPEN:
|
|
49
|
+
if time.time() - self._last_failure_time >= self._recovery_timeout:
|
|
50
|
+
self._state = self.HALF_OPEN
|
|
51
|
+
self._half_open_calls = 0
|
|
52
|
+
return self._state
|
|
53
|
+
|
|
54
|
+
def can_execute(self) -> bool:
|
|
55
|
+
"""Check if a request can be executed."""
|
|
56
|
+
state = self.state
|
|
57
|
+
if state == self.CLOSED:
|
|
58
|
+
return True
|
|
59
|
+
if state == self.HALF_OPEN:
|
|
60
|
+
return self._half_open_calls < self._half_open_max_calls
|
|
61
|
+
return False # OPEN
|
|
62
|
+
|
|
63
|
+
def record_success(self) -> None:
|
|
64
|
+
"""Record a successful call."""
|
|
65
|
+
if self._state == self.HALF_OPEN:
|
|
66
|
+
self._state = self.CLOSED
|
|
67
|
+
self._failure_count = 0
|
|
68
|
+
self._half_open_calls = 0
|
|
69
|
+
|
|
70
|
+
def record_failure(self) -> None:
|
|
71
|
+
"""Record a failed call."""
|
|
72
|
+
self._failure_count += 1
|
|
73
|
+
self._last_failure_time = time.time()
|
|
74
|
+
|
|
75
|
+
if self._state == self.HALF_OPEN:
|
|
76
|
+
# Failed during recovery test, go back to OPEN
|
|
77
|
+
self._state = self.OPEN
|
|
78
|
+
elif self._failure_count >= self._failure_threshold:
|
|
79
|
+
self._state = self.OPEN
|
|
80
|
+
|
|
81
|
+
def record_half_open_call(self) -> None:
|
|
82
|
+
"""Record a call attempt in HALF_OPEN state."""
|
|
83
|
+
self._half_open_calls += 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def exponential_backoff_with_jitter(
|
|
87
|
+
attempt: int,
|
|
88
|
+
base_delay: float = 1.0,
|
|
89
|
+
max_delay: float = 30.0,
|
|
90
|
+
jitter: float = 0.5
|
|
91
|
+
) -> float:
|
|
92
|
+
"""
|
|
93
|
+
Calculate backoff delay with jitter to prevent thundering herd.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
attempt: Zero-based attempt number
|
|
97
|
+
base_delay: Base delay in seconds
|
|
98
|
+
max_delay: Maximum delay cap
|
|
99
|
+
jitter: Jitter factor (0-1), adds randomness
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Delay in seconds
|
|
103
|
+
"""
|
|
104
|
+
delay = min(base_delay * (2 ** attempt), max_delay)
|
|
105
|
+
jitter_range = delay * jitter
|
|
106
|
+
return delay + random.uniform(-jitter_range, jitter_range)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DominusCache:
|
|
110
|
+
"""
|
|
111
|
+
Internal process-local cache with auto-encryption.
|
|
112
|
+
|
|
113
|
+
Used by dominus services only:
|
|
114
|
+
- Validation state
|
|
115
|
+
- Service URLs
|
|
116
|
+
- API responses
|
|
117
|
+
|
|
118
|
+
NOT accessible by SDK users.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, default_ttl: int = 300):
|
|
122
|
+
self._store: Dict[str, Tuple[bytes, float]] = {}
|
|
123
|
+
self._default_ttl = default_ttl
|
|
124
|
+
self._cipher: Optional[Fernet] = None
|
|
125
|
+
|
|
126
|
+
def set_encryption_key(self, token: str) -> None:
|
|
127
|
+
"""Initialize encryption using auth token."""
|
|
128
|
+
if not token:
|
|
129
|
+
return
|
|
130
|
+
key = base64.urlsafe_b64encode(sha256(token.encode()).digest())
|
|
131
|
+
self._cipher = Fernet(key)
|
|
132
|
+
|
|
133
|
+
def get(self, key: str) -> Optional[Any]:
|
|
134
|
+
"""Get and decrypt, refresh TTL."""
|
|
135
|
+
entry = self._store.get(key)
|
|
136
|
+
if not entry:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
encrypted_value, expires_at = entry
|
|
140
|
+
|
|
141
|
+
# Check expiry
|
|
142
|
+
if time.time() >= expires_at:
|
|
143
|
+
del self._store[key]
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
# Decrypt
|
|
147
|
+
if self._cipher:
|
|
148
|
+
try:
|
|
149
|
+
decrypted = self._cipher.decrypt(encrypted_value)
|
|
150
|
+
value = json.loads(decrypted.decode())
|
|
151
|
+
except Exception:
|
|
152
|
+
del self._store[key]
|
|
153
|
+
return None
|
|
154
|
+
else:
|
|
155
|
+
value = json.loads(encrypted_value.decode())
|
|
156
|
+
|
|
157
|
+
# Touch TTL
|
|
158
|
+
self._store[key] = (encrypted_value, time.time() + self._default_ttl)
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
162
|
+
"""Encrypt and store."""
|
|
163
|
+
duration = ttl if ttl is not None else self._default_ttl
|
|
164
|
+
|
|
165
|
+
if self._cipher:
|
|
166
|
+
plaintext = json.dumps(value).encode()
|
|
167
|
+
encrypted_value = self._cipher.encrypt(plaintext)
|
|
168
|
+
else:
|
|
169
|
+
encrypted_value = json.dumps(value).encode()
|
|
170
|
+
|
|
171
|
+
self._store[key] = (encrypted_value, time.time() + duration)
|
|
172
|
+
|
|
173
|
+
def delete(self, key: str) -> bool:
|
|
174
|
+
"""Delete key."""
|
|
175
|
+
return bool(self._store.pop(key, None))
|
|
176
|
+
|
|
177
|
+
def clear(self) -> int:
|
|
178
|
+
"""Clear all."""
|
|
179
|
+
count = len(self._store)
|
|
180
|
+
self._store.clear()
|
|
181
|
+
return count
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Internal singletons - NOT exported to users
|
|
185
|
+
dominus_cache = DominusCache(default_ttl=300)
|
|
186
|
+
|
|
187
|
+
# Circuit breakers for different services (prevents retry storms)
|
|
188
|
+
sovereign_circuit_breaker = CircuitBreaker(
|
|
189
|
+
failure_threshold=5, # Open after 5 consecutive failures
|
|
190
|
+
recovery_timeout=30.0, # Try again after 30 seconds
|
|
191
|
+
half_open_max_calls=1 # Allow 1 test call in half-open state
|
|
192
|
+
)
|