unrealon 1.1.5__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.5.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.5.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 -2
- unrealon_browser/cli/browser_cli.py +18 -9
- unrealon_browser/cli/interactive_mode.py +18 -7
- unrealon_browser/core/browser_manager.py +76 -13
- unrealon_browser/dto/__init__.py +21 -0
- unrealon_browser/dto/bot_detection.py +175 -0
- unrealon_browser/dto/models/config.py +14 -1
- unrealon_browser/managers/__init__.py +4 -1
- unrealon_browser/managers/logger_bridge.py +3 -6
- unrealon_browser/managers/page_wait_manager.py +198 -0
- 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 +88 -50
- 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/__init__.py +0 -40
- unrealon-1.1.5.dist-info/METADATA +0 -621
- unrealon-1.1.5.dist-info/RECORD +0 -54
- unrealon-1.1.5.dist-info/entry_points.txt +0 -9
- unrealon_browser/managers/stealth.py +0 -388
- 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,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit Breaker Pattern
|
|
3
|
+
|
|
4
|
+
Prevents cascading failures by temporarily disabling failing services.
|
|
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, Optional, Dict
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
17
|
+
|
|
18
|
+
from ..exceptions.base import UnrealOnError
|
|
19
|
+
from ..utils.time import utc_now
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CircuitBreakerState(str, Enum):
|
|
25
|
+
"""Circuit breaker states."""
|
|
26
|
+
CLOSED = "closed" # Normal operation
|
|
27
|
+
OPEN = "open" # Failing, requests blocked
|
|
28
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CircuitBreakerError(UnrealOnError):
|
|
32
|
+
"""Circuit breaker is open."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CircuitBreakerConfig(BaseModel):
|
|
37
|
+
"""Configuration for circuit breaker."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(
|
|
40
|
+
validate_assignment=True,
|
|
41
|
+
extra="forbid"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
failure_threshold: int = Field(
|
|
45
|
+
default=5,
|
|
46
|
+
ge=1,
|
|
47
|
+
le=20,
|
|
48
|
+
description="Failures needed to open circuit"
|
|
49
|
+
)
|
|
50
|
+
recovery_timeout: float = Field(
|
|
51
|
+
default=60.0,
|
|
52
|
+
ge=1.0,
|
|
53
|
+
le=300.0,
|
|
54
|
+
description="Seconds before trying half-open"
|
|
55
|
+
)
|
|
56
|
+
success_threshold: int = Field(
|
|
57
|
+
default=3,
|
|
58
|
+
ge=1,
|
|
59
|
+
le=10,
|
|
60
|
+
description="Successes needed to close circuit"
|
|
61
|
+
)
|
|
62
|
+
timeout: float = Field(
|
|
63
|
+
default=30.0,
|
|
64
|
+
ge=1.0,
|
|
65
|
+
le=120.0,
|
|
66
|
+
description="Operation timeout in seconds"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CircuitBreakerStats(BaseModel):
|
|
71
|
+
"""Circuit breaker statistics."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(
|
|
74
|
+
validate_assignment=True,
|
|
75
|
+
extra="forbid"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
total_requests: int = Field(default=0, description="Total requests processed")
|
|
79
|
+
successful_requests: int = Field(default=0, description="Successful requests")
|
|
80
|
+
failed_requests: int = Field(default=0, description="Failed requests")
|
|
81
|
+
rejected_requests: int = Field(default=0, description="Requests rejected by circuit")
|
|
82
|
+
last_failure_time: Optional[datetime] = Field(default=None, description="Last failure timestamp")
|
|
83
|
+
last_success_time: Optional[datetime] = Field(default=None, description="Last success timestamp")
|
|
84
|
+
|
|
85
|
+
def get_failure_rate(self) -> float:
|
|
86
|
+
"""Calculate failure rate percentage."""
|
|
87
|
+
if self.total_requests == 0:
|
|
88
|
+
return 0.0
|
|
89
|
+
return (self.failed_requests / self.total_requests) * 100.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CircuitBreaker:
|
|
93
|
+
"""
|
|
94
|
+
Circuit breaker implementation.
|
|
95
|
+
|
|
96
|
+
Prevents cascading failures by monitoring service health
|
|
97
|
+
and temporarily blocking requests to failing services.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, name: str, config: CircuitBreakerConfig):
|
|
101
|
+
"""Initialize circuit breaker."""
|
|
102
|
+
self.name = name
|
|
103
|
+
self.config = config
|
|
104
|
+
self.state = CircuitBreakerState.CLOSED
|
|
105
|
+
self.stats = CircuitBreakerStats()
|
|
106
|
+
|
|
107
|
+
# State tracking
|
|
108
|
+
self._consecutive_failures = 0
|
|
109
|
+
self._consecutive_successes = 0
|
|
110
|
+
self._last_failure_time: Optional[datetime] = None
|
|
111
|
+
self._lock = asyncio.Lock()
|
|
112
|
+
|
|
113
|
+
self.logger = logging.getLogger(f"circuit_breaker.{name}")
|
|
114
|
+
|
|
115
|
+
async def call(self, func: Callable[..., Any], *args, **kwargs) -> Any:
|
|
116
|
+
"""
|
|
117
|
+
Execute function through circuit breaker.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
func: Function to execute
|
|
121
|
+
*args, **kwargs: Function arguments
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Function result
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
CircuitBreakerError: If circuit is open
|
|
128
|
+
"""
|
|
129
|
+
async with self._lock:
|
|
130
|
+
await self._update_state()
|
|
131
|
+
|
|
132
|
+
if self.state == CircuitBreakerState.OPEN:
|
|
133
|
+
self.stats.rejected_requests += 1
|
|
134
|
+
self.logger.warning(f"Circuit breaker {self.name} is OPEN - rejecting request")
|
|
135
|
+
raise CircuitBreakerError(f"Circuit breaker {self.name} is open")
|
|
136
|
+
|
|
137
|
+
self.stats.total_requests += 1
|
|
138
|
+
|
|
139
|
+
# Execute function with timeout
|
|
140
|
+
try:
|
|
141
|
+
result = await asyncio.wait_for(
|
|
142
|
+
func(*args, **kwargs),
|
|
143
|
+
timeout=self.config.timeout
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await self._record_success()
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
await self._record_failure()
|
|
151
|
+
raise
|
|
152
|
+
|
|
153
|
+
async def _update_state(self) -> None:
|
|
154
|
+
"""Update circuit breaker state based on current conditions."""
|
|
155
|
+
now = utc_now()
|
|
156
|
+
|
|
157
|
+
if self.state == CircuitBreakerState.OPEN:
|
|
158
|
+
# Check if we should transition to half-open
|
|
159
|
+
if (self._last_failure_time and
|
|
160
|
+
(now - self._last_failure_time).total_seconds() >= self.config.recovery_timeout):
|
|
161
|
+
|
|
162
|
+
self.state = CircuitBreakerState.HALF_OPEN
|
|
163
|
+
self._consecutive_successes = 0
|
|
164
|
+
self.logger.info(f"Circuit breaker {self.name} transitioning to HALF_OPEN")
|
|
165
|
+
|
|
166
|
+
elif self.state == CircuitBreakerState.HALF_OPEN:
|
|
167
|
+
# Check if we should close the circuit
|
|
168
|
+
if self._consecutive_successes >= self.config.success_threshold:
|
|
169
|
+
self.state = CircuitBreakerState.CLOSED
|
|
170
|
+
self._consecutive_failures = 0
|
|
171
|
+
self.logger.info(f"Circuit breaker {self.name} transitioning to CLOSED")
|
|
172
|
+
|
|
173
|
+
async def _record_success(self) -> None:
|
|
174
|
+
"""Record successful operation."""
|
|
175
|
+
async with self._lock:
|
|
176
|
+
self.stats.successful_requests += 1
|
|
177
|
+
self.stats.last_success_time = utc_now()
|
|
178
|
+
|
|
179
|
+
if self.state == CircuitBreakerState.HALF_OPEN:
|
|
180
|
+
self._consecutive_successes += 1
|
|
181
|
+
self.logger.debug(
|
|
182
|
+
f"Circuit breaker {self.name} success "
|
|
183
|
+
f"({self._consecutive_successes}/{self.config.success_threshold})"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Reset failure counter on success
|
|
187
|
+
self._consecutive_failures = 0
|
|
188
|
+
|
|
189
|
+
async def _record_failure(self) -> None:
|
|
190
|
+
"""Record failed operation."""
|
|
191
|
+
async with self._lock:
|
|
192
|
+
self.stats.failed_requests += 1
|
|
193
|
+
self.stats.last_failure_time = utc_now()
|
|
194
|
+
self._last_failure_time = self.stats.last_failure_time
|
|
195
|
+
|
|
196
|
+
self._consecutive_failures += 1
|
|
197
|
+
self._consecutive_successes = 0
|
|
198
|
+
|
|
199
|
+
self.logger.debug(
|
|
200
|
+
f"Circuit breaker {self.name} failure "
|
|
201
|
+
f"({self._consecutive_failures}/{self.config.failure_threshold})"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Check if we should open the circuit
|
|
205
|
+
if (self.state == CircuitBreakerState.CLOSED and
|
|
206
|
+
self._consecutive_failures >= self.config.failure_threshold):
|
|
207
|
+
|
|
208
|
+
self.state = CircuitBreakerState.OPEN
|
|
209
|
+
self.logger.warning(f"Circuit breaker {self.name} transitioning to OPEN")
|
|
210
|
+
|
|
211
|
+
elif self.state == CircuitBreakerState.HALF_OPEN:
|
|
212
|
+
# Any failure in half-open state reopens the circuit
|
|
213
|
+
self.state = CircuitBreakerState.OPEN
|
|
214
|
+
self.logger.warning(f"Circuit breaker {self.name} reopening due to failure in HALF_OPEN")
|
|
215
|
+
|
|
216
|
+
def get_status(self) -> Dict[str, Any]:
|
|
217
|
+
"""Get circuit breaker status."""
|
|
218
|
+
return {
|
|
219
|
+
'name': self.name,
|
|
220
|
+
'state': self.state.value,
|
|
221
|
+
'consecutive_failures': self._consecutive_failures,
|
|
222
|
+
'consecutive_successes': self._consecutive_successes,
|
|
223
|
+
'config': self.config.model_dump(),
|
|
224
|
+
'stats': self.stats.model_dump()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
def reset(self) -> None:
|
|
228
|
+
"""Reset circuit breaker to closed state."""
|
|
229
|
+
self.state = CircuitBreakerState.CLOSED
|
|
230
|
+
self._consecutive_failures = 0
|
|
231
|
+
self._consecutive_successes = 0
|
|
232
|
+
self._last_failure_time = None
|
|
233
|
+
self.logger.info(f"Circuit breaker {self.name} manually reset to CLOSED")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Global circuit breaker registry
|
|
237
|
+
_circuit_breakers: Dict[str, CircuitBreaker] = {}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_circuit_breaker(
|
|
241
|
+
name: str,
|
|
242
|
+
config: Optional[CircuitBreakerConfig] = None
|
|
243
|
+
) -> CircuitBreaker:
|
|
244
|
+
"""Get or create circuit breaker by name."""
|
|
245
|
+
if name not in _circuit_breakers:
|
|
246
|
+
if config is None:
|
|
247
|
+
config = CircuitBreakerConfig()
|
|
248
|
+
_circuit_breakers[name] = CircuitBreaker(name, config)
|
|
249
|
+
|
|
250
|
+
return _circuit_breakers[name]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def circuit_breaker(
|
|
254
|
+
name: str,
|
|
255
|
+
config: Optional[CircuitBreakerConfig] = None
|
|
256
|
+
):
|
|
257
|
+
"""Decorator to wrap function with circuit breaker."""
|
|
258
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
259
|
+
cb = get_circuit_breaker(name, config)
|
|
260
|
+
|
|
261
|
+
async def async_wrapper(*args, **kwargs):
|
|
262
|
+
return await cb.call(func, *args, **kwargs)
|
|
263
|
+
|
|
264
|
+
def sync_wrapper(*args, **kwargs):
|
|
265
|
+
# For sync functions, we need to handle differently
|
|
266
|
+
# This is a simplified version - in production you might want
|
|
267
|
+
# to use threading or a different approach
|
|
268
|
+
import asyncio
|
|
269
|
+
try:
|
|
270
|
+
loop = asyncio.get_event_loop()
|
|
271
|
+
return loop.run_until_complete(cb.call(func, *args, **kwargs))
|
|
272
|
+
except RuntimeError:
|
|
273
|
+
# No event loop running
|
|
274
|
+
return asyncio.run(cb.call(func, *args, **kwargs))
|
|
275
|
+
|
|
276
|
+
if asyncio.iscoroutinefunction(func):
|
|
277
|
+
return async_wrapper
|
|
278
|
+
else:
|
|
279
|
+
return sync_wrapper
|
|
280
|
+
|
|
281
|
+
return decorator
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_all_circuit_breakers() -> Dict[str, Dict[str, Any]]:
|
|
285
|
+
"""Get status of all circuit breakers."""
|
|
286
|
+
return {name: cb.get_status() for name, cb in _circuit_breakers.items()}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def reset_all_circuit_breakers() -> None:
|
|
290
|
+
"""Reset all circuit breakers."""
|
|
291
|
+
for cb in _circuit_breakers.values():
|
|
292
|
+
cb.reset()
|
|
@@ -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
|