dominus-sdk-python-dev 2.1.6__py3-none-any.whl → 2.1.8__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/helpers/auth.py CHANGED
@@ -1,23 +1,23 @@
1
- """
2
- Token resolution helper.
3
-
4
- Handles resolution of authentication token from environment variables.
5
- No hardcoded tokens supported - environment variable only.
6
- """
7
- import os
8
- from typing import Optional
9
-
10
-
11
- def _resolve_token() -> Optional[str]:
12
- """
13
- Resolve auth token from environment variable.
14
-
15
- Note: When fetching from Infisical via Sovereign, the secret is stored as
16
- PROVISION_DOMINUS_TOKEN but should be set as DOMINUS_TOKEN in environment.
17
- The PROVISION_ prefix is dropped when setting environment variables.
18
-
19
- Returns:
20
- Token string or None if not set
21
- """
22
- return os.getenv("DOMINUS_TOKEN")
23
-
1
+ """
2
+ Token resolution helper.
3
+
4
+ Handles resolution of authentication token from environment variables.
5
+ No hardcoded tokens supported - environment variable only.
6
+ """
7
+ import os
8
+ from typing import Optional
9
+
10
+
11
+ def _resolve_token() -> Optional[str]:
12
+ """
13
+ Resolve auth token from environment variable.
14
+
15
+ Note: When fetching from Infisical via Sovereign, the secret is stored as
16
+ PROVISION_DOMINUS_TOKEN but should be set as DOMINUS_TOKEN in environment.
17
+ The PROVISION_ prefix is dropped when setting environment variables.
18
+
19
+ Returns:
20
+ Token string or None if not set
21
+ """
22
+ return os.getenv("DOMINUS_TOKEN")
23
+
dominus/helpers/cache.py CHANGED
@@ -1,192 +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
- )
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
+ )