py4quality 0.1.0__py3-none-any.whl

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 (59) hide show
  1. py4quality-0.1.0.dist-info/METADATA +129 -0
  2. py4quality-0.1.0.dist-info/RECORD +59 -0
  3. py4quality-0.1.0.dist-info/WHEEL +5 -0
  4. py4quality-0.1.0.dist-info/top_level.txt +1 -0
  5. pyquality/__init__.py +15 -0
  6. pyquality/api.py +59 -0
  7. pyquality/browser.py +179 -0
  8. pyquality/cdp_driver.py +126 -0
  9. pyquality/domains/__init__.py +50 -0
  10. pyquality/domains/accessibility.py +114 -0
  11. pyquality/domains/animation.py +96 -0
  12. pyquality/domains/audits.py +42 -0
  13. pyquality/domains/autofill.py +45 -0
  14. pyquality/domains/backgroundservice.py +42 -0
  15. pyquality/domains/bluetoothemulation.py +171 -0
  16. pyquality/domains/browser.py +242 -0
  17. pyquality/domains/cachestorage.py +72 -0
  18. pyquality/domains/cast.py +57 -0
  19. pyquality/domains/css.py +409 -0
  20. pyquality/domains/deviceaccess.py +38 -0
  21. pyquality/domains/deviceorientation.py +24 -0
  22. pyquality/domains/dom.py +641 -0
  23. pyquality/domains/domdebugger.py +113 -0
  24. pyquality/domains/domsnapshot.py +61 -0
  25. pyquality/domains/domstorage.py +60 -0
  26. pyquality/domains/emulation.py +610 -0
  27. pyquality/domains/eventbreakpoints.py +29 -0
  28. pyquality/domains/extensions.py +94 -0
  29. pyquality/domains/fedcm.py +77 -0
  30. pyquality/domains/fetch.py +147 -0
  31. pyquality/domains/filesystem.py +13 -0
  32. pyquality/domains/headlessexperimental.py +43 -0
  33. pyquality/domains/indexeddb.py +169 -0
  34. pyquality/domains/input.py +327 -0
  35. pyquality/domains/inspector.py +18 -0
  36. pyquality/domains/io.py +38 -0
  37. pyquality/domains/layertree.py +99 -0
  38. pyquality/domains/log.py +41 -0
  39. pyquality/domains/media.py +18 -0
  40. pyquality/domains/memory.py +91 -0
  41. pyquality/domains/network.py +518 -0
  42. pyquality/domains/overlay.py +332 -0
  43. pyquality/domains/page.py +738 -0
  44. pyquality/domains/performance.py +37 -0
  45. pyquality/domains/performancetimeline.py +17 -0
  46. pyquality/domains/preload.py +18 -0
  47. pyquality/domains/pwa.py +94 -0
  48. pyquality/domains/security.py +47 -0
  49. pyquality/domains/serviceworker.py +120 -0
  50. pyquality/domains/smartcardemulation.py +134 -0
  51. pyquality/domains/storage.py +377 -0
  52. pyquality/domains/systeminfo.py +27 -0
  53. pyquality/domains/target.py +280 -0
  54. pyquality/domains/tethering.py +22 -0
  55. pyquality/domains/tracing.py +86 -0
  56. pyquality/domains/webaudio.py +27 -0
  57. pyquality/domains/webauthn.py +154 -0
  58. pyquality/domains/webmcp.py +11 -0
  59. pyquality/generate_cdp_wrappers.py +83 -0
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: py4quality
3
+ Version: 0.1.0
4
+ Summary: A highly user-friendly, kid-friendly automation framework using Chrome DevTools Protocol (CDP).
5
+ Author: Venkata Santosh Tharigoppala
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/santoshtvk-new/Pyquality
8
+ Project-URL: Bug Tracker, https://github.com/santoshtvk-new/Pyquality/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: websocket-client>=1.6.1
12
+ Requires-Dist: requests>=2.31.0
13
+
14
+ # pyquality 🎈
15
+
16
+ A kid-friendly, yet exceptionally powerful automation framework for Python driven directly by the Chrome DevTools Protocol (CDP).
17
+
18
+ `pyquality` is designed to be as simple as possible. Want to close the browser? Just call `b.bye()`. Want to navigate? `b.go_to("https://...")`
19
+
20
+ Under the hood, it retains uncompromising power. Advanced users have **unfiltered access** to all 40+ raw Chromium DevTools domains (DOM, Network, Page, etc.) auto-generated from the official Google spec!
21
+
22
+ ---
23
+
24
+ ## 🚀 Installation (Source)
25
+ If you are developing locally:
26
+ ```bash
27
+ git clone https://github.com/santoshtvk-new/Pyquality.git
28
+ cd pyquality
29
+ pip install -e .
30
+ ```
31
+
32
+ ## 🛠️ Installation (PIP)
33
+ *(When published to PyPi)*
34
+ ```bash
35
+ pip install pyquality
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 📖 Quick Start
41
+
42
+ ```python
43
+ from pyquality.browser import Browser
44
+
45
+ # 1. Start it up
46
+ b = Browser(headless=False)
47
+
48
+ # 2. Go somewhere
49
+ b.go_to("https://example.com")
50
+
51
+ # 3. Validation is easy
52
+ b.should_be_seen("h1")
53
+
54
+ # 4. Say goodbye
55
+ b.bye()
56
+ ```
57
+
58
+ ## ✨ The Kid-Friendly Aliases
59
+
60
+ `pyquality` dynamically binds methods defined in `METHOD_ALIASES.yml` so you can use vocabulary that makes sense to you.
61
+
62
+ ### Navigation
63
+ - `navigate(url)` -> `go_to`, `open_website`
64
+ - `reload()` -> `refresh`, `reload_page`
65
+ - `close()` -> `close_browser`, `bye`
66
+ - `go_back()` -> `step_back`, `previous_page`
67
+ - `go_forward()` -> `step_forward`, `next_page`
68
+
69
+ ### Interaction
70
+ - `click_element(selector)` -> `click_this`, `tap`
71
+ - `type_text(selector, text)` -> `type_in`, `write_this`
72
+ - `hover_element(selector)` -> `point_at`, `hover_over`
73
+
74
+ ### APIs & Data
75
+ - `API.get(endpoint)` -> `fetch_data`, `grab`
76
+ - `API.post(endpoint, json)` -> `create_data`, `send_new`
77
+
78
+ ---
79
+
80
+ ## ⚡ Raw CDP Power (Advanced)
81
+
82
+ If you need capabilities beyond the simple wrappers, `pyquality` has raw auto-generated bindings for **EVERY** CDP Domain. They are attached natively to the browser instance:
83
+
84
+ ```python
85
+ from pyquality.browser import Browser
86
+ b = Browser()
87
+
88
+ # Use raw 'Page' domain to capture a PDF
89
+ b.page.printToPDF()
90
+
91
+ # Use raw 'Network' domain to throttle connection
92
+ b.network.emulateNetworkConditions(
93
+ offline=False,
94
+ latency=200,
95
+ downloadThroughput=1000,
96
+ uploadThroughput=1000
97
+ )
98
+
99
+ # Use raw 'DOM' domain to get node information
100
+ b.dom.getDocument(depth=-1)
101
+ ```
102
+
103
+ ## 📦 Deploying Initial PIP Package
104
+
105
+ To package this framework and upload it to PyPi:
106
+
107
+ 1. **Install build tools:**
108
+ ```bash
109
+ pip install build twine
110
+ ```
111
+
112
+ 2. **Generate Native Wrappers:**
113
+ Ensure your CDP protocol bindings are up-to-date:
114
+ ```bash
115
+ python src/pyquality/generate_cdp_wrappers.py
116
+ ```
117
+
118
+ 3. **Build wheels:**
119
+ ```bash
120
+ python -m build
121
+ ```
122
+ *This generates `.tar.gz` and `.whl` files in the `/dist` folder.*
123
+
124
+ 4. **Upload to PyPi (Requires Account):**
125
+ ```bash
126
+ python -m twine upload dist/*
127
+ ```
128
+
129
+ 5. **Done!** Users can now run `pip install pyquality`.
@@ -0,0 +1,59 @@
1
+ pyquality/__init__.py,sha256=HHsujZB71SvZ6QlCT31aQqoCsPFVPmst04O2k7s1Wi4,416
2
+ pyquality/api.py,sha256=Nvn9anJpojGKgY-K9nxDAE1_PhRFtl9gYgY0Tm3rCuU,2272
3
+ pyquality/browser.py,sha256=IcPp2bI0df7QvRct67lZfucGHsX5XIZ8gqKcwSBEPOs,7144
4
+ pyquality/cdp_driver.py,sha256=akVj_sjzqKsmFaV7xBB8641Z3LoE3PrfrpLB7RNetJg,4097
5
+ pyquality/generate_cdp_wrappers.py,sha256=evf7QgYjZpPhMaegZ1h2mq_dujSGvBgoXhwSTD8-DSA,3087
6
+ pyquality/domains/__init__.py,sha256=0gsq3rF99-4uvgjRwdXxiTsaawEVMuBaAI1v4se-yEg,1992
7
+ pyquality/domains/accessibility.py,sha256=WgpLadf9olN5W0nwsGb3xxk2RcilkJstq1CJnwAL5Nw,5674
8
+ pyquality/domains/animation.py,sha256=QgY4iAVmvZe-FeK_s6SfT5jVnqWTIH5-I2CWP2gu7BQ,3428
9
+ pyquality/domains/audits.py,sha256=cEq8azHFw0FQE-5atj-zjXSWmNE8oIWjYEbhVvqKiV8,1731
10
+ pyquality/domains/autofill.py,sha256=PEHOpaEDsQk9XbomFlcecD1AtRB1yeAlyhv_bMvkDwo,1777
11
+ pyquality/domains/backgroundservice.py,sha256=CPfqpUnU6weiDeSIuSkgfhuAfFKRjmIt1DM3pKD0LMo,1346
12
+ pyquality/domains/bluetoothemulation.py,sha256=x39bOCFAYX2OGw44MvOt0ELjlJ1y8anTl90KcbItCNU,7369
13
+ pyquality/domains/browser.py,sha256=UiYFvDyU7q128bE0mPHei6sybexXBxWOeOHlCMhRMec,10732
14
+ pyquality/domains/cachestorage.py,sha256=fh-cYMbAvJgg9EATy48995m_sXBiVzic_7Pk9flJdz4,2963
15
+ pyquality/domains/cast.py,sha256=uET2TgiHgib-Axs4cNv_SDyZV2sm3UQmHso-BvZybhg,2118
16
+ pyquality/domains/css.py,sha256=nyUOq6WXhjyY41w2IMbJHAaKGjnZ0UBxOpTmPC00ZWg,18042
17
+ pyquality/domains/deviceaccess.py,sha256=Re_OieoKFAfbYGk1C-M-amaCkrfUuKi0AakQ-mbUc8Q,1144
18
+ pyquality/domains/deviceorientation.py,sha256=4Med9HC--h4BVIsCCK7jhSioB5i8djliXGxYkj5iAIo,818
19
+ pyquality/domains/dom.py,sha256=1gXgLS1tdeGW1q88dFD6rIHSGVzq8kc_CIqR2KTkffU,28498
20
+ pyquality/domains/domdebugger.py,sha256=IkvlRlll_Y-IBDDPoyEIR-tzceb4IMqewy0-IfA4Wmc,4733
21
+ pyquality/domains/domsnapshot.py,sha256=4Fr5QrxjfwldqoSBMlAhLqfE2p4zV6HgbdOre7hRcVk,3907
22
+ pyquality/domains/domstorage.py,sha256=oky4QBdftbV6f9mBWwAVev7_-_83lObruyXDAQUTQaw,1894
23
+ pyquality/domains/emulation.py,sha256=0ks1WIVNQKW00Az9wm8s-QEoOFtjABahjseyegj2aTs,29839
24
+ pyquality/domains/eventbreakpoints.py,sha256=KrP66M00M5rw0bX0F9prbNyVAZnQKM_PlwP6UJxNnJk,1022
25
+ pyquality/domains/extensions.py,sha256=xReADdABrXOpaemW3CHeLZtuX23AyNMDsCWDgoKxdTE,4285
26
+ pyquality/domains/fedcm.py,sha256=Q6wPtAgARce8V1u50Xx2Xv7rjRh770mrPCYOtvdPGzY,2720
27
+ pyquality/domains/fetch.py,sha256=jq1W9AZQyTZINzzd25j5PB7aMJQruI1yb-CHY56BUWU,8677
28
+ pyquality/domains/filesystem.py,sha256=DkKcf7blP5HsqRX2cB3CNAQuQh1aDMSWfzi8gkK3Tnw,423
29
+ pyquality/domains/headlessexperimental.py,sha256=L5Nq0FNIvGYkDZA5DO4knrg5VjHNwaGvZzzYhQ34NVQ,2391
30
+ pyquality/domains/indexeddb.py,sha256=6BBCAlJZlh5i2tni96ybmQGcLnshaFlKTakwPdrmI6U,8036
31
+ pyquality/domains/input.py,sha256=wAJrOZNs2dpoPDxi9sYYffKo_op8VTKzPRefvMJ-f4Q,17963
32
+ pyquality/domains/inspector.py,sha256=XV0paJ_oowB8r3NZmGndoOr7TUwCVDSD1l0JsFPbrzM,480
33
+ pyquality/domains/io.py,sha256=CrDgCBmfskWESloabCLSylX99T7EHqo4kUQsw_hapY4,1446
34
+ pyquality/domains/layertree.py,sha256=bp6oqTz0_cpRrlN0xQ6wPy842ufJ76kqsWlN6IDIh1Q,3940
35
+ pyquality/domains/log.py,sha256=PkexU_T_FUHw887KxAoaiR8cZ1TeazFiESU4jwHLMmg,1247
36
+ pyquality/domains/media.py,sha256=w8G38lEZh1qhjGzzAf4e2Eskvm0n8U8wstxfalvrrKk,439
37
+ pyquality/domains/memory.py,sha256=BPeksrnBFBcSp2z5h6XLB6-hpi0BNZn5eajcVvp3Iew,3566
38
+ pyquality/domains/network.py,sha256=-dQs_YDwpd_a11HY742VJvPLuC_rFF5yaYr1tXkl5Lk,26817
39
+ pyquality/domains/overlay.py,sha256=sZvHRb1cXbbZW26BEUouv6xNIdwVzQhcU52ambKCOVY,14918
40
+ pyquality/domains/page.py,sha256=kPiVD9L3rObkWcA4S1RCuZWUl0s7oQOseiHgLcI9Gv0,33744
41
+ pyquality/domains/performance.py,sha256=y6A1HgEEZRJBXUQXW0oPzi4Zq0pW2FPc0uhBVZlp4lc,1375
42
+ pyquality/domains/performancetimeline.py,sha256=GPtXkjdgVnsRIHPLBoOI1dw3Md_nDODDxNufdsbQNtk,742
43
+ pyquality/domains/preload.py,sha256=XaZeI9JAvojjnYFElBcSrKdQWswRH-6gbT3Pxx-ZbPM,434
44
+ pyquality/domains/pwa.py,sha256=Redp4yZSbFrM4dz4uJ2bjSTSSwSLrjuUSz9i73MC5Zw,5971
45
+ pyquality/domains/security.py,sha256=a9i2beL_Q-d-dQdYol99eUCPDDLAIyunoT46aooVMh4,1835
46
+ pyquality/domains/serviceworker.py,sha256=0s0PQrGY1ddHqDebHdXcsl4yKOIFxVKsPtLoKoK2ebE,3927
47
+ pyquality/domains/smartcardemulation.py,sha256=ktRgNAMeq2ISxsqV2bHaT4PPyo9Meq4_Zf1wVGMCeaM,8136
48
+ pyquality/domains/storage.py,sha256=B1QzqZPAUt7BP5PSM2XtiKDCzleImz7OGROcndC5Mc8,15233
49
+ pyquality/domains/systeminfo.py,sha256=Lh93Ndor1h6NygtFGdSkoqc6tNlIuxGebZ6IlD4uzEk,819
50
+ pyquality/domains/target.py,sha256=QcBHgJJdm4oXlYq71zh8TDeg_oIa57gn-uXqOFmDDMM,14564
51
+ pyquality/domains/tethering.py,sha256=Lw9zOYAHC3dna0yBDpgv2iAU6nc2wrvYA69fCDEWmW0,627
52
+ pyquality/domains/tracing.py,sha256=VXo1Qp_861RkFyc25wMH2aihHCDWhaO5Wypf3UjSCjM,3908
53
+ pyquality/domains/webaudio.py,sha256=wJGBgK3mPsRdJbI_kWly03odOQC4P34cEVXdFbLSWIk,804
54
+ pyquality/domains/webauthn.py,sha256=euR3K3EDQRyhS4_khiOJYosoiWhE0x8qb3yrEkt8AHE,6487
55
+ pyquality/domains/webmcp.py,sha256=LwopI6ctyDGwjfigaHH-vq6URQQiaR5PwNHPbAJ2L_c,380
56
+ py4quality-0.1.0.dist-info/METADATA,sha256=odRepNz5yaegFHhPoJx2Seqc4Tga4HdUevIgOYVBwgo,3514
57
+ py4quality-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
58
+ py4quality-0.1.0.dist-info/top_level.txt,sha256=Ee7HP6bjyNHs6rDI1Pp21yQ8sBDrr47snG9LhORbtuk,10
59
+ py4quality-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyquality
pyquality/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ import yaml
2
+ import os
3
+
4
+ def load_aliases():
5
+ alias_path = os.path.join(os.path.dirname(__file__), "..", "..", "METHOD_ALIASES.yml")
6
+ if not os.path.exists(alias_path):
7
+ return {}
8
+ with open(alias_path, 'r') as f:
9
+ return yaml.safe_load(f)
10
+
11
+ # The loaded aliases can be used by the browser instance to dynamically map methods.
12
+ ALIASES = load_aliases()
13
+
14
+ from .api import API
15
+ __version__ = "0.1.0"
pyquality/api.py ADDED
@@ -0,0 +1,59 @@
1
+ import requests
2
+
3
+ class API:
4
+ """
5
+ Kid-friendly wrapper for REST API calls.
6
+ """
7
+ def __init__(self, base_url=""):
8
+ self.base_url = base_url
9
+ self.session = requests.Session()
10
+ self._apply_aliases()
11
+
12
+ def _apply_aliases(self):
13
+ """Bind kid-friendly aliases to instance methods dynamically."""
14
+ from .__init__ import ALIASES
15
+ if "api" in ALIASES:
16
+ for method_name, aliases in ALIASES["api"].items():
17
+ if hasattr(self, method_name):
18
+ func = getattr(self, method_name)
19
+ for alias in aliases:
20
+ setattr(self, alias, func)
21
+
22
+ def _build_url(self, endpoint):
23
+ if endpoint.startswith("http"):
24
+ return endpoint
25
+ return f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
26
+
27
+ def set_header(self, key, value):
28
+ """Set a header for subsequent requests."""
29
+ self.session.headers.update({key: value})
30
+
31
+ def use_token(self, token):
32
+ """Authenticate with a Bearer token."""
33
+ self.set_header("Authorization", f"Bearer {token}")
34
+
35
+ def get(self, endpoint, **kwargs):
36
+ """Send a GET request."""
37
+ return self.session.get(self._build_url(endpoint), **kwargs)
38
+
39
+ def post(self, endpoint, json=None, data=None, **kwargs):
40
+ """Send a POST request."""
41
+ return self.session.post(self._build_url(endpoint), json=json, data=data, **kwargs)
42
+
43
+ def put(self, endpoint, json=None, data=None, **kwargs):
44
+ """Send a PUT request."""
45
+ return self.session.put(self._build_url(endpoint), json=json, data=data, **kwargs)
46
+
47
+ def delete(self, endpoint, **kwargs):
48
+ """Send a DELETE request."""
49
+ return self.session.delete(self._build_url(endpoint), **kwargs)
50
+
51
+ def patch(self, endpoint, json=None, data=None, **kwargs):
52
+ """Send a PATCH request."""
53
+ return self.session.patch(self._build_url(endpoint), json=json, data=data, **kwargs)
54
+
55
+ def should_be_status(self, response, expected_status):
56
+ """Kid-friendly assertion for status codes."""
57
+ if response.status_code != expected_status:
58
+ raise AssertionError(f"Expected status {expected_status}, but got {response.status_code}.")
59
+ return True
pyquality/browser.py ADDED
@@ -0,0 +1,179 @@
1
+ from .cdp_driver import CDPDriver
2
+ from .__init__ import ALIASES
3
+ from . import domains
4
+
5
+ class Browser:
6
+ """
7
+ Kid-friendly wrapper around CDPDriver.
8
+ This class can dynamically expose methods mapped by aliases.
9
+ """
10
+ def __init__(self, headless=False):
11
+ self.driver = CDPDriver()
12
+ self.driver.start_chrome(headless=headless)
13
+ self.driver.connect()
14
+ # Enable network tracking
15
+ self.driver.execute_and_wait("Network.enable")
16
+
17
+ # Initialize native CDP domains dynamically
18
+ for attr_name in dir(domains):
19
+ if attr_name.endswith("Domain"):
20
+ domain_class = getattr(domains, attr_name)
21
+ # Ensure the property name doesn't collide with existing class/instance vars like `browser` -> `cdp_browser`
22
+ prop_name = attr_name[:-6].lower()
23
+ if prop_name == "browser":
24
+ prop_name = "cdp_browser"
25
+
26
+ setattr(self, prop_name, domain_class(self.driver))
27
+
28
+ self._apply_aliases()
29
+
30
+ def _apply_aliases(self):
31
+ """Bind kid-friendly aliases to instance methods dynamically."""
32
+ for category, methods in ALIASES.items():
33
+ for method_name, aliases in methods.items():
34
+ if hasattr(self, method_name):
35
+ func = getattr(self, method_name)
36
+ for alias in aliases:
37
+ setattr(self, alias, func)
38
+
39
+ # ===============
40
+ # CORE METHODS
41
+ # ===============
42
+ def navigate(self, url):
43
+ """Navigate to a URL."""
44
+ response = self.driver.execute_and_wait("Page.navigate", {"url": url})
45
+ return response
46
+
47
+ def reload(self):
48
+ """Reload the current page using raw CDP."""
49
+ return self.page.reload()
50
+
51
+ def go_back(self):
52
+ """Navigate backward in history."""
53
+ return self.evaluate_js("window.history.back()")
54
+
55
+ def go_forward(self):
56
+ """Navigate forward in history."""
57
+ return self.evaluate_js("window.history.forward()")
58
+
59
+ def get_title(self):
60
+ """Get page title."""
61
+ res = self.evaluate_js("document.title")
62
+ return res.get('result', {}).get('result', {}).get('value', '')
63
+
64
+ def get_url(self):
65
+ """Get current URL."""
66
+ res = self.evaluate_js("window.location.href")
67
+ return res.get('result', {}).get('result', {}).get('value', '')
68
+
69
+ def evaluate_js(self, expression):
70
+ """Evaluate JS in the browser."""
71
+ response = self.driver.execute_and_wait("Runtime.evaluate", {
72
+ "expression": expression,
73
+ "returnByValue": True
74
+ })
75
+ return response
76
+
77
+ def find_element(self, selector):
78
+ """Check if element exists."""
79
+ res = self.evaluate_js(f"document.querySelector('{selector}') !== null")
80
+ return res.get('result', {}).get('result', {}).get('value', False)
81
+
82
+ def click_element(self, selector):
83
+ """Click an element using JS."""
84
+ return self.evaluate_js(f"document.querySelector('{selector}').click()")
85
+
86
+ def type_text(self, selector, text):
87
+ """Type text into an input."""
88
+ # Escape quotes in text
89
+ safe_text = text.replace("'", "\\'").replace('"', '\\"')
90
+ return self.evaluate_js(
91
+ f"document.querySelector('{selector}').value = '{safe_text}';"
92
+ f"document.querySelector('{selector}').dispatchEvent(new Event('input'));"
93
+ )
94
+
95
+ def hover_element(self, selector):
96
+ """Hover over an element (dispatches mouseover/mouseenter events)."""
97
+ return self.evaluate_js(
98
+ f"var el = document.querySelector('{selector}');"
99
+ f"if (el) {{ el.dispatchEvent(new MouseEvent('mouseenter', {{bubbles: true}}));"
100
+ f"el.dispatchEvent(new MouseEvent('mouseover', {{bubbles: true}})); }}"
101
+ )
102
+
103
+ # ===============
104
+ # DISCOVERY & VALIDATION
105
+ # ===============
106
+ def wait_for_selector(self, selector, timeout=10):
107
+ """Wait until an element appears in the DOM."""
108
+ import time
109
+ start = time.time()
110
+ while time.time() - start < timeout:
111
+ if self.find_element(selector):
112
+ return True
113
+ time.sleep(0.5)
114
+ raise Exception(f"Timeout waiting for selector: {selector}")
115
+
116
+ def assert_visible(self, selector):
117
+ """Check if an element is visible (naive check for presence and display structure)."""
118
+ res = self.evaluate_js(
119
+ f"var el = document.querySelector('{selector}');"
120
+ f"el ? (el.offsetWidth > 0 && el.offsetHeight > 0 && getComputedStyle(el).display !== 'none') : false"
121
+ )
122
+ is_vis = res.get('result', {}).get('result', {}).get('value', False)
123
+ if not is_vis:
124
+ raise AssertionError(f"Element {selector} is not visible.")
125
+ return True
126
+
127
+ def assert_text(self, selector, expected_text):
128
+ """Check if an element contains expected text."""
129
+ res = self.evaluate_js(f"var el = document.querySelector('{selector}'); el ? el.innerText : ''")
130
+ actual_text = res.get('result', {}).get('result', {}).get('value', '')
131
+ if expected_text not in actual_text:
132
+ raise AssertionError(f"Expected text '{expected_text}' not found in '{actual_text}'.")
133
+ return True
134
+
135
+ # ===============
136
+ # ADVANCED
137
+ # ===============
138
+ def take_screenshot(self, filename="screenshot.png"):
139
+ """Take a full page screenshot."""
140
+ import base64
141
+ response = self.driver.execute_and_wait("Page.captureScreenshot")
142
+ data = response.get('result', {}).get('data', '')
143
+ if data:
144
+ with open(filename, "wb") as f:
145
+ f.write(base64.b64decode(data))
146
+ return filename
147
+
148
+ def get_network_logs(self):
149
+ """Return all network events captured."""
150
+ return self.driver.events
151
+
152
+ def get_accessibility_tree(self):
153
+ """Fetch the accessibility tree for the page."""
154
+ response = self.driver.execute_and_wait("Accessibility.getFullAXTree")
155
+ return response.get('result', {}).get('nodes', [])
156
+
157
+ def get_cookies(self):
158
+ """Get all browser cookies via Network domain."""
159
+ return self.network.getCookies().get('result', {}).get('cookies', [])
160
+
161
+ def clear_cookies(self):
162
+ """Clear all browser cookies via Network domain."""
163
+ return self.network.clearBrowserCookies()
164
+
165
+ def set_viewport(self, width, height, is_mobile=False):
166
+ """Emulate device metrics / viewport."""
167
+ return self.emulation.setDeviceMetricsOverride(width=width, height=height, deviceScaleFactor=3 if is_mobile else 1, mobile=is_mobile)
168
+
169
+ def act_like_phone(self):
170
+ """Emulate a generic phone viewport."""
171
+ return self.set_viewport(375, 812, is_mobile=True)
172
+
173
+ def set_location(self, latitude, longitude, accuracy=100):
174
+ """Emulate geographical location."""
175
+ return self.emulation.setGeolocationOverride(latitude=latitude, longitude=longitude, accuracy=accuracy)
176
+
177
+ def close(self):
178
+ """Close the browser."""
179
+ self.driver.stop_chrome()
@@ -0,0 +1,126 @@
1
+ import json
2
+ import websocket
3
+ import threading
4
+ import time
5
+ import requests
6
+ import subprocess
7
+ import os
8
+ import signal
9
+
10
+ class CDPDriver:
11
+ """
12
+ Direct websocket communicator for Chrome DevTools Protocol.
13
+ This avoids relying on heavy CDP wrapper dependencies.
14
+ """
15
+ def __init__(self, port=9222):
16
+ self.port = port
17
+ self.ws = None
18
+ self.msg_id = 1
19
+ self.responses = {}
20
+ self.events = []
21
+ self._browser_process = None
22
+ self.connected_event = threading.Event()
23
+
24
+ def start_chrome(self, headless=False):
25
+ # We need a robust way to find/start Chrome across platforms.
26
+ # For Windows, typically it's in Program Files.
27
+ chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
28
+ if not os.path.exists(chrome_path):
29
+ chrome_path = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
30
+
31
+ args = [
32
+ chrome_path,
33
+ f"--remote-debugging-port={self.port}",
34
+ "--remote-allow-origins=*",
35
+ "--user-data-dir=C:\\Temp\\pyquality_profile" # Temporary profile
36
+ ]
37
+
38
+ if headless:
39
+ args.append("--headless")
40
+ args.append("--disable-gpu")
41
+ args.append("--no-sandbox")
42
+
43
+ self._browser_process = subprocess.Popen(args)
44
+ time.sleep(2) # Give it time to start
45
+
46
+ def stop_chrome(self):
47
+ if self._browser_process:
48
+ self._browser_process.send_signal(signal.SIGTERM)
49
+
50
+ def _on_open(self, ws):
51
+ self.connected_event.set()
52
+
53
+ def _on_error(self, ws, error):
54
+ print(f"WebSocket Error: {error}")
55
+
56
+ def _on_close(self, ws, close_status_code, close_msg):
57
+ print("WebSocket Closed")
58
+
59
+ def connect(self):
60
+ # Fetch the websocket debugger URL
61
+ url = f"http://127.0.0.1:{self.port}/json"
62
+
63
+ # Add a retry logic in case Chrome takes time to open the port
64
+ response = None
65
+ for _ in range(5):
66
+ try:
67
+ response = requests.get(url).json()
68
+ break
69
+ except Exception:
70
+ time.sleep(1)
71
+
72
+ # We just grab the first page
73
+ if not response:
74
+ raise Exception("No active pages found or CDP port not responding!")
75
+
76
+ page_ws_url = response[0]['webSocketDebuggerUrl']
77
+ self.ws = websocket.WebSocketApp(
78
+ page_ws_url,
79
+ on_message=self._on_message,
80
+ on_open=self._on_open,
81
+ on_error=self._on_error,
82
+ on_close=self._on_close
83
+ )
84
+
85
+ # Start receiver thread
86
+ self.connected_event.clear()
87
+ self._thread = threading.Thread(target=self.ws.run_forever, kwargs={"suppress_origin": True})
88
+ self._thread.daemon = True
89
+ self._thread.start()
90
+
91
+ # Wait up to 5 seconds for the connection to establish
92
+ if not self.connected_event.wait(5):
93
+ raise Exception("Failed to connect to browser websocket within 5 seconds")
94
+
95
+ def _on_message(self, ws, message):
96
+ data = json.loads(message)
97
+ if 'id' in data:
98
+ self.responses[data['id']] = data
99
+ else:
100
+ self.events.append(data)
101
+
102
+ def send_command(self, method, params=None):
103
+ if params is None:
104
+ params = {}
105
+
106
+ cmd = {
107
+ "id": self.msg_id,
108
+ "method": method,
109
+ "params": params
110
+ }
111
+ self.ws.send(json.dumps(cmd))
112
+ current_id = self.msg_id
113
+ self.msg_id += 1
114
+ return current_id
115
+
116
+ def wait_for_response(self, msg_id, timeout=5):
117
+ start_time = time.time()
118
+ while time.time() - start_time < timeout:
119
+ if msg_id in self.responses:
120
+ return self.responses.pop(msg_id)
121
+ time.sleep(0.01)
122
+ raise TimeoutError(f"No response for message {msg_id}")
123
+
124
+ def execute_and_wait(self, method, params=None):
125
+ msg_id = self.send_command(method, params)
126
+ return self.wait_for_response(msg_id)
@@ -0,0 +1,50 @@
1
+ # Auto-generated CDP Domains
2
+ from .accessibility import AccessibilityDomain
3
+ from .animation import AnimationDomain
4
+ from .audits import AuditsDomain
5
+ from .autofill import AutofillDomain
6
+ from .backgroundservice import BackgroundServiceDomain
7
+ from .bluetoothemulation import BluetoothEmulationDomain
8
+ from .browser import BrowserDomain
9
+ from .css import CSSDomain
10
+ from .cachestorage import CacheStorageDomain
11
+ from .cast import CastDomain
12
+ from .dom import DOMDomain
13
+ from .domdebugger import DOMDebuggerDomain
14
+ from .domsnapshot import DOMSnapshotDomain
15
+ from .domstorage import DOMStorageDomain
16
+ from .deviceaccess import DeviceAccessDomain
17
+ from .deviceorientation import DeviceOrientationDomain
18
+ from .emulation import EmulationDomain
19
+ from .eventbreakpoints import EventBreakpointsDomain
20
+ from .extensions import ExtensionsDomain
21
+ from .fedcm import FedCmDomain
22
+ from .fetch import FetchDomain
23
+ from .filesystem import FileSystemDomain
24
+ from .headlessexperimental import HeadlessExperimentalDomain
25
+ from .io import IODomain
26
+ from .indexeddb import IndexedDBDomain
27
+ from .input import InputDomain
28
+ from .inspector import InspectorDomain
29
+ from .layertree import LayerTreeDomain
30
+ from .log import LogDomain
31
+ from .media import MediaDomain
32
+ from .memory import MemoryDomain
33
+ from .network import NetworkDomain
34
+ from .overlay import OverlayDomain
35
+ from .pwa import PWADomain
36
+ from .page import PageDomain
37
+ from .performance import PerformanceDomain
38
+ from .performancetimeline import PerformanceTimelineDomain
39
+ from .preload import PreloadDomain
40
+ from .security import SecurityDomain
41
+ from .serviceworker import ServiceWorkerDomain
42
+ from .smartcardemulation import SmartCardEmulationDomain
43
+ from .storage import StorageDomain
44
+ from .systeminfo import SystemInfoDomain
45
+ from .target import TargetDomain
46
+ from .tethering import TetheringDomain
47
+ from .tracing import TracingDomain
48
+ from .webaudio import WebAudioDomain
49
+ from .webauthn import WebAuthnDomain
50
+ from .webmcp import WebMCPDomain