pydoll-python 2.18.0__tar.gz → 2.19.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 (102) hide show
  1. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/PKG-INFO +46 -18
  2. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/README.md +45 -17
  3. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/chromium/base.py +50 -5
  4. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/options.py +15 -0
  5. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/tab.py +21 -3
  6. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/__init__.py +2 -0
  7. pydoll_python-2.19.0/pydoll/commands/emulation_commands.py +61 -0
  8. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/mixins/find_elements_mixin.py +16 -2
  9. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/web_element.py +31 -8
  10. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/interactions/__init__.py +4 -0
  11. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/interactions/keyboard.py +3 -3
  12. pydoll_python-2.19.0/pydoll/interactions/mouse.py +475 -0
  13. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/interactions/scroll.py +4 -76
  14. pydoll_python-2.19.0/pydoll/interactions/utils.py +167 -0
  15. pydoll_python-2.19.0/pydoll/protocol/emulation/__init__.py +1 -0
  16. pydoll_python-2.19.0/pydoll/protocol/emulation/methods.py +25 -0
  17. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/utils/__init__.py +2 -0
  18. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/utils/socks5_proxy_forwarder.py +165 -71
  19. pydoll_python-2.19.0/pydoll/utils/user_agent_parser.py +289 -0
  20. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pyproject.toml +1 -1
  21. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/LICENSE +0 -0
  22. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/__init__.py +0 -0
  23. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/__init__.py +0 -0
  24. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/chromium/__init__.py +0 -0
  25. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/chromium/chrome.py +0 -0
  26. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/chromium/edge.py +0 -0
  27. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/interfaces.py +0 -0
  28. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/managers/__init__.py +0 -0
  29. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
  30. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
  31. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/managers/proxy_manager.py +0 -0
  32. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
  33. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/requests/__init__.py +0 -0
  34. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/requests/request.py +0 -0
  35. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/browser/requests/response.py +0 -0
  36. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/browser_commands.py +0 -0
  37. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/dom_commands.py +0 -0
  38. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/fetch_commands.py +0 -0
  39. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/input_commands.py +0 -0
  40. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/network_commands.py +0 -0
  41. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/page_commands.py +0 -0
  42. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/runtime_commands.py +0 -0
  43. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/storage_commands.py +0 -0
  44. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/commands/target_commands.py +0 -0
  45. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/connection/__init__.py +0 -0
  46. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/connection/connection_handler.py +0 -0
  47. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/connection/managers/__init__.py +0 -0
  48. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/connection/managers/commands_manager.py +0 -0
  49. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/connection/managers/events_manager.py +0 -0
  50. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/constants.py +0 -0
  51. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/decorators.py +0 -0
  52. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/__init__.py +0 -0
  53. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/mixins/__init__.py +0 -0
  54. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/shadow_root.py +0 -0
  55. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/utils/__init__.py +0 -0
  56. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/elements/utils/selector_parser.py +0 -0
  57. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/exceptions.py +0 -0
  58. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/interactions/iframe.py +0 -0
  59. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/__init__.py +0 -0
  60. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/base.py +0 -0
  61. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/browser/__init__.py +0 -0
  62. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/browser/events.py +0 -0
  63. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/browser/methods.py +0 -0
  64. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/browser/types.py +0 -0
  65. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/debugger/types.py +0 -0
  66. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/dom/__init__.py +0 -0
  67. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/dom/events.py +0 -0
  68. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/dom/methods.py +0 -0
  69. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/dom/types.py +0 -0
  70. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/emulation/types.py +0 -0
  71. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/fetch/__init__.py +0 -0
  72. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/fetch/events.py +0 -0
  73. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/fetch/methods.py +0 -0
  74. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/fetch/types.py +0 -0
  75. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/input/__init__.py +0 -0
  76. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/input/events.py +0 -0
  77. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/input/methods.py +0 -0
  78. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/input/types.py +0 -0
  79. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/io/types.py +0 -0
  80. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/network/__init__.py +0 -0
  81. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/network/events.py +0 -0
  82. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/network/methods.py +0 -0
  83. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/network/types.py +0 -0
  84. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/page/__init__.py +0 -0
  85. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/page/events.py +0 -0
  86. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/page/methods.py +0 -0
  87. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/page/types.py +0 -0
  88. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/runtime/__init__.py +0 -0
  89. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/runtime/events.py +0 -0
  90. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/runtime/methods.py +0 -0
  91. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/runtime/types.py +0 -0
  92. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/security/types.py +0 -0
  93. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/storage/__init__.py +0 -0
  94. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/storage/events.py +0 -0
  95. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/storage/methods.py +0 -0
  96. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/storage/types.py +0 -0
  97. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/target/__init__.py +0 -0
  98. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/target/events.py +0 -0
  99. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/target/methods.py +0 -0
  100. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/protocol/target/types.py +0 -0
  101. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/py.typed +0 -0
  102. {pydoll_python-2.18.0 → pydoll_python-2.19.0}/pydoll/utils/general.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydoll-python
