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.
Files changed (146) hide show
  1. {unrealon-1.1.5.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
  2. unrealon-2.0.4.dist-info/METADATA +491 -0
  3. unrealon-2.0.4.dist-info/RECORD +129 -0
  4. {unrealon-1.1.5.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
  5. unrealon-2.0.4.dist-info/entry_points.txt +3 -0
  6. unrealon-2.0.4.dist-info/top_level.txt +3 -0
  7. unrealon_browser/__init__.py +5 -2
  8. unrealon_browser/cli/browser_cli.py +18 -9
  9. unrealon_browser/cli/interactive_mode.py +18 -7
  10. unrealon_browser/core/browser_manager.py +76 -13
  11. unrealon_browser/dto/__init__.py +21 -0
  12. unrealon_browser/dto/bot_detection.py +175 -0
  13. unrealon_browser/dto/models/config.py +14 -1
  14. unrealon_browser/managers/__init__.py +4 -1
  15. unrealon_browser/managers/logger_bridge.py +3 -6
  16. unrealon_browser/managers/page_wait_manager.py +198 -0
  17. unrealon_browser/stealth/__init__.py +27 -0
  18. unrealon_browser/stealth/bypass_techniques.pyc +0 -0
  19. unrealon_browser/stealth/manager.pyc +0 -0
  20. unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
  21. unrealon_browser/stealth/playwright_stealth.pyc +0 -0
  22. unrealon_browser/stealth/scanner_tester.pyc +0 -0
  23. unrealon_browser/stealth/undetected_chrome.pyc +0 -0
  24. unrealon_core/__init__.py +160 -0
  25. unrealon_core/config/__init__.py +16 -0
  26. unrealon_core/config/environment.py +98 -0
  27. unrealon_core/config/urls.py +93 -0
  28. unrealon_core/enums/__init__.py +24 -0
  29. unrealon_core/enums/status.py +216 -0
  30. unrealon_core/enums/types.py +240 -0
  31. unrealon_core/error_handling/__init__.py +45 -0
  32. unrealon_core/error_handling/circuit_breaker.py +292 -0
  33. unrealon_core/error_handling/error_context.py +324 -0
  34. unrealon_core/error_handling/recovery.py +371 -0
  35. unrealon_core/error_handling/retry.py +268 -0
  36. unrealon_core/exceptions/__init__.py +46 -0
  37. unrealon_core/exceptions/base.py +292 -0
  38. unrealon_core/exceptions/communication.py +22 -0
  39. unrealon_core/exceptions/driver.py +11 -0
  40. unrealon_core/exceptions/proxy.py +11 -0
  41. unrealon_core/exceptions/task.py +12 -0
  42. unrealon_core/exceptions/validation.py +17 -0
  43. unrealon_core/models/__init__.py +98 -0
  44. unrealon_core/models/arq_context.py +252 -0
  45. unrealon_core/models/arq_responses.py +125 -0
  46. unrealon_core/models/base.py +291 -0
  47. unrealon_core/models/bridge_stats.py +58 -0
  48. unrealon_core/models/communication.py +39 -0
  49. unrealon_core/models/config.py +47 -0
  50. unrealon_core/models/connection_stats.py +47 -0
  51. unrealon_core/models/driver.py +30 -0
  52. unrealon_core/models/driver_details.py +98 -0
  53. unrealon_core/models/logging.py +28 -0
  54. unrealon_core/models/task.py +21 -0
  55. unrealon_core/models/typed_responses.py +210 -0
  56. unrealon_core/models/websocket/__init__.py +91 -0
  57. unrealon_core/models/websocket/base.py +49 -0
  58. unrealon_core/models/websocket/config.py +200 -0
  59. unrealon_core/models/websocket/driver.py +215 -0
  60. unrealon_core/models/websocket/errors.py +138 -0
  61. unrealon_core/models/websocket/heartbeat.py +100 -0
  62. unrealon_core/models/websocket/logging.py +261 -0
  63. unrealon_core/models/websocket/proxy.py +496 -0
  64. unrealon_core/models/websocket/tasks.py +275 -0
  65. unrealon_core/models/websocket/utils.py +153 -0
  66. unrealon_core/models/websocket_session.py +144 -0
  67. unrealon_core/monitoring/__init__.py +43 -0
  68. unrealon_core/monitoring/alerts.py +398 -0
  69. unrealon_core/monitoring/dashboard.py +307 -0
  70. unrealon_core/monitoring/health_check.py +354 -0
  71. unrealon_core/monitoring/metrics.py +352 -0
  72. unrealon_core/utils/__init__.py +11 -0
  73. unrealon_core/utils/time.py +61 -0
  74. unrealon_core/version.py +219 -0
  75. unrealon_driver/__init__.py +88 -50
  76. unrealon_driver/core_module/__init__.py +34 -0
  77. unrealon_driver/core_module/base.py +184 -0
  78. unrealon_driver/core_module/config.py +30 -0
  79. unrealon_driver/core_module/event_manager.py +127 -0
  80. unrealon_driver/core_module/protocols.py +98 -0
  81. unrealon_driver/core_module/registry.py +146 -0
  82. unrealon_driver/decorators/__init__.py +15 -0
  83. unrealon_driver/decorators/retry.py +117 -0
  84. unrealon_driver/decorators/schedule.py +137 -0
  85. unrealon_driver/decorators/task.py +61 -0
  86. unrealon_driver/decorators/timing.py +132 -0
  87. unrealon_driver/driver/__init__.py +20 -0
  88. unrealon_driver/driver/communication/__init__.py +10 -0
  89. unrealon_driver/driver/communication/session.py +203 -0
  90. unrealon_driver/driver/communication/websocket_client.py +197 -0
  91. unrealon_driver/driver/core/__init__.py +10 -0
  92. unrealon_driver/driver/core/config.py +85 -0
  93. unrealon_driver/driver/core/driver.py +221 -0
  94. unrealon_driver/driver/factory/__init__.py +9 -0
  95. unrealon_driver/driver/factory/manager_factory.py +130 -0
  96. unrealon_driver/driver/lifecycle/__init__.py +11 -0
  97. unrealon_driver/driver/lifecycle/daemon.py +76 -0
  98. unrealon_driver/driver/lifecycle/initialization.py +97 -0
  99. unrealon_driver/driver/lifecycle/shutdown.py +48 -0
  100. unrealon_driver/driver/monitoring/__init__.py +9 -0
  101. unrealon_driver/driver/monitoring/health.py +63 -0
  102. unrealon_driver/driver/utilities/__init__.py +10 -0
  103. unrealon_driver/driver/utilities/logging.py +51 -0
  104. unrealon_driver/driver/utilities/serialization.py +61 -0
  105. unrealon_driver/managers/__init__.py +32 -0
  106. unrealon_driver/managers/base.py +174 -0
  107. unrealon_driver/managers/browser.py +98 -0
  108. unrealon_driver/managers/cache.py +116 -0
  109. unrealon_driver/managers/http.py +107 -0
  110. unrealon_driver/managers/logger.py +286 -0
  111. unrealon_driver/managers/proxy.py +99 -0
  112. unrealon_driver/managers/registry.py +87 -0
  113. unrealon_driver/managers/threading.py +54 -0
  114. unrealon_driver/managers/update.py +107 -0
  115. unrealon_driver/utils/__init__.py +9 -0
  116. unrealon_driver/utils/time.py +10 -0
  117. unrealon/__init__.py +0 -40
  118. unrealon-1.1.5.dist-info/METADATA +0 -621
  119. unrealon-1.1.5.dist-info/RECORD +0 -54
  120. unrealon-1.1.5.dist-info/entry_points.txt +0 -9
  121. unrealon_browser/managers/stealth.py +0 -388
  122. unrealon_driver/exceptions.py +0 -33
  123. unrealon_driver/html_analyzer/__init__.py +0 -32
  124. unrealon_driver/html_analyzer/cleaner.py +0 -657
  125. unrealon_driver/html_analyzer/config.py +0 -64
  126. unrealon_driver/html_analyzer/manager.py +0 -247
  127. unrealon_driver/html_analyzer/models.py +0 -115
  128. unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
  129. unrealon_driver/models/__init__.py +0 -31
  130. unrealon_driver/models/websocket.py +0 -98
  131. unrealon_driver/parser/__init__.py +0 -36
  132. unrealon_driver/parser/cli_manager.py +0 -142
  133. unrealon_driver/parser/daemon_manager.py +0 -403
  134. unrealon_driver/parser/managers/__init__.py +0 -25
  135. unrealon_driver/parser/managers/config.py +0 -293
  136. unrealon_driver/parser/managers/error.py +0 -412
  137. unrealon_driver/parser/managers/result.py +0 -321
  138. unrealon_driver/parser/parser_manager.py +0 -458
  139. unrealon_driver/smart_logging/__init__.py +0 -24
  140. unrealon_driver/smart_logging/models.py +0 -44
  141. unrealon_driver/smart_logging/smart_logger.py +0 -406
  142. unrealon_driver/smart_logging/unified_logger.py +0 -525
  143. unrealon_driver/websocket/__init__.py +0 -31
  144. unrealon_driver/websocket/client.py +0 -249
  145. unrealon_driver/websocket/config.py +0 -188
  146. unrealon_driver/websocket/manager.py +0 -90
@@ -1,525 +0,0 @@
1
- """
2
- Unified Logger: Smart + Rich + Structured.
3
-
4
- Combines:
5
- - SmartLogger: WebSocket batching, connection management
6
- - Rich console: Beautiful colored output
7
- - Structured logging: Context management and metadata
8
- """
9
-
10
- import asyncio
11
- import logging
12
- import yaml
13
- from pathlib import Path
14
- from typing import Optional, Any
15
- from pydantic import BaseModel, Field, ConfigDict
16
- from datetime import datetime, timezone
17
-
18
- from rich.console import Console
19
- from rich.text import Text
20
-
21
- from .smart_logger import ConnectionManager, LogBuffer
22
- from .models import LogLevel, LogContext, LogEntry
23
-
24
-
25
- def get_project_root() -> Path:
26
- """
27
- Get the project root directory (backend/unrealon-rpc/).
28
-
29
- Returns:
30
- Path to the project root directory
31
- """
32
- # This file is in src/unrealon_driver/smart_logging/unified_logger.py
33
- # Path levels: unified_logger.py -> smart_logging -> unrealon_driver -> src -> backend/unrealon-rpc/
34
- return Path(__file__).parent.parent.parent.parent
35
-
36
-
37
- def resolve_log_path(relative_path: str) -> Path:
38
- """
39
- Resolve a relative log path to an absolute path within the project.
40
-
41
- Args:
42
- relative_path: Relative path like "logs/parser_name.log"
43
-
44
- Returns:
45
- Absolute path to the log file
46
- """
47
- project_root = get_project_root()
48
- return project_root / relative_path
49
-
50
-
51
- class UnifiedLoggerConfig(BaseModel):
52
- """Configuration for unified logger"""
53
- model_config = ConfigDict(
54
- validate_assignment=True,
55
- extra="forbid"
56
- )
57
-
58
- # Parser identity
59
- parser_id: str = Field(..., min_length=1)
60
- parser_name: str = Field(..., min_length=1)
61
-
62
- # Local logging
63
- log_file: Optional[Path] = Field(default=None)
64
- console_enabled: bool = Field(default=True)
65
- file_enabled: bool = Field(default=True)
66
-
67
- # WebSocket logging
68
- bridge_logs_url: Optional[str] = Field(default=None)
69
- batch_interval: float = Field(default=5.0, gt=0.0)
70
- daemon_mode: Optional[bool] = Field(default=None)
71
-
72
- # Log levels
73
- console_level: LogLevel = Field(default=LogLevel.INFO)
74
- file_level: LogLevel = Field(default=LogLevel.DEBUG)
75
- bridge_level: LogLevel = Field(default=LogLevel.INFO)
76
-
77
-
78
- class UnifiedLogger:
79
- """
80
- 🚀 Unified Logger: Smart + Rich + Structured
81
-
82
- Features from SmartLogger:
83
- - WebSocket batching and transport
84
- - Smart connection management
85
- - Daemon vs script mode detection
86
-
87
- Features from LegacyManager:
88
- - Rich console output with colors
89
- - Structured logging with context
90
- - Multiple output destinations
91
-
92
- Developer Experience:
93
- - Simple API: logger.info("message")
94
- - Context management: logger.set_session("123")
95
- - Automatic batching and fallback
96
- """
97
-
98
- def __init__(self, config: UnifiedLoggerConfig):
99
- self.config = config
100
- self._context = LogContext()
101
-
102
- # Rich console
103
- self.console = Console() if config.console_enabled else None
104
-
105
- # Local file logger
106
- self.file_logger = self._setup_file_logger() if config.file_enabled else None
107
-
108
- # SmartLogger components (WebSocket)
109
- self.bridge_enabled = config.bridge_logs_url is not None
110
- if self.bridge_enabled:
111
- self.log_buffer = LogBuffer()
112
- self.connection_manager = ConnectionManager(
113
- config.bridge_logs_url,
114
- config.parser_id
115
- )
116
-
117
- # Detect daemon mode
118
- daemon_mode = config.daemon_mode
119
- if daemon_mode is None:
120
- daemon_mode = self._detect_daemon_mode()
121
- self.connection_manager.set_daemon_mode(daemon_mode)
122
-
123
- # Start batch timer
124
- self._batch_task = None
125
- self._ensure_batch_timer()
126
- else:
127
- self.log_buffer = None
128
- self.connection_manager = None
129
- self._batch_task = None
130
-
131
- # ==========================================
132
- # PUBLIC LOGGING API
133
- # ==========================================
134
-
135
- def debug(self, message: str, **kwargs):
136
- """Log DEBUG message"""
137
- self._log(LogLevel.DEBUG, message, **kwargs)
138
-
139
- def info(self, message: str, **kwargs):
140
- """Log INFO message"""
141
- self._log(LogLevel.INFO, message, **kwargs)
142
-
143
- def warning(self, message: str, **kwargs):
144
- """Log WARNING message"""
145
- self._log(LogLevel.WARNING, message, **kwargs)
146
-
147
- def error(self, message: str, **kwargs):
148
- """Log ERROR message"""
149
- self._log(LogLevel.ERROR, message, **kwargs)
150
-
151
- def critical(self, message: str, **kwargs):
152
- """Log CRITICAL message"""
153
- self._log(LogLevel.CRITICAL, message, **kwargs)
154
-
155
- # Aliases
156
- warn = warning
157
-
158
- # ==========================================
159
- # CONTEXT MANAGEMENT
160
- # ==========================================
161
-
162
- def set_session(self, session_id: str):
163
- """Set session ID for all future logs"""
164
- if not session_id.strip():
165
- raise ValueError("Session ID cannot be empty")
166
- self._context.session_id = session_id
167
-
168
- def set_operation(self, operation: str):
169
- """Set current operation"""
170
- if not operation.strip():
171
- raise ValueError("Operation cannot be empty")
172
- self._context.operation = operation
173
-
174
- def set_url(self, url: str):
175
- """Set current URL"""
176
- if not url.strip():
177
- raise ValueError("URL cannot be empty")
178
- self._context.url = url
179
-
180
- def add_context_data(self, key: str, value: str):
181
- """Add additional context data"""
182
- if not key.strip():
183
- raise ValueError("Context key cannot be empty")
184
- self._context.additional_data[key] = str(value)
185
-
186
- def clear_context(self):
187
- """Clear all context"""
188
- self._context = LogContext()
189
-
190
- # ==========================================
191
- # SPECIALIZED LOGGING METHODS
192
- # ==========================================
193
-
194
- def start_operation(self, operation: str, **kwargs):
195
- """Log start of operation"""
196
- self.set_operation(operation)
197
- self.info(f"🚀 Starting {operation}", **kwargs)
198
-
199
- def end_operation(self, operation: str, duration: Optional[float] = None, **kwargs):
200
- """Log end of operation"""
201
- if duration is not None:
202
- self.info(f"✅ Completed {operation} in {duration:.2f}s", duration=str(duration), **kwargs)
203
- else:
204
- self.info(f"✅ Completed {operation}", **kwargs)
205
-
206
- def fail_operation(self, operation: str, error: str, **kwargs):
207
- """Log failed operation"""
208
- self.error(f"❌ Failed {operation}: {error}", error=error, **kwargs)
209
-
210
- def url_access(self, url: str, status: str = "accessing", **kwargs):
211
- """Log URL access"""
212
- self.set_url(url)
213
- self.info(f"🌐 {status.title()} URL: {url}", status=status, **kwargs)
214
-
215
- def data_extracted(self, data_type: str, count: int, **kwargs):
216
- """Log data extraction"""
217
- if count < 0:
218
- raise ValueError("Count must be non-negative")
219
-
220
- self.info(
221
- f"📦 Extracted {count} {data_type}",
222
- data_type=data_type,
223
- count=str(count),
224
- **kwargs
225
- )
226
-
227
- # ==========================================
228
- # INTERNAL IMPLEMENTATION
229
- # ==========================================
230
-
231
- def _format_value_for_console(self, value: Any) -> str:
232
- """Format value for console output with YAML for complex objects"""
233
- if value is None:
234
- return "None"
235
- elif isinstance(value, (str, int, float, bool)):
236
- return str(value)
237
- elif isinstance(value, (dict, list, tuple)):
238
- try:
239
- # Use YAML for complex objects - more readable than JSON
240
- yaml_str = yaml.dump(value, default_flow_style=True, allow_unicode=True)
241
- # Remove trailing newline and make it one-line for console
242
- return yaml_str.strip().replace('\n', ' ')
243
- except Exception:
244
- return str(value)
245
- else:
246
- # For custom objects, try to convert to dict first
247
- try:
248
- if hasattr(value, 'model_dump'): # Pydantic
249
- dict_value = value.model_dump()
250
- elif hasattr(value, '__dict__'): # Regular objects
251
- dict_value = value.__dict__
252
- else:
253
- return str(value)
254
-
255
- yaml_str = yaml.dump(dict_value, default_flow_style=True, allow_unicode=True)
256
- return yaml_str.strip().replace('\n', ' ')
257
- except Exception:
258
- return str(value)
259
-
260
- def _log(self, level: LogLevel, message: str, **kwargs):
261
- """Internal logging method"""
262
- # Create context from current context + kwargs
263
- context = self._create_context(**kwargs)
264
-
265
- # Create log entry
266
- log_entry = LogEntry(
267
- timestamp=datetime.now(timezone.utc).isoformat(),
268
- level=level.value,
269
- message=message,
270
- parser_id=self.config.parser_id,
271
- session_id=context.session_id,
272
- url=context.url,
273
- operation=context.operation,
274
- extra=context.additional_data if context.additional_data else None
275
- )
276
-
277
- # Console output (Rich)
278
- if self.console and self._should_log_to_console(level):
279
- self._log_to_console(log_entry, level)
280
-
281
- # File output
282
- if self.file_logger and self._should_log_to_file(level):
283
- self._log_to_file(log_entry, level)
284
-
285
- # WebSocket output (batched) - ALWAYS send to bridge regardless of level
286
- if self.bridge_enabled:
287
- self._log_to_bridge(log_entry)
288
-
289
- def _create_context(self, **kwargs) -> LogContext:
290
- """Create log context from current context and kwargs"""
291
- context_data = self._context.additional_data.copy()
292
-
293
- # Add kwargs - keep original types for console formatting
294
- for key, value in kwargs.items():
295
- if key not in ['session_id', 'command_id', 'operation', 'url']:
296
- context_data[key] = value # Keep original type
297
-
298
- return LogContext(
299
- session_id=kwargs.get('session_id') or self._context.session_id,
300
- command_id=kwargs.get('command_id') or self._context.command_id,
301
- operation=kwargs.get('operation') or self._context.operation,
302
- url=kwargs.get('url') or self._context.url,
303
- additional_data=context_data
304
- )
305
-
306
- def _should_log_to_console(self, level: LogLevel) -> bool:
307
- """Check if should log to console"""
308
- level_value = getattr(logging, level.value)
309
- console_level_value = getattr(logging, self.config.console_level.value)
310
- return level_value >= console_level_value
311
-
312
- def _should_log_to_file(self, level: LogLevel) -> bool:
313
- """Check if should log to file"""
314
- level_value = getattr(logging, level.value)
315
- file_level_value = getattr(logging, self.config.file_level.value)
316
- return level_value >= file_level_value
317
-
318
- def _should_log_to_bridge(self, level: LogLevel) -> bool:
319
- """Check if should log to bridge - ALWAYS True now"""
320
- return True # Always send to bridge regardless of level
321
-
322
- def _log_to_console(self, log_entry: LogEntry, level: LogLevel):
323
- """Log to console with Rich formatting"""
324
- if not self.console:
325
- return
326
-
327
- # Parse timestamp for display
328
- try:
329
- from datetime import datetime
330
- timestamp = datetime.fromisoformat(log_entry.timestamp.replace('Z', '+00:00'))
331
- time_str = timestamp.strftime('%H:%M:%S')
332
- except:
333
- time_str = "00:00:00"
334
-
335
- # Color based on level
336
- level_colors = {
337
- LogLevel.DEBUG: "dim white",
338
- LogLevel.INFO: "bright_blue",
339
- LogLevel.WARNING: "yellow",
340
- LogLevel.ERROR: "red",
341
- LogLevel.CRITICAL: "bold red"
342
- }
343
-
344
- level_color = level_colors.get(level, "white")
345
-
346
- # Format message
347
- formatted_message = Text()
348
- formatted_message.append(f"[{time_str}] ", style="dim")
349
- formatted_message.append(f"{level.value}", style=level_color)
350
- formatted_message.append(f" - {self.config.parser_name} - ", style="dim")
351
- formatted_message.append(log_entry.message, style="white")
352
-
353
- # Add context if available
354
- context_parts = []
355
- if log_entry.extra:
356
- for key, value in log_entry.extra.items():
357
- context_parts.append(f"{key}={value}")
358
-
359
- if context_parts:
360
- formatted_message.append(f" ({', '.join(context_parts)})", style="dim")
361
-
362
- self.console.print(formatted_message)
363
-
364
- def _log_to_file(self, log_entry: LogEntry, level: LogLevel):
365
- """Log to file"""
366
- if not self.file_logger:
367
- return
368
-
369
- try:
370
- # Create log message with context
371
- extra_info = ""
372
- if log_entry.extra:
373
- context_parts = [f"{k}={v}" for k, v in log_entry.extra.items()]
374
- extra_info = f" - {', '.join(context_parts)}"
375
-
376
- full_message = f"{log_entry.message}{extra_info}"
377
-
378
- # Log to file
379
- log_level = getattr(logging, level.value)
380
- self.file_logger.log(log_level, full_message)
381
-
382
- except Exception:
383
- # Fail silently for file logging
384
- pass
385
-
386
- def _log_to_bridge(self, log_entry: LogEntry):
387
- """Log to bridge via WebSocket (batched)"""
388
- if not self.log_buffer:
389
- return
390
-
391
- # Add to buffer (non-blocking)
392
- try:
393
- asyncio.create_task(self.log_buffer.add(log_entry))
394
- except RuntimeError:
395
- # No event loop, skip bridge logging
396
- pass
397
-
398
- def _setup_file_logger(self) -> Optional[logging.Logger]:
399
- """Setup file logger"""
400
- if not self.config.log_file:
401
- return None
402
-
403
- # Ensure log directory exists
404
- self.config.log_file.parent.mkdir(parents=True, exist_ok=True)
405
-
406
- # Create file logger
407
- logger_name = f"unified_{self.config.parser_id}"
408
- logger = logging.getLogger(logger_name)
409
- logger.setLevel(getattr(logging, self.config.file_level.value))
410
-
411
- # Remove existing handlers
412
- for handler in logger.handlers[:]:
413
- logger.removeHandler(handler)
414
-
415
- # Add file handler
416
- file_handler = logging.FileHandler(self.config.log_file, encoding='utf-8')
417
- file_handler.setLevel(getattr(logging, self.config.file_level.value))
418
-
419
- # Format for file logging
420
- formatter = logging.Formatter(
421
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
422
- datefmt='%Y-%m-%d %H:%M:%S'
423
- )
424
- file_handler.setFormatter(formatter)
425
-
426
- logger.addHandler(file_handler)
427
- logger.propagate = False
428
-
429
- return logger
430
-
431
- def _detect_daemon_mode(self) -> bool:
432
- """Auto-detect daemon vs script mode"""
433
- try:
434
- # If there's an active event loop, likely daemon mode
435
- asyncio.get_running_loop()
436
- return True
437
- except RuntimeError:
438
- return False
439
-
440
- def _ensure_batch_timer(self):
441
- """Ensure batch timer is started"""
442
- if self._batch_task is None or self._batch_task.done():
443
- try:
444
- asyncio.get_running_loop()
445
- self._batch_task = asyncio.create_task(self._batch_loop())
446
- except RuntimeError:
447
- # No event loop, will start later
448
- pass
449
-
450
- async def _batch_loop(self):
451
- """Main batch sending loop"""
452
- try:
453
- while True:
454
- await asyncio.sleep(self.config.batch_interval)
455
- await self._send_batch()
456
- except asyncio.CancelledError:
457
- # Final batch send on cancellation
458
- await self._send_batch()
459
- raise
460
-
461
- async def _send_batch(self):
462
- """Send accumulated logs"""
463
- if not self.log_buffer or not self.connection_manager:
464
- return
465
-
466
- entries = await self.log_buffer.flush()
467
- if entries:
468
- await self.connection_manager.send_batch(entries)
469
-
470
- async def flush(self):
471
- """Force send all accumulated logs"""
472
- if self.bridge_enabled:
473
- await self._send_batch()
474
-
475
- async def close(self):
476
- """Close logger and cleanup resources"""
477
- # Send remaining logs
478
- await self.flush()
479
-
480
- # Stop batch timer
481
- if self._batch_task and not self._batch_task.done():
482
- self._batch_task.cancel()
483
- try:
484
- await self._batch_task
485
- except asyncio.CancelledError:
486
- pass
487
-
488
- # Close connection
489
- if self.connection_manager:
490
- await self.connection_manager.close()
491
-
492
-
493
- def create_unified_logger(
494
- parser_id: str,
495
- parser_name: str,
496
- bridge_logs_url: Optional[str] = None,
497
- log_file: Optional[Path] = None,
498
- **kwargs
499
- ) -> UnifiedLogger:
500
- """
501
- Create unified logger with optimal settings.
502
-
503
- Args:
504
- parser_id: Parser identifier
505
- parser_name: Parser name
506
- bridge_logs_url: WebSocket URL for Bridge logs
507
- log_file: Local log file path (if None, creates default path)
508
- **kwargs: Additional configuration
509
-
510
- Returns:
511
- Configured UnifiedLogger instance
512
- """
513
- # Auto-create log file path if not provided
514
- if log_file is None and kwargs.get('file_enabled', True):
515
- log_file = resolve_log_path(f"logs/{parser_name}.log")
516
-
517
- config = UnifiedLoggerConfig(
518
- parser_id=parser_id,
519
- parser_name=parser_name,
520
- bridge_logs_url=bridge_logs_url,
521
- log_file=log_file,
522
- **kwargs
523
- )
524
-
525
- return UnifiedLogger(config)
@@ -1,31 +0,0 @@
1
- """
2
- WebSocket module for unrealon_driver.
3
-
4
- Provides independent WebSocket connectivity for:
5
- - Logging transport
6
- - HTML analysis requests
7
- - Other driver-server communication
8
-
9
- Features automatic URL detection - no need to specify URLs in config files!
10
- """
11
-
12
- from .client import WebSocketClient, WebSocketConfig
13
- from .manager import WebSocketManager
14
- from .config import (
15
- GlobalWebSocketConfig, Environment, global_websocket_config,
16
- get_websocket_url, get_environment, set_environment,
17
- get_debug_info, is_production, is_development, is_local
18
- )
19
-
20
- # Global websocket manager instance
21
- websocket_manager = WebSocketManager()
22
-
23
- __all__ = [
24
- # Client and manager
25
- "WebSocketClient", "WebSocketConfig", "WebSocketManager", "websocket_manager",
26
- # Global configuration
27
- "GlobalWebSocketConfig", "Environment", "global_websocket_config",
28
- # Convenience functions
29
- "get_websocket_url", "get_environment", "set_environment",
30
- "get_debug_info", "is_production", "is_development", "is_local"
31
- ]