pydoll-python 1.3.3__tar.gz → 1.4.0__tar.gz
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.
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/PKG-INFO +23 -2
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/README.md +22 -1
- pydoll_python-1.4.0/pydoll/browser/__init__.py +4 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/browser/base.py +22 -19
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/browser/chrome.py +4 -5
- pydoll_python-1.4.0/pydoll/browser/constants.py +6 -0
- pydoll_python-1.4.0/pydoll/browser/edge.py +74 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/browser/managers.py +61 -33
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/browser/options.py +23 -2
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/browser/page.py +94 -1
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/dom.py +71 -1
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/input.py +89 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/page.py +23 -0
- pydoll_python-1.4.0/pydoll/common/__init__.py +1 -0
- pydoll_python-1.4.0/pydoll/common/keyboard.py +101 -0
- pydoll_python-1.4.0/pydoll/common/keys.py +52 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/connection/connection.py +6 -2
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/element.py +96 -6
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pyproject.toml +1 -1
- pydoll_python-1.3.3/pydoll/mixins/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/LICENSE +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/browser.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/fetch.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/network.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/runtime.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/storage.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/commands/target.py +0 -0
- {pydoll_python-1.3.3/pydoll/browser → pydoll_python-1.4.0/pydoll/connection}/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/connection/managers.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/constants.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/browser.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/dom.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/fetch.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/network.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/events/page.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/exceptions.py +0 -0
- {pydoll_python-1.3.3/pydoll/connection → pydoll_python-1.4.0/pydoll/mixins}/__init__.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/mixins/find_elements.py +0 -0
- {pydoll_python-1.3.3 → pydoll_python-1.4.0}/pydoll/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pydoll-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Thalison Fernandes
|
|
6
6
|
Author-email: thalissfernandes99@gmail.com
|
|
@@ -26,7 +26,9 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
<p align="center">
|
|
29
|
-
<
|
|
29
|
+
<a href="https://codecov.io/gh/autoscrape-labs/pydoll">
|
|
30
|
+
<img src="https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9"/>
|
|
31
|
+
</a>
|
|
30
32
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg" alt="Tests">
|
|
31
33
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg" alt="Ruff CI">
|
|
32
34
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg" alt="Release">
|
|
@@ -669,6 +671,25 @@ input_field = await page.find_element(By.CSS_SELECTOR, 'input[name="username"]')
|
|
|
669
671
|
await input_field.send_keys("user123")
|
|
670
672
|
```
|
|
671
673
|
|
|
674
|
+
##### `async set_input_files(files: Union[str, Path, List[Union[str, Path]]])`
|
|
675
|
+
Send files to an input element:
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
file_input_field = await page.find_element(By.XPATH, '//input[@type="file"]')
|
|
679
|
+
# Single-file upload
|
|
680
|
+
await file_input_field.set_input_files(r'c:\demo\demo1.file')
|
|
681
|
+
# Multi-file uploads
|
|
682
|
+
await file_input_field.set_input_files([r'c:\demo\demo1.file', r'c:\demo\demo2.file'])
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
You can also do this with the `expect_file_chooser` context manager:
|
|
686
|
+
```python
|
|
687
|
+
async with page.expect_file_chooser(files=r'c:\demo\demo1.file'):
|
|
688
|
+
file_input_field = await page.find_element(By.XPATH, '//button[@id="upload-button-demo"]')
|
|
689
|
+
await file_input_field.click()
|
|
690
|
+
```
|
|
691
|
+
This way supports any type of elements, not only inputs.
|
|
692
|
+
|
|
672
693
|
##### `async type_keys(text: str)`
|
|
673
694
|
Type realistically, key by key, simulating human input.
|
|
674
695
|
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<
|
|
10
|
+
<a href="https://codecov.io/gh/autoscrape-labs/pydoll">
|
|
11
|
+
<img src="https://codecov.io/gh/autoscrape-labs/pydoll/graph/badge.svg?token=40I938OGM9"/>
|
|
12
|
+
</a>
|
|
11
13
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/tests.yml/badge.svg" alt="Tests">
|
|
12
14
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg" alt="Ruff CI">
|
|
13
15
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg" alt="Release">
|
|
@@ -650,6 +652,25 @@ input_field = await page.find_element(By.CSS_SELECTOR, 'input[name="username"]')
|
|
|
650
652
|
await input_field.send_keys("user123")
|
|
651
653
|
```
|
|
652
654
|
|
|
655
|
+
##### `async set_input_files(files: Union[str, Path, List[Union[str, Path]]])`
|
|
656
|
+
Send files to an input element:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
file_input_field = await page.find_element(By.XPATH, '//input[@type="file"]')
|
|
660
|
+
# Single-file upload
|
|
661
|
+
await file_input_field.set_input_files(r'c:\demo\demo1.file')
|
|
662
|
+
# Multi-file uploads
|
|
663
|
+
await file_input_field.set_input_files([r'c:\demo\demo1.file', r'c:\demo\demo2.file'])
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
You can also do this with the `expect_file_chooser` context manager:
|
|
667
|
+
```python
|
|
668
|
+
async with page.expect_file_chooser(files=r'c:\demo\demo1.file'):
|
|
669
|
+
file_input_field = await page.find_element(By.XPATH, '//button[@id="upload-button-demo"]')
|
|
670
|
+
await file_input_field.click()
|
|
671
|
+
```
|
|
672
|
+
This way supports any type of elements, not only inputs.
|
|
673
|
+
|
|
653
674
|
##### `async type_keys(text: str)`
|
|
654
675
|
Type realistically, key by key, simulating human input.
|
|
655
676
|
|
|
@@ -4,6 +4,7 @@ from functools import partial
|
|
|
4
4
|
from random import randint
|
|
5
5
|
|
|
6
6
|
from pydoll import exceptions
|
|
7
|
+
from pydoll.browser.constants import BrowserType
|
|
7
8
|
from pydoll.browser.managers import (
|
|
8
9
|
BrowserOptionsManager,
|
|
9
10
|
BrowserProcessManager,
|
|
@@ -35,6 +36,7 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
35
36
|
self,
|
|
36
37
|
options: Options | None = None,
|
|
37
38
|
connection_port: int = None,
|
|
39
|
+
browser_type: BrowserType = None
|
|
38
40
|
):
|
|
39
41
|
"""
|
|
40
42
|
Initializes the Browser instance.
|
|
@@ -43,11 +45,15 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
43
45
|
options (Options | None): An instance of the Options class to
|
|
44
46
|
configure the browser. If None, default options will be used.
|
|
45
47
|
connection_port (int): The port to connect to the browser.
|
|
48
|
+
browser_type (BrowserType): The type of browser to use.
|
|
49
|
+
If None, it will be inferred from the options.
|
|
46
50
|
|
|
47
51
|
Raises:
|
|
48
52
|
TypeError: If any of the arguments are not callable.
|
|
49
53
|
"""
|
|
50
|
-
self.options = BrowserOptionsManager.initialize_options(
|
|
54
|
+
self.options = BrowserOptionsManager.initialize_options(
|
|
55
|
+
options, browser_type
|
|
56
|
+
)
|
|
51
57
|
self._proxy_manager = ProxyManager(self.options)
|
|
52
58
|
self._connection_port = (
|
|
53
59
|
connection_port if connection_port else randint(9223, 9322)
|
|
@@ -77,7 +83,7 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
77
83
|
exc_val: The exception value, if raised.
|
|
78
84
|
exc_tb: The traceback, if an exception was raised.
|
|
79
85
|
"""
|
|
80
|
-
if await self._is_browser_running():
|
|
86
|
+
if await self._is_browser_running(timeout=2):
|
|
81
87
|
await self.stop()
|
|
82
88
|
|
|
83
89
|
await self._connection_handler.close()
|
|
@@ -242,6 +248,7 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
242
248
|
await self._execute_command(BrowserCommands.CLOSE)
|
|
243
249
|
self._browser_process_manager.stop_process()
|
|
244
250
|
self._temp_directory_manager.cleanup()
|
|
251
|
+
await self._connection_handler.close()
|
|
245
252
|
else:
|
|
246
253
|
raise exceptions.BrowserNotRunning('Browser is not running')
|
|
247
254
|
|
|
@@ -505,13 +512,16 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
505
512
|
"""
|
|
506
513
|
|
|
507
514
|
valid_page = next(
|
|
508
|
-
(
|
|
509
|
-
|
|
510
|
-
|
|
515
|
+
(
|
|
516
|
+
page
|
|
517
|
+
for page in pages
|
|
518
|
+
if page.get('type') == 'page' and page.get('attached')
|
|
519
|
+
),
|
|
520
|
+
None,
|
|
511
521
|
)
|
|
512
522
|
|
|
513
523
|
if not valid_page:
|
|
514
|
-
raise RuntimeError(
|
|
524
|
+
raise RuntimeError('No valid attached browser page found.')
|
|
515
525
|
|
|
516
526
|
target_id = valid_page.get('targetId')
|
|
517
527
|
if not target_id:
|
|
@@ -549,20 +559,13 @@ class Browser(ABC): # noqa: PLR0904
|
|
|
549
559
|
)
|
|
550
560
|
|
|
551
561
|
def _setup_user_dir(self):
|
|
552
|
-
"""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
This method creates a temporary directory for browser data if
|
|
556
|
-
no user directory is specified in the browser options.
|
|
557
|
-
|
|
558
|
-
Returns:
|
|
559
|
-
None
|
|
560
|
-
"""
|
|
561
|
-
temp_dir = self._temp_directory_manager.create_temp_dir()
|
|
562
|
-
if '--user-data-dir' not in [
|
|
563
|
-
arg.split('=')[0] for arg in self.options.arguments
|
|
562
|
+
"""Prepares the user data directory if necessary."""
|
|
563
|
+
if "--user-data-dir" not in [
|
|
564
|
+
arg.split("=")[0] for arg in self.options.arguments
|
|
564
565
|
]:
|
|
565
|
-
|
|
566
|
+
# For all browsers, use a temporary directory
|
|
567
|
+
temp_dir = self._temp_directory_manager.create_temp_dir()
|
|
568
|
+
self.options.arguments.append(f"--user-data-dir={temp_dir.name}")
|
|
566
569
|
|
|
567
570
|
@abstractmethod
|
|
568
571
|
def _get_default_binary_location(self) -> str:
|
|
@@ -2,6 +2,7 @@ import platform
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from pydoll.browser.base import Browser
|
|
5
|
+
from pydoll.browser.constants import BrowserType
|
|
5
6
|
from pydoll.browser.managers import BrowserOptionsManager
|
|
6
7
|
from pydoll.browser.options import Options
|
|
7
8
|
|
|
@@ -28,7 +29,7 @@ class Chrome(Browser):
|
|
|
28
29
|
connection_port (int): The port to connect to the browser.
|
|
29
30
|
Defaults to 9222.
|
|
30
31
|
"""
|
|
31
|
-
super().__init__(options, connection_port)
|
|
32
|
+
super().__init__(options, connection_port, BrowserType.CHROME)
|
|
32
33
|
|
|
33
34
|
@staticmethod
|
|
34
35
|
def _get_default_binary_location():
|
|
@@ -57,7 +58,7 @@ class Chrome(Browser):
|
|
|
57
58
|
],
|
|
58
59
|
'Darwin': [
|
|
59
60
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
60
|
-
]
|
|
61
|
+
],
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
browser_path = browser_paths.get(os_name)
|
|
@@ -65,6 +66,4 @@ class Chrome(Browser):
|
|
|
65
66
|
if not browser_path:
|
|
66
67
|
raise ValueError('Unsupported OS')
|
|
67
68
|
|
|
68
|
-
return BrowserOptionsManager.validate_browser_paths(
|
|
69
|
-
browser_path
|
|
70
|
-
)
|
|
69
|
+
return BrowserOptionsManager.validate_browser_paths(browser_path)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydoll.browser.base import Browser
|
|
5
|
+
from pydoll.browser.constants import BrowserType
|
|
6
|
+
from pydoll.browser.managers import BrowserOptionsManager
|
|
7
|
+
from pydoll.browser.options import Options
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Edge(Browser):
|
|
11
|
+
"""
|
|
12
|
+
A class that implements the Edge browser functionality.
|
|
13
|
+
|
|
14
|
+
This class provides specific implementation for launching and
|
|
15
|
+
controlling Microsoft Edge browsers.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
options: Optional[Options] = None,
|
|
21
|
+
connection_port: Optional[int] = None,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initializes the Edge browser instance.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
options (Options | None): An instance of Options class to configure
|
|
28
|
+
the browser. If None, default options will be used.
|
|
29
|
+
connection_port (int): The port to connect to the browser.
|
|
30
|
+
Defaults to a random port between 9223 and 9322.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(options, connection_port, BrowserType.EDGE)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _get_default_binary_location():
|
|
36
|
+
"""
|
|
37
|
+
Gets the default location of the Edge browser executable.
|
|
38
|
+
|
|
39
|
+
This method determines the default Edge executable path based
|
|
40
|
+
on the operating system.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: The path to the Edge browser executable.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the operating system is not supported or
|
|
47
|
+
the browser executable is not found at the default location.
|
|
48
|
+
"""
|
|
49
|
+
os_name = platform.system()
|
|
50
|
+
|
|
51
|
+
browser_paths = {
|
|
52
|
+
"Windows": [
|
|
53
|
+
(r"C:\Program Files\Microsoft\Edge\Application"
|
|
54
|
+
r"\msedge.exe"),
|
|
55
|
+
(r"C:\Program Files (x86)\Microsoft\Edge"
|
|
56
|
+
r"\Application\msedge.exe"),
|
|
57
|
+
],
|
|
58
|
+
"Linux": [
|
|
59
|
+
"/usr/bin/microsoft-edge",
|
|
60
|
+
],
|
|
61
|
+
"Darwin": [
|
|
62
|
+
("/Applications/Microsoft Edge.app/Contents/MacOS"
|
|
63
|
+
"/Microsoft Edge"),
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
browser_path = browser_paths.get(os_name)
|
|
68
|
+
|
|
69
|
+
if not browser_path:
|
|
70
|
+
raise ValueError('Unsupported OS')
|
|
71
|
+
|
|
72
|
+
return BrowserOptionsManager.validate_browser_paths(
|
|
73
|
+
browser_path
|
|
74
|
+
)
|
|
@@ -4,7 +4,8 @@ import subprocess
|
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from tempfile import TemporaryDirectory
|
|
6
6
|
|
|
7
|
-
from pydoll.browser.
|
|
7
|
+
from pydoll.browser.constants import BrowserType
|
|
8
|
+
from pydoll.browser.options import ChromeOptions, EdgeOptions, Options
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class ProxyManager:
|
|
@@ -64,8 +65,8 @@ class ProxyManager:
|
|
|
64
65
|
argument and the proxy value if found, None otherwise.
|
|
65
66
|
"""
|
|
66
67
|
for index, arg in enumerate(self.options.arguments):
|
|
67
|
-
if arg.startswith(
|
|
68
|
-
return index, arg.split(
|
|
68
|
+
if arg.startswith("--proxy-server="):
|
|
69
|
+
return index, arg.split("=", 1)[1]
|
|
69
70
|
return None
|
|
70
71
|
|
|
71
72
|
@staticmethod
|
|
@@ -87,12 +88,12 @@ class ProxyManager:
|
|
|
87
88
|
- str: Password (or None if no credentials)
|
|
88
89
|
- str: Clean proxy URL without credentials
|
|
89
90
|
"""
|
|
90
|
-
if
|
|
91
|
+
if "@" not in proxy_value:
|
|
91
92
|
return False, None, None, proxy_value
|
|
92
93
|
|
|
93
94
|
try:
|
|
94
|
-
creds_part, server_part = proxy_value.split(
|
|
95
|
-
username, password = creds_part.split(
|
|
95
|
+
creds_part, server_part = proxy_value.split("@", 1)
|
|
96
|
+
username, password = creds_part.split(":", 1)
|
|
96
97
|
return True, username, password, server_part
|
|
97
98
|
except ValueError:
|
|
98
99
|
return False, None, None, proxy_value
|
|
@@ -112,7 +113,7 @@ class ProxyManager:
|
|
|
112
113
|
Returns:
|
|
113
114
|
None
|
|
114
115
|
"""
|
|
115
|
-
self.options.arguments[index] = f
|
|
116
|
+
self.options.arguments[index] = f"--proxy-server={clean_proxy}"
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
class BrowserProcessManager:
|
|
@@ -149,11 +150,13 @@ class BrowserProcessManager:
|
|
|
149
150
|
Returns:
|
|
150
151
|
subprocess.Popen: The started browser process.
|
|
151
152
|
"""
|
|
152
|
-
self._process = self._process_creator(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
self._process = self._process_creator(
|
|
154
|
+
[
|
|
155
|
+
binary_location,
|
|
156
|
+
f"--remote-debugging-port={port}",
|
|
157
|
+
*arguments,
|
|
158
|
+
]
|
|
159
|
+
)
|
|
157
160
|
return self._process
|
|
158
161
|
|
|
159
162
|
@staticmethod
|
|
@@ -239,44 +242,69 @@ class TempDirectoryManager:
|
|
|
239
242
|
|
|
240
243
|
class BrowserOptionsManager:
|
|
241
244
|
@staticmethod
|
|
242
|
-
def initialize_options(
|
|
245
|
+
def initialize_options(
|
|
246
|
+
options: Options | None, browser_type: BrowserType = None
|
|
247
|
+
) -> Options:
|
|
243
248
|
"""
|
|
244
|
-
|
|
249
|
+
Initialize browser options based on browser type.
|
|
245
250
|
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
Creates a new options instance based on browser type if none
|
|
252
|
+
is provided, or validates and returns the provided
|
|
253
|
+
options instance.
|
|
248
254
|
|
|
249
255
|
Args:
|
|
250
|
-
options (Options | None):
|
|
256
|
+
options (Options | None): Browser options instance.
|
|
257
|
+
If None, a new instance
|
|
258
|
+
will be created based on browser_type
|
|
259
|
+
browser_type (BrowserType): Type of browser, used to create
|
|
260
|
+
appropriate options instance
|
|
251
261
|
|
|
252
262
|
Returns:
|
|
253
|
-
Options:
|
|
263
|
+
Options: The initialized browser options instance
|
|
254
264
|
|
|
255
265
|
Raises:
|
|
256
|
-
ValueError: If options is not
|
|
266
|
+
ValueError: If provided options is not an instance
|
|
267
|
+
of Options class
|
|
257
268
|
"""
|
|
258
269
|
if options is None:
|
|
259
|
-
|
|
270
|
+
if browser_type == BrowserType.CHROME:
|
|
271
|
+
return ChromeOptions()
|
|
272
|
+
elif browser_type == BrowserType.EDGE:
|
|
273
|
+
return EdgeOptions()
|
|
274
|
+
else:
|
|
275
|
+
return Options()
|
|
276
|
+
|
|
260
277
|
if not isinstance(options, Options):
|
|
261
|
-
raise ValueError(
|
|
278
|
+
raise ValueError("Invalid options")
|
|
279
|
+
|
|
262
280
|
return options
|
|
263
281
|
|
|
264
282
|
@staticmethod
|
|
265
283
|
def add_default_arguments(options: Options):
|
|
266
|
-
"""
|
|
267
|
-
|
|
284
|
+
"""Adds default arguments to the provided options"""
|
|
285
|
+
options.arguments.append("--no-first-run")
|
|
286
|
+
options.arguments.append("--no-default-browser-check")
|
|
268
287
|
|
|
269
|
-
|
|
270
|
-
|
|
288
|
+
# Add browser-specific arguments based on options type
|
|
289
|
+
if isinstance(options, EdgeOptions):
|
|
290
|
+
BrowserOptionsManager._add_edge_arguments(options)
|
|
291
|
+
elif isinstance(options, ChromeOptions):
|
|
292
|
+
BrowserOptionsManager._add_chrome_arguments(options)
|
|
271
293
|
|
|
272
|
-
|
|
273
|
-
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _add_edge_arguments(options: Options):
|
|
296
|
+
"""Adds Edge-specific arguments to the options"""
|
|
297
|
+
options.add_argument("--disable-crash-reporter")
|
|
298
|
+
options.add_argument("--disable-features=TranslateUI")
|
|
299
|
+
options.add_argument("--disable-component-update")
|
|
300
|
+
options.add_argument("--disable-background-networking")
|
|
301
|
+
options.add_argument("--remote-allow-origins=*")
|
|
274
302
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"""
|
|
278
|
-
options.
|
|
279
|
-
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _add_chrome_arguments(options: Options):
|
|
305
|
+
"""Adds Chrome-specific arguments to the options"""
|
|
306
|
+
options.add_argument("--remote-allow-origins=*")
|
|
307
|
+
# Add other Chrome-specific arguments here
|
|
280
308
|
|
|
281
309
|
@staticmethod
|
|
282
310
|
def validate_browser_paths(paths: list[str]) -> str:
|
|
@@ -299,4 +327,4 @@ class BrowserOptionsManager:
|
|
|
299
327
|
for path in paths:
|
|
300
328
|
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
301
329
|
return path
|
|
302
|
-
raise ValueError(f
|
|
330
|
+
raise ValueError(f'No valid browser path found in: {paths}')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
class Options:
|
|
2
3
|
"""
|
|
3
4
|
A class to manage command-line options for a browser instance.
|
|
@@ -26,6 +27,16 @@ class Options:
|
|
|
26
27
|
"""
|
|
27
28
|
return self._arguments
|
|
28
29
|
|
|
30
|
+
@arguments.setter
|
|
31
|
+
def arguments(self, args_list: list):
|
|
32
|
+
"""
|
|
33
|
+
Sets the list of command-line arguments.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
args_list (list): A list of command-line arguments.
|
|
37
|
+
"""
|
|
38
|
+
self._arguments = args_list
|
|
39
|
+
|
|
29
40
|
@property
|
|
30
41
|
def binary_location(self) -> str:
|
|
31
42
|
"""
|
|
@@ -56,7 +67,17 @@ class Options:
|
|
|
56
67
|
Raises:
|
|
57
68
|
ValueError: If the argument is already in the list of arguments.
|
|
58
69
|
"""
|
|
59
|
-
if argument not in self.
|
|
60
|
-
self.
|
|
70
|
+
if argument not in self._arguments:
|
|
71
|
+
self._arguments.append(argument)
|
|
61
72
|
else:
|
|
62
73
|
raise ValueError(f'Argument already exists: {argument}')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ChromeOptions(Options):
|
|
77
|
+
def __init__(self):
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EdgeOptions(Options):
|
|
82
|
+
def __init__(self):
|
|
83
|
+
super().__init__()
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Union
|
|
3
6
|
|
|
4
7
|
import aiofiles
|
|
5
8
|
|
|
@@ -13,6 +16,7 @@ from pydoll.commands import (
|
|
|
13
16
|
)
|
|
14
17
|
from pydoll.connection.connection import ConnectionHandler
|
|
15
18
|
from pydoll.element import WebElement
|
|
19
|
+
from pydoll.events import PageEvents
|
|
16
20
|
from pydoll.exceptions import InvalidFileExtension
|
|
17
21
|
from pydoll.mixins.find_elements import FindElementsMixin
|
|
18
22
|
from pydoll.utils import decode_image_to_bytes
|
|
@@ -34,6 +38,7 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
34
38
|
self._network_events_enabled = False
|
|
35
39
|
self._fetch_events_enabled = False
|
|
36
40
|
self._dom_events_enabled = False
|
|
41
|
+
self._intercept_file_chooser_dialog_enabled = False
|
|
37
42
|
|
|
38
43
|
@property
|
|
39
44
|
def page_events_enabled(self) -> bool:
|
|
@@ -75,6 +80,17 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
75
80
|
"""
|
|
76
81
|
return self._dom_events_enabled
|
|
77
82
|
|
|
83
|
+
@property
|
|
84
|
+
def intercept_file_chooser_dialog_enabled(self) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Returns whether file chooser dialogs are being intercepted or not.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if file chooser dialogs are being intercepted,
|
|
90
|
+
False otherwise.
|
|
91
|
+
"""
|
|
92
|
+
return self._intercept_file_chooser_dialog_enabled
|
|
93
|
+
|
|
78
94
|
@property
|
|
79
95
|
async def current_url(self) -> str:
|
|
80
96
|
"""
|
|
@@ -287,7 +303,7 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
287
303
|
Args:
|
|
288
304
|
path (str): The file path to save the PDF file to.
|
|
289
305
|
"""
|
|
290
|
-
response = await self._execute_command(PageCommands.print_to_pdf(
|
|
306
|
+
response = await self._execute_command(PageCommands.print_to_pdf())
|
|
291
307
|
pdf_b64 = response['result']['data'].encode('utf-8')
|
|
292
308
|
pdf_bytes = decode_image_to_bytes(pdf_b64)
|
|
293
309
|
async with aiofiles.open(path, 'wb') as file:
|
|
@@ -430,6 +446,22 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
430
446
|
await self._execute_command(DomCommands.enable_dom_events())
|
|
431
447
|
self._dom_events_enabled = True
|
|
432
448
|
|
|
449
|
+
async def enable_intercept_file_chooser_dialog(self):
|
|
450
|
+
"""
|
|
451
|
+
Enable intercepting file chooser dialogs.
|
|
452
|
+
|
|
453
|
+
When file chooser interception is enabled, native file chooser dialog
|
|
454
|
+
is not shown. Instead, a protocol event Page.fileChooserOpened is
|
|
455
|
+
emitted.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
None
|
|
459
|
+
"""
|
|
460
|
+
await self._execute_command(
|
|
461
|
+
PageCommands.set_intercept_file_chooser_dialog(True)
|
|
462
|
+
)
|
|
463
|
+
self._intercept_file_chooser_dialog_enabled = True
|
|
464
|
+
|
|
433
465
|
async def disable_fetch_events(self):
|
|
434
466
|
"""
|
|
435
467
|
Disables fetch events for the page.
|
|
@@ -456,6 +488,21 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
456
488
|
await self._execute_command(PageCommands.disable_page())
|
|
457
489
|
self._page_events_enabled = False
|
|
458
490
|
|
|
491
|
+
async def disable_intercept_file_chooser_dialog(self):
|
|
492
|
+
"""
|
|
493
|
+
Disable intercepting file chooser dialogs.
|
|
494
|
+
|
|
495
|
+
When file chooser interception is disabled, native file chooser
|
|
496
|
+
dialog is shown.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
None
|
|
500
|
+
"""
|
|
501
|
+
await self._execute_command(
|
|
502
|
+
PageCommands.set_intercept_file_chooser_dialog(False)
|
|
503
|
+
)
|
|
504
|
+
self._intercept_file_chooser_dialog_enabled = False
|
|
505
|
+
|
|
459
506
|
async def on(
|
|
460
507
|
self, event_name: str, callback: callable, temporary: bool = False
|
|
461
508
|
):
|
|
@@ -554,3 +601,49 @@ class Page(FindElementsMixin): # noqa: PLR0904
|
|
|
554
601
|
if asyncio.get_event_loop().time() - start_time > timeout:
|
|
555
602
|
raise asyncio.TimeoutError('Page load timed out')
|
|
556
603
|
await asyncio.sleep(0.5)
|
|
604
|
+
|
|
605
|
+
@asynccontextmanager
|
|
606
|
+
async def expect_file_chooser(
|
|
607
|
+
self, files: Union[str, Path, List[Union[str, Path]]]
|
|
608
|
+
):
|
|
609
|
+
"""
|
|
610
|
+
Provide a context manager that expects a file chooser dialog to be
|
|
611
|
+
opened and handles the file upload. When a file selection signal
|
|
612
|
+
is captured, the file is uploaded.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
files (Union[str, Path, List[Union[str, Path]]]): The files to be
|
|
616
|
+
uploaded.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
|
|
620
|
+
"""
|
|
621
|
+
|
|
622
|
+
async def event_handler(event):
|
|
623
|
+
await self._execute_command(
|
|
624
|
+
DomCommands.upload_files(
|
|
625
|
+
files=files,
|
|
626
|
+
backend_node_id=event['params']['backendNodeId'],
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if self.page_events_enabled is False:
|
|
631
|
+
_before_page_events_enabled = False
|
|
632
|
+
await self.enable_page_events()
|
|
633
|
+
else:
|
|
634
|
+
_before_page_events_enabled = True
|
|
635
|
+
|
|
636
|
+
if self.intercept_file_chooser_dialog_enabled is False:
|
|
637
|
+
await self.enable_intercept_file_chooser_dialog()
|
|
638
|
+
|
|
639
|
+
await self.on(
|
|
640
|
+
PageEvents.FILE_CHOOSER_OPENED, event_handler, temporary=True
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
yield
|
|
644
|
+
|
|
645
|
+
if self.intercept_file_chooser_dialog_enabled is True:
|
|
646
|
+
await self.disable_intercept_file_chooser_dialog()
|
|
647
|
+
|
|
648
|
+
if _before_page_events_enabled is False:
|
|
649
|
+
await self.disable_page_events()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
|
-
from
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Literal, Optional, Union
|
|
3
4
|
|
|
4
5
|
from pydoll.commands.runtime import RuntimeCommands
|
|
5
6
|
from pydoll.constants import By, Scripts
|
|
@@ -43,6 +44,10 @@ class DomCommands:
|
|
|
43
44
|
'method': 'DOM.scrollIntoViewIfNeeded',
|
|
44
45
|
'params': {},
|
|
45
46
|
}
|
|
47
|
+
SET_FILE_INPUT_FILES_TEMPLATE = {
|
|
48
|
+
'method': 'DOM.setFileInputFiles',
|
|
49
|
+
'params': {},
|
|
50
|
+
}
|
|
46
51
|
|
|
47
52
|
@classmethod
|
|
48
53
|
def scroll_into_view(cls, object_id: str) -> dict:
|
|
@@ -325,3 +330,68 @@ class DomCommands:
|
|
|
325
330
|
to make it relative.
|
|
326
331
|
"""
|
|
327
332
|
return f'.{xpath}' if not xpath.startswith('.') else xpath
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _ensure_file_exists(
|
|
336
|
+
files: Union[str, Path, List[Union[str, Path]]],
|
|
337
|
+
missing_ok: bool = False,
|
|
338
|
+
) -> List[str]:
|
|
339
|
+
"""
|
|
340
|
+
Ensures that the file exists.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
files (Union[str, Path, List[Union[str, Path]]]): Files to check.
|
|
344
|
+
missing_ok (bool): If True, skips the file existence check.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
List[str]: List of file paths.
|
|
348
|
+
"""
|
|
349
|
+
if isinstance(files, str):
|
|
350
|
+
files = [Path(files).absolute()]
|
|
351
|
+
elif isinstance(files, Path):
|
|
352
|
+
files = [files]
|
|
353
|
+
|
|
354
|
+
_has_ensure_files = []
|
|
355
|
+
for filepath in files:
|
|
356
|
+
if isinstance(filepath, str):
|
|
357
|
+
_filepath = Path(filepath).absolute()
|
|
358
|
+
else:
|
|
359
|
+
_filepath = filepath
|
|
360
|
+
if missing_ok is False and _filepath.is_file() is False:
|
|
361
|
+
raise FileExistsError(f'{_filepath} does not exist.')
|
|
362
|
+
_has_ensure_files.append(str(_filepath))
|
|
363
|
+
|
|
364
|
+
return _has_ensure_files
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def upload_files(
|
|
368
|
+
cls,
|
|
369
|
+
files: Union[str, Path, List[Union[str, Path]]],
|
|
370
|
+
object_id: Optional[str] = None,
|
|
371
|
+
backend_node_id: Optional[int] = None,
|
|
372
|
+
missing_ok: bool = False,
|
|
373
|
+
) -> dict:
|
|
374
|
+
"""
|
|
375
|
+
Sets the value of the file input to these file paths or files.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
files (Union[str, Path, List[Union[str, Path]]]): Files to upload.
|
|
379
|
+
object_id (Optional[str]): JavaScript object id of node wrapper.
|
|
380
|
+
backend_node_id (Optional[int]): Identifier of the backend node.
|
|
381
|
+
missing_ok (bool): If True, skips the file existence check.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
dict: The CDP command to set the file input files.
|
|
385
|
+
"""
|
|
386
|
+
command = copy.deepcopy(cls.SET_FILE_INPUT_FILES_TEMPLATE)
|
|
387
|
+
if object_id is None and backend_node_id is None:
|
|
388
|
+
raise ValueError(
|
|
389
|
+
'Either object_id or backend_node_id is required.'
|
|
390
|
+
)
|
|
391
|
+
if object_id is not None:
|
|
392
|
+
command['params']['objectId'] = object_id
|
|
393
|
+
if backend_node_id is not None:
|
|
394
|
+
command['params']['backendNodeId'] = backend_node_id
|
|
395
|
+
command['params']['files'] = cls._ensure_file_exists(files, missing_ok)
|
|
396
|
+
|
|
397
|
+
return command
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from pydoll.common.keyboard import Keyboard
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class InputCommands:
|
|
2
5
|
"""
|
|
3
6
|
A class to define input commands for simulating user interactions
|
|
@@ -11,6 +14,7 @@ class InputCommands:
|
|
|
11
14
|
}
|
|
12
15
|
KEY_PRESS_TEMPLATE = {'method': 'Input.dispatchKeyEvent', 'params': {}}
|
|
13
16
|
INSERT_TEXT_TEMPLATE = {'method': 'Input.insertText', 'params': {}}
|
|
17
|
+
KEYBOARD = Keyboard()
|
|
14
18
|
|
|
15
19
|
@classmethod
|
|
16
20
|
def mouse_press(cls, x: int, y: int) -> dict:
|
|
@@ -104,3 +108,88 @@ class InputCommands:
|
|
|
104
108
|
'text': text,
|
|
105
109
|
}
|
|
106
110
|
return command
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def key_down(cls, key: list | tuple, command_id: int) -> dict:
|
|
114
|
+
"""
|
|
115
|
+
Generates the command to simulate pressing a key on the keyboard.
|
|
116
|
+
|
|
117
|
+
This method creates a command following the Chrome DevTools
|
|
118
|
+
Protocol (CDP)
|
|
119
|
+
to simulate a "keyDown" event, which represents pressing a key.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
key (list | tuple): The key and code of the key to be pressed.
|
|
123
|
+
command_id (int): The id of the command to be sent.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict: A dictionary containing the command to be sent to
|
|
127
|
+
the browser.
|
|
128
|
+
"""
|
|
129
|
+
key, key_code = key
|
|
130
|
+
if key in cls.KEYBOARD.MODIFIER_KEYS:
|
|
131
|
+
cls.KEYBOARD.modifiers.add(key)
|
|
132
|
+
|
|
133
|
+
modifiers = sum(
|
|
134
|
+
cls.KEYBOARD.MODIFIER_KEYS[k] for k in cls.KEYBOARD.modifiers
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
special_key = cls.KEYBOARD.get_special_key(key, modifiers, key_code)
|
|
138
|
+
vk_code = cls.KEYBOARD.SHIFT_SPECIAL.get(special_key, key_code)
|
|
139
|
+
special_code = cls.KEYBOARD.get_special_code(key)
|
|
140
|
+
|
|
141
|
+
key_down = {
|
|
142
|
+
'type': 'keyDown',
|
|
143
|
+
'key': key,
|
|
144
|
+
'code': special_code,
|
|
145
|
+
'windowsVirtualKeyCode': vk_code,
|
|
146
|
+
'modifiers': modifiers,
|
|
147
|
+
'text': special_key,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
command = cls.KEY_PRESS_TEMPLATE.copy()
|
|
151
|
+
command['id'] = command_id
|
|
152
|
+
command['params'] = key_down
|
|
153
|
+
return command
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def key_up(cls, key: list | tuple, command_id) -> dict:
|
|
157
|
+
"""
|
|
158
|
+
Generates the command to simulate releasing a key on the keyboard.
|
|
159
|
+
|
|
160
|
+
This method creates a command following the Chrome DevTools
|
|
161
|
+
Protocol (CDP)
|
|
162
|
+
to simulate a "keyUp" event, which represents releasing a key.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
key (list | tuple): The character of the key to be released.
|
|
166
|
+
command_id (int): The id of the command to be sent.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
dict: A dictionary containing the command to be sent to
|
|
170
|
+
the browser.
|
|
171
|
+
"""
|
|
172
|
+
key, key_code = key
|
|
173
|
+
if key in cls.KEYBOARD.MODIFIER_KEYS:
|
|
174
|
+
cls.KEYBOARD.modifiers.discard(key)
|
|
175
|
+
|
|
176
|
+
modifiers = sum(
|
|
177
|
+
cls.KEYBOARD.MODIFIER_KEYS[k] for k in cls.KEYBOARD.modifiers
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
special_key = cls.KEYBOARD.SHIFT_MAP.get(key, key)
|
|
181
|
+
vk_code = cls.KEYBOARD.SHIFT_SPECIAL.get(special_key, key_code)
|
|
182
|
+
special_code = cls.KEYBOARD.get_special_code(key)
|
|
183
|
+
|
|
184
|
+
key_up = {
|
|
185
|
+
'type': 'keyUp',
|
|
186
|
+
'key': key,
|
|
187
|
+
'code': special_code,
|
|
188
|
+
'windowsVirtualKeyCode': vk_code,
|
|
189
|
+
'modifiers': modifiers,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
command = cls.KEY_PRESS_TEMPLATE.copy()
|
|
193
|
+
command['id'] = command_id
|
|
194
|
+
command['params'] = key_up
|
|
195
|
+
return command
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
class PageCommands:
|
|
2
5
|
"""
|
|
3
6
|
PageCommands class provides a set of commands to interact with the
|
|
@@ -32,6 +35,10 @@ class PageCommands:
|
|
|
32
35
|
}
|
|
33
36
|
HANDLE_DIALOG = {'method': 'Page.handleJavaScriptDialog', 'params': {}}
|
|
34
37
|
CLOSE = {'method': 'Page.close'}
|
|
38
|
+
SET_INTERCEPT_FILE_CHOOSER_DIALOG = {
|
|
39
|
+
'method': 'Page.setInterceptFileChooserDialog',
|
|
40
|
+
'params': {},
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
@classmethod
|
|
37
44
|
def handle_dialog(cls, accept: bool = True) -> dict:
|
|
@@ -185,3 +192,19 @@ class PageCommands:
|
|
|
185
192
|
containing the method to close the current page.
|
|
186
193
|
"""
|
|
187
194
|
return cls.CLOSE
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def set_intercept_file_chooser_dialog(cls, enabled: bool) -> dict:
|
|
198
|
+
"""
|
|
199
|
+
Generates the command to set whether to intercept file chooser dialogs.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
enabled: A boolean value indicating whether to enable or disable
|
|
203
|
+
the interception of file chooser dialogs.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
command = copy.deepcopy(cls.SET_INTERCEPT_FILE_CHOOSER_DIALOG)
|
|
209
|
+
command['params']['enabled'] = enabled
|
|
210
|
+
return command
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pydoll.common.keys import Keys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Keyboard(Keys):
|
|
5
|
+
MODIFIER_KEYS = {'Alt': 1, 'Control': 2, 'Meta': 4, 'Shift': 8}
|
|
6
|
+
SHIFT_MAP = {
|
|
7
|
+
'1': '!',
|
|
8
|
+
'2': '@',
|
|
9
|
+
'3': '#',
|
|
10
|
+
'4': '$',
|
|
11
|
+
'5': '%',
|
|
12
|
+
'6': '^',
|
|
13
|
+
'7': '&',
|
|
14
|
+
'8': '*',
|
|
15
|
+
'9': '(',
|
|
16
|
+
'0': ')',
|
|
17
|
+
'-': '_',
|
|
18
|
+
'=': '+',
|
|
19
|
+
'[': '{',
|
|
20
|
+
']': '}',
|
|
21
|
+
'\\': '|',
|
|
22
|
+
';': ':',
|
|
23
|
+
"'": '"',
|
|
24
|
+
',': '<',
|
|
25
|
+
'.': '>',
|
|
26
|
+
'/': '?',
|
|
27
|
+
}
|
|
28
|
+
SHIFT_CODES = {
|
|
29
|
+
'\\': 'Backslash',
|
|
30
|
+
'|': 'Backslash',
|
|
31
|
+
'[': 'BracketLeft',
|
|
32
|
+
']': 'BracketRight',
|
|
33
|
+
';': 'Semicolon',
|
|
34
|
+
':': 'Semicolon',
|
|
35
|
+
'/': 'Slash',
|
|
36
|
+
'?': 'Slash',
|
|
37
|
+
'.': 'Period',
|
|
38
|
+
',': 'Comma',
|
|
39
|
+
'-': 'Minus',
|
|
40
|
+
'+': 'Equal',
|
|
41
|
+
'=': 'Equal',
|
|
42
|
+
}
|
|
43
|
+
SHIFT_SPECIAL = {
|
|
44
|
+
'?': 63,
|
|
45
|
+
'|': 124,
|
|
46
|
+
'~': 126,
|
|
47
|
+
'+': 43,
|
|
48
|
+
'_': 95,
|
|
49
|
+
':': 58,
|
|
50
|
+
'!': 33,
|
|
51
|
+
'*': 42,
|
|
52
|
+
'(': 57,
|
|
53
|
+
')': 41,
|
|
54
|
+
'<': 60,
|
|
55
|
+
'>': 62,
|
|
56
|
+
'.': 190,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.modifiers = set()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_special_key(cls, key: str, modifiers: int, key_code: int) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Return the appropriate text value for special keys
|
|
66
|
+
like Shift, Enter, Space, etc.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key (str): The key code of the key to be pressed.
|
|
70
|
+
modifiers (int): The modifiers code of the key to be pressed.
|
|
71
|
+
key_code (int): The key code of the key to be pressed.
|
|
72
|
+
"""
|
|
73
|
+
if key_code in cls.SPACE:
|
|
74
|
+
return ' '
|
|
75
|
+
elif key_code in cls.ENTER:
|
|
76
|
+
return '\r'
|
|
77
|
+
elif modifiers & 8 and key != 'Shift':
|
|
78
|
+
return cls.SHIFT_MAP.get(key, key.upper())
|
|
79
|
+
elif len(key) == 1 and key.isprintable():
|
|
80
|
+
return key
|
|
81
|
+
|
|
82
|
+
return ''
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_special_code(cls, key) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Return the appropriate text value for special keys
|
|
88
|
+
like Shift, Enter, Space, etc.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
key (str): The key code of the special key.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
code = cls.SHIFT_CODES.get(key, key)
|
|
95
|
+
key_type = f'Digit{key}' if key.isdigit() else f'Key{key.upper()}'
|
|
96
|
+
if len(key) == 1:
|
|
97
|
+
return key_type
|
|
98
|
+
elif code in cls.MODIFIER_KEYS.keys():
|
|
99
|
+
return f'{code}Left'
|
|
100
|
+
|
|
101
|
+
return code
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class Keys:
|
|
2
|
+
BACKSPACE = ('Backspace', 8)
|
|
3
|
+
TAB = ('Tab', 9)
|
|
4
|
+
ENTER = ('Enter', 13)
|
|
5
|
+
SHIFT = ('Shift', 16)
|
|
6
|
+
CONTROL = ('Control', 17)
|
|
7
|
+
ALT = ('Alt', 18)
|
|
8
|
+
PAUSE = ('Pause', 19)
|
|
9
|
+
CAPSLOCK = ('CapsLock', 20)
|
|
10
|
+
ESCAPE = ('Escape', 27)
|
|
11
|
+
SPACE = ('Space', 32)
|
|
12
|
+
PAGEUP = ('PageUp', 33)
|
|
13
|
+
PAGEDOWN = ('PageDown', 34)
|
|
14
|
+
END = ('End', 35)
|
|
15
|
+
HOME = ('Home', 36)
|
|
16
|
+
ARROWLEFT = ('ArrowLeft', 37)
|
|
17
|
+
ARROWUP = ('ArrowUp', 38)
|
|
18
|
+
ARROWRIGHT = ('ArrowRight', 39)
|
|
19
|
+
ARROWDOWN = ('ArrowDown', 40)
|
|
20
|
+
PRINTSCREEN = ('PrintScreen', 44)
|
|
21
|
+
INSERT = ('Insert', 45)
|
|
22
|
+
DELETE = ('Delete', 46)
|
|
23
|
+
META = ('Meta', 91)
|
|
24
|
+
METARIGHT = ('MetaRight', 92)
|
|
25
|
+
CONTEXTMENU = ('ContextMenu', 93)
|
|
26
|
+
NUMLOCK = ('NumLock', 144)
|
|
27
|
+
SCROLLLOCK = ('ScrollLock', 145)
|
|
28
|
+
|
|
29
|
+
F1 = ('F1', 112)
|
|
30
|
+
F2 = ('F2', 113)
|
|
31
|
+
F3 = ('F3', 114)
|
|
32
|
+
F4 = ('F4', 115)
|
|
33
|
+
F5 = ('F5', 116)
|
|
34
|
+
F6 = ('F6', 117)
|
|
35
|
+
F7 = ('F7', 118)
|
|
36
|
+
F8 = ('F8', 119)
|
|
37
|
+
F9 = ('F9', 120)
|
|
38
|
+
F10 = ('F10', 121)
|
|
39
|
+
F11 = ('F11', 122)
|
|
40
|
+
F12 = ('F12', 123)
|
|
41
|
+
|
|
42
|
+
SEMICOLON = ('Semicolon', 186)
|
|
43
|
+
EQUALSIGN = ('EqualSign', 187)
|
|
44
|
+
COMMA = ('Comma', 188)
|
|
45
|
+
MINUS = ('Minus', 189)
|
|
46
|
+
PERIOD = ('Period', 190)
|
|
47
|
+
SLASH = ('Slash', 191)
|
|
48
|
+
GRAVEACCENT = ('GraveAccent', 192)
|
|
49
|
+
BRACKETLEFT = ('BracketLeft', 219)
|
|
50
|
+
BACKSLASH = ('Backslash', 220)
|
|
51
|
+
BRACKETRIGHT = ('BracketRight', 221)
|
|
52
|
+
QUOTE = ('Quote', 222)
|
|
@@ -180,7 +180,10 @@ class ConnectionHandler:
|
|
|
180
180
|
Closes the WebSocket connection and clears all event callbacks.
|
|
181
181
|
"""
|
|
182
182
|
await self.clear_callbacks()
|
|
183
|
-
|
|
183
|
+
|
|
184
|
+
if self._ws_connection is not None:
|
|
185
|
+
await self._ws_connection.close()
|
|
186
|
+
|
|
184
187
|
logger.info('WebSocket connection closed.')
|
|
185
188
|
|
|
186
189
|
async def _ensure_active_connection(self):
|
|
@@ -209,7 +212,8 @@ class ConnectionHandler:
|
|
|
209
212
|
ws_address = await self._resolve_ws_address()
|
|
210
213
|
logger.info(f'Connecting to {ws_address}')
|
|
211
214
|
self._ws_connection = await self._ws_connector(
|
|
212
|
-
ws_address,
|
|
215
|
+
ws_address,
|
|
216
|
+
max_size=1024 * 1024 * 10, # 10MB
|
|
213
217
|
)
|
|
214
218
|
self._receive_task = asyncio.create_task(self._receive_events())
|
|
215
219
|
logger.debug('WebSocket connection established')
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Union
|
|
3
5
|
|
|
4
6
|
import aiofiles
|
|
5
7
|
from bs4 import BeautifulSoup
|
|
@@ -17,7 +19,7 @@ from pydoll.mixins.find_elements import FindElementsMixin
|
|
|
17
19
|
from pydoll.utils import decode_image_to_bytes
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
class WebElement(FindElementsMixin):
|
|
22
|
+
class WebElement(FindElementsMixin): # noqa: PLR0904
|
|
21
23
|
"""
|
|
22
24
|
Represents a DOM element in the browser.
|
|
23
25
|
|
|
@@ -26,6 +28,7 @@ class WebElement(FindElementsMixin):
|
|
|
26
28
|
attributes, and other common web element interactions. It inherits element
|
|
27
29
|
finding capabilities from FindElementsMixin.
|
|
28
30
|
"""
|
|
31
|
+
|
|
29
32
|
def __init__(
|
|
30
33
|
self,
|
|
31
34
|
object_id: str,
|
|
@@ -53,6 +56,8 @@ class WebElement(FindElementsMixin):
|
|
|
53
56
|
self._selector = selector
|
|
54
57
|
self._connection_handler = connection_handler
|
|
55
58
|
self._attributes = {}
|
|
59
|
+
self._last_input = ''
|
|
60
|
+
self._command_id = 0
|
|
56
61
|
self._def_attributes(attributes_list)
|
|
57
62
|
|
|
58
63
|
def __repr__(self):
|
|
@@ -316,7 +321,12 @@ class WebElement(FindElementsMixin):
|
|
|
316
321
|
'Element is not interactable.'
|
|
317
322
|
)
|
|
318
323
|
|
|
319
|
-
async def click(
|
|
324
|
+
async def click(
|
|
325
|
+
self,
|
|
326
|
+
x_offset: int = 0,
|
|
327
|
+
y_offset: int = 0,
|
|
328
|
+
hold_time: float = 0.1,
|
|
329
|
+
):
|
|
320
330
|
"""
|
|
321
331
|
Clicks on the element using mouse events.
|
|
322
332
|
|
|
@@ -329,6 +339,8 @@ class WebElement(FindElementsMixin):
|
|
|
329
339
|
Defaults to 0.
|
|
330
340
|
y_offset (int): Vertical offset from the center of the element.
|
|
331
341
|
Defaults to 0.
|
|
342
|
+
hold_time (float, optional): The duration (in seconds) to hold
|
|
343
|
+
the mouse button before releasing.
|
|
332
344
|
|
|
333
345
|
Raises:
|
|
334
346
|
ElementNotVisible: If the element is not visible on the page.
|
|
@@ -363,7 +375,7 @@ class WebElement(FindElementsMixin):
|
|
|
363
375
|
press_command = InputCommands.mouse_press(*position_to_click)
|
|
364
376
|
release_command = InputCommands.mouse_release(*position_to_click)
|
|
365
377
|
await self._connection_handler.execute_command(press_command)
|
|
366
|
-
await asyncio.sleep(
|
|
378
|
+
await asyncio.sleep(hold_time)
|
|
367
379
|
await self._connection_handler.execute_command(release_command)
|
|
368
380
|
|
|
369
381
|
async def click_option_tag(self):
|
|
@@ -380,7 +392,7 @@ class WebElement(FindElementsMixin):
|
|
|
380
392
|
script = Scripts.CLICK_OPTION_TAG.replace('{self.value}', self.value)
|
|
381
393
|
await self._execute_command(RuntimeCommands.evaluate_script(script))
|
|
382
394
|
|
|
383
|
-
async def
|
|
395
|
+
async def insert_text(self, text: str):
|
|
384
396
|
"""
|
|
385
397
|
Sends a sequence of keys to the element.
|
|
386
398
|
|
|
@@ -389,16 +401,94 @@ class WebElement(FindElementsMixin):
|
|
|
389
401
|
"""
|
|
390
402
|
await self._execute_command(InputCommands.insert_text(text))
|
|
391
403
|
|
|
392
|
-
async def
|
|
404
|
+
async def set_input_files(
|
|
405
|
+
self, files: Union[str, Path, List[Union[str, Path]]]
|
|
406
|
+
):
|
|
407
|
+
"""
|
|
408
|
+
Sets the value of the file input to these file paths.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
files (Union[str, Path, List[Union[str, Path]]]): Files to upload.
|
|
412
|
+
"""
|
|
413
|
+
if (
|
|
414
|
+
self._attributes.get('tag_name', '').lower() != 'input'
|
|
415
|
+
or self._attributes.get('type', '').lower() != 'file'
|
|
416
|
+
):
|
|
417
|
+
raise exceptions.ElementNotInteractable(
|
|
418
|
+
'The element is not a file input.'
|
|
419
|
+
)
|
|
420
|
+
await self._execute_command(
|
|
421
|
+
DomCommands.upload_files(files=files, object_id=self._object_id)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
async def type_keys(self, text: str, interval: float = 0.1):
|
|
393
425
|
"""
|
|
394
426
|
Types in a realistic manner by sending keys one by one.
|
|
395
427
|
|
|
396
428
|
Args:
|
|
397
429
|
text (str): The text to send to the element.
|
|
430
|
+
interval (float): The interval between two keys.
|
|
398
431
|
"""
|
|
399
432
|
for char in text:
|
|
400
433
|
await self._execute_command(InputCommands.key_press(char))
|
|
401
|
-
await asyncio.sleep(
|
|
434
|
+
await asyncio.sleep(interval)
|
|
435
|
+
|
|
436
|
+
self._last_input = text
|
|
437
|
+
|
|
438
|
+
async def key_down(self, key: list | tuple):
|
|
439
|
+
"""
|
|
440
|
+
Simulates pressing down a key.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
key (list|tuple): The key to press.
|
|
444
|
+
"""
|
|
445
|
+
self._command_id += 1
|
|
446
|
+
await self._execute_command(
|
|
447
|
+
InputCommands.key_down(key, self._command_id)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
async def key_up(self, key: list | tuple):
|
|
451
|
+
"""
|
|
452
|
+
Simulates releasing a key.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
key (list|tuple): The key to release.
|
|
456
|
+
"""
|
|
457
|
+
self._command_id += 1
|
|
458
|
+
await self._execute_command(
|
|
459
|
+
InputCommands.key_up(key, self._command_id)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
async def send_keys(self, keys: str | tuple, interval: float = 0.1):
|
|
463
|
+
"""
|
|
464
|
+
Dispatches an event.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
keys (str | tuple): The event to dispatch.
|
|
468
|
+
interval (float): The interval between two keys.
|
|
469
|
+
"""
|
|
470
|
+
if isinstance(keys, str):
|
|
471
|
+
self._last_input = keys
|
|
472
|
+
keys = [(char, ord(char.upper())) for char in keys]
|
|
473
|
+
elif not all(isinstance(k, tuple) for k in keys):
|
|
474
|
+
keys = [keys]
|
|
475
|
+
|
|
476
|
+
for key in keys:
|
|
477
|
+
await self.key_down(key)
|
|
478
|
+
await self.key_up(key)
|
|
479
|
+
await asyncio.sleep(interval)
|
|
480
|
+
|
|
481
|
+
async def backspace(self, interval: float = 0.1):
|
|
482
|
+
"""
|
|
483
|
+
Backspaces the key at the element.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
interval (float): The interval between two keys.
|
|
487
|
+
"""
|
|
488
|
+
for _ in range(len(self._last_input)):
|
|
489
|
+
await self.send_keys(('Backspace', 8))
|
|
490
|
+
await asyncio.sleep(interval)
|
|
491
|
+
self._last_input = self._last_input[:-1]
|
|
402
492
|
|
|
403
493
|
def _is_option_tag(self):
|
|
404
494
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|