pydoll-python 2.9.0__tar.gz → 2.9.2__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 (86) hide show
  1. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/PKG-INFO +14 -1
  2. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/README.md +13 -0
  3. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/chromium/base.py +136 -21
  4. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/chromium/chrome.py +8 -1
  5. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/chromium/edge.py +8 -1
  6. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/managers/browser_options_manager.py +11 -0
  7. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/managers/browser_process_manager.py +19 -2
  8. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/managers/proxy_manager.py +22 -2
  9. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/managers/temp_dir_manager.py +10 -0
  10. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/requests/request.py +50 -9
  11. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/requests/response.py +11 -0
  12. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/tab.py +154 -23
  13. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/connection/connection_handler.py +36 -5
  14. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/connection/managers/commands_manager.py +6 -0
  15. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/connection/managers/events_manager.py +9 -0
  16. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/elements/mixins/find_elements_mixin.py +42 -1
  17. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/elements/web_element.py +56 -4
  18. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/page/events.py +2 -0
  19. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pyproject.toml +1 -1
  20. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/LICENSE +0 -0
  21. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/__init__.py +0 -0
  22. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/__init__.py +0 -0
  23. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/chromium/__init__.py +0 -0
  24. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/interfaces.py +0 -0
  25. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/managers/__init__.py +0 -0
  26. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/options.py +0 -0
  27. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/browser/requests/__init__.py +0 -0
  28. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/__init__.py +0 -0
  29. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/browser_commands.py +0 -0
  30. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/dom_commands.py +0 -0
  31. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/fetch_commands.py +0 -0
  32. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/input_commands.py +0 -0
  33. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/network_commands.py +0 -0
  34. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/page_commands.py +0 -0
  35. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/runtime_commands.py +0 -0
  36. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/storage_commands.py +0 -0
  37. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/commands/target_commands.py +0 -0
  38. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/connection/__init__.py +0 -0
  39. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/connection/managers/__init__.py +0 -0
  40. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/constants.py +0 -0
  41. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/elements/__init__.py +0 -0
  42. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/elements/mixins/__init__.py +0 -0
  43. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/exceptions.py +0 -0
  44. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/__init__.py +0 -0
  45. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/base.py +0 -0
  46. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/browser/__init__.py +0 -0
  47. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/browser/events.py +0 -0
  48. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/browser/methods.py +0 -0
  49. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/browser/types.py +0 -0
  50. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/debugger/types.py +0 -0
  51. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/dom/__init__.py +0 -0
  52. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/dom/events.py +0 -0
  53. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/dom/methods.py +0 -0
  54. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/dom/types.py +0 -0
  55. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/emulation/types.py +0 -0
  56. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/fetch/__init__.py +0 -0
  57. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/fetch/events.py +0 -0
  58. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/fetch/methods.py +0 -0
  59. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/fetch/types.py +0 -0
  60. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/input/__init__.py +0 -0
  61. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/input/events.py +0 -0
  62. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/input/methods.py +0 -0
  63. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/input/types.py +0 -0
  64. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/io/types.py +0 -0
  65. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/network/__init__.py +0 -0
  66. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/network/events.py +0 -0
  67. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/network/methods.py +0 -0
  68. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/network/types.py +0 -0
  69. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/page/__init__.py +0 -0
  70. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/page/methods.py +0 -0
  71. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/page/types.py +0 -0
  72. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/runtime/__init__.py +0 -0
  73. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/runtime/events.py +0 -0
  74. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/runtime/methods.py +0 -0
  75. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/runtime/types.py +0 -0
  76. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/security/types.py +0 -0
  77. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/storage/__init__.py +0 -0
  78. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/storage/events.py +0 -0
  79. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/storage/methods.py +0 -0
  80. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/storage/types.py +0 -0
  81. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/target/__init__.py +0 -0
  82. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/target/events.py +0 -0
  83. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/target/methods.py +0 -0
  84. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/protocol/target/types.py +0 -0
  85. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/py.typed +0 -0
  86. {pydoll_python-2.9.0 → pydoll_python-2.9.2}/pydoll/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydoll-python
3
- Version: 2.9.0
3
+ Version: 2.9.2
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
@@ -73,6 +73,19 @@ pip install pydoll-python
73
73
 
74
74
  And that's it! Just install and start automating.
75
75
 
