unrealon 1.1.1__py3-none-any.whl → 1.1.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.
Files changed (83) hide show
  1. unrealon/__init__.py +16 -6
  2. unrealon-1.1.4.dist-info/METADATA +658 -0
  3. unrealon-1.1.4.dist-info/RECORD +54 -0
  4. {unrealon-1.1.1.dist-info → unrealon-1.1.4.dist-info}/entry_points.txt +1 -1
  5. unrealon_browser/__init__.py +3 -6
  6. unrealon_browser/core/browser_manager.py +86 -84
  7. unrealon_browser/dto/models/config.py +2 -0
  8. unrealon_browser/managers/captcha.py +165 -185
  9. unrealon_browser/managers/cookies.py +57 -28
  10. unrealon_browser/managers/logger_bridge.py +94 -34
  11. unrealon_browser/managers/profile.py +186 -158
  12. unrealon_browser/managers/stealth.py +58 -47
  13. unrealon_driver/__init__.py +8 -21
  14. unrealon_driver/exceptions.py +5 -0
  15. unrealon_driver/html_analyzer/__init__.py +32 -0
  16. unrealon_driver/{parser/managers/html.py → html_analyzer/cleaner.py} +330 -405
  17. unrealon_driver/html_analyzer/config.py +64 -0
  18. unrealon_driver/html_analyzer/manager.py +247 -0
  19. unrealon_driver/html_analyzer/models.py +115 -0
  20. unrealon_driver/html_analyzer/websocket_analyzer.py +157 -0
  21. unrealon_driver/models/__init__.py +31 -0
  22. unrealon_driver/models/websocket.py +98 -0
  23. unrealon_driver/parser/__init__.py +4 -23
  24. unrealon_driver/parser/cli_manager.py +6 -5
  25. unrealon_driver/parser/daemon_manager.py +242 -66
  26. unrealon_driver/parser/managers/__init__.py +0 -21
  27. unrealon_driver/parser/managers/config.py +15 -3
  28. unrealon_driver/parser/parser_manager.py +225 -395
  29. unrealon_driver/smart_logging/__init__.py +24 -0
  30. unrealon_driver/smart_logging/models.py +44 -0
  31. unrealon_driver/smart_logging/smart_logger.py +406 -0
  32. unrealon_driver/smart_logging/unified_logger.py +525 -0
  33. unrealon_driver/websocket/__init__.py +31 -0
  34. unrealon_driver/websocket/client.py +249 -0
  35. unrealon_driver/websocket/config.py +188 -0
  36. unrealon_driver/websocket/manager.py +90 -0
  37. unrealon-1.1.1.dist-info/METADATA +0 -722
  38. unrealon-1.1.1.dist-info/RECORD +0 -82
  39. unrealon_bridge/__init__.py +0 -114
  40. unrealon_bridge/cli.py +0 -316
  41. unrealon_bridge/client/__init__.py +0 -93
  42. unrealon_bridge/client/base.py +0 -78
  43. unrealon_bridge/client/commands.py +0 -89
  44. unrealon_bridge/client/connection.py +0 -90
  45. unrealon_bridge/client/events.py +0 -65
  46. unrealon_bridge/client/health.py +0 -38
  47. unrealon_bridge/client/html_parser.py +0 -146
  48. unrealon_bridge/client/logging.py +0 -139
  49. unrealon_bridge/client/proxy.py +0 -70
  50. unrealon_bridge/client/scheduler.py +0 -450
  51. unrealon_bridge/client/session.py +0 -70
  52. unrealon_bridge/configs/__init__.py +0 -14
  53. unrealon_bridge/configs/bridge_config.py +0 -212
  54. unrealon_bridge/configs/bridge_config.yaml +0 -39
  55. unrealon_bridge/models/__init__.py +0 -138
  56. unrealon_bridge/models/base.py +0 -28
  57. unrealon_bridge/models/command.py +0 -41
  58. unrealon_bridge/models/events.py +0 -40
  59. unrealon_bridge/models/html_parser.py +0 -79
  60. unrealon_bridge/models/logging.py +0 -55
  61. unrealon_bridge/models/parser.py +0 -63
  62. unrealon_bridge/models/proxy.py +0 -41
  63. unrealon_bridge/models/requests.py +0 -95
  64. unrealon_bridge/models/responses.py +0 -88
  65. unrealon_bridge/models/scheduler.py +0 -592
  66. unrealon_bridge/models/session.py +0 -28
  67. unrealon_bridge/server/__init__.py +0 -91
  68. unrealon_bridge/server/base.py +0 -171
  69. unrealon_bridge/server/handlers/__init__.py +0 -23
  70. unrealon_bridge/server/handlers/command.py +0 -110
  71. unrealon_bridge/server/handlers/html_parser.py +0 -139
  72. unrealon_bridge/server/handlers/logging.py +0 -95
  73. unrealon_bridge/server/handlers/parser.py +0 -95
  74. unrealon_bridge/server/handlers/proxy.py +0 -75
  75. unrealon_bridge/server/handlers/scheduler.py +0 -545
  76. unrealon_bridge/server/handlers/session.py +0 -66
  77. unrealon_driver/browser/__init__.py +0 -8
  78. unrealon_driver/browser/config.py +0 -74
  79. unrealon_driver/browser/manager.py +0 -416
  80. unrealon_driver/parser/managers/browser.py +0 -51
  81. unrealon_driver/parser/managers/logging.py +0 -609
  82. {unrealon-1.1.1.dist-info → unrealon-1.1.4.dist-info}/WHEEL +0 -0
  83. {unrealon-1.1.1.dist-info → unrealon-1.1.4.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
- "LoggingManager",
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, LoggingConfig, HTMLCleaningConfig, BrowserConfig
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, websocket_url: str = "ws://localhost:8000/ws"):
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
- logging_config = LoggingConfig(parser_name=parser_name)
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, Dict, Any
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, LoggingConfig, HTMLCleaningConfig, BrowserConfig
17
-
18
- # RPC removed - all commands go through WebSocket bridge
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
- parser_name=parser_name,
42
- parser_type=parser_type,
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
- browser_config = BrowserConfig()
52
-
50
+
53
51
  # Create manager config
54
52
  manager_config = ParserManagerConfig(
55
- parser_config=parser_config,
56
- logging_config=logging_config,
57
- html_config=html_config,
58
- browser_config=browser_config,
59
- bridge_enabled=bridge_enabled
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
- # RPC removed - commands come through WebSocket bridge
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
- # RPC server removed - using WebSocket bridge
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
- # RPC server removed - only parent cleanup needed
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
+ }