miso-client 0.1.0__py3-none-any.whl → 3.7.2__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.
- miso_client/__init__.py +523 -130
- miso_client/api/__init__.py +35 -0
- miso_client/api/auth_api.py +367 -0
- miso_client/api/logs_api.py +91 -0
- miso_client/api/permissions_api.py +88 -0
- miso_client/api/roles_api.py +88 -0
- miso_client/api/types/__init__.py +75 -0
- miso_client/api/types/auth_types.py +183 -0
- miso_client/api/types/logs_types.py +71 -0
- miso_client/api/types/permissions_types.py +31 -0
- miso_client/api/types/roles_types.py +31 -0
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +275 -72
- miso_client/models/error_response.py +39 -0
- miso_client/models/filter.py +255 -0
- miso_client/models/pagination.py +44 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/__init__.py +6 -5
- miso_client/services/auth.py +496 -87
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +467 -328
- miso_client/services/logger_chain.py +288 -0
- miso_client/services/permission.py +130 -67
- miso_client/services/redis.py +28 -23
- miso_client/services/role.py +145 -62
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/audit_log_queue.py +222 -0
- miso_client/utils/auth_strategy.py +88 -0
- miso_client/utils/auth_utils.py +65 -0
- miso_client/utils/circuit_breaker.py +125 -0
- miso_client/utils/client_token_manager.py +244 -0
- miso_client/utils/config_loader.py +88 -17
- miso_client/utils/controller_url_resolver.py +80 -0
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/environment_token.py +126 -0
- miso_client/utils/error_utils.py +216 -0
- miso_client/utils/fastapi_endpoints.py +166 -0
- miso_client/utils/filter.py +364 -0
- miso_client/utils/filter_applier.py +143 -0
- miso_client/utils/filter_parser.py +110 -0
- miso_client/utils/flask_endpoints.py +169 -0
- miso_client/utils/http_client.py +494 -262
- miso_client/utils/http_client_logging.py +352 -0
- miso_client/utils/http_client_logging_helpers.py +197 -0
- miso_client/utils/http_client_query_helpers.py +138 -0
- miso_client/utils/http_error_handler.py +92 -0
- miso_client/utils/http_log_formatter.py +115 -0
- miso_client/utils/http_log_masker.py +203 -0
- miso_client/utils/internal_http_client.py +435 -0
- miso_client/utils/jwt_tools.py +125 -16
- miso_client/utils/logger_helpers.py +206 -0
- miso_client/utils/logging_helpers.py +70 -0
- miso_client/utils/origin_validator.py +128 -0
- miso_client/utils/pagination.py +275 -0
- miso_client/utils/request_context.py +285 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- miso_client/utils/token_utils.py +114 -0
- miso_client/utils/url_validator.py +66 -0
- miso_client/utils/user_token_refresh.py +245 -0
- miso_client-3.7.2.dist-info/METADATA +1021 -0
- miso_client-3.7.2.dist-info/RECORD +68 -0
- miso_client-0.1.0.dist-info/METADATA +0 -551
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit log queue for batching multiple logs into single requests.
|
|
3
|
+
|
|
4
|
+
Reduces network overhead by batching audit logs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import signal
|
|
9
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..models.config import AuditConfig, LogEntry, MisoClientConfig
|
|
12
|
+
from ..services.redis import RedisService
|
|
13
|
+
from ..utils.circuit_breaker import CircuitBreaker
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..utils.http_client import HttpClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueuedLogEntry:
|
|
20
|
+
"""Internal class for queued log entries."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, entry: LogEntry, timestamp: int):
|
|
23
|
+
"""
|
|
24
|
+
Initialize queued log entry.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
entry: LogEntry object
|
|
28
|
+
timestamp: Timestamp in milliseconds
|
|
29
|
+
"""
|
|
30
|
+
self.entry = entry
|
|
31
|
+
self.timestamp = timestamp
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuditLogQueue:
|
|
35
|
+
"""
|
|
36
|
+
Audit log queue for batching multiple logs into single requests.
|
|
37
|
+
|
|
38
|
+
Automatically batches audit logs based on size and time thresholds.
|
|
39
|
+
Supports Redis LIST for efficient queuing with HTTP fallback.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
http_client: "HttpClient",
|
|
45
|
+
redis: RedisService,
|
|
46
|
+
config: MisoClientConfig,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize audit log queue.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
http_client: HttpClient instance for sending logs
|
|
53
|
+
redis: RedisService instance for queuing
|
|
54
|
+
config: MisoClientConfig with audit configuration
|
|
55
|
+
"""
|
|
56
|
+
self.http_client = http_client
|
|
57
|
+
self.redis = redis
|
|
58
|
+
self.config = config
|
|
59
|
+
self.queue: List[QueuedLogEntry] = []
|
|
60
|
+
self.flush_timer: Optional[asyncio.Task] = None
|
|
61
|
+
self.is_flushing = False
|
|
62
|
+
|
|
63
|
+
audit_config: Optional[AuditConfig] = config.audit
|
|
64
|
+
self.batch_size: int = (
|
|
65
|
+
audit_config.batchSize if audit_config and audit_config.batchSize is not None else 10
|
|
66
|
+
)
|
|
67
|
+
self.batch_interval: int = (
|
|
68
|
+
audit_config.batchInterval
|
|
69
|
+
if audit_config and audit_config.batchInterval is not None
|
|
70
|
+
else 100
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Initialize circuit breaker for HTTP logging
|
|
74
|
+
circuit_breaker_config = audit_config.circuitBreaker if audit_config else None
|
|
75
|
+
self.circuit_breaker = CircuitBreaker(circuit_breaker_config)
|
|
76
|
+
|
|
77
|
+
# Setup graceful shutdown handlers (if available)
|
|
78
|
+
try:
|
|
79
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
80
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
81
|
+
except (ValueError, OSError):
|
|
82
|
+
# Signal handlers may not be available in all environments
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
def _signal_handler(self, signum, frame):
|
|
86
|
+
"""Handle shutdown signals."""
|
|
87
|
+
# Schedule flush on next event loop iteration
|
|
88
|
+
if asyncio.get_event_loop().is_running():
|
|
89
|
+
asyncio.create_task(self.flush(True))
|
|
90
|
+
|
|
91
|
+
async def add(self, entry: LogEntry) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Add log entry to queue.
|
|
94
|
+
|
|
95
|
+
Automatically flushes if batch size is reached.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
entry: LogEntry to add to queue
|
|
99
|
+
"""
|
|
100
|
+
self.queue.append(QueuedLogEntry(entry, self._current_timestamp()))
|
|
101
|
+
|
|
102
|
+
# Flush if batch size reached
|
|
103
|
+
if len(self.queue) >= self.batch_size:
|
|
104
|
+
await self.flush(False)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Setup flush timer if not already set
|
|
108
|
+
if self.flush_timer is None and len(self.queue) > 0:
|
|
109
|
+
self.flush_timer = asyncio.create_task(self._schedule_flush())
|
|
110
|
+
|
|
111
|
+
async def _schedule_flush(self) -> None:
|
|
112
|
+
"""Schedule automatic flush after batch interval."""
|
|
113
|
+
try:
|
|
114
|
+
await asyncio.sleep(self.batch_interval / 1000.0) # Convert ms to seconds
|
|
115
|
+
await self.flush(False)
|
|
116
|
+
except asyncio.CancelledError:
|
|
117
|
+
# Timer was cancelled, ignore
|
|
118
|
+
pass
|
|
119
|
+
finally:
|
|
120
|
+
self.flush_timer = None
|
|
121
|
+
|
|
122
|
+
def _current_timestamp(self) -> int:
|
|
123
|
+
"""Get current timestamp in milliseconds."""
|
|
124
|
+
import time
|
|
125
|
+
|
|
126
|
+
return int(time.time() * 1000)
|
|
127
|
+
|
|
128
|
+
async def flush(self, sync: bool = False) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Flush queued logs.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
sync: If True, wait for flush to complete (for shutdown)
|
|
134
|
+
"""
|
|
135
|
+
if self.is_flushing:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Cancel flush timer
|
|
139
|
+
if self.flush_timer:
|
|
140
|
+
self.flush_timer.cancel()
|
|
141
|
+
try:
|
|
142
|
+
await self.flush_timer
|
|
143
|
+
except asyncio.CancelledError:
|
|
144
|
+
pass
|
|
145
|
+
self.flush_timer = None
|
|
146
|
+
|
|
147
|
+
if len(self.queue) == 0:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
self.is_flushing = True
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
entries = self.queue[:] # Copy queue
|
|
154
|
+
self.queue.clear() # Clear queue
|
|
155
|
+
|
|
156
|
+
if len(entries) == 0:
|
|
157
|
+
self.is_flushing = False
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
log_entries = [e.entry for e in entries]
|
|
161
|
+
|
|
162
|
+
# Try Redis first (if available)
|
|
163
|
+
if self.redis.is_connected():
|
|
164
|
+
queue_name = f"audit-logs:{self.config.client_id}"
|
|
165
|
+
# Serialize all entries as JSON array
|
|
166
|
+
import json
|
|
167
|
+
|
|
168
|
+
entries_json = json.dumps([entry.model_dump() for entry in log_entries])
|
|
169
|
+
success = await self.redis.rpush(queue_name, entries_json)
|
|
170
|
+
|
|
171
|
+
if success:
|
|
172
|
+
self.is_flushing = False
|
|
173
|
+
return # Successfully queued in Redis
|
|
174
|
+
|
|
175
|
+
# Check circuit breaker before attempting HTTP logging
|
|
176
|
+
if self.circuit_breaker.is_open():
|
|
177
|
+
# Circuit is open, skip HTTP logging to prevent infinite retry loops
|
|
178
|
+
self.is_flushing = False
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Fallback to HTTP batch endpoint
|
|
182
|
+
try:
|
|
183
|
+
await self.http_client.request(
|
|
184
|
+
"POST",
|
|
185
|
+
"/api/v1/logs/batch",
|
|
186
|
+
{
|
|
187
|
+
"logs": [
|
|
188
|
+
entry.model_dump(
|
|
189
|
+
exclude={"environment", "application"}, exclude_none=True
|
|
190
|
+
)
|
|
191
|
+
for entry in log_entries
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
# Record success in circuit breaker
|
|
196
|
+
self.circuit_breaker.record_success()
|
|
197
|
+
except Exception:
|
|
198
|
+
# Failed to send logs - record failure in circuit breaker
|
|
199
|
+
self.circuit_breaker.record_failure()
|
|
200
|
+
# Silently fail to avoid infinite loops
|
|
201
|
+
pass
|
|
202
|
+
except Exception:
|
|
203
|
+
# Silently swallow errors - never break logging
|
|
204
|
+
pass
|
|
205
|
+
finally:
|
|
206
|
+
self.is_flushing = False
|
|
207
|
+
|
|
208
|
+
def get_queue_size(self) -> int:
|
|
209
|
+
"""
|
|
210
|
+
Get current queue size.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Number of entries in queue
|
|
214
|
+
"""
|
|
215
|
+
return len(self.queue)
|
|
216
|
+
|
|
217
|
+
def clear(self) -> None:
|
|
218
|
+
"""Clear queue (for testing/cleanup)."""
|
|
219
|
+
if self.flush_timer:
|
|
220
|
+
self.flush_timer.cancel()
|
|
221
|
+
self.flush_timer = None
|
|
222
|
+
self.queue.clear()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication strategy handler utility.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for managing authentication strategies with
|
|
5
|
+
priority-based fallback support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..models.config import AuthMethod, AuthStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthStrategyHandler:
|
|
14
|
+
"""Handler for authentication strategies with priority-based fallback."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def build_auth_headers(
|
|
18
|
+
method: AuthMethod,
|
|
19
|
+
strategy: AuthStrategy,
|
|
20
|
+
client_token: Optional[str] = None,
|
|
21
|
+
) -> Dict[str, str]:
|
|
22
|
+
"""
|
|
23
|
+
Build authentication headers for a specific auth method.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
method: Authentication method to use
|
|
27
|
+
strategy: Auth strategy configuration
|
|
28
|
+
client_token: Optional client token (for client-token and client-credentials methods)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary of headers to add to the request
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If required credentials are missing for the method
|
|
35
|
+
"""
|
|
36
|
+
headers: Dict[str, str] = {}
|
|
37
|
+
|
|
38
|
+
if method == "bearer":
|
|
39
|
+
if not strategy.bearerToken:
|
|
40
|
+
raise ValueError("bearerToken is required for bearer authentication method")
|
|
41
|
+
headers["Authorization"] = f"Bearer {strategy.bearerToken}"
|
|
42
|
+
|
|
43
|
+
elif method == "client-token":
|
|
44
|
+
if not client_token:
|
|
45
|
+
raise ValueError("client_token is required for client-token authentication method")
|
|
46
|
+
headers["x-client-token"] = client_token
|
|
47
|
+
|
|
48
|
+
elif method == "client-credentials":
|
|
49
|
+
# Client credentials uses the same client token mechanism
|
|
50
|
+
# The client token is already automatically sent via _ensure_client_token
|
|
51
|
+
# This method is mainly for strategy ordering
|
|
52
|
+
if not client_token:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"client_token is required for client-credentials authentication method"
|
|
55
|
+
)
|
|
56
|
+
headers["x-client-token"] = client_token
|
|
57
|
+
|
|
58
|
+
elif method == "api-key":
|
|
59
|
+
if not strategy.apiKey:
|
|
60
|
+
raise ValueError("apiKey is required for api-key authentication method")
|
|
61
|
+
# API key is sent as Bearer token (same format as bearer tokens)
|
|
62
|
+
headers["Authorization"] = f"Bearer {strategy.apiKey}"
|
|
63
|
+
|
|
64
|
+
return headers
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def should_try_method(method: AuthMethod, strategy: AuthStrategy) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if a method should be tried based on the strategy.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
method: Authentication method to check
|
|
73
|
+
strategy: Auth strategy configuration
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if method should be tried, False otherwise
|
|
77
|
+
"""
|
|
78
|
+
return method in strategy.methods
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_default_strategy() -> AuthStrategy:
|
|
82
|
+
"""
|
|
83
|
+
Get default authentication strategy.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Default AuthStrategy with ['bearer', 'client-token'] methods
|
|
87
|
+
"""
|
|
88
|
+
return AuthStrategy(methods=["bearer", "client-token"])
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication utilities for shared use across services.
|
|
3
|
+
|
|
4
|
+
This module provides shared authentication utilities to avoid code duplication
|
|
5
|
+
across service classes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..models.config import AuthStrategy
|
|
11
|
+
from ..utils.http_client import HttpClient
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..api import ApiClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def validate_token_request(
|
|
18
|
+
token: str,
|
|
19
|
+
http_client: HttpClient,
|
|
20
|
+
api_client: Optional["ApiClient"] = None,
|
|
21
|
+
auth_strategy: Optional[AuthStrategy] = None,
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Helper function to call /api/v1/auth/validate endpoint with proper request body.
|
|
25
|
+
|
|
26
|
+
Shared utility for RoleService and PermissionService to avoid code duplication.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
token: JWT token to validate
|
|
30
|
+
http_client: HTTP client instance (for backward compatibility)
|
|
31
|
+
api_client: Optional API client instance (for typed API calls)
|
|
32
|
+
auth_strategy: Optional authentication strategy
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Validation result dictionary
|
|
36
|
+
"""
|
|
37
|
+
if api_client:
|
|
38
|
+
# Use ApiClient for typed API calls
|
|
39
|
+
response = await api_client.auth.validate_token(token, auth_strategy=auth_strategy)
|
|
40
|
+
# Extract data from typed response
|
|
41
|
+
return {
|
|
42
|
+
"success": response.success,
|
|
43
|
+
"data": {
|
|
44
|
+
"authenticated": response.data.authenticated,
|
|
45
|
+
"user": response.data.user.model_dump() if response.data.user else None,
|
|
46
|
+
"expiresAt": response.data.expiresAt,
|
|
47
|
+
},
|
|
48
|
+
"timestamp": response.timestamp,
|
|
49
|
+
}
|
|
50
|
+
else:
|
|
51
|
+
# Fallback to HttpClient for backward compatibility
|
|
52
|
+
if auth_strategy is not None:
|
|
53
|
+
result = await http_client.authenticated_request(
|
|
54
|
+
"POST",
|
|
55
|
+
"/api/v1/auth/validate",
|
|
56
|
+
token,
|
|
57
|
+
{"token": token},
|
|
58
|
+
auth_strategy=auth_strategy,
|
|
59
|
+
)
|
|
60
|
+
return result # type: ignore[no-any-return]
|
|
61
|
+
else:
|
|
62
|
+
result = await http_client.authenticated_request(
|
|
63
|
+
"POST", "/api/v1/auth/validate", token, {"token": token}
|
|
64
|
+
)
|
|
65
|
+
return result # type: ignore[no-any-return]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit breaker implementation for HTTP logging.
|
|
3
|
+
|
|
4
|
+
Prevents infinite retry loops when logging service is unavailable by opening
|
|
5
|
+
the circuit after consecutive failures and resetting after a timeout period.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from ..models.config import CircuitBreakerConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CircuitState(Enum):
|
|
16
|
+
"""Circuit breaker state."""
|
|
17
|
+
|
|
18
|
+
CLOSED = "CLOSED" # Normal operation, requests allowed
|
|
19
|
+
OPEN = "OPEN" # Circuit open, requests blocked
|
|
20
|
+
HALF_OPEN = "HALF_OPEN" # Testing if service recovered
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CircuitBreaker:
|
|
24
|
+
"""
|
|
25
|
+
Circuit breaker for HTTP logging.
|
|
26
|
+
|
|
27
|
+
Prevents infinite retry loops when logging service is unavailable.
|
|
28
|
+
Opens circuit after consecutive failures and resets after timeout period.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
failure_threshold: Number of consecutive failures before opening circuit
|
|
32
|
+
reset_timeout: Seconds to wait before resetting circuit
|
|
33
|
+
state: Current circuit state
|
|
34
|
+
failure_count: Current consecutive failure count
|
|
35
|
+
last_failure_time: Timestamp of last failure
|
|
36
|
+
opened_at: Timestamp when circuit was opened
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: Optional[CircuitBreakerConfig] = None):
|
|
40
|
+
"""
|
|
41
|
+
Initialize circuit breaker with configuration.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Circuit breaker configuration (optional)
|
|
45
|
+
"""
|
|
46
|
+
if config:
|
|
47
|
+
self.failure_threshold = config.failureThreshold or 3
|
|
48
|
+
self.reset_timeout = config.resetTimeout or 60
|
|
49
|
+
else:
|
|
50
|
+
self.failure_threshold = 3
|
|
51
|
+
self.reset_timeout = 60
|
|
52
|
+
|
|
53
|
+
self.state = CircuitState.CLOSED
|
|
54
|
+
self.failure_count = 0
|
|
55
|
+
self.last_failure_time: Optional[float] = None
|
|
56
|
+
self.opened_at: Optional[float] = None
|
|
57
|
+
|
|
58
|
+
def is_open(self) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if circuit is open (requests should be blocked).
|
|
61
|
+
|
|
62
|
+
Automatically transitions from OPEN to HALF_OPEN after reset timeout.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if circuit is open, False otherwise
|
|
66
|
+
"""
|
|
67
|
+
if self.state == CircuitState.CLOSED:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
if self.state == CircuitState.OPEN:
|
|
71
|
+
# Check if reset timeout has passed
|
|
72
|
+
if self.opened_at and (time.time() - self.opened_at) >= self.reset_timeout:
|
|
73
|
+
# Transition to HALF_OPEN to test if service recovered
|
|
74
|
+
self.state = CircuitState.HALF_OPEN
|
|
75
|
+
self.failure_count = 0
|
|
76
|
+
return False
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# HALF_OPEN state - allow requests to test recovery
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def record_success(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Record successful request.
|
|
85
|
+
|
|
86
|
+
Resets failure count and closes circuit if it was open.
|
|
87
|
+
"""
|
|
88
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
89
|
+
# Service recovered, close circuit
|
|
90
|
+
self.state = CircuitState.CLOSED
|
|
91
|
+
self.failure_count = 0
|
|
92
|
+
self.opened_at = None
|
|
93
|
+
elif self.state == CircuitState.CLOSED:
|
|
94
|
+
# Reset failure count on success
|
|
95
|
+
self.failure_count = 0
|
|
96
|
+
|
|
97
|
+
def record_failure(self) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Record failed request.
|
|
100
|
+
|
|
101
|
+
Increments failure count and opens circuit if threshold reached.
|
|
102
|
+
"""
|
|
103
|
+
self.failure_count += 1
|
|
104
|
+
self.last_failure_time = time.time()
|
|
105
|
+
|
|
106
|
+
if self.failure_count >= self.failure_threshold:
|
|
107
|
+
# Open circuit
|
|
108
|
+
self.state = CircuitState.OPEN
|
|
109
|
+
self.opened_at = time.time()
|
|
110
|
+
|
|
111
|
+
def reset(self) -> None:
|
|
112
|
+
"""Reset circuit breaker to initial state."""
|
|
113
|
+
self.state = CircuitState.CLOSED
|
|
114
|
+
self.failure_count = 0
|
|
115
|
+
self.last_failure_time = None
|
|
116
|
+
self.opened_at = None
|
|
117
|
+
|
|
118
|
+
def get_state(self) -> CircuitState:
|
|
119
|
+
"""
|
|
120
|
+
Get current circuit state.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Current circuit state
|
|
124
|
+
"""
|
|
125
|
+
return self.state
|