76
+
77
+ ## ⭐ Sponsors
78
+ The support from sponsors is essential to keep the project alive, evolving, and accessible to the entire community. Each partnership helps cover costs, drive new features, and ensure ongoing development.
79
+ We are truly grateful to everyone who believes in and supports the project!
80
+
81
+ <br>
82
+ <p style="font-size:21px; color:black;">Browser testing via
83
+ <a href="https://www.lambdatest.com/?utm_source=pydoll&utm_medium=sponsor" target="_blank">
84
+ <img src="https://www.lambdatest.com/blue-logo.png" style="vertical-align: middle;" width="250" height="45" />
85
+ </a>
86
+ </p>
87
+ <br>
88
+
76
89
  ## 🚀 Getting Started
77
90
 
78
91
  ### Your first automation
@@ -53,6 +53,19 @@ pip install pydoll-python
53
53
 
54
54
  And that's it! Just install and start automating.
55
55
 
56
+
57
+ ## ⭐ Sponsors
58
+ The support from sponsors is essential to keep the project alive, evolving, and accessible to the entire community. Each partnership helps cover costs, drive new features, and ensure ongoing development.
59
+ We are truly grateful to everyone who believes in and supports the project!
60
+
61
+ <br>
62
+ <p style="font-size:21px; color:black;">Browser testing via
63
+ <a href="https://www.lambdatest.com/?utm_source=pydoll&utm_medium=sponsor" target="_blank">
64
+ <img src="https://www.lambdatest.com/blue-logo.png" style="vertical-align: middle;" width="250" height="45" />
65
+ </a>
66
+ </p>
67
+ <br>
68
+
56
69
  ## 🚀 Getting Started
57
70
 
58
71
  ### Your first automation
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import logging
3
4
  import os
4
5
  import shutil
5
6
  import warnings
@@ -59,6 +60,8 @@ from pydoll.protocol.target.methods import (
59
60
  )
60
61
  from pydoll.protocol.target.types import TargetInfo
61
62
 
63
+ logger = logging.getLogger(__name__)
64
+
62
65
 
63
66
  class Browser(ABC): # noqa: PLR0904
64
67
  """
@@ -95,14 +98,21 @@ class Browser(ABC): # noqa: PLR0904
95
98
  self._backup_preferences_dir = ''
96
99
  self._tabs_opened: dict[str, Tab] = {}
97
100
  self._context_proxy_auth: dict[str, tuple[str, str]] = {}
101
+ logger.debug(
102
+ f'Browser initialized: port={self._connection_port}, '
103
+ f'headless={getattr(self.options, "headless", None)}'
104
+ )
98
105
 
99
106
  async def __aenter__(self) -> 'Browser':
100
107
  """Async context manager entry."""
108
+ logger.debug('Entering browser async context')
101
109
  return self
102
110
 
103
111
  async def __aexit__(self, exc_type, exc_val, exc_tb):
104
112
  """Async context manager exit with cleanup."""
113
+ logger.debug(f'Exiting browser async context: exc_type={exc_type}')
105
114
  if self._backup_preferences_dir:
115
+ logger.debug(f'Restoring backup preferences directory: {self._backup_preferences_dir}')
106
116
  user_data_dir = self._get_user_data_dir()
107
117
  shutil.copy2(
108
118
  self._backup_preferences_dir,
@@ -129,8 +139,10 @@ class Browser(ABC): # noqa: PLR0904
129
139
  You are supposed to use this method only if you want to connect to a browser
130
140
  that is already running.
131
141
  """
142
+ logger.info(f'Connecting to browser via WebSocket: {ws_address}')
132
143
  await self._setup_ws_address(ws_address)
133
144
  tabs = await self.get_opened_tabs()
145
+ logger.info(f'Connected. Tabs available: {len(tabs)}')
134
146
  return tabs[0]
135
147
 
136
148
  async def start(self, headless: bool = False) -> Tab:
@@ -156,19 +168,24 @@ class Browser(ABC): # noqa: PLR0904
156
168
  self.options.headless = headless
157
169
 
158
170
  binary_location = self.options.binary_location or self._get_default_binary_location()
171
+ logger.debug('Resolved binary location: %s', binary_location)
159
172
 
160
173
  self._setup_user_dir()
174
+ logger.debug('User data directory configured')
161
175
  proxy_config = self._proxy_manager.get_proxy_credentials()
162
176
 
177
+ logger.info(f'Starting browser process on port {self._connection_port}')
163
178
  self._browser_process_manager.start_browser_process(
164
179
  binary_location, self._connection_port, self.options.arguments
165
180
  )
166
181
  await self._verify_browser_running()
182
+ logger.info('Browser process started and responsive')
167
183
  await self._configure_proxy(proxy_config[0], proxy_config[1])
168
184
 
169
185
  valid_tab_id = await self._get_valid_tab_id(await self.get_targets())
