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 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)
@@ -0,0 +1,2 @@
1
+ """Helper modules for CB Dominus SDK - Internal use only"""
2
+
@@ -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
+
@@ -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
+ )