unrealon 1.1.6__py3-none-any.whl → 2.0.4__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.
- {unrealon-1.1.6.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
- unrealon-2.0.4.dist-info/METADATA +491 -0
- unrealon-2.0.4.dist-info/RECORD +129 -0
- {unrealon-1.1.6.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
- unrealon-2.0.4.dist-info/entry_points.txt +3 -0
- unrealon-2.0.4.dist-info/top_level.txt +3 -0
- unrealon_browser/__init__.py +5 -6
- unrealon_browser/cli/browser_cli.py +18 -9
- unrealon_browser/cli/interactive_mode.py +13 -4
- unrealon_browser/core/browser_manager.py +29 -16
- unrealon_browser/dto/__init__.py +21 -0
- unrealon_browser/dto/bot_detection.py +175 -0
- unrealon_browser/dto/models/config.py +9 -3
- unrealon_browser/managers/__init__.py +1 -1
- unrealon_browser/managers/logger_bridge.py +1 -4
- unrealon_browser/stealth/__init__.py +27 -0
- unrealon_browser/stealth/bypass_techniques.pyc +0 -0
- unrealon_browser/stealth/manager.pyc +0 -0
- unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
- unrealon_browser/stealth/playwright_stealth.pyc +0 -0
- unrealon_browser/stealth/scanner_tester.pyc +0 -0
- unrealon_browser/stealth/undetected_chrome.pyc +0 -0
- unrealon_core/__init__.py +160 -0
- unrealon_core/config/__init__.py +16 -0
- unrealon_core/config/environment.py +98 -0
- unrealon_core/config/urls.py +93 -0
- unrealon_core/enums/__init__.py +24 -0
- unrealon_core/enums/status.py +216 -0
- unrealon_core/enums/types.py +240 -0
- unrealon_core/error_handling/__init__.py +45 -0
- unrealon_core/error_handling/circuit_breaker.py +292 -0
- unrealon_core/error_handling/error_context.py +324 -0
- unrealon_core/error_handling/recovery.py +371 -0
- unrealon_core/error_handling/retry.py +268 -0
- unrealon_core/exceptions/__init__.py +46 -0
- unrealon_core/exceptions/base.py +292 -0
- unrealon_core/exceptions/communication.py +22 -0
- unrealon_core/exceptions/driver.py +11 -0
- unrealon_core/exceptions/proxy.py +11 -0
- unrealon_core/exceptions/task.py +12 -0
- unrealon_core/exceptions/validation.py +17 -0
- unrealon_core/models/__init__.py +98 -0
- unrealon_core/models/arq_context.py +252 -0
- unrealon_core/models/arq_responses.py +125 -0
- unrealon_core/models/base.py +291 -0
- unrealon_core/models/bridge_stats.py +58 -0
- unrealon_core/models/communication.py +39 -0
- unrealon_core/models/config.py +47 -0
- unrealon_core/models/connection_stats.py +47 -0
- unrealon_core/models/driver.py +30 -0
- unrealon_core/models/driver_details.py +98 -0
- unrealon_core/models/logging.py +28 -0
- unrealon_core/models/task.py +21 -0
- unrealon_core/models/typed_responses.py +210 -0
- unrealon_core/models/websocket/__init__.py +91 -0
- unrealon_core/models/websocket/base.py +49 -0
- unrealon_core/models/websocket/config.py +200 -0
- unrealon_core/models/websocket/driver.py +215 -0
- unrealon_core/models/websocket/errors.py +138 -0
- unrealon_core/models/websocket/heartbeat.py +100 -0
- unrealon_core/models/websocket/logging.py +261 -0
- unrealon_core/models/websocket/proxy.py +496 -0
- unrealon_core/models/websocket/tasks.py +275 -0
- unrealon_core/models/websocket/utils.py +153 -0
- unrealon_core/models/websocket_session.py +144 -0
- unrealon_core/monitoring/__init__.py +43 -0
- unrealon_core/monitoring/alerts.py +398 -0
- unrealon_core/monitoring/dashboard.py +307 -0
- unrealon_core/monitoring/health_check.py +354 -0
- unrealon_core/monitoring/metrics.py +352 -0
- unrealon_core/utils/__init__.py +11 -0
- unrealon_core/utils/time.py +61 -0
- unrealon_core/version.py +219 -0
- unrealon_driver/__init__.py +90 -51
- unrealon_driver/core_module/__init__.py +34 -0
- unrealon_driver/core_module/base.py +184 -0
- unrealon_driver/core_module/config.py +30 -0
- unrealon_driver/core_module/event_manager.py +127 -0
- unrealon_driver/core_module/protocols.py +98 -0
- unrealon_driver/core_module/registry.py +146 -0
- unrealon_driver/decorators/__init__.py +15 -0
- unrealon_driver/decorators/retry.py +117 -0
- unrealon_driver/decorators/schedule.py +137 -0
- unrealon_driver/decorators/task.py +61 -0
- unrealon_driver/decorators/timing.py +132 -0
- unrealon_driver/driver/__init__.py +20 -0
- unrealon_driver/driver/communication/__init__.py +10 -0
- unrealon_driver/driver/communication/session.py +203 -0
- unrealon_driver/driver/communication/websocket_client.py +197 -0
- unrealon_driver/driver/core/__init__.py +10 -0
- unrealon_driver/driver/core/config.py +85 -0
- unrealon_driver/driver/core/driver.py +221 -0
- unrealon_driver/driver/factory/__init__.py +9 -0
- unrealon_driver/driver/factory/manager_factory.py +130 -0
- unrealon_driver/driver/lifecycle/__init__.py +11 -0
- unrealon_driver/driver/lifecycle/daemon.py +76 -0
- unrealon_driver/driver/lifecycle/initialization.py +97 -0
- unrealon_driver/driver/lifecycle/shutdown.py +48 -0
- unrealon_driver/driver/monitoring/__init__.py +9 -0
- unrealon_driver/driver/monitoring/health.py +63 -0
- unrealon_driver/driver/utilities/__init__.py +10 -0
- unrealon_driver/driver/utilities/logging.py +51 -0
- unrealon_driver/driver/utilities/serialization.py +61 -0
- unrealon_driver/managers/__init__.py +32 -0
- unrealon_driver/managers/base.py +174 -0
- unrealon_driver/managers/browser.py +98 -0
- unrealon_driver/managers/cache.py +116 -0
- unrealon_driver/managers/http.py +107 -0
- unrealon_driver/managers/logger.py +286 -0
- unrealon_driver/managers/proxy.py +99 -0
- unrealon_driver/managers/registry.py +87 -0
- unrealon_driver/managers/threading.py +54 -0
- unrealon_driver/managers/update.py +107 -0
- unrealon_driver/utils/__init__.py +9 -0
- unrealon_driver/utils/time.py +10 -0
- unrealon-1.1.6.dist-info/METADATA +0 -625
- unrealon-1.1.6.dist-info/RECORD +0 -55
- unrealon-1.1.6.dist-info/entry_points.txt +0 -9
- unrealon_browser/managers/stealth.py +0 -388
- unrealon_driver/README.md +0 -0
- unrealon_driver/exceptions.py +0 -33
- unrealon_driver/html_analyzer/__init__.py +0 -32
- unrealon_driver/html_analyzer/cleaner.py +0 -657
- unrealon_driver/html_analyzer/config.py +0 -64
- unrealon_driver/html_analyzer/manager.py +0 -247
- unrealon_driver/html_analyzer/models.py +0 -115
- unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
- unrealon_driver/models/__init__.py +0 -31
- unrealon_driver/models/websocket.py +0 -98
- unrealon_driver/parser/__init__.py +0 -36
- unrealon_driver/parser/cli_manager.py +0 -142
- unrealon_driver/parser/daemon_manager.py +0 -403
- unrealon_driver/parser/managers/__init__.py +0 -25
- unrealon_driver/parser/managers/config.py +0 -293
- unrealon_driver/parser/managers/error.py +0 -412
- unrealon_driver/parser/managers/result.py +0 -321
- unrealon_driver/parser/parser_manager.py +0 -458
- unrealon_driver/smart_logging/__init__.py +0 -24
- unrealon_driver/smart_logging/models.py +0 -44
- unrealon_driver/smart_logging/smart_logger.py +0 -406
- unrealon_driver/smart_logging/unified_logger.py +0 -525
- unrealon_driver/websocket/__init__.py +0 -31
- unrealon_driver/websocket/client.py +0 -249
- unrealon_driver/websocket/config.py +0 -188
- unrealon_driver/websocket/manager.py +0 -90
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Context System
|
|
3
|
+
|
|
4
|
+
Rich error context for debugging and monitoring.
|
|
5
|
+
Following critical requirements - max 500 lines, functions < 20 lines.
|
|
6
|
+
|
|
7
|
+
Phase 2: Core Systems - Error Handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import traceback
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, Dict, Optional, List
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
16
|
+
|
|
17
|
+
from ..utils.time import utc_now
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorSeverity(str, Enum):
|
|
21
|
+
"""Error severity levels."""
|
|
22
|
+
LOW = "low"
|
|
23
|
+
MEDIUM = "medium"
|
|
24
|
+
HIGH = "high"
|
|
25
|
+
CRITICAL = "critical"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ErrorContext(BaseModel):
|
|
29
|
+
"""Rich error context information."""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(
|
|
32
|
+
validate_assignment=True,
|
|
33
|
+
extra="forbid"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Basic error info
|
|
37
|
+
error_id: str = Field(description="Unique error identifier")
|
|
38
|
+
error_type: str = Field(description="Error class name")
|
|
39
|
+
error_message: str = Field(description="Error message")
|
|
40
|
+
severity: ErrorSeverity = Field(description="Error severity level")
|
|
41
|
+
|
|
42
|
+
# Timing
|
|
43
|
+
timestamp: datetime = Field(description="When error occurred")
|
|
44
|
+
duration_ms: Optional[float] = Field(default=None, description="Operation duration before error")
|
|
45
|
+
|
|
46
|
+
# Context
|
|
47
|
+
operation: str = Field(description="Operation that failed")
|
|
48
|
+
component: str = Field(description="Component where error occurred")
|
|
49
|
+
user_id: Optional[str] = Field(default=None, description="Associated user ID")
|
|
50
|
+
request_id: Optional[str] = Field(default=None, description="Request correlation ID")
|
|
51
|
+
|
|
52
|
+
# Technical details
|
|
53
|
+
stack_trace: Optional[str] = Field(default=None, description="Full stack trace")
|
|
54
|
+
function_name: str = Field(description="Function where error occurred")
|
|
55
|
+
file_name: str = Field(description="File where error occurred")
|
|
56
|
+
line_number: int = Field(description="Line number where error occurred")
|
|
57
|
+
|
|
58
|
+
# Additional context
|
|
59
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="Function parameters")
|
|
60
|
+
environment: Dict[str, str] = Field(default_factory=dict, description="Environment variables")
|
|
61
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
|
62
|
+
|
|
63
|
+
# Recovery info
|
|
64
|
+
is_retryable: bool = Field(default=True, description="Whether error is retryable")
|
|
65
|
+
retry_count: int = Field(default=0, description="Number of retries attempted")
|
|
66
|
+
recovery_suggestions: List[str] = Field(default_factory=list, description="Recovery suggestions")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_error_context(
|
|
70
|
+
error: Exception,
|
|
71
|
+
operation: str,
|
|
72
|
+
component: str,
|
|
73
|
+
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
|
74
|
+
**kwargs
|
|
75
|
+
) -> ErrorContext:
|
|
76
|
+
"""
|
|
77
|
+
Create error context from exception.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
error: Exception that occurred
|
|
81
|
+
operation: Operation that failed
|
|
82
|
+
component: Component where error occurred
|
|
83
|
+
severity: Error severity level
|
|
84
|
+
**kwargs: Additional context data
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
ErrorContext with rich error information
|
|
88
|
+
"""
|
|
89
|
+
import uuid
|
|
90
|
+
import inspect
|
|
91
|
+
import os
|
|
92
|
+
|
|
93
|
+
# Get stack trace info
|
|
94
|
+
tb = traceback.extract_tb(error.__traceback__)
|
|
95
|
+
if tb:
|
|
96
|
+
frame = tb[-1] # Last frame (where error occurred)
|
|
97
|
+
file_name = os.path.basename(frame.filename)
|
|
98
|
+
function_name = frame.name
|
|
99
|
+
line_number = frame.lineno
|
|
100
|
+
else:
|
|
101
|
+
file_name = "unknown"
|
|
102
|
+
function_name = "unknown"
|
|
103
|
+
line_number = 0
|
|
104
|
+
|
|
105
|
+
# Generate error ID
|
|
106
|
+
error_id = str(uuid.uuid4())[:8]
|
|
107
|
+
|
|
108
|
+
# Extract parameters from current frame
|
|
109
|
+
parameters = {}
|
|
110
|
+
try:
|
|
111
|
+
frame = inspect.currentframe()
|
|
112
|
+
if frame and frame.f_back:
|
|
113
|
+
parameters = {
|
|
114
|
+
k: str(v) for k, v in frame.f_back.f_locals.items()
|
|
115
|
+
if not k.startswith('_') and not callable(v)
|
|
116
|
+
}
|
|
117
|
+
except Exception:
|
|
118
|
+
pass # Ignore parameter extraction errors
|
|
119
|
+
|
|
120
|
+
# Basic environment info
|
|
121
|
+
environment = {
|
|
122
|
+
'python_version': f"{os.sys.version_info.major}.{os.sys.version_info.minor}",
|
|
123
|
+
'platform': os.sys.platform,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return ErrorContext(
|
|
127
|
+
error_id=error_id,
|
|
128
|
+
error_type=error.__class__.__name__,
|
|
129
|
+
error_message=str(error),
|
|
130
|
+
severity=severity,
|
|
131
|
+
timestamp=utc_now(),
|
|
132
|
+
operation=operation,
|
|
133
|
+
component=component,
|
|
134
|
+
stack_trace=traceback.format_exc(),
|
|
135
|
+
function_name=function_name,
|
|
136
|
+
file_name=file_name,
|
|
137
|
+
line_number=line_number,
|
|
138
|
+
parameters=parameters,
|
|
139
|
+
environment=environment,
|
|
140
|
+
**kwargs
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def format_error_context(context: ErrorContext, include_stack_trace: bool = False) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Format error context for logging.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
context: Error context to format
|
|
150
|
+
include_stack_trace: Whether to include full stack trace
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Formatted error message
|
|
154
|
+
"""
|
|
155
|
+
lines = [
|
|
156
|
+
f"🚨 ERROR [{context.error_id}] {context.severity.upper()}",
|
|
157
|
+
f" Type: {context.error_type}",
|
|
158
|
+
f" Message: {context.error_message}",
|
|
159
|
+
f" Operation: {context.operation}",
|
|
160
|
+
f" Component: {context.component}",
|
|
161
|
+
f" Location: {context.file_name}:{context.line_number} in {context.function_name}()",
|
|
162
|
+
f" Time: {context.timestamp.isoformat()}",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if context.duration_ms:
|
|
166
|
+
lines.append(f" Duration: {context.duration_ms:.2f}ms")
|
|
167
|
+
|
|
168
|
+
if context.request_id:
|
|
169
|
+
lines.append(f" Request ID: {context.request_id}")
|
|
170
|
+
|
|
171
|
+
if context.user_id:
|
|
172
|
+
lines.append(f" User ID: {context.user_id}")
|
|
173
|
+
|
|
174
|
+
if context.retry_count > 0:
|
|
175
|
+
lines.append(f" Retries: {context.retry_count}")
|
|
176
|
+
|
|
177
|
+
if context.parameters:
|
|
178
|
+
lines.append(" Parameters:")
|
|
179
|
+
for key, value in context.parameters.items():
|
|
180
|
+
lines.append(f" {key}: {value}")
|
|
181
|
+
|
|
182
|
+
if context.recovery_suggestions:
|
|
183
|
+
lines.append(" Recovery suggestions:")
|
|
184
|
+
for suggestion in context.recovery_suggestions:
|
|
185
|
+
lines.append(f" • {suggestion}")
|
|
186
|
+
|
|
187
|
+
if include_stack_trace and context.stack_trace:
|
|
188
|
+
lines.append(" Stack trace:")
|
|
189
|
+
for line in context.stack_trace.split('\n'):
|
|
190
|
+
if line.strip():
|
|
191
|
+
lines.append(f" {line}")
|
|
192
|
+
|
|
193
|
+
return '\n'.join(lines)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def determine_severity(error: Exception) -> ErrorSeverity:
|
|
197
|
+
"""
|
|
198
|
+
Determine error severity based on exception type.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
error: Exception to analyze
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Appropriate severity level
|
|
205
|
+
"""
|
|
206
|
+
error_name = error.__class__.__name__
|
|
207
|
+
|
|
208
|
+
# Critical errors
|
|
209
|
+
critical_errors = [
|
|
210
|
+
'SystemError', 'MemoryError', 'KeyboardInterrupt',
|
|
211
|
+
'SystemExit', 'GeneratorExit'
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
# High severity errors
|
|
215
|
+
high_errors = [
|
|
216
|
+
'ConnectionError', 'DatabaseError', 'AuthenticationError',
|
|
217
|
+
'PermissionError', 'SecurityError'
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# Medium severity errors
|
|
221
|
+
medium_errors = [
|
|
222
|
+
'ValidationError', 'ValueError', 'TypeError',
|
|
223
|
+
'AttributeError', 'KeyError', 'IndexError'
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
if error_name in critical_errors:
|
|
227
|
+
return ErrorSeverity.CRITICAL
|
|
228
|
+
elif error_name in high_errors:
|
|
229
|
+
return ErrorSeverity.HIGH
|
|
230
|
+
elif error_name in medium_errors:
|
|
231
|
+
return ErrorSeverity.MEDIUM
|
|
232
|
+
else:
|
|
233
|
+
return ErrorSeverity.LOW
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def is_retryable_error(error: Exception) -> bool:
|
|
237
|
+
"""
|
|
238
|
+
Determine if error is retryable.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
error: Exception to analyze
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if error should be retried
|
|
245
|
+
"""
|
|
246
|
+
error_name = error.__class__.__name__
|
|
247
|
+
|
|
248
|
+
# Non-retryable errors
|
|
249
|
+
non_retryable = [
|
|
250
|
+
'ValidationError', 'ValueError', 'TypeError',
|
|
251
|
+
'AttributeError', 'KeyError', 'IndexError',
|
|
252
|
+
'AuthenticationError', 'PermissionError',
|
|
253
|
+
'NotImplementedError', 'AssertionError'
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
# Retryable errors
|
|
257
|
+
retryable = [
|
|
258
|
+
'ConnectionError', 'TimeoutError', 'HTTPError',
|
|
259
|
+
'NetworkError', 'ServiceUnavailableError',
|
|
260
|
+
'TemporaryError', 'RateLimitError'
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
if error_name in non_retryable:
|
|
264
|
+
return False
|
|
265
|
+
elif error_name in retryable:
|
|
266
|
+
return True
|
|
267
|
+
else:
|
|
268
|
+
# Default to retryable for unknown errors
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_recovery_suggestions(error: Exception, operation: str) -> List[str]:
|
|
273
|
+
"""
|
|
274
|
+
Get recovery suggestions for error.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
error: Exception that occurred
|
|
278
|
+
operation: Operation that failed
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of recovery suggestions
|
|
282
|
+
"""
|
|
283
|
+
error_name = error.__class__.__name__
|
|
284
|
+
suggestions = []
|
|
285
|
+
|
|
286
|
+
if error_name == 'ConnectionError':
|
|
287
|
+
suggestions.extend([
|
|
288
|
+
"Check network connectivity",
|
|
289
|
+
"Verify service endpoint is accessible",
|
|
290
|
+
"Check firewall settings"
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
elif error_name == 'TimeoutError':
|
|
294
|
+
suggestions.extend([
|
|
295
|
+
"Increase timeout value",
|
|
296
|
+
"Check service performance",
|
|
297
|
+
"Retry with exponential backoff"
|
|
298
|
+
])
|
|
299
|
+
|
|
300
|
+
elif error_name == 'ValidationError':
|
|
301
|
+
suggestions.extend([
|
|
302
|
+
"Check input data format",
|
|
303
|
+
"Verify required fields are present",
|
|
304
|
+
"Review data validation rules"
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
elif error_name == 'AuthenticationError':
|
|
308
|
+
suggestions.extend([
|
|
309
|
+
"Check authentication credentials",
|
|
310
|
+
"Verify token is not expired",
|
|
311
|
+
"Refresh authentication token"
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
elif error_name == 'PermissionError':
|
|
315
|
+
suggestions.extend([
|
|
316
|
+
"Check user permissions",
|
|
317
|
+
"Verify access rights for operation",
|
|
318
|
+
"Contact administrator for access"
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
suggestions.append(f"Review {operation} implementation")
|
|
323
|
+
|
|
324
|
+
return suggestions
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recovery System
|
|
3
|
+
|
|
4
|
+
Automatic error recovery and healing mechanisms.
|
|
5
|
+
Following critical requirements - max 500 lines, functions < 20 lines.
|
|
6
|
+
|
|
7
|
+
Phase 2: Core Systems - Error Handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
17
|
+
|
|
18
|
+
from .error_context import ErrorContext, ErrorSeverity
|
|
19
|
+
from ..utils.time import utc_now
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecoveryStrategy(str, Enum):
|
|
26
|
+
"""Recovery strategy types."""
|
|
27
|
+
RETRY = "retry"
|
|
28
|
+
FALLBACK = "fallback"
|
|
29
|
+
CIRCUIT_BREAK = "circuit_break"
|
|
30
|
+
GRACEFUL_DEGRADE = "graceful_degrade"
|
|
31
|
+
RESTART = "restart"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RecoveryAction(BaseModel):
|
|
35
|
+
"""Recovery action configuration."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
validate_assignment=True,
|
|
39
|
+
extra="forbid"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
strategy: RecoveryStrategy = Field(description="Recovery strategy to use")
|
|
43
|
+
max_attempts: int = Field(default=3, ge=1, le=10, description="Maximum recovery attempts")
|
|
44
|
+
delay_seconds: float = Field(default=1.0, ge=0.1, le=60.0, description="Delay between attempts")
|
|
45
|
+
timeout_seconds: float = Field(default=30.0, ge=1.0, le=300.0, description="Recovery timeout")
|
|
46
|
+
fallback_value: Optional[Any] = Field(default=None, description="Fallback value to return")
|
|
47
|
+
enabled: bool = Field(default=True, description="Whether recovery is enabled")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RecoveryResult(BaseModel):
|
|
51
|
+
"""Result of recovery attempt."""
|
|
52
|
+
|
|
53
|
+
model_config = ConfigDict(
|
|
54
|
+
validate_assignment=True,
|
|
55
|
+
extra="forbid"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
success: bool = Field(description="Whether recovery succeeded")
|
|
59
|
+
strategy_used: RecoveryStrategy = Field(description="Strategy that was used")
|
|
60
|
+
attempts_made: int = Field(description="Number of recovery attempts")
|
|
61
|
+
duration_seconds: float = Field(description="Total recovery duration")
|
|
62
|
+
result: Optional[Any] = Field(default=None, description="Recovery result")
|
|
63
|
+
error_message: Optional[str] = Field(default=None, description="Error if recovery failed")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AutoRecovery:
|
|
67
|
+
"""
|
|
68
|
+
Automatic recovery system.
|
|
69
|
+
|
|
70
|
+
Provides intelligent error recovery based on error context
|
|
71
|
+
and configured recovery strategies.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
"""Initialize auto recovery system."""
|
|
76
|
+
self._recovery_actions: Dict[str, RecoveryAction] = {}
|
|
77
|
+
self._recovery_stats: Dict[str, Dict[str, int]] = {}
|
|
78
|
+
self.logger = logging.getLogger("auto_recovery")
|
|
79
|
+
|
|
80
|
+
def register_recovery_action(
|
|
81
|
+
self,
|
|
82
|
+
error_type: str,
|
|
83
|
+
action: RecoveryAction
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Register recovery action for error type.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
error_type: Exception class name
|
|
90
|
+
action: Recovery action configuration
|
|
91
|
+
"""
|
|
92
|
+
self._recovery_actions[error_type] = action
|
|
93
|
+
self._recovery_stats[error_type] = {
|
|
94
|
+
'attempts': 0,
|
|
95
|
+
'successes': 0,
|
|
96
|
+
'failures': 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.logger.info(f"Registered recovery action for {error_type}: {action.strategy}")
|
|
100
|
+
|
|
101
|
+
async def attempt_recovery(
|
|
102
|
+
self,
|
|
103
|
+
error_context: ErrorContext,
|
|
104
|
+
operation_func: Callable[..., Any],
|
|
105
|
+
*args,
|
|
106
|
+
**kwargs
|
|
107
|
+
) -> RecoveryResult:
|
|
108
|
+
"""
|
|
109
|
+
Attempt to recover from error.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
error_context: Context of the error
|
|
113
|
+
operation_func: Function to retry
|
|
114
|
+
*args, **kwargs: Function arguments
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
RecoveryResult with outcome
|
|
118
|
+
"""
|
|
119
|
+
start_time = utc_now()
|
|
120
|
+
error_type = error_context.error_type
|
|
121
|
+
|
|
122
|
+
# Get recovery action for this error type
|
|
123
|
+
action = self._recovery_actions.get(error_type)
|
|
124
|
+
if not action or not action.enabled:
|
|
125
|
+
self.logger.debug(f"No recovery action configured for {error_type}")
|
|
126
|
+
return RecoveryResult(
|
|
127
|
+
success=False,
|
|
128
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
129
|
+
attempts_made=0,
|
|
130
|
+
duration_seconds=0.0,
|
|
131
|
+
error_message="No recovery action configured"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Update stats
|
|
135
|
+
self._recovery_stats[error_type]['attempts'] += 1
|
|
136
|
+
|
|
137
|
+
# Attempt recovery based on strategy
|
|
138
|
+
try:
|
|
139
|
+
if action.strategy == RecoveryStrategy.RETRY:
|
|
140
|
+
result = await self._retry_recovery(action, operation_func, *args, **kwargs)
|
|
141
|
+
elif action.strategy == RecoveryStrategy.FALLBACK:
|
|
142
|
+
result = await self._fallback_recovery(action, operation_func, *args, **kwargs)
|
|
143
|
+
elif action.strategy == RecoveryStrategy.GRACEFUL_DEGRADE:
|
|
144
|
+
result = await self._graceful_degrade_recovery(action, operation_func, *args, **kwargs)
|
|
145
|
+
else:
|
|
146
|
+
result = RecoveryResult(
|
|
147
|
+
success=False,
|
|
148
|
+
strategy_used=action.strategy,
|
|
149
|
+
attempts_made=0,
|
|
150
|
+
duration_seconds=0.0,
|
|
151
|
+
error_message=f"Recovery strategy {action.strategy} not implemented"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Update stats
|
|
155
|
+
if result.success:
|
|
156
|
+
self._recovery_stats[error_type]['successes'] += 1
|
|
157
|
+
else:
|
|
158
|
+
self._recovery_stats[error_type]['failures'] += 1
|
|
159
|
+
|
|
160
|
+
# Calculate duration
|
|
161
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
162
|
+
result.duration_seconds = duration
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.logger.error(f"Recovery attempt failed: {e}")
|
|
168
|
+
self._recovery_stats[error_type]['failures'] += 1
|
|
169
|
+
|
|
170
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
171
|
+
return RecoveryResult(
|
|
172
|
+
success=False,
|
|
173
|
+
strategy_used=action.strategy,
|
|
174
|
+
attempts_made=1,
|
|
175
|
+
duration_seconds=duration,
|
|
176
|
+
error_message=str(e)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def _retry_recovery(
|
|
180
|
+
self,
|
|
181
|
+
action: RecoveryAction,
|
|
182
|
+
operation_func: Callable[..., Any],
|
|
183
|
+
*args,
|
|
184
|
+
**kwargs
|
|
185
|
+
) -> RecoveryResult:
|
|
186
|
+
"""Attempt recovery using retry strategy."""
|
|
187
|
+
for attempt in range(action.max_attempts):
|
|
188
|
+
try:
|
|
189
|
+
self.logger.debug(f"Recovery retry attempt {attempt + 1}/{action.max_attempts}")
|
|
190
|
+
|
|
191
|
+
result = await asyncio.wait_for(
|
|
192
|
+
operation_func(*args, **kwargs),
|
|
193
|
+
timeout=action.timeout_seconds
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.logger.info(f"Recovery succeeded on attempt {attempt + 1}")
|
|
197
|
+
return RecoveryResult(
|
|
198
|
+
success=True,
|
|
199
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
200
|
+
attempts_made=attempt + 1,
|
|
201
|
+
duration_seconds=0.0, # Will be set by caller
|
|
202
|
+
result=result
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
if attempt < action.max_attempts - 1:
|
|
207
|
+
self.logger.warning(f"Recovery attempt {attempt + 1} failed: {e}")
|
|
208
|
+
await asyncio.sleep(action.delay_seconds)
|
|
209
|
+
else:
|
|
210
|
+
self.logger.error(f"All recovery attempts failed: {e}")
|
|
211
|
+
|
|
212
|
+
return RecoveryResult(
|
|
213
|
+
success=False,
|
|
214
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
215
|
+
attempts_made=action.max_attempts,
|
|
216
|
+
duration_seconds=0.0,
|
|
217
|
+
error_message="All retry attempts failed"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def _fallback_recovery(
|
|
221
|
+
self,
|
|
222
|
+
action: RecoveryAction,
|
|
223
|
+
operation_func: Callable[..., Any],
|
|
224
|
+
*args,
|
|
225
|
+
**kwargs
|
|
226
|
+
) -> RecoveryResult:
|
|
227
|
+
"""Attempt recovery using fallback strategy."""
|
|
228
|
+
self.logger.info("Using fallback recovery strategy")
|
|
229
|
+
|
|
230
|
+
return RecoveryResult(
|
|
231
|
+
success=True,
|
|
232
|
+
strategy_used=RecoveryStrategy.FALLBACK,
|
|
233
|
+
attempts_made=1,
|
|
234
|
+
duration_seconds=0.0,
|
|
235
|
+
result=action.fallback_value
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def _graceful_degrade_recovery(
|
|
239
|
+
self,
|
|
240
|
+
action: RecoveryAction,
|
|
241
|
+
operation_func: Callable[..., Any],
|
|
242
|
+
*args,
|
|
243
|
+
**kwargs
|
|
244
|
+
) -> RecoveryResult:
|
|
245
|
+
"""Attempt recovery using graceful degradation."""
|
|
246
|
+
self.logger.info("Using graceful degradation recovery strategy")
|
|
247
|
+
|
|
248
|
+
# Return a simplified/degraded version of the expected result
|
|
249
|
+
degraded_result = {
|
|
250
|
+
"status": "degraded",
|
|
251
|
+
"message": "Service operating in degraded mode",
|
|
252
|
+
"data": action.fallback_value
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return RecoveryResult(
|
|
256
|
+
success=True,
|
|
257
|
+
strategy_used=RecoveryStrategy.GRACEFUL_DEGRADE,
|
|
258
|
+
attempts_made=1,
|
|
259
|
+
duration_seconds=0.0,
|
|
260
|
+
result=degraded_result
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def get_recovery_stats(self) -> Dict[str, Dict[str, Any]]:
|
|
264
|
+
"""Get recovery statistics."""
|
|
265
|
+
stats = {}
|
|
266
|
+
|
|
267
|
+
for error_type, counts in self._recovery_stats.items():
|
|
268
|
+
total_attempts = counts['attempts']
|
|
269
|
+
success_rate = 0.0
|
|
270
|
+
|
|
271
|
+
if total_attempts > 0:
|
|
272
|
+
success_rate = (counts['successes'] / total_attempts) * 100.0
|
|
273
|
+
|
|
274
|
+
stats[error_type] = {
|
|
275
|
+
'total_attempts': total_attempts,
|
|
276
|
+
'successes': counts['successes'],
|
|
277
|
+
'failures': counts['failures'],
|
|
278
|
+
'success_rate_percent': round(success_rate, 2),
|
|
279
|
+
'recovery_action': self._recovery_actions.get(error_type, {})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return stats
|
|
283
|
+
|
|
284
|
+
def clear_stats(self) -> None:
|
|
285
|
+
"""Clear recovery statistics."""
|
|
286
|
+
for error_type in self._recovery_stats:
|
|
287
|
+
self._recovery_stats[error_type] = {
|
|
288
|
+
'attempts': 0,
|
|
289
|
+
'successes': 0,
|
|
290
|
+
'failures': 0
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
self.logger.info("Recovery statistics cleared")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Global auto recovery instance
|
|
297
|
+
_auto_recovery = AutoRecovery()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_auto_recovery() -> AutoRecovery:
|
|
301
|
+
"""Get global auto recovery instance."""
|
|
302
|
+
return _auto_recovery
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def recovery_handler(
|
|
306
|
+
error_type: str,
|
|
307
|
+
strategy: RecoveryStrategy = RecoveryStrategy.RETRY,
|
|
308
|
+
max_attempts: int = 3,
|
|
309
|
+
delay_seconds: float = 1.0,
|
|
310
|
+
fallback_value: Optional[Any] = None
|
|
311
|
+
):
|
|
312
|
+
"""
|
|
313
|
+
Decorator to add automatic recovery to functions.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
error_type: Exception class name to handle
|
|
317
|
+
strategy: Recovery strategy to use
|
|
318
|
+
max_attempts: Maximum recovery attempts
|
|
319
|
+
delay_seconds: Delay between attempts
|
|
320
|
+
fallback_value: Fallback value for fallback strategy
|
|
321
|
+
"""
|
|
322
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
323
|
+
# Register recovery action
|
|
324
|
+
action = RecoveryAction(
|
|
325
|
+
strategy=strategy,
|
|
326
|
+
max_attempts=max_attempts,
|
|
327
|
+
delay_seconds=delay_seconds,
|
|
328
|
+
fallback_value=fallback_value
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
_auto_recovery.register_recovery_action(error_type, action)
|
|
332
|
+
|
|
333
|
+
async def async_wrapper(*args, **kwargs):
|
|
334
|
+
try:
|
|
335
|
+
return await func(*args, **kwargs)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
from .error_context import create_error_context
|
|
338
|
+
|
|
339
|
+
# Create error context
|
|
340
|
+
error_context = create_error_context(
|
|
341
|
+
error=e,
|
|
342
|
+
operation=func.__name__,
|
|
343
|
+
component=func.__module__
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Attempt recovery
|
|
347
|
+
recovery_result = await _auto_recovery.attempt_recovery(
|
|
348
|
+
error_context, func, *args, **kwargs
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if recovery_result.success:
|
|
352
|
+
return recovery_result.result
|
|
353
|
+
else:
|
|
354
|
+
# Re-raise original exception if recovery failed
|
|
355
|
+
raise
|
|
356
|
+
|
|
357
|
+
def sync_wrapper(*args, **kwargs):
|
|
358
|
+
# For sync functions, convert to async temporarily
|
|
359
|
+
import asyncio
|
|
360
|
+
try:
|
|
361
|
+
loop = asyncio.get_event_loop()
|
|
362
|
+
return loop.run_until_complete(async_wrapper(*args, **kwargs))
|
|
363
|
+
except RuntimeError:
|
|
364
|
+
return asyncio.run(async_wrapper(*args, **kwargs))
|
|
365
|
+
|
|
366
|
+
if asyncio.iscoroutinefunction(func):
|
|
367
|
+
return async_wrapper
|
|
368
|
+
else:
|
|
369
|
+
return sync_wrapper
|
|
370
|
+
|
|
371
|
+
return decorator
|