unrealon 1.0.9__py3-none-any.whl → 1.1.1__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 (302) hide show
  1. unrealon/__init__.py +23 -21
  2. unrealon-1.1.1.dist-info/METADATA +722 -0
  3. unrealon-1.1.1.dist-info/RECORD +82 -0
  4. {unrealon-1.0.9.dist-info → unrealon-1.1.1.dist-info}/WHEEL +1 -1
  5. unrealon-1.1.1.dist-info/entry_points.txt +9 -0
  6. {unrealon-1.0.9.dist-info → unrealon-1.1.1.dist-info/licenses}/LICENSE +1 -1
  7. unrealon_bridge/__init__.py +114 -0
  8. unrealon_bridge/cli.py +316 -0
  9. unrealon_bridge/client/__init__.py +93 -0
  10. unrealon_bridge/client/base.py +78 -0
  11. unrealon_bridge/client/commands.py +89 -0
  12. unrealon_bridge/client/connection.py +90 -0
  13. unrealon_bridge/client/events.py +65 -0
  14. unrealon_bridge/client/health.py +38 -0
  15. unrealon_bridge/client/html_parser.py +146 -0
  16. unrealon_bridge/client/logging.py +139 -0
  17. unrealon_bridge/client/proxy.py +70 -0
  18. unrealon_bridge/client/scheduler.py +450 -0
  19. unrealon_bridge/client/session.py +70 -0
  20. unrealon_bridge/configs/__init__.py +14 -0
  21. unrealon_bridge/configs/bridge_config.py +212 -0
  22. unrealon_bridge/configs/bridge_config.yaml +39 -0
  23. unrealon_bridge/models/__init__.py +138 -0
  24. unrealon_bridge/models/base.py +28 -0
  25. unrealon_bridge/models/command.py +41 -0
  26. unrealon_bridge/models/events.py +40 -0
  27. unrealon_bridge/models/html_parser.py +79 -0
  28. unrealon_bridge/models/logging.py +55 -0
  29. unrealon_bridge/models/parser.py +63 -0
  30. unrealon_bridge/models/proxy.py +41 -0
  31. unrealon_bridge/models/requests.py +95 -0
  32. unrealon_bridge/models/responses.py +88 -0
  33. unrealon_bridge/models/scheduler.py +592 -0
  34. unrealon_bridge/models/session.py +28 -0
  35. unrealon_bridge/server/__init__.py +91 -0
  36. unrealon_bridge/server/base.py +171 -0
  37. unrealon_bridge/server/handlers/__init__.py +23 -0
  38. unrealon_bridge/server/handlers/command.py +110 -0
  39. unrealon_bridge/server/handlers/html_parser.py +139 -0
  40. unrealon_bridge/server/handlers/logging.py +95 -0
  41. unrealon_bridge/server/handlers/parser.py +95 -0
  42. unrealon_bridge/server/handlers/proxy.py +75 -0
  43. unrealon_bridge/server/handlers/scheduler.py +545 -0
  44. unrealon_bridge/server/handlers/session.py +66 -0
  45. unrealon_browser/__init__.py +61 -18
  46. unrealon_browser/{src/cli → cli}/browser_cli.py +6 -13
  47. unrealon_browser/{src/cli → cli}/cookies_cli.py +5 -1
  48. unrealon_browser/{src/core → core}/browser_manager.py +2 -2
  49. unrealon_browser/{src/managers → managers}/captcha.py +1 -1
  50. unrealon_browser/{src/managers → managers}/cookies.py +1 -1
  51. unrealon_browser/managers/logger_bridge.py +231 -0
  52. unrealon_browser/{src/managers → managers}/profile.py +1 -1
  53. unrealon_driver/__init__.py +73 -19
  54. unrealon_driver/browser/__init__.py +8 -0
  55. unrealon_driver/browser/config.py +74 -0
  56. unrealon_driver/browser/manager.py +416 -0
  57. unrealon_driver/exceptions.py +28 -0
  58. unrealon_driver/parser/__init__.py +55 -0
  59. unrealon_driver/parser/cli_manager.py +141 -0
  60. unrealon_driver/parser/daemon_manager.py +227 -0
  61. unrealon_driver/parser/managers/__init__.py +46 -0
  62. unrealon_driver/parser/managers/browser.py +51 -0
  63. unrealon_driver/parser/managers/config.py +281 -0
  64. unrealon_driver/parser/managers/error.py +412 -0
  65. unrealon_driver/parser/managers/html.py +732 -0
  66. unrealon_driver/parser/managers/logging.py +609 -0
  67. unrealon_driver/parser/managers/result.py +321 -0
  68. unrealon_driver/parser/parser_manager.py +628 -0
  69. unrealon/sdk_config.py +0 -88
  70. unrealon-1.0.9.dist-info/METADATA +0 -810
  71. unrealon-1.0.9.dist-info/RECORD +0 -246
  72. unrealon_browser/pyproject.toml +0 -182
  73. unrealon_browser/src/__init__.py +0 -62
  74. unrealon_browser/src/managers/logger_bridge.py +0 -395
  75. unrealon_driver/README.md +0 -204
  76. unrealon_driver/pyproject.toml +0 -187
  77. unrealon_driver/src/__init__.py +0 -90
  78. unrealon_driver/src/cli/__init__.py +0 -10
  79. unrealon_driver/src/cli/main.py +0 -66
  80. unrealon_driver/src/cli/simple.py +0 -510
  81. unrealon_driver/src/config/__init__.py +0 -11
  82. unrealon_driver/src/config/auto_config.py +0 -478
  83. unrealon_driver/src/core/__init__.py +0 -18
  84. unrealon_driver/src/core/exceptions.py +0 -289
  85. unrealon_driver/src/core/parser.py +0 -638
  86. unrealon_driver/src/dto/__init__.py +0 -66
  87. unrealon_driver/src/dto/cli.py +0 -119
  88. unrealon_driver/src/dto/config.py +0 -18
  89. unrealon_driver/src/dto/events.py +0 -237
  90. unrealon_driver/src/dto/execution.py +0 -313
  91. unrealon_driver/src/dto/services.py +0 -311
  92. unrealon_driver/src/execution/__init__.py +0 -23
  93. unrealon_driver/src/execution/daemon_mode.py +0 -317
  94. unrealon_driver/src/execution/interactive_mode.py +0 -88
  95. unrealon_driver/src/execution/modes.py +0 -45
  96. unrealon_driver/src/execution/scheduled_mode.py +0 -209
  97. unrealon_driver/src/execution/test_mode.py +0 -250
  98. unrealon_driver/src/logging/__init__.py +0 -24
  99. unrealon_driver/src/logging/driver_logger.py +0 -512
  100. unrealon_driver/src/services/__init__.py +0 -24
  101. unrealon_driver/src/services/browser_service.py +0 -726
  102. unrealon_driver/src/services/llm/__init__.py +0 -15
  103. unrealon_driver/src/services/llm/browser_llm_service.py +0 -363
  104. unrealon_driver/src/services/llm/llm.py +0 -195
  105. unrealon_driver/src/services/logger_service.py +0 -232
  106. unrealon_driver/src/services/metrics_service.py +0 -185
  107. unrealon_driver/src/services/scheduler_service.py +0 -489
  108. unrealon_driver/src/services/websocket_service.py +0 -362
  109. unrealon_driver/src/utils/__init__.py +0 -16
  110. unrealon_driver/src/utils/service_factory.py +0 -317
  111. unrealon_driver/src/utils/time_formatter.py +0 -338
  112. unrealon_llm/README.md +0 -44
  113. unrealon_llm/__init__.py +0 -26
  114. unrealon_llm/pyproject.toml +0 -154
  115. unrealon_llm/src/__init__.py +0 -228
  116. unrealon_llm/src/cli/__init__.py +0 -0
  117. unrealon_llm/src/core/__init__.py +0 -11
  118. unrealon_llm/src/core/smart_client.py +0 -438
  119. unrealon_llm/src/dto/__init__.py +0 -155
  120. unrealon_llm/src/dto/models/__init__.py +0 -0
  121. unrealon_llm/src/dto/models/config.py +0 -343
  122. unrealon_llm/src/dto/models/core.py +0 -328
  123. unrealon_llm/src/dto/models/enums.py +0 -123
  124. unrealon_llm/src/dto/models/html_analysis.py +0 -345
  125. unrealon_llm/src/dto/models/statistics.py +0 -473
  126. unrealon_llm/src/dto/models/translation.py +0 -383
  127. unrealon_llm/src/dto/models/type_conversion.py +0 -462
  128. unrealon_llm/src/dto/schemas/__init__.py +0 -0
  129. unrealon_llm/src/exceptions.py +0 -392
  130. unrealon_llm/src/llm_config/__init__.py +0 -20
  131. unrealon_llm/src/llm_config/logging_config.py +0 -178
  132. unrealon_llm/src/llm_logging/__init__.py +0 -42
  133. unrealon_llm/src/llm_logging/llm_events.py +0 -107
  134. unrealon_llm/src/llm_logging/llm_logger.py +0 -466
  135. unrealon_llm/src/managers/__init__.py +0 -15
  136. unrealon_llm/src/managers/cache_manager.py +0 -67
  137. unrealon_llm/src/managers/cost_manager.py +0 -107
  138. unrealon_llm/src/managers/request_manager.py +0 -298
  139. unrealon_llm/src/modules/__init__.py +0 -0
  140. unrealon_llm/src/modules/html_processor/__init__.py +0 -25
  141. unrealon_llm/src/modules/html_processor/base_processor.py +0 -415
  142. unrealon_llm/src/modules/html_processor/details_processor.py +0 -85
  143. unrealon_llm/src/modules/html_processor/listing_processor.py +0 -91
  144. unrealon_llm/src/modules/html_processor/models/__init__.py +0 -20
  145. unrealon_llm/src/modules/html_processor/models/processing_models.py +0 -40
  146. unrealon_llm/src/modules/html_processor/models/universal_model.py +0 -56
  147. unrealon_llm/src/modules/html_processor/processor.py +0 -102
  148. unrealon_llm/src/modules/llm/__init__.py +0 -0
  149. unrealon_llm/src/modules/translator/__init__.py +0 -0
  150. unrealon_llm/src/provider.py +0 -116
  151. unrealon_llm/src/utils/__init__.py +0 -95
  152. unrealon_llm/src/utils/common.py +0 -64
  153. unrealon_llm/src/utils/data_extractor.py +0 -188
  154. unrealon_llm/src/utils/html_cleaner.py +0 -767
  155. unrealon_llm/src/utils/language_detector.py +0 -308
  156. unrealon_llm/src/utils/models_cache.py +0 -592
  157. unrealon_llm/src/utils/smart_counter.py +0 -229
  158. unrealon_llm/src/utils/token_counter.py +0 -189
  159. unrealon_sdk/README.md +0 -25
  160. unrealon_sdk/__init__.py +0 -30
  161. unrealon_sdk/pyproject.toml +0 -231
  162. unrealon_sdk/src/__init__.py +0 -150
  163. unrealon_sdk/src/cli/__init__.py +0 -12
  164. unrealon_sdk/src/cli/commands/__init__.py +0 -22
  165. unrealon_sdk/src/cli/commands/benchmark.py +0 -42
  166. unrealon_sdk/src/cli/commands/diagnostics.py +0 -573
  167. unrealon_sdk/src/cli/commands/health.py +0 -46
  168. unrealon_sdk/src/cli/commands/integration.py +0 -498
  169. unrealon_sdk/src/cli/commands/reports.py +0 -43
  170. unrealon_sdk/src/cli/commands/security.py +0 -36
  171. unrealon_sdk/src/cli/commands/server.py +0 -483
  172. unrealon_sdk/src/cli/commands/servers.py +0 -56
  173. unrealon_sdk/src/cli/commands/tests.py +0 -55
  174. unrealon_sdk/src/cli/main.py +0 -126
  175. unrealon_sdk/src/cli/utils/reporter.py +0 -519
  176. unrealon_sdk/src/clients/openapi.yaml +0 -3347
  177. unrealon_sdk/src/clients/python_http/__init__.py +0 -3
  178. unrealon_sdk/src/clients/python_http/api_config.py +0 -228
  179. unrealon_sdk/src/clients/python_http/models/BaseModel.py +0 -12
  180. unrealon_sdk/src/clients/python_http/models/BroadcastDeliveryStats.py +0 -33
  181. unrealon_sdk/src/clients/python_http/models/BroadcastMessage.py +0 -17
  182. unrealon_sdk/src/clients/python_http/models/BroadcastMessageRequest.py +0 -35
  183. unrealon_sdk/src/clients/python_http/models/BroadcastPriority.py +0 -10
  184. unrealon_sdk/src/clients/python_http/models/BroadcastResponse.py +0 -21
  185. unrealon_sdk/src/clients/python_http/models/BroadcastResultResponse.py +0 -33
  186. unrealon_sdk/src/clients/python_http/models/BroadcastTarget.py +0 -11
  187. unrealon_sdk/src/clients/python_http/models/ConnectionStats.py +0 -27
  188. unrealon_sdk/src/clients/python_http/models/ConnectionsResponse.py +0 -21
  189. unrealon_sdk/src/clients/python_http/models/DeveloperMessageResponse.py +0 -23
  190. unrealon_sdk/src/clients/python_http/models/ErrorResponse.py +0 -25
  191. unrealon_sdk/src/clients/python_http/models/HTTPValidationError.py +0 -16
  192. unrealon_sdk/src/clients/python_http/models/HealthResponse.py +0 -23
  193. unrealon_sdk/src/clients/python_http/models/HealthStatus.py +0 -33
  194. unrealon_sdk/src/clients/python_http/models/LogLevel.py +0 -10
  195. unrealon_sdk/src/clients/python_http/models/LoggingRequest.py +0 -27
  196. unrealon_sdk/src/clients/python_http/models/LoggingResponse.py +0 -23
  197. unrealon_sdk/src/clients/python_http/models/MaintenanceMode.py +0 -9
  198. unrealon_sdk/src/clients/python_http/models/MaintenanceModeRequest.py +0 -33
  199. unrealon_sdk/src/clients/python_http/models/MaintenanceStatusResponse.py +0 -39
  200. unrealon_sdk/src/clients/python_http/models/ParserCommandRequest.py +0 -25
  201. unrealon_sdk/src/clients/python_http/models/ParserMessageResponse.py +0 -21
  202. unrealon_sdk/src/clients/python_http/models/ParserRegistrationRequest.py +0 -28
  203. unrealon_sdk/src/clients/python_http/models/ParserRegistrationResponse.py +0 -25
  204. unrealon_sdk/src/clients/python_http/models/ParserType.py +0 -10
  205. unrealon_sdk/src/clients/python_http/models/ProxyBlockRequest.py +0 -19
  206. unrealon_sdk/src/clients/python_http/models/ProxyEndpointResponse.py +0 -20
  207. unrealon_sdk/src/clients/python_http/models/ProxyListResponse.py +0 -19
  208. unrealon_sdk/src/clients/python_http/models/ProxyProvider.py +0 -10
  209. unrealon_sdk/src/clients/python_http/models/ProxyPurchaseRequest.py +0 -25
  210. unrealon_sdk/src/clients/python_http/models/ProxyResponse.py +0 -47
  211. unrealon_sdk/src/clients/python_http/models/ProxyRotationRequest.py +0 -23
  212. unrealon_sdk/src/clients/python_http/models/ProxyStatus.py +0 -10
  213. unrealon_sdk/src/clients/python_http/models/ProxyUsageRequest.py +0 -19
  214. unrealon_sdk/src/clients/python_http/models/ProxyUsageStatsResponse.py +0 -26
  215. unrealon_sdk/src/clients/python_http/models/ServiceRegistrationDto.py +0 -23
  216. unrealon_sdk/src/clients/python_http/models/ServiceStatsResponse.py +0 -31
  217. unrealon_sdk/src/clients/python_http/models/SessionStartRequest.py +0 -23
  218. unrealon_sdk/src/clients/python_http/models/SuccessResponse.py +0 -25
  219. unrealon_sdk/src/clients/python_http/models/SystemNotificationResponse.py +0 -23
  220. unrealon_sdk/src/clients/python_http/models/ValidationError.py +0 -18
  221. unrealon_sdk/src/clients/python_http/models/ValidationErrorResponse.py +0 -21
  222. unrealon_sdk/src/clients/python_http/models/WebSocketMetrics.py +0 -21
  223. unrealon_sdk/src/clients/python_http/models/__init__.py +0 -44
  224. unrealon_sdk/src/clients/python_http/services/None_service.py +0 -35
  225. unrealon_sdk/src/clients/python_http/services/ParserManagement_service.py +0 -190
  226. unrealon_sdk/src/clients/python_http/services/ProxyManagement_service.py +0 -289
  227. unrealon_sdk/src/clients/python_http/services/SocketLogging_service.py +0 -187
  228. unrealon_sdk/src/clients/python_http/services/SystemHealth_service.py +0 -119
  229. unrealon_sdk/src/clients/python_http/services/WebSocketAPI_service.py +0 -198
  230. unrealon_sdk/src/clients/python_http/services/__init__.py +0 -0
  231. unrealon_sdk/src/clients/python_http/services/admin_service.py +0 -125
  232. unrealon_sdk/src/clients/python_http/services/async_None_service.py +0 -35
  233. unrealon_sdk/src/clients/python_http/services/async_ParserManagement_service.py +0 -190
  234. unrealon_sdk/src/clients/python_http/services/async_ProxyManagement_service.py +0 -289
  235. unrealon_sdk/src/clients/python_http/services/async_SocketLogging_service.py +0 -189
  236. unrealon_sdk/src/clients/python_http/services/async_SystemHealth_service.py +0 -123
  237. unrealon_sdk/src/clients/python_http/services/async_WebSocketAPI_service.py +0 -200
  238. unrealon_sdk/src/clients/python_http/services/async_admin_service.py +0 -125
  239. unrealon_sdk/src/clients/python_websocket/__init__.py +0 -28
  240. unrealon_sdk/src/clients/python_websocket/client.py +0 -490
  241. unrealon_sdk/src/clients/python_websocket/events.py +0 -732
  242. unrealon_sdk/src/clients/python_websocket/example.py +0 -136
  243. unrealon_sdk/src/clients/python_websocket/types.py +0 -871
  244. unrealon_sdk/src/core/__init__.py +0 -64
  245. unrealon_sdk/src/core/client.py +0 -556
  246. unrealon_sdk/src/core/config.py +0 -465
  247. unrealon_sdk/src/core/exceptions.py +0 -239
  248. unrealon_sdk/src/core/metadata.py +0 -191
  249. unrealon_sdk/src/core/models.py +0 -142
  250. unrealon_sdk/src/core/types.py +0 -68
  251. unrealon_sdk/src/dto/__init__.py +0 -268
  252. unrealon_sdk/src/dto/authentication.py +0 -108
  253. unrealon_sdk/src/dto/cache.py +0 -208
  254. unrealon_sdk/src/dto/common.py +0 -19
  255. unrealon_sdk/src/dto/concurrency.py +0 -393
  256. unrealon_sdk/src/dto/events.py +0 -108
  257. unrealon_sdk/src/dto/health.py +0 -339
  258. unrealon_sdk/src/dto/load_balancing.py +0 -336
  259. unrealon_sdk/src/dto/logging.py +0 -230
  260. unrealon_sdk/src/dto/performance.py +0 -165
  261. unrealon_sdk/src/dto/rate_limiting.py +0 -295
  262. unrealon_sdk/src/dto/resource_pooling.py +0 -128
  263. unrealon_sdk/src/dto/structured_logging.py +0 -112
  264. unrealon_sdk/src/dto/task_scheduling.py +0 -121
  265. unrealon_sdk/src/dto/websocket.py +0 -55
  266. unrealon_sdk/src/enterprise/__init__.py +0 -59
  267. unrealon_sdk/src/enterprise/authentication.py +0 -401
  268. unrealon_sdk/src/enterprise/cache_manager.py +0 -578
  269. unrealon_sdk/src/enterprise/error_recovery.py +0 -494
  270. unrealon_sdk/src/enterprise/event_system.py +0 -549
  271. unrealon_sdk/src/enterprise/health_monitor.py +0 -747
  272. unrealon_sdk/src/enterprise/load_balancer.py +0 -964
  273. unrealon_sdk/src/enterprise/logging/__init__.py +0 -68
  274. unrealon_sdk/src/enterprise/logging/cleanup.py +0 -156
  275. unrealon_sdk/src/enterprise/logging/development.py +0 -744
  276. unrealon_sdk/src/enterprise/logging/service.py +0 -410
  277. unrealon_sdk/src/enterprise/multithreading_manager.py +0 -853
  278. unrealon_sdk/src/enterprise/performance_monitor.py +0 -539
  279. unrealon_sdk/src/enterprise/proxy_manager.py +0 -696
  280. unrealon_sdk/src/enterprise/rate_limiter.py +0 -652
  281. unrealon_sdk/src/enterprise/resource_pool.py +0 -763
  282. unrealon_sdk/src/enterprise/task_scheduler.py +0 -709
  283. unrealon_sdk/src/internal/__init__.py +0 -10
  284. unrealon_sdk/src/internal/command_router.py +0 -497
  285. unrealon_sdk/src/internal/connection_manager.py +0 -397
  286. unrealon_sdk/src/internal/http_client.py +0 -446
  287. unrealon_sdk/src/internal/websocket_client.py +0 -420
  288. unrealon_sdk/src/provider.py +0 -471
  289. unrealon_sdk/src/utils.py +0 -234
  290. /unrealon_browser/{src/cli → cli}/__init__.py +0 -0
  291. /unrealon_browser/{src/cli → cli}/interactive_mode.py +0 -0
  292. /unrealon_browser/{src/cli → cli}/main.py +0 -0
  293. /unrealon_browser/{src/core → core}/__init__.py +0 -0
  294. /unrealon_browser/{src/dto → dto}/__init__.py +0 -0
  295. /unrealon_browser/{src/dto → dto}/models/config.py +0 -0
  296. /unrealon_browser/{src/dto → dto}/models/core.py +0 -0
  297. /unrealon_browser/{src/dto → dto}/models/dataclasses.py +0 -0
  298. /unrealon_browser/{src/dto → dto}/models/detection.py +0 -0
  299. /unrealon_browser/{src/dto → dto}/models/enums.py +0 -0
  300. /unrealon_browser/{src/dto → dto}/models/statistics.py +0 -0
  301. /unrealon_browser/{src/managers → managers}/__init__.py +0 -0
  302. /unrealon_browser/{src/managers → managers}/stealth.py +0 -0
