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,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for driver components.
|
|
3
|
+
|
|
4
|
+
Provides convenient logging methods and utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from unrealon_driver.managers.logger import LoggerManager
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoggingUtility:
|
|
17
|
+
"""Utility class for driver logging operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, driver_id: str):
|
|
20
|
+
"""Initialize logging utility."""
|
|
21
|
+
self.driver_id = driver_id
|
|
22
|
+
self.logger_manager: Optional['LoggerManager'] = None
|
|
23
|
+
|
|
24
|
+
def log(self, level: str, message: str, context: Optional[Dict[str, Any]] = None):
|
|
25
|
+
"""Log message through logger manager."""
|
|
26
|
+
if self.logger_manager:
|
|
27
|
+
self.logger_manager.log(level, message, context)
|
|
28
|
+
else:
|
|
29
|
+
# Fallback to standard logging
|
|
30
|
+
log_func = getattr(logger, level.lower(), logger.info)
|
|
31
|
+
log_func(f"{message} | Context: {context}" if context else message)
|
|
32
|
+
|
|
33
|
+
def debug(self, message: str, context: Optional[Dict[str, Any]] = None):
|
|
34
|
+
"""Log debug message."""
|
|
35
|
+
self.log("DEBUG", message, context)
|
|
36
|
+
|
|
37
|
+
def info(self, message: str, context: Optional[Dict[str, Any]] = None):
|
|
38
|
+
"""Log info message."""
|
|
39
|
+
self.log("INFO", message, context)
|
|
40
|
+
|
|
41
|
+
def warning(self, message: str, context: Optional[Dict[str, Any]] = None):
|
|
42
|
+
"""Log warning message."""
|
|
43
|
+
self.log("WARNING", message, context)
|
|
44
|
+
|
|
45
|
+
def error(self, message: str, context: Optional[Dict[str, Any]] = None):
|
|
46
|
+
"""Log error message."""
|
|
47
|
+
self.log("ERROR", message, context)
|
|
48
|
+
|
|
49
|
+
def critical(self, message: str, context: Optional[Dict[str, Any]] = None):
|
|
50
|
+
"""Log critical message."""
|
|
51
|
+
self.log("CRITICAL", message, context)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serialization utilities for driver components.
|
|
3
|
+
|
|
4
|
+
Handles result saving and JSON serialization with Pydantic support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SerializationUtility:
|
|
17
|
+
"""Utility class for serialization and result saving."""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def save_results_to_file(data: dict, filename: str, results_dir: Optional[str] = None) -> Path:
|
|
21
|
+
"""
|
|
22
|
+
Save parsing results to JSON file with automatic serialization.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: Data to save (can contain Pydantic models)
|
|
26
|
+
filename: Base filename (without extension)
|
|
27
|
+
results_dir: Directory to save to (default: ./data/results)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to saved file
|
|
31
|
+
"""
|
|
32
|
+
if results_dir is None:
|
|
33
|
+
results_dir = "./data/results"
|
|
34
|
+
|
|
35
|
+
results_path = Path(results_dir)
|
|
36
|
+
results_path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
39
|
+
filepath = results_path / f"{filename}_{timestamp}.json"
|
|
40
|
+
|
|
41
|
+
# Convert Pydantic models to dict for JSON serialization
|
|
42
|
+
serializable_data = SerializationUtility._serialize_for_json(data)
|
|
43
|
+
|
|
44
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
45
|
+
json.dump(serializable_data, f, indent=2, ensure_ascii=False, default=str)
|
|
46
|
+
|
|
47
|
+
logger.info(f"Results saved to: {filepath}")
|
|
48
|
+
return filepath
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _serialize_for_json(data: Any) -> Any:
|
|
52
|
+
"""Recursively serialize data for JSON, handling Pydantic models."""
|
|
53
|
+
if hasattr(data, "model_dump"):
|
|
54
|
+
# Pydantic v2 model
|
|
55
|
+
return data.model_dump()
|
|
56
|
+
elif isinstance(data, dict):
|
|
57
|
+
return {key: SerializationUtility._serialize_for_json(value) for key, value in data.items()}
|
|
58
|
+
elif isinstance(data, list):
|
|
59
|
+
return [SerializationUtility._serialize_for_json(item) for item in data]
|
|
60
|
+
else:
|
|
61
|
+
return data
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean manager system for UnrealOn Driver.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import BaseManager, ManagerConfig, ManagerStatus
|
|
6
|
+
from .logger import LoggerManager, LoggerManagerConfig
|
|
7
|
+
from .http import HttpManager, HttpManagerConfig
|
|
8
|
+
from .browser import BrowserManager, BrowserManagerConfig
|
|
9
|
+
from .cache import CacheManager, CacheManagerConfig
|
|
10
|
+
from .proxy import ProxyManager, ProxyManagerConfig
|
|
11
|
+
from .threading import ThreadManager, ThreadManagerConfig
|
|
12
|
+
from .update import UpdateManager, UpdateManagerConfig
|
|
13
|
+
from .registry import ManagerRegistry
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# Base
|
|
17
|
+
"BaseManager",
|
|
18
|
+
"ManagerConfig",
|
|
19
|
+
"ManagerStatus",
|
|
20
|
+
|
|
21
|
+
# Managers
|
|
22
|
+
"LoggerManager", "LoggerManagerConfig",
|
|
23
|
+
"HttpManager", "HttpManagerConfig",
|
|
24
|
+
"BrowserManager", "BrowserManagerConfig",
|
|
25
|
+
"CacheManager", "CacheManagerConfig",
|
|
26
|
+
"ProxyManager", "ProxyManagerConfig",
|
|
27
|
+
"ThreadManager", "ThreadManagerConfig",
|
|
28
|
+
"UpdateManager", "UpdateManagerConfig",
|
|
29
|
+
|
|
30
|
+
# Registry
|
|
31
|
+
"ManagerRegistry",
|
|
32
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean base manager system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Optional, Dict, Any
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from ..utils.time import utc_now
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ManagerStatus(str, Enum):
|
|
20
|
+
"""Manager lifecycle status."""
|
|
21
|
+
UNINITIALIZED = "uninitialized"
|
|
22
|
+
INITIALIZING = "initializing"
|
|
23
|
+
READY = "ready"
|
|
24
|
+
ERROR = "error"
|
|
25
|
+
SHUTTING_DOWN = "shutting_down"
|
|
26
|
+
SHUTDOWN = "shutdown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ManagerConfig(BaseModel):
|
|
30
|
+
"""Base configuration for all managers."""
|
|
31
|
+
enabled: bool = Field(default=True, description="Whether manager is enabled")
|
|
32
|
+
timeout: int = Field(default=30, description="Operation timeout seconds")
|
|
33
|
+
max_retries: int = Field(default=3, description="Max retry attempts")
|
|
34
|
+
log_level: str = Field(default="INFO", description="Logging level")
|
|
35
|
+
|
|
36
|
+
model_config = {"extra": "forbid"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ManagerStats(BaseModel):
|
|
40
|
+
"""Manager operation statistics."""
|
|
41
|
+
operations_total: int = 0
|
|
42
|
+
operations_successful: int = 0
|
|
43
|
+
operations_failed: int = 0
|
|
44
|
+
last_operation: Optional[datetime] = None
|
|
45
|
+
average_duration: float = 0.0
|
|
46
|
+
|
|
47
|
+
def record_operation(self, success: bool, duration: float):
|
|
48
|
+
"""Record operation result."""
|
|
49
|
+
self.operations_total += 1
|
|
50
|
+
if success:
|
|
51
|
+
self.operations_successful += 1
|
|
52
|
+
else:
|
|
53
|
+
self.operations_failed += 1
|
|
54
|
+
|
|
55
|
+
self.last_operation = utc_now()
|
|
56
|
+
|
|
57
|
+
# Simple moving average
|
|
58
|
+
if self.operations_total == 1:
|
|
59
|
+
self.average_duration = duration
|
|
60
|
+
else:
|
|
61
|
+
self.average_duration = (
|
|
62
|
+
(self.average_duration * (self.operations_total - 1) + duration)
|
|
63
|
+
/ self.operations_total
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def get_success_rate(self) -> float:
|
|
67
|
+
"""Get success rate percentage."""
|
|
68
|
+
if self.operations_total == 0:
|
|
69
|
+
return 0.0
|
|
70
|
+
return (self.operations_successful / self.operations_total) * 100.0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class BaseManager(ABC):
|
|
74
|
+
"""
|
|
75
|
+
Clean base manager class.
|
|
76
|
+
|
|
77
|
+
Provides common functionality for all managers:
|
|
78
|
+
- Lifecycle management
|
|
79
|
+
- Statistics tracking
|
|
80
|
+
- Error handling
|
|
81
|
+
- Health checks
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: ManagerConfig, name: str):
|
|
85
|
+
self.config = config
|
|
86
|
+
self.name = name
|
|
87
|
+
self.status = ManagerStatus.UNINITIALIZED
|
|
88
|
+
self.stats = ManagerStats()
|
|
89
|
+
self.logger = logging.getLogger(f"{__name__}.{name}")
|
|
90
|
+
|
|
91
|
+
# Set log level
|
|
92
|
+
if hasattr(logging, config.log_level):
|
|
93
|
+
self.logger.setLevel(getattr(logging, config.log_level))
|
|
94
|
+
|
|
95
|
+
async def initialize(self) -> bool:
|
|
96
|
+
"""Initialize manager."""
|
|
97
|
+
if not self.config.enabled:
|
|
98
|
+
self.logger.info(f"Manager {self.name} is disabled")
|
|
99
|
+
self.status = ManagerStatus.SHUTDOWN
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
self.status = ManagerStatus.INITIALIZING
|
|
104
|
+
self.logger.info(f"Initializing manager: {self.name}")
|
|
105
|
+
|
|
106
|
+
success = await self._initialize()
|
|
107
|
+
|
|
108
|
+
if success:
|
|
109
|
+
self.status = ManagerStatus.READY
|
|
110
|
+
self.logger.info(f"Manager {self.name} initialized successfully")
|
|
111
|
+
else:
|
|
112
|
+
self.status = ManagerStatus.ERROR
|
|
113
|
+
self.logger.error(f"Manager {self.name} initialization failed")
|
|
114
|
+
|
|
115
|
+
return success
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
self.status = ManagerStatus.ERROR
|
|
119
|
+
self.logger.error(f"Manager {self.name} initialization error: {e}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
async def shutdown(self):
|
|
123
|
+
"""Shutdown manager."""
|
|
124
|
+
try:
|
|
125
|
+
self.status = ManagerStatus.SHUTTING_DOWN
|
|
126
|
+
self.logger.info(f"Shutting down manager: {self.name}")
|
|
127
|
+
|
|
128
|
+
await self._shutdown()
|
|
129
|
+
|
|
130
|
+
self.status = ManagerStatus.SHUTDOWN
|
|
131
|
+
self.logger.info(f"Manager {self.name} shutdown complete")
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.error(f"Manager {self.name} shutdown error: {e}")
|
|
135
|
+
self.status = ManagerStatus.ERROR
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
async def _initialize(self) -> bool:
|
|
139
|
+
"""Manager-specific initialization."""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
async def _shutdown(self):
|
|
144
|
+
"""Manager-specific shutdown."""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
148
|
+
"""Perform health check."""
|
|
149
|
+
try:
|
|
150
|
+
health_data = await self._health_check()
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"name": self.name,
|
|
154
|
+
"status": self.status.value,
|
|
155
|
+
"enabled": self.config.enabled,
|
|
156
|
+
"stats": self.stats.model_dump(),
|
|
157
|
+
"health": health_data
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.logger.error(f"Health check failed for {self.name}: {e}")
|
|
162
|
+
return {
|
|
163
|
+
"name": self.name,
|
|
164
|
+
"status": "error",
|
|
165
|
+
"error": str(e)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async def _health_check(self) -> Dict[str, Any]:
|
|
169
|
+
"""Manager-specific health check."""
|
|
170
|
+
return {"status": "ok"}
|
|
171
|
+
|
|
172
|
+
def is_ready(self) -> bool:
|
|
173
|
+
"""Check if manager is ready."""
|
|
174
|
+
return self.status == ManagerStatus.READY
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean browser manager using unrealon_browser.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from unrealon_browser import BrowserManager as CoreBrowserManager, BrowserConfig
|
|
9
|
+
from .base import BaseManager, ManagerConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BrowserManagerConfig(ManagerConfig):
|
|
13
|
+
"""Browser manager configuration."""
|
|
14
|
+
|
|
15
|
+
headless: bool = Field(default=True, description="Run headless")
|
|
16
|
+
parser_name: str = Field(..., description="Parser name for browser")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BrowserManager(BaseManager):
|
|
20
|
+
"""Clean browser manager wrapper."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: BrowserManagerConfig):
|
|
23
|
+
super().__init__(config, "browser")
|
|
24
|
+
self.config: BrowserManagerConfig = config
|
|
25
|
+
self.browser: Optional[CoreBrowserManager] = None
|
|
26
|
+
|
|
27
|
+
async def _initialize(self) -> bool:
|
|
28
|
+
"""Initialize browser manager (but not the actual browser yet)."""
|
|
29
|
+
try:
|
|
30
|
+
# Don't initialize the actual browser here - do it lazily on first use
|
|
31
|
+
# This prevents browser from starting in daemon mode until needed
|
|
32
|
+
self.logger.info("Browser manager ready (browser will start on first use)")
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
except Exception as e:
|
|
36
|
+
self.logger.error(f"Browser manager initialization failed: {e}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
async def _shutdown(self):
|
|
40
|
+
"""Shutdown browser."""
|
|
41
|
+
if self.browser:
|
|
42
|
+
await self.browser.close_async()
|
|
43
|
+
self.browser = None
|
|
44
|
+
|
|
45
|
+
async def _ensure_browser_initialized(self) -> bool:
|
|
46
|
+
"""Ensure browser is initialized (lazy initialization)."""
|
|
47
|
+
if self.browser is not None:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
self.logger.info("🚀 Starting browser (lazy initialization)...")
|
|
52
|
+
|
|
53
|
+
# Create browser config
|
|
54
|
+
browser_config = BrowserConfig(parser_name=self.config.parser_name)
|
|
55
|
+
|
|
56
|
+
# Create browser manager
|
|
57
|
+
self.browser = CoreBrowserManager(browser_config)
|
|
58
|
+
|
|
59
|
+
# Initialize browser
|
|
60
|
+
await self.browser.initialize_async()
|
|
61
|
+
|
|
62
|
+
self.logger.info("✅ Browser started successfully")
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
self.logger.error(f"❌ Browser initialization failed: {e}")
|
|
67
|
+
self.browser = None
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
async def navigate(self, url: str) -> bool:
|
|
71
|
+
"""Navigate to URL (with lazy browser initialization)."""
|
|
72
|
+
# Ensure browser is initialized
|
|
73
|
+
if not await self._ensure_browser_initialized():
|
|
74
|
+
raise RuntimeError("Failed to initialize browser")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
await self.browser.navigate_async(url)
|
|
78
|
+
self.stats.record_operation(True, 0.0)
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
self.logger.error(f"Navigation failed: {e}")
|
|
82
|
+
self.stats.record_operation(False, 0.0)
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
async def get_html(self) -> Optional[str]:
|
|
86
|
+
"""Get page HTML (with lazy browser initialization)."""
|
|
87
|
+
# Ensure browser is initialized
|
|
88
|
+
if not await self._ensure_browser_initialized():
|
|
89
|
+
raise RuntimeError("Failed to initialize browser")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
html = await self.browser.get_page_content_async()
|
|
93
|
+
self.stats.record_operation(True, 0.0)
|
|
94
|
+
return html
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self.logger.error(f"Get HTML failed: {e}")
|
|
97
|
+
self.stats.record_operation(False, 0.0)
|
|
98
|
+
return None
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean cache manager.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, Optional, Dict
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from ..utils.time import utc_now
|
|
11
|
+
from .base import BaseManager, ManagerConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CacheManagerConfig(ManagerConfig):
|
|
15
|
+
"""Cache manager configuration."""
|
|
16
|
+
default_ttl: int = Field(default=3600, description="Default TTL seconds")
|
|
17
|
+
max_size: int = Field(default=1000, description="Max cache entries")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CacheEntry:
|
|
21
|
+
"""Cache entry with TTL."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, value: Any, ttl: int):
|
|
24
|
+
self.value = value
|
|
25
|
+
self.expires_at = utc_now() + timedelta(seconds=ttl)
|
|
26
|
+
|
|
27
|
+
def is_expired(self) -> bool:
|
|
28
|
+
"""Check if entry is expired."""
|
|
29
|
+
return utc_now() > self.expires_at
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CacheManager(BaseManager):
|
|
33
|
+
"""Simple in-memory cache manager."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: CacheManagerConfig):
|
|
36
|
+
super().__init__(config, "cache")
|
|
37
|
+
self.config: CacheManagerConfig = config
|
|
38
|
+
self._cache: Dict[str, CacheEntry] = {}
|
|
39
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
40
|
+
|
|
41
|
+
async def _initialize(self) -> bool:
|
|
42
|
+
"""Initialize cache."""
|
|
43
|
+
# Start cleanup task
|
|
44
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_expired())
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
async def _shutdown(self):
|
|
48
|
+
"""Shutdown cache."""
|
|
49
|
+
if self._cleanup_task:
|
|
50
|
+
self._cleanup_task.cancel()
|
|
51
|
+
try:
|
|
52
|
+
await self._cleanup_task
|
|
53
|
+
except asyncio.CancelledError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
self._cache.clear()
|
|
57
|
+
|
|
58
|
+
async def _cleanup_expired(self):
|
|
59
|
+
"""Background task to clean expired entries."""
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
await asyncio.sleep(60) # Cleanup every minute
|
|
63
|
+
|
|
64
|
+
expired_keys = []
|
|
65
|
+
for key, entry in self._cache.items():
|
|
66
|
+
if entry.is_expired():
|
|
67
|
+
expired_keys.append(key)
|
|
68
|
+
|
|
69
|
+
for key in expired_keys:
|
|
70
|
+
del self._cache[key]
|
|
71
|
+
|
|
72
|
+
except asyncio.CancelledError:
|
|
73
|
+
break
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.error(f"Cache cleanup error: {e}")
|
|
76
|
+
|
|
77
|
+
def get(self, key: str) -> Optional[Any]:
|
|
78
|
+
"""Get value from cache."""
|
|
79
|
+
entry = self._cache.get(key)
|
|
80
|
+
if not entry:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if entry.is_expired():
|
|
84
|
+
del self._cache[key]
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
return entry.value
|
|
88
|
+
|
|
89
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
90
|
+
"""Set value in cache."""
|
|
91
|
+
if len(self._cache) >= self.config.max_size:
|
|
92
|
+
# Remove oldest entry
|
|
93
|
+
oldest_key = next(iter(self._cache))
|
|
94
|
+
del self._cache[oldest_key]
|
|
95
|
+
|
|
96
|
+
ttl = ttl or self.config.default_ttl
|
|
97
|
+
self._cache[key] = CacheEntry(value, ttl)
|
|
98
|
+
|
|
99
|
+
def delete(self, key: str) -> bool:
|
|
100
|
+
"""Delete key from cache."""
|
|
101
|
+
if key in self._cache:
|
|
102
|
+
del self._cache[key]
|
|
103
|
+
return True
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def clear(self) -> None:
|
|
107
|
+
"""Clear all cache entries."""
|
|
108
|
+
self._cache.clear()
|
|
109
|
+
|
|
110
|
+
async def _health_check(self) -> Dict[str, Any]:
|
|
111
|
+
"""Cache health check."""
|
|
112
|
+
return {
|
|
113
|
+
"status": "ok",
|
|
114
|
+
"entries": len(self._cache),
|
|
115
|
+
"max_size": self.config.max_size
|
|
116
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean HTTP manager for requests.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import aiohttp
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .base import BaseManager, ManagerConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HttpManagerConfig(ManagerConfig):
|
|
14
|
+
"""HTTP manager configuration."""
|
|
15
|
+
user_agent: str = Field(default="UnrealOn-Driver/1.0", description="User agent string")
|
|
16
|
+
max_connections: int = Field(default=100, description="Max concurrent connections")
|
|
17
|
+
connector_limit: int = Field(default=30, description="Connector limit per host")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpManager(BaseManager):
|
|
21
|
+
"""Clean HTTP manager with aiohttp."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: HttpManagerConfig):
|
|
24
|
+
super().__init__(config, "http")
|
|
25
|
+
self.config: HttpManagerConfig = config
|
|
26
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
27
|
+
|
|
28
|
+
async def _initialize(self) -> bool:
|
|
29
|
+
"""Initialize HTTP session."""
|
|
30
|
+
try:
|
|
31
|
+
# Create connector
|
|
32
|
+
connector = aiohttp.TCPConnector(
|
|
33
|
+
limit=self.config.max_connections,
|
|
34
|
+
limit_per_host=self.config.connector_limit,
|
|
35
|
+
ttl_dns_cache=300,
|
|
36
|
+
use_dns_cache=True
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create session
|
|
40
|
+
timeout = aiohttp.ClientTimeout(total=self.config.timeout)
|
|
41
|
+
headers = {"User-Agent": self.config.user_agent}
|
|
42
|
+
|
|
43
|
+
self.session = aiohttp.ClientSession(
|
|
44
|
+
connector=connector,
|
|
45
|
+
timeout=timeout,
|
|
46
|
+
headers=headers
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self.logger.error(f"HTTP manager initialization failed: {e}")
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
async def _shutdown(self):
|
|
56
|
+
"""Shutdown HTTP session."""
|
|
57
|
+
if self.session:
|
|
58
|
+
await self.session.close()
|
|
59
|
+
self.session = None
|
|
60
|
+
|
|
61
|
+
async def get(self, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
62
|
+
"""Make GET request."""
|
|
63
|
+
if not self.session:
|
|
64
|
+
raise RuntimeError("HTTP manager not initialized")
|
|
65
|
+
|
|
66
|
+
start_time = asyncio.get_event_loop().time()
|
|
67
|
+
success = False
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
response = await self.session.get(url, **kwargs)
|
|
71
|
+
success = True
|
|
72
|
+
return response
|
|
73
|
+
finally:
|
|
74
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
75
|
+
self.stats.record_operation(success, duration)
|
|
76
|
+
|
|
77
|
+
async def post(self, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
78
|
+
"""Make POST request."""
|
|
79
|
+
if not self.session:
|
|
80
|
+
raise RuntimeError("HTTP manager not initialized")
|
|
81
|
+
|
|
82
|
+
start_time = asyncio.get_event_loop().time()
|
|
83
|
+
success = False
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
response = await self.session.post(url, **kwargs)
|
|
87
|
+
success = True
|
|
88
|
+
return response
|
|
89
|
+
finally:
|
|
90
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
91
|
+
self.stats.record_operation(success, duration)
|
|
92
|
+
|
|
93
|
+
async def request(self, method: str, url: str, **kwargs) -> aiohttp.ClientResponse:
|
|
94
|
+
"""Make generic request."""
|
|
95
|
+
if not self.session:
|
|
96
|
+
raise RuntimeError("HTTP manager not initialized")
|
|
97
|
+
|
|
98
|
+
start_time = asyncio.get_event_loop().time()
|
|
99
|
+
success = False
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
response = await self.session.request(method, url, **kwargs)
|
|
103
|
+
success = True
|
|
104
|
+
return response
|
|
105
|
+
finally:
|
|
106
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
107
|
+
self.stats.record_operation(success, duration)
|