pydoll-python 2.11.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.
Files changed (90) hide show
  1. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/PKG-INFO +54 -2
  2. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/README.md +52 -0
  3. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/tab.py +225 -55
  4. pydoll_python-2.12.0/pydoll/decorators.py +140 -0
  5. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/elements/web_element.py +73 -15
  6. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pyproject.toml +2 -2
  7. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/LICENSE +0 -0
  8. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/__init__.py +0 -0
  9. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/__init__.py +0 -0
  10. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/__init__.py +0 -0
  11. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/base.py +0 -0
  12. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/chrome.py +0 -0
  13. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/chromium/edge.py +0 -0
  14. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/interfaces.py +0 -0
  15. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/managers/__init__.py +0 -0
  16. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
  17. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
  18. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/managers/proxy_manager.py +0 -0
  19. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
  20. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/options.py +0 -0
  21. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/requests/__init__.py +0 -0
  22. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/requests/request.py +0 -0
  23. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/browser/requests/response.py +0 -0
  24. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/__init__.py +0 -0
  25. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/browser_commands.py +0 -0
  26. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/dom_commands.py +0 -0
  27. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/fetch_commands.py +0 -0
  28. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/input_commands.py +0 -0
  29. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/network_commands.py +0 -0
  30. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/page_commands.py +0 -0
  31. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/runtime_commands.py +0 -0
  32. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/storage_commands.py +0 -0
  33. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/commands/target_commands.py +0 -0
  34. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/connection/__init__.py +0 -0
  35. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/connection/connection_handler.py +0 -0
  36. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/connection/managers/__init__.py +0 -0
  37. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/connection/managers/commands_manager.py +0 -0
  38. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/connection/managers/events_manager.py +0 -0
  39. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/constants.py +0 -0
  40. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/elements/__init__.py +0 -0
  41. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/elements/mixins/__init__.py +0 -0
  42. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/elements/mixins/find_elements_mixin.py +0 -0
  43. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/exceptions.py +0 -0
  44. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/interactions/__init__.py +0 -0
  45. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/interactions/keyboard.py +0 -0
  46. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/interactions/scroll.py +0 -0
  47. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/__init__.py +0 -0
  48. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/base.py +0 -0
  49. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/__init__.py +0 -0
  50. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/events.py +0 -0
  51. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/methods.py +0 -0
  52. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/browser/types.py +0 -0
  53. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/debugger/types.py +0 -0
  54. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/__init__.py +0 -0
  55. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/events.py +0 -0
  56. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/methods.py +0 -0
  57. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/dom/types.py +0 -0
  58. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/emulation/types.py +0 -0
  59. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/__init__.py +0 -0
  60. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/events.py +0 -0
  61. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/methods.py +0 -0
  62. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/fetch/types.py +0 -0
  63. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/input/__init__.py +0 -0
  64. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/input/events.py +0 -0
  65. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/input/methods.py +0 -0
  66. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/input/types.py +0 -0
  67. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/io/types.py +0 -0
  68. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/network/__init__.py +0 -0
  69. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/network/events.py +0 -0
  70. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/network/methods.py +0 -0
  71. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/network/types.py +0 -0
  72. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/page/__init__.py +0 -0
  73. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/page/events.py +0 -0
  74. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/page/methods.py +0 -0
  75. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/page/types.py +0 -0
  76. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/__init__.py +0 -0
  77. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/events.py +0 -0
  78. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/methods.py +0 -0
  79. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/runtime/types.py +0 -0
  80. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/security/types.py +0 -0
  81. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/__init__.py +0 -0
  82. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/events.py +0 -0
  83. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/methods.py +0 -0
  84. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/storage/types.py +0 -0
  85. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/target/__init__.py +0 -0
  86. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/target/events.py +0 -0
  87. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/target/methods.py +0 -0
  88. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/protocol/target/types.py +0 -0
  89. {pydoll_python-2.11.0 → pydoll_python-2.12.0}/pydoll/py.typed +0 -0
  90. {pydoll_python-2.11.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.11.0
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 (>=23.2.1,<24.0.0)
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)
@@ -112,6 +112,58 @@ await tab.keyboard.up(Key.SHIFT)
112
112
 
