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,509 @@
|
|
|
1
|
+
import trio
|
|
2
|
+
import shutil
|
|
3
|
+
import logging
|
|
4
|
+
import warnings
|
|
5
|
+
import traceback
|
|
6
|
+
import functools
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from osn_selenium.dev_tools.errors import cdp_end_exceptions
|
|
10
|
+
from osn_selenium.dev_tools._types import (
|
|
11
|
+
devtools_background_func_type
|
|
12
|
+
)
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
Callable,
|
|
16
|
+
Iterable,
|
|
17
|
+
Literal,
|
|
18
|
+
Optional,
|
|
19
|
+
TYPE_CHECKING,
|
|
20
|
+
Union
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from osn_selenium.dev_tools.logger import LoggerSettings
|
|
26
|
+
from osn_selenium.dev_tools.manager import DevTools, DevToolsTarget
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def warn_if_active(func: Callable) -> Callable:
|
|
30
|
+
"""
|
|
31
|
+
Decorator to warn if DevTools operations are attempted while DevTools is active.
|
|
32
|
+
|
|
33
|
+
This decorator is used to wrap methods in the DevTools class that should not be called
|
|
34
|
+
while the DevTools event handler context manager is active. It checks the `is_active` flag
|
|
35
|
+
of the DevTools instance. If DevTools is active, it issues a warning; otherwise, it proceeds
|
|
36
|
+
to execute the original method.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
func (Callable): The function to be wrapped. This should be a method of the DevTools class.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Callable: The wrapped function. When called, it will check if DevTools is active and either
|
|
43
|
+
execute the original function or issue a warning.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@functools.wraps(func)
|
|
47
|
+
def wrapper(self: "DevTools", *args: Any, **kwargs: Any) -> Any:
|
|
48
|
+
if self.is_active:
|
|
49
|
+
warnings.warn("DevTools is active. Exit dev_tools context before changing settings.")
|
|
50
|
+
|
|
51
|
+
return func(self, *args, **kwargs)
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def wait_one(*events: trio.Event):
|
|
57
|
+
"""
|
|
58
|
+
Waits for the first of multiple Trio events to be set.
|
|
59
|
+
|
|
60
|
+
This function creates a nursery and starts a task for each provided event.
|
|
61
|
+
As soon as any event is set, it receives a signal, cancels the nursery,
|
|
62
|
+
and returns.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
*events (trio.Event): One or more Trio Event objects to wait for.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
async def waiter(event: trio.Event, send_chan_: trio.MemorySendChannel):
|
|
69
|
+
"""Internal helper to wait for an event and send a signal."""
|
|
70
|
+
|
|
71
|
+
await event.wait()
|
|
72
|
+
await send_chan_.send(0)
|
|
73
|
+
|
|
74
|
+
send_chan, receive_chan = trio.open_memory_channel(0)
|
|
75
|
+
|
|
76
|
+
async with trio.open_nursery() as nursery:
|
|
77
|
+
for event_ in events:
|
|
78
|
+
nursery.start_soon(waiter, event_, send_chan.clone())
|
|
79
|
+
|
|
80
|
+
await receive_chan.receive()
|
|
81
|
+
nursery.cancel_scope.cancel()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def log_on_error(func: Callable) -> Callable:
|
|
85
|
+
"""
|
|
86
|
+
Decorator that logs any `BaseException` raised by the decorated async function.
|
|
87
|
+
|
|
88
|
+
If an exception occurs, it is logged using `log_exception`, and an `ExceptionThrown`
|
|
89
|
+
object wrapping the exception is returned instead of re-raising it.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
func (Callable): The asynchronous function to be wrapped.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Callable: The wrapped asynchronous function.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@functools.wraps(func)
|
|
99
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
100
|
+
try:
|
|
101
|
+
return await func(*args, **kwargs)
|
|
102
|
+
except BaseException as exception:
|
|
103
|
+
log_exception(exception)
|
|
104
|
+
return ExceptionThrown(exception)
|
|
105
|
+
|
|
106
|
+
return wrapper
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def extract_exception_trace(exception: BaseException) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Extracts a comprehensive traceback string for an exception, including handling for `ExceptionGroup`s.
|
|
112
|
+
|
|
113
|
+
This function recursively flattens `ExceptionGroup`s to ensure all nested exceptions
|
|
114
|
+
have their tracebacks included in the final output string.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
exception (BaseException): The exception object to extract the trace from.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
str: A multi-line string containing the formatted traceback(s) for the given exception
|
|
121
|
+
and any nested exceptions within an `ExceptionGroup`.
|
|
122
|
+
|
|
123
|
+
EXAMPLES
|
|
124
|
+
________
|
|
125
|
+
>>> try:
|
|
126
|
+
... raise ValueError("Simple error occurred")
|
|
127
|
+
... except ValueError as e:
|
|
128
|
+
... trace = extract_exception_trace(e)
|
|
129
|
+
... # The first line typically indicates the start of a traceback
|
|
130
|
+
... print(trace.splitlines()[0].strip())
|
|
131
|
+
...
|
|
132
|
+
>>> try:
|
|
133
|
+
... raise ExceptionGroup(
|
|
134
|
+
... "Multiple issues",
|
|
135
|
+
... [
|
|
136
|
+
... TypeError("Invalid type provided"),
|
|
137
|
+
... ValueError("Value out of range")
|
|
138
|
+
... ]
|
|
139
|
+
... )
|
|
140
|
+
... except ExceptionGroup as eg:
|
|
141
|
+
... trace = extract_exception_trace(eg)
|
|
142
|
+
... # Check if tracebacks for both nested exceptions are present
|
|
143
|
+
... print("TypeError" in trace and "ValueError" in trace)
|
|
144
|
+
...
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def flatten_exceptions(exception_: BaseException) -> list[BaseException]:
|
|
148
|
+
"""Recursively flattens an ExceptionGroup into a list of individual exceptions."""
|
|
149
|
+
|
|
150
|
+
if isinstance(exception_, ExceptionGroup):
|
|
151
|
+
inner_exceptions = exception_.exceptions
|
|
152
|
+
else:
|
|
153
|
+
return [exception_]
|
|
154
|
+
|
|
155
|
+
result = []
|
|
156
|
+
for exception__ in inner_exceptions:
|
|
157
|
+
result.extend(flatten_exceptions(exception__))
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
def format_exception(exception_: BaseException) -> str:
|
|
162
|
+
"""Formats a single exception's traceback into a string."""
|
|
163
|
+
|
|
164
|
+
return "".join(
|
|
165
|
+
traceback.format_exception(exception_.__class__, exception_, exception_.__traceback__)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return "\n".join(format_exception(exc) for exc in flatten_exceptions(exception))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def log_exception(exception: BaseException):
|
|
172
|
+
"""
|
|
173
|
+
Logs the full traceback of an exception at the ERROR level.
|
|
174
|
+
|
|
175
|
+
This function uses `extract_exception_trace` to get a comprehensive traceback string
|
|
176
|
+
and then logs it using the standard logging module.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
exception (BaseException): The exception object to log.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
logging.log(logging.ERROR, extract_exception_trace(exception))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ExceptionThrown:
|
|
186
|
+
"""
|
|
187
|
+
A wrapper class to indicate that an exception was thrown during an operation.
|
|
188
|
+
|
|
189
|
+
This is used in `execute_cdp_command` when `error_mode` is "log" or "pass"
|
|
190
|
+
to return an object indicating an error occurred without re-raising it immediately.
|
|
191
|
+
|
|
192
|
+
Attributes:
|
|
193
|
+
exception (BaseException): The exception that was caught.
|
|
194
|
+
traceback (str): The formatted traceback string of the exception.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, exception: BaseException):
|
|
198
|
+
"""
|
|
199
|
+
Initializes the ExceptionThrown wrapper.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
exception (BaseException): The exception to wrap.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
self.exception = exception
|
|
206
|
+
self.traceback = extract_exception_trace(exception)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def execute_cdp_command(
|
|
210
|
+
self: "DevToolsTarget",
|
|
211
|
+
error_mode: Literal["raise", "log", "pass"],
|
|
212
|
+
function: Callable[..., Any],
|
|
213
|
+
*args: Any,
|
|
214
|
+
**kwargs: Any
|
|
215
|
+
) -> Union[Any, ExceptionThrown]:
|
|
216
|
+
"""
|
|
217
|
+
Executes a Chrome DevTools Protocol (CDP) command with specified error handling.
|
|
218
|
+
|
|
219
|
+
This function attempts to execute a CDP command via the `cdp_session`.
|
|
220
|
+
It provides different behaviors based on the `error_mode` if an exception occurs:
|
|
221
|
+
- "raise": Re-raises the exception immediately.
|
|
222
|
+
- "log": Logs the exception using the target's logger and returns an `ExceptionThrown` object.
|
|
223
|
+
- "pass": Returns an `ExceptionThrown` object without logging the exception.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
self ("DevToolsTarget"): The `DevToolsTarget` instance through which the command is executed.
|
|
227
|
+
error_mode (Literal["raise", "log", "pass"]): Defines how exceptions are handled.
|
|
228
|
+
"raise": Re-raises the exception.
|
|
229
|
+
"log": Logs the exception and returns `ExceptionThrown`.
|
|
230
|
+
"pass": Returns `ExceptionThrown` without logging.
|
|
231
|
+
function (Callable[..., Any]): The CDP command function to execute (e.g., `devtools.page.navigate`).
|
|
232
|
+
*args (Any): Positional arguments to pass to the CDP command function.
|
|
233
|
+
**kwargs (Any): Keyword arguments to pass to the CDP command function.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Union[Any, ExceptionThrown]: The result of the CDP command if successful,
|
|
237
|
+
or an `ExceptionThrown` object if an error occurred and `error_mode` is "log" or "pass".
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
cdp_end_exceptions: If a CDP-related connection error occurs, these are always re-raised.
|
|
241
|
+
BaseException: If `error_mode` is "raise" and any other exception occurs.
|
|
242
|
+
ValueError: If an unknown `error_mode` is provided.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
return await self.cdp_session.execute(function(*args, **kwargs))
|
|
247
|
+
except cdp_end_exceptions as error:
|
|
248
|
+
raise error
|
|
249
|
+
except BaseException as error:
|
|
250
|
+
if error_mode == "raise":
|
|
251
|
+
raise error
|
|
252
|
+
elif error_mode == "log":
|
|
253
|
+
await self.log_error(error=error)
|
|
254
|
+
return ExceptionThrown(exception=error)
|
|
255
|
+
elif error_mode == "pass":
|
|
256
|
+
return ExceptionThrown(exception=error)
|
|
257
|
+
else:
|
|
258
|
+
raise ValueError(f"Wrong error_mode: {error_mode}. Expected: 'raise', 'log', 'pass'.")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_log_filter(
|
|
262
|
+
filter_mode: Literal["include", "exclude"],
|
|
263
|
+
log_filter: Optional[Union[Any, Iterable[Any]]]
|
|
264
|
+
) -> Callable[[Any], bool]:
|
|
265
|
+
"""
|
|
266
|
+
Creates a callable filter function based on the specified filter mode and values.
|
|
267
|
+
|
|
268
|
+
This function generates a lambda that can be used to check if a given log level
|
|
269
|
+
or target type should be processed, based on whether the filter is set to
|
|
270
|
+
"include" (only process items in the filter) or "exclude" (process all items
|
|
271
|
+
except those in the filter).
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
filter_mode (Literal["include", "exclude"]): The mode of the filter.
|
|
275
|
+
"include" means only items present in `log_filter` will pass.
|
|
276
|
+
"exclude" means all items except those present in `log_filter` will pass.
|
|
277
|
+
log_filter (Optional[Union[Any, Sequence[Any]]]):
|
|
278
|
+
A single log filter item or a sequence of such items.
|
|
279
|
+
If None:
|
|
280
|
+
- In "include" mode, the generated filter will always return False (nothing is included).
|
|
281
|
+
- In "exclude" mode, the generated filter will always return True (nothing is excluded).
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Callable[[Any], bool]: A callable function that takes a single argument (e.g., a log level or target type)
|
|
285
|
+
and returns True if it passes the filter, False otherwise.
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
ValueError: If `filter_mode` is invalid.
|
|
289
|
+
|
|
290
|
+
EXAMPLES
|
|
291
|
+
________
|
|
292
|
+
>>> # Example 1: Include only "INFO" logs
|
|
293
|
+
... info_only_filter = _validate_log_filter("include", "INFO")
|
|
294
|
+
... print(info_only_filter("INFO")) # True
|
|
295
|
+
... print(info_only_filter("ERROR")) # False
|
|
296
|
+
|
|
297
|
+
>>> # Example 2: Exclude "DEBUG" and "WARNING" logs
|
|
298
|
+
... no_debug_warning_filter = _validate_log_filter("exclude", ["DEBUG", "WARNING"])
|
|
299
|
+
... print(no_debug_warning_filter("INFO")) # True
|
|
300
|
+
... print(no_debug_warning_filter("DEBUG")) # False
|
|
301
|
+
|
|
302
|
+
>>> # Example 3: No filter (exclude mode, so everything passes)
|
|
303
|
+
... all_logs_filter = _validate_log_filter("exclude", None)
|
|
304
|
+
... print(all_logs_filter("INFO")) # True
|
|
305
|
+
... print(all_logs_filter("ERROR")) # True
|
|
306
|
+
|
|
307
|
+
>>> # Example 4: No filter (include mode, so nothing passes)
|
|
308
|
+
... no_logs_filter = _validate_log_filter("include", None)
|
|
309
|
+
... print(no_logs_filter("INFO")) # False
|
|
310
|
+
... print(no_logs_filter("ERROR")) # False
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
if log_filter is None:
|
|
314
|
+
if filter_mode == "include":
|
|
315
|
+
return lambda x: False
|
|
316
|
+
elif filter_mode == "exclude":
|
|
317
|
+
return lambda x: True
|
|
318
|
+
|
|
319
|
+
raise ValueError(f"Invalid log filter_mode ({filter_mode}).")
|
|
320
|
+
|
|
321
|
+
if isinstance(log_filter, Iterable):
|
|
322
|
+
if filter_mode == "include":
|
|
323
|
+
return lambda x: x in log_filter
|
|
324
|
+
elif filter_mode == "exclude":
|
|
325
|
+
return lambda x: x not in log_filter
|
|
326
|
+
|
|
327
|
+
raise ValueError(f"Invalid log filter_mode ({filter_mode}).")
|
|
328
|
+
|
|
329
|
+
if filter_mode == "include":
|
|
330
|
+
return lambda x: x == log_filter
|
|
331
|
+
elif filter_mode == "exclude":
|
|
332
|
+
return lambda x: x != log_filter
|
|
333
|
+
|
|
334
|
+
raise ValueError(f"Invalid log filter_mode ({filter_mode}).")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _validate_type_filter(
|
|
338
|
+
type_: str,
|
|
339
|
+
filter_mode: Literal["include", "exclude"],
|
|
340
|
+
filter_instances: Any
|
|
341
|
+
):
|
|
342
|
+
"""
|
|
343
|
+
Validates a target type against a given filter mode and filter instances.
|
|
344
|
+
|
|
345
|
+
This is a wrapper around `_validate_log_filter` specifically for target types.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
type_ (str): The target type string to check (e.g., "page", "iframe").
|
|
349
|
+
filter_mode (Literal["include", "exclude"]): The mode of the filter ("include" or "exclude").
|
|
350
|
+
filter_instances (Any): The filter value(s) (e.g., a string or a sequence of strings).
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
bool: True if the `type_` passes the filter, False otherwise.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
return _validate_log_filter(filter_mode, filter_instances)(type_)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _prepare_log_dir(logger_settings: "LoggerSettings"):
|
|
360
|
+
"""
|
|
361
|
+
Prepares the log directory based on the provided logger settings.
|
|
362
|
+
|
|
363
|
+
If `log_dir_path` is specified:
|
|
364
|
+
- Creates the directory if it doesn't exist.
|
|
365
|
+
- If `renew_log` is True and the directory exists, it is cleared (recreated).
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
logger_settings ("LoggerSettings"): The settings object containing log directory configuration.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
ValueError: If `log_dir_path` is provided but is not a valid `Path` object
|
|
372
|
+
or does not represent a directory.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
if isinstance(logger_settings.log_dir_path, Path) and (
|
|
376
|
+
logger_settings.log_dir_path.is_dir()
|
|
377
|
+
or not logger_settings.log_dir_path.exists()
|
|
378
|
+
):
|
|
379
|
+
if not logger_settings.log_dir_path.exists():
|
|
380
|
+
logger_settings.log_dir_path.mkdir(parents=True)
|
|
381
|
+
elif logger_settings.renew_log:
|
|
382
|
+
shutil.rmtree(logger_settings.log_dir_path)
|
|
383
|
+
|
|
384
|
+
logger_settings.log_dir_path.mkdir()
|
|
385
|
+
elif logger_settings.log_dir_path is not None:
|
|
386
|
+
raise ValueError(
|
|
387
|
+
f"'log_dir_path' must be a pathlib.Path to directory or None, got {logger_settings.log_dir_path} (type: {type(logger_settings.log_dir_path)})"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _background_task_decorator(func: devtools_background_func_type) -> devtools_background_func_type:
|
|
392
|
+
"""
|
|
393
|
+
Decorator for DevToolsTarget background tasks to manage their lifecycle.
|
|
394
|
+
|
|
395
|
+
This decorator wraps a target's background task function. It ensures that
|
|
396
|
+
`target.background_task_ended` event is set when the function completes,
|
|
397
|
+
allowing the `DevToolsTarget` to track the task's termination.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
func (devtools_background_func_type): The asynchronous background task function
|
|
401
|
+
to be wrapped. It should accept a `DevToolsTarget` instance.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
devtools_background_func_type: The wrapped function.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
@functools.wraps(func)
|
|
408
|
+
async def wrapper(target: "DevToolsTarget") -> Any:
|
|
409
|
+
target.background_task_ended = trio.Event()
|
|
410
|
+
|
|
411
|
+
await func(target)
|
|
412
|
+
|
|
413
|
+
target.background_task_ended.set()
|
|
414
|
+
|
|
415
|
+
return wrapper
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass
|
|
419
|
+
class TargetFilter:
|
|
420
|
+
"""
|
|
421
|
+
Dataclass to define a filter for discovering new browser targets.
|
|
422
|
+
|
|
423
|
+
Used in `DevToolsSettings` to specify which types of targets (e.g., "page", "iframe")
|
|
424
|
+
should be automatically attached to or excluded.
|
|
425
|
+
|
|
426
|
+
Attributes:
|
|
427
|
+
type_ (Optional[str]): The type of target to filter by (e.g., "page", "iframe").
|
|
428
|
+
If None, this filter applies regardless of type.
|
|
429
|
+
exclude (Optional[bool]): If True, targets matching `type_` will be excluded.
|
|
430
|
+
If False or None, targets matching `type_` will be included.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
type_: Optional[str] = None
|
|
434
|
+
exclude: Optional[bool] = None
|
|
435
|
+
|
|
436
|
+
def to_dict(self) -> dict[str, Any]:
|
|
437
|
+
"""
|
|
438
|
+
Converts the target filter to a dictionary suitable for CDP command parameters.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
dict[str, Any]: A dictionary representation of the target filter.
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
dict_ = {}
|
|
445
|
+
|
|
446
|
+
if self.type_ is not None:
|
|
447
|
+
dict_["type"] = self.type_
|
|
448
|
+
|
|
449
|
+
if self.exclude is not None:
|
|
450
|
+
dict_["exclude"] = self.exclude
|
|
451
|
+
|
|
452
|
+
return dict_
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@dataclass
|
|
456
|
+
class TargetData:
|
|
457
|
+
"""
|
|
458
|
+
Dataclass to hold essential information about a browser target (e.g., a tab, iframe, or worker).
|
|
459
|
+
|
|
460
|
+
Attributes:
|
|
461
|
+
target_id (Optional[str]): The unique identifier for the target.
|
|
462
|
+
type_ (Optional[str]): The type of the target (e.g., "page", "iframe", "worker").
|
|
463
|
+
title (Optional[str]): The title of the target (e.g., the page title).
|
|
464
|
+
url (Optional[str]): The URL of the target.
|
|
465
|
+
attached (Optional[bool]): Indicates if the DevTools session is currently attached to this target.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
target_id: Optional[str] = None
|
|
469
|
+
type_: Optional[str] = None
|
|
470
|
+
title: Optional[str] = None
|
|
471
|
+
url: Optional[str] = None
|
|
472
|
+
attached: Optional[bool] = None
|
|
473
|
+
can_access_opener: Optional[bool] = None
|
|
474
|
+
opener_id: Optional[str] = None
|
|
475
|
+
opener_frame_id: Optional[str] = None
|
|
476
|
+
browser_context_id: Optional[str] = None
|
|
477
|
+
subtype: Optional[str] = None
|
|
478
|
+
|
|
479
|
+
def to_dict(self) -> dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Converts the target data to a dictionary.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
dict[str, Any]: A dictionary representation of the target data.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
"target_id": self.target_id,
|
|
489
|
+
"type": self.type_,
|
|
490
|
+
"title": self.title,
|
|
491
|
+
"url": self.url,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
def to_json(self) -> dict[str, Any]:
|
|
495
|
+
"""
|
|
496
|
+
Converts the target data to a JSON-serializable dictionary.
|
|
497
|
+
|
|
498
|
+
Note: `cdp_session` is converted to its string representation as it's not directly JSON-serializable.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
dict[str, Any]: A JSON-serializable dictionary representation of the target data.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
"target_id": self.target_id,
|
|
506
|
+
"type": self.type_,
|
|
507
|
+
"title": self.title,
|
|
508
|
+
"url": self.url,
|
|
509
|
+
}
|
osn_selenium/errors.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class PlatformNotSupportedError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Custom exception raised when the current platform is not supported.
|
|
4
|
+
|
|
5
|
+
This exception is intended to be raised when the script or application is run on a platform that is not explicitly supported by the program logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, platform: str):
|
|
9
|
+
"""
|
|
10
|
+
Initializes a new instance of `PlatformNotSupportedError`.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
platform (str): The name of the unsupported operating system.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
super().__init__(f"Platform not supported: {platform}.")
|
osn_selenium/types.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from win32api import GetSystemMetrics
|
|
2
|
+
from typing import Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WindowRect:
|
|
6
|
+
"""
|
|
7
|
+
Represents a window rectangle with x, y, width, and height.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
x (int): The x-coordinate of the top-left corner. Defaults to 1/4 of screen width.
|
|
11
|
+
y (int): The y-coordinate of the top-left corner. Defaults to 10% of screen height.
|
|
12
|
+
width (int): The width of the rectangle. Defaults to 1/2 of screen width.
|
|
13
|
+
height (int): The height of the rectangle. Defaults to 80% of screen height.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
x: Optional[int] = None,
|
|
19
|
+
y: Optional[int] = None,
|
|
20
|
+
width: Optional[int] = None,
|
|
21
|
+
height: Optional[int] = None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initializes WindowRect with optional x, y, width, and height values.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
x (Optional[int]): The x-coordinate. Defaults to None.
|
|
28
|
+
y (Optional[int]): The y-coordinate. Defaults to None.
|
|
29
|
+
width (Optional[int]): The width. Defaults to None.
|
|
30
|
+
height (Optional[int]): The height. Defaults to None.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
self.x = GetSystemMetrics(0) // 4
|
|
34
|
+
|
|
35
|
+
self.y = int(GetSystemMetrics(1) * 0.1)
|
|
36
|
+
|
|
37
|
+
self.width = GetSystemMetrics(0) // 2
|
|
38
|
+
|
|
39
|
+
self.height = int(GetSystemMetrics(1) * 0.8)
|
|
40
|
+
|
|
41
|
+
self.set_rect(x, y, width, height)
|
|
42
|
+
|
|
43
|
+
def set_rect(
|
|
44
|
+
self,
|
|
45
|
+
x: Optional[int] = None,
|
|
46
|
+
y: Optional[int] = None,
|
|
47
|
+
width: Optional[int] = None,
|
|
48
|
+
height: Optional[int] = None
|
|
49
|
+
) -> "WindowRect":
|
|
50
|
+
"""
|
|
51
|
+
Sets the rectangle dimensions.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
x (Optional[int]): The x-coordinate. Defaults to None.
|
|
55
|
+
y (Optional[int]): The y-coordinate. Defaults to None.
|
|
56
|
+
width (Optional[int]): The width. Defaults to None.
|
|
57
|
+
height (Optional[int]): The height. Defaults to None.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
WindowRect: Returns the instance for method chaining.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if x is not None:
|
|
64
|
+
self.x = x
|
|
65
|
+
|
|
66
|
+
if y is not None:
|
|
67
|
+
self.y = y
|
|
68
|
+
|
|
69
|
+
if width is not None:
|
|
70
|
+
self.width = width
|
|
71
|
+
|
|
72
|
+
if height is not None:
|
|
73
|
+
self.height = height
|
|
74
|
+
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Size(TypedDict):
|
|
79
|
+
"""
|
|
80
|
+
Represents a dictionary structure defining the size.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
width (int): The width of the rectangle along the x-axis.
|
|
84
|
+
height (int): The height of the rectangle along the y-axis.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
width: int
|
|
88
|
+
height: int
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Rectangle(TypedDict):
|
|
92
|
+
"""
|
|
93
|
+
Represents a dictionary structure defining the properties of a rectangle.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
x (int): The x-coordinate of the top-left corner of the rectangle.
|
|
97
|
+
y (int): The y-coordinate of the top-left corner of the rectangle.
|
|
98
|
+
width (int): The width of the rectangle along the x-axis.
|
|
99
|
+
height (int): The height of the rectangle along the y-axis.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
x: int
|
|
103
|
+
y: int
|
|
104
|
+
width: int
|
|
105
|
+
height: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Position(TypedDict):
|
|
109
|
+
"""
|
|
110
|
+
Represents a dictionary structure defining the position.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
x (int): The x-coordinate of the top-left corner of the rectangle.
|
|
114
|
+
y (int): The y-coordinate of the top-left corner of the rectangle.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
x: int
|
|
118
|
+
y: int
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import trio
|
|
2
|
+
from trio import CapacityLimiter
|
|
3
|
+
from functools import partial, wraps
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Coroutine,
|
|
8
|
+
Optional,
|
|
9
|
+
ParamSpec,
|
|
10
|
+
TypeVar
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TrioWrapInput = ParamSpec("TrioWrapInput")
|
|
15
|
+
TrioWrapOutput = TypeVar("TrioWrapOutput")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def trio_async_wrap(
|
|
19
|
+
func: Callable[TrioWrapInput, TrioWrapOutput],
|
|
20
|
+
trio_capacity_limiter: Optional[CapacityLimiter] = None
|
|
21
|
+
) -> Callable[TrioWrapInput, Coroutine[Any, Any, TrioWrapOutput]]:
|
|
22
|
+
@wraps(func)
|
|
23
|
+
def wrapper(*args: TrioWrapInput.args, **kwargs: TrioWrapInput.kwargs) -> Coroutine[Any, Any, TrioWrapOutput]:
|
|
24
|
+
func_with_kwargs = partial(func, **kwargs)
|
|
25
|
+
return trio.to_thread.run_sync(func_with_kwargs, *args, limiter=trio_capacity_limiter)
|
|
26
|
+
|
|
27
|
+
return wrapper
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_cdp_kwargs(**kwargs):
|
|
31
|
+
dict_ = {}
|
|
32
|
+
|
|
33
|
+
for key, value in kwargs.items():
|
|
34
|
+
if value is not None:
|
|
35
|
+
dict_[key] = value
|
|
36
|
+
|
|
37
|
+
return dict_
|