unrealon 1.1.6__py3-none-any.whl → 2.0.5__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.5.dist-info}/LICENSE +1 -1
- unrealon-2.0.5.dist-info/METADATA +491 -0
- unrealon-2.0.5.dist-info/RECORD +128 -0
- {unrealon-1.1.6.dist-info → unrealon-2.0.5.dist-info}/WHEEL +2 -1
- unrealon-2.0.5.dist-info/entry_points.txt +3 -0
- unrealon-2.0.5.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 +172 -0
- unrealon_core/config/__init__.py +16 -0
- unrealon_core/config/environment.py +151 -0
- unrealon_core/config/urls.py +94 -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 +79 -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/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 +205 -0
- unrealon_driver/driver/core/__init__.py +10 -0
- unrealon_driver/driver/core/config.py +175 -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,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retry System
|
|
3
|
+
|
|
4
|
+
Advanced retry logic with multiple backoff strategies.
|
|
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
|
+
import random
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Any, Callable, Optional, Type, Union, List
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
19
|
+
|
|
20
|
+
from ..exceptions.base import UnrealOnError
|
|
21
|
+
from ..utils.time import utc_now
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RetryStrategy(str, Enum):
|
|
28
|
+
"""Retry strategy types."""
|
|
29
|
+
EXPONENTIAL = "exponential"
|
|
30
|
+
LINEAR = "linear"
|
|
31
|
+
FIXED = "fixed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RetryResult(BaseModel):
|
|
35
|
+
"""Result of retry operation."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
validate_assignment=True,
|
|
39
|
+
extra="forbid"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
success: bool = Field(description="Whether operation succeeded")
|
|
43
|
+
attempts: int = Field(description="Number of attempts made")
|
|
44
|
+
total_duration: float = Field(description="Total duration in seconds")
|
|
45
|
+
last_error: Optional[str] = Field(default=None, description="Last error message")
|
|
46
|
+
result: Optional[Any] = Field(default=None, description="Operation result if successful")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RetryConfig(BaseModel):
|
|
50
|
+
"""Configuration for retry behavior."""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(
|
|
53
|
+
validate_assignment=True,
|
|
54
|
+
extra="forbid"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
max_attempts: int = Field(default=3, ge=1, le=10, description="Maximum retry attempts")
|
|
58
|
+
strategy: RetryStrategy = Field(default=RetryStrategy.EXPONENTIAL, description="Backoff strategy")
|
|
59
|
+
base_delay: float = Field(default=1.0, ge=0.1, le=60.0, description="Base delay in seconds")
|
|
60
|
+
max_delay: float = Field(default=60.0, ge=1.0, le=300.0, description="Maximum delay in seconds")
|
|
61
|
+
jitter: bool = Field(default=True, description="Add random jitter to delays")
|
|
62
|
+
retryable_exceptions: List[str] = Field(
|
|
63
|
+
default_factory=lambda: ["ConnectionError", "TimeoutError", "HTTPError"],
|
|
64
|
+
description="Exception types that should trigger retry"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BackoffCalculator(ABC):
|
|
69
|
+
"""Abstract base for backoff calculation strategies."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: RetryConfig):
|
|
72
|
+
self.config = config
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
76
|
+
"""Calculate delay for given attempt number."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def add_jitter(self, delay: float) -> float:
|
|
80
|
+
"""Add random jitter to delay."""
|
|
81
|
+
if not self.config.jitter:
|
|
82
|
+
return delay
|
|
83
|
+
|
|
84
|
+
# Add ±25% jitter
|
|
85
|
+
jitter_range = delay * 0.25
|
|
86
|
+
jitter = random.uniform(-jitter_range, jitter_range)
|
|
87
|
+
return max(0.1, delay + jitter)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ExponentialBackoff(BackoffCalculator):
|
|
91
|
+
"""Exponential backoff strategy."""
|
|
92
|
+
|
|
93
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
94
|
+
"""Calculate exponential backoff delay."""
|
|
95
|
+
delay = self.config.base_delay * (2 ** (attempt - 1))
|
|
96
|
+
delay = min(delay, self.config.max_delay)
|
|
97
|
+
return self.add_jitter(delay)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LinearBackoff(BackoffCalculator):
|
|
101
|
+
"""Linear backoff strategy."""
|
|
102
|
+
|
|
103
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
104
|
+
"""Calculate linear backoff delay."""
|
|
105
|
+
delay = self.config.base_delay * attempt
|
|
106
|
+
delay = min(delay, self.config.max_delay)
|
|
107
|
+
return self.add_jitter(delay)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FixedBackoff(BackoffCalculator):
|
|
111
|
+
"""Fixed delay backoff strategy."""
|
|
112
|
+
|
|
113
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
114
|
+
"""Calculate fixed backoff delay."""
|
|
115
|
+
return self.add_jitter(self.config.base_delay)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_backoff_calculator(config: RetryConfig) -> BackoffCalculator:
|
|
119
|
+
"""Factory function to create backoff calculator."""
|
|
120
|
+
if config.strategy == RetryStrategy.EXPONENTIAL:
|
|
121
|
+
return ExponentialBackoff(config)
|
|
122
|
+
elif config.strategy == RetryStrategy.LINEAR:
|
|
123
|
+
return LinearBackoff(config)
|
|
124
|
+
else:
|
|
125
|
+
return FixedBackoff(config)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_retryable_exception(
|
|
129
|
+
exception: Exception,
|
|
130
|
+
retryable_types: List[str]
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Check if exception is retryable."""
|
|
133
|
+
exception_name = exception.__class__.__name__
|
|
134
|
+
return exception_name in retryable_types
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def retry_async(
|
|
138
|
+
func: Callable[..., Any],
|
|
139
|
+
config: RetryConfig,
|
|
140
|
+
*args,
|
|
141
|
+
**kwargs
|
|
142
|
+
) -> RetryResult:
|
|
143
|
+
"""
|
|
144
|
+
Execute async function with retry logic.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
func: Async function to execute
|
|
148
|
+
config: Retry configuration
|
|
149
|
+
*args, **kwargs: Function arguments
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
RetryResult with operation outcome
|
|
153
|
+
"""
|
|
154
|
+
start_time = utc_now()
|
|
155
|
+
backoff = create_backoff_calculator(config)
|
|
156
|
+
last_error = None
|
|
157
|
+
|
|
158
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
159
|
+
try:
|
|
160
|
+
logger.debug(f"Attempt {attempt}/{config.max_attempts} for {func.__name__}")
|
|
161
|
+
|
|
162
|
+
result = await func(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
165
|
+
|
|
166
|
+
if attempt > 1:
|
|
167
|
+
logger.info(f"{func.__name__} succeeded on attempt {attempt}")
|
|
168
|
+
|
|
169
|
+
return RetryResult(
|
|
170
|
+
success=True,
|
|
171
|
+
attempts=attempt,
|
|
172
|
+
total_duration=duration,
|
|
173
|
+
result=result
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
last_error = str(e)
|
|
178
|
+
|
|
179
|
+
if not is_retryable_exception(e, config.retryable_exceptions):
|
|
180
|
+
logger.error(f"{func.__name__} failed with non-retryable error: {e}")
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
if attempt < config.max_attempts:
|
|
184
|
+
delay = backoff.calculate_delay(attempt)
|
|
185
|
+
logger.warning(
|
|
186
|
+
f"{func.__name__} attempt {attempt} failed: {e}. "
|
|
187
|
+
f"Retrying in {delay:.2f}s..."
|
|
188
|
+
)
|
|
189
|
+
await asyncio.sleep(delay)
|
|
190
|
+
else:
|
|
191
|
+
logger.error(f"{func.__name__} failed after {attempt} attempts: {e}")
|
|
192
|
+
|
|
193
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
194
|
+
|
|
195
|
+
return RetryResult(
|
|
196
|
+
success=False,
|
|
197
|
+
attempts=config.max_attempts,
|
|
198
|
+
total_duration=duration,
|
|
199
|
+
last_error=last_error
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def retry_sync(
|
|
204
|
+
func: Callable[..., Any],
|
|
205
|
+
config: RetryConfig,
|
|
206
|
+
*args,
|
|
207
|
+
**kwargs
|
|
208
|
+
) -> RetryResult:
|
|
209
|
+
"""
|
|
210
|
+
Execute sync function with retry logic.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
func: Sync function to execute
|
|
214
|
+
config: Retry configuration
|
|
215
|
+
*args, **kwargs: Function arguments
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
RetryResult with operation outcome
|
|
219
|
+
"""
|
|
220
|
+
import time
|
|
221
|
+
|
|
222
|
+
start_time = utc_now()
|
|
223
|
+
backoff = create_backoff_calculator(config)
|
|
224
|
+
last_error = None
|
|
225
|
+
|
|
226
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
227
|
+
try:
|
|
228
|
+
logger.debug(f"Attempt {attempt}/{config.max_attempts} for {func.__name__}")
|
|
229
|
+
|
|
230
|
+
result = func(*args, **kwargs)
|
|
231
|
+
|
|
232
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
233
|
+
|
|
234
|
+
if attempt > 1:
|
|
235
|
+
logger.info(f"{func.__name__} succeeded on attempt {attempt}")
|
|
236
|
+
|
|
237
|
+
return RetryResult(
|
|
238
|
+
success=True,
|
|
239
|
+
attempts=attempt,
|
|
240
|
+
total_duration=duration,
|
|
241
|
+
result=result
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
last_error = str(e)
|
|
246
|
+
|
|
247
|
+
if not is_retryable_exception(e, config.retryable_exceptions):
|
|
248
|
+
logger.error(f"{func.__name__} failed with non-retryable error: {e}")
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
if attempt < config.max_attempts:
|
|
252
|
+
delay = backoff.calculate_delay(attempt)
|
|
253
|
+
logger.warning(
|
|
254
|
+
f"{func.__name__} attempt {attempt} failed: {e}. "
|
|
255
|
+
f"Retrying in {delay:.2f}s..."
|
|
256
|
+
)
|
|
257
|
+
time.sleep(delay)
|
|
258
|
+
else:
|
|
259
|
+
logger.error(f"{func.__name__} failed after {attempt} attempts: {e}")
|
|
260
|
+
|
|
261
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
262
|
+
|
|
263
|
+
return RetryResult(
|
|
264
|
+
success=False,
|
|
265
|
+
attempts=config.max_attempts,
|
|
266
|
+
total_duration=duration,
|
|
267
|
+
last_error=last_error
|
|
268
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UnrealOn Core Exceptions
|
|
3
|
+
|
|
4
|
+
Custom exception hierarchy for the UnrealOn system.
|
|
5
|
+
Provides specific exceptions for different error conditions
|
|
6
|
+
with proper error codes and context information.
|
|
7
|
+
|
|
8
|
+
Phase 1: Foundation exceptions with proper hierarchy
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .base import UnrealOnError, UnrealOnWarning
|
|
12
|
+
from .validation import ValidationError, ConfigurationError
|
|
13
|
+
from .communication import CommunicationError, WebSocketError, RPCError
|
|
14
|
+
from .driver import DriverError, DriverNotFoundError, DriverTimeoutError
|
|
15
|
+
from .task import TaskError, TaskTimeoutError, TaskValidationError
|
|
16
|
+
from .proxy import ProxyError, ProxyNotAvailableError, ProxyTimeoutError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Base exceptions
|
|
20
|
+
"UnrealOnError",
|
|
21
|
+
"UnrealOnWarning",
|
|
22
|
+
|
|
23
|
+
# Validation exceptions
|
|
24
|
+
"ValidationError",
|
|
25
|
+
"ConfigurationError",
|
|
26
|
+
|
|
27
|
+
# Communication exceptions
|
|
28
|
+
"CommunicationError",
|
|
29
|
+
"WebSocketError",
|
|
30
|
+
"RPCError",
|
|
31
|
+
|
|
32
|
+
# Driver exceptions
|
|
33
|
+
"DriverError",
|
|
34
|
+
"DriverNotFoundError",
|
|
35
|
+
"DriverTimeoutError",
|
|
36
|
+
|
|
37
|
+
# Task exceptions
|
|
38
|
+
"TaskError",
|
|
39
|
+
"TaskTimeoutError",
|
|
40
|
+
"TaskValidationError",
|
|
41
|
+
|
|
42
|
+
# Proxy exceptions
|
|
43
|
+
"ProxyError",
|
|
44
|
+
"ProxyNotAvailableError",
|
|
45
|
+
"ProxyTimeoutError",
|
|
46
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base exceptions for UnrealOn system.
|
|
3
|
+
|
|
4
|
+
Provides the foundation exception classes that all other
|
|
5
|
+
UnrealOn exceptions inherit from. Includes error codes,
|
|
6
|
+
context information, and structured error handling.
|
|
7
|
+
|
|
8
|
+
Phase 1: Foundation exception hierarchy
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Dict, Any, Union
|
|
12
|
+
from unrealon_core.utils.time import utc_now
|
|
13
|
+
|
|
14
|
+
class UnrealOnError(Exception):
|
|
15
|
+
"""
|
|
16
|
+
Base exception for all UnrealOn errors.
|
|
17
|
+
|
|
18
|
+
Provides:
|
|
19
|
+
- Error codes for programmatic handling
|
|
20
|
+
- Context information for debugging
|
|
21
|
+
- Structured error data
|
|
22
|
+
- Timestamp tracking
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
error_code: Optional[str] = None,
|
|
29
|
+
context: Optional[Dict[str, Any]] = None,
|
|
30
|
+
cause: Optional[Exception] = None
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize UnrealOn error.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
message: Human-readable error message
|
|
37
|
+
error_code: Machine-readable error code
|
|
38
|
+
context: Additional context information
|
|
39
|
+
cause: Original exception that caused this error
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.message = message
|
|
43
|
+
self.error_code = error_code or self._get_default_error_code()
|
|
44
|
+
self.context = context or {}
|
|
45
|
+
self.cause = cause
|
|
46
|
+
self.timestamp = utc_now()
|
|
47
|
+
|
|
48
|
+
# Set the cause for proper exception chaining
|
|
49
|
+
if cause:
|
|
50
|
+
self.__cause__ = cause
|
|
51
|
+
|
|
52
|
+
def _get_default_error_code(self) -> str:
|
|
53
|
+
"""Get default error code based on exception class name."""
|
|
54
|
+
class_name = self.__class__.__name__
|
|
55
|
+
# Convert CamelCase to UPPER_SNAKE_CASE
|
|
56
|
+
import re
|
|
57
|
+
error_code = re.sub('([a-z0-9])([A-Z])', r'\1_\2', class_name).upper()
|
|
58
|
+
return error_code
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
"""Convert exception to dictionary for serialization."""
|
|
62
|
+
return {
|
|
63
|
+
'error_type': self.__class__.__name__,
|
|
64
|
+
'error_code': self.error_code,
|
|
65
|
+
'message': self.message,
|
|
66
|
+
'context': self.context,
|
|
67
|
+
'timestamp': self.timestamp.isoformat(),
|
|
68
|
+
'cause': str(self.cause) if self.cause else None
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def add_context(self, key: str, value: Any) -> 'UnrealOnError':
|
|
72
|
+
"""Add context information to the error."""
|
|
73
|
+
self.context[key] = value
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def with_context(self, **context) -> 'UnrealOnError':
|
|
77
|
+
"""Add multiple context items to the error."""
|
|
78
|
+
self.context.update(context)
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
"""String representation of the error."""
|
|
83
|
+
parts = [f"[{self.error_code}] {self.message}"]
|
|
84
|
+
|
|
85
|
+
if self.context:
|
|
86
|
+
context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
|
|
87
|
+
parts.append(f"Context: {context_str}")
|
|
88
|
+
|
|
89
|
+
if self.cause:
|
|
90
|
+
parts.append(f"Caused by: {self.cause}")
|
|
91
|
+
|
|
92
|
+
return " | ".join(parts)
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
"""Detailed representation of the error."""
|
|
96
|
+
return (
|
|
97
|
+
f"{self.__class__.__name__}("
|
|
98
|
+
f"message='{self.message}', "
|
|
99
|
+
f"error_code='{self.error_code}', "
|
|
100
|
+
f"context={self.context}, "
|
|
101
|
+
f"cause={self.cause!r})"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class UnrealOnWarning(UserWarning):
|
|
106
|
+
"""
|
|
107
|
+
Base warning for UnrealOn system.
|
|
108
|
+
|
|
109
|
+
Used for non-fatal issues that should be logged
|
|
110
|
+
but don't prevent operation from continuing.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
message: str,
|
|
116
|
+
warning_code: Optional[str] = None,
|
|
117
|
+
context: Optional[Dict[str, Any]] = None
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Initialize UnrealOn warning.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
message: Human-readable warning message
|
|
124
|
+
warning_code: Machine-readable warning code
|
|
125
|
+
context: Additional context information
|
|
126
|
+
"""
|
|
127
|
+
super().__init__(message)
|
|
128
|
+
self.message = message
|
|
129
|
+
self.warning_code = warning_code or self._get_default_warning_code()
|
|
130
|
+
self.context = context or {}
|
|
131
|
+
self.timestamp = utc_now()
|
|
132
|
+
|
|
133
|
+
def _get_default_warning_code(self) -> str:
|
|
134
|
+
"""Get default warning code based on class name."""
|
|
135
|
+
class_name = self.__class__.__name__
|
|
136
|
+
import re
|
|
137
|
+
warning_code = re.sub('([a-z0-9])([A-Z])', r'\1_\2', class_name).upper()
|
|
138
|
+
return warning_code
|
|
139
|
+
|
|
140
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
141
|
+
"""Convert warning to dictionary for serialization."""
|
|
142
|
+
return {
|
|
143
|
+
'warning_type': self.__class__.__name__,
|
|
144
|
+
'warning_code': self.warning_code,
|
|
145
|
+
'message': self.message,
|
|
146
|
+
'context': self.context,
|
|
147
|
+
'timestamp': self.timestamp.isoformat()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class UnrealOnTimeoutError(UnrealOnError):
|
|
152
|
+
"""
|
|
153
|
+
Base timeout error for operations that exceed time limits.
|
|
154
|
+
|
|
155
|
+
Used when operations take longer than expected or configured
|
|
156
|
+
timeout values are exceeded.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
message: str,
|
|
162
|
+
timeout_seconds: Optional[float] = None,
|
|
163
|
+
operation: Optional[str] = None,
|
|
164
|
+
**kwargs
|
|
165
|
+
):
|
|
166
|
+
"""
|
|
167
|
+
Initialize timeout error.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
message: Error message
|
|
171
|
+
timeout_seconds: The timeout value that was exceeded
|
|
172
|
+
operation: Name of the operation that timed out
|
|
173
|
+
**kwargs: Additional arguments for base class
|
|
174
|
+
"""
|
|
175
|
+
super().__init__(message, **kwargs)
|
|
176
|
+
|
|
177
|
+
if timeout_seconds is not None:
|
|
178
|
+
self.add_context('timeout_seconds', timeout_seconds)
|
|
179
|
+
|
|
180
|
+
if operation:
|
|
181
|
+
self.add_context('operation', operation)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class UnrealOnConfigurationError(UnrealOnError):
|
|
185
|
+
"""
|
|
186
|
+
Configuration-related errors.
|
|
187
|
+
|
|
188
|
+
Used when there are issues with system configuration,
|
|
189
|
+
invalid settings, or missing required configuration.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
message: str,
|
|
195
|
+
config_key: Optional[str] = None,
|
|
196
|
+
config_value: Optional[Any] = None,
|
|
197
|
+
**kwargs
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Initialize configuration error.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
message: Error message
|
|
204
|
+
config_key: The configuration key that caused the error
|
|
205
|
+
config_value: The invalid configuration value
|
|
206
|
+
**kwargs: Additional arguments for base class
|
|
207
|
+
"""
|
|
208
|
+
super().__init__(message, **kwargs)
|
|
209
|
+
|
|
210
|
+
if config_key:
|
|
211
|
+
self.add_context('config_key', config_key)
|
|
212
|
+
|
|
213
|
+
if config_value is not None:
|
|
214
|
+
self.add_context('config_value', str(config_value))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class UnrealOnRetryableError(UnrealOnError):
|
|
218
|
+
"""
|
|
219
|
+
Base class for errors that can be retried.
|
|
220
|
+
|
|
221
|
+
Used for temporary failures that might succeed
|
|
222
|
+
if the operation is attempted again.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
message: str,
|
|
228
|
+
retry_after: Optional[float] = None,
|
|
229
|
+
max_retries: Optional[int] = None,
|
|
230
|
+
**kwargs
|
|
231
|
+
):
|
|
232
|
+
"""
|
|
233
|
+
Initialize retryable error.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
message: Error message
|
|
237
|
+
retry_after: Suggested delay before retry (seconds)
|
|
238
|
+
max_retries: Maximum number of retries recommended
|
|
239
|
+
**kwargs: Additional arguments for base class
|
|
240
|
+
"""
|
|
241
|
+
super().__init__(message, **kwargs)
|
|
242
|
+
|
|
243
|
+
if retry_after is not None:
|
|
244
|
+
self.add_context('retry_after', retry_after)
|
|
245
|
+
|
|
246
|
+
if max_retries is not None:
|
|
247
|
+
self.add_context('max_retries', max_retries)
|
|
248
|
+
|
|
249
|
+
def should_retry(self, attempt_count: int) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Check if the operation should be retried.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
attempt_count: Number of attempts already made
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
True if retry should be attempted
|
|
258
|
+
"""
|
|
259
|
+
max_retries = self.context.get('max_retries')
|
|
260
|
+
if max_retries is None:
|
|
261
|
+
return True # No limit specified
|
|
262
|
+
|
|
263
|
+
return attempt_count < max_retries
|
|
264
|
+
|
|
265
|
+
def get_retry_delay(self, attempt_count: int) -> float:
|
|
266
|
+
"""
|
|
267
|
+
Get suggested delay before retry.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
attempt_count: Number of attempts already made
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Delay in seconds
|
|
274
|
+
"""
|
|
275
|
+
base_delay = self.context.get('retry_after', 1.0)
|
|
276
|
+
|
|
277
|
+
# Exponential backoff: delay = base_delay * (2 ^ attempt_count)
|
|
278
|
+
return base_delay * (2 ** min(attempt_count, 6)) # Cap at 2^6 = 64x
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class UnrealOnFatalError(UnrealOnError):
|
|
282
|
+
"""
|
|
283
|
+
Fatal errors that should stop system operation.
|
|
284
|
+
|
|
285
|
+
Used for critical errors that indicate the system
|
|
286
|
+
cannot continue operating safely.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
def __init__(self, message: str, **kwargs):
|
|
290
|
+
"""Initialize fatal error."""
|
|
291
|
+
super().__init__(message, **kwargs)
|
|
292
|
+
self.add_context('fatal', True)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Communication exceptions for UnrealOn system.
|
|
3
|
+
|
|
4
|
+
Phase 1: Basic communication exceptions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import UnrealOnError, UnrealOnTimeoutError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommunicationError(UnrealOnError):
|
|
11
|
+
"""Base communication error."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WebSocketError(CommunicationError):
|
|
16
|
+
"""WebSocket communication error."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RPCError(CommunicationError):
|
|
21
|
+
"""RPC communication error."""
|
|
22
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Task exceptions - Phase 1 stubs."""
|
|
2
|
+
from .base import UnrealOnError, UnrealOnTimeoutError
|
|
3
|
+
from .validation import ValidationError
|
|
4
|
+
|
|
5
|
+
class TaskError(UnrealOnError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
class TaskTimeoutError(UnrealOnTimeoutError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class TaskValidationError(ValidationError):
|
|
12
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation exceptions for UnrealOn system.
|
|
3
|
+
|
|
4
|
+
Phase 1: Basic validation exceptions
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import UnrealOnError, UnrealOnConfigurationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationError(UnrealOnError):
|
|
11
|
+
"""Validation error for invalid data."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigurationError(UnrealOnConfigurationError):
|
|
16
|
+
"""Configuration validation error."""
|
|
17
|
+
pass
|