@@ -0,0 +1,609 @@
1
+ """
2
+ Logging Manager - Universal logging for parser developers with Pydantic v2
3
+
4
+ Strict compliance with CRITICAL_REQUIREMENTS.md:
5
+ - No Dict[str, Any] usage
6
+ - Complete type annotations
7
+ - Pydantic v2 models everywhere
8
+ - Custom exception hierarchy
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+ import sys
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Optional, Union, Any
17
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
18
+ from enum import Enum
19
+
20
+ from rich.console import Console
21
+ from rich.logging import RichHandler
22
+ from rich.text import Text
23
+
24
+ from unrealon_rpc.logging import get_logger
25
+
26
+
27
+ class LogLevel(str, Enum):
28
+ """Log levels for driver logger"""
29
+ DEBUG = "DEBUG"
30
+ INFO = "INFO"
31
+ WARNING = "WARNING"
32
+ ERROR = "ERROR"
33
+ CRITICAL = "CRITICAL"
34
+
35
+
36
+ class LoggingConfig(BaseModel):
37
+ """Logging configuration with strict typing"""
38
+ model_config = ConfigDict(
39
+ validate_assignment=True,
40
+ extra="forbid"
41
+ )
42
+
43
+ parser_name: str = Field(
44
+ ...,
45
+ min_length=1,
46
+ description="Name of the parser"
47
+ )
48
+ log_dir: Path = Field(
49
+ default_factory=lambda: Path.cwd() / "logs",
50
+ description="Directory for log files"
51
+ )
52
+ console_enabled: bool = Field(
53
+ default=True,
54
+ description="Enable console output"
55
+ )
56
+ file_enabled: bool = Field(
57
+ default=True,
58
+ description="Enable file logging"
59
+ )
60
+ bridge_enabled: bool = Field(
61
+ default=True,
62
+ description="Enable bridge logging"
63
+ )
64
+ console_level: LogLevel = Field(
65
+ default=LogLevel.DEBUG,
66
+ description="Minimum level for console output"
67
+ )
68
+ file_level: LogLevel = Field(
69
+ default=LogLevel.DEBUG,
70
+ description="Minimum level for file logging"
71
+ )
72
+ bridge_level: LogLevel = Field(
73
+ default=LogLevel.DEBUG,
74
+ description="Minimum level for bridge logging"
75
+ )
76
+
77
+ @field_validator('parser_name')
78
+ @classmethod
79
+ def validate_parser_name(cls, v: str) -> str:
80
+ """Validate parser name is not empty"""
81
+ if not v.strip():
82
+ raise ValueError("Parser name cannot be empty")
83
+ return v.strip()
84
+
85
+ def model_post_init(self, __context) -> None:
86
+ """Create log directory if it doesn't exist"""
87
+ self.log_dir.mkdir(parents=True, exist_ok=True)
88
+
89
+
90
+ class LogContext(BaseModel):
91
+ """Log context information"""
92
+ model_config = ConfigDict(
93
+ validate_assignment=True,
94
+ extra="forbid"
95
+ )
96
+
97
+ session_id: Optional[str] = Field(default=None)
98
+ command_id: Optional[str] = Field(default=None)
99
+ operation: Optional[str] = Field(default=None)
100
+ url: Optional[str] = Field(default=None)
101
+ additional_data: dict[str, str] = Field(default_factory=dict)
102
+
103
+
104
+ class LogEntry(BaseModel):
105
+ """Log entry with structured data"""
106
+ model_config = ConfigDict(
107
+ validate_assignment=True,
108
+ extra="forbid"
109
+ )
110
+
111
+ level: LogLevel = Field(...)
112
+ message: str = Field(..., min_length=1)
113
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
114
+ parser_name: str = Field(..., min_length=1)
115
+ context: LogContext = Field(default_factory=LogContext)
116
+
117
+
118
+ class LoggingManagerError(Exception):
119
+ """Base exception for logging manager"""
120
+ def __init__(self, message: str, operation: str, details: Optional[dict[str, str]] = None):
121
+ self.message = message
122
+ self.operation = operation
123
+ self.details = details or {}
124
+ super().__init__(message)
125
+
126
+
127
+ class BridgeLoggingError(LoggingManagerError):
128
+ """Raised when bridge logging fails"""
129
+ pass
130
+
131
+
132
+ class FileLoggingError(LoggingManagerError):
133
+ """Raised when file logging fails"""
134
+ pass
135
+
136
+
137
+ class LoggingManager:
138
+ """
139
+ 📝 Logging Manager - Universal logging for parser developers
140
+
141
+ Features:
142
+ - Rich console output with colors and formatting
143
+ - File logging to developer-specified directory
144
+ - Bridge logging (sends all logs to Django via bridge)
145
+ - Easy-to-use API with multiple log levels
146
+ - Configurable output formats and destinations
147
+ - Type-safe logging with Pydantic v2
148
+ """
149
+
150
+ def __init__(
151
+ self,
152
+ config: LoggingConfig,
153
+ bridge_client: Optional[Any] = None
154
+ ):
155
+ self.config = config
156
+ self.bridge_client = bridge_client
157
+ self._context = LogContext()
158
+
159
+ # Setup console
160
+ if self.config.console_enabled:
161
+ self.console = Console()
162
+ else:
163
+ self.console = None
164
+
165
+ # Setup file logger
166
+ self.file_logger: Optional[logging.Logger] = None
167
+ if self.config.file_enabled:
168
+ self._setup_file_logger()
169
+
170
+ # Fallback logger for internal logging
171
+ self.internal_logger = get_logger()
172
+
173
+ def _setup_file_logger(self) -> None:
174
+ """Setup file logger"""
175
+ log_file = self._get_log_file_path()
176
+
177
+ # Ensure log directory exists
178
+ log_file.parent.mkdir(parents=True, exist_ok=True)
179
+
180
+ # Create file logger
181
+ logger_name = f"driver_{self.config.parser_name}"
182
+ self.file_logger = logging.getLogger(logger_name)
183
+ self.file_logger.setLevel(getattr(logging, self.config.file_level.value))
184
+
185
+ # Remove existing handlers
186
+ for handler in self.file_logger.handlers[:]:
187
+ self.file_logger.removeHandler(handler)
188
+
189
+ # Add file handler
190
+ file_handler = logging.FileHandler(log_file, encoding='utf-8')
191
+ file_handler.setLevel(getattr(logging, self.config.file_level.value))
192
+
193
+ # Format for file logging
194
+ formatter = logging.Formatter(
195
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
196
+ datefmt='%Y-%m-%d %H:%M:%S'
197
+ )
198
+ file_handler.setFormatter(formatter)
199
+
200
+ self.file_logger.addHandler(file_handler)
201
+ self.file_logger.propagate = False
202
+
203
+ # ==========================================
204
+ # CONTEXT MANAGEMENT
205
+ # ==========================================
206
+
207
+ def set_session(self, session_id: str) -> None:
208
+ """Set current session ID for context"""
209
+ if not session_id.strip():
210
+ raise ValueError("Session ID cannot be empty")
211
+ self._context.session_id = session_id
212
+
213
+ def set_command(self, command_id: str) -> None:
214
+ """Set current command ID for context"""
215
+ if not command_id.strip():
216
+ raise ValueError("Command ID cannot be empty")
217
+ self._context.command_id = command_id
218
+
219
+ def set_operation(self, operation: str) -> None:
220
+ """Set current operation for context"""
221
+ if not operation.strip():
222
+ raise ValueError("Operation cannot be empty")
223
+ self._context.operation = operation
224
+
225
+ def set_url(self, url: str) -> None:
226
+ """Set current URL for context"""
227
+ if not url.strip():
228
+ raise ValueError("URL cannot be empty")
229
+ self._context.url = url
230
+
231
+ def add_context_data(self, key: str, value: str) -> None:
232
+ """Add additional context data"""
233
+ if not key.strip():
234
+ raise ValueError("Context key cannot be empty")
235
+ self._context.additional_data[key] = str(value)
236
+
237
+ def clear_context(self) -> None:
238
+ """Clear all context"""
239
+ self._context = LogContext()
240
+
241
+ # ==========================================
242
+ # SYNCHRONOUS LOGGING METHODS
243
+ # ==========================================
244
+
245
+ def debug(self, message: str, **kwargs) -> None:
246
+ """Log debug message"""
247
+ self._log(LogLevel.DEBUG, message, **kwargs)
248
+
249
+ def info(self, message: str, **kwargs) -> None:
250
+ """Log info message"""
251
+ self._log(LogLevel.INFO, message, **kwargs)
252
+
253
+ def warning(self, message: str, **kwargs) -> None:
254
+ """Log warning message"""
255
+ self._log(LogLevel.WARNING, message, **kwargs)
256
+
257
+ def error(self, message: str, **kwargs) -> None:
258
+ """Log error message"""
259
+ self._log(LogLevel.ERROR, message, **kwargs)
260
+
261
+ def critical(self, message: str, **kwargs) -> None:
262
+ """Log critical message"""
263
+ self._log(LogLevel.CRITICAL, message, **kwargs)
264
+
265
+ # Aliases
266
+ warn = warning
267
+
268
+ # ==========================================
269
+ # ASYNCHRONOUS LOGGING METHODS
270
+ # ==========================================
271
+
272
+ async def debug_async(self, message: str, **kwargs) -> None:
273
+ """Log debug message (async)"""
274
+ await self._log_async(LogLevel.DEBUG, message, **kwargs)
275
+
276
+ async def info_async(self, message: str, **kwargs) -> None:
277
+ """Log info message (async)"""
278
+ await self._log_async(LogLevel.INFO, message, **kwargs)
279
+
280
+ async def warning_async(self, message: str, **kwargs) -> None:
281
+ """Log warning message (async)"""
282
+ await self._log_async(LogLevel.WARNING, message, **kwargs)
283
+
284
+ async def error_async(self, message: str, **kwargs) -> None:
285
+ """Log error message (async)"""
286
+ await self._log_async(LogLevel.ERROR, message, **kwargs)
287
+
288
+ async def critical_async(self, message: str, **kwargs) -> None:
289
+ """Log critical message (async)"""
290
+ await self._log_async(LogLevel.CRITICAL, message, **kwargs)
291
+
292
+ # Aliases
293
+ warn_async = warning_async
294
+
295
+ # ==========================================
296
+ # SPECIALIZED LOGGING METHODS
297
+ # ==========================================
298
+
299
+ def start_operation(self, operation: str, **kwargs) -> None:
300
+ """Log start of operation"""
301
+ self.set_operation(operation)
302
+ self.info(f"🚀 Starting {operation}", **kwargs)
303
+
304
+ def end_operation(self, operation: str, duration: Optional[float] = None, **kwargs) -> None:
305
+ """Log end of operation"""
306
+ if duration is not None:
307
+ self.info(f"✅ Completed {operation} in {duration:.2f}s", duration=str(duration), **kwargs)
308
+ else:
309
+ self.info(f"✅ Completed {operation}", **kwargs)
310
+
311
+ def fail_operation(self, operation: str, error: str, **kwargs) -> None:
312
+ """Log failed operation"""
313
+ self.error(f"❌ Failed {operation}: {error}", error=error, **kwargs)
314
+
315
+ def progress(self, message: str, current: int, total: int, **kwargs) -> None:
316
+ """Log progress information"""
317
+ if total <= 0:
318
+ raise ValueError("Total must be positive")
319
+ if current < 0:
320
+ raise ValueError("Current must be non-negative")
321
+
322
+ percentage = (current / total * 100)
323
+ self.info(
324
+ f"📊 {message} ({current}/{total} - {percentage:.1f}%)",
325
+ current=str(current),
326
+ total=str(total),
327
+ percentage=f"{percentage:.1f}",
328
+ **kwargs
329
+ )
330
+
331
+ def url_access(self, url: str, status: str = "accessing", **kwargs) -> None:
332
+ """Log URL access"""
333
+ self.set_url(url)
334
+ self.info(f"🌐 {status.title()} URL: {url}", status=status, **kwargs)
335
+
336
+ def data_extracted(self, data_type: str, count: int, **kwargs) -> None:
337
+ """Log data extraction"""
338
+ if count < 0:
339
+ raise ValueError("Count must be non-negative")
340
+
341
+ self.info(
342
+ f"📦 Extracted {count} {data_type}",
343
+ data_type=data_type,
344
+ count=str(count),
345
+ **kwargs
346
+ )
347
+
348
+ # ==========================================
349
+ # INTERNAL LOGGING IMPLEMENTATION
350
+ # ==========================================
351
+
352
+ def _log(self, level: LogLevel, message: str, **kwargs) -> None:
353
+ """Internal synchronous logging"""
354
+ # Create log entry
355
+ context = self._create_context(**kwargs)
356
+ log_entry = LogEntry(
357
+ level=level,
358
+ message=message,
359
+ parser_name=self.config.parser_name,
360
+ context=context
361
+ )
362
+
363
+ # Console output
364
+ if self.config.console_enabled and self._should_log_to_console(level):
365
+ self._log_to_console(log_entry)
366
+
367
+ # File output
368
+ if self.config.file_enabled and self._should_log_to_file(level):
369
+ self._log_to_file(log_entry)
370
+
371
+ # Bridge output (async in background)
372
+ if self.config.bridge_enabled and self.bridge_client and self._should_log_to_bridge(level):
373
+ try:
374
+ loop = asyncio.get_event_loop()
375
+ if loop.is_running():
376
+ loop.create_task(self._log_to_bridge_async(log_entry))
377
+ except RuntimeError:
378
+ # No event loop, skip bridge logging
379
+ pass
380
+
381
+ async def _log_async(self, level: LogLevel, message: str, **kwargs) -> None:
382
+ """Internal asynchronous logging"""
383
+ # Create log entry
384
+ context = self._create_context(**kwargs)
385
+ log_entry = LogEntry(
386
+ level=level,
387
+ message=message,
388
+ parser_name=self.config.parser_name,
389
+ context=context
390
+ )
391
+
392
+ # Console output
393
+ if self.config.console_enabled and self._should_log_to_console(level):
394
+ self._log_to_console(log_entry)
395
+
396
+ # File output
397
+ if self.config.file_enabled and self._should_log_to_file(level):
398
+ self._log_to_file(log_entry)
399
+
400
+ # Bridge output
401
+ if self.config.bridge_enabled and self.bridge_client and self._should_log_to_bridge(level):
402
+ await self._log_to_bridge_async(log_entry)
403
+
404
+ def _create_context(self, **kwargs) -> LogContext:
405
+ """Create log context from current context and kwargs"""
406
+ context_data = self._context.additional_data.copy()
407
+
408
+ # Add kwargs as string values
409
+ for key, value in kwargs.items():
410
+ if key not in ['session_id', 'command_id', 'operation', 'url']:
411
+ context_data[key] = str(value)
412
+
413
+ return LogContext(
414
+ session_id=kwargs.get('session_id') or self._context.session_id,
415
+ command_id=kwargs.get('command_id') or self._context.command_id,
416
+ operation=kwargs.get('operation') or self._context.operation,
417
+ url=kwargs.get('url') or self._context.url,
418
+ additional_data=context_data
419
+ )
420
+
421
+ def _should_log_to_console(self, level: LogLevel) -> bool:
422
+ """Check if should log to console"""
423
+ level_value = getattr(logging, level.value)
424
+ console_level_value = getattr(logging, self.config.console_level.value)
425
+ return level_value >= console_level_value
426
+
427
+ def _should_log_to_file(self, level: LogLevel) -> bool:
428
+ """Check if should log to file"""
429
+ level_value = getattr(logging, level.value)
430
+ file_level_value = getattr(logging, self.config.file_level.value)
431
+ return level_value >= file_level_value
432
+
433
+ def _should_log_to_bridge(self, level: LogLevel) -> bool:
434
+ """Check if should log to bridge"""
435
+ level_value = getattr(logging, level.value)
436
+ bridge_level_value = getattr(logging, self.config.bridge_level.value)
437
+ return level_value >= bridge_level_value
438
+
439
+ def _log_to_console(self, log_entry: LogEntry) -> None:
440
+ """Log to console with rich formatting"""
441
+ if not self.console:
442
+ # Fallback to print if rich not available
443
+ time_str = log_entry.timestamp.strftime('%H:%M:%S')
444
+ print(f"[{time_str}] {log_entry.level.value} - {log_entry.parser_name} - {log_entry.message}")
445
+ return
446
+
447
+ # Rich console output
448
+ time_str = log_entry.timestamp.strftime('%H:%M:%S')
449
+
450
+ # Color based on level
451
+ level_colors = {
452
+ LogLevel.DEBUG: "dim white",
453
+ LogLevel.INFO: "bright_blue",
454
+ LogLevel.WARNING: "yellow",
455
+ LogLevel.ERROR: "red",
456
+ LogLevel.CRITICAL: "bold red"
457
+ }
458
+
459
+ level_color = level_colors.get(log_entry.level, "white")
460
+
461
+ # Format message
462
+ formatted_message = Text()
463
+ formatted_message.append(f"[{time_str}] ", style="dim")
464
+ formatted_message.append(f"{log_entry.level.value}", style=level_color)
465
+ formatted_message.append(f" - {log_entry.parser_name} - ", style="dim")
466
+ formatted_message.append(log_entry.message, style="white")
467
+
468
+ # Add context if available
469
+ context_parts = []
470
+ if log_entry.context.additional_data:
471
+ for key, value in log_entry.context.additional_data.items():
472
+ context_parts.append(f"{key}={value}")
473
+
474
+ if context_parts:
475
+ formatted_message.append(f" ({', '.join(context_parts)})", style="dim")
476
+
477
+ self.console.print(formatted_message)
478
+
479
+ def _log_to_file(self, log_entry: LogEntry) -> None:
480
+ """Log to file"""
481
+ if not self.file_logger:
482
+ return
483
+
484
+ try:
485
+ # Create log message with context
486
+ extra_info = ""
487
+ if log_entry.context.additional_data:
488
+ context_parts = [f"{k}={v}" for k, v in log_entry.context.additional_data.items()]
489
+ extra_info = f" - {', '.join(context_parts)}"
490
+
491
+ full_message = f"{log_entry.message}{extra_info}"
492
+
493
+ # Log to file
494
+ log_level = getattr(logging, log_entry.level.value)
495
+ self.file_logger.log(log_level, full_message)
496
+
497
+ except Exception as e:
498
+ raise FileLoggingError(
499
+ message=f"Failed to write to log file: {e}",
500
+ operation="file_logging",
501
+ details={"log_file": str(self._get_log_file_path())}
502
+ ) from e
503
+
504
+ async def _log_to_bridge_async(self, log_entry: LogEntry) -> None:
505
+ """Log to bridge asynchronously"""
506
+ if not self.bridge_client:
507
+ return
508
+
509
+ try:
510
+ # Send to bridge
511
+ await self.bridge_client.send_log(
512
+ level=log_entry.level.value,
513
+ message=log_entry.message,
514
+ session_id=log_entry.context.session_id,
515
+ command_id=log_entry.context.command_id,
516
+ operation=log_entry.context.operation,
517
+ url=log_entry.context.url,
518
+ data=log_entry.context.additional_data if log_entry.context.additional_data else None,
519
+ error_details=log_entry.context.additional_data.get('error') if log_entry.level in [LogLevel.ERROR, LogLevel.CRITICAL] else None
520
+ )
521
+
522
+ except Exception as e:
523
+ # Log bridge error to internal logger but don't raise
524
+ self.internal_logger.warning(f"Failed to send log to bridge: {e}")
525
+
526
+ # ==========================================
527
+ # UTILITY METHODS
528
+ # ==========================================
529
+
530
+ def _get_log_file_path(self) -> Path:
531
+ """Get path to log file"""
532
+ safe_name = self.config.parser_name.lower().replace(' ', '_')
533
+ return self.config.log_dir / f"{safe_name}.log"
534
+
535
+ def get_log_file_path(self) -> Path:
536
+ """Get path to log file (public method)"""
537
+ return self._get_log_file_path()
538
+
539
+ def clear_log_file(self) -> None:
540
+ """Clear log file"""
541
+ log_file = self._get_log_file_path()
542
+ try:
543
+ if log_file.exists():
544
+ log_file.write_text("")
545
+ self.info("Log file cleared")
546
+ except Exception as e:
547
+ raise FileLoggingError(
548
+ message=f"Failed to clear log file: {e}",
549
+ operation="clear_log_file",
550
+ details={"log_file": str(log_file)}
551
+ ) from e
552
+
553
+ def get_log_stats(self) -> dict[str, str]:
554
+ """Get logging statistics"""
555
+ log_file = self._get_log_file_path()
556
+
557
+ return {
558
+ "parser_name": self.config.parser_name,
559
+ "log_dir": str(self.config.log_dir),
560
+ "log_file": str(log_file),
561
+ "log_file_exists": str(log_file.exists()),
562
+ "log_file_size": str(log_file.stat().st_size if log_file.exists() else 0),
563
+ "console_enabled": str(self.config.console_enabled),
564
+ "file_enabled": str(self.config.file_enabled),
565
+ "bridge_enabled": str(self.config.bridge_enabled),
566
+ "session_id": self._context.session_id or "",
567
+ "command_id": self._context.command_id or ""
568
+ }
569
+
570
+ def update_bridge_client(self, bridge_client: Any) -> None:
571
+ """Update bridge client"""
572
+ self.bridge_client = bridge_client
573
+
574
+ def __repr__(self) -> str:
575
+ return f"<LoggingManager(parser='{self.config.parser_name}', log_dir='{self.config.log_dir}')>"
576
+
577
+
578
+ # ==========================================
579
+ # CONVENIENCE FUNCTIONS
580
+ # ==========================================
581
+
582
+ def get_logging_manager(
583
+ parser_name: str,
584
+ log_dir: Optional[Union[str, Path]] = None,
585
+ bridge_client: Optional[Any] = None,
586
+ **kwargs
587
+ ) -> LoggingManager:
588
+ """
589
+ Get a logging manager instance
590
+
591
+ Args:
592
+ parser_name: Name of the parser
593
+ log_dir: Directory for log files
594
+ bridge_client: Bridge client for sending logs
595
+ **kwargs: Additional logger configuration
596
+
597
+ Returns:
598
+ Configured LoggingManager instance
599
+ """
600
+ config_data = {
601
+ "parser_name": parser_name,
602
+ **kwargs
603
+ }
604
+
605
+ if log_dir is not None:
606
+ config_data["log_dir"] = Path(log_dir)
607
+
608
+ config = LoggingConfig.model_validate(config_data)
609
+ return LoggingManager(config=config, bridge_client=bridge_client)