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.
Files changed (146) hide show
  1. {unrealon-1.1.5.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
  2. unrealon-2.0.4.dist-info/METADATA +491 -0
  3. unrealon-2.0.4.dist-info/RECORD +129 -0
  4. {unrealon-1.1.5.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
  5. unrealon-2.0.4.dist-info/entry_points.txt +3 -0
  6. unrealon-2.0.4.dist-info/top_level.txt +3 -0
  7. unrealon_browser/__init__.py +5 -2
  8. unrealon_browser/cli/browser_cli.py +18 -9
  9. unrealon_browser/cli/interactive_mode.py +18 -7
  10. unrealon_browser/core/browser_manager.py +76 -13
  11. unrealon_browser/dto/__init__.py +21 -0
  12. unrealon_browser/dto/bot_detection.py +175 -0
  13. unrealon_browser/dto/models/config.py +14 -1
  14. unrealon_browser/managers/__init__.py +4 -1
  15. unrealon_browser/managers/logger_bridge.py +3 -6
  16. unrealon_browser/managers/page_wait_manager.py +198 -0
  17. unrealon_browser/stealth/__init__.py +27 -0
  18. unrealon_browser/stealth/bypass_techniques.pyc +0 -0
  19. unrealon_browser/stealth/manager.pyc +0 -0
  20. unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
  21. unrealon_browser/stealth/playwright_stealth.pyc +0 -0
  22. unrealon_browser/stealth/scanner_tester.pyc +0 -0
  23. unrealon_browser/stealth/undetected_chrome.pyc +0 -0
  24. unrealon_core/__init__.py +160 -0
  25. unrealon_core/config/__init__.py +16 -0
  26. unrealon_core/config/environment.py +98 -0
  27. unrealon_core/config/urls.py +93 -0
  28. unrealon_core/enums/__init__.py +24 -0
  29. unrealon_core/enums/status.py +216 -0
  30. unrealon_core/enums/types.py +240 -0
  31. unrealon_core/error_handling/__init__.py +45 -0
  32. unrealon_core/error_handling/circuit_breaker.py +292 -0
  33. unrealon_core/error_handling/error_context.py +324 -0
  34. unrealon_core/error_handling/recovery.py +371 -0
  35. unrealon_core/error_handling/retry.py +268 -0
  36. unrealon_core/exceptions/__init__.py +46 -0
  37. unrealon_core/exceptions/base.py +292 -0
  38. unrealon_core/exceptions/communication.py +22 -0
  39. unrealon_core/exceptions/driver.py +11 -0
  40. unrealon_core/exceptions/proxy.py +11 -0
  41. unrealon_core/exceptions/task.py +12 -0
  42. unrealon_core/exceptions/validation.py +17 -0
  43. unrealon_core/models/__init__.py +98 -0
  44. unrealon_core/models/arq_context.py +252 -0
  45. unrealon_core/models/arq_responses.py +125 -0
  46. unrealon_core/models/base.py +291 -0
  47. unrealon_core/models/bridge_stats.py +58 -0
  48. unrealon_core/models/communication.py +39 -0
  49. unrealon_core/models/config.py +47 -0
  50. unrealon_core/models/connection_stats.py +47 -0
  51. unrealon_core/models/driver.py +30 -0
  52. unrealon_core/models/driver_details.py +98 -0
  53. unrealon_core/models/logging.py +28 -0
  54. unrealon_core/models/task.py +21 -0
  55. unrealon_core/models/typed_responses.py +210 -0
  56. unrealon_core/models/websocket/__init__.py +91 -0
  57. unrealon_core/models/websocket/base.py +49 -0
  58. unrealon_core/models/websocket/config.py +200 -0
  59. unrealon_core/models/websocket/driver.py +215 -0
  60. unrealon_core/models/websocket/errors.py +138 -0
  61. unrealon_core/models/websocket/heartbeat.py +100 -0
  62. unrealon_core/models/websocket/logging.py +261 -0
  63. unrealon_core/models/websocket/proxy.py +496 -0
  64. unrealon_core/models/websocket/tasks.py +275 -0
  65. unrealon_core/models/websocket/utils.py +153 -0
  66. unrealon_core/models/websocket_session.py +144 -0
  67. unrealon_core/monitoring/__init__.py +43 -0
  68. unrealon_core/monitoring/alerts.py +398 -0
  69. unrealon_core/monitoring/dashboard.py +307 -0
  70. unrealon_core/monitoring/health_check.py +354 -0
  71. unrealon_core/monitoring/metrics.py +352 -0
  72. unrealon_core/utils/__init__.py +11 -0
  73. unrealon_core/utils/time.py +61 -0
  74. unrealon_core/version.py +219 -0
  75. unrealon_driver/__init__.py +88 -50
  76. unrealon_driver/core_module/__init__.py +34 -0
  77. unrealon_driver/core_module/base.py +184 -0
  78. unrealon_driver/core_module/config.py +30 -0
  79. unrealon_driver/core_module/event_manager.py +127 -0
  80. unrealon_driver/core_module/protocols.py +98 -0
  81. unrealon_driver/core_module/registry.py +146 -0
  82. unrealon_driver/decorators/__init__.py +15 -0
  83. unrealon_driver/decorators/retry.py +117 -0
  84. unrealon_driver/decorators/schedule.py +137 -0
  85. unrealon_driver/decorators/task.py +61 -0
  86. unrealon_driver/decorators/timing.py +132 -0
  87. unrealon_driver/driver/__init__.py +20 -0
  88. unrealon_driver/driver/communication/__init__.py +10 -0
  89. unrealon_driver/driver/communication/session.py +203 -0
  90. unrealon_driver/driver/communication/websocket_client.py +197 -0
  91. unrealon_driver/driver/core/__init__.py +10 -0
  92. unrealon_driver/driver/core/config.py +85 -0
  93. unrealon_driver/driver/core/driver.py +221 -0
  94. unrealon_driver/driver/factory/__init__.py +9 -0
  95. unrealon_driver/driver/factory/manager_factory.py +130 -0
  96. unrealon_driver/driver/lifecycle/__init__.py +11 -0
  97. unrealon_driver/driver/lifecycle/daemon.py +76 -0
  98. unrealon_driver/driver/lifecycle/initialization.py +97 -0
  99. unrealon_driver/driver/lifecycle/shutdown.py +48 -0
  100. unrealon_driver/driver/monitoring/__init__.py +9 -0
  101. unrealon_driver/driver/monitoring/health.py +63 -0
  102. unrealon_driver/driver/utilities/__init__.py +10 -0
  103. unrealon_driver/driver/utilities/logging.py +51 -0
  104. unrealon_driver/driver/utilities/serialization.py +61 -0
  105. unrealon_driver/managers/__init__.py +32 -0
  106. unrealon_driver/managers/base.py +174 -0
  107. unrealon_driver/managers/browser.py +98 -0
  108. unrealon_driver/managers/cache.py +116 -0
  109. unrealon_driver/managers/http.py +107 -0
  110. unrealon_driver/managers/logger.py +286 -0
  111. unrealon_driver/managers/proxy.py +99 -0
  112. unrealon_driver/managers/registry.py +87 -0
  113. unrealon_driver/managers/threading.py +54 -0
  114. unrealon_driver/managers/update.py +107 -0
  115. unrealon_driver/utils/__init__.py +9 -0
  116. unrealon_driver/utils/time.py +10 -0
  117. unrealon/__init__.py +0 -40
  118. unrealon-1.1.5.dist-info/METADATA +0 -621
  119. unrealon-1.1.5.dist-info/RECORD +0 -54
  120. unrealon-1.1.5.dist-info/entry_points.txt +0 -9
  121. unrealon_browser/managers/stealth.py +0 -388
  122. unrealon_driver/exceptions.py +0 -33
  123. unrealon_driver/html_analyzer/__init__.py +0 -32
  124. unrealon_driver/html_analyzer/cleaner.py +0 -657
  125. unrealon_driver/html_analyzer/config.py +0 -64
  126. unrealon_driver/html_analyzer/manager.py +0 -247
  127. unrealon_driver/html_analyzer/models.py +0 -115
  128. unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
  129. unrealon_driver/models/__init__.py +0 -31
  130. unrealon_driver/models/websocket.py +0 -98
  131. unrealon_driver/parser/__init__.py +0 -36
  132. unrealon_driver/parser/cli_manager.py +0 -142
  133. unrealon_driver/parser/daemon_manager.py +0 -403
  134. unrealon_driver/parser/managers/__init__.py +0 -25
  135. unrealon_driver/parser/managers/config.py +0 -293
  136. unrealon_driver/parser/managers/error.py +0 -412
  137. unrealon_driver/parser/managers/result.py +0 -321
  138. unrealon_driver/parser/parser_manager.py +0 -458
  139. unrealon_driver/smart_logging/__init__.py +0 -24
  140. unrealon_driver/smart_logging/models.py +0 -44
  141. unrealon_driver/smart_logging/smart_logger.py +0 -406
  142. unrealon_driver/smart_logging/unified_logger.py +0 -525
  143. unrealon_driver/websocket/__init__.py +0 -31
  144. unrealon_driver/websocket/client.py +0 -249
  145. unrealon_driver/websocket/config.py +0 -188
  146. unrealon_driver/websocket/manager.py +0 -90
@@ -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
@@ -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
+ ]