unrealon 1.1.1__py3-none-any.whl → 1.1.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.
- unrealon/__init__.py +16 -6
- unrealon-1.1.5.dist-info/METADATA +621 -0
- unrealon-1.1.5.dist-info/RECORD +54 -0
- {unrealon-1.1.1.dist-info → unrealon-1.1.5.dist-info}/entry_points.txt +1 -1
- unrealon_browser/__init__.py +3 -6
- unrealon_browser/core/browser_manager.py +86 -84
- unrealon_browser/dto/models/config.py +2 -0
- unrealon_browser/managers/captcha.py +165 -185
- unrealon_browser/managers/cookies.py +57 -28
- unrealon_browser/managers/logger_bridge.py +94 -34
- unrealon_browser/managers/profile.py +186 -158
- unrealon_browser/managers/stealth.py +58 -47
- unrealon_driver/__init__.py +8 -21
- unrealon_driver/exceptions.py +5 -0
- unrealon_driver/html_analyzer/__init__.py +32 -0
- unrealon_driver/{parser/managers/html.py → html_analyzer/cleaner.py} +330 -405
- unrealon_driver/html_analyzer/config.py +64 -0
- unrealon_driver/html_analyzer/manager.py +247 -0
- unrealon_driver/html_analyzer/models.py +115 -0
- unrealon_driver/html_analyzer/websocket_analyzer.py +157 -0
- unrealon_driver/models/__init__.py +31 -0
- unrealon_driver/models/websocket.py +98 -0
- unrealon_driver/parser/__init__.py +4 -23
- unrealon_driver/parser/cli_manager.py +6 -5
- unrealon_driver/parser/daemon_manager.py +242 -66
- unrealon_driver/parser/managers/__init__.py +0 -21
- unrealon_driver/parser/managers/config.py +15 -3
- unrealon_driver/parser/parser_manager.py +225 -395
- unrealon_driver/smart_logging/__init__.py +24 -0
- unrealon_driver/smart_logging/models.py +44 -0
- unrealon_driver/smart_logging/smart_logger.py +406 -0
- unrealon_driver/smart_logging/unified_logger.py +525 -0
- unrealon_driver/websocket/__init__.py +31 -0
- unrealon_driver/websocket/client.py +249 -0
- unrealon_driver/websocket/config.py +188 -0
- unrealon_driver/websocket/manager.py +90 -0
- unrealon-1.1.1.dist-info/METADATA +0 -722
- unrealon-1.1.1.dist-info/RECORD +0 -82
- unrealon_bridge/__init__.py +0 -114
- unrealon_bridge/cli.py +0 -316
- unrealon_bridge/client/__init__.py +0 -93
- unrealon_bridge/client/base.py +0 -78
- unrealon_bridge/client/commands.py +0 -89
- unrealon_bridge/client/connection.py +0 -90
- unrealon_bridge/client/events.py +0 -65
- unrealon_bridge/client/health.py +0 -38
- unrealon_bridge/client/html_parser.py +0 -146
- unrealon_bridge/client/logging.py +0 -139
- unrealon_bridge/client/proxy.py +0 -70
- unrealon_bridge/client/scheduler.py +0 -450
- unrealon_bridge/client/session.py +0 -70
- unrealon_bridge/configs/__init__.py +0 -14
- unrealon_bridge/configs/bridge_config.py +0 -212
- unrealon_bridge/configs/bridge_config.yaml +0 -39
- unrealon_bridge/models/__init__.py +0 -138
- unrealon_bridge/models/base.py +0 -28
- unrealon_bridge/models/command.py +0 -41
- unrealon_bridge/models/events.py +0 -40
- unrealon_bridge/models/html_parser.py +0 -79
- unrealon_bridge/models/logging.py +0 -55
- unrealon_bridge/models/parser.py +0 -63
- unrealon_bridge/models/proxy.py +0 -41
- unrealon_bridge/models/requests.py +0 -95
- unrealon_bridge/models/responses.py +0 -88
- unrealon_bridge/models/scheduler.py +0 -592
- unrealon_bridge/models/session.py +0 -28
- unrealon_bridge/server/__init__.py +0 -91
- unrealon_bridge/server/base.py +0 -171
- unrealon_bridge/server/handlers/__init__.py +0 -23
- unrealon_bridge/server/handlers/command.py +0 -110
- unrealon_bridge/server/handlers/html_parser.py +0 -139
- unrealon_bridge/server/handlers/logging.py +0 -95
- unrealon_bridge/server/handlers/parser.py +0 -95
- unrealon_bridge/server/handlers/proxy.py +0 -75
- unrealon_bridge/server/handlers/scheduler.py +0 -545
- unrealon_bridge/server/handlers/session.py +0 -66
- unrealon_driver/browser/__init__.py +0 -8
- unrealon_driver/browser/config.py +0 -74
- unrealon_driver/browser/manager.py +0 -416
- unrealon_driver/parser/managers/browser.py +0 -51
- unrealon_driver/parser/managers/logging.py +0 -609
- {unrealon-1.1.1.dist-info → unrealon-1.1.5.dist-info}/WHEEL +0 -0
- {unrealon-1.1.1.dist-info → unrealon-1.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WebSocket message models for daemon communication.
|
|
3
|
+
|
|
4
|
+
Strict Pydantic v2 compliance and type safety.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Any
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageType(str, Enum):
|
|
13
|
+
"""WebSocket message types."""
|
|
14
|
+
REGISTER = "register"
|
|
15
|
+
COMMAND = "command"
|
|
16
|
+
COMMAND_RESPONSE = "command_response"
|
|
17
|
+
STATUS = "status"
|
|
18
|
+
HEARTBEAT = "heartbeat"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BridgeMessageType(str, Enum):
|
|
22
|
+
"""Bridge WebSocket message types."""
|
|
23
|
+
REGISTER = "register"
|
|
24
|
+
RPC_CALL = "rpc_call"
|
|
25
|
+
PUBSUB_PUBLISH = "pubsub_publish"
|
|
26
|
+
HEARTBEAT = "heartbeat"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RegistrationMessage(BaseModel):
|
|
30
|
+
"""Daemon registration message."""
|
|
31
|
+
type: MessageType = Field(default=MessageType.REGISTER)
|
|
32
|
+
parser_id: str = Field(..., min_length=1, description="Parser identifier")
|
|
33
|
+
parser_type: str = Field(default="daemon", description="Parser type")
|
|
34
|
+
version: str = Field(default="1.0.0", description="Parser version")
|
|
35
|
+
capabilities: List[str] = Field(default_factory=lambda: ["parse", "search", "status", "health"])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CommandMessage(BaseModel):
|
|
39
|
+
"""Incoming command message."""
|
|
40
|
+
type: MessageType = Field(default=MessageType.COMMAND)
|
|
41
|
+
command_type: str = Field(..., min_length=1, description="Command type")
|
|
42
|
+
command_id: str = Field(..., min_length=1, description="Command identifier")
|
|
43
|
+
parameters: dict[str, Any] = Field(default_factory=dict, description="Command parameters")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CommandResponseMessage(BaseModel):
|
|
47
|
+
"""Command response message."""
|
|
48
|
+
type: MessageType = Field(default=MessageType.COMMAND_RESPONSE)
|
|
49
|
+
command_id: str = Field(..., min_length=1, description="Command identifier")
|
|
50
|
+
success: bool = Field(..., description="Command success status")
|
|
51
|
+
result_data: Optional[dict[str, Any]] = Field(default=None, description="Command result data")
|
|
52
|
+
error: Optional[str] = Field(default=None, description="Error message if failed")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class StatusMessage(BaseModel):
|
|
56
|
+
"""Daemon status message."""
|
|
57
|
+
type: MessageType = Field(default=MessageType.STATUS)
|
|
58
|
+
parser_id: str = Field(..., min_length=1, description="Parser identifier")
|
|
59
|
+
running: bool = Field(..., description="Daemon running status")
|
|
60
|
+
uptime_seconds: float = Field(..., ge=0, description="Uptime in seconds")
|
|
61
|
+
total_runs: int = Field(..., ge=0, description="Total runs executed")
|
|
62
|
+
successful_runs: int = Field(..., ge=0, description="Successful runs")
|
|
63
|
+
failed_runs: int = Field(..., ge=0, description="Failed runs")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HeartbeatMessage(BaseModel):
|
|
67
|
+
"""Daemon heartbeat message."""
|
|
68
|
+
type: MessageType = Field(default=MessageType.HEARTBEAT)
|
|
69
|
+
parser_id: str = Field(..., min_length=1, description="Parser identifier")
|
|
70
|
+
timestamp: str = Field(..., description="Heartbeat timestamp")
|
|
71
|
+
status: str = Field(default="alive", description="Daemon status")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Bridge message models
|
|
75
|
+
class BridgeRegistrationPayload(BaseModel):
|
|
76
|
+
"""Payload for bridge registration message."""
|
|
77
|
+
client_type: str = Field(default="daemon", description="Client type")
|
|
78
|
+
parser_id: str = Field(..., min_length=1, description="Parser identifier")
|
|
79
|
+
version: str = Field(default="1.0.0", description="Parser version")
|
|
80
|
+
capabilities: List[str] = Field(default_factory=lambda: ["parse", "search", "status", "health"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BridgeMessage(BaseModel):
|
|
84
|
+
"""Bridge WebSocket message format."""
|
|
85
|
+
message_type: BridgeMessageType = Field(..., description="Message type")
|
|
86
|
+
payload: dict[str, Any] = Field(default_factory=dict, description="Message payload")
|
|
87
|
+
message_id: Optional[str] = Field(default=None, description="Message ID")
|
|
88
|
+
api_key: Optional[str] = Field(default=None, description="API key")
|
|
89
|
+
correlation_id: Optional[str] = Field(default=None, description="Correlation ID")
|
|
90
|
+
reply_to: Optional[str] = Field(default=None, description="Reply to address")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BridgeRegistrationMessage(BaseModel):
|
|
94
|
+
"""Bridge registration message."""
|
|
95
|
+
message_type: BridgeMessageType = Field(default=BridgeMessageType.REGISTER)
|
|
96
|
+
payload: BridgeRegistrationPayload = Field(..., description="Registration payload")
|
|
97
|
+
message_id: Optional[str] = Field(default=None, description="Message ID")
|
|
98
|
+
api_key: Optional[str] = Field(default=None, description="API key")
|
|
@@ -7,49 +7,30 @@ Strict Pydantic v2 compliance and type safety
|
|
|
7
7
|
from .parser_manager import ParserManager, ParserManagerConfig, ParserStats, get_parser_manager, quick_parse
|
|
8
8
|
from .daemon_manager import DaemonManager, DaemonStatus
|
|
9
9
|
from .cli_manager import CLIManager
|
|
10
|
-
from .managers import
|
|
11
|
-
ConfigManager, ParserConfig,
|
|
12
|
-
ResultManager, ParseResult, ParseMetrics, OperationStatus,
|
|
13
|
-
ErrorManager, RetryConfig, ErrorInfo, ErrorSeverity,
|
|
14
|
-
LoggingManager, LoggingConfig, LogLevel, LogContext,
|
|
15
|
-
HTMLManager, HTMLCleaningConfig, HTMLCleaningStats,
|
|
16
|
-
BrowserManager, BrowserConfig, BrowserStats
|
|
17
|
-
)
|
|
10
|
+
from .managers import ConfigManager, ParserConfig, ResultManager, ParseResult, ParseMetrics, OperationStatus, ErrorManager, RetryConfig, ErrorInfo, ErrorSeverity
|
|
18
11
|
|
|
19
12
|
__all__ = [
|
|
20
13
|
# Main Parser Manager
|
|
21
14
|
"ParserManager",
|
|
22
|
-
"ParserManagerConfig",
|
|
15
|
+
"ParserManagerConfig",
|
|
23
16
|
"ParserStats",
|
|
24
17
|
"get_parser_manager",
|
|
25
18
|
"quick_parse",
|
|
26
|
-
|
|
27
19
|
# Daemon Manager
|
|
28
20
|
"DaemonManager",
|
|
29
21
|
"DaemonStatus",
|
|
30
|
-
|
|
31
22
|
# CLI Manager
|
|
32
23
|
"CLIManager",
|
|
33
|
-
|
|
34
24
|
# Individual Managers
|
|
35
25
|
"ConfigManager",
|
|
36
26
|
"ParserConfig",
|
|
37
27
|
"ResultManager",
|
|
38
|
-
"ParseResult",
|
|
28
|
+
"ParseResult",
|
|
39
29
|
"ParseMetrics",
|
|
40
30
|
"OperationStatus",
|
|
41
31
|
"ErrorManager",
|
|
42
32
|
"RetryConfig",
|
|
43
33
|
"ErrorInfo",
|
|
44
34
|
"ErrorSeverity",
|
|
45
|
-
|
|
46
|
-
"LoggingConfig",
|
|
47
|
-
"LogLevel",
|
|
48
|
-
"LogContext",
|
|
49
|
-
"HTMLManager",
|
|
50
|
-
"HTMLCleaningConfig",
|
|
51
|
-
"HTMLCleaningStats",
|
|
52
|
-
"BrowserManager",
|
|
53
|
-
"BrowserConfig",
|
|
54
|
-
"BrowserStats"
|
|
35
|
+
|
|
55
36
|
]
|
|
@@ -11,14 +11,16 @@ from typing import List, Optional, Any, Dict
|
|
|
11
11
|
import click
|
|
12
12
|
|
|
13
13
|
from .parser_manager import ParserManager, ParserManagerConfig
|
|
14
|
-
from .managers import ParserConfig
|
|
14
|
+
from .managers import ParserConfig
|
|
15
|
+
from unrealon_browser.dto.models.config import BrowserConfig
|
|
16
|
+
from unrealon_driver.html_analyzer import HTMLCleaningConfig
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class CLIManager(ParserManager):
|
|
18
20
|
"""Base CLI manager with common CLI functionality."""
|
|
19
21
|
|
|
20
22
|
def __init__(self, parser_name: str, parser_type: str, system_dir: str,
|
|
21
|
-
bridge_enabled: bool = False
|
|
23
|
+
bridge_enabled: bool = False):
|
|
22
24
|
# Create parser config
|
|
23
25
|
parser_config = ParserConfig(
|
|
24
26
|
parser_name=parser_name,
|
|
@@ -27,7 +29,7 @@ class CLIManager(ParserManager):
|
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
# Create logging config
|
|
30
|
-
|
|
32
|
+
# Logging config is now handled internally by ParserManagerConfig
|
|
31
33
|
|
|
32
34
|
# Create other configs
|
|
33
35
|
html_config = HTMLCleaningConfig()
|
|
@@ -36,7 +38,6 @@ class CLIManager(ParserManager):
|
|
|
36
38
|
# Create manager config
|
|
37
39
|
manager_config = ParserManagerConfig(
|
|
38
40
|
parser_config=parser_config,
|
|
39
|
-
logging_config=logging_config,
|
|
40
41
|
html_config=html_config,
|
|
41
42
|
browser_config=browser_config,
|
|
42
43
|
bridge_enabled=bridge_enabled
|
|
@@ -122,7 +123,7 @@ class CLIManager(ParserManager):
|
|
|
122
123
|
click.echo(f"System Dir: {self.config.system_dir}")
|
|
123
124
|
click.echo(f"Bridge: {'Enabled' if self.config.bridge_enabled else 'Disabled'}")
|
|
124
125
|
if self.config.bridge_enabled:
|
|
125
|
-
click.echo(f" URL: {self.config.websocket_url}")
|
|
126
|
+
click.echo(f" URL: {self.config.parser_config.websocket_url} (auto-detected)")
|
|
126
127
|
|
|
127
128
|
@staticmethod
|
|
128
129
|
def create_config_file(config_path: Path, create_func) -> None:
|
|
@@ -9,17 +9,24 @@ import signal
|
|
|
9
9
|
import time
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Optional,
|
|
12
|
+
from typing import Optional, Callable, Awaitable
|
|
13
13
|
from pydantic import BaseModel, Field
|
|
14
14
|
|
|
15
15
|
from .parser_manager import ParserManager, ParserManagerConfig
|
|
16
|
-
from .managers import ParserConfig
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
from .managers import ParserConfig
|
|
17
|
+
from unrealon_driver.models import (
|
|
18
|
+
RegistrationMessage, CommandMessage, CommandResponseMessage,
|
|
19
|
+
StatusMessage, HeartbeatMessage, MessageType,
|
|
20
|
+
BridgeRegistrationMessage, BridgeRegistrationPayload
|
|
21
|
+
)
|
|
22
|
+
from unrealon_driver.html_analyzer import HTMLCleaningConfig
|
|
23
|
+
from unrealon_driver.websocket import WebSocketClient, WebSocketConfig
|
|
24
|
+
from unrealon_browser.dto.models.config import BrowserConfig
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
class DaemonStatus(BaseModel):
|
|
22
28
|
"""Daemon status information."""
|
|
29
|
+
|
|
23
30
|
running: bool = Field(..., description="Whether daemon is running")
|
|
24
31
|
parser_id: str = Field(..., description="Parser identifier")
|
|
25
32
|
started_at: datetime = Field(..., description="Daemon start time")
|
|
@@ -33,134 +40,146 @@ class DaemonStatus(BaseModel):
|
|
|
33
40
|
|
|
34
41
|
class DaemonManager(ParserManager):
|
|
35
42
|
"""Base daemon manager with scheduling and status display."""
|
|
36
|
-
|
|
37
|
-
def __init__(self, parser_name: str, parser_type: str, system_dir: str,
|
|
38
|
-
bridge_enabled: bool = False, websocket_url: str = "ws://localhost:8000/ws"):
|
|
43
|
+
|
|
44
|
+
def __init__(self, parser_name: str, parser_type: str, system_dir: str, bridge_enabled: bool = False):
|
|
39
45
|
# Create parser config
|
|
40
|
-
parser_config = ParserConfig(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
system_dir=Path(system_dir)
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# Create logging config
|
|
47
|
-
logging_config = LoggingConfig(parser_name=parser_name)
|
|
48
|
-
|
|
49
|
-
# Create other configs
|
|
46
|
+
parser_config = ParserConfig(parser_name=parser_name, parser_type=parser_type, system_dir=Path(system_dir))
|
|
47
|
+
|
|
48
|
+
# Create configs
|
|
50
49
|
html_config = HTMLCleaningConfig()
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
|
|
53
51
|
# Create manager config
|
|
54
52
|
manager_config = ParserManagerConfig(
|
|
55
|
-
parser_config=parser_config,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
parser_config=parser_config,
|
|
54
|
+
html_config=html_config,
|
|
55
|
+
bridge_enabled=bridge_enabled,
|
|
56
|
+
console_enabled=True,
|
|
57
|
+
file_enabled=True
|
|
60
58
|
)
|
|
61
|
-
|
|
59
|
+
|
|
62
60
|
super().__init__(manager_config)
|
|
63
|
-
|
|
61
|
+
|
|
64
62
|
# Daemon state
|
|
65
63
|
self.running = False
|
|
66
64
|
self.started_at: Optional[datetime] = None
|
|
67
65
|
self.next_run_at: Optional[datetime] = None
|
|
68
|
-
|
|
66
|
+
|
|
69
67
|
# Statistics
|
|
70
68
|
self.total_runs = 0
|
|
71
69
|
self.successful_runs = 0
|
|
72
70
|
self.failed_runs = 0
|
|
73
|
-
|
|
71
|
+
|
|
74
72
|
# Setup signal handlers
|
|
75
73
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
76
74
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
75
|
+
|
|
76
|
+
# WebSocket bridge connection
|
|
77
|
+
self.bridge_enabled = bridge_enabled
|
|
78
|
+
self.websocket_client: Optional[WebSocketClient] = None
|
|
77
79
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
+
# Registration status
|
|
81
|
+
self.registered = False
|
|
82
|
+
|
|
83
|
+
# Command handlers registry
|
|
84
|
+
self.command_handlers: dict[str, Callable[[dict[str, str]], Awaitable[dict[str, str]]]] = {}
|
|
85
|
+
|
|
86
|
+
# Register built-in commands
|
|
87
|
+
self._register_builtin_commands()
|
|
88
|
+
|
|
80
89
|
def _signal_handler(self, signum: int, frame) -> None:
|
|
81
90
|
"""Handle shutdown signals."""
|
|
82
91
|
self.logger.info(f"🛑 Received signal {signum}, shutting down...")
|
|
83
92
|
self.running = False
|
|
84
|
-
|
|
93
|
+
|
|
85
94
|
# RPC methods removed - commands handled through WebSocket bridge
|
|
86
|
-
|
|
95
|
+
|
|
87
96
|
async def start_daemon(self, schedule_enabled: bool = False, interval_minutes: Optional[int] = None) -> bool:
|
|
88
97
|
"""Start the daemon."""
|
|
89
98
|
try:
|
|
90
99
|
self.logger.info("🚀 Starting daemon...")
|
|
91
100
|
self.running = True
|
|
92
101
|
self.started_at = datetime.now()
|
|
93
|
-
|
|
102
|
+
|
|
94
103
|
# Initialize parser
|
|
95
104
|
await self.initialize()
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
|
|
105
|
+
|
|
106
|
+
# Connect to WebSocket bridge
|
|
107
|
+
if self.bridge_enabled:
|
|
108
|
+
bridge_connected = await self._connect_to_bridge()
|
|
109
|
+
if not bridge_connected:
|
|
110
|
+
self.logger.warning("⚠️ Failed to connect to bridge, continuing without WebSocket commands")
|
|
111
|
+
else:
|
|
112
|
+
# Register daemon with bridge server
|
|
113
|
+
self.logger.info("🔗 Attempting to register with bridge server...")
|
|
114
|
+
registration_success = await self._register_with_bridge()
|
|
115
|
+
if not registration_success:
|
|
116
|
+
self.logger.warning("⚠️ Failed to register with bridge server")
|
|
117
|
+
|
|
99
118
|
# Calculate next run if scheduling enabled
|
|
100
119
|
if schedule_enabled and interval_minutes:
|
|
101
120
|
self._calculate_next_run(interval_minutes)
|
|
102
|
-
|
|
121
|
+
|
|
103
122
|
# Start main loop
|
|
104
123
|
await self._daemon_loop(schedule_enabled, interval_minutes)
|
|
105
|
-
|
|
124
|
+
|
|
106
125
|
return True
|
|
107
|
-
|
|
126
|
+
|
|
108
127
|
except Exception as e:
|
|
109
128
|
self.logger.error(f"❌ Daemon startup failed: {e}")
|
|
110
129
|
return False
|
|
111
130
|
finally:
|
|
112
131
|
await self.cleanup()
|
|
113
|
-
|
|
132
|
+
|
|
114
133
|
def _calculate_next_run(self, interval_minutes: int) -> None:
|
|
115
134
|
"""Calculate next scheduled run time."""
|
|
116
135
|
now = datetime.now()
|
|
117
136
|
self.next_run_at = now + timedelta(minutes=interval_minutes)
|
|
118
|
-
|
|
137
|
+
|
|
119
138
|
async def _daemon_loop(self, schedule_enabled: bool, interval_minutes: Optional[int]) -> None:
|
|
120
139
|
"""Main daemon loop."""
|
|
121
140
|
self.logger.info("🔄 Daemon loop started")
|
|
122
|
-
|
|
141
|
+
|
|
123
142
|
if schedule_enabled and self.next_run_at:
|
|
124
143
|
self.logger.info(f"⏰ Next run: {self.next_run_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
125
144
|
else:
|
|
126
145
|
self.logger.info("📋 Manual mode")
|
|
127
|
-
|
|
146
|
+
|
|
128
147
|
last_status_update = time.time()
|
|
129
|
-
|
|
148
|
+
|
|
130
149
|
while self.running:
|
|
131
150
|
try:
|
|
132
151
|
current_time = time.time()
|
|
133
|
-
|
|
152
|
+
|
|
134
153
|
# Update status every second
|
|
135
154
|
if current_time - last_status_update >= 1.0:
|
|
136
155
|
self._display_status(schedule_enabled)
|
|
137
156
|
last_status_update = current_time
|
|
138
|
-
|
|
157
|
+
|
|
139
158
|
# Check for scheduled run
|
|
140
159
|
if self._should_run_now():
|
|
141
160
|
await self._execute_run()
|
|
142
161
|
if interval_minutes:
|
|
143
162
|
self._calculate_next_run(interval_minutes)
|
|
144
|
-
|
|
163
|
+
|
|
145
164
|
await asyncio.sleep(0.1)
|
|
146
|
-
|
|
165
|
+
|
|
147
166
|
except Exception as e:
|
|
148
167
|
self.logger.error(f"❌ Daemon loop error: {e}")
|
|
149
168
|
await asyncio.sleep(1)
|
|
150
|
-
|
|
169
|
+
|
|
151
170
|
def _display_status(self, schedule_enabled: bool) -> None:
|
|
152
171
|
"""Display live status."""
|
|
153
172
|
if not self.running:
|
|
154
173
|
return
|
|
155
|
-
|
|
174
|
+
|
|
156
175
|
# Clear previous lines
|
|
157
176
|
print("\033[2K\033[1A" * 3, end="")
|
|
158
|
-
|
|
177
|
+
|
|
159
178
|
now = datetime.now()
|
|
160
179
|
uptime = (now - self.started_at).total_seconds() if self.started_at else 0
|
|
161
|
-
|
|
180
|
+
|
|
162
181
|
print(f"🕐 {now.strftime('%H:%M:%S')} | ⏱️ Uptime: {int(uptime//3600):02d}:{int((uptime%3600)//60):02d}:{int(uptime%60):02d}")
|
|
163
|
-
|
|
182
|
+
|
|
164
183
|
# Schedule status
|
|
165
184
|
if self.next_run_at and schedule_enabled:
|
|
166
185
|
seconds_until = (self.next_run_at - now).total_seconds()
|
|
@@ -173,42 +192,42 @@ class DaemonManager(ParserManager):
|
|
|
173
192
|
print(f"🚀 Running now... | 📊 Runs: {self.successful_runs}✅ {self.failed_runs}❌")
|
|
174
193
|
else:
|
|
175
194
|
print(f"📋 Manual mode | 📊 Runs: {self.successful_runs}✅ {self.failed_runs}❌")
|
|
176
|
-
|
|
195
|
+
|
|
177
196
|
status = "🟢 RUNNING" if self.running else "🔴 STOPPED"
|
|
178
|
-
print(f"{status} | 💾 System: {self.config.system_dir}")
|
|
179
|
-
|
|
197
|
+
print(f"{status} | 💾 System: {self.config.parser_config.system_dir}")
|
|
198
|
+
|
|
180
199
|
def _should_run_now(self) -> bool:
|
|
181
200
|
"""Check if should run now."""
|
|
182
201
|
if not self.next_run_at:
|
|
183
202
|
return False
|
|
184
203
|
return datetime.now() >= self.next_run_at
|
|
185
|
-
|
|
204
|
+
|
|
186
205
|
async def _execute_run(self) -> None:
|
|
187
206
|
"""Execute a parsing run - override in subclass."""
|
|
188
207
|
self.logger.info("🚀 Starting parsing run...")
|
|
189
|
-
|
|
208
|
+
|
|
190
209
|
try:
|
|
191
210
|
# Default implementation - override in subclass
|
|
192
211
|
result = await self.parse_url("https://example.com")
|
|
193
|
-
|
|
212
|
+
|
|
194
213
|
self.total_runs += 1
|
|
195
|
-
|
|
214
|
+
|
|
196
215
|
if result.get("success") == "true":
|
|
197
216
|
self.successful_runs += 1
|
|
198
217
|
self.logger.info("✅ Run completed successfully")
|
|
199
218
|
else:
|
|
200
219
|
self.failed_runs += 1
|
|
201
220
|
self.logger.error("❌ Run failed")
|
|
202
|
-
|
|
221
|
+
|
|
203
222
|
except Exception as e:
|
|
204
223
|
self.failed_runs += 1
|
|
205
224
|
self.logger.error(f"❌ Run exception: {e}")
|
|
206
|
-
|
|
225
|
+
|
|
207
226
|
def get_status(self) -> DaemonStatus:
|
|
208
227
|
"""Get daemon status."""
|
|
209
228
|
now = datetime.now()
|
|
210
229
|
uptime = (now - self.started_at).total_seconds() if self.started_at else 0
|
|
211
|
-
|
|
230
|
+
|
|
212
231
|
return DaemonStatus(
|
|
213
232
|
running=self.running,
|
|
214
233
|
parser_id=self.config.parser_config.parser_name,
|
|
@@ -218,10 +237,167 @@ class DaemonManager(ParserManager):
|
|
|
218
237
|
next_run_at=self.next_run_at,
|
|
219
238
|
total_runs=self.total_runs,
|
|
220
239
|
successful_runs=self.successful_runs,
|
|
221
|
-
failed_runs=self.failed_runs
|
|
240
|
+
failed_runs=self.failed_runs,
|
|
222
241
|
)
|
|
223
|
-
|
|
242
|
+
|
|
224
243
|
async def cleanup(self):
|
|
225
244
|
"""Cleanup daemon resources."""
|
|
226
|
-
#
|
|
245
|
+
# Disconnect from bridge
|
|
246
|
+
await self._disconnect_from_bridge()
|
|
247
|
+
|
|
248
|
+
# Parent cleanup
|
|
227
249
|
await super().cleanup()
|
|
250
|
+
|
|
251
|
+
# ==========================================
|
|
252
|
+
# WEBSOCKET BRIDGE MANAGEMENT
|
|
253
|
+
# ==========================================
|
|
254
|
+
|
|
255
|
+
async def _connect_to_bridge(self) -> bool:
|
|
256
|
+
"""Connect to WebSocket bridge server."""
|
|
257
|
+
if not self.bridge_enabled:
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
self.logger.info(f"🔌 Connecting to bridge: {self.config.parser_config.websocket_url}")
|
|
262
|
+
|
|
263
|
+
# Create WebSocket config
|
|
264
|
+
ws_config = WebSocketConfig(
|
|
265
|
+
url=self.config.parser_config.websocket_url,
|
|
266
|
+
parser_id=self.config.parser_config.parser_name,
|
|
267
|
+
reconnect_interval=5.0,
|
|
268
|
+
max_reconnect_attempts=10
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Create and connect WebSocket client
|
|
272
|
+
self.websocket_client = WebSocketClient(ws_config)
|
|
273
|
+
|
|
274
|
+
# Add command handler
|
|
275
|
+
self.websocket_client.add_message_handler("command", self._handle_websocket_command)
|
|
276
|
+
|
|
277
|
+
success = await self.websocket_client.connect()
|
|
278
|
+
if success:
|
|
279
|
+
self.logger.info("✅ Connected to bridge server")
|
|
280
|
+
return True
|
|
281
|
+
else:
|
|
282
|
+
self.logger.error("❌ Failed to connect to bridge server")
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.logger.error(f"❌ Failed to connect to bridge: {e}")
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
async def _register_with_bridge(self) -> bool:
|
|
290
|
+
"""Register daemon with bridge server via WebSocket."""
|
|
291
|
+
if not self.websocket_client or not self.websocket_client.connected:
|
|
292
|
+
self.logger.warning("⚠️ Cannot register - WebSocket not connected")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
# Create registration message using Pydantic models
|
|
297
|
+
payload = BridgeRegistrationPayload(
|
|
298
|
+
client_type="daemon",
|
|
299
|
+
parser_id=self.config.parser_config.parser_name,
|
|
300
|
+
version="1.0.0",
|
|
301
|
+
capabilities=["parse", "search", "status", "health"]
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
registration_message = BridgeRegistrationMessage(payload=payload)
|
|
305
|
+
|
|
306
|
+
success = await self.websocket_client.send_message(registration_message.model_dump())
|
|
307
|
+
if success:
|
|
308
|
+
self.registered = True
|
|
309
|
+
self.logger.info(f"✅ Registered daemon with bridge server: {self.config.parser_config.parser_name}")
|
|
310
|
+
return True
|
|
311
|
+
else:
|
|
312
|
+
self.logger.error("❌ Failed to send registration message")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
self.logger.error(f"❌ Failed to register with bridge: {e}")
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
async def _disconnect_from_bridge(self):
|
|
320
|
+
"""Disconnect from WebSocket bridge."""
|
|
321
|
+
if self.websocket_client:
|
|
322
|
+
try:
|
|
323
|
+
await self.websocket_client.disconnect()
|
|
324
|
+
self.logger.info("🔌 Disconnected from bridge")
|
|
325
|
+
except Exception as e:
|
|
326
|
+
self.logger.error(f"❌ Error disconnecting from bridge: {e}")
|
|
327
|
+
finally:
|
|
328
|
+
self.websocket_client = None
|
|
329
|
+
self.registered = False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def _handle_websocket_command(self, message_data: dict[str, str]):
|
|
334
|
+
"""Handle incoming WebSocket command."""
|
|
335
|
+
try:
|
|
336
|
+
# Parse command message using Pydantic model
|
|
337
|
+
command_msg = CommandMessage.model_validate(message_data)
|
|
338
|
+
|
|
339
|
+
self.logger.info(f"📨 Received command: {command_msg.command_type} (id: {command_msg.command_id})")
|
|
340
|
+
|
|
341
|
+
# Find and execute command handler
|
|
342
|
+
if command_msg.command_type in self.command_handlers:
|
|
343
|
+
result = await self.command_handlers[command_msg.command_type](command_msg.parameters)
|
|
344
|
+
|
|
345
|
+
# Send success response using Pydantic model
|
|
346
|
+
response = CommandResponseMessage(
|
|
347
|
+
command_id=command_msg.command_id,
|
|
348
|
+
success=True,
|
|
349
|
+
result_data=result
|
|
350
|
+
)
|
|
351
|
+
await self.websocket_client.send_message(response.model_dump())
|
|
352
|
+
self.logger.info(f"✅ Command {command_msg.command_type} completed")
|
|
353
|
+
|
|
354
|
+
else:
|
|
355
|
+
raise ValueError(f"Unknown command type: {command_msg.command_type}")
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
self.logger.error(f"❌ Command failed: {e}")
|
|
359
|
+
|
|
360
|
+
# Send error response using Pydantic model
|
|
361
|
+
command_id = message_data.get("command_id", "unknown")
|
|
362
|
+
response = CommandResponseMessage(
|
|
363
|
+
command_id=command_id,
|
|
364
|
+
success=False,
|
|
365
|
+
error=str(e)
|
|
366
|
+
)
|
|
367
|
+
await self.websocket_client.send_message(response.model_dump())
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ==========================================
|
|
372
|
+
# COMMAND SYSTEM
|
|
373
|
+
# ==========================================
|
|
374
|
+
|
|
375
|
+
def register_command(self, command_type: str, handler: Callable[[dict[str, str]], Awaitable[dict[str, str]]]):
|
|
376
|
+
"""Register a command handler."""
|
|
377
|
+
self.command_handlers[command_type] = handler
|
|
378
|
+
self.logger.info(f"🔧 Registered command handler: {command_type}")
|
|
379
|
+
|
|
380
|
+
def _register_builtin_commands(self):
|
|
381
|
+
"""Register built-in command handlers."""
|
|
382
|
+
self.register_command("status", self._handle_status_command)
|
|
383
|
+
self.register_command("health", self._handle_health_command)
|
|
384
|
+
|
|
385
|
+
async def _handle_status_command(self, parameters: dict[str, str]) -> dict[str, str]:
|
|
386
|
+
"""Built-in status command handler."""
|
|
387
|
+
status = self.get_status()
|
|
388
|
+
return {
|
|
389
|
+
"command_type": "status",
|
|
390
|
+
"running": str(status.running),
|
|
391
|
+
"uptime_seconds": str(status.uptime_seconds),
|
|
392
|
+
"total_runs": str(status.total_runs),
|
|
393
|
+
"successful_runs": str(status.successful_runs),
|
|
394
|
+
"failed_runs": str(status.failed_runs)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async def _handle_health_command(self, parameters: dict[str, str]) -> dict[str, str]:
|
|
398
|
+
"""Built-in health command handler."""
|
|
399
|
+
return {
|
|
400
|
+
"command_type": "health",
|
|
401
|
+
"status": "healthy",
|
|
402
|
+
"bridge_connected": str(self.websocket_client.connected if self.websocket_client else False)
|
|
403
|
+
}
|