unrealon 1.1.5__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unrealon-1.1.5.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
- unrealon-2.0.4.dist-info/METADATA +491 -0
- unrealon-2.0.4.dist-info/RECORD +129 -0
- {unrealon-1.1.5.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
- unrealon-2.0.4.dist-info/entry_points.txt +3 -0
- unrealon-2.0.4.dist-info/top_level.txt +3 -0
- unrealon_browser/__init__.py +5 -2
- unrealon_browser/cli/browser_cli.py +18 -9
- unrealon_browser/cli/interactive_mode.py +18 -7
- unrealon_browser/core/browser_manager.py +76 -13
- unrealon_browser/dto/__init__.py +21 -0
- unrealon_browser/dto/bot_detection.py +175 -0
- unrealon_browser/dto/models/config.py +14 -1
- unrealon_browser/managers/__init__.py +4 -1
- unrealon_browser/managers/logger_bridge.py +3 -6
- unrealon_browser/managers/page_wait_manager.py +198 -0
- unrealon_browser/stealth/__init__.py +27 -0
- unrealon_browser/stealth/bypass_techniques.pyc +0 -0
- unrealon_browser/stealth/manager.pyc +0 -0
- unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
- unrealon_browser/stealth/playwright_stealth.pyc +0 -0
- unrealon_browser/stealth/scanner_tester.pyc +0 -0
- unrealon_browser/stealth/undetected_chrome.pyc +0 -0
- unrealon_core/__init__.py +160 -0
- unrealon_core/config/__init__.py +16 -0
- unrealon_core/config/environment.py +98 -0
- unrealon_core/config/urls.py +93 -0
- unrealon_core/enums/__init__.py +24 -0
- unrealon_core/enums/status.py +216 -0
- unrealon_core/enums/types.py +240 -0
- unrealon_core/error_handling/__init__.py +45 -0
- unrealon_core/error_handling/circuit_breaker.py +292 -0
- unrealon_core/error_handling/error_context.py +324 -0
- unrealon_core/error_handling/recovery.py +371 -0
- unrealon_core/error_handling/retry.py +268 -0
- unrealon_core/exceptions/__init__.py +46 -0
- unrealon_core/exceptions/base.py +292 -0
- unrealon_core/exceptions/communication.py +22 -0
- unrealon_core/exceptions/driver.py +11 -0
- unrealon_core/exceptions/proxy.py +11 -0
- unrealon_core/exceptions/task.py +12 -0
- unrealon_core/exceptions/validation.py +17 -0
- unrealon_core/models/__init__.py +98 -0
- unrealon_core/models/arq_context.py +252 -0
- unrealon_core/models/arq_responses.py +125 -0
- unrealon_core/models/base.py +291 -0
- unrealon_core/models/bridge_stats.py +58 -0
- unrealon_core/models/communication.py +39 -0
- unrealon_core/models/config.py +47 -0
- unrealon_core/models/connection_stats.py +47 -0
- unrealon_core/models/driver.py +30 -0
- unrealon_core/models/driver_details.py +98 -0
- unrealon_core/models/logging.py +28 -0
- unrealon_core/models/task.py +21 -0
- unrealon_core/models/typed_responses.py +210 -0
- unrealon_core/models/websocket/__init__.py +91 -0
- unrealon_core/models/websocket/base.py +49 -0
- unrealon_core/models/websocket/config.py +200 -0
- unrealon_core/models/websocket/driver.py +215 -0
- unrealon_core/models/websocket/errors.py +138 -0
- unrealon_core/models/websocket/heartbeat.py +100 -0
- unrealon_core/models/websocket/logging.py +261 -0
- unrealon_core/models/websocket/proxy.py +496 -0
- unrealon_core/models/websocket/tasks.py +275 -0
- unrealon_core/models/websocket/utils.py +153 -0
- unrealon_core/models/websocket_session.py +144 -0
- unrealon_core/monitoring/__init__.py +43 -0
- unrealon_core/monitoring/alerts.py +398 -0
- unrealon_core/monitoring/dashboard.py +307 -0
- unrealon_core/monitoring/health_check.py +354 -0
- unrealon_core/monitoring/metrics.py +352 -0
- unrealon_core/utils/__init__.py +11 -0
- unrealon_core/utils/time.py +61 -0
- unrealon_core/version.py +219 -0
- unrealon_driver/__init__.py +88 -50
- unrealon_driver/core_module/__init__.py +34 -0
- unrealon_driver/core_module/base.py +184 -0
- unrealon_driver/core_module/config.py +30 -0
- unrealon_driver/core_module/event_manager.py +127 -0
- unrealon_driver/core_module/protocols.py +98 -0
- unrealon_driver/core_module/registry.py +146 -0
- unrealon_driver/decorators/__init__.py +15 -0
- unrealon_driver/decorators/retry.py +117 -0
- unrealon_driver/decorators/schedule.py +137 -0
- unrealon_driver/decorators/task.py +61 -0
- unrealon_driver/decorators/timing.py +132 -0
- unrealon_driver/driver/__init__.py +20 -0
- unrealon_driver/driver/communication/__init__.py +10 -0
- unrealon_driver/driver/communication/session.py +203 -0
- unrealon_driver/driver/communication/websocket_client.py +197 -0
- unrealon_driver/driver/core/__init__.py +10 -0
- unrealon_driver/driver/core/config.py +85 -0
- unrealon_driver/driver/core/driver.py +221 -0
- unrealon_driver/driver/factory/__init__.py +9 -0
- unrealon_driver/driver/factory/manager_factory.py +130 -0
- unrealon_driver/driver/lifecycle/__init__.py +11 -0
- unrealon_driver/driver/lifecycle/daemon.py +76 -0
- unrealon_driver/driver/lifecycle/initialization.py +97 -0
- unrealon_driver/driver/lifecycle/shutdown.py +48 -0
- unrealon_driver/driver/monitoring/__init__.py +9 -0
- unrealon_driver/driver/monitoring/health.py +63 -0
- unrealon_driver/driver/utilities/__init__.py +10 -0
- unrealon_driver/driver/utilities/logging.py +51 -0
- unrealon_driver/driver/utilities/serialization.py +61 -0
- unrealon_driver/managers/__init__.py +32 -0
- unrealon_driver/managers/base.py +174 -0
- unrealon_driver/managers/browser.py +98 -0
- unrealon_driver/managers/cache.py +116 -0
- unrealon_driver/managers/http.py +107 -0
- unrealon_driver/managers/logger.py +286 -0
- unrealon_driver/managers/proxy.py +99 -0
- unrealon_driver/managers/registry.py +87 -0
- unrealon_driver/managers/threading.py +54 -0
- unrealon_driver/managers/update.py +107 -0
- unrealon_driver/utils/__init__.py +9 -0
- unrealon_driver/utils/time.py +10 -0
- unrealon/__init__.py +0 -40
- unrealon-1.1.5.dist-info/METADATA +0 -621
- unrealon-1.1.5.dist-info/RECORD +0 -54
- unrealon-1.1.5.dist-info/entry_points.txt +0 -9
- unrealon_browser/managers/stealth.py +0 -388
- unrealon_driver/exceptions.py +0 -33
- unrealon_driver/html_analyzer/__init__.py +0 -32
- unrealon_driver/html_analyzer/cleaner.py +0 -657
- unrealon_driver/html_analyzer/config.py +0 -64
- unrealon_driver/html_analyzer/manager.py +0 -247
- unrealon_driver/html_analyzer/models.py +0 -115
- unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
- unrealon_driver/models/__init__.py +0 -31
- unrealon_driver/models/websocket.py +0 -98
- unrealon_driver/parser/__init__.py +0 -36
- unrealon_driver/parser/cli_manager.py +0 -142
- unrealon_driver/parser/daemon_manager.py +0 -403
- unrealon_driver/parser/managers/__init__.py +0 -25
- unrealon_driver/parser/managers/config.py +0 -293
- unrealon_driver/parser/managers/error.py +0 -412
- unrealon_driver/parser/managers/result.py +0 -321
- unrealon_driver/parser/parser_manager.py +0 -458
- unrealon_driver/smart_logging/__init__.py +0 -24
- unrealon_driver/smart_logging/models.py +0 -44
- unrealon_driver/smart_logging/smart_logger.py +0 -406
- unrealon_driver/smart_logging/unified_logger.py +0 -525
- unrealon_driver/websocket/__init__.py +0 -31
- unrealon_driver/websocket/client.py +0 -249
- unrealon_driver/websocket/config.py +0 -188
- unrealon_driver/websocket/manager.py +0 -90
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean timing decorator for performance monitoring.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Callable, Optional, Dict, Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def timing(
|
|
15
|
+
log_result: bool = True,
|
|
16
|
+
log_level: str = "INFO",
|
|
17
|
+
include_args: bool = False,
|
|
18
|
+
threshold: Optional[float] = None
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Clean timing decorator.
|
|
22
|
+
|
|
23
|
+
Measures and logs function execution time.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
log_result: Whether to log the timing result
|
|
27
|
+
log_level: Log level for timing messages
|
|
28
|
+
include_args: Include function arguments in log
|
|
29
|
+
threshold: Only log if execution time exceeds threshold (seconds)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def decorator(func: Callable) -> Callable:
|
|
33
|
+
@wraps(func)
|
|
34
|
+
async def async_wrapper(*args, **kwargs):
|
|
35
|
+
"""Async timing wrapper."""
|
|
36
|
+
|
|
37
|
+
start_time = asyncio.get_event_loop().time()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = await func(*args, **kwargs)
|
|
41
|
+
success = True
|
|
42
|
+
error = None
|
|
43
|
+
except Exception as e:
|
|
44
|
+
result = None
|
|
45
|
+
success = False
|
|
46
|
+
error = str(e)
|
|
47
|
+
raise
|
|
48
|
+
finally:
|
|
49
|
+
end_time = asyncio.get_event_loop().time()
|
|
50
|
+
duration = end_time - start_time
|
|
51
|
+
|
|
52
|
+
# Log timing if enabled and threshold met
|
|
53
|
+
if log_result and (threshold is None or duration >= threshold):
|
|
54
|
+
_log_timing(func, duration, success, error, args, kwargs, include_args, log_level)
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
@wraps(func)
|
|
59
|
+
def sync_wrapper(*args, **kwargs):
|
|
60
|
+
"""Sync timing wrapper."""
|
|
61
|
+
|
|
62
|
+
start_time = time.perf_counter()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = func(*args, **kwargs)
|
|
66
|
+
success = True
|
|
67
|
+
error = None
|
|
68
|
+
except Exception as e:
|
|
69
|
+
result = None
|
|
70
|
+
success = False
|
|
71
|
+
error = str(e)
|
|
72
|
+
raise
|
|
73
|
+
finally:
|
|
74
|
+
end_time = time.perf_counter()
|
|
75
|
+
duration = end_time - start_time
|
|
76
|
+
|
|
77
|
+
# Log timing if enabled and threshold met
|
|
78
|
+
if log_result and (threshold is None or duration >= threshold):
|
|
79
|
+
_log_timing(func, duration, success, error, args, kwargs, include_args, log_level)
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
# Return appropriate wrapper based on function type
|
|
84
|
+
if asyncio.iscoroutinefunction(func):
|
|
85
|
+
return async_wrapper
|
|
86
|
+
else:
|
|
87
|
+
return sync_wrapper
|
|
88
|
+
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _log_timing(
|
|
93
|
+
func: Callable,
|
|
94
|
+
duration: float,
|
|
95
|
+
success: bool,
|
|
96
|
+
error: Optional[str],
|
|
97
|
+
args: tuple,
|
|
98
|
+
kwargs: dict,
|
|
99
|
+
include_args: bool,
|
|
100
|
+
log_level: str
|
|
101
|
+
):
|
|
102
|
+
"""Log timing information."""
|
|
103
|
+
|
|
104
|
+
# Format duration
|
|
105
|
+
if duration < 0.001:
|
|
106
|
+
duration_str = f"{duration * 1000000:.0f}μs"
|
|
107
|
+
elif duration < 1.0:
|
|
108
|
+
duration_str = f"{duration * 1000:.1f}ms"
|
|
109
|
+
else:
|
|
110
|
+
duration_str = f"{duration:.2f}s"
|
|
111
|
+
|
|
112
|
+
# Build log message
|
|
113
|
+
status = "✓" if success else "✗"
|
|
114
|
+
message = f"{status} {func.__name__} took {duration_str}"
|
|
115
|
+
|
|
116
|
+
if error:
|
|
117
|
+
message += f" (failed: {error})"
|
|
118
|
+
|
|
119
|
+
if include_args:
|
|
120
|
+
args_str = ", ".join([repr(arg) for arg in args])
|
|
121
|
+
kwargs_str = ", ".join([f"{k}={repr(v)}" for k, v in kwargs.items()])
|
|
122
|
+
|
|
123
|
+
if args_str and kwargs_str:
|
|
124
|
+
message += f" | Args: ({args_str}, {kwargs_str})"
|
|
125
|
+
elif args_str:
|
|
126
|
+
message += f" | Args: ({args_str})"
|
|
127
|
+
elif kwargs_str:
|
|
128
|
+
message += f" | Args: ({kwargs_str})"
|
|
129
|
+
|
|
130
|
+
# Log at specified level
|
|
131
|
+
log_func = getattr(logger, log_level.lower(), logger.info)
|
|
132
|
+
log_func(message)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UniversalDriver - Modular driver system.
|
|
3
|
+
|
|
4
|
+
Clean, organized driver architecture with:
|
|
5
|
+
- Core driver functionality
|
|
6
|
+
- Lifecycle management
|
|
7
|
+
- Communication layer
|
|
8
|
+
- Factory pattern for managers
|
|
9
|
+
- Health monitoring
|
|
10
|
+
- Utilities for common operations
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .core.driver import UniversalDriver
|
|
14
|
+
from .core.config import DriverConfig, DriverMode
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"UniversalDriver",
|
|
18
|
+
"DriverConfig",
|
|
19
|
+
"DriverMode"
|
|
20
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean driver session management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from unrealon_core.models.websocket import (
|
|
14
|
+
TaskAssignmentData,
|
|
15
|
+
TaskResultData,
|
|
16
|
+
WebSocketMessage
|
|
17
|
+
)
|
|
18
|
+
from unrealon_core.enums import DriverStatus, TaskStatus, MessageType
|
|
19
|
+
from unrealon_driver.utils.time import utc_now
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SessionStatus(str, Enum):
|
|
25
|
+
"""Clean session status."""
|
|
26
|
+
DISCONNECTED = "disconnected"
|
|
27
|
+
CONNECTING = "connecting"
|
|
28
|
+
CONNECTED = "connected"
|
|
29
|
+
REGISTERED = "registered"
|
|
30
|
+
ACTIVE = "active"
|
|
31
|
+
ERROR = "error"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionStats(BaseModel):
|
|
35
|
+
"""Session statistics."""
|
|
36
|
+
connected_at: Optional[datetime] = None
|
|
37
|
+
registered_at: Optional[datetime] = None
|
|
38
|
+
tasks_completed: int = 0
|
|
39
|
+
tasks_failed: int = 0
|
|
40
|
+
last_activity: Optional[datetime] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DriverSession:
|
|
44
|
+
"""
|
|
45
|
+
Clean session manager.
|
|
46
|
+
|
|
47
|
+
Handles connection, registration, and task processing
|
|
48
|
+
with simple, clear interface.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, driver_id: str, websocket_client=None):
|
|
52
|
+
self.driver_id = driver_id
|
|
53
|
+
self.websocket_client = websocket_client
|
|
54
|
+
self.status = SessionStatus.DISCONNECTED
|
|
55
|
+
self.stats = SessionStats()
|
|
56
|
+
|
|
57
|
+
# Task handling
|
|
58
|
+
self.task_handlers: Dict[str, Callable] = {}
|
|
59
|
+
|
|
60
|
+
# Setup WebSocket message handler
|
|
61
|
+
if self.websocket_client:
|
|
62
|
+
self.websocket_client.on_message = self._handle_websocket_message
|
|
63
|
+
|
|
64
|
+
async def start_session(self, capabilities: List[str] = None) -> bool:
|
|
65
|
+
"""Start session with registration."""
|
|
66
|
+
if not self.websocket_client:
|
|
67
|
+
logger.error("No WebSocket client configured")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Connect WebSocket
|
|
72
|
+
self.status = SessionStatus.CONNECTING
|
|
73
|
+
if not await self.websocket_client.connect():
|
|
74
|
+
self.status = SessionStatus.ERROR
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
self.status = SessionStatus.CONNECTED
|
|
78
|
+
self.stats.connected_at = utc_now()
|
|
79
|
+
|
|
80
|
+
# Register driver
|
|
81
|
+
if await self.register(capabilities or []):
|
|
82
|
+
self.status = SessionStatus.REGISTERED
|
|
83
|
+
self.stats.registered_at = utc_now()
|
|
84
|
+
logger.info(f"Session started for driver: {self.driver_id}")
|
|
85
|
+
return True
|
|
86
|
+
else:
|
|
87
|
+
self.status = SessionStatus.ERROR
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Session start failed: {e}")
|
|
92
|
+
self.status = SessionStatus.ERROR
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def stop_session(self):
|
|
96
|
+
"""Stop session cleanly."""
|
|
97
|
+
try:
|
|
98
|
+
if self.websocket_client:
|
|
99
|
+
await self.websocket_client.disconnect()
|
|
100
|
+
|
|
101
|
+
self.status = SessionStatus.DISCONNECTED
|
|
102
|
+
logger.info(f"Session stopped for driver: {self.driver_id}")
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"Session stop error: {e}")
|
|
106
|
+
|
|
107
|
+
async def register(self, capabilities: List[str]) -> bool:
|
|
108
|
+
"""Register driver with capabilities."""
|
|
109
|
+
if not self.websocket_client:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
success = await self.websocket_client.register_driver(capabilities)
|
|
114
|
+
if success:
|
|
115
|
+
logger.info(f"Driver registered: {self.driver_id} with capabilities: {capabilities}")
|
|
116
|
+
return success
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Registration failed: {e}")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def register_task_handler(self, task_type: str, handler: Callable):
|
|
122
|
+
"""Register handler for task type."""
|
|
123
|
+
self.task_handlers[task_type] = handler
|
|
124
|
+
logger.debug(f"Registered handler for task type: {task_type}")
|
|
125
|
+
|
|
126
|
+
def _handle_websocket_message(self, message: WebSocketMessage):
|
|
127
|
+
"""Handle incoming WebSocket messages."""
|
|
128
|
+
try:
|
|
129
|
+
self.stats.last_activity = utc_now()
|
|
130
|
+
|
|
131
|
+
if message.type == MessageType.TASK_ASSIGN:
|
|
132
|
+
asyncio.create_task(self._handle_task_assignment(message))
|
|
133
|
+
else:
|
|
134
|
+
logger.debug(f"Received message type: {message.type}")
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Error handling WebSocket message: {e}")
|
|
138
|
+
|
|
139
|
+
async def _handle_task_assignment(self, message: WebSocketMessage):
|
|
140
|
+
"""Handle task assignment."""
|
|
141
|
+
try:
|
|
142
|
+
# Validate task data
|
|
143
|
+
task_data = TaskAssignmentData.model_validate(message.data)
|
|
144
|
+
|
|
145
|
+
# Find handler
|
|
146
|
+
handler = self.task_handlers.get(task_data.task_type)
|
|
147
|
+
if not handler:
|
|
148
|
+
logger.warning(f"No handler for task type: {task_data.task_type}")
|
|
149
|
+
await self._send_task_result(
|
|
150
|
+
task_data.task_id,
|
|
151
|
+
TaskStatus.FAILED,
|
|
152
|
+
error="No handler for task type"
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Execute task
|
|
157
|
+
try:
|
|
158
|
+
result = await handler(task_data)
|
|
159
|
+
await self._send_task_result(
|
|
160
|
+
task_data.task_id,
|
|
161
|
+
TaskStatus.COMPLETED,
|
|
162
|
+
result=result
|
|
163
|
+
)
|
|
164
|
+
self.stats.tasks_completed += 1
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.error(f"Task execution failed: {e}")
|
|
168
|
+
await self._send_task_result(
|
|
169
|
+
task_data.task_id,
|
|
170
|
+
TaskStatus.FAILED,
|
|
171
|
+
error=str(e)
|
|
172
|
+
)
|
|
173
|
+
self.stats.tasks_failed += 1
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Task assignment handling failed: {e}")
|
|
177
|
+
|
|
178
|
+
async def _send_task_result(self, task_id: str, status: TaskStatus, result: Any = None, error: str = None):
|
|
179
|
+
"""Send task result back."""
|
|
180
|
+
try:
|
|
181
|
+
result_data = TaskResultData(
|
|
182
|
+
task_id=task_id,
|
|
183
|
+
driver_id=self.driver_id,
|
|
184
|
+
status=status,
|
|
185
|
+
result=result,
|
|
186
|
+
error_message=error,
|
|
187
|
+
completed_at=utc_now()
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if self.websocket_client:
|
|
191
|
+
self.websocket_client.send(result_data)
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Failed to send task result: {e}")
|
|
195
|
+
|
|
196
|
+
def get_status(self) -> Dict[str, Any]:
|
|
197
|
+
"""Get session status."""
|
|
198
|
+
return {
|
|
199
|
+
"driver_id": self.driver_id,
|
|
200
|
+
"status": self.status.value,
|
|
201
|
+
"stats": self.stats.model_dump(),
|
|
202
|
+
"handlers": list(self.task_handlers.keys())
|
|
203
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean WebSocket client with auto-reconnection and message queuing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional, Callable, List
|
|
9
|
+
from collections import deque
|
|
10
|
+
|
|
11
|
+
import websockets
|
|
12
|
+
from websockets.asyncio.client import ClientConnection
|
|
13
|
+
|
|
14
|
+
from unrealon_core.models.websocket import (
|
|
15
|
+
DriverRegistrationMessage,
|
|
16
|
+
DriverRegistrationData,
|
|
17
|
+
WebSocketMessage
|
|
18
|
+
)
|
|
19
|
+
from unrealon_core.enums.types import MessageType
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WebSocketClient:
|
|
25
|
+
"""
|
|
26
|
+
Clean WebSocket client with robust connection management.
|
|
27
|
+
|
|
28
|
+
Features:
|
|
29
|
+
- Auto-reconnection with exponential backoff
|
|
30
|
+
- Message queuing during disconnections
|
|
31
|
+
- Background message processing
|
|
32
|
+
- Clean error handling
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, websocket_url: str, driver_id: str):
|
|
36
|
+
self.websocket_url = websocket_url
|
|
37
|
+
self.driver_id = driver_id
|
|
38
|
+
self.websocket: Optional[ClientConnection] = None
|
|
39
|
+
self.connected = False
|
|
40
|
+
self.running = False
|
|
41
|
+
|
|
42
|
+
# Message handling
|
|
43
|
+
self.on_message: Optional[Callable[[WebSocketMessage], None]] = None
|
|
44
|
+
self.message_queue: deque = deque()
|
|
45
|
+
|
|
46
|
+
# Background tasks
|
|
47
|
+
self._sender_task: Optional[asyncio.Task] = None
|
|
48
|
+
self._receiver_task: Optional[asyncio.Task] = None
|
|
49
|
+
self._monitor_task: Optional[asyncio.Task] = None
|
|
50
|
+
|
|
51
|
+
# Reconnection settings
|
|
52
|
+
self.reconnect_delay = 1.0
|
|
53
|
+
self.max_reconnect_delay = 60.0
|
|
54
|
+
self.reconnect_attempts = 0
|
|
55
|
+
|
|
56
|
+
async def connect(self) -> bool:
|
|
57
|
+
"""Connect and start background tasks."""
|
|
58
|
+
if self.running:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
self.running = True
|
|
62
|
+
|
|
63
|
+
# Start background tasks
|
|
64
|
+
self._sender_task = asyncio.create_task(self._message_sender())
|
|
65
|
+
self._receiver_task = asyncio.create_task(self._message_receiver())
|
|
66
|
+
self._monitor_task = asyncio.create_task(self._connection_monitor())
|
|
67
|
+
|
|
68
|
+
return await self._establish_connection()
|
|
69
|
+
|
|
70
|
+
async def disconnect(self):
|
|
71
|
+
"""Clean disconnect and stop all tasks."""
|
|
72
|
+
self.running = False
|
|
73
|
+
|
|
74
|
+
# Cancel background tasks
|
|
75
|
+
for task in [self._sender_task, self._receiver_task, self._monitor_task]:
|
|
76
|
+
if task and not task.done():
|
|
77
|
+
task.cancel()
|
|
78
|
+
try:
|
|
79
|
+
await task
|
|
80
|
+
except asyncio.CancelledError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Close WebSocket
|
|
84
|
+
if self.websocket:
|
|
85
|
+
await self.websocket.close()
|
|
86
|
+
self.websocket = None
|
|
87
|
+
|
|
88
|
+
self.connected = False
|
|
89
|
+
logger.info("WebSocket client stopped")
|
|
90
|
+
|
|
91
|
+
async def _establish_connection(self) -> bool:
|
|
92
|
+
"""Establish WebSocket connection."""
|
|
93
|
+
try:
|
|
94
|
+
logger.info(f"Connecting to WebSocket: {self.websocket_url}")
|
|
95
|
+
self.websocket = await websockets.connect(self.websocket_url)
|
|
96
|
+
self.connected = True
|
|
97
|
+
self.reconnect_attempts = 0
|
|
98
|
+
self.reconnect_delay = 1.0
|
|
99
|
+
logger.info("WebSocket connected successfully")
|
|
100
|
+
return True
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"WebSocket connection failed: {e}")
|
|
103
|
+
self.connected = False
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
async def _connection_monitor(self):
|
|
107
|
+
"""Monitor connection and handle reconnection."""
|
|
108
|
+
while self.running:
|
|
109
|
+
if not self.connected and self.running:
|
|
110
|
+
logger.info(f"Attempting reconnection (attempt {self.reconnect_attempts + 1})")
|
|
111
|
+
|
|
112
|
+
if await self._establish_connection():
|
|
113
|
+
logger.info("Reconnection successful")
|
|
114
|
+
else:
|
|
115
|
+
self.reconnect_attempts += 1
|
|
116
|
+
# Exponential backoff
|
|
117
|
+
self.reconnect_delay = min(
|
|
118
|
+
self.reconnect_delay * 2,
|
|
119
|
+
self.max_reconnect_delay
|
|
120
|
+
)
|
|
121
|
+
await asyncio.sleep(self.reconnect_delay)
|
|
122
|
+
else:
|
|
123
|
+
await asyncio.sleep(5.0) # Check every 5 seconds
|
|
124
|
+
|
|
125
|
+
async def _message_sender(self):
|
|
126
|
+
"""Background task to send queued messages."""
|
|
127
|
+
while self.running:
|
|
128
|
+
if self.connected and self.websocket and self.message_queue:
|
|
129
|
+
try:
|
|
130
|
+
message = self.message_queue.popleft()
|
|
131
|
+
await self.websocket.send(message)
|
|
132
|
+
except (websockets.exceptions.ConnectionClosed, ConnectionResetError):
|
|
133
|
+
self.connected = False
|
|
134
|
+
# Put message back in queue
|
|
135
|
+
self.message_queue.appendleft(message)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Error sending message: {e}")
|
|
138
|
+
else:
|
|
139
|
+
await asyncio.sleep(0.1)
|
|
140
|
+
|
|
141
|
+
async def _message_receiver(self):
|
|
142
|
+
"""Background task to receive messages."""
|
|
143
|
+
while self.running:
|
|
144
|
+
if self.connected and self.websocket:
|
|
145
|
+
try:
|
|
146
|
+
message_str = await self.websocket.recv()
|
|
147
|
+
|
|
148
|
+
# Parse and validate message
|
|
149
|
+
message_data = json.loads(message_str)
|
|
150
|
+
message = WebSocketMessage.model_validate(message_data)
|
|
151
|
+
|
|
152
|
+
# Handle message
|
|
153
|
+
if self.on_message:
|
|
154
|
+
self.on_message(message)
|
|
155
|
+
|
|
156
|
+
except (websockets.exceptions.ConnectionClosed, ConnectionResetError):
|
|
157
|
+
self.connected = False
|
|
158
|
+
logger.warning("WebSocket connection lost")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Error receiving message: {e}")
|
|
161
|
+
else:
|
|
162
|
+
await asyncio.sleep(0.1)
|
|
163
|
+
|
|
164
|
+
def send(self, message_data) -> None:
|
|
165
|
+
"""Queue message for sending."""
|
|
166
|
+
if hasattr(message_data, 'model_dump_json'):
|
|
167
|
+
# Pydantic model
|
|
168
|
+
message_json = message_data.model_dump_json()
|
|
169
|
+
else:
|
|
170
|
+
# Raw data
|
|
171
|
+
message_json = json.dumps(message_data)
|
|
172
|
+
|
|
173
|
+
self.message_queue.append(message_json)
|
|
174
|
+
|
|
175
|
+
async def register_driver(self, capabilities: List[str]) -> bool:
|
|
176
|
+
"""Register driver with capabilities."""
|
|
177
|
+
try:
|
|
178
|
+
# Create registration data
|
|
179
|
+
registration_data = DriverRegistrationData(
|
|
180
|
+
driver_id=self.driver_id,
|
|
181
|
+
driver_name=self.driver_id,
|
|
182
|
+
driver_type="universal",
|
|
183
|
+
capabilities=capabilities
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Create registration message with correct type
|
|
187
|
+
registration_message = DriverRegistrationMessage(data=registration_data)
|
|
188
|
+
|
|
189
|
+
# Queue for sending
|
|
190
|
+
self.send(registration_message)
|
|
191
|
+
|
|
192
|
+
logger.info(f"Driver registration queued: {self.driver_id}")
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Driver registration failed: {e}")
|
|
197
|
+
return False
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean driver configuration without hardcoded values.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pydantic import BaseModel, Field, computed_field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DriverMode(str, Enum):
|
|
12
|
+
"""Driver operation modes."""
|
|
13
|
+
STANDALONE = "standalone"
|
|
14
|
+
DAEMON = "daemon"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DriverConfig(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Clean driver configuration.
|
|
20
|
+
No hardcoded presets - user configures everything explicitly.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Basic settings
|
|
24
|
+
name: str = Field(..., description="Driver name")
|
|
25
|
+
mode: DriverMode = Field(default=DriverMode.STANDALONE, description="Operation mode")
|
|
26
|
+
|
|
27
|
+
# WebSocket connection (auto-detected)
|
|
28
|
+
websocket_url: Optional[str] = Field(default=None, description="Manual WebSocket URL override")
|
|
29
|
+
websocket_timeout: int = Field(default=30, description="WebSocket timeout seconds")
|
|
30
|
+
|
|
31
|
+
@computed_field
|
|
32
|
+
@property
|
|
33
|
+
def effective_websocket_url(self) -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Auto-detect WebSocket URL from multiple sources.
|
|
36
|
+
|
|
37
|
+
Priority:
|
|
38
|
+
1. Explicit websocket_url field
|
|
39
|
+
2. Environment variable UNREALON_WEBSOCKET_URL
|
|
40
|
+
3. Environment variable UNREALON_WS_URL
|
|
41
|
+
4. Default localhost for development
|
|
42
|
+
"""
|
|
43
|
+
# 1. Explicit override
|
|
44
|
+
if self.websocket_url:
|
|
45
|
+
return self.websocket_url
|
|
46
|
+
|
|
47
|
+
# 2. Environment variables
|
|
48
|
+
env_url = (
|
|
49
|
+
os.getenv('UNREALON_WEBSOCKET_URL') or
|
|
50
|
+
os.getenv('UNREALON_WS_URL') or
|
|
51
|
+
os.getenv('WS_URL')
|
|
52
|
+
)
|
|
53
|
+
if env_url:
|
|
54
|
+
return env_url
|
|
55
|
+
|
|
56
|
+
# 3. No default - return None if nothing is configured
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Logging
|
|
60
|
+
log_level: str = Field(default="INFO", description="Logging level")
|
|
61
|
+
log_file: Optional[str] = Field(default=None, description="Log file path")
|
|
62
|
+
|
|
63
|
+
# HTTP settings
|
|
64
|
+
http_timeout: int = Field(default=30, description="HTTP timeout seconds")
|
|
65
|
+
max_retries: int = Field(default=3, description="Max HTTP retries")
|
|
66
|
+
|
|
67
|
+
# Browser settings
|
|
68
|
+
browser_headless: bool = Field(default=True, description="Run browser headless")
|
|
69
|
+
browser_timeout: int = Field(default=30, description="Browser timeout seconds")
|
|
70
|
+
|
|
71
|
+
# Proxy settings
|
|
72
|
+
proxy_enabled: bool = Field(default=False, description="Enable proxy rotation")
|
|
73
|
+
proxy_rotation_interval: int = Field(default=300, description="Proxy rotation seconds")
|
|
74
|
+
|
|
75
|
+
# Cache settings
|
|
76
|
+
cache_enabled: bool = Field(default=True, description="Enable response caching")
|
|
77
|
+
cache_ttl: int = Field(default=3600, description="Cache TTL seconds")
|
|
78
|
+
|
|
79
|
+
# Threading
|
|
80
|
+
max_workers: int = Field(default=4, description="Max thread workers")
|
|
81
|
+
|
|
82
|
+
# Performance
|
|
83
|
+
batch_size: int = Field(default=10, description="Batch processing size")
|
|
84
|
+
|
|
85
|
+
model_config = {"extra": "forbid"}
|