3
- Version: 2.18.0
3
+ Version: 2.19.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
@@ -125,29 +125,28 @@ for sr in shadow_roots:
125
125
  </details>
126
126
 
127
127
  <details>
128
- <summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
128
+ <summary><b>Humanized Keyboard Input</b></summary>
129
129
  <br>
130
130
 
131
- Pydoll now includes a **humanized typing engine** that simulates realistic human typing behavior:
131
+ Pydoll's typing engine simulates realistic human typing behavior out of the box:
132
132
 
133
133
  - **Variable keystroke timing**: 30-120ms between keys (not fixed intervals)
134
134
  - **Realistic typos**: ~2% error rate with automatic correction behavior
135
- - **No more `interval` parameter**: Just use `humanize=True` for anti-bot evasion
136
135
 
137
136
  ```python
138
- # Old way (detectable)
139
- await element.type_text("hello", interval=0.1)
137
+ # Realistic typing by default
138
+ await element.type_text("hello")
140
139
 
141
- # New way (human-like, anti-bot)
142
- await element.type_text("hello", humanize=True)
140
+ # Opt out when speed is critical
141
+ await element.type_text("hello", humanize=False)
143
142
  ```
144
143
  </details>
145
144
 
146
145
  <details>
147
- <summary><b>Humanized Scroll with Physics Engine (<code>humanize=True</code>)</b></summary>
146
+ <summary><b>Humanized Scroll with Physics Engine</b></summary>
148
147
  <br>
149
148
 
150
- The scroll API now features a **Cubic Bezier curve physics engine** for realistic scrolling:
149
+ The scroll API features a **Cubic Bezier curve physics engine** for realistic scrolling:
151
150
 
152
151
  - **Momentum & friction**: Natural acceleration and deceleration
153
152
  - **Micro-pauses**: Brief stops during long scrolls (simulates reading)
@@ -155,23 +154,52 @@ The scroll API now features a **Cubic Bezier curve physics engine** for realisti
155
154
  - **Overshoot correction**: Occasionally scrolls past target and corrects back
156
155
 
157
156
  ```python
158
- # Smooth scroll (CSS animation, predictable timing)
159
- await tab.scroll.by(ScrollPosition.DOWN, 500, smooth=True)
157
+ # Humanized by default (physics engine, anti-bot)
158
+ await tab.scroll.by(ScrollPosition.DOWN, 500)
159
+ await tab.scroll.to_bottom()
160
160
 
161
- # Humanized scroll (physics engine, anti-bot)
162
- await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=True)
163
- await tab.scroll.to_bottom(humanize=True)
161
+ # CSS smooth scroll (predictable timing)
162
+ await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=False, smooth=True)
164
163
  ```
165
164
 
166
165
  | Mode | Parameter | Use Case |
167
166
  |------|-----------|----------|
168
- | **Instant** | `smooth=False` | Speed-critical operations |
169
- | **Smooth** | `smooth=True` | General browsing simulation |
170
- | **Humanized** | `humanize=True` | **Anti-bot evasion** |
167
+ | **Humanized** | default | **Anti-bot evasion** |
168
+ | **Smooth** | `humanize=False, smooth=True` | General browsing simulation |
169
+ | **Instant** | `humanize=False, smooth=False` | Speed-critical operations |
171
170
 
172
171
  [**📖 Human-Like Interactions Docs**](https://pydoll.tech/docs/features/automation/human-interactions/)
173
172
  </details>
174
173
 
174
+ <details>
175
+ <summary><b>Humanized Mouse Movement</b></summary>
176
+ <br>
177
+
178
+ All mouse operations produce **human-like cursor movement** by default, using a multi-layered simulation pipeline:
179
+
180
+ - **Bezier curve paths**: Curved trajectories with asymmetric control points
181
+ - **Fitts's Law timing**: Movement duration scales naturally with distance
182
+ - **Minimum-jerk velocity**: Bell-shaped speed profile (slow start, peak, slow end)
183
+ - **Physiological tremor**: Gaussian noise (σ ≈ 1px) scaled inversely with velocity
184
+ - **Overshoot correction**: ~70% chance of overshooting fast movements, then correcting back
185
+
186
+ ```python
187
+ # All operations are humanized by default
188
+ await tab.mouse.move(500, 300)
189
+ await tab.mouse.click(500, 300)
190
+ await tab.mouse.drag(100, 200, 500, 400)
191
+
192
+ # Element clicks also use realistic Bezier curve movement
193
+ button = await tab.find(id='submit')
194
+ await button.click()
195
+
196
+ # Opt out when speed matters
197
+ await tab.mouse.click(500, 300, humanize=False)
198
+ ```
199
+
200
+ [**📖 Mouse Control Docs**](https://pydoll.tech/docs/features/automation/mouse-control/)
201
+ </details>
202
+
175
203
  ## 🚀 Getting Started in 60 Seconds
176
204
 
177
205
  Thanks to its `async` architecture and context managers, Pydoll is clean and efficient.
@@ -105,29 +105,28 @@ for sr in shadow_roots:
105
105
  </details>
106
106
 
107
107
  <details>
108
- <summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
108
+ <summary><b>Humanized Keyboard Input</b></summary>
109
109
  <br>
110
110
 
111
- Pydoll now includes a **humanized typing engine** that simulates realistic human typing behavior:
111
+ Pydoll's typing engine simulates realistic human typing behavior out of the box:
112
112
 
113
113
  - **Variable keystroke timing**: 30-120ms between keys (not fixed intervals)
114
114
  - **Realistic typos**: ~2% error rate with automatic correction behavior
115
- - **No more `interval` parameter**: Just use `humanize=True` for anti-bot evasion
116
115
 
117
116
  ```python
118
- # Old way (detectable)
119
- await element.type_text("hello", interval=0.1)
117
+ # Realistic typing by default
118
+ await element.type_text("hello")
120
119
 
121
- # New way (human-like, anti-bot)
122
- await element.type_text("hello", humanize=True)
120
+ # Opt out when speed is critical
121
+ await element.type_text("hello", humanize=False)
123
122
  ```
124
123
  </details>
125
124
 
126
125
  <details>
127
- <summary><b>Humanized Scroll with Physics Engine (<code>humanize=True</code>)</b></summary>
126
+ <summary><b>Humanized Scroll with Physics Engine</b></summary>
128
127
  <br>
129
128
 
130
- The scroll API now features a **Cubic Bezier curve physics engine** for realistic scrolling:
129
+ The scroll API features a **Cubic Bezier curve physics engine** for realistic scrolling:
131
130
 
132
131
  - **Momentum & friction**: Natural acceleration and deceleration
133
132
  - **Micro-pauses**: Brief stops during long scrolls (simulates reading)
@@ -135,23 +134,52 @@ The scroll API now features a **Cubic Bezier curve physics engine** for realisti
135
134
  - **Overshoot correction**: Occasionally scrolls past target and corrects back
136
135
 
137
136
  ```python
138
- # Smooth scroll (CSS animation, predictable timing)
139
- await tab.scroll.by(ScrollPosition.DOWN, 500, smooth=True)
137
+ # Humanized by default (physics engine, anti-bot)
138
+ await tab.scroll.by(ScrollPosition.DOWN, 500)
139
+ await tab.scroll.to_bottom()
140
140
 
141
- # Humanized scroll (physics engine, anti-bot)
142
- await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=True)
143
- await tab.scroll.to_bottom(humanize=True)
141
+ # CSS smooth scroll (predictable timing)
142
+ await tab.scroll.by(ScrollPosition.DOWN, 500, humanize=False, smooth=True)
144
143
  ```
145
144
 
146
145
  | Mode | Parameter | Use Case |
147
146
  |------|-----------|----------|
148
- | **Instant** | `smooth=False` | Speed-critical operations |
149
- | **Smooth** | `smooth=True` | General browsing simulation |
150
- | **Humanized** | `humanize=True` | **Anti-bot evasion** |
147
+ | **Humanized** | default | **Anti-bot evasion** |
148
+ | **Smooth** | `humanize=False, smooth=True` | General browsing simulation |
149
+ | **Instant** | `humanize=False, smooth=False` | Speed-critical operations |
151
150
 
152
151
  [**📖 Human-Like Interactions Docs**](https://pydoll.tech/docs/features/automation/human-interactions/)
153
152
  </details>
154
153
 
154
+ <details>
155
+ <summary><b>Humanized Mouse Movement</b></summary>
156
+ <br>
157
+
158
+ All mouse operations produce **human-like cursor movement** by default, using a multi-layered simulation pipeline:
159
+
160
+ - **Bezier curve paths**: Curved trajectories with asymmetric control points
161
+ - **Fitts's Law timing**: Movement duration scales naturally with distance
162
+ - **Minimum-jerk velocity**: Bell-shaped speed profile (slow start, peak, slow end)
163
+ - **Physiological tremor**: Gaussian noise (σ ≈ 1px) scaled inversely with velocity
164
+ - **Overshoot correction**: ~70% chance of overshooting fast movements, then correcting back
165
+
166
+ ```python
167
+ # All operations are humanized by default
168
+ await tab.mouse.move(500, 300)
169
+ await tab.mouse.click(500, 300)
170
+ await tab.mouse.drag(100, 200, 500, 400)
171
+
172
+ # Element clicks also use realistic Bezier curve movement
173
+ button = await tab.find(id='submit')
174
+ await button.click()
175
+
176
+ # Opt out when speed matters
177
+ await tab.mouse.click(500, 300, humanize=False)
178
+ ```
179
+
180
+ [**📖 Mouse Control Docs**](https://pydoll.tech/docs/features/automation/mouse-control/)
181
+ </details>
182
+
155
183
  ## 🚀 Getting Started in 60 Seconds
156
184
 
157
185
  Thanks to its `async` architecture and context managers, Pydoll is clean and efficient.
@@ -21,7 +21,9 @@ from pydoll.browser.managers import (
21
21
  from pydoll.browser.tab import Tab
22
22
  from pydoll.commands import (
23
23
  BrowserCommands,
24
+ EmulationCommands,
24
25
  FetchCommands,
26
+ PageCommands,
25
27
  RuntimeCommands,
26
28
  StorageCommands,
27
29
  TargetCommands,
@@ -38,6 +40,7 @@ from pydoll.exceptions import (
38
40
  from pydoll.protocol.browser.types import DownloadBehavior
39
41
  from pydoll.protocol.fetch.events import FetchEvent
40
42
  from pydoll.protocol.fetch.types import AuthChallengeResponseType
43
+ from pydoll.utils.user_agent_parser import UserAgentParser
41
44
 
42
45
  if TYPE_CHECKING:
43
46
  from tempfile import TemporaryDirectory
@@ -193,6 +196,7 @@ class Browser(ABC): # noqa: PLR0904
193
196
  valid_tab_id = await self._get_valid_tab_id(await self.get_targets())
194
197
  tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)
195
198
  self._tabs_opened[valid_tab_id] = tab
199
+ await self._apply_user_agent_override(tab)
196
200
  logger.info(f'Initial tab attached: {valid_tab_id}')
197
201
  return tab
198
202
 
@@ -306,6 +310,7 @@ class Browser(ABC): # noqa: PLR0904
306
310
  target_id = response['result']['targetId']
307
311
  tab = Tab(self, **self._get_tab_kwargs(target_id, browser_context_id))
308
312
  self._tabs_opened[target_id] = tab
313
+ await self._apply_user_agent_override(tab)
309
314
  await self._setup_context_proxy_auth_for_tab(tab, browser_context_id)
310
315
  if url:
311
316
  await tab.go_to(url)
@@ -347,10 +352,11 @@ class Browser(ABC): # noqa: PLR0904
347
352
  target_id for target_id in all_target_ids if target_id not in existing_target_ids
348
353
  ]
349
354
  existing_tabs = [self._tabs_opened[target_id] for target_id in existing_target_ids]
350
- new_tabs = [
351
- Tab(self, **self._get_tab_kwargs(target_id))
352
- for target_id in reversed(remaining_target_ids)
353
- ]
355
+ new_tabs = []
356
+ for target_id in reversed(remaining_target_ids):
357
+ tab = Tab(self, **self._get_tab_kwargs(target_id))
358
+ await self._apply_user_agent_override(tab)
359
+ new_tabs.append(tab)
354
360
  self._tabs_opened.update(dict(zip(remaining_target_ids, new_tabs)))
355
361
  logger.debug(
356
362
  f'Opened tabs resolved: existing={len(existing_tabs)}, new={len(new_tabs)}',
@@ -358,7 +364,9 @@ class Browser(ABC): # noqa: PLR0904
358
364
  return existing_tabs + new_tabs
359
365
 
360
366
  async def get_tab_by_target(self, target: TargetInfo) -> Tab:
361
- return Tab(self, **self._get_tab_kwargs(target['targetId']))
367
+ tab = Tab(self, **self._get_tab_kwargs(target['targetId']))
368
+ await self._apply_user_agent_override(tab)
369
+ return tab
362
370
 
363
371
  async def set_download_path(self, path: str, browser_context_id: Optional[str] = None):
364
372
  """Set download directory path (convenience method for set_download_behavior)."""
@@ -754,6 +762,43 @@ class Browser(ABC): # noqa: PLR0904
754
762
  temporary=True,
755
763
  )
756
764
 
765
+ async def _apply_user_agent_override(self, tab: Tab) -> None:
766
+ """Apply consistent User-Agent override to a tab if --user-agent= is set.
767
+
768
+ Detects the --user-agent= argument in browser options and automatically
769
+ synchronizes HTTP headers, navigator JS properties, and Client Hints
770
+ via CDP Emulation.setUserAgentOverride + JS injection.
771
+ """
772
+ user_agent = self._get_user_agent_from_options()
773
+ if not user_agent:
774
+ return
775
+
776
+ parsed = UserAgentParser.parse(user_agent)
777
+ logger.debug('Applying User-Agent override: %s', user_agent[:60])
778
+
779
+ await tab._execute_command(
780
+ EmulationCommands.set_user_agent_override(
781
+ user_agent=user_agent,
782
+ platform=parsed.platform,
783
+ user_agent_metadata=parsed.user_agent_metadata,
784
+ )
785
+ )
786
+
787
+ if parsed.navigator_override_js:
788
+ await tab._execute_command(
789
+ PageCommands.add_script_to_evaluate_on_new_document(
790
+ source=parsed.navigator_override_js,
791
+ run_immediately=True,
792
+ )
793
+ )
794
+
795
+ def _get_user_agent_from_options(self) -> Optional[str]:
796
+ """Extract User-Agent value from --user-agent= browser argument."""
797
+ for arg in self.options.arguments:
798
+ if arg.startswith('--user-agent='):
799
+ return arg[len('--user-agent=') :]
800
+ return None
801
+
757
802
  async def _verify_browser_running(self):
758
803
  """
759
804
  Verify browser started successfully.
@@ -29,6 +29,7 @@ class ChromiumOptions(Options):
29
29
  self._start_timeout = 10
30
30
  self._browser_preferences = {}
31
31
  self._headless = False
32
+ self._webrtc_leak_protection = False
32
33
  self._page_load_state = PageLoadState.COMPLETE
33
34
 
34
35
  @property
@@ -319,6 +320,20 @@ class ChromiumOptions(Options):
319
320
  return
320
321
  methods_map[headless]('--headless')
321
322
 
323
+ @property
324
+ def webrtc_leak_protection(self) -> bool:
325
+ return self._webrtc_leak_protection
326
+
327
+ @webrtc_leak_protection.setter
328
+ def webrtc_leak_protection(self, enabled: bool):
329
+ self._webrtc_leak_protection = enabled
330
+ argument = '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
331
+ has_argument = argument in self.arguments
332
+ methods_map = {True: self.add_argument, False: self.remove_argument}
333
+ if enabled == has_argument:
334
+ return
335
+ methods_map[enabled](argument)
336
+
322
337
  @property
323
338
  def page_load_state(self) -> PageLoadState:
324
339
  return self._page_load_state
@@ -56,7 +56,7 @@ from pydoll.exceptions import (
56
56
  WaitElementTimeout,
57
57
  WebSocketConnectionClosed,
58
58
  )
59
- from pydoll.interactions import KeyboardAPI, ScrollAPI
59
+ from pydoll.interactions import KeyboardAPI, MouseAPI, ScrollAPI
60
60
  from pydoll.interactions.iframe import IFrameContext
61
61
  from pydoll.protocol.browser.types import DownloadBehavior, DownloadProgressState
62
62
  from pydoll.protocol.dom.types import Node, ShadowRootType
@@ -160,6 +160,7 @@ class Tab(FindElementsMixin):
160
160
  self._request: Optional[Request] = None
161
161
  self._scroll: Optional[ScrollAPI] = None
162
162
  self._keyboard: Optional[KeyboardAPI] = None
163
+ self._mouse: MouseAPI = MouseAPI(self)
163
164
  logger.debug(
164
165
  (
165
166
  f'Tab initialized: target_id={self._target_id}, '
@@ -229,6 +230,16 @@ class Tab(FindElementsMixin):
229
230
  self._keyboard = KeyboardAPI(self)
230
231
  return self._keyboard
231
232
 
233
+ @property
234
+ def mouse(self) -> MouseAPI:
235
+ """
236
+ Get the mouse API for controlling mouse input.
237
+
238
+ Returns:
239
+ MouseAPI: An instance of the MouseAPI class for mouse operations.
240
+ """
241
+ return self._mouse
242
+
232
243
  @property
233
244
  def intercept_file_chooser_dialog_enabled(self) -> bool:
234
245
  """Whether file chooser dialog interception is active."""
@@ -605,7 +616,9 @@ class Tab(FindElementsMixin):
605
616
  )
606
617
  host_object_id = host_response['result']['object']['objectId']
607
618
  host_attrs = await self._get_object_attributes(object_id=host_object_id)
608
- return WebElement(host_object_id, self._connection_handler, attributes_list=host_attrs)
619
+ return WebElement(
620
+ host_object_id, self._connection_handler, attributes_list=host_attrs, mouse=self._mouse
621
+ )
609
622
 
610
623
  async def _collect_oopif_shadow_roots(self) -> list[ShadowRoot]:
611
624
  """Discover shadow roots inside cross-origin iframes (OOPIFs)."""
@@ -748,7 +761,12 @@ class Tab(FindElementsMixin):
748
761
  tag_name = node_info.get('nodeName', '').lower()
749
762
  attributes.extend(['tag_name', tag_name])
750
763
 
751
- return WebElement(host_object_id, self._connection_handler, attributes_list=attributes)
764
+ return WebElement(
765
+ host_object_id,
766
+ self._connection_handler,
767
+ attributes_list=attributes,
768
+ mouse=self._mouse,
769
+ )
752
770
  except (CommandExecutionTimeout, WebSocketConnectionClosed, KeyError):
753
771
  logger.debug(f'Failed to resolve OOPIF shadow host: backend_node_id={host_backend_id}')
754
772
  return None
@@ -1,6 +1,7 @@
1
1
  # global imports
2
2
  from pydoll.commands.browser_commands import BrowserCommands
3
3
  from pydoll.commands.dom_commands import DomCommands
4
+ from pydoll.commands.emulation_commands import EmulationCommands
4
5
  from pydoll.commands.fetch_commands import FetchCommands
5
6
  from pydoll.commands.input_commands import InputCommands
6
7
  from pydoll.commands.network_commands import NetworkCommands
@@ -11,6 +12,7 @@ from pydoll.commands.target_commands import TargetCommands
11
12
 
12
13
  __all__ = [
13
14
  'DomCommands',
15
+ 'EmulationCommands',
14
16
  'FetchCommands',
15
17
  'InputCommands',
16
18
  'NetworkCommands',
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from pydoll.protocol.base import Command
6
+ from pydoll.protocol.emulation.methods import (
7
+ EmulationMethod,
8
+ SetUserAgentOverrideParams,
9
+ )
10
+
11
+ if TYPE_CHECKING:
12
+ from pydoll.protocol.emulation.methods import SetUserAgentOverrideCommand
13
+ from pydoll.protocol.emulation.types import UserAgentMetadata
14
+
15
+
16
+ class EmulationCommands:
17
+ """
18
+ Implementation of Chrome DevTools Protocol for the Emulation domain.
19
+
20
+ This class provides commands for emulating different environments,
21
+ including user agent overrides, device metrics, and other browser
22
+ characteristics useful for testing and automation.
23
+
24
+ See https://chromedevtools.github.io/devtools-protocol/tot/Emulation/
25
+ """
26
+
27
+ @staticmethod
28
+ def set_user_agent_override(
29
+ user_agent: str,
30
+ accept_language: Optional[str] = None,
31
+ platform: Optional[str] = None,
32
+ user_agent_metadata: Optional[UserAgentMetadata] = None,
33
+ ) -> SetUserAgentOverrideCommand:
34
+ """
35
+ Overrides the browser's User-Agent string via the Emulation domain.
36
+
37
+ This is the canonical CDP method for User-Agent override. It modifies
38
+ both HTTP headers and navigator JavaScript properties, ensuring
39
+ consistency between all layers.
40
+
41
+ When userAgentMetadata is provided, Client Hint headers (Sec-CH-UA-*)
42
+ will also be sent consistently with the overridden User-Agent.
43
+
44
+ Args:
45
+ user_agent: Complete User-Agent string to use.
46
+ accept_language: Browser language preference (e.g., 'en-US,en;q=0.9').
47
+ platform: Value for navigator.platform (e.g., 'Win32', 'MacIntel').
48
+ user_agent_metadata: Client Hints metadata for Sec-CH-UA-* headers
49
+ and navigator.userAgentData.
50
+
51
+ Returns:
52
+ SetUserAgentOverrideCommand: CDP command to override user agent.
53
+ """
54
+ params = SetUserAgentOverrideParams(userAgent=user_agent)
55
+ if accept_language is not None:
56
+ params['acceptLanguage'] = accept_language
57
+ if platform is not None:
58
+ params['platform'] = platform
59
+ if user_agent_metadata is not None:
60
+ params['userAgentMetadata'] = user_agent_metadata
61
+ return Command(method=EmulationMethod.SET_USER_AGENT_OVERRIDE, params=params)
@@ -496,7 +496,14 @@ class FindElementsMixin:
496
496
  object_id = response_for_command['result']['result']['objectId']
497
497
  attributes = await self._get_object_attributes(object_id=object_id)
498
498
  logger.debug(f'_find_element() found object_id={object_id}')
499
- element = create_web_element(object_id, self._connection_handler, by, value, attributes)
499
+ element = create_web_element(
500
+ object_id,
501
+ self._connection_handler,
502
+ by,
503
+ value,
504
+ attributes,
505
+ mouse=getattr(self, '_mouse', None),
506
+ )
500
507
  self._apply_iframe_context_to_element(
501
508
  element, iframe_context or getattr(self, '_iframe_context', None)
502
509
  )
@@ -571,7 +578,14 @@ class FindElementsMixin:
571
578
  tag_name = node_description.get('nodeName', '').lower()
572
579
  attributes.extend(['tag_name', tag_name])
573
580
 
574
- child = create_web_element(object_id, self._connection_handler, by, value, attributes)
581
+ child = create_web_element(
582
+ object_id,
583
+ self._connection_handler,
584
+ by,
585
+ value,
586
+ attributes,
587
+ mouse=getattr(self, '_mouse', None),
588
+ )
575
589
  self._apply_iframe_context_to_element(child, inherited_context)
576
590
  elements.append(child)
577
591
  logger.debug(f'_find_elements() returning {len(elements)} elements')
@@ -57,6 +57,7 @@ from pydoll.utils import (
57
57
  )
58
58
 
59
59
  if TYPE_CHECKING:
60
+ from pydoll.interactions.mouse import Mouse as MouseType
60
61
  from pydoll.protocol.dom.methods import (
61
62
  DescribeNodeResponse,
62
63
  GetBoxModelResponse,
@@ -90,6 +91,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
90
91
  method: Optional[str] = None,
91
92
  selector: Optional[str] = None,
92
93
  attributes_list: list[str] = [],
94
+ mouse: Optional['MouseType'] = None,
93
95
  ):
94
96
  """
95
97
  Initialize WebElement wrapper.
@@ -100,6 +102,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
100
102
  method: Search method used to find this element (for debugging).
101
103
  selector: Selector string used to find this element (for debugging).
102
104
  attributes_list: Flat list of alternating attribute names and values.
105
+ mouse: Optional Mouse instance for humanized click behavior.
103
106
  """
104
107
  self._object_id = object_id
105
108
  self._search_method = method
@@ -107,6 +110,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
107
110
  self._connection_handler = connection_handler
108
111
  self._attributes: dict[str, str] = {}
109
112
  self._keyboard: Optional[Keyboard] = None
113
+ self._mouse = mouse
110
114
  self._iframe_context: Optional[IFrameContext] = None
111
115
  self._iframe_resolver: Optional[IFrameContextResolver] = None
112
116
  self._def_attributes(attributes_list)
@@ -264,7 +268,9 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
264
268
  object_id = result['result']['result']['objectId']
265
269
  attributes = await self._get_object_attributes(object_id=object_id)
266
270
  logger.debug(f'Parent element resolved: object_id={object_id}')
267
- return WebElement(object_id, self._connection_handler, attributes_list=attributes)
271
+ return WebElement(
272
+ object_id, self._connection_handler, attributes_list=attributes, mouse=self._mouse
273
+ )
268
274
 
269
275
  async def get_shadow_root(self, timeout: float = 0) -> ShadowRoot:
270
276
  """
@@ -544,6 +550,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
544
550
  x_offset: int = 0,
545
551
  y_offset: int = 0,
546
552
  hold_time: float = 0.1,
553
+ humanize: bool = True,
547
554
  ):
548
555
  """
549
556
  Click element using simulated mouse events.
@@ -551,7 +558,11 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
551
558
  Args:
552
559
  x_offset: Horizontal offset from element center.
553
560
  y_offset: Vertical offset from element center.
554
- hold_time: Duration to hold mouse button down.
561
+ hold_time: Duration to hold mouse button down (used when humanize=False).
562
+ humanize: When True and a Mouse instance is available, uses humanized
563
+ Bezier curve movement from the current tracked position to the
564
+ element center before clicking. When False, dispatches raw CDP
565
+ mousePressed/mouseReleased events directly.
555
566
 
556
567
  Raises:
557
568
  ElementNotVisible: If element is not visible.
@@ -578,14 +589,21 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
578
589
  except KeyError:
579
590
  element_bounds_js = await self.get_bounds_using_js()
580
591
  position_to_click = (
581
- element_bounds_js['x'] + element_bounds_js['width'] / 2,
582
- element_bounds_js['y'] + element_bounds_js['height'] / 2,
592
+ element_bounds_js['x'] + element_bounds_js['width'] / 2 + x_offset,
593
+ element_bounds_js['y'] + element_bounds_js['height'] / 2 + y_offset,
594
+ )
595
+
596
+ if humanize and self._mouse is not None:
597
+ logger.info(
598
+ f'Clicking element (humanized): x={position_to_click[0]}, y={position_to_click[1]}'
583
599
  )
600
+ await self._mouse.click(position_to_click[0], position_to_click[1])
601
+ return
602
+
584
603
  logger.info(
585
604
  f'Clicking element: x={position_to_click[0]}, '
586
605
  f'y={position_to_click[1]}, hold={hold_time}s'
587
606
  )
588
-
589
607
  press_command = InputCommands.dispatch_mouse_event(
590
608
  type=MouseEventType.MOUSE_PRESSED,
591
609
  x=int(position_to_click[0]),
@@ -681,7 +699,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
681
699
  async def type_text(
682
700
  self,
683
701
  text: str,
684
- humanize: bool = False,
702
+ humanize: bool = True,
685
703
  interval: Optional[float] = None,
686
704
  ):
687
705
  """
@@ -689,7 +707,7 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
689
707
 
690
708
  Args:
691
709
  text: Text to type into the element.
692
- humanize: When True, simulates human-like typing.
710
+ humanize: When True (default), simulates human-like typing.
693
711
  interval: Deprecated. Use humanize=True instead.
694
712
  """
695
713
  logger.info(f'Typing text (length={len(text)}, humanize={humanize})')
@@ -964,7 +982,12 @@ class WebElement(FindElementsMixin): # noqa: PLR0904
964
982
  child_object_id = prop['value']['objectId']
965
983
  attributes = await self._get_object_attributes(object_id=child_object_id)
966
984
  family_elements.append(
967
- WebElement(child_object_id, self._connection_handler, attributes_list=attributes)
985
+ WebElement(
986
+ child_object_id,
987
+ self._connection_handler,
988
+ attributes_list=attributes,
989
+ mouse=self._mouse,
990
+ )
968
991
  )
969
992
 
970
993
  logger.debug(f'Family elements found: {len(family_elements)}')
@@ -7,6 +7,7 @@ from pydoll.interactions.keyboard import (
7
7
  TypoConfig,
8
8
  TypoResult,
9
9
  )
10
+ from pydoll.interactions.mouse import Mouse, MouseAPI, MouseTimingConfig
10
11
  from pydoll.interactions.scroll import Scroll, ScrollAPI, ScrollTimingConfig
11
12
 
12
13
  __all__ = [
@@ -15,6 +16,9 @@ __all__ = [
15
16
  'IFrameContextResolver',
16
17
  'Keyboard',
17
18
  'KeyboardAPI',
19
+ 'Mouse',
20
+ 'MouseAPI',
21
+ 'MouseTimingConfig',
18
22
  'Scroll',
19
23
  'ScrollAPI',
20
24
  'ScrollTimingConfig',
@@ -184,7 +184,7 @@ class Keyboard:
184
184
  async def type_text(
185
185
  self,
186
186
  text: str,
187
- humanize: bool = False,
187
+ humanize: bool = True,
188
188
  interval: Optional[float] = None,
189
189
  ):
190
190
  """
@@ -192,13 +192,13 @@ class Keyboard:
192
192
 
193
193
  Args:
194
194
  text: Text to type.
195
- humanize: When True, simulates human-like typing with
195
+ humanize: When True (default), simulates human-like typing with
196
196
  variable delays and occasional typos (~2%).
197
197
  interval: Deprecated. Use humanize=True instead.
198
198
 
199
199
  Example:
200
200
  await tab.keyboard.type_text("Hello World")
201
- await tab.keyboard.type_text("Hello World", humanize=True)
201
+ await tab.keyboard.type_text("Hello World", humanize=False)
202
202
  """
203
203
  if interval is not None:
204
204
  warnings.warn(