osn-selenium 0.0.0__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.
- osn_selenium/__init__.py +1 -0
- osn_selenium/browsers_handler/__init__.py +70 -0
- osn_selenium/browsers_handler/_windows.py +130 -0
- osn_selenium/browsers_handler/types.py +20 -0
- osn_selenium/captcha_workers/__init__.py +26 -0
- osn_selenium/dev_tools/__init__.py +1 -0
- osn_selenium/dev_tools/_types.py +22 -0
- osn_selenium/dev_tools/domains/__init__.py +63 -0
- osn_selenium/dev_tools/domains/abstract.py +378 -0
- osn_selenium/dev_tools/domains/fetch.py +1295 -0
- osn_selenium/dev_tools/domains_default/__init__.py +1 -0
- osn_selenium/dev_tools/domains_default/fetch.py +155 -0
- osn_selenium/dev_tools/errors.py +89 -0
- osn_selenium/dev_tools/logger.py +558 -0
- osn_selenium/dev_tools/manager.py +1551 -0
- osn_selenium/dev_tools/utils.py +509 -0
- osn_selenium/errors.py +16 -0
- osn_selenium/types.py +118 -0
- osn_selenium/webdrivers/BaseDriver/__init__.py +1 -0
- osn_selenium/webdrivers/BaseDriver/_utils.py +37 -0
- osn_selenium/webdrivers/BaseDriver/flags.py +644 -0
- osn_selenium/webdrivers/BaseDriver/protocols.py +2135 -0
- osn_selenium/webdrivers/BaseDriver/trio_wrapper.py +71 -0
- osn_selenium/webdrivers/BaseDriver/webdriver.py +2626 -0
- osn_selenium/webdrivers/Blink/__init__.py +1 -0
- osn_selenium/webdrivers/Blink/flags.py +1349 -0
- osn_selenium/webdrivers/Blink/protocols.py +330 -0
- osn_selenium/webdrivers/Blink/webdriver.py +637 -0
- osn_selenium/webdrivers/Chrome/__init__.py +1 -0
- osn_selenium/webdrivers/Chrome/flags.py +192 -0
- osn_selenium/webdrivers/Chrome/protocols.py +228 -0
- osn_selenium/webdrivers/Chrome/webdriver.py +394 -0
- osn_selenium/webdrivers/Edge/__init__.py +1 -0
- osn_selenium/webdrivers/Edge/flags.py +192 -0
- osn_selenium/webdrivers/Edge/protocols.py +228 -0
- osn_selenium/webdrivers/Edge/webdriver.py +394 -0
- osn_selenium/webdrivers/Yandex/__init__.py +1 -0
- osn_selenium/webdrivers/Yandex/flags.py +192 -0
- osn_selenium/webdrivers/Yandex/protocols.py +211 -0
- osn_selenium/webdrivers/Yandex/webdriver.py +350 -0
- osn_selenium/webdrivers/__init__.py +1 -0
- osn_selenium/webdrivers/_functions.py +504 -0
- osn_selenium/webdrivers/js_scripts/check_element_in_viewport.js +18 -0
- osn_selenium/webdrivers/js_scripts/get_document_scroll_size.js +4 -0
- osn_selenium/webdrivers/js_scripts/get_element_css.js +6 -0
- osn_selenium/webdrivers/js_scripts/get_element_rect_in_viewport.js +9 -0
- osn_selenium/webdrivers/js_scripts/get_random_element_point_in_viewport.js +59 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_position.js +4 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_rect.js +6 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_size.js +4 -0
- osn_selenium/webdrivers/js_scripts/open_new_tab.js +1 -0
- osn_selenium/webdrivers/js_scripts/stop_window_loading.js +1 -0
- osn_selenium/webdrivers/types.py +390 -0
- osn_selenium-0.0.0.dist-info/METADATA +710 -0
- osn_selenium-0.0.0.dist-info/RECORD +57 -0
- osn_selenium-0.0.0.dist-info/WHEEL +5 -0
- osn_selenium-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import trio
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from osn_selenium.dev_tools._types import LogLevelsType
|
|
7
|
+
from osn_selenium.dev_tools.errors import trio_end_exceptions
|
|
8
|
+
from osn_selenium.dev_tools.utils import (
|
|
9
|
+
_validate_log_filter,
|
|
10
|
+
log_exception
|
|
11
|
+
)
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
Optional,
|
|
16
|
+
Sequence,
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Union
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from osn_selenium.dev_tools.utils import TargetData
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LoggerSettings:
|
|
28
|
+
"""
|
|
29
|
+
Settings for configuring the LoggerManager.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
log_dir_path (Optional[Path]): The base directory where log files will be stored.
|
|
33
|
+
If None, logging will only happen in memory and not be written to files.
|
|
34
|
+
Defaults to None.
|
|
35
|
+
renew_log (bool): If True and `log_dir_path` is provided, the log directory
|
|
36
|
+
will be cleared (recreated) on initialization. Defaults to True.
|
|
37
|
+
log_level_filter_mode (Literal["exclude", "include"]): The mode for filtering log levels.
|
|
38
|
+
"exclude" means log levels in `log_level_filter` will be excluded.
|
|
39
|
+
"include" means only log levels in `log_level_filter` will be included.
|
|
40
|
+
Defaults to "exclude".
|
|
41
|
+
log_level_filter (Optional[Union[LogLevelsType, Sequence[LogLevelsType]]]):
|
|
42
|
+
A single log level or a sequence of log levels to filter by.
|
|
43
|
+
Used in conjunction with `log_level_filter_mode`. Defaults to None.
|
|
44
|
+
target_type_filter_mode (Literal["exclude", "include"]): The mode for filtering target types.
|
|
45
|
+
"exclude" means target types in `target_type_filter` will be excluded.
|
|
46
|
+
"include" means only target types in `target_type_filter` will be included.
|
|
47
|
+
Defaults to "exclude".
|
|
48
|
+
target_type_filter (Optional[Union[str, Sequence[str]]]):
|
|
49
|
+
A single target type string or a sequence of target type strings to filter by.
|
|
50
|
+
Used in conjunction with `target_type_filter_mode`. Defaults to None.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
log_dir_path: Optional[Path] = None
|
|
54
|
+
renew_log: bool = True
|
|
55
|
+
log_level_filter_mode: Literal["exclude", "include"] = "exclude"
|
|
56
|
+
log_level_filter: Optional[Union[LogLevelsType, Sequence[LogLevelsType]]] = None
|
|
57
|
+
target_type_filter_mode: Literal["exclude", "include"] = "exclude"
|
|
58
|
+
target_type_filter: Optional[Union[str, Sequence[str]]] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class LogEntry:
|
|
63
|
+
"""
|
|
64
|
+
Represents a single log entry with detailed information.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
target_data ("TargetData"): Data about the target (browser tab/session) from which the log originated.
|
|
68
|
+
message (str): The main log message.
|
|
69
|
+
level (LogLevelsType): The severity level of the log (e.g., "INFO", "ERROR").
|
|
70
|
+
timestamp (datetime): The exact time when the log entry was created.
|
|
71
|
+
source_function (str): The name of the function that generated the log.
|
|
72
|
+
extra_data (Optional[dict[str, Any]]): Optional additional data associated with the log.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
target_data: "TargetData"
|
|
76
|
+
message: str
|
|
77
|
+
level: LogLevelsType
|
|
78
|
+
timestamp: datetime
|
|
79
|
+
source_function: str
|
|
80
|
+
extra_data: Optional[dict[str, Any]] = None
|
|
81
|
+
|
|
82
|
+
def to_json(self) -> dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
Converts the log entry to a JSON-serializable dictionary.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
dict[str, Any]: A dictionary representation of the log entry.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
log_dict = {
|
|
91
|
+
"target_data": self.target_data.to_json(),
|
|
92
|
+
"timestamp": self.timestamp.isoformat(),
|
|
93
|
+
"level": self.level,
|
|
94
|
+
"source_function": self.source_function,
|
|
95
|
+
"message": self.message,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if self.extra_data is not None:
|
|
99
|
+
log_dict["extra_data"] = {key: str(value) for key, value in self.extra_data.items()}
|
|
100
|
+
|
|
101
|
+
return log_dict
|
|
102
|
+
|
|
103
|
+
def to_string(self) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Converts the log entry to a human-readable string format.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: A multi-line string representation of the log entry.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
return "\n\n".join(
|
|
112
|
+
f"{key}: {json.dumps(value, indent=4, ensure_ascii=False)}"
|
|
113
|
+
for key, value in self.to_json().items()
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TargetLogger:
|
|
118
|
+
"""
|
|
119
|
+
Manages logging for a specific browser target (e.g., a tab or iframe).
|
|
120
|
+
|
|
121
|
+
Each `TargetLogger` instance is responsible for writing log entries
|
|
122
|
+
related to its associated `TargetData` to a dedicated file.
|
|
123
|
+
|
|
124
|
+
Attributes:
|
|
125
|
+
_target_data ("TargetData"): The data of the browser target this logger is associated with.
|
|
126
|
+
_nursery_object (trio.Nursery): The Trio nursery for managing concurrent tasks.
|
|
127
|
+
_receive_channel (trio.MemoryReceiveChannel[LogEntry]): The receive channel for log entries specific to this target.
|
|
128
|
+
_log_level_filter (Callable[[Any], bool]): Filter function for log levels.
|
|
129
|
+
_target_type_filter (Callable[[Any], bool]): Filter function for target types.
|
|
130
|
+
_file_writing_stopped (Optional[trio.Event]): An event set when file writing task stops.
|
|
131
|
+
_file_path (Optional[Path]): The path to the target-specific log file.
|
|
132
|
+
_is_active (bool): Flag indicating if the target logger is active.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
target_data: "TargetData",
|
|
138
|
+
nursery_object: trio.Nursery,
|
|
139
|
+
receive_channel: trio.MemoryReceiveChannel[LogEntry],
|
|
140
|
+
logger_settings: LoggerSettings,
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Initializes the TargetLogger.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
target_data ("TargetData"): The data of the browser target this logger will log for.
|
|
147
|
+
nursery_object (trio.Nursery): The Trio nursery to spawn background tasks.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
self._target_data = target_data
|
|
151
|
+
self._nursery_object = nursery_object
|
|
152
|
+
self._receive_channel = receive_channel
|
|
153
|
+
|
|
154
|
+
self._log_level_filter = _validate_log_filter(
|
|
155
|
+
logger_settings.log_level_filter_mode,
|
|
156
|
+
logger_settings.log_level_filter
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self._target_type_filter = _validate_log_filter(
|
|
160
|
+
logger_settings.target_type_filter_mode,
|
|
161
|
+
logger_settings.target_type_filter
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._file_writing_stopped: Optional[trio.Event] = None
|
|
165
|
+
|
|
166
|
+
if logger_settings.log_dir_path is None:
|
|
167
|
+
self._file_path = None
|
|
168
|
+
else:
|
|
169
|
+
self._file_path = logger_settings.log_dir_path.joinpath(f"{target_data.target_id}.txt")
|
|
170
|
+
|
|
171
|
+
if self._file_path.exists():
|
|
172
|
+
with open(self._file_path, "w", encoding="utf-8") as file:
|
|
173
|
+
file.write("")
|
|
174
|
+
|
|
175
|
+
self._is_active = False
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def is_active(self) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Checks if the target logger is currently active.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
bool: True if the logger is active and running, False otherwise.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
return self._is_active
|
|
187
|
+
|
|
188
|
+
async def close(self):
|
|
189
|
+
"""
|
|
190
|
+
Closes the target logger, including its receive channel.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
if self._receive_channel is not None:
|
|
194
|
+
await self._receive_channel.aclose()
|
|
195
|
+
self._receive_channel = None
|
|
196
|
+
|
|
197
|
+
if self._file_writing_stopped is not None:
|
|
198
|
+
await self._file_writing_stopped.wait()
|
|
199
|
+
self._file_writing_stopped = None
|
|
200
|
+
|
|
201
|
+
self._is_active = False
|
|
202
|
+
|
|
203
|
+
async def _write_file(self):
|
|
204
|
+
"""
|
|
205
|
+
Asynchronously writes log entries to the target-specific file.
|
|
206
|
+
|
|
207
|
+
This method continuously receives `LogEntry` objects from its channel
|
|
208
|
+
and appends their string representation to the configured file,
|
|
209
|
+
applying log level and target type filters.
|
|
210
|
+
It runs as a background task.
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
BaseException: If an unexpected error occurs during file writing.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
end_of_entry = "\n\n" + "=" * 100 + "\n\n"
|
|
218
|
+
|
|
219
|
+
async with await trio.open_file(self._file_path, "a+", encoding="utf-8") as file:
|
|
220
|
+
async for log_entry in self._receive_channel:
|
|
221
|
+
if self._log_level_filter(log_entry.level) and self._target_type_filter(log_entry.target_data.type_):
|
|
222
|
+
await file.write(log_entry.to_string() + end_of_entry)
|
|
223
|
+
except* trio_end_exceptions:
|
|
224
|
+
pass
|
|
225
|
+
except* BaseException as error:
|
|
226
|
+
log_exception(error)
|
|
227
|
+
finally:
|
|
228
|
+
if self._file_writing_stopped is not None:
|
|
229
|
+
self._file_writing_stopped.set()
|
|
230
|
+
|
|
231
|
+
async def run(self):
|
|
232
|
+
"""Starts the target logger, setting up its receive channel and file writing task."""
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if not self._is_active:
|
|
236
|
+
self._file_writing_stopped = trio.Event()
|
|
237
|
+
|
|
238
|
+
if self._file_path is not None:
|
|
239
|
+
self._nursery_object.start_soon(self._write_file,)
|
|
240
|
+
|
|
241
|
+
self._is_active = True
|
|
242
|
+
except* trio_end_exceptions:
|
|
243
|
+
await self.close()
|
|
244
|
+
except* BaseException as error:
|
|
245
|
+
log_exception(error)
|
|
246
|
+
await self.close()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_target_logger(
|
|
250
|
+
target_data: "TargetData",
|
|
251
|
+
nursery_object: trio.Nursery,
|
|
252
|
+
logger_settings: LoggerSettings
|
|
253
|
+
) -> tuple[trio.MemorySendChannel[LogEntry], TargetLogger]:
|
|
254
|
+
"""
|
|
255
|
+
Builds and initializes a `TargetLogger` instance along with its send channel.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
target_data ("TargetData"): The data for the target this logger will serve.
|
|
259
|
+
nursery_object (trio.Nursery): The Trio nursery to associate with the logger for background tasks.
|
|
260
|
+
logger_settings (LoggerSettings): The logger configuration settings.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
tuple[trio.MemorySendChannel[LogEntry], TargetLogger]: A tuple containing
|
|
264
|
+
the send channel for `LogEntry` objects and the initialized `TargetLogger` instance.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
send_channel, receive_channel = trio.open_memory_channel(1000)
|
|
268
|
+
target_logger = TargetLogger(target_data, nursery_object, receive_channel, logger_settings)
|
|
269
|
+
|
|
270
|
+
return send_channel, target_logger
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class LogLevelStats:
|
|
275
|
+
"""
|
|
276
|
+
Dataclass to store statistics for a specific log level.
|
|
277
|
+
|
|
278
|
+
Attributes:
|
|
279
|
+
num_logs (int): The total number of logs recorded for this level.
|
|
280
|
+
last_log_time (datetime): The timestamp of the most recent log entry for this level.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
num_logs: int
|
|
284
|
+
last_log_time: datetime
|
|
285
|
+
|
|
286
|
+
def to_json(self) -> dict[str, Any]:
|
|
287
|
+
"""
|
|
288
|
+
Converts the statistics to a JSON-serializable dictionary.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
dict[str, Any]: A dictionary representation of the log level statistics.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"num_logs": self.num_logs,
|
|
296
|
+
"last_log_time": self.last_log_time.isoformat()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class LoggerChannelStats:
|
|
302
|
+
"""
|
|
303
|
+
Dataclass to store statistics for a specific logging channel (per target).
|
|
304
|
+
|
|
305
|
+
Attributes:
|
|
306
|
+
target_id (str): The unique ID of the target associated with this channel.
|
|
307
|
+
title (str): The title of the target (e.g., page title).
|
|
308
|
+
url (str): The URL of the target.
|
|
309
|
+
num_logs (int): The total number of log entries for this channel.
|
|
310
|
+
last_log_time (datetime): The timestamp of the most recent log entry for this channel.
|
|
311
|
+
log_level_stats (dict[str, LogLevelStats]): A dictionary mapping log levels to their specific statistics.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
target_id: str
|
|
315
|
+
title: str
|
|
316
|
+
url: str
|
|
317
|
+
num_logs: int
|
|
318
|
+
last_log_time: datetime
|
|
319
|
+
log_level_stats: dict[str, LogLevelStats]
|
|
320
|
+
|
|
321
|
+
async def add_log(self, log_entry: LogEntry):
|
|
322
|
+
"""
|
|
323
|
+
Updates the channel statistics based on a new log entry.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
log_entry (LogEntry): The new log entry to incorporate into the statistics.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
self.num_logs += 1
|
|
330
|
+
self.last_log_time = log_entry.timestamp
|
|
331
|
+
|
|
332
|
+
if log_entry.level not in self.log_level_stats:
|
|
333
|
+
self.log_level_stats[log_entry.level] = LogLevelStats(num_logs=1, last_log_time=log_entry.timestamp)
|
|
334
|
+
else:
|
|
335
|
+
self.log_level_stats[log_entry.level].num_logs += 1
|
|
336
|
+
self.log_level_stats[log_entry.level].last_log_time = log_entry.timestamp
|
|
337
|
+
|
|
338
|
+
def to_json(self) -> dict[str, Any]:
|
|
339
|
+
"""
|
|
340
|
+
Converts the channel statistics to a JSON-serializable dictionary.
|
|
341
|
+
|
|
342
|
+
Nested statistics objects are converted to their JSON representations.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
dict[str, Any]: A dictionary representation of the logger channel statistics.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
"target_id": self.target_id,
|
|
350
|
+
"title": self.title,
|
|
351
|
+
"url": self.url,
|
|
352
|
+
"num_logs": self.num_logs,
|
|
353
|
+
"last_log_time": self.last_log_time.isoformat(),
|
|
354
|
+
"log_level_stats": {
|
|
355
|
+
log_level: log_level_stats.to_json()
|
|
356
|
+
for log_level, log_level_stats in self.log_level_stats.items()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@dataclass
|
|
362
|
+
class TargetTypeStats:
|
|
363
|
+
"""
|
|
364
|
+
Dataclass to store statistics for a specific target type.
|
|
365
|
+
|
|
366
|
+
Attributes:
|
|
367
|
+
num_targets (int): The count of targets of this type.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
num_targets: int
|
|
371
|
+
|
|
372
|
+
def to_json(self) -> dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Converts the statistics to a JSON-serializable dictionary.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
dict[str, Any]: A dictionary representation of the target type statistics.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
return {"num_targets": self.num_targets}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@dataclass(frozen=True)
|
|
384
|
+
class MainLogEntry:
|
|
385
|
+
"""
|
|
386
|
+
Represents a summary log entry for the entire logging system.
|
|
387
|
+
|
|
388
|
+
Attributes:
|
|
389
|
+
num_channels (int): The total number of active logging channels (targets).
|
|
390
|
+
targets_types_stats (dict[str, TargetTypeStats]): Statistics grouped by target type.
|
|
391
|
+
num_logs (int): The total number of log entries across all channels.
|
|
392
|
+
log_level_stats (dict[str, LogLevelStats]): Overall statistics for each log level.
|
|
393
|
+
channels_stats (Sequence[LoggerChannelStats]): A list of statistics for each active logging channel.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
num_channels: int
|
|
397
|
+
targets_types_stats: dict[str, TargetTypeStats]
|
|
398
|
+
num_logs: int
|
|
399
|
+
log_level_stats: dict[str, LogLevelStats]
|
|
400
|
+
channels_stats: Sequence[LoggerChannelStats]
|
|
401
|
+
|
|
402
|
+
def to_json(self) -> dict[str, Any]:
|
|
403
|
+
"""
|
|
404
|
+
Converts the main log entry to a JSON-serializable dictionary.
|
|
405
|
+
|
|
406
|
+
Nested statistics objects are converted to their JSON representations.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
dict[str, Any]: A dictionary representation of the main log entry.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"num_channels": self.num_channels,
|
|
414
|
+
"targets_types_stats": {
|
|
415
|
+
type_: target_type_stats.to_json()
|
|
416
|
+
for type_, target_type_stats in self.targets_types_stats.items()
|
|
417
|
+
},
|
|
418
|
+
"num_logs": self.num_logs,
|
|
419
|
+
"log_level_stats": {
|
|
420
|
+
log_level: log_level_stats.to_json()
|
|
421
|
+
for log_level, log_level_stats in self.log_level_stats.items()
|
|
422
|
+
},
|
|
423
|
+
"channels_stats": [channel_stats.to_json() for channel_stats in self.channels_stats]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
def to_string(self) -> str:
|
|
427
|
+
"""
|
|
428
|
+
Converts the main log entry to a human-readable string format.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
str: A multi-line string representation of the main log entry.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
return "\n\n".join(
|
|
435
|
+
f"{key}: {json.dumps(value, indent=4, ensure_ascii=False)}"
|
|
436
|
+
for key, value in self.to_json().items()
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class MainLogger:
|
|
441
|
+
"""
|
|
442
|
+
Manages the main log file, summarizing overall logging activity.
|
|
443
|
+
|
|
444
|
+
This logger is responsible for writing aggregated statistics about all active
|
|
445
|
+
logging channels and target types to a designated file.
|
|
446
|
+
|
|
447
|
+
Attributes:
|
|
448
|
+
_nursery_object (trio.Nursery): The Trio nursery for managing concurrent tasks.
|
|
449
|
+
_receive_channel (trio.MemoryReceiveChannel[MainLogEntry]): The receive channel for main log entries.
|
|
450
|
+
_file_writing_stopped (Optional[trio.Event]): An event set when the file writing task stops.
|
|
451
|
+
_is_active (bool): Flag indicating if the main logger is active.
|
|
452
|
+
_file_path (Optional[Path]): The path to the main log file.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def __init__(
|
|
456
|
+
self,
|
|
457
|
+
logger_settings: LoggerSettings,
|
|
458
|
+
nursery_object: trio.Nursery,
|
|
459
|
+
receive_channel: trio.MemoryReceiveChannel[MainLogEntry]
|
|
460
|
+
):
|
|
461
|
+
"""
|
|
462
|
+
Initializes the MainLogger.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
logger_settings (LoggerSettings): The settings for logging, including log directory.
|
|
466
|
+
nursery_object (trio.Nursery): The Trio nursery to spawn background tasks.
|
|
467
|
+
receive_channel (trio.MemoryReceiveChannel[MainLogEntry]): The channel from which main log entries are received.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
self._nursery_object = nursery_object
|
|
471
|
+
self._receive_channel = receive_channel
|
|
472
|
+
self._file_writing_stopped: Optional[trio.Event] = None
|
|
473
|
+
self._is_active = False
|
|
474
|
+
|
|
475
|
+
if logger_settings.log_dir_path is None:
|
|
476
|
+
self._file_path = None
|
|
477
|
+
else:
|
|
478
|
+
self._file_path = logger_settings.log_dir_path.joinpath("__MAIN__.txt")
|
|
479
|
+
|
|
480
|
+
async def close(self):
|
|
481
|
+
"""
|
|
482
|
+
Closes the main logger, including its receive channel.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
if self._receive_channel is not None:
|
|
486
|
+
await self._receive_channel.aclose()
|
|
487
|
+
self._receive_channel = None
|
|
488
|
+
|
|
489
|
+
if self._file_writing_stopped is not None:
|
|
490
|
+
await self._file_writing_stopped.wait()
|
|
491
|
+
|
|
492
|
+
self._is_active = False
|
|
493
|
+
|
|
494
|
+
async def _write_file(self):
|
|
495
|
+
"""
|
|
496
|
+
Asynchronously writes main log entries to the file.
|
|
497
|
+
|
|
498
|
+
This method continuously receives `MainLogEntry` objects from its channel
|
|
499
|
+
and overwrites the configured file with their string representation.
|
|
500
|
+
It runs as a background task.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
BaseException: If an unexpected error occurs during file writing.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
async with await trio.open_file(self._file_path, "w+", encoding="utf-8") as file:
|
|
508
|
+
async for log_entry in self._receive_channel:
|
|
509
|
+
await file.write(log_entry.to_string())
|
|
510
|
+
await file.seek(0)
|
|
511
|
+
except* trio_end_exceptions:
|
|
512
|
+
pass
|
|
513
|
+
except* BaseException as error:
|
|
514
|
+
log_exception(error)
|
|
515
|
+
finally:
|
|
516
|
+
if self._file_writing_stopped is not None:
|
|
517
|
+
self._file_writing_stopped.set()
|
|
518
|
+
|
|
519
|
+
async def run(self):
|
|
520
|
+
"""
|
|
521
|
+
Starts the main logger, setting up its receive channel and file writing task.
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
BaseException: If an error occurs during setup or if the logger fails to activate.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
if not self._is_active:
|
|
529
|
+
self._file_writing_stopped = trio.Event()
|
|
530
|
+
|
|
531
|
+
if self._file_path is not None:
|
|
532
|
+
self._nursery_object.start_soon(self._write_file,)
|
|
533
|
+
|
|
534
|
+
self._is_active = True
|
|
535
|
+
except* trio_end_exceptions:
|
|
536
|
+
await self.close()
|
|
537
|
+
except* BaseException as error:
|
|
538
|
+
log_exception(error)
|
|
539
|
+
await self.close()
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def build_main_logger(nursery_object: trio.Nursery, logger_settings: LoggerSettings) -> tuple[trio.MemorySendChannel[MainLogEntry], MainLogger]:
|
|
543
|
+
"""
|
|
544
|
+
Builds and initializes a `MainLogger` instance along with its send channel.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
nursery_object (trio.Nursery): The Trio nursery to associate with the logger for background tasks.
|
|
548
|
+
logger_settings (LoggerSettings): The logger configuration settings.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
tuple[trio.MemorySendChannel[MainLogEntry], MainLogger]: A tuple containing
|
|
552
|
+
the send channel for `MainLogEntry` objects and the initialized `MainLogger` instance.
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
send_channel, receive_channel = trio.open_memory_channel(1000)
|
|
556
|
+
target_logger = MainLogger(logger_settings, nursery_object, receive_channel)
|
|
557
|
+
|
|
558
|
+
return send_channel, target_logger
|