pydoll-python 2.17.0__tar.gz → 2.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/PKG-INFO +1 -28
  2. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/README.md +0 -27
  3. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/mixins/find_elements_mixin.py +101 -52
  4. pydoll_python-2.18.0/pydoll/elements/utils/__init__.py +3 -0
  5. pydoll_python-2.18.0/pydoll/elements/utils/selector_parser.py +448 -0
  6. pydoll_python-2.18.0/pydoll/utils/__init__.py +25 -0
  7. pydoll_python-2.18.0/pydoll/utils/socks5_proxy_forwarder.py +592 -0
  8. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pyproject.toml +1 -1
  9. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/LICENSE +0 -0
  10. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/__init__.py +0 -0
  11. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/__init__.py +0 -0
  12. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/__init__.py +0 -0
  13. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/base.py +0 -0
  14. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/chrome.py +0 -0
  15. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/chromium/edge.py +0 -0
  16. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/interfaces.py +0 -0
  17. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/__init__.py +0 -0
  18. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/browser_options_manager.py +0 -0
  19. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/browser_process_manager.py +0 -0
  20. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/proxy_manager.py +0 -0
  21. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/managers/temp_dir_manager.py +0 -0
  22. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/options.py +0 -0
  23. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/__init__.py +0 -0
  24. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/request.py +0 -0
  25. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/requests/response.py +0 -0
  26. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/browser/tab.py +0 -0
  27. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/__init__.py +0 -0
  28. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/browser_commands.py +0 -0
  29. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/dom_commands.py +0 -0
  30. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/fetch_commands.py +0 -0
  31. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/input_commands.py +0 -0
  32. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/network_commands.py +0 -0
  33. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/page_commands.py +0 -0
  34. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/runtime_commands.py +0 -0
  35. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/storage_commands.py +0 -0
  36. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/commands/target_commands.py +0 -0
  37. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/__init__.py +0 -0
  38. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/connection_handler.py +0 -0
  39. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/__init__.py +0 -0
  40. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/commands_manager.py +0 -0
  41. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/connection/managers/events_manager.py +0 -0
  42. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/constants.py +0 -0
  43. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/decorators.py +0 -0
  44. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/__init__.py +0 -0
  45. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/mixins/__init__.py +0 -0
  46. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/shadow_root.py +0 -0
  47. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/elements/web_element.py +0 -0
  48. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/exceptions.py +0 -0
  49. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/__init__.py +0 -0
  50. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/iframe.py +0 -0
  51. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/keyboard.py +0 -0
  52. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/interactions/scroll.py +0 -0
  53. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/__init__.py +0 -0
  54. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/base.py +0 -0
  55. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/__init__.py +0 -0
  56. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/events.py +0 -0
  57. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/methods.py +0 -0
  58. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/browser/types.py +0 -0
  59. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/debugger/types.py +0 -0
  60. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/__init__.py +0 -0
  61. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/events.py +0 -0
  62. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/methods.py +0 -0
  63. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/dom/types.py +0 -0
  64. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/emulation/types.py +0 -0
  65. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/__init__.py +0 -0
  66. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/events.py +0 -0
  67. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/methods.py +0 -0
  68. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/fetch/types.py +0 -0
  69. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/__init__.py +0 -0
  70. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/events.py +0 -0
  71. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/methods.py +0 -0
  72. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/input/types.py +0 -0
  73. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/io/types.py +0 -0
  74. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/__init__.py +0 -0
  75. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/events.py +0 -0
  76. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/methods.py +0 -0
  77. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/network/types.py +0 -0
  78. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/__init__.py +0 -0
  79. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/events.py +0 -0
  80. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/methods.py +0 -0
  81. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/page/types.py +0 -0
  82. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/__init__.py +0 -0
  83. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/events.py +0 -0
  84. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/methods.py +0 -0
  85. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/runtime/types.py +0 -0
  86. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/security/types.py +0 -0
  87. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/__init__.py +0 -0
  88. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/events.py +0 -0
  89. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/methods.py +0 -0
  90. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/storage/types.py +0 -0
  91. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/__init__.py +0 -0
  92. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/events.py +0 -0
  93. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/methods.py +0 -0
  94. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/protocol/target/types.py +0 -0
  95. {pydoll_python-2.17.0 → pydoll_python-2.18.0}/pydoll/py.typed +0 -0
  96. /pydoll_python-2.17.0/pydoll/utils.py → /pydoll_python-2.18.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.17.0
