pydoll-python 2.0.1__tar.gz → 2.1.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.0.1 → pydoll_python-2.1.0}/PKG-INFO +6 -13
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/README.md +5 -12
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/tab.py +120 -19
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/constants.py +60 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/web_element.py +1 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/exceptions.py +18 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/types.py +136 -0
- pydoll_python-2.1.0/pydoll/utils.py +166 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pyproject.toml +1 -1
- pydoll_python-2.0.1/pydoll/utils.py +0 -72
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/LICENSE +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/base.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/chrome.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/edge.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/interfaces.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/proxy_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/options.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/browser_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/dom_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/fetch_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/input_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/network_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/page_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/runtime_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/storage_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/target_commands.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/connection_handler.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/commands_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/events_manager.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/mixins/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/mixins/find_elements_mixin.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/base.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/types.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/__init__.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/events.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/methods.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/params.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/responses.py +0 -0
- {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pydoll-python
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Thalison Fernandes
|
|
6
6
|
Author-email: thalissfernandes99@gmail.com
|
|
@@ -29,6 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg" alt="Ruff CI">
|
|
30
30
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg" alt="Release">
|
|
31
31
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg" alt="MyPy CI">
|
|
32
|
+
<a href="https://deepwiki.com/autoscrape-labs/pydoll"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
32
33
|
</p>
|
|
33
34
|
|
|
34
35
|
<p align="center">
|
|
@@ -171,8 +172,8 @@ async def advanced_captcha_bypass():
|
|
|
171
172
|
print("Cloudflare Turnstile automatically solved!")
|
|
172
173
|
|
|
173
174
|
# Continue with your automation - captcha is handled
|
|
174
|
-
await tab.find(id='username').
|
|
175
|
-
await tab.find(id='password').
|
|
175
|
+
await tab.find(id='username').type_text('user@example.com')
|
|
176
|
+
await tab.find(id='password').type_text('password123')
|
|
176
177
|
await tab.find(tag_name='button', text='Login').click()
|
|
177
178
|
|
|
178
179
|
# Method 2: Background processing (non-blocking)
|
|
@@ -192,14 +193,6 @@ async def advanced_captcha_bypass():
|
|
|
192
193
|
asyncio.run(advanced_captcha_bypass())
|
|
193
194
|
```
|
|
194
195
|
|
|
195
|
-
**Why This Matters:**
|
|
196
|
-
- **No External Dependencies**: No need for captcha solving services or API keys
|
|
197
|
-
- **Cost Effective**: Eliminate monthly captcha solving service fees
|
|
198
|
-
- **Reliable**: Works consistently without depending on third-party availability
|
|
199
|
-
- **Fast**: Instant solving without network delays to external services
|
|
200
|
-
- **Seamless Integration**: Captcha bypass happens transparently in your automation flow
|
|
201
|
-
|
|
202
|
-
|
|
203
196
|
### Advanced Element Finding
|
|
204
197
|
|
|
205
198
|
Pydoll offers multiple intuitive ways to find elements. No matter how you prefer to work, we have an approach that makes sense for you:
|
|
@@ -307,7 +300,7 @@ async def event_driven_automation():
|
|
|
307
300
|
print("Page loaded! Starting automation...")
|
|
308
301
|
# Perform actions after page loads
|
|
309
302
|
search_box = await tab.find(id='search-box')
|
|
310
|
-
await search_box.
|
|
303
|
+
await search_box.type_text('automation')
|
|
311
304
|
|
|
312
305
|
# React to navigation
|
|
313
306
|
async def on_navigation(event):
|
|
@@ -348,7 +341,7 @@ async def iframe_interaction():
|
|
|
348
341
|
|
|
349
342
|
# You can use all Tab methods on the frame
|
|
350
343
|
form_input = await frame.find(id='captcha-input')
|
|
351
|
-
await form_input.
|
|
344
|
+
await form_input.type_text('verification-code')
|
|
352
345
|
|
|
353
346
|
# Find elements by various methods
|
|
354
347
|
links = await frame.find(tag_name='a', find_all=True)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/ruff-ci.yml/badge.svg" alt="Ruff CI">
|
|
11
11
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/release.yml/badge.svg" alt="Release">
|
|
12
12
|
<img src="https://github.com/thalissonvs/pydoll/actions/workflows/mypy.yml/badge.svg" alt="MyPy CI">
|
|
13
|
+
<a href="https://deepwiki.com/autoscrape-labs/pydoll"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
<p align="center">
|
|
@@ -152,8 +153,8 @@ async def advanced_captcha_bypass():
|
|
|
152
153
|
print("Cloudflare Turnstile automatically solved!")
|
|
153
154
|
|
|
154
155
|
# Continue with your automation - captcha is handled
|
|
155
|
-
await tab.find(id='username').
|
|
156
|
-
await tab.find(id='password').
|
|
156
|
+
await tab.find(id='username').type_text('user@example.com')
|
|
157
|
+
await tab.find(id='password').type_text('password123')
|
|
157
158
|
await tab.find(tag_name='button', text='Login').click()
|
|
158
159
|
|
|
159
160
|
# Method 2: Background processing (non-blocking)
|
|
@@ -173,14 +174,6 @@ async def advanced_captcha_bypass():
|
|
|
173
174
|
asyncio.run(advanced_captcha_bypass())
|
|
174
175
|
```
|
|
175
176
|
|
|
176
|
-
**Why This Matters:**
|
|
177
|
-
- **No External Dependencies**: No need for captcha solving services or API keys
|
|
178
|
-
- **Cost Effective**: Eliminate monthly captcha solving service fees
|
|
179
|
-
- **Reliable**: Works consistently without depending on third-party availability
|
|
180
|
-
- **Fast**: Instant solving without network delays to external services
|
|
181
|
-
- **Seamless Integration**: Captcha bypass happens transparently in your automation flow
|
|
182
|
-
|
|
183
|
-
|
|
184
177
|
### Advanced Element Finding
|
|
185
178
|
|
|
186
179
|
Pydoll offers multiple intuitive ways to find elements. No matter how you prefer to work, we have an approach that makes sense for you:
|
|
@@ -288,7 +281,7 @@ async def event_driven_automation():
|
|
|
288
281
|
print("Page loaded! Starting automation...")
|
|
289
282
|
# Perform actions after page loads
|
|
290
283
|
search_box = await tab.find(id='search-box')
|
|
291
|
-
await search_box.
|
|
284
|
+
await search_box.type_text('automation')
|
|
292
285
|
|
|
293
286
|
# React to navigation
|
|
294
287
|
async def on_navigation(event):
|
|
@@ -329,7 +322,7 @@ async def iframe_interaction():
|
|
|
329
322
|
|
|
330
323
|
# You can use all Tab methods on the frame
|
|
331
324
|
form_input = await frame.find(id='captcha-input')
|
|
332
|
-
await form_input.
|
|
325
|
+
await form_input.type_text('verification-code')
|
|
333
326
|
|
|
334
327
|
# Find elements by various methods
|
|
335
328
|
links = await frame.find(tag_name='a', find_all=True)
|
|
@@ -12,6 +12,7 @@ from typing import (
|
|
|
12
12
|
TypeAlias,
|
|
13
13
|
Union,
|
|
14
14
|
cast,
|
|
15
|
+
overload,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
import aiofiles
|
|
@@ -32,18 +33,25 @@ from pydoll.exceptions import (
|
|
|
32
33
|
IFrameNotFound,
|
|
33
34
|
InvalidFileExtension,
|
|
34
35
|
InvalidIFrame,
|
|
36
|
+
InvalidScriptWithElement,
|
|
37
|
+
NetworkEventsNotEnabled,
|
|
35
38
|
NoDialogPresent,
|
|
36
39
|
NotAnIFrame,
|
|
37
40
|
PageLoadTimeout,
|
|
38
41
|
WaitElementTimeout,
|
|
39
42
|
)
|
|
40
43
|
from pydoll.protocol.base import Response
|
|
41
|
-
from pydoll.protocol.network.
|
|
44
|
+
from pydoll.protocol.network.responses import GetResponseBodyResponse
|
|
45
|
+
from pydoll.protocol.network.types import Cookie, CookieParam, NetworkLog
|
|
42
46
|
from pydoll.protocol.page.events import PageEvent
|
|
43
47
|
from pydoll.protocol.page.responses import CaptureScreenshotResponse, PrintToPDFResponse
|
|
44
|
-
from pydoll.protocol.runtime.responses import EvaluateResponse
|
|
48
|
+
from pydoll.protocol.runtime.responses import CallFunctionOnResponse, EvaluateResponse
|
|
45
49
|
from pydoll.protocol.storage.responses import GetCookiesResponse
|
|
46
|
-
from pydoll.utils import
|
|
50
|
+
from pydoll.utils import (
|
|
51
|
+
decode_base64_to_bytes,
|
|
52
|
+
has_return_outside_function,
|
|
53
|
+
is_script_already_function,
|
|
54
|
+
)
|
|
47
55
|
|
|
48
56
|
if TYPE_CHECKING:
|
|
49
57
|
from pydoll.browser.chromium.base import Browser
|
|
@@ -212,9 +220,6 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
212
220
|
custom_selector: Custom captcha selector (default: cf-turnstile class).
|
|
213
221
|
time_before_click: Delay before clicking captcha (default 2s).
|
|
214
222
|
time_to_wait_captcha: Timeout for captcha detection (default 5s).
|
|
215
|
-
|
|
216
|
-
Returns:
|
|
217
|
-
Callback ID for disabling auto-solver.
|
|
218
223
|
"""
|
|
219
224
|
if not self.page_events_enabled:
|
|
220
225
|
await self.enable_page_events()
|
|
@@ -285,7 +290,7 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
285
290
|
Get Tab object for interacting with iframe content.
|
|
286
291
|
|
|
287
292
|
Args:
|
|
288
|
-
frame: Tab representing the iframe
|
|
293
|
+
frame: Tab representing the iframe tag.
|
|
289
294
|
|
|
290
295
|
Returns:
|
|
291
296
|
Tab instance configured for iframe interaction.
|
|
@@ -295,7 +300,6 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
295
300
|
InvalidIFrame: If iframe lacks valid src attribute.
|
|
296
301
|
IFrameNotFound: If iframe target not found in browser.
|
|
297
302
|
"""
|
|
298
|
-
print('frame.tag_name: ', frame.tag_name)
|
|
299
303
|
if not frame.tag_name == 'iframe':
|
|
300
304
|
raise NotAnIFrame
|
|
301
305
|
|
|
@@ -317,6 +321,50 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
317
321
|
)
|
|
318
322
|
return response['result']['cookies']
|
|
319
323
|
|
|
324
|
+
async def get_network_response_body(self, request_id: str) -> str:
|
|
325
|
+
"""
|
|
326
|
+
Get the response body for a given request ID.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
request_id: Request ID to get the response body for.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The response body for the given request ID.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
NetworkEventsNotEnabled: If network events are not enabled.
|
|
336
|
+
"""
|
|
337
|
+
if not self.network_events_enabled:
|
|
338
|
+
raise NetworkEventsNotEnabled('Network events must be enabled to get response body')
|
|
339
|
+
|
|
340
|
+
response: GetResponseBodyResponse = await self._execute_command(
|
|
341
|
+
NetworkCommands.get_response_body(request_id)
|
|
342
|
+
)
|
|
343
|
+
return response['result']['body']
|
|
344
|
+
|
|
345
|
+
async def get_network_logs(self, filter: Optional[str] = None) -> list[NetworkLog]:
|
|
346
|
+
"""
|
|
347
|
+
Get network logs.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
filter: Filter to apply to the network logs.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The network logs.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
NetworkEventsNotEnabled: If network events are not enabled.
|
|
357
|
+
"""
|
|
358
|
+
if not self.network_events_enabled:
|
|
359
|
+
raise NetworkEventsNotEnabled('Network events must be enabled to get network logs')
|
|
360
|
+
|
|
361
|
+
logs = self._connection_handler.network_logs
|
|
362
|
+
if filter:
|
|
363
|
+
logs = [
|
|
364
|
+
log for log in logs if filter in log['params'].get('request', {}).get('url', '')
|
|
365
|
+
]
|
|
366
|
+
return logs
|
|
367
|
+
|
|
320
368
|
async def set_cookies(self, cookies: list[CookieParam]):
|
|
321
369
|
"""
|
|
322
370
|
Set multiple cookies for current page.
|
|
@@ -512,7 +560,15 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
512
560
|
PageCommands.handle_javascript_dialog(accept=accept, prompt_text=prompt_text)
|
|
513
561
|
)
|
|
514
562
|
|
|
515
|
-
|
|
563
|
+
@overload
|
|
564
|
+
async def execute_script(self, script: str) -> EvaluateResponse: ...
|
|
565
|
+
|
|
566
|
+
@overload
|
|
567
|
+
async def execute_script(self, script: str, element: WebElement) -> CallFunctionOnResponse: ...
|
|
568
|
+
|
|
569
|
+
async def execute_script(
|
|
570
|
+
self, script: str, element: Optional[WebElement] = None
|
|
571
|
+
) -> Union[EvaluateResponse, CallFunctionOnResponse]:
|
|
516
572
|
"""
|
|
517
573
|
Execute JavaScript in page context.
|
|
518
574
|
|
|
@@ -523,17 +579,17 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
523
579
|
Examples:
|
|
524
580
|
await page.execute_script('argument.click()', element)
|
|
525
581
|
await page.execute_script('argument.value = "Hello"', element)
|
|
582
|
+
|
|
583
|
+
Raises:
|
|
584
|
+
InvalidScriptWithElement: If script contains 'argument' but no element is provided.
|
|
526
585
|
"""
|
|
586
|
+
if 'argument' in script and element is None:
|
|
587
|
+
raise InvalidScriptWithElement('Script contains "argument" but no element was provided')
|
|
588
|
+
|
|
527
589
|
if element:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
command = RuntimeCommands.call_function_on(
|
|
532
|
-
object_id=object_id, function_declaration=script, return_by_value=True
|
|
533
|
-
)
|
|
534
|
-
else:
|
|
535
|
-
command = RuntimeCommands.evaluate(expression=script)
|
|
536
|
-
return await self._execute_command(command)
|
|
590
|
+
return await self._execute_script_with_element(script, element)
|
|
591
|
+
|
|
592
|
+
return await self._execute_script_without_element(script)
|
|
537
593
|
|
|
538
594
|
@asynccontextmanager
|
|
539
595
|
async def expect_file_chooser(
|
|
@@ -616,7 +672,12 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
616
672
|
if not _before_page_events_enabled:
|
|
617
673
|
await self.disable_page_events()
|
|
618
674
|
|
|
619
|
-
async def on(
|
|
675
|
+
async def on(
|
|
676
|
+
self,
|
|
677
|
+
event_name: str,
|
|
678
|
+
callback: Callable[[dict], Any],
|
|
679
|
+
temporary: bool = False,
|
|
680
|
+
) -> int:
|
|
620
681
|
"""
|
|
621
682
|
Register CDP event listener.
|
|
622
683
|
|
|
@@ -646,6 +707,46 @@ class Tab(FindElementsMixin): # noqa: PLR0904
|
|
|
646
707
|
event_name, function_to_register, temporary
|
|
647
708
|
)
|
|
648
709
|
|
|
710
|
+
async def _execute_script_with_element(self, script: str, element: WebElement):
|
|
711
|
+
"""
|
|
712
|
+
Execute script with element context.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
script: JavaScript code to execute.
|
|
716
|
+
element: Element context (use 'argument' in script to reference).
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
The result of the script execution.
|
|
720
|
+
"""
|
|
721
|
+
if 'argument' not in script:
|
|
722
|
+
raise InvalidScriptWithElement('Script does not contain "argument"')
|
|
723
|
+
|
|
724
|
+
script = script.replace('argument', 'this')
|
|
725
|
+
|
|
726
|
+
if not is_script_already_function(script):
|
|
727
|
+
script = f'function(){{ {script} }}'
|
|
728
|
+
|
|
729
|
+
command = RuntimeCommands.call_function_on(
|
|
730
|
+
object_id=element._object_id, function_declaration=script, return_by_value=True
|
|
731
|
+
)
|
|
732
|
+
return await self._execute_command(command)
|
|
733
|
+
|
|
734
|
+
async def _execute_script_without_element(self, script: str):
|
|
735
|
+
"""
|
|
736
|
+
Execute script without element context.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
script: JavaScript code to execute.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
The result of the script execution.
|
|
743
|
+
"""
|
|
744
|
+
if has_return_outside_function(script):
|
|
745
|
+
script = f'(function(){{ {script} }})()'
|
|
746
|
+
|
|
747
|
+
command = RuntimeCommands.evaluate(expression=script)
|
|
748
|
+
return await self._execute_command(command)
|
|
749
|
+
|
|
649
750
|
async def _refresh_if_url_not_changed(self, url: str) -> bool:
|
|
650
751
|
"""Refresh page if URL hasn't changed."""
|
|
651
752
|
current_url = await self.current_url
|
|
@@ -913,3 +913,63 @@ class DialogType(str, Enum):
|
|
|
913
913
|
CONFIRM = 'confirm'
|
|
914
914
|
PROMPT = 'prompt'
|
|
915
915
|
BEFORE_UNLOAD = 'beforeunload'
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
class InitiatorType(str, Enum):
|
|
919
|
+
PARSER = 'parser'
|
|
920
|
+
SCRIPT = 'script'
|
|
921
|
+
PRELOAD = 'preload'
|
|
922
|
+
SIGNED_EXCHANGE = 'SignedExchange'
|
|
923
|
+
PREFLIGHT = 'preflight'
|
|
924
|
+
OTHER = 'other'
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class NetworkServiceWorkerRouterSourceType(str, Enum):
|
|
928
|
+
"""Network service worker router source types."""
|
|
929
|
+
|
|
930
|
+
NETWORK = 'network'
|
|
931
|
+
CACHE = 'cache'
|
|
932
|
+
FETCH_EVENT = 'fetch-event'
|
|
933
|
+
RACE_NETWORK = 'race-network'
|
|
934
|
+
RACE_NETWORK_AND_FETCH_HANDLER = 'race-network-and-fetch-handler'
|
|
935
|
+
RACE_NETWORK_AND_CACHE = 'race-network-and-cache'
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class NetworkServiceWorkerResponseSource(str, Enum):
|
|
939
|
+
"""Network service worker response source types."""
|
|
940
|
+
|
|
941
|
+
CACHE_STORAGE = 'cache-storage'
|
|
942
|
+
HTTP_CACHE = 'http-cache'
|
|
943
|
+
FALLBACK_CODE = 'fallback-code'
|
|
944
|
+
NETWORK = 'network'
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
class AlternateProtocolUsage(str, Enum):
|
|
948
|
+
"""Alternate protocol usage types."""
|
|
949
|
+
|
|
950
|
+
ALTERNATIVE_JOB_WON_WITHOUT_RACE = 'alternativeJobWonWithoutRace'
|
|
951
|
+
ALTERNATIVE_JOB_WON_RACE = 'alternativeJobWonRace'
|
|
952
|
+
MAIN_JOB_WON_RACE = 'mainJobWonRace'
|
|
953
|
+
MAPPING_MISSING = 'mappingMissing'
|
|
954
|
+
BROKEN = 'broken'
|
|
955
|
+
DNS_ALPN_H3_JOB_WON_WITHOUT_RACE = 'dnsAlpnH3JobWonWithoutRace'
|
|
956
|
+
DNS_ALPN_H3_JOB_WON_RACE = 'dnsAlpnH3JobWonRace'
|
|
957
|
+
UNSPECIFIED_REASON = 'unspecifiedReason'
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class SecurityState(str, Enum):
|
|
961
|
+
"""Security state types."""
|
|
962
|
+
|
|
963
|
+
UNKNOWN = 'unknown'
|
|
964
|
+
NEUTRAL = 'neutral'
|
|
965
|
+
INSECURE = 'insecure'
|
|
966
|
+
INFO = 'info'
|
|
967
|
+
INSECURE_BROKEN = 'insecure-broken'
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
class CertificateTransparencyCompliance(str, Enum):
|
|
971
|
+
"""Certificate transparency compliance types."""
|
|
972
|
+
|
|
973
|
+
UNKNOWN = 'unknown'
|
|
974
|
+
NOT_COMPLIANT = 'not-compliant'
|
|
975
|
+
COMPLIANT = 'compliant'
|
|
@@ -233,3 +233,21 @@ class IFrameNotFound(PydollException):
|
|
|
233
233
|
"""Raised when an iframe is not found."""
|
|
234
234
|
|
|
235
235
|
message = 'The iframe was not found'
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class NetworkEventsNotEnabled(PydollException):
|
|
239
|
+
"""Raised when network events are not enabled."""
|
|
240
|
+
|
|
241
|
+
message = 'Network events not enabled'
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ScriptException(PydollException):
|
|
245
|
+
"""Base class for exceptions related to JavaScript execution."""
|
|
246
|
+
|
|
247
|
+
message = 'A script execution error occurred'
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class InvalidScriptWithElement(ScriptException):
|
|
251
|
+
"""Raised when a script contains 'argument' but no element is provided."""
|
|
252
|
+
|
|
253
|
+
message = 'Script contains "argument" but no element was provided'
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
from typing import NotRequired, TypedDict
|
|
2
2
|
|
|
3
3
|
from pydoll.constants import (
|
|
4
|
+
AlternateProtocolUsage,
|
|
5
|
+
CertificateTransparencyCompliance,
|
|
4
6
|
ContentSecurityPolicySource,
|
|
5
7
|
CookiePriority,
|
|
6
8
|
CookieSameSite,
|
|
7
9
|
CookieSourceScheme,
|
|
10
|
+
InitiatorType,
|
|
8
11
|
MixedContentType,
|
|
12
|
+
NetworkServiceWorkerResponseSource,
|
|
13
|
+
NetworkServiceWorkerRouterSourceType,
|
|
9
14
|
ReferrerPolicy,
|
|
10
15
|
RefreshPolicy,
|
|
11
16
|
ResourcePriority,
|
|
17
|
+
ResourceType,
|
|
18
|
+
SecurityState,
|
|
12
19
|
TrustTokenOperationType,
|
|
13
20
|
)
|
|
21
|
+
from pydoll.protocol.runtime.types import StackTrace
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
class SearchMatch(TypedDict):
|
|
@@ -150,3 +158,131 @@ class RequestPausedEventParams(TypedDict):
|
|
|
150
158
|
class RequestPausedEvent(TypedDict):
|
|
151
159
|
method: str
|
|
152
160
|
params: RequestPausedEventParams
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Initiator(TypedDict):
|
|
164
|
+
type: InitiatorType
|
|
165
|
+
stack: NotRequired[StackTrace]
|
|
166
|
+
url: NotRequired[str]
|
|
167
|
+
lineNumber: NotRequired[int]
|
|
168
|
+
columnNumber: NotRequired[int]
|
|
169
|
+
requestId: NotRequired[str]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ServiceWorkerRouterInfo(TypedDict):
|
|
173
|
+
"""Service worker router info object."""
|
|
174
|
+
|
|
175
|
+
ruleIdMatched: NotRequired[int]
|
|
176
|
+
matchedSourceType: NotRequired[NetworkServiceWorkerRouterSourceType]
|
|
177
|
+
actualSourceType: NotRequired[NetworkServiceWorkerRouterSourceType]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ResourceTiming(TypedDict):
|
|
181
|
+
"""Resource timing object."""
|
|
182
|
+
|
|
183
|
+
requestTime: float
|
|
184
|
+
proxyStart: float
|
|
185
|
+
proxyEnd: float
|
|
186
|
+
dnsStart: float
|
|
187
|
+
dnsEnd: float
|
|
188
|
+
connectStart: float
|
|
189
|
+
connectEnd: float
|
|
190
|
+
sslStart: float
|
|
191
|
+
sslEnd: float
|
|
192
|
+
workerStart: float
|
|
193
|
+
workerReady: float
|
|
194
|
+
workerFetchStart: float
|
|
195
|
+
workerRespondWithSettled: float
|
|
196
|
+
workerRouterEvaluationStart: NotRequired[float]
|
|
197
|
+
workerCacheLookupStart: NotRequired[float]
|
|
198
|
+
sendStart: float
|
|
199
|
+
sendEnd: float
|
|
200
|
+
pushStart: float
|
|
201
|
+
pushEnd: float
|
|
202
|
+
receiveHeadersStart: float
|
|
203
|
+
receiveHeadersEnd: float
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class SignedCertificateTimestamp(TypedDict):
|
|
207
|
+
"""Signed certificate timestamp object."""
|
|
208
|
+
|
|
209
|
+
status: str
|
|
210
|
+
origin: str
|
|
211
|
+
logDescription: str
|
|
212
|
+
logId: str
|
|
213
|
+
timestamp: float
|
|
214
|
+
hashAlgorithm: str
|
|
215
|
+
signatureAlgorithm: str
|
|
216
|
+
signatureData: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class SecurityDetails(TypedDict):
|
|
220
|
+
"""Security details object."""
|
|
221
|
+
|
|
222
|
+
protocol: str
|
|
223
|
+
keyExchange: str
|
|
224
|
+
keyExchangeGroup: NotRequired[str]
|
|
225
|
+
cipher: str
|
|
226
|
+
mac: NotRequired[str]
|
|
227
|
+
certificateId: int
|
|
228
|
+
subjectName: str
|
|
229
|
+
sanList: list[str]
|
|
230
|
+
issuer: str
|
|
231
|
+
validFrom: float
|
|
232
|
+
validTo: float
|
|
233
|
+
signedCertificateTimestampList: list[SignedCertificateTimestamp]
|
|
234
|
+
certificateTransparencyCompliance: CertificateTransparencyCompliance
|
|
235
|
+
serverSignatureAlgorithm: NotRequired[int]
|
|
236
|
+
encryptedClientHello: bool
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Response(TypedDict):
|
|
240
|
+
url: str
|
|
241
|
+
status: int
|
|
242
|
+
statusText: str
|
|
243
|
+
headers: list[dict]
|
|
244
|
+
headersText: NotRequired[str]
|
|
245
|
+
mimeType: str
|
|
246
|
+
charset: str
|
|
247
|
+
requestHeaders: NotRequired[list[dict]]
|
|
248
|
+
requestHeadersText: NotRequired[str]
|
|
249
|
+
connectionReused: bool
|
|
250
|
+
connectionId: float
|
|
251
|
+
remoteIPAddress: NotRequired[str]
|
|
252
|
+
remotePort: NotRequired[int]
|
|
253
|
+
fromDiskCache: NotRequired[bool]
|
|
254
|
+
fromServiceWorker: NotRequired[bool]
|
|
255
|
+
fromPrefetchCache: NotRequired[bool]
|
|
256
|
+
fromEarlyHints: NotRequired[bool]
|
|
257
|
+
serviceWorkerRouterInfo: NotRequired[ServiceWorkerRouterInfo]
|
|
258
|
+
encodedDataLength: float
|
|
259
|
+
timing: NotRequired[ResourceTiming]
|
|
260
|
+
serviceWorkerResponseSource: NotRequired[NetworkServiceWorkerResponseSource]
|
|
261
|
+
responseTime: NotRequired[float]
|
|
262
|
+
cacheStorageCacheName: NotRequired[str]
|
|
263
|
+
protocol: NotRequired[str]
|
|
264
|
+
alternateProtocolUsage: NotRequired[AlternateProtocolUsage]
|
|
265
|
+
securityState: SecurityState
|
|
266
|
+
securityDetails: NotRequired[SecurityDetails]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class NetworkLogParams(TypedDict):
|
|
270
|
+
requestId: str
|
|
271
|
+
loaderId: str
|
|
272
|
+
documentURL: str
|
|
273
|
+
request: Request
|
|
274
|
+
timestamp: float
|
|
275
|
+
wallTime: float
|
|
276
|
+
initiator: Initiator
|
|
277
|
+
redirectHasExtraInfo: bool
|
|
278
|
+
redirectResponse: NotRequired[Response]
|
|
279
|
+
type: NotRequired[ResourceType]
|
|
280
|
+
frameId: NotRequired[str]
|
|
281
|
+
hasUserGesture: NotRequired[bool]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class NetworkLog(TypedDict):
|
|
285
|
+
"""Network log object."""
|
|
286
|
+
|
|
287
|
+
method: str
|
|
288
|
+
params: RequestPausedEventParams
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from pydoll.exceptions import InvalidBrowserPath, InvalidResponse, NetworkError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def decode_base64_to_bytes(image: str) -> bytes:
|
|
14
|
+
"""
|
|
15
|
+
Decodes a base64 image string to bytes.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image (str): The base64 image string to decode.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
bytes: The decoded image as bytes.
|
|
22
|
+
"""
|
|
23
|
+
return base64.b64decode(image.encode('utf-8'))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_browser_ws_address(port: int) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Fetches the WebSocket address for the browser instance.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The WebSocket address for the browser.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
NetworkError: If the address cannot be fetched due to network errors
|
|
35
|
+
or missing data.
|
|
36
|
+
InvalidResponse: If the response is not valid JSON.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
async with aiohttp.ClientSession() as session:
|
|
40
|
+
async with session.get(f'http://localhost:{port}/json/version') as response:
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
data = await response.json()
|
|
43
|
+
return data['webSocketDebuggerUrl']
|
|
44
|
+
|
|
45
|
+
except aiohttp.ClientError as e:
|
|
46
|
+
raise NetworkError(f'Failed to get browser ws address: {e}')
|
|
47
|
+
|
|
48
|
+
except KeyError as e:
|
|
49
|
+
raise InvalidResponse(f'Failed to get browser ws address: {e}')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_browser_paths(paths: list[str]) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Validates potential browser executable paths and returns the first valid one.
|
|
55
|
+
|
|
56
|
+
Checks a list of possible browser binary locations to find an existing,
|
|
57
|
+
executable browser. This is used by browser-specific subclasses to locate
|
|
58
|
+
the browser executable when no explicit binary path is provided.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
paths: List of potential file paths to check for the browser executable.
|
|
62
|
+
These should be absolute paths appropriate for the current OS.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: The first valid browser executable path found.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
InvalidBrowserPath: If the browser executable is not found at the path.
|
|
69
|
+
"""
|
|
70
|
+
for path in paths:
|
|
71
|
+
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
72
|
+
return path
|
|
73
|
+
raise InvalidBrowserPath(f'No valid browser path found in: {paths}')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def clean_script_for_analysis(script: str) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Clean JavaScript code by removing comments and string literals.
|
|
79
|
+
|
|
80
|
+
This helps avoid false positives when analyzing script structure.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
script: JavaScript code to clean.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str: Cleaned script with comments and strings removed.
|
|
87
|
+
"""
|
|
88
|
+
# Remove line comments
|
|
89
|
+
cleaned = re.sub(r'//.*?$', '', script, flags=re.MULTILINE)
|
|
90
|
+
# Remove block comments
|
|
91
|
+
cleaned = re.sub(r'/\*.*?\*/', '', cleaned, flags=re.DOTALL)
|
|
92
|
+
# Remove double quoted strings
|
|
93
|
+
cleaned = re.sub(r'"[^"]*"', '""', cleaned)
|
|
94
|
+
# Remove single quoted strings
|
|
95
|
+
cleaned = re.sub(r"'[^']*'", "''", cleaned)
|
|
96
|
+
# Remove template literals
|
|
97
|
+
cleaned = re.sub(r'`[^`]*`', '``', cleaned)
|
|
98
|
+
|
|
99
|
+
return cleaned
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_script_already_function(script: str) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Check if a JavaScript script is already wrapped in a function.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
script: JavaScript code to analyze.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
bool: True if script is already a function, False otherwise.
|
|
111
|
+
"""
|
|
112
|
+
cleaned_script = clean_script_for_analysis(script)
|
|
113
|
+
|
|
114
|
+
function_pattern = r'^\s*function\s*\([^)]*\)\s*\{'
|
|
115
|
+
arrow_function_pattern = r'^\s*\([^)]*\)\s*=>\s*\{'
|
|
116
|
+
|
|
117
|
+
return bool(
|
|
118
|
+
re.match(function_pattern, cleaned_script.strip())
|
|
119
|
+
or re.match(arrow_function_pattern, cleaned_script.strip())
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def has_return_outside_function(script: str) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Check if a JavaScript script has return statements outside of functions.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
script: JavaScript code to analyze.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
bool: True if script has return outside function, False otherwise.
|
|
132
|
+
"""
|
|
133
|
+
cleaned_script = clean_script_for_analysis(script)
|
|
134
|
+
|
|
135
|
+
# If already a function, no need to check
|
|
136
|
+
if is_script_already_function(cleaned_script):
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# Look for 'return' statements
|
|
140
|
+
return_pattern = r'\breturn\b'
|
|
141
|
+
if not re.search(return_pattern, cleaned_script):
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
# Check if return is inside a function by counting braces
|
|
145
|
+
lines = cleaned_script.split('\n')
|
|
146
|
+
brace_count = 0
|
|
147
|
+
in_function = False
|
|
148
|
+
|
|
149
|
+
for line in lines:
|
|
150
|
+
# Check for function declarations
|
|
151
|
+
if re.search(r'\bfunction\b', line) or re.search(r'=>', line):
|
|
152
|
+
in_function = True
|
|
153
|
+
|
|
154
|
+
# Count braces
|
|
155
|
+
brace_count += line.count('{') - line.count('}')
|
|
156
|
+
|
|
157
|
+
# Check for return statement
|
|
158
|
+
if re.search(return_pattern, line):
|
|
159
|
+
if not in_function or brace_count <= 0:
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
# Reset function flag if we're back to top level
|
|
163
|
+
if brace_count <= 0:
|
|
164
|
+
in_function = False
|
|
165
|
+
|
|
166
|
+
return False
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
import aiohttp
|
|
6
|
-
|
|
7
|
-
from pydoll.exceptions import InvalidBrowserPath, InvalidResponse, NetworkError
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def decode_base64_to_bytes(image: str) -> bytes:
|
|
13
|
-
"""
|
|
14
|
-
Decodes a base64 image string to bytes.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
image (str): The base64 image string to decode.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
bytes: The decoded image as bytes.
|
|
21
|
-
"""
|
|
22
|
-
return base64.b64decode(image.encode('utf-8'))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async def get_browser_ws_address(port: int) -> str:
|
|
26
|
-
"""
|
|
27
|
-
Fetches the WebSocket address for the browser instance.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
str: The WebSocket address for the browser.
|
|
31
|
-
|
|
32
|
-
Raises:
|
|
33
|
-
NetworkError: If the address cannot be fetched due to network errors
|
|
34
|
-
or missing data.
|
|
35
|
-
InvalidResponse: If the response is not valid JSON.
|
|
36
|
-
"""
|
|
37
|
-
try:
|
|
38
|
-
async with aiohttp.ClientSession() as session:
|
|
39
|
-
async with session.get(f'http://localhost:{port}/json/version') as response:
|
|
40
|
-
response.raise_for_status()
|
|
41
|
-
data = await response.json()
|
|
42
|
-
return data['webSocketDebuggerUrl']
|
|
43
|
-
|
|
44
|
-
except aiohttp.ClientError as e:
|
|
45
|
-
raise NetworkError(f'Failed to get browser ws address: {e}')
|
|
46
|
-
|
|
47
|
-
except KeyError as e:
|
|
48
|
-
raise InvalidResponse(f'Failed to get browser ws address: {e}')
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def validate_browser_paths(paths: list[str]) -> str:
|
|
52
|
-
"""
|
|
53
|
-
Validates potential browser executable paths and returns the first valid one.
|
|
54
|
-
|
|
55
|
-
Checks a list of possible browser binary locations to find an existing,
|
|
56
|
-
executable browser. This is used by browser-specific subclasses to locate
|
|
57
|
-
the browser executable when no explicit binary path is provided.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
paths: List of potential file paths to check for the browser executable.
|
|
61
|
-
These should be absolute paths appropriate for the current OS.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
str: The first valid browser executable path found.
|
|
65
|
-
|
|
66
|
-
Raises:
|
|
67
|
-
InvalidBrowserPath: If the browser executable is not found at the path.
|
|
68
|
-
"""
|
|
69
|
-
for path in paths:
|
|
70
|
-
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
71
|
-
return path
|
|
72
|
-
raise InvalidBrowserPath(f'No valid browser path found in: {paths}')
|
|
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
|
{pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_options_manager.py
RENAMED
|
File without changes
|
{pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_process_manager.py
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|