113
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
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
+
115
167
  ## 📦 Installation
116
168
 
117
169
  ```bash
@@ -92,6 +92,58 @@ await tab.keyboard.up(Key.SHIFT)
92
92
 
93
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
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
+
95
147
  ## 📦 Installation
96
148
 
97
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
@@ -54,10 +55,16 @@ 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:
@@ -762,37 +769,174 @@ class Tab(FindElementsMixin):
762
769
  )
763
770
 
764
771
  @overload
765
- async def execute_script(self, script: str) -> EvaluateResponse: ...
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: ...
766
792
 
767
793
  @overload
768
794
  async def execute_script(
769
- self, script: str, element: 'WebElement'
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,
770
810
  ) -> CallFunctionOnResponse: ...
771
811
 
772
812
  async def execute_script(
773
- self, script: str, element: Optional['WebElement'] = None
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,
774
834
  ) -> Union[EvaluateResponse, CallFunctionOnResponse]:
775
835
  """
776
836
  Execute JavaScript in page context.
777
837
 
778
838
  Args:
779
- script: JavaScript code to execute.
780
- element: Element context (use 'argument' in script to reference).
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.
781
875
 
782
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
783
884
  await page.execute_script('argument.click()', element)
784
- await page.execute_script('argument.value = "Hello"', element)
785
885
 
786
- Raises:
787
- InvalidScriptWithElement: If script contains 'argument' but no element is provided.
886
+ # Execute a script on an element to set its value
887
+ await page.execute_script('argument.value = "Hello"', element)
788
888
  """
789
- if 'argument' in script and element is None:
790
- raise InvalidScriptWithElement('Script contains "argument" but no element was provided')
791
-
792
889
  logger.debug(f'Executing script: with_element={bool(element)}, length={len(script)}')
793
- if element:
794
- return await self._execute_script_with_element(script, element)
795
- return await self._execute_script_without_element(script)
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
796
940
 
797
941
  # TODO: think about how to remove these duplications with the base class
798
942
  async def continue_request(
@@ -1168,46 +1312,45 @@ class Tab(FindElementsMixin):
1168
1312
  )
1169
1313
  return ConnectionHandler(self._connection_port, self._target_id)
1170
1314
 
1171
- async def _execute_script_with_element(self, script: str, element: 'WebElement'):
1172
- """
1173
- Execute script with element context.
1174
-
1175
- Args:
1176
- script: JavaScript code to execute.
1177
- element: Element context (use 'argument' in script to reference).
1178
-
1179
- Returns:
1180
- The result of the script execution.
1181
- """
1182
- if 'argument' not in script:
1183
- raise InvalidScriptWithElement('Script does not contain "argument"')
1184
-
1185
- script = script.replace('argument', 'this')
1186
-
1187
- if not is_script_already_function(script):
1188
- script = f'function(){{ {script} }}'
1189
-
1190
- command = RuntimeCommands.call_function_on(
1191
- object_id=element._object_id, function_declaration=script, return_by_value=True
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,
1192
1353
  )
1193
- return await self._execute_command(command)
1194
-
1195
- async def _execute_script_without_element(self, script: str):
1196
- """
1197
- Execute script without element context.
1198
-
1199
- Args:
1200
- script: JavaScript code to execute.
1201
-
1202
- Returns:
1203
- The result of the script execution.
1204
- """
1205
- if has_return_outside_function(script):
1206
- script = f'(function(){{ {script} }})()'
1207
-
1208
- command = RuntimeCommands.evaluate(expression=script)
1209
- logger.debug(f'Executing script without element: length={len(script)}')
1210
- return await self._execute_command(command)
1211
1354
 
1212
1355
  async def _refresh_if_url_not_changed(self, url: str) -> bool:
1213
1356
  """Refresh page if URL hasn't changed."""
@@ -1217,6 +1360,33 @@ class Tab(FindElementsMixin):
1217
1360
  return True
1218
1361
  return False
1219
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
+
1220
1390
  async def _wait_page_load(self, timeout: int = 300):
1221
1391
  """
1222
1392
  Wait for page to finish loading.
@@ -1253,7 +1423,7 @@ class Tab(FindElementsMixin):
1253
1423
  element = cast('WebElement', element)
1254
1424
  if element:
1255
1425
  # adjust the external div size to shadow root width (usually 300px)
1256
- await self.execute_script('argument.style="width: 300px"', element)
1426
+ await element.execute_script('this.style="width: 300px"')
1257
1427
  await asyncio.sleep(time_before_click)
1258
1428
  await element.click()
1259
1429
  except Exception as exc:
@@ -0,0 +1,140 @@
1
+ import asyncio
2
+ import logging
3
+ import traceback
4
+ from functools import wraps
5
+ from typing import Any, Callable, Coroutine, List, Optional, Type, TypeVar, Union
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ T = TypeVar('T')
10
+
11
+
12
+ class RetryConfig:
13
+ def __init__(
14
+ self,
15
+ max_retries: int = 5,
16
+ exceptions: Union[Type[Exception], List[Type[Exception]]] = Exception,
17
+ on_retry: Optional[Callable] = None,
18
+ delay: float = 0,
19
+ exponential_backoff: bool = False,
20
+ ):
21
+ self.max_retries = max_retries
22
+ self.exceptions = exceptions
23
+ self.on_retry = on_retry
24
+ self.delay = delay
25
+ self.exponential_backoff = exponential_backoff
26
+
27
+ def calculate_delay(self, attempt: int) -> float:
28
+ if not self.delay:
29
+ return 0
30
+ return self.delay * (2**attempt if self.exponential_backoff else 1)
31
+
32
+ async def call_callback(self, caller_instance: Any) -> None:
33
+ if not self.on_retry:
34
+ return
35
+
36
+ try:
37
+ await self.on_retry(caller_instance)
38
+ except TypeError as e:
39
+ error_msg = str(e)
40
+ if (
41
+ 'takes 1 positional argument but 2 were given' in error_msg
42
+ or 'takes 0 positional arguments but 1 was given' in error_msg
43
+ ):
44
+ try:
45
+ await self.on_retry()
46
+ return
47
+ except Exception as e_inner:
48
+ raise e_inner
49
+ raise e
50
+ except Exception as e:
51
+ raise e
52
+
53
+ async def handle_delay(self, attempt: int) -> None:
54
+ """
55
+ Wait for delay.
56
+
57
+ Args:
58
+ attempt (int): The current attempt number
59
+ """
60
+ wait_time = self.calculate_delay(attempt)
61
+ if wait_time:
62
+ await asyncio.sleep(wait_time)
63
+
64
+ def is_matching_exception(self, exc: Exception) -> bool:
65
+ if isinstance(self.exceptions, (list, tuple)):
66
+ return any(isinstance(exc, e) for e in self.exceptions)
67
+ return isinstance(exc, self.exceptions)
68
+
69
+
70
+ def retry(
71
+ max_retries: int = 5,
72
+ exceptions: Union[Type[Exception], List[Type[Exception]]] = Exception,
73
+ on_retry: Optional[Callable] = None,
74
+ delay: float = 0,
75
+ exponential_backoff: bool = False,
76
+ exception_to_raise: Optional[Exception] = None,
77
+ ):
78
+ """
79
+ Decorator to try to execute a function again in case of exception.
80
+ For greater control, it is a good practice to specify the exceptions that should be handled.
81
+
82
+ Args:
83
+ max_retries (int): Maximum number of attempts
84
+ exceptions (Union[Type[Exception], List[Type[Exception]]]): Exception types that should be
85
+ handled
86
+ on_retry (Optional[Callable], optional): Function called after each failed attempt
87
+ delay (float): Delay between attempts in seconds
88
+ exponential_backoff (bool): If True, increase the delay exponentially
89
+
90
+ Usage:
91
+ @retry_on_exception(
92
+ max_retries=3,
93
+ exceptions=[ValueError, TypeError],
94
+ delay=1
95
+ )
96
+ def my_function():
97
+ ...
98
+ """
99
+ config = RetryConfig(
100
+ max_retries=max_retries,
101
+ exceptions=exceptions,
102
+ on_retry=on_retry,
103
+ delay=delay,
104
+ exponential_backoff=exponential_backoff,
105
+ )
106
+
107
+ def decorator(
108
+ func: Callable[..., Coroutine[Any, Any, T]],
109
+ ) -> Callable[..., Coroutine[Any, Any, T]]:
110
+ @wraps(func)
111
+ async def wrapper(*args: Any, **kwargs: Any) -> T:
112
+ last_exception: Optional[Exception] = None
113
+ caller_instance = args[0] if args else None
114
+
115
+ for attempt in range(config.max_retries + 1):
116
+ try:
117
+ return await func(*args, **kwargs)
118
+ except Exception as exc:
119
+ logger.error(
120
+ f'Error trying to execute the function {func.__name__}: '
121
+ f'{traceback.format_exc()}'
122
+ )
123
+ if not config.is_matching_exception(exc):
124
+ raise exc
125
+
126
+ last_exception = exc
127
+
128
+ if attempt < config.max_retries:
129
+ await config.handle_delay(attempt + 1)
130
+ await config.call_callback(caller_instance)
131
+ continue
132
+
133
+ if last_exception is not None:
134
+ raise exception_to_raise or last_exception
135
+
136
+ raise RuntimeError('Unreachable: all retries exhausted without exception')
137
+
138
+ return wrapper
139
+
140
+ return decorator
@@ -37,10 +37,16 @@ from pydoll.protocol.input.types import (
37
37
  MouseEventType,
38
38
  )
39
39
  from pydoll.protocol.page.types import ScreenshotFormat, Viewport
40
+ from pydoll.protocol.runtime.methods import (
41
+ CallFunctionOnResponse,
42
+ GetPropertiesResponse,
43
+ SerializationOptions,
44
+ )
40
45
  from pydoll.protocol.runtime.types import CallArgument
41
46
  from pydoll.utils import (
42
47
  decode_base64_to_bytes,
43
48
  extract_text_from_html,
49
+ is_script_already_function,
44
50
  )
45
51
 
46
52
  if TYPE_CHECKING:
@@ -639,33 +645,85 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
639
645
  async def execute_script(
640
646
  self,
641
647
  script: str,
642
- return_by_value: bool = False,
648
+ *,
643
649
  arguments: Optional[list[CallArgument]] = None,
644
- ):
650
+ silent: Optional[bool] = None,
651
+ return_by_value: Optional[bool] = None,
652
+ generate_preview: Optional[bool] = None,
653
+ user_gesture: Optional[bool] = None,
654
+ await_promise: Optional[bool] = None,
655
+ execution_context_id: Optional[int] = None,
656
+ object_group: Optional[str] = None,
657
+ throw_on_side_effect: Optional[bool] = None,
658
+ unique_context_id: Optional[str] = None,
659
+ serialization_options: Optional[SerializationOptions] = None,
660
+ ) -> CallFunctionOnResponse:
645
661
  """
646
662
  Execute JavaScript in element context.
647
663
 
648
664
  Args:
649
- script: JavaScript function to execute.
650
- return_by_value: Whether to return result by value.
651
- arguments: Optional list of arguments to pass to the function.
665
+ script (str): JavaScript code to execute. Use 'this' to reference this element.
666
+ arguments (Optional[list[CallArgument]]): Arguments to pass to the function
667
+ (Runtime.callFunctionOn).
668
+ silent (Optional[bool]): Whether to silence exceptions (Runtime.callFunctionOn).
669
+ return_by_value (Optional[bool]): Whether to return the result by value instead of
670
+ reference (Runtime.callFunctionOn).
671
+ generate_preview (Optional[bool]): Whether to generate a preview for the result
672
+ (Runtime.callFunctionOn).
673
+ user_gesture (Optional[bool]): Whether to treat the call as initiated by user
674
+ gesture (Runtime.callFunctionOn).
675
+ await_promise (Optional[bool]): Whether to await promise result
676
+ (Runtime.callFunctionOn).
677
+ execution_context_id (Optional[int]): ID of the execution context to call the
678
+ function in (Runtime.callFunctionOn).
679
+ object_group (Optional[str]): Symbolic group name for the result
680
+ (Runtime.callFunctionOn).
681
+ throw_on_side_effect (Optional[bool]): Whether to throw if side effect cannot be
682
+ ruled out (Runtime.callFunctionOn).
683
+ unique_context_id (Optional[str]): Unique context ID for the function call
684
+ (Runtime.callFunctionOn).
685
+ serialization_options (Optional[SerializationOptions]): Serialization options for
686
+ the result (Runtime.callFunctionOn).
652
687
 
653
- Note:
654
- Element is available as 'this' within the script.
655
- Arguments are accessible via 'arguments' array in JavaScript.
688
+ Returns:
689
+ CallFunctionOnResponse: The result of the script execution.
690
+
691
+ Examples:
692
+ # Click the element
693
+ await element.execute_script('this.click()')
694
+
695
+ # Modify element style
696
+ await element.execute_script('this.style.border = "2px solid red"')
697
+
698
+ # Get element text
699
+ result = await element.execute_script('return this.textContent', return_by_value=True)
700
+
701
+ # Set element content
702
+ await element.execute_script('this.textContent = "Hello World"')
656
703
  """
704
+ if not is_script_already_function(script):
705
+ script = f'function(){{ {script} }}'
706
+
657
707
  logger.debug(
658
708
  f'Executing script on element: return_by_value={return_by_value}, '
659
709
  f'length={len(script)}, args={len(arguments) if arguments else 0}'
660
710
  )
661
- return await self._execute_command(
662
- RuntimeCommands.call_function_on(
663
- object_id=self._object_id,
664
- function_declaration=script,
665
- return_by_value=return_by_value,
666
- arguments=arguments,
667
- )
711
+ command = RuntimeCommands.call_function_on(
712
+ function_declaration=script,
713
+ object_id=self._object_id,
714
+ arguments=arguments,
715
+ silent=silent,
716
+ return_by_value=return_by_value,
717
+ generate_preview=generate_preview,
718
+ user_gesture=user_gesture,
719
+ await_promise=await_promise,
720
+ execution_context_id=execution_context_id,
721
+ object_group=object_group,
722
+ throw_on_side_effect=throw_on_side_effect,
723
+ unique_context_id=unique_context_id,
724
+ serialization_options=serialization_options,
668
725
  )
726
+ return await self._execute_command(command)
669
727
 
670
728
  async def _get_family_elements(
671
729
  self, script: str, max_depth: int = 1, tag_filter: list[str] = []
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pydoll-python"
3
- version = "2.11.0"
3
+ version = "2.12.0"
4
4
  description = "Pydoll is a library for automating chromium-based browsers without a WebDriver, offering realistic interactions."
5
5
  authors = ["Thalison Fernandes <thalissfernandes99@gmail.com>"]
6
6
  readme = "README.md"
@@ -13,7 +13,7 @@ include = ["pydoll/py.typed"]
13
13
  python = "^3.10"
14
14
  websockets = "^14"
15
15
  aiohttp = "^3.9.5"
16
- aiofiles = "^23.2.1"
16
+ aiofiles = "^25.1.0"
17
17
  typing_extensions = "^4.14.0"
18
18
 
19
19
 
File without changes