3
+ Version: 2.18.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
@@ -124,33 +124,6 @@ for sr in shadow_roots:
124
124
  [**📖 Shadow DOM Docs**](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/)
125
125
  </details>
126
126
 
127
- <details>
128
- <summary><b>Cloudflare Turnstile Bypass: Faster, More Robust, Fully Automatic</b></summary>
129
- <br>
130
-
131
- The Cloudflare Turnstile bypass has been **completely rewritten** using shadow root traversal, making it significantly more reliable than the old selector-based approach.
132
-
133
- **What changed:**
134
-
135
- - **Automatic detection** — Pydoll now polls the page's shadow DOM for the Cloudflare challenge domain, no selectors needed
136
- - **Full shadow root traversal** — navigates outer shadow root → cross-origin iframe → inner shadow root → actual checkbox element
137
- - **No more artificial delays** — the old `time_before_click` sleep is gone; the checkbox is clicked as soon as it's found
138
- - **Zero configuration** — just call the method, Pydoll handles the rest
139
-
140
- ```python
141
- async with Chrome() as browser:
142
- tab = await browser.start()
143
-
144
- # That's it — fully automatic
145
- async with tab.expect_and_bypass_cloudflare_captcha():
146
- await tab.go_to('https://site-with-turnstile.com')
147
-
148
- print("Captcha handled!")
149
- ```
150
-
151
- [**📖 Cloudflare Turnstile Docs**](https://pydoll.tech/docs/features/advanced/behavioral-captcha-bypass/)
152
- </details>
153
-
154
127
  <details>
155
128
  <summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
156
129
  <br>
@@ -104,33 +104,6 @@ for sr in shadow_roots:
104
104
  [**📖 Shadow DOM Docs**](https://pydoll.tech/docs/deep-dive/architecture/shadow-dom/)
105
105
  </details>
106
106
 
107
- <details>
108
- <summary><b>Cloudflare Turnstile Bypass: Faster, More Robust, Fully Automatic</b></summary>
109
- <br>
110
-
111
- The Cloudflare Turnstile bypass has been **completely rewritten** using shadow root traversal, making it significantly more reliable than the old selector-based approach.
112
-
113
- **What changed:**
114
-
115
- - **Automatic detection** — Pydoll now polls the page's shadow DOM for the Cloudflare challenge domain, no selectors needed
116
- - **Full shadow root traversal** — navigates outer shadow root → cross-origin iframe → inner shadow root → actual checkbox element
117
- - **No more artificial delays** — the old `time_before_click` sleep is gone; the checkbox is clicked as soon as it's found
118
- - **Zero configuration** — just call the method, Pydoll handles the rest
119
-
120
- ```python
121
- async with Chrome() as browser:
122
- tab = await browser.start()
123
-
124
- # That's it — fully automatic
125
- async with tab.expect_and_bypass_cloudflare_captcha():
126
- await tab.go_to('https://site-with-turnstile.com')
127
-
128
- print("Captcha handled!")
129
- ```
130
-
131
- [**📖 Cloudflare Turnstile Docs**](https://pydoll.tech/docs/features/advanced/behavioral-captcha-bypass/)
132
- </details>
133
-
134
107
  <details>
135
108
  <summary><b>Humanized Keyboard Input (<code>humanize=True</code>)</b></summary>
136
109
  <br>
@@ -10,8 +10,8 @@ from pydoll.commands import (
10
10
  )
11
11
  from pydoll.connection.connection_handler import ConnectionHandler
12
12
  from pydoll.constants import By, Scripts
13
+ from pydoll.elements.utils import SelectorParser
13
14
  from pydoll.exceptions import ElementNotFound, WaitElementTimeout
14
- from pydoll.utils import normalize_synthetic_xpath
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from typing import Literal, Optional, Union
@@ -65,29 +65,7 @@ class FindElementsMixin:
65
65
  """
66
66
  Build JS expression using Scripts to extract textContent based on selector type.
67
67
  """
68
- raw = str(selector)
69
- method_lc = (method or '').lower()
70
-
71
- if 'xpath' in method_lc:
72
- normalized_xpath = normalize_synthetic_xpath(raw)
73
- escaped_xpath = normalized_xpath.replace('"', '\\"')
74
- return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', escaped_xpath)
75
-
76
- if method_lc == 'name':
77
- escaped_name = raw.replace('"', '\\"')
78
- xpath = f'//*[@name="{escaped_name}"]'
79
- return Scripts.GET_TEXT_BY_XPATH.replace('{escaped_value}', xpath)
80
-
81
- escaped = raw.replace('\\', '\\\\').replace('"', '\\"')
82
- if method_lc == 'id':
83
- css = f'#{escaped}'
84
- elif method_lc == 'class_name':
85
- css = f'.{escaped}'
86
- elif method_lc == 'tag_name':
87
- css = escaped
88
- else:
89
- css = escaped
90
- return Scripts.GET_TEXT_BY_CSS.replace('{selector}', css)
68
+ return SelectorParser.build_text_expression(selector, method)
91
69
 
92
70
  @overload
93
71
  async def find(
@@ -344,6 +322,17 @@ class FindElementsMixin:
344
322
  f'find_or_wait_element(): by={by}, value={value}, timeout={timeout}, '
345
323
  f'find_all={find_all}, raise_exc={raise_exc}'
346
324
  )
325
+
326
+ if by == By.XPATH:
327
+ segments = SelectorParser.parse_iframe_segments_xpath(value)
328
+ elif by == By.CSS_SELECTOR:
329
+ segments = SelectorParser.parse_iframe_segments_css(value)
330
+ else:
331
+ segments = [(by, value)]
332
+
333
+ if len(segments) > 1:
334
+ return await self._find_across_iframes(segments, timeout, find_all, raise_exc)
335
+
347
336
  find_method = self._find_element if not find_all else self._find_elements
348
337
  start_time = asyncio.get_event_loop().time()
349
338
 
@@ -371,6 +360,90 @@ class FindElementsMixin:
371
360
 
372
361
  await asyncio.sleep(0.5)
373
362
 
363
+ async def _find_across_iframes(
364
+ self,
365
+ segments: list[tuple[By, str]],
366
+ timeout: int,
367
+ find_all: bool,
368
+ raise_exc: bool,
369
+ ) -> Union[WebElement, list[WebElement], None]:
370
+ """
371
+ Retry loop for iframe-crossing element searches.
372
+
373
+ Repeatedly calls :meth:`_attempt_find_across_iframes` until the target
374
+ element is found or the *timeout* expires.
375
+
376
+ Args:
377
+ segments: Ordered ``(By, selector)`` pairs — one per iframe boundary
378
+ plus a final selector for the target element(s).
379
+ timeout: Maximum seconds to wait (0 = single attempt).
380
+ find_all: If ``True``, the last segment uses ``_find_elements``.
381
+ raise_exc: Whether to raise on failure.
382
+
383
+ Returns:
384
+ The found element(s), or ``None`` / ``[]`` on failure.
385
+
386
+ Raises:
387
+ ElementNotFound: If ``timeout=0``, nothing found, and ``raise_exc=True``.
388
+ WaitElementTimeout: If timeout expires and ``raise_exc=True``.
389
+ """
390
+ start_time = asyncio.get_event_loop().time()
391
+ selector_repr = ' -> '.join(seg for _, seg in segments)
392
+
393
+ while True:
394
+ result = await self._attempt_find_across_iframes(segments, find_all)
395
+ if result is not None and result != []:
396
+ return result
397
+
398
+ if not timeout:
399
+ if raise_exc:
400
+ raise ElementNotFound(f'Element not found across iframes: {selector_repr}')
401
+ return [] if find_all else None
402
+
403
+ if asyncio.get_event_loop().time() - start_time > timeout:
404
+ if raise_exc:
405
+ raise WaitElementTimeout(
406
+ f'Timed out after {timeout}s waiting for element '
407
+ f'across iframes: {selector_repr}'
408
+ )
409
+ return [] if find_all else None
410
+
411
+ await asyncio.sleep(0.5)
412
+
413
+ async def _attempt_find_across_iframes(
414
+ self,
415
+ segments: list[tuple[By, str]],
416
+ find_all: bool,
417
+ ) -> Union[WebElement, list[WebElement], None]:
418
+ """
419
+ Single attempt to walk iframe segments and find the target element.
420
+
421
+ For each intermediate segment, finds a single iframe element and uses it
422
+ as the search context for the next segment. The last segment respects
423
+ *find_all*.
424
+
425
+ Args:
426
+ segments: Ordered ``(By, selector)`` pairs.
427
+ find_all: Whether the final segment should return all matches.
428
+
429
+ Returns:
430
+ Found element(s) or ``None`` / ``[]`` if any intermediate step fails.
431
+ """
432
+ current_context: FindElementsMixin = self
433
+ for i, (by, selector) in enumerate(segments):
434
+ is_last = i == len(segments) - 1
435
+ if is_last:
436
+ if find_all:
437
+ result = await current_context._find_elements(by, selector, raise_exc=False)
438
+ return result if result else []
439
+ return await current_context._find_element(by, selector, raise_exc=False)
440
+
441
+ element = await current_context._find_element(by, selector, raise_exc=False)
442
+ if not element or not getattr(element, 'is_iframe', False):
443
+ return None
444
+ current_context = element
445
+ return None
446
+
374
447
  async def _find_element(
375
448
  self, by: By, value: str, raise_exc: bool = True
376
449
  ) -> Optional[WebElement]:
@@ -568,7 +641,7 @@ class FindElementsMixin:
568
641
  name: Optional[str] = None,
569
642
  tag_name: Optional[str] = None,
570
643
  text: Optional[str] = None,
571
- **attributes,
644
+ **attributes: str,
572
645
  ) -> str:
573
646
  """
574
647
  Build XPath expression from multiple attribute criteria.
@@ -581,28 +654,7 @@ class FindElementsMixin:
581
654
  Attribute names with underscores are automatically converted to hyphens
582
655
  to match HTML attribute naming conventions (e.g., data_test -> data-test).
583
656
  """
584
- xpath_conditions = []
585
- base_xpath = f'//{tag_name}' if tag_name else '//*'
586
- if id:
587
- xpath_conditions.append(f'@id="{id}"')
588
- if class_name:
589
- xpath_conditions.append(
590
- f'contains(concat(" ", normalize-space(@class), " "), " {class_name} ")'
591
- )
592
- if name:
593
- xpath_conditions.append(f'@name="{name}"')
594
- if text:
595
- xpath_conditions.append(f'contains(text(), "{text}")')
596
- for attribute, value in attributes.items():
597
- # Convert underscores to hyphens for HTML attribute names
598
- html_attribute = attribute.replace('_', '-')
599
- xpath_conditions.append(f'@{html_attribute}="{value}"')
600
-
601
- xpath = (
602
- f'{base_xpath}[{" and ".join(xpath_conditions)}]' if xpath_conditions else base_xpath
603
- )
604
- logger.debug(f'_build_xpath() -> {xpath}')
605
- return xpath
657
+ return SelectorParser.build_xpath(id, class_name, name, tag_name, text, **attributes)
606
658
 
607
659
  @staticmethod
608
660
  def _get_expression_type(expression: str) -> By:
@@ -613,10 +665,7 @@ class FindElementsMixin:
613
665
  - XPath: starts with ./, or /
614
666
  - Default: CSS_SELECTOR
615
667
  """
616
- if expression.startswith(('./', '/', '(/')):
617
- return By.XPATH
618
-
619
- return By.CSS_SELECTOR
668
+ return SelectorParser.get_expression_type(expression)
620
669
 
621
670
  async def _describe_node(self, object_id: str = '') -> Node:
622
671
  """
@@ -834,7 +883,7 @@ class FindElementsMixin:
834
883
 
835
884
  Converts absolute XPath to relative for context-based searches.
836
885
  """
837
- return f'.{xpath}' if not xpath.startswith('.') else xpath
886
+ return SelectorParser.ensure_relative_xpath(xpath)
838
887
 
839
888
  @staticmethod
840
889
  def _has_object_id_key(response: Union[EvaluateResponse, CallFunctionOnResponse]) -> bool:
@@ -0,0 +1,3 @@
1
+ from pydoll.elements.utils.selector_parser import SelectorParser
2
+
3
+ __all__ = ['SelectorParser']