170
186
  tab = Tab(self, target_id=valid_tab_id, connection_port=self._connection_port)
171
187
  self._tabs_opened[valid_tab_id] = tab
188
+ logger.info(f'Initial tab attached: {valid_tab_id}')
172
189
  return tab
173
190
 
174
191
  async def stop(self):
@@ -182,12 +199,15 @@ class Browser(ABC): # noqa: PLR0904
182
199
  BrowserNotRunning: If the browser is not currently running.
183
200
  """
184
201
  if not await self._is_browser_running():
202
+ logger.error('Stop called but browser is not running')
185
203
  raise BrowserNotRunning()
186
204
 
205
+ logger.info('Stopping browser process')
187
206
  await self._execute_command(BrowserCommands.close())
188
207
  self._browser_process_manager.stop_process()
189
208
  self._temp_directory_manager.cleanup()
190
209
  await self._connection_handler.close()
210
+ logger.info('Browser process stopped and resources cleaned up')
191
211
 
192
212
  async def create_browser_context(
193
213
  self, proxy_server: Optional[str] = None, proxy_bypass_list: Optional[str] = None
@@ -210,6 +230,10 @@ class Browser(ABC): # noqa: PLR0904
210
230
  extracted_auth: Optional[tuple[str, str]] = None
211
231
  if proxy_server:
212
232
  sanitized_proxy, extracted_auth = self._sanitize_proxy_and_extract_auth(proxy_server)
233
+ logger.debug(
234
+ f'Creating browser context with proxy: {sanitized_proxy}'
235
+ f'(credentials provided={bool(extracted_auth)})'
236
+ )
213
237
 
214
238
  response: CreateBrowserContextResponse = await self._execute_command(
215
239
  TargetCommands.create_browser_context(
@@ -220,6 +244,7 @@ class Browser(ABC): # noqa: PLR0904
220
244
  context_id = response['result']['browserContextId']
221
245
  if extracted_auth:
222
246
  self._context_proxy_auth[context_id] = extracted_auth
247
+ logger.info(f'Created browser context: {context_id}')
223
248
  return context_id
224
249
 
225
250
  async def delete_browser_context(self, browser_context_id: str):
@@ -232,6 +257,7 @@ class Browser(ABC): # noqa: PLR0904
232
257
  Note:
233
258
  Closes all associated tabs immediately.
234
259
  """
260
+ logger.info(f'Deleting browser context: {browser_context_id}')
235
261
  return await self._execute_command(
236
262
  TargetCommands.dispose_browser_context(browser_context_id)
237
263
  )
@@ -241,6 +267,7 @@ class Browser(ABC): # noqa: PLR0904
241
267
  response: GetBrowserContextsResponse = await self._execute_command(
242
268
  TargetCommands.get_browser_contexts()
243
269
  )
270
+ logger.debug(f'Fetched {len(response["result"]["browserContextIds"])} browser contexts')
244
271
  return response['result']['browserContextIds']
245
272
 
246
273
  async def new_tab(self, url: str = '', browser_context_id: Optional[str] = None) -> Tab:
@@ -254,6 +281,7 @@ class Browser(ABC): # noqa: PLR0904
254
281
  Returns:
255
282
  Tab instance for page navigation and element interaction.
256
283
  """
284
+ logger.info(f'Creating new tab (context={browser_context_id})')
257
285
  response: CreateTargetResponse = await self._execute_command(
258
286
  TargetCommands.create_target(
259
287
  browser_context_id=browser_context_id,
@@ -263,7 +291,9 @@ class Browser(ABC): # noqa: PLR0904
263
291
  tab = Tab(self, **self._get_tab_kwargs(target_id, browser_context_id))
264
292
  self._tabs_opened[target_id] = tab
265
293
  await self._setup_context_proxy_auth_for_tab(tab, browser_context_id)
266
- if url: await tab.go_to(url)
294
+ if url:
295
+ await tab.go_to(url)
296
+ logger.info(f'New tab created: {target_id}')
267
297
  return tab
268
298
 
269
299
  async def get_targets(self) -> list[TargetInfo]:
@@ -277,6 +307,7 @@ class Browser(ABC): # noqa: PLR0904
277
307
  List of TargetInfo objects.
278
308
  """
279
309
  response: GetTargetsResponse = await self._execute_command(TargetCommands.get_targets())
310
+ logger.debug(f'Fetched {len(response["result"]["targetInfos"])} targets')
280
311
  return response['result']['targetInfos']
281
312
 
282
313
  async def get_opened_tabs(self) -> list[Tab]:
@@ -305,10 +336,14 @@ class Browser(ABC): # noqa: PLR0904
305
336
  for target_id in reversed(remaining_target_ids)
306
337
  ]
307
338
  self._tabs_opened.update(dict(zip(remaining_target_ids, new_tabs)))
339
+ logger.debug(
340
+ f'Opened tabs resolved: existing={len(existing_tabs)}, new={len(new_tabs)}',
341
+ )
308
342
  return existing_tabs + new_tabs
309
343
 
310
344
  async def set_download_path(self, path: str, browser_context_id: Optional[str] = None):
311
345
  """Set download directory path (convenience method for set_download_behavior)."""
346
+ logger.info(f'Setting download path: {path} (context={browser_context_id})')
312
347
  return await self._execute_command(
313
348
  BrowserCommands.set_download_behavior(
314
349
  behavior=DownloadBehavior.ALLOW,
@@ -333,6 +368,11 @@ class Browser(ABC): # noqa: PLR0904
333
368
  browser_context_id: Context to apply to (default if None).
334
369
  events_enabled: Generate download events for progress tracking.
335
370
  """
371
+ logger.info(
372
+ f'Setting download behavior: behavior={behavior},'
373
+ f'path={download_path}, context={browser_context_id},'
374
+ f'events={events_enabled}'
375
+ )
336
376
  return await self._execute_command(
337
377
  BrowserCommands.set_download_behavior(
338
378
  behavior=behavior,
@@ -344,12 +384,14 @@ class Browser(ABC): # noqa: PLR0904
344
384
 
345
385
  async def delete_all_cookies(self, browser_context_id: Optional[str] = None):
346
386
  """Delete all cookies (session, persistent, third-party) from browser or context."""
387
+ logger.info(f'Clearing all cookies (context={browser_context_id})')
347
388
  return await self._execute_command(StorageCommands.clear_cookies(browser_context_id))
348
389
 
349
390
  async def set_cookies(
350
391
  self, cookies: list[CookieParam], browser_context_id: Optional[str] = None
351
392
  ):
352
393
  """Set multiple cookies in browser or context."""
394
+ logger.debug(f'Setting {len(cookies)} cookies (context={browser_context_id})')
353
395
  return await self._execute_command(StorageCommands.set_cookies(cookies, browser_context_id))
354
396
 
355
397
  async def get_cookies(self, browser_context_id: Optional[str] = None) -> list[Cookie]:
@@ -357,11 +399,15 @@ class Browser(ABC): # noqa: PLR0904
357
399
  response: GetCookiesResponse = await self._execute_command(
358
400
  StorageCommands.get_cookies(browser_context_id)
359
401
  )
402
+ logger.debug(
403
+ f'Retrieved {len(response["result"]["cookies"])} cookies (context={browser_context_id})'
404
+ )
360
405
  return response['result']['cookies']
361
406
 
362
407
  async def get_version(self) -> GetVersionResult:
363
408
  """Get browser version and CDP protocol information."""
364
409
  response: GetVersionResponse = await self._execute_command(BrowserCommands.get_version())
410
+ logger.debug(f'Browser version: {response["result"]}')
365
411
  return response['result']
366
412
 
367
413
  async def get_window_id_for_target(self, target_id: str) -> int:
@@ -369,12 +415,14 @@ class Browser(ABC): # noqa: PLR0904
369
415
  response: GetWindowForTargetResponse = await self._execute_command(
370
416
  BrowserCommands.get_window_for_target(target_id)
371
417
  )
418
+ logger.debug(f'Window id for target {target_id}: {response["result"]["windowId"]}')
372
419
  return response['result']['windowId']
373
420
 
374
421
  async def get_window_id_for_tab(self, tab: Tab) -> int:
375
422
  """Get window ID for tab (convenience method)."""
376
423
  target_id = tab._target_id or (tab._ws_address.split('/')[-1] if tab._ws_address else None)
377
424
  if not target_id:
425
+ logger.error('Missing target id or ws address for tab when getting window id')
378
426
  raise MissingTargetOrWebSocket()
379
427
  return await self.get_window_id_for_target(target_id)
380
428
 
@@ -392,11 +440,13 @@ class Browser(ABC): # noqa: PLR0904
392
440
  async def set_window_maximized(self):
393
441
  """Maximize browser window (affects all tabs in window)."""
394
442
  window_id = await self.get_window_id()
443
+ logger.info(f'Maximizing window: id={window_id}')
395
444
  return await self._execute_command(BrowserCommands.set_window_maximized(window_id))
396
445
 
397
446
  async def set_window_minimized(self):
398
447
  """Minimize browser window to taskbar/dock."""
399
448
  window_id = await self.get_window_id()
449
+ logger.info(f'Minimizing window: id={window_id}')
400
450
  return await self._execute_command(BrowserCommands.set_window_minimized(window_id))
401
451
 
402
452
  async def set_window_bounds(self, bounds: Bounds):
@@ -408,6 +458,7 @@ class Browser(ABC): # noqa: PLR0904
408
458
  Only specified properties are changed.
409
459
  """
410
460
  window_id = await self.get_window_id()
461
+ logger.info(f'Setting window bounds: id={window_id}, bounds={bounds}')
411
462
  return await self._execute_command(BrowserCommands.set_window_bounds(window_id, bounds))
412
463
 
413
464
  async def grant_permissions(
@@ -426,12 +477,16 @@ class Browser(ABC): # noqa: PLR0904
426
477
  origin: Origin to grant to (all origins if None).
427
478
  browser_context_id: Context to apply to (default if None).
428
479
  """
480
+ logger.info(
481
+ f'Granting permissions: {permissions} (origin={origin}, context={browser_context_id})',
482
+ )
429
483
  return await self._execute_command(
430
484
  BrowserCommands.grant_permissions(permissions, origin, browser_context_id)
431
485
  )
432
486
 
433
487
  async def reset_permissions(self, browser_context_id: Optional[str] = None):
434
488
  """Reset all permissions to defaults and restore prompting behavior."""
489
+ logger.info(f'Resetting permissions (context={browser_context_id})')
435
490
  return await self._execute_command(BrowserCommands.reset_permissions(browser_context_id))
436
491
 
437
492
  @overload
@@ -467,12 +522,17 @@ class Browser(ABC): # noqa: PLR0904
467
522
  function_to_register = callback_wrapper
468
523
  else:
469
524
  function_to_register = callback
525
+ logger.debug(
526
+ f'Registering callback: event={event_name}, temporary={temporary}, '
527
+ f'async={asyncio.iscoroutinefunction(callback)}'
528
+ )
470
529
  return await self._connection_handler.register_callback(
471
530
  event_name, function_to_register, temporary
472
531
  )
473
532
 
474
533
  async def remove_callback(self, callback_id: int):
475
534
  """Remove callback from browser."""
535
+ logger.debug(f'Removing callback: id={callback_id}')
476
536
  return await self._connection_handler.remove_callback(callback_id)
477
537
 
478
538
  async def enable_fetch_events(
@@ -493,6 +553,10 @@ class Browser(ABC): # noqa: PLR0904
493
553
  Note:
494
554
  Paused requests must be continued or they will timeout.
495
555
  """
556
+ logger.debug(
557
+ f'Enabling Fetch events: handle_auth={handle_auth_requests}, '
558
+ f'resource_type={resource_type}'
559
+ )
496
560
  return await self._connection_handler.execute_command(
497
561
  FetchCommands.enable(
498
562
  handle_auth_requests=handle_auth_requests,
@@ -502,14 +566,17 @@ class Browser(ABC): # noqa: PLR0904
502
566
 
503
567
  async def disable_fetch_events(self):
504
568
  """Disable request interception and release any paused requests."""
569
+ logger.debug('Disabling Fetch events')
505
570
  return await self._connection_handler.execute_command(FetchCommands.disable())
506
571
 
507
572
  async def enable_runtime_events(self):
508
573
  """Enable runtime events."""
574
+ logger.debug('Enabling Runtime events')
509
575
  return await self._connection_handler.execute_command(RuntimeCommands.enable())
510
576
 
511
577
  async def disable_runtime_events(self):
512
578
  """Disable runtime events."""
579
+ logger.debug('Disabling Runtime events')
513
580
  return await self._connection_handler.execute_command(RuntimeCommands.disable())
514
581
 
515
582
  async def continue_request(
@@ -524,6 +591,7 @@ class Browser(ABC): # noqa: PLR0904
524
591
  """
525
592
  Continue paused request without modifications.
526
593
  """
594
+ logger.debug(f'Continuing request: id={request_id}')
527
595
  return await self._execute_command(
528
596
  FetchCommands.continue_request(
529
597
  request_id=request_id,
@@ -537,6 +605,7 @@ class Browser(ABC): # noqa: PLR0904
537
605
 
538
606
  async def fail_request(self, request_id: str, error_reason: ErrorReason):
539
607
  """Fail request with error code."""
608
+ logger.debug(f'Failing request: id={request_id}, reason={error_reason}')
540
609
  return await self._execute_command(FetchCommands.fail_request(request_id, error_reason))
541
610
 
542
611
  async def fulfill_request(
@@ -548,6 +617,10 @@ class Browser(ABC): # noqa: PLR0904
548
617
  response_phrase: Optional[str] = None,
549
618
  ):
550
619
  """Fulfill request with response data."""
620
+ logger.debug(
621
+ f'Fulfilling request: id={request_id}, code={response_code}, '
622
+ f'headers={bool(response_headers)}, body={bool(body)}'
623
+ )
551
624
  return await self._execute_command(
552
625
  FetchCommands.fulfill_request(
553
626
  request_id=request_id,
@@ -562,11 +635,13 @@ class Browser(ABC): # noqa: PLR0904
562
635
  def _validate_connection_port(connection_port: Optional[int]):
563
636
  """Validate connection port."""
564
637
  if connection_port and connection_port < 0:
638
+ logger.error(f'Invalid connection port: {connection_port}')
565
639
  raise InvalidConnectionPort()
566
640
 
567
641
  async def _continue_request_callback(self, event: RequestPausedEvent):
568
642
  """Internal callback to continue paused requests."""
569
643
  request_id = event['params']['requestId']
644
+ logger.debug(f'[Fetch] REQUEST_PAUSED -> continue: id={request_id}')
570
645
  return await self.continue_request(request_id)
571
646
 
572
647
  async def _continue_request_with_auth_callback(
@@ -577,6 +652,10 @@ class Browser(ABC): # noqa: PLR0904
577
652
  ):
578
653
  """Internal callback for proxy authentication."""
579
654
  request_id = event['params']['requestId']
655
+ logger.debug(
656
+ f'[Fetch] AUTH_REQUIRED -> provide credentials: id={request_id}, '
657
+ f'user_set={bool(proxy_username)}'
658
+ )
580
659
  response: Response = await self._execute_command(
581
660
  FetchCommands.continue_request_with_auth(
582
661
  request_id,
@@ -592,6 +671,7 @@ class Browser(ABC): # noqa: PLR0904
592
671
  async def _tab_continue_request_callback(event: RequestPausedEvent, tab: Tab):
593
672
  """Internal callback to continue paused requests at Tab level."""
594
673
  request_id = event['params']['requestId']
674
+ logger.debug(f'[Tab Fetch] REQUEST_PAUSED -> continue: id={request_id}')
595
675
  return await tab.continue_request(request_id)
596
676
 
597
677
  @staticmethod
@@ -603,6 +683,10 @@ class Browser(ABC): # noqa: PLR0904
603
683
  ):
604
684
  """Internal callback for proxy/server authentication at Tab level."""
605
685
  request_id = event['params']['requestId']
686
+ logger.debug(
687
+ f'[Tab Fetch] AUTH_REQUIRED -> provide credentials: id={request_id}, '
688
+ f'user_set={bool(proxy_username)}'
689
+ )
606
690
  response: Response = await tab.continue_with_auth(
607
691
  request_id=request_id,
608
692
  auth_challenge_response=AuthChallengeResponseType.PROVIDE_CREDENTIALS,
@@ -622,6 +706,10 @@ class Browser(ABC): # noqa: PLR0904
622
706
  if not creds:
623
707
  return
624
708
  username, password = creds
709
+ logger.debug(
710
+ f'Enabling context-level proxy auth for tab (context={browser_context_id}, '
711
+ f'user_set={bool(username)}'
712
+ )
625
713
  await tab.enable_fetch_events(handle_auth=True)
626
714
  await tab.on(
627
715
  FetchEvent.REQUEST_PAUSED,
@@ -649,29 +737,37 @@ class Browser(ABC): # noqa: PLR0904
649
737
  Raises:
650
738
  FailedToStartBrowser: If the browser failed to start.
651
739
  """
740
+ logger.debug(f'Verifying browser is running (timeout={self.options.start_timeout})')
652
741
  if not await self._is_browser_running(self.options.start_timeout):
742
+ logger.error('Browser failed to start within timeout')
653
743
  raise FailedToStartBrowser()
654
744
 
655
745
  async def _configure_proxy(
656
746
  self, private_proxy: bool, proxy_credentials: tuple[Optional[str], Optional[str]]
657
747
  ):
658
748
  """Setup proxy authentication handling if needed."""
659
- if private_proxy:
660
- await self.enable_fetch_events(handle_auth_requests=True)
661
- await self.on(
662
- FetchEvent.REQUEST_PAUSED,
663
- self._continue_request_callback,
664
- temporary=True,
665
- )
666
- await self.on(
667
- FetchEvent.AUTH_REQUIRED,
668
- partial(
669
- self._continue_request_with_auth_callback,
670
- proxy_username=proxy_credentials[0],
671
- proxy_password=proxy_credentials[1],
672
- ),
673
- temporary=True,
674
- )
749
+ if not private_proxy:
750
+ return
751
+
752
+ logger.debug(
753
+ 'Configuring proxy authentication: '
754
+ f'credentials provided={bool(proxy_credentials[0] or proxy_credentials[1])}'
755
+ )
756
+ await self.enable_fetch_events(handle_auth_requests=True)
757
+ await self.on(
758
+ FetchEvent.REQUEST_PAUSED,
759
+ self._continue_request_callback,
760
+ temporary=True,
761
+ )
762
+ await self.on(
763
+ FetchEvent.AUTH_REQUIRED,
764
+ partial(
765
+ self._continue_request_with_auth_callback,
766
+ proxy_username=proxy_credentials[0],
767
+ proxy_password=proxy_credentials[1],
768
+ ),
769
+ temporary=True,
770
+ )
675
771
 
676
772
  @staticmethod
677
773
  def _is_valid_tab(target: TargetInfo) -> bool:
@@ -696,10 +792,12 @@ class Browser(ABC): # noqa: PLR0904
696
792
  )
697
793
 
698
794
  if not valid_tab:
795
+ logger.error(f'No valid tab found among {len(targets)} targets')
699
796
  raise NoValidTabFound()
700
797
 
701
798
  tab_id = valid_tab.get('targetId')
702
799
  if not tab_id:
800
+ logger.error('Valid tab missing targetId')
703
801
  raise NoValidTabFound('Tab missing targetId')
704
802
 
705
803
  return tab_id
@@ -717,6 +815,7 @@ class Browser(ABC): # noqa: PLR0904
717
815
  self, command: Command[T_CommandParams, T_CommandResponse], timeout: int = 10
718
816
  ) -> T_CommandResponse:
719
817
  """Execute CDP command and return result (core method for browser communication)."""
818
+ logger.debug(f'Executing command: {command.get("method")} (timeout={timeout})')
720
819
  return await self._connection_handler.execute_command(command, timeout=timeout)
721
820
 
722
821
  def _setup_user_dir(self):
@@ -730,6 +829,7 @@ class Browser(ABC): # noqa: PLR0904
730
829
  self.options.arguments.append(f'--user-data-dir={temp_dir.name}')
731
830
  if self.options.browser_preferences:
732
831
  self._set_browser_preferences_in_temp_dir(temp_dir)
832
+ logger.debug(f'User dir setup complete: {self._get_user_data_dir()}')
733
833
 
734
834
  def _set_browser_preferences_in_temp_dir(self, temp_dir: TemporaryDirectory):
735
835
  os.mkdir(os.path.join(temp_dir.name, 'Default'))
@@ -738,6 +838,7 @@ class Browser(ABC): # noqa: PLR0904
738
838
  os.path.join(temp_dir.name, 'Default', 'Preferences'), 'w', encoding='utf-8'
739
839
  ) as json_file:
740
840
  json.dump(preferences, json_file)
841
+ logger.debug('Wrote browser preferences to temp user dir')
741
842
 
742
843
  def _set_browser_preferences_in_user_data_dir(self, user_data_dir: str):
743
844
  """
@@ -769,6 +870,7 @@ class Browser(ABC): # noqa: PLR0904
769
870
  preferences.update(self.options.browser_preferences)
770
871
  with open(preferences_path, 'w', encoding='utf-8') as json_file:
771
872
  json.dump(preferences, json_file, indent=2)
873
+ logger.debug(f'Updated browser preferences in user data dir: {preferences_path}')
772
874
 
773
875
  def _get_user_data_dir(self) -> Optional[str]:
774
876
  for arg in self.options.arguments:
@@ -781,8 +883,10 @@ class Browser(ABC): # noqa: PLR0904
781
883
  """Validate WebSocket address."""
782
884
  min_slashes = 4
783
885
  if not ws_address.startswith('ws://'):
886
+ logger.error('Invalid WebSocket address: missing ws:// prefix')
784
887
  raise InvalidWebSocketAddress('WebSocket address must start with ws://')
785
888
  if len(ws_address.split('/')) < min_slashes:
889
+ logger.error('Invalid WebSocket address: not enough slashes')
786
890
  raise InvalidWebSocketAddress(
787
891
  f'WebSocket address must contain at least {min_slashes} slashes'
788
892
  )
@@ -793,6 +897,7 @@ class Browser(ABC): # noqa: PLR0904
793
897
  self._ws_address = ws_address
794
898
  self._connection_handler._ws_address = self._ws_address
795
899
  await self._connection_handler._ensure_active_connection()
900
+ logger.info('WebSocket address set for browser-level connection')
796
901
 
797
902
  def _get_tab_kwargs(self, target_id: str, browser_context_id: Optional[str] = None) -> dict:
798
903
  """
@@ -815,18 +920,28 @@ class Browser(ABC): # noqa: PLR0904
815
920
  kwargs['ws_address'] = self._get_tab_ws_address(target_id)
816
921
  else:
817
922
  kwargs['connection_port'] = self._connection_port
923
+ logger.debug(f'Tab kwargs resolved for {target_id}: using_ws={bool(self._ws_address)}')
818
924
  return kwargs
819
925
 
820
926
  def _get_tab_ws_address(self, tab_id: str) -> str:
821
927
  """
822
- Get WebSocket address for tab. If tab_id is not provided,
823
- it will be derived from the targets.
928
+ Get WebSocket address for a specific tab, preserving any query or fragment
929
+ components present in the original browser-level WebSocket URL.
930
+
931
+ This ensures authentication tokens passed via query string (e.g.,
932
+ ws://host/devtools/browser/abc?token=XYZ) are retained when switching
933
+ to the page-level endpoint (devtools/page/<tab_id>), which is critical
934
+ for providers like Browserless or authenticated CDP proxies.
824
935
  """
825
936
  if not self._ws_address:
826
937
  raise InvalidWebSocketAddress('WebSocket address is not set')
827
938
 
828
- ws_domain = '/'.join(self._ws_address.split('/')[:3])
829
- return f'{ws_domain}/devtools/page/{tab_id}'
939
+ parts = urlsplit(self._ws_address)
940
+ # Preserve scheme and netloc; build the page path and keep query/fragment
941
+ page_path = f'/devtools/page/{tab_id}'
942
+ ws = urlunsplit((parts.scheme, parts.netloc, page_path, parts.query, parts.fragment))
943
+ logger.debug(f'Resolved tab WebSocket address: {ws}')
944
+ return ws
830
945
 
831
946
  @staticmethod
832
947
  def _sanitize_proxy_and_extract_auth(
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import platform
2
3
  from typing import Optional
3
4
 
@@ -7,6 +8,8 @@ from pydoll.browser.options import ChromiumOptions
7
8
  from pydoll.exceptions import UnsupportedOS
8
9
  from pydoll.utils import validate_browser_paths
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
 
11
14
  class Chrome(Browser):
12
15
  """Chrome browser implementation for CDP automation."""
@@ -39,6 +42,7 @@ class Chrome(Browser):
39
42
  ValueError: If executable not found at default location.
40
43
  """
41
44
  os_name = platform.system()
45
+ logger.debug(f'Resolving default Chrome binary for OS: {os_name}')
42
46
 
43
47
  browser_paths = {
44
48
  'Windows': [
@@ -57,6 +61,9 @@ class Chrome(Browser):
57
61
  browser_path = browser_paths.get(os_name)
58
62
 
59
63
  if not browser_path:
64
+ logger.error(f'Unsupported OS: {os_name}')
60
65
  raise UnsupportedOS(f'Unsupported OS: {os_name}')
61
66
 
62
- return validate_browser_paths(browser_path)
67
+ path = validate_browser_paths(browser_path)
68
+ logger.debug(f'Using Chrome binary: {path}')
69
+ return path
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import platform
2
3
  from typing import Optional
3
4
 
@@ -7,6 +8,8 @@ from pydoll.browser.options import Options
7
8
  from pydoll.exceptions import UnsupportedOS
8
9
  from pydoll.utils import validate_browser_paths
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
 
11
14
  class Edge(Browser):
12
15
  """Edge browser implementation for CDP automation."""
@@ -39,6 +42,7 @@ class Edge(Browser):
39
42
  ValueError: If executable not found at default location.
40
43
  """
41
44
  os_name = platform.system()
45
+ logger.debug(f'Resolving default Edge binary for OS: {os_name}')
42
46
 
43
47
  browser_paths = {
44
48
  'Windows': [
@@ -62,6 +66,9 @@ class Edge(Browser):
62
66
  browser_path = browser_paths.get(os_name)
63
67
 
64
68
  if not browser_path:
69
+ logger.error(f'Unsupported OS: {os_name}')
65
70
  raise UnsupportedOS()
66
71
 
67
- return validate_browser_paths(browser_path)
72
+ path = validate_browser_paths(browser_path)
73
+ logger.debug(f'Using Edge binary: {path}')
74
+ return path