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.
Files changed (96) hide show
  1. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/PKG-INFO +6 -13
  2. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/README.md +5 -12
  3. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/tab.py +120 -19
  4. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/constants.py +60 -0
  5. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/web_element.py +1 -0
  6. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/exceptions.py +18 -0
  7. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/types.py +136 -0
  8. pydoll_python-2.1.0/pydoll/utils.py +166 -0
  9. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pyproject.toml +1 -1
  10. pydoll_python-2.0.1/pydoll/utils.py +0 -72
  11. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/LICENSE +0 -0
  12. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/__init__.py +0 -0
  13. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/__init__.py +0 -0
  14. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/__init__.py +0 -0
  15. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/base.py +0 -0
  16. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/chrome.py +0 -0
  17. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/chromium/edge.py +0 -0
  18. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/interfaces.py +0 -0
  19. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/__init__.py +0 -0
  20. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
  21. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
  22. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/proxy_manager.py +0 -0
  23. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
  24. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/browser/options.py +0 -0
  25. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/__init__.py +0 -0
  26. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/browser_commands.py +0 -0
  27. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/dom_commands.py +0 -0
  28. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/fetch_commands.py +0 -0
  29. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/input_commands.py +0 -0
  30. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/network_commands.py +0 -0
  31. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/page_commands.py +0 -0
  32. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/runtime_commands.py +0 -0
  33. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/storage_commands.py +0 -0
  34. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/commands/target_commands.py +0 -0
  35. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/__init__.py +0 -0
  36. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/connection_handler.py +0 -0
  37. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/__init__.py +0 -0
  38. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/commands_manager.py +0 -0
  39. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/connection/managers/events_manager.py +0 -0
  40. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/__init__.py +0 -0
  41. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/mixins/__init__.py +0 -0
  42. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/elements/mixins/find_elements_mixin.py +0 -0
  43. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/__init__.py +0 -0
  44. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/base.py +0 -0
  45. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/__init__.py +0 -0
  46. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/events.py +0 -0
  47. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/methods.py +0 -0
  48. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/params.py +0 -0
  49. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/responses.py +0 -0
  50. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/browser/types.py +0 -0
  51. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/__init__.py +0 -0
  52. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/events.py +0 -0
  53. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/methods.py +0 -0
  54. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/params.py +0 -0
  55. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/responses.py +0 -0
  56. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/dom/types.py +0 -0
  57. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/__init__.py +0 -0
  58. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/events.py +0 -0
  59. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/methods.py +0 -0
  60. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/params.py +0 -0
  61. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/responses.py +0 -0
  62. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/fetch/types.py +0 -0
  63. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/__init__.py +0 -0
  64. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/events.py +0 -0
  65. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/methods.py +0 -0
  66. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/params.py +0 -0
  67. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/input/types.py +0 -0
  68. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/__init__.py +0 -0
  69. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/events.py +0 -0
  70. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/methods.py +0 -0
  71. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/params.py +0 -0
  72. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/network/responses.py +0 -0
  73. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/__init__.py +0 -0
  74. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/events.py +0 -0
  75. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/methods.py +0 -0
  76. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/params.py +0 -0
  77. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/responses.py +0 -0
  78. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/page/types.py +0 -0
  79. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/__init__.py +0 -0
  80. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/events.py +0 -0
  81. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/methods.py +0 -0
  82. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/params.py +0 -0
  83. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/responses.py +0 -0
  84. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/runtime/types.py +0 -0
  85. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/__init__.py +0 -0
  86. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/events.py +0 -0
  87. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/methods.py +0 -0
  88. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/params.py +0 -0
  89. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/responses.py +0 -0
  90. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/storage/types.py +0 -0
  91. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/__init__.py +0 -0
  92. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/events.py +0 -0
  93. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/methods.py +0 -0
  94. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/params.py +0 -0
  95. {pydoll_python-2.0.1 → pydoll_python-2.1.0}/pydoll/protocol/target/responses.py +0 -0
  96. {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.1
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').type('user@example.com')
175
- await tab.find(id='password').type('password123')
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.type('automation')
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.type('verification-code')
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').type('user@example.com')
156
- await tab.find(id='password').type('password123')
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.type('automation')
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.type('verification-code')
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.types import Cookie, CookieParam
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 decode_base64_to_bytes
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 (<iframe> tag).
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
- async def execute_script(self, script: str, element: Optional[WebElement] = None):
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
- script = script.replace('argument', 'this')
529
- script = f'function(){{ {script} }}'
530
- object_id = element._object_id
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(self, event_name: str, callback: Callable[[dict], Any], temporary: bool = False):
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'
@@ -287,6 +287,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
287
287
 
288
288
  More realistic than insert_text() but slower.
289
289
  """
290
+ await self.click()
290
291
  for char in text:
291
292
  await self._execute_command(
292
293
  InputCommands.dispatch_key_event(
@@ -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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pydoll-python"
3
- version = "2.0.1"
3
+ version = "2.1.0"
4
4
  description = ""
5
5
  authors = ["Thalison Fernandes <thalissfernandes99@gmail.com>"]
6
6
  readme = "README.md"
@@ -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