pydoll-python 2.10.0__tar.gz → 2.12.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-2.10.0 → pydoll_python-2.12.0}/PKG-INFO +84 -2
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/README.md +82 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/tab.py +239 -56
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/constants.py +118 -2
- pydoll_python-2.12.0/pydoll/decorators.py +140 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/elements/web_element.py +147 -14
- pydoll_python-2.12.0/pydoll/interactions/__init__.py +4 -0
- pydoll_python-2.12.0/pydoll/interactions/keyboard.py +178 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pyproject.toml +2 -2
- pydoll_python-2.10.0/pydoll/interactions/__init__.py +0 -3
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/LICENSE +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/base.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/chrome.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/edge.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/interfaces.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/managers/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/managers/proxy_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/options.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/requests/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/requests/request.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/browser/requests/response.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/browser_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/dom_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/fetch_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/input_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/network_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/page_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/runtime_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/storage_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/commands/target_commands.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/connection/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/connection/connection_handler.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/connection/managers/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/connection/managers/commands_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/connection/managers/events_manager.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/elements/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/elements/mixins/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/elements/mixins/find_elements_mixin.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/exceptions.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/interactions/scroll.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/base.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/debugger/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/emulation/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/input/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/input/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/input/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/input/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/io/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/network/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/network/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/network/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/network/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/page/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/page/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/page/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/page/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/security/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/target/__init__.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/target/events.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/target/methods.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/protocol/target/types.py +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/py.typed +0 -0
- {pydoll_python-2.10.0 → pydoll_python-2.12.0}/pydoll/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydoll-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: Pydoll is a library for automating chromium-based browsers without a WebDriver, offering realistic interactions.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Thalison Fernandes
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
-
Requires-Dist: aiofiles (>=
|
|
15
|
+
Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
|
|
16
16
|
Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
|
|
17
17
|
Requires-Dist: typing_extensions (>=4.14.0,<5.0.0)
|
|
18
18
|
Requires-Dist: websockets (>=14,<15)
|
|
@@ -82,6 +82,88 @@ await tab.scroll.by(ScrollPosition.UP, 300, smooth=False)
|
|
|
82
82
|
|
|
83
83
|
Unlike `execute_script("window.scrollBy(...)")` which returns immediately, the scroll API uses CDP's `awaitPromise` to wait for the browser's `scrollend` event, ensuring your next actions only execute after scrolling completely finishes. Perfect for taking screenshots, loading lazy content, or creating realistic reading patterns.
|
|
84
84
|
|
|
85
|
+
### Keyboard API: Complete Control Over Keyboard Input
|
|
86
|
+
|
|
87
|
+
The new `KeyboardAPI` provides a clean, centralized interface for all keyboard interactions at the page level:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pydoll.constants import Key
|
|
91
|
+
|
|
92
|
+
# Press individual keys
|
|
93
|
+
await tab.keyboard.press(Key.ENTER)
|
|
94
|
+
await tab.keyboard.press(Key.TAB)
|
|
95
|
+
|
|
96
|
+
# Use hotkeys/shortcuts with up to 3 keys
|
|
97
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.A) # Select all (works!)
|
|
98
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.C) # Copy (works!)
|
|
99
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT) # Select word right
|
|
100
|
+
|
|
101
|
+
# Manual control for complex sequences
|
|
102
|
+
await tab.keyboard.down(Key.SHIFT)
|
|
103
|
+
await tab.keyboard.press(Key.ARROWRIGHT) # Select text while holding Shift
|
|
104
|
+
await tab.keyboard.up(Key.SHIFT)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Key improvements:**
|
|
108
|
+
- **Centralized**: All keyboard operations accessible via `tab.keyboard`
|
|
109
|
+
- **Smart modifier detection**: Hotkeys automatically detect and apply modifiers (Ctrl, Shift, Alt, Meta)
|
|
110
|
+
- **Complete key support**: 26 letters (A-Z), 10 digits (0-9), all function keys, numpad, and special keys
|
|
111
|
+
- **Page-level shortcuts**: Works for Ctrl+C, Ctrl+V, Ctrl+A, etc.
|
|
112
|
+
|
|
113
|
+
> **⚠️ CDP Limitation:** Browser UI shortcuts (like Ctrl+T for new tab, F12 for DevTools) don't work via CDP. Use Pydoll's methods instead: `await browser.new_tab()`, `await tab.close()`.
|
|
114
|
+
|
|
115
|
+
### Retry Decorator: Production-Ready Error Recovery
|
|
116
|
+
|
|
117
|
+
Transform fragile scripts into robust production scrapers with the `@retry` decorator. Automatically recover from network failures, timeouts, and transient errors with exponential backoff and custom recovery strategies:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import asyncio
|
|
121
|
+
from pydoll.browser.chromium import Chrome
|
|
122
|
+
from pydoll.decorators import retry
|
|
123
|
+
from pydoll.exceptions import ElementNotFound, NetworkError
|
|
124
|
+
|
|
125
|
+
class ProductScraper:
|
|
126
|
+
def __init__(self):
|
|
127
|
+
self.tab = None
|
|
128
|
+
self.retry_count = 0
|
|
129
|
+
|
|
130
|
+
# Recovery callback executed before each retry
|
|
131
|
+
async def recover_from_failure(self):
|
|
132
|
+
self.retry_count += 1
|
|
133
|
+
print(f"Attempt {self.retry_count} failed. Recovering...")
|
|
134
|
+
|
|
135
|
+
# Refresh page and restore state
|
|
136
|
+
if self.tab:
|
|
137
|
+
await self.tab.refresh()
|
|
138
|
+
await asyncio.sleep(2)
|
|
139
|
+
|
|
140
|
+
@retry(
|
|
141
|
+
max_retries=3,
|
|
142
|
+
exceptions=[ElementNotFound, NetworkError],
|
|
143
|
+
on_retry=recover_from_failure, # Execute recovery logic
|
|
144
|
+
delay=2.0,
|
|
145
|
+
exponential_backoff=True
|
|
146
|
+
)
|
|
147
|
+
async def scrape_product(self, url: str):
|
|
148
|
+
if not self.tab:
|
|
149
|
+
browser = Chrome()
|
|
150
|
+
self.tab = await browser.start()
|
|
151
|
+
|
|
152
|
+
await self.tab.go_to(url)
|
|
153
|
+
title = await self.tab.find(class_name='product-title', timeout=5)
|
|
154
|
+
return await title.text
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Powerful features:**
|
|
158
|
+
- **Smart retry logic**: Only retry on specific exceptions you define
|
|
159
|
+
- **Exponential backoff**: Progressively increase wait times (1s → 2s → 4s → 8s)
|
|
160
|
+
- **Recovery callbacks**: Execute custom logic between retries (refresh page, switch proxy, restart browser)
|
|
161
|
+
- **Production-tested**: Handle the chaos of real-world scraping with confidence
|
|
162
|
+
|
|
163
|
+
Perfect for handling rate limits, network instability, dynamic content loading, and CAPTCHA detection. Turn unreliable scrapers into bulletproof automation.
|
|
164
|
+
|
|
165
|
+
[**📖 Full Documentation**](https://pydoll.tech/docs/features/advanced/decorators/)
|
|
166
|
+
|
|
85
167
|
## 📦 Installation
|
|
86
168
|
|
|
87
169
|
```bash
|
|
@@ -62,6 +62,88 @@ await tab.scroll.by(ScrollPosition.UP, 300, smooth=False)
|
|
|
62
62
|
|
|
63
63
|
Unlike `execute_script("window.scrollBy(...)")` which returns immediately, the scroll API uses CDP's `awaitPromise` to wait for the browser's `scrollend` event, ensuring your next actions only execute after scrolling completely finishes. Perfect for taking screenshots, loading lazy content, or creating realistic reading patterns.
|
|
64
64
|
|
|
65
|
+
### Keyboard API: Complete Control Over Keyboard Input
|
|
66
|
+
|
|
67
|
+
The new `KeyboardAPI` provides a clean, centralized interface for all keyboard interactions at the page level:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pydoll.constants import Key
|
|
71
|
+
|
|
72
|
+
# Press individual keys
|
|
73
|
+
await tab.keyboard.press(Key.ENTER)
|
|
74
|
+
await tab.keyboard.press(Key.TAB)
|
|
75
|
+
|
|
76
|
+
# Use hotkeys/shortcuts with up to 3 keys
|
|
77
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.A) # Select all (works!)
|
|
78
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.C) # Copy (works!)
|
|
79
|
+
await tab.keyboard.hotkey(Key.CONTROL, Key.SHIFT, Key.ARROWRIGHT) # Select word right
|
|
80
|
+
|
|
81
|
+
# Manual control for complex sequences
|
|
82
|
+
await tab.keyboard.down(Key.SHIFT)
|
|
83
|
+
await tab.keyboard.press(Key.ARROWRIGHT) # Select text while holding Shift
|
|
84
|
+
await tab.keyboard.up(Key.SHIFT)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Key improvements:**
|
|
88
|
+
- **Centralized**: All keyboard operations accessible via `tab.keyboard`
|
|
89
|
+
- **Smart modifier detection**: Hotkeys automatically detect and apply modifiers (Ctrl, Shift, Alt, Meta)
|
|
90
|
+
- **Complete key support**: 26 letters (A-Z), 10 digits (0-9), all function keys, numpad, and special keys
|
|
91
|
+
- **Page-level shortcuts**: Works for Ctrl+C, Ctrl+V, Ctrl+A, etc.
|
|
92
|
+
|
|
93
|
+
> **⚠️ CDP Limitation:** Browser UI shortcuts (like Ctrl+T for new tab, F12 for DevTools) don't work via CDP. Use Pydoll's methods instead: `await browser.new_tab()`, `await tab.close()`.
|
|
94
|
+
|
|
95
|
+
### Retry Decorator: Production-Ready Error Recovery
|
|
96
|
+
|
|
97
|
+
Transform fragile scripts into robust production scrapers with the `@retry` decorator. Automatically recover from network failures, timeouts, and transient errors with exponential backoff and custom recovery strategies:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from pydoll.browser.chromium import Chrome
|
|
102
|
+
from pydoll.decorators import retry
|
|
103
|
+
from pydoll.exceptions import ElementNotFound, NetworkError
|
|
104
|
+
|
|
105
|
+
class ProductScraper:
|
|
106
|
+
def __init__(self):
|
|
107
|
+
self.tab = None
|
|
108
|
+
self.retry_count = 0
|
|
109
|
+
|
|
110
|
+
# Recovery callback executed before each retry
|
|
111
|
+
async def recover_from_failure(self):
|
|
112
|
+
self.retry_count += 1
|
|
113
|
+
print(f"Attempt {self.retry_count} failed. Recovering...")
|
|
114
|
+
|
|
115
|
+
# Refresh page and restore state
|
|
116
|
+
if self.tab:
|
|
117
|
+
await self.tab.refresh()
|
|
118
|
+
await asyncio.sleep(2)
|
|
119
|
+
|
|
120
|
+
@retry(
|
|
121
|
+
max_retries=3,
|
|
122
|
+
exceptions=[ElementNotFound, NetworkError],
|
|
123
|
+
on_retry=recover_from_failure, # Execute recovery logic
|
|
124
|
+
delay=2.0,
|
|
125
|
+
exponential_backoff=True
|
|
126
|
+
)
|
|
127
|
+
async def scrape_product(self, url: str):
|
|
128
|
+
if not self.tab:
|
|
129
|
+
browser = Chrome()
|
|
130
|
+
self.tab = await browser.start()
|
|
131
|
+
|
|
132
|
+
await self.tab.go_to(url)
|
|
133
|
+
title = await self.tab.find(class_name='product-title', timeout=5)
|
|
134
|
+
return await title.text
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Powerful features:**
|
|
138
|
+
- **Smart retry logic**: Only retry on specific exceptions you define
|
|
139
|
+
- **Exponential backoff**: Progressively increase wait times (1s → 2s → 4s → 8s)
|
|
140
|
+
- **Recovery callbacks**: Execute custom logic between retries (refresh page, switch proxy, restart browser)
|
|
141
|
+
- **Production-tested**: Handle the chaos of real-world scraping with confidence
|
|
142
|
+
|
|
143
|
+
Perfect for handling rate limits, network instability, dynamic content loading, and CAPTCHA detection. Turn unreliable scrapers into bulletproof automation.
|
|
144
|
+
|
|
145
|
+
[**📖 Full Documentation**](https://pydoll.tech/docs/features/advanced/decorators/)
|
|
146
|
+
|
|
65
147
|
## 📦 Installation
|
|
66
148
|
|
|
67
149
|
```bash
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import base64 as _b64
|
|
5
5
|
import logging
|
|
6
6
|
import shutil
|
|
7
|
+
import warnings
|
|
7
8
|
from contextlib import asynccontextmanager
|
|
8
9
|
from functools import partial
|
|
9
10
|
from pathlib import Path
|
|
@@ -50,14 +51,20 @@ from pydoll.exceptions import (
|
|
|
50
51
|
TopLevelTargetRequired,
|
|
51
52
|
WaitElementTimeout,
|
|
52
53
|
)
|
|
53
|
-
from pydoll.interactions
|
|
54
|
+
from pydoll.interactions import KeyboardAPI, ScrollAPI
|
|
54
55
|
from pydoll.protocol.browser.types import DownloadBehavior, DownloadProgressState
|
|
55
56
|
from pydoll.protocol.page.events import PageEvent
|
|
56
57
|
from pydoll.protocol.page.types import ScreenshotFormat
|
|
58
|
+
from pydoll.protocol.runtime.methods import (
|
|
59
|
+
CallFunctionOnResponse,
|
|
60
|
+
EvaluateResponse,
|
|
61
|
+
SerializationOptions,
|
|
62
|
+
)
|
|
63
|
+
from pydoll.protocol.runtime.types import CallArgument
|
|
64
|
+
from pydoll.protocol.storage.methods import GetCookiesResponse
|
|
57
65
|
from pydoll.utils import (
|
|
58
66
|
decode_base64_to_bytes,
|
|
59
67
|
has_return_outside_function,
|
|
60
|
-
is_script_already_function,
|
|
61
68
|
)
|
|
62
69
|
|
|
63
70
|
if TYPE_CHECKING:
|
|
@@ -133,6 +140,7 @@ class Tab(FindElementsMixin):
|
|
|
133
140
|
self._cloudflare_captcha_callback_id: Optional[int] = None
|
|
134
141
|
self._request: Optional[Request] = None
|
|
135
142
|
self._scroll: Optional[ScrollAPI] = None
|
|
143
|
+
self._keyboard: Optional[KeyboardAPI] = None
|
|
136
144
|
logger.debug(
|
|
137
145
|
(
|
|
138
146
|
f'Tab initialized: target_id={self._target_id}, '
|
|
@@ -190,6 +198,18 @@ class Tab(FindElementsMixin):
|
|
|
190
198
|
self._scroll = ScrollAPI(self)
|
|
191
199
|
return self._scroll
|
|
192
200
|
|
|
201
|
+
@property
|
|
202
|
+
def keyboard(self) -> KeyboardAPI:
|
|
203
|
+
"""
|
|
204
|
+
Get the keyboard API for controlling keyboard input at page level.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
KeyboardAPI: An instance of the KeyboardAPI class for keyboard operations.
|
|
208
|
+
"""
|
|
209
|
+
if self._keyboard is None:
|
|
210
|
+
self._keyboard = KeyboardAPI(self)
|
|
211
|
+
return self._keyboard
|
|
212
|
+
|
|
193
213
|
@property
|
|
194
214
|
def intercept_file_chooser_dialog_enabled(self) -> bool:
|
|
195
215
|
"""Whether file chooser dialog interception is active."""
|
|
@@ -749,37 +769,174 @@ class Tab(FindElementsMixin):
|
|
|
749
769
|
)
|
|
750
770
|
|
|
751
771
|
@overload
|
|
752
|
-
async def execute_script(
|
|
772
|
+
async def execute_script(
|
|
773
|
+
self,
|
|
774
|
+
script: str,
|
|
775
|
+
*,
|
|
776
|
+
object_group: Optional[str] = None,
|
|
777
|
+
include_command_line_api: Optional[bool] = None,
|
|
778
|
+
silent: Optional[bool] = None,
|
|
779
|
+
context_id: Optional[int] = None,
|
|
780
|
+
return_by_value: Optional[bool] = None,
|
|
781
|
+
generate_preview: Optional[bool] = None,
|
|
782
|
+
user_gesture: Optional[bool] = None,
|
|
783
|
+
await_promise: Optional[bool] = None,
|
|
784
|
+
throw_on_side_effect: Optional[bool] = None,
|
|
785
|
+
timeout: Optional[float] = None,
|
|
786
|
+
disable_breaks: Optional[bool] = None,
|
|
787
|
+
repl_mode: Optional[bool] = None,
|
|
788
|
+
allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,
|
|
789
|
+
unique_context_id: Optional[str] = None,
|
|
790
|
+
serialization_options: Optional[SerializationOptions] = None,
|
|
791
|
+
) -> EvaluateResponse: ...
|
|
753
792
|
|
|
754
793
|
@overload
|
|
755
794
|
async def execute_script(
|
|
756
|
-
self,
|
|
795
|
+
self,
|
|
796
|
+
script: str,
|
|
797
|
+
element: WebElement,
|
|
798
|
+
*,
|
|
799
|
+
arguments: Optional[list[CallArgument]] = None,
|
|
800
|
+
silent: Optional[bool] = None,
|
|
801
|
+
return_by_value: Optional[bool] = None,
|
|
802
|
+
generate_preview: Optional[bool] = None,
|
|
803
|
+
user_gesture: Optional[bool] = None,
|
|
804
|
+
await_promise: Optional[bool] = None,
|
|
805
|
+
execution_context_id: Optional[int] = None,
|
|
806
|
+
object_group: Optional[str] = None,
|
|
807
|
+
throw_on_side_effect: Optional[bool] = None,
|
|
808
|
+
unique_context_id: Optional[str] = None,
|
|
809
|
+
serialization_options: Optional[SerializationOptions] = None,
|
|
757
810
|
) -> CallFunctionOnResponse: ...
|
|
758
811
|
|
|
759
812
|
async def execute_script(
|
|
760
|
-
self,
|
|
813
|
+
self,
|
|
814
|
+
script: str,
|
|
815
|
+
element: Optional[WebElement] = None,
|
|
816
|
+
*,
|
|
817
|
+
arguments: Optional[list[CallArgument]] = None,
|
|
818
|
+
object_group: Optional[str] = None,
|
|
819
|
+
include_command_line_api: Optional[bool] = None,
|
|
820
|
+
silent: Optional[bool] = None,
|
|
821
|
+
context_id: Optional[int] = None,
|
|
822
|
+
return_by_value: Optional[bool] = None,
|
|
823
|
+
generate_preview: Optional[bool] = None,
|
|
824
|
+
user_gesture: Optional[bool] = None,
|
|
825
|
+
await_promise: Optional[bool] = None,
|
|
826
|
+
execution_context_id: Optional[int] = None,
|
|
827
|
+
throw_on_side_effect: Optional[bool] = None,
|
|
828
|
+
timeout: Optional[float] = None,
|
|
829
|
+
disable_breaks: Optional[bool] = None,
|
|
830
|
+
repl_mode: Optional[bool] = None,
|
|
831
|
+
allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,
|
|
832
|
+
unique_context_id: Optional[str] = None,
|
|
833
|
+
serialization_options: Optional[SerializationOptions] = None,
|
|
761
834
|
) -> Union[EvaluateResponse, CallFunctionOnResponse]:
|
|
762
835
|
"""
|
|
763
836
|
Execute JavaScript in page context.
|
|
764
837
|
|
|
765
838
|
Args:
|
|
766
|
-
script: JavaScript code to execute.
|
|
767
|
-
element:
|
|
839
|
+
script (str): JavaScript code to execute.
|
|
840
|
+
element (Optional[WebElement]): Optional WebElement to execute script on.
|
|
841
|
+
arguments (Optional[list[CallArgument]]): Arguments to pass to the function.
|
|
842
|
+
object_group (Optional[str]): Symbolic group name for the result (Runtime.evaluate).
|
|
843
|
+
include_command_line_api (Optional[bool]): Whether to include command line API
|
|
844
|
+
(Runtime.evaluate).
|
|
845
|
+
silent (Optional[bool]): Whether to silence exceptions (Runtime.evaluate).
|
|
846
|
+
context_id (Optional[int]): ID of the execution context to evaluate in
|
|
847
|
+
(Runtime.evaluate).
|
|
848
|
+
return_by_value (Optional[bool]): Whether to return the result by value instead of
|
|
849
|
+
reference (Runtime.evaluate).
|
|
850
|
+
generate_preview (Optional[bool]): Whether to generate a preview for the result
|
|
851
|
+
(Runtime.evaluate).
|
|
852
|
+
user_gesture (Optional[bool]): Whether to treat evaluation as initiated by user
|
|
853
|
+
gesture (Runtime.evaluate).
|
|
854
|
+
await_promise (Optional[bool]): Whether to await promise result (Runtime.evaluate).
|
|
855
|
+
execution_context_id (Optional[int]): ID of the execution context to call the
|
|
856
|
+
function in.
|
|
857
|
+
throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be
|
|
858
|
+
ruled out (Runtime.evaluate).
|
|
859
|
+
timeout (Optional[float]): Timeout in milliseconds (Runtime.evaluate).
|
|
860
|
+
disable_breaks (Optional[bool]): Whether to disable breakpoints during evaluation
|
|
861
|
+
(Runtime.evaluate).
|
|
862
|
+
repl_mode (Optional[bool]): Whether to execute in REPL mode (Runtime.evaluate).
|
|
863
|
+
allow_unsafe_eval_blocked_by_csp (Optional[bool]): Allow unsafe evaluation
|
|
864
|
+
(Runtime.evaluate).
|
|
865
|
+
unique_context_id (Optional[str]): Unique context ID for evaluation
|
|
866
|
+
(Runtime.evaluate).
|
|
867
|
+
serialization_options (Optional[SerializationOptions]): Serialization options for
|
|
868
|
+
the result (Runtime.evaluate).
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Union[EvaluateResponse, CallFunctionOnResponse]: The result of the script execution.
|
|
872
|
+
|
|
873
|
+
Raises:
|
|
874
|
+
InvalidScriptWithElement: If script uses 'argument' keyword but no element is provided.
|
|
768
875
|
|
|
769
876
|
Examples:
|
|
877
|
+
# Execute a simple script to log a message
|
|
878
|
+
await page.execute_script('console.log("Hello World")')
|
|
879
|
+
|
|
880
|
+
# Execute a script that returns the page title
|
|
881
|
+
await page.execute_script('return document.title')
|
|
882
|
+
|
|
883
|
+
# Execute a script on an element to click it
|
|
770
884
|
await page.execute_script('argument.click()', element)
|
|
771
|
-
await page.execute_script('argument.value = "Hello"', element)
|
|
772
885
|
|
|
773
|
-
|
|
774
|
-
|
|
886
|
+
# Execute a script on an element to set its value
|
|
887
|
+
await page.execute_script('argument.value = "Hello"', element)
|
|
775
888
|
"""
|
|
776
|
-
if 'argument' in script and element is None:
|
|
777
|
-
raise InvalidScriptWithElement('Script contains "argument" but no element was provided')
|
|
778
|
-
|
|
779
889
|
logger.debug(f'Executing script: with_element={bool(element)}, length={len(script)}')
|
|
780
|
-
if element:
|
|
781
|
-
|
|
782
|
-
|
|
890
|
+
if element is not None:
|
|
891
|
+
warnings.warn(
|
|
892
|
+
'Passing a WebElement to Tab.execute_script() is deprecated. '
|
|
893
|
+
'Use WebElement.execute_script() instead.',
|
|
894
|
+
DeprecationWarning,
|
|
895
|
+
stacklevel=2,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
return await element.execute_script(
|
|
899
|
+
script,
|
|
900
|
+
arguments=arguments,
|
|
901
|
+
silent=silent,
|
|
902
|
+
return_by_value=return_by_value,
|
|
903
|
+
generate_preview=generate_preview,
|
|
904
|
+
user_gesture=user_gesture,
|
|
905
|
+
await_promise=await_promise,
|
|
906
|
+
execution_context_id=execution_context_id,
|
|
907
|
+
object_group=object_group,
|
|
908
|
+
throw_on_side_effect=throw_on_side_effect,
|
|
909
|
+
unique_context_id=unique_context_id,
|
|
910
|
+
serialization_options=serialization_options,
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
if has_return_outside_function(script):
|
|
914
|
+
script = f'(function(){{ {script} }})()'
|
|
915
|
+
|
|
916
|
+
command = self._get_evaluate_command(
|
|
917
|
+
script,
|
|
918
|
+
object_group=object_group,
|
|
919
|
+
include_command_line_api=include_command_line_api,
|
|
920
|
+
silent=silent,
|
|
921
|
+
context_id=context_id,
|
|
922
|
+
return_by_value=return_by_value,
|
|
923
|
+
generate_preview=generate_preview,
|
|
924
|
+
user_gesture=user_gesture,
|
|
925
|
+
await_promise=await_promise,
|
|
926
|
+
throw_on_side_effect=throw_on_side_effect,
|
|
927
|
+
timeout=timeout,
|
|
928
|
+
disable_breaks=disable_breaks,
|
|
929
|
+
repl_mode=repl_mode,
|
|
930
|
+
allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp,
|
|
931
|
+
unique_context_id=unique_context_id,
|
|
932
|
+
serialization_options=serialization_options,
|
|
933
|
+
)
|
|
934
|
+
logger.debug(f'Executing script without element: length={len(script)}')
|
|
935
|
+
result: Union[EvaluateResponse, CallFunctionOnResponse] = await self._execute_command(
|
|
936
|
+
command
|
|
937
|
+
)
|
|
938
|
+
self._validate_argument_error(result)
|
|
939
|
+
return result
|
|
783
940
|
|
|
784
941
|
# TODO: think about how to remove these duplications with the base class
|
|
785
942
|
async def continue_request(
|
|
@@ -1155,46 +1312,45 @@ class Tab(FindElementsMixin):
|
|
|
1155
1312
|
)
|
|
1156
1313
|
return ConnectionHandler(self._connection_port, self._target_id)
|
|
1157
1314
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1315
|
+
@staticmethod
|
|
1316
|
+
def _get_evaluate_command(
|
|
1317
|
+
script: str,
|
|
1318
|
+
*,
|
|
1319
|
+
object_group: Optional[str] = None,
|
|
1320
|
+
include_command_line_api: Optional[bool] = None,
|
|
1321
|
+
silent: Optional[bool] = None,
|
|
1322
|
+
context_id: Optional[int] = None,
|
|
1323
|
+
return_by_value: Optional[bool] = None,
|
|
1324
|
+
generate_preview: Optional[bool] = None,
|
|
1325
|
+
user_gesture: Optional[bool] = None,
|
|
1326
|
+
await_promise: Optional[bool] = None,
|
|
1327
|
+
throw_on_side_effect: Optional[bool] = None,
|
|
1328
|
+
timeout: Optional[float] = None,
|
|
1329
|
+
disable_breaks: Optional[bool] = None,
|
|
1330
|
+
repl_mode: Optional[bool] = None,
|
|
1331
|
+
allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,
|
|
1332
|
+
unique_context_id: Optional[str] = None,
|
|
1333
|
+
serialization_options: Optional[SerializationOptions] = None,
|
|
1334
|
+
):
|
|
1335
|
+
"""Create an evaluate command with the given parameters."""
|
|
1336
|
+
return RuntimeCommands.evaluate(
|
|
1337
|
+
expression=script,
|
|
1338
|
+
object_group=object_group,
|
|
1339
|
+
include_command_line_api=include_command_line_api,
|
|
1340
|
+
silent=silent,
|
|
1341
|
+
context_id=context_id,
|
|
1342
|
+
return_by_value=return_by_value,
|
|
1343
|
+
generate_preview=generate_preview,
|
|
1344
|
+
user_gesture=user_gesture,
|
|
1345
|
+
await_promise=await_promise,
|
|
1346
|
+
throw_on_side_effect=throw_on_side_effect,
|
|
1347
|
+
timeout=timeout,
|
|
1348
|
+
disable_breaks=disable_breaks,
|
|
1349
|
+
repl_mode=repl_mode,
|
|
1350
|
+
allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp,
|
|
1351
|
+
unique_context_id=unique_context_id,
|
|
1352
|
+
serialization_options=serialization_options,
|
|
1179
1353
|
)
|
|
1180
|
-
return await self._execute_command(command)
|
|
1181
|
-
|
|
1182
|
-
async def _execute_script_without_element(self, script: str):
|
|
1183
|
-
"""
|
|
1184
|
-
Execute script without element context.
|
|
1185
|
-
|
|
1186
|
-
Args:
|
|
1187
|
-
script: JavaScript code to execute.
|
|
1188
|
-
|
|
1189
|
-
Returns:
|
|
1190
|
-
The result of the script execution.
|
|
1191
|
-
"""
|
|
1192
|
-
if has_return_outside_function(script):
|
|
1193
|
-
script = f'(function(){{ {script} }})()'
|
|
1194
|
-
|
|
1195
|
-
command = RuntimeCommands.evaluate(expression=script)
|
|
1196
|
-
logger.debug(f'Executing script without element: length={len(script)}')
|
|
1197
|
-
return await self._execute_command(command)
|
|
1198
1354
|
|
|
1199
1355
|
async def _refresh_if_url_not_changed(self, url: str) -> bool:
|
|
1200
1356
|
"""Refresh page if URL hasn't changed."""
|
|
@@ -1204,6 +1360,33 @@ class Tab(FindElementsMixin):
|
|
|
1204
1360
|
return True
|
|
1205
1361
|
return False
|
|
1206
1362
|
|
|
1363
|
+
@staticmethod
|
|
1364
|
+
def _validate_argument_error(response: EvaluateResponse) -> None:
|
|
1365
|
+
"""
|
|
1366
|
+
Validate that script didn't fail with ReferenceError about 'argument' being undefined.
|
|
1367
|
+
|
|
1368
|
+
Raises:
|
|
1369
|
+
InvalidScriptWithElement: If script uses 'argument' keyword but no element was provided.
|
|
1370
|
+
"""
|
|
1371
|
+
evaluate_result = response.get('result')
|
|
1372
|
+
if not isinstance(evaluate_result, dict):
|
|
1373
|
+
return
|
|
1374
|
+
|
|
1375
|
+
remote_object = evaluate_result.get('result')
|
|
1376
|
+
if not isinstance(remote_object, dict):
|
|
1377
|
+
return
|
|
1378
|
+
|
|
1379
|
+
if not (
|
|
1380
|
+
remote_object.get('type') == 'object'
|
|
1381
|
+
and remote_object.get('subtype') == 'error'
|
|
1382
|
+
and remote_object.get('className') == 'ReferenceError'
|
|
1383
|
+
):
|
|
1384
|
+
return
|
|
1385
|
+
|
|
1386
|
+
description = remote_object.get('description', '')
|
|
1387
|
+
if 'argument is not defined' in description:
|
|
1388
|
+
raise InvalidScriptWithElement('Script contains "argument" but no element was provided')
|
|
1389
|
+
|
|
1207
1390
|
async def _wait_page_load(self, timeout: int = 300):
|
|
1208
1391
|
"""
|
|
1209
1392
|
Wait for page to finish loading.
|
|
@@ -1240,7 +1423,7 @@ class Tab(FindElementsMixin):
|
|
|
1240
1423
|
element = cast('WebElement', element)
|
|
1241
1424
|
if element:
|
|
1242
1425
|
# adjust the external div size to shadow root width (usually 300px)
|
|
1243
|
-
await
|
|
1426
|
+
await element.execute_script('this.style="width: 300px"')
|
|
1244
1427
|
await asyncio.sleep(time_before_click)
|
|
1245
1428
|
await element.click()
|
|
1246
1429
|
except Exception as exc:
|