unrealon 1.1.6__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unrealon-1.1.6.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
- unrealon-2.0.4.dist-info/METADATA +491 -0
- unrealon-2.0.4.dist-info/RECORD +129 -0
- {unrealon-1.1.6.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
- unrealon-2.0.4.dist-info/entry_points.txt +3 -0
- unrealon-2.0.4.dist-info/top_level.txt +3 -0
- unrealon_browser/__init__.py +5 -6
- unrealon_browser/cli/browser_cli.py +18 -9
- unrealon_browser/cli/interactive_mode.py +13 -4
- unrealon_browser/core/browser_manager.py +29 -16
- unrealon_browser/dto/__init__.py +21 -0
- unrealon_browser/dto/bot_detection.py +175 -0
- unrealon_browser/dto/models/config.py +9 -3
- unrealon_browser/managers/__init__.py +1 -1
- unrealon_browser/managers/logger_bridge.py +1 -4
- unrealon_browser/stealth/__init__.py +27 -0
- unrealon_browser/stealth/bypass_techniques.pyc +0 -0
- unrealon_browser/stealth/manager.pyc +0 -0
- unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
- unrealon_browser/stealth/playwright_stealth.pyc +0 -0
- unrealon_browser/stealth/scanner_tester.pyc +0 -0
- unrealon_browser/stealth/undetected_chrome.pyc +0 -0
- unrealon_core/__init__.py +160 -0
- unrealon_core/config/__init__.py +16 -0
- unrealon_core/config/environment.py +98 -0
- unrealon_core/config/urls.py +93 -0
- unrealon_core/enums/__init__.py +24 -0
- unrealon_core/enums/status.py +216 -0
- unrealon_core/enums/types.py +240 -0
- unrealon_core/error_handling/__init__.py +45 -0
- unrealon_core/error_handling/circuit_breaker.py +292 -0
- unrealon_core/error_handling/error_context.py +324 -0
- unrealon_core/error_handling/recovery.py +371 -0
- unrealon_core/error_handling/retry.py +268 -0
- unrealon_core/exceptions/__init__.py +46 -0
- unrealon_core/exceptions/base.py +292 -0
- unrealon_core/exceptions/communication.py +22 -0
- unrealon_core/exceptions/driver.py +11 -0
- unrealon_core/exceptions/proxy.py +11 -0
- unrealon_core/exceptions/task.py +12 -0
- unrealon_core/exceptions/validation.py +17 -0
- unrealon_core/models/__init__.py +98 -0
- unrealon_core/models/arq_context.py +252 -0
- unrealon_core/models/arq_responses.py +125 -0
- unrealon_core/models/base.py +291 -0
- unrealon_core/models/bridge_stats.py +58 -0
- unrealon_core/models/communication.py +39 -0
- unrealon_core/models/config.py +47 -0
- unrealon_core/models/connection_stats.py +47 -0
- unrealon_core/models/driver.py +30 -0
- unrealon_core/models/driver_details.py +98 -0
- unrealon_core/models/logging.py +28 -0
- unrealon_core/models/task.py +21 -0
- unrealon_core/models/typed_responses.py +210 -0
- unrealon_core/models/websocket/__init__.py +91 -0
- unrealon_core/models/websocket/base.py +49 -0
- unrealon_core/models/websocket/config.py +200 -0
- unrealon_core/models/websocket/driver.py +215 -0
- unrealon_core/models/websocket/errors.py +138 -0
- unrealon_core/models/websocket/heartbeat.py +100 -0
- unrealon_core/models/websocket/logging.py +261 -0
- unrealon_core/models/websocket/proxy.py +496 -0
- unrealon_core/models/websocket/tasks.py +275 -0
- unrealon_core/models/websocket/utils.py +153 -0
- unrealon_core/models/websocket_session.py +144 -0
- unrealon_core/monitoring/__init__.py +43 -0
- unrealon_core/monitoring/alerts.py +398 -0
- unrealon_core/monitoring/dashboard.py +307 -0
- unrealon_core/monitoring/health_check.py +354 -0
- unrealon_core/monitoring/metrics.py +352 -0
- unrealon_core/utils/__init__.py +11 -0
- unrealon_core/utils/time.py +61 -0
- unrealon_core/version.py +219 -0
- unrealon_driver/__init__.py +90 -51
- unrealon_driver/core_module/__init__.py +34 -0
- unrealon_driver/core_module/base.py +184 -0
- unrealon_driver/core_module/config.py +30 -0
- unrealon_driver/core_module/event_manager.py +127 -0
- unrealon_driver/core_module/protocols.py +98 -0
- unrealon_driver/core_module/registry.py +146 -0
- unrealon_driver/decorators/__init__.py +15 -0
- unrealon_driver/decorators/retry.py +117 -0
- unrealon_driver/decorators/schedule.py +137 -0
- unrealon_driver/decorators/task.py +61 -0
- unrealon_driver/decorators/timing.py +132 -0
- unrealon_driver/driver/__init__.py +20 -0
- unrealon_driver/driver/communication/__init__.py +10 -0
- unrealon_driver/driver/communication/session.py +203 -0
- unrealon_driver/driver/communication/websocket_client.py +197 -0
- unrealon_driver/driver/core/__init__.py +10 -0
- unrealon_driver/driver/core/config.py +85 -0
- unrealon_driver/driver/core/driver.py +221 -0
- unrealon_driver/driver/factory/__init__.py +9 -0
- unrealon_driver/driver/factory/manager_factory.py +130 -0
- unrealon_driver/driver/lifecycle/__init__.py +11 -0
- unrealon_driver/driver/lifecycle/daemon.py +76 -0
- unrealon_driver/driver/lifecycle/initialization.py +97 -0
- unrealon_driver/driver/lifecycle/shutdown.py +48 -0
- unrealon_driver/driver/monitoring/__init__.py +9 -0
- unrealon_driver/driver/monitoring/health.py +63 -0
- unrealon_driver/driver/utilities/__init__.py +10 -0
- unrealon_driver/driver/utilities/logging.py +51 -0
- unrealon_driver/driver/utilities/serialization.py +61 -0
- unrealon_driver/managers/__init__.py +32 -0
- unrealon_driver/managers/base.py +174 -0
- unrealon_driver/managers/browser.py +98 -0
- unrealon_driver/managers/cache.py +116 -0
- unrealon_driver/managers/http.py +107 -0
- unrealon_driver/managers/logger.py +286 -0
- unrealon_driver/managers/proxy.py +99 -0
- unrealon_driver/managers/registry.py +87 -0
- unrealon_driver/managers/threading.py +54 -0
- unrealon_driver/managers/update.py +107 -0
- unrealon_driver/utils/__init__.py +9 -0
- unrealon_driver/utils/time.py +10 -0
- unrealon-1.1.6.dist-info/METADATA +0 -625
- unrealon-1.1.6.dist-info/RECORD +0 -55
- unrealon-1.1.6.dist-info/entry_points.txt +0 -9
- unrealon_browser/managers/stealth.py +0 -388
- unrealon_driver/README.md +0 -0
- unrealon_driver/exceptions.py +0 -33
- unrealon_driver/html_analyzer/__init__.py +0 -32
- unrealon_driver/html_analyzer/cleaner.py +0 -657
- unrealon_driver/html_analyzer/config.py +0 -64
- unrealon_driver/html_analyzer/manager.py +0 -247
- unrealon_driver/html_analyzer/models.py +0 -115
- unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
- unrealon_driver/models/__init__.py +0 -31
- unrealon_driver/models/websocket.py +0 -98
- unrealon_driver/parser/__init__.py +0 -36
- unrealon_driver/parser/cli_manager.py +0 -142
- unrealon_driver/parser/daemon_manager.py +0 -403
- unrealon_driver/parser/managers/__init__.py +0 -25
- unrealon_driver/parser/managers/config.py +0 -293
- unrealon_driver/parser/managers/error.py +0 -412
- unrealon_driver/parser/managers/result.py +0 -321
- unrealon_driver/parser/parser_manager.py +0 -458
- unrealon_driver/smart_logging/__init__.py +0 -24
- unrealon_driver/smart_logging/models.py +0 -44
- unrealon_driver/smart_logging/smart_logger.py +0 -406
- unrealon_driver/smart_logging/unified_logger.py +0 -525
- unrealon_driver/websocket/__init__.py +0 -31
- unrealon_driver/websocket/client.py +0 -249
- unrealon_driver/websocket/config.py +0 -188
- unrealon_driver/websocket/manager.py +0 -90
|
@@ -0,0 +1,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()
|