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.
Files changed (144) hide show
  1. {unrealon-1.1.6.dist-info/licenses → unrealon-2.0.5.dist-info}/LICENSE +1 -1
  2. unrealon-2.0.5.dist-info/METADATA +491 -0
  3. unrealon-2.0.5.dist-info/RECORD +128 -0
  4. {unrealon-1.1.6.dist-info → unrealon-2.0.5.dist-info}/WHEEL +2 -1
  5. unrealon-2.0.5.dist-info/entry_points.txt +3 -0
  6. unrealon-2.0.5.dist-info/top_level.txt +3 -0
  7. unrealon_browser/__init__.py +5 -6
  8. unrealon_browser/cli/browser_cli.py +18 -9
  9. unrealon_browser/cli/interactive_mode.py +13 -4
  10. unrealon_browser/core/browser_manager.py +29 -16
  11. unrealon_browser/dto/__init__.py +21 -0
  12. unrealon_browser/dto/bot_detection.py +175 -0
  13. unrealon_browser/dto/models/config.py +9 -3
  14. unrealon_browser/managers/__init__.py +1 -1
  15. unrealon_browser/managers/logger_bridge.py +1 -4
  16. unrealon_browser/stealth/__init__.py +27 -0
  17. unrealon_browser/stealth/bypass_techniques.pyc +0 -0
  18. unrealon_browser/stealth/manager.pyc +0 -0
  19. unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
  20. unrealon_browser/stealth/playwright_stealth.pyc +0 -0
  21. unrealon_browser/stealth/scanner_tester.pyc +0 -0
  22. unrealon_browser/stealth/undetected_chrome.pyc +0 -0
  23. unrealon_core/__init__.py +172 -0
  24. unrealon_core/config/__init__.py +16 -0
  25. unrealon_core/config/environment.py +151 -0
  26. unrealon_core/config/urls.py +94 -0
  27. unrealon_core/enums/__init__.py +24 -0
  28. unrealon_core/enums/status.py +216 -0
  29. unrealon_core/enums/types.py +240 -0
  30. unrealon_core/error_handling/__init__.py +45 -0
  31. unrealon_core/error_handling/circuit_breaker.py +292 -0
  32. unrealon_core/error_handling/error_context.py +324 -0
  33. unrealon_core/error_handling/recovery.py +371 -0
  34. unrealon_core/error_handling/retry.py +268 -0
  35. unrealon_core/exceptions/__init__.py +46 -0
  36. unrealon_core/exceptions/base.py +292 -0
  37. unrealon_core/exceptions/communication.py +22 -0
  38. unrealon_core/exceptions/driver.py +11 -0
  39. unrealon_core/exceptions/proxy.py +11 -0
  40. unrealon_core/exceptions/task.py +12 -0
  41. unrealon_core/exceptions/validation.py +17 -0
  42. unrealon_core/models/__init__.py +79 -0
  43. unrealon_core/models/arq_context.py +252 -0
  44. unrealon_core/models/arq_responses.py +125 -0
  45. unrealon_core/models/base.py +291 -0
  46. unrealon_core/models/bridge_stats.py +58 -0
  47. unrealon_core/models/communication.py +39 -0
  48. unrealon_core/models/connection_stats.py +47 -0
  49. unrealon_core/models/driver.py +30 -0
  50. unrealon_core/models/driver_details.py +98 -0
  51. unrealon_core/models/logging.py +28 -0
  52. unrealon_core/models/task.py +21 -0
  53. unrealon_core/models/typed_responses.py +210 -0
  54. unrealon_core/models/websocket/__init__.py +91 -0
  55. unrealon_core/models/websocket/base.py +49 -0
  56. unrealon_core/models/websocket/config.py +200 -0
  57. unrealon_core/models/websocket/driver.py +215 -0
  58. unrealon_core/models/websocket/errors.py +138 -0
  59. unrealon_core/models/websocket/heartbeat.py +100 -0
  60. unrealon_core/models/websocket/logging.py +261 -0
  61. unrealon_core/models/websocket/proxy.py +496 -0
  62. unrealon_core/models/websocket/tasks.py +275 -0
  63. unrealon_core/models/websocket/utils.py +153 -0
  64. unrealon_core/models/websocket_session.py +144 -0
  65. unrealon_core/monitoring/__init__.py +43 -0
  66. unrealon_core/monitoring/alerts.py +398 -0
  67. unrealon_core/monitoring/dashboard.py +307 -0
  68. unrealon_core/monitoring/health_check.py +354 -0
  69. unrealon_core/monitoring/metrics.py +352 -0
  70. unrealon_core/utils/__init__.py +11 -0
  71. unrealon_core/utils/time.py +61 -0
  72. unrealon_core/version.py +219 -0
  73. unrealon_driver/__init__.py +90 -51
  74. unrealon_driver/core_module/__init__.py +34 -0
  75. unrealon_driver/core_module/base.py +184 -0
  76. unrealon_driver/core_module/config.py +30 -0
  77. unrealon_driver/core_module/event_manager.py +127 -0
  78. unrealon_driver/core_module/protocols.py +98 -0
  79. unrealon_driver/core_module/registry.py +146 -0
  80. unrealon_driver/decorators/__init__.py +15 -0
  81. unrealon_driver/decorators/retry.py +117 -0
  82. unrealon_driver/decorators/schedule.py +137 -0
  83. unrealon_driver/decorators/task.py +61 -0
  84. unrealon_driver/decorators/timing.py +132 -0
  85. unrealon_driver/driver/__init__.py +20 -0
  86. unrealon_driver/driver/communication/__init__.py +10 -0
  87. unrealon_driver/driver/communication/session.py +203 -0
  88. unrealon_driver/driver/communication/websocket_client.py +205 -0
  89. unrealon_driver/driver/core/__init__.py +10 -0
  90. unrealon_driver/driver/core/config.py +175 -0
  91. unrealon_driver/driver/core/driver.py +221 -0
  92. unrealon_driver/driver/factory/__init__.py +9 -0
  93. unrealon_driver/driver/factory/manager_factory.py +130 -0
  94. unrealon_driver/driver/lifecycle/__init__.py +11 -0
  95. unrealon_driver/driver/lifecycle/daemon.py +76 -0
  96. unrealon_driver/driver/lifecycle/initialization.py +97 -0
  97. unrealon_driver/driver/lifecycle/shutdown.py +48 -0
  98. unrealon_driver/driver/monitoring/__init__.py +9 -0
  99. unrealon_driver/driver/monitoring/health.py +63 -0
  100. unrealon_driver/driver/utilities/__init__.py +10 -0
  101. unrealon_driver/driver/utilities/logging.py +51 -0
  102. unrealon_driver/driver/utilities/serialization.py +61 -0
  103. unrealon_driver/managers/__init__.py +32 -0
  104. unrealon_driver/managers/base.py +174 -0
  105. unrealon_driver/managers/browser.py +98 -0
  106. unrealon_driver/managers/cache.py +116 -0
  107. unrealon_driver/managers/http.py +107 -0
  108. unrealon_driver/managers/logger.py +286 -0
  109. unrealon_driver/managers/proxy.py +99 -0
  110. unrealon_driver/managers/registry.py +87 -0
  111. unrealon_driver/managers/threading.py +54 -0
  112. unrealon_driver/managers/update.py +107 -0
  113. unrealon_driver/utils/__init__.py +9 -0
  114. unrealon_driver/utils/time.py +10 -0
  115. unrealon-1.1.6.dist-info/METADATA +0 -625
  116. unrealon-1.1.6.dist-info/RECORD +0 -55
  117. unrealon-1.1.6.dist-info/entry_points.txt +0 -9
  118. unrealon_browser/managers/stealth.py +0 -388
  119. unrealon_driver/README.md +0 -0
  120. unrealon_driver/exceptions.py +0 -33
  121. unrealon_driver/html_analyzer/__init__.py +0 -32
  122. unrealon_driver/html_analyzer/cleaner.py +0 -657
  123. unrealon_driver/html_analyzer/config.py +0 -64
  124. unrealon_driver/html_analyzer/manager.py +0 -247
  125. unrealon_driver/html_analyzer/models.py +0 -115
  126. unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
  127. unrealon_driver/models/__init__.py +0 -31
  128. unrealon_driver/models/websocket.py +0 -98
  129. unrealon_driver/parser/__init__.py +0 -36
  130. unrealon_driver/parser/cli_manager.py +0 -142
  131. unrealon_driver/parser/daemon_manager.py +0 -403
  132. unrealon_driver/parser/managers/__init__.py +0 -25
  133. unrealon_driver/parser/managers/config.py +0 -293
  134. unrealon_driver/parser/managers/error.py +0 -412
  135. unrealon_driver/parser/managers/result.py +0 -321
  136. unrealon_driver/parser/parser_manager.py +0 -458
  137. unrealon_driver/smart_logging/__init__.py +0 -24
  138. unrealon_driver/smart_logging/models.py +0 -44
  139. unrealon_driver/smart_logging/smart_logger.py +0 -406
  140. unrealon_driver/smart_logging/unified_logger.py +0 -525
  141. unrealon_driver/websocket/__init__.py +0 -31
  142. unrealon_driver/websocket/client.py +0 -249
  143. unrealon_driver/websocket/config.py +0 -188
  144. unrealon_driver/websocket/manager.py +0 -90
@@ -0,0 +1,240 @@
1
+ """
2
+ Type enums for UnrealOn system.
3
+
4
+ These enums define types for messages, proxies, tasks, and other
5
+ system components. They ensure type safety and consistent
6
+ categorization throughout the system.
7
+
8
+ Phase 1: Foundation type enums
9
+ """
10
+
11
+ from enum import Enum
12
+
13
+
14
+ class MessageType(str, Enum):
15
+ """
16
+ WebSocket message types for driver communication.
17
+
18
+ Defines all possible message types that can be sent
19
+ between the system and drivers via WebSocket.
20
+ """
21
+
22
+ # Driver lifecycle messages
23
+ DRIVER_REGISTER = "driver_register" # Driver registration request
24
+ DRIVER_REGISTER_RESPONSE = "driver_register_response" # Registration response
25
+ DRIVER_HEARTBEAT = "driver_heartbeat" # Driver heartbeat/ping
26
+ DRIVER_STATUS = "driver_status" # Driver status update
27
+ DRIVER_DISCONNECT = "driver_disconnect" # Driver disconnect notification
28
+
29
+ # Task management messages
30
+ TASK_ASSIGN = "task_assign" # Assign task to driver
31
+ TASK_RESULT = "task_result" # Task result from driver
32
+ TASK_ERROR = "task_error" # Task error from driver
33
+ TASK_PROGRESS = "task_progress" # Task progress update
34
+ TASK_CANCEL = "task_cancel" # Cancel task request
35
+
36
+ # Command messages
37
+ COMMAND_START = "command_start" # Start command
38
+ COMMAND_STOP = "command_stop" # Stop command
39
+ COMMAND_RESTART = "command_restart" # Restart command
40
+ COMMAND_PAUSE = "command_pause" # Pause command
41
+ COMMAND_RESUME = "command_resume" # Resume command
42
+ COMMAND_CONFIG_UPDATE = "command_config_update" # Update configuration
43
+
44
+ # Proxy management messages
45
+ PROXY_REQUEST = "proxy_request" # Request proxy from pool
46
+ PROXY_RESPONSE = "proxy_response" # Proxy response
47
+ PROXY_HEALTH_REPORT = "proxy_health_report" # Report proxy health
48
+ PROXY_ROTATION_REQUEST = "proxy_rotation_request" # Request proxy rotation
49
+ PROXY_RELEASE = "proxy_release" # Release proxy assignment
50
+
51
+ # Logging messages
52
+ LOG_MESSAGE = "log_message" # Log message from driver
53
+ LOG_BATCH = "log_batch" # Batch of log messages
54
+
55
+ # System messages
56
+ PING = "ping" # Ping message
57
+ PONG = "pong" # Pong response
58
+ ERROR = "error" # Error message
59
+ ACK = "ack" # Acknowledgment
60
+
61
+ def is_driver_lifecycle(self) -> bool:
62
+ """Check if message is related to driver lifecycle."""
63
+ return self in [
64
+ MessageType.DRIVER_REGISTER,
65
+ MessageType.DRIVER_REGISTER_RESPONSE,
66
+ MessageType.DRIVER_HEARTBEAT,
67
+ MessageType.DRIVER_STATUS,
68
+ MessageType.DRIVER_DISCONNECT
69
+ ]
70
+
71
+ def is_task_related(self) -> bool:
72
+ """Check if message is related to task management."""
73
+ return self in [
74
+ MessageType.TASK_ASSIGN,
75
+ MessageType.TASK_RESULT,
76
+ MessageType.TASK_ERROR,
77
+ MessageType.TASK_PROGRESS,
78
+ MessageType.TASK_CANCEL
79
+ ]
80
+
81
+ def is_command(self) -> bool:
82
+ """Check if message is a command."""
83
+ return self.value.startswith("command_")
84
+
85
+ def requires_response(self) -> bool:
86
+ """Check if message type requires a response."""
87
+ return self in [
88
+ MessageType.DRIVER_REGISTER,
89
+ MessageType.PROXY_REQUEST,
90
+ MessageType.PING
91
+ ]
92
+
93
+
94
+ class ProxyType(str, Enum):
95
+ """
96
+ Proxy server types supported by the system.
97
+
98
+ Defines the different types of proxy servers
99
+ that can be used for web requests.
100
+ """
101
+
102
+ HTTP = "http" # HTTP proxy
103
+ HTTPS = "https" # HTTPS proxy
104
+ SOCKS4 = "socks4" # SOCKS4 proxy
105
+ SOCKS5 = "socks5" # SOCKS5 proxy
106
+
107
+ def supports_https(self) -> bool:
108
+ """Check if proxy type supports HTTPS."""
109
+ return self in [ProxyType.HTTPS, ProxyType.SOCKS5]
110
+
111
+ def is_socks(self) -> bool:
112
+ """Check if proxy is a SOCKS proxy."""
113
+ return self in [ProxyType.SOCKS4, ProxyType.SOCKS5]
114
+
115
+ def get_default_port(self) -> int:
116
+ """Get default port for proxy type."""
117
+ ports = {
118
+ ProxyType.HTTP: 8080,
119
+ ProxyType.HTTPS: 8080,
120
+ ProxyType.SOCKS4: 1080,
121
+ ProxyType.SOCKS5: 1080,
122
+ }
123
+ return ports.get(self, 8080)
124
+
125
+
126
+ class TaskPriority(str, Enum):
127
+ """
128
+ Task priority levels for queue management.
129
+
130
+ Defines priority levels that determine the order
131
+ in which tasks are processed by drivers.
132
+ """
133
+
134
+ LOW = "low" # Low priority task
135
+ NORMAL = "normal" # Normal priority task (default)
136
+ HIGH = "high" # High priority task
137
+ URGENT = "urgent" # Urgent priority task
138
+ CRITICAL = "critical" # Critical priority task
139
+
140
+ def get_numeric_priority(self) -> int:
141
+ """Get numeric priority for sorting (higher = more urgent)."""
142
+ priorities = {
143
+ TaskPriority.LOW: 1,
144
+ TaskPriority.NORMAL: 5,
145
+ TaskPriority.HIGH: 10,
146
+ TaskPriority.URGENT: 20,
147
+ TaskPriority.CRITICAL: 50,
148
+ }
149
+ return priorities.get(self, 5)
150
+
151
+ def is_urgent(self) -> bool:
152
+ """Check if task has urgent or critical priority."""
153
+ return self in [TaskPriority.URGENT, TaskPriority.CRITICAL]
154
+
155
+ @classmethod
156
+ def from_numeric(cls, priority: int) -> 'TaskPriority':
157
+ """Create TaskPriority from numeric value."""
158
+ if priority >= 50:
159
+ return cls.CRITICAL
160
+ elif priority >= 20:
161
+ return cls.URGENT
162
+ elif priority >= 10:
163
+ return cls.HIGH
164
+ elif priority >= 5:
165
+ return cls.NORMAL
166
+ else:
167
+ return cls.LOW
168
+
169
+
170
+ class DriverType(str, Enum):
171
+ """
172
+ Driver types for categorization and capability matching.
173
+
174
+ Defines different types of drivers based on their
175
+ primary functionality and target websites.
176
+ """
177
+
178
+ # General purpose
179
+ UNIVERSAL = "universal" # Universal driver for any site
180
+ GENERIC = "generic" # Generic web scraping driver
181
+
182
+ # E-commerce specific
183
+ ECOMMERCE = "ecommerce" # E-commerce sites
184
+ MARKETPLACE = "marketplace" # Online marketplaces
185
+ PRODUCT_CATALOG = "product_catalog" # Product catalogs
186
+
187
+ # Content specific
188
+ NEWS = "news" # News websites
189
+ BLOG = "blog" # Blog sites
190
+ SOCIAL_MEDIA = "social_media" # Social media platforms
191
+
192
+ # Data specific
193
+ API = "api" # API-based data collection
194
+ DATABASE = "database" # Database extraction
195
+ FILE = "file" # File processing
196
+
197
+ def is_web_scraping(self) -> bool:
198
+ """Check if driver type involves web scraping."""
199
+ return self in [
200
+ DriverType.UNIVERSAL,
201
+ DriverType.GENERIC,
202
+ DriverType.ECOMMERCE,
203
+ DriverType.MARKETPLACE,
204
+ DriverType.PRODUCT_CATALOG,
205
+ DriverType.NEWS,
206
+ DriverType.BLOG,
207
+ DriverType.SOCIAL_MEDIA
208
+ ]
209
+
210
+ def requires_browser(self) -> bool:
211
+ """Check if driver type typically requires a browser."""
212
+ return self in [
213
+ DriverType.ECOMMERCE,
214
+ DriverType.MARKETPLACE,
215
+ DriverType.SOCIAL_MEDIA
216
+ ]
217
+
218
+
219
+ class ConfigType(str, Enum):
220
+ """
221
+ Configuration types for different system components.
222
+
223
+ Used to categorize and validate different types
224
+ of configuration objects in the system.
225
+ """
226
+
227
+ SYSTEM = "system" # System-wide configuration
228
+ DRIVER = "driver" # Driver-specific configuration
229
+ HTTP = "http" # HTTP client configuration
230
+ PROXY = "proxy" # Proxy management configuration
231
+ BROWSER = "browser" # Browser automation configuration
232
+ LOGGING = "logging" # Logging configuration
233
+ CACHE = "cache" # Cache configuration
234
+ THREAD = "thread" # Threading configuration
235
+ DATABASE = "database" # Database configuration
236
+ SECURITY = "security" # Security configuration
237
+
238
+ def get_config_schema(self) -> str:
239
+ """Get the schema identifier for this config type."""
240
+ return f"unrealon.config.{self.value}"
@@ -0,0 +1,45 @@
1
+ """
2
+ Error Handling Package
3
+
4
+ Comprehensive error handling and retry logic for UnrealOn RPC system.
5
+ Following critical requirements - max 500 lines, 100% Pydantic v2.
6
+
7
+ Phase 2: Core Systems - Error Handling
8
+ """
9
+
10
+ from .retry import (
11
+ RetryConfig, RetryStrategy, RetryResult,
12
+ ExponentialBackoff, LinearBackoff, FixedBackoff,
13
+ retry_async, retry_sync
14
+ )
15
+ from .circuit_breaker import (
16
+ CircuitBreakerConfig, CircuitBreakerState,
17
+ CircuitBreaker, circuit_breaker, get_circuit_breaker
18
+ )
19
+ from .error_context import (
20
+ ErrorContext, ErrorSeverity,
21
+ create_error_context, format_error_context
22
+ )
23
+ from .recovery import (
24
+ RecoveryStrategy, RecoveryAction,
25
+ AutoRecovery, recovery_handler
26
+ )
27
+
28
+ __all__ = [
29
+ # Retry system
30
+ 'RetryConfig', 'RetryStrategy', 'RetryResult',
31
+ 'ExponentialBackoff', 'LinearBackoff', 'FixedBackoff',
32
+ 'retry_async', 'retry_sync',
33
+
34
+ # Circuit breaker
35
+ 'CircuitBreakerConfig', 'CircuitBreakerState',
36
+ 'CircuitBreaker', 'circuit_breaker', 'get_circuit_breaker',
37
+
38
+ # Error context
39
+ 'ErrorContext', 'ErrorSeverity',
40
+ 'create_error_context', 'format_error_context',
41
+
42
+ # Recovery system
43
+ 'RecoveryStrategy', 'RecoveryAction',
44
+ 'AutoRecovery', 'recovery_handler',
45
+ ]
@@ -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()