purecdp 0.1.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 (114) hide show
  1. purecdp-0.1.0/PKG-INFO +193 -0
  2. purecdp-0.1.0/Readme.md +174 -0
  3. purecdp-0.1.0/pyproject.toml +46 -0
  4. purecdp-0.1.0/setup.cfg +4 -0
  5. purecdp-0.1.0/src/purecdp/__init__.py +64 -0
  6. purecdp-0.1.0/src/purecdp/_b64.py +24 -0
  7. purecdp-0.1.0/src/purecdp/_generator/__init__.py +10 -0
  8. purecdp-0.1.0/src/purecdp/_generator/__main__.py +53 -0
  9. purecdp-0.1.0/src/purecdp/_generator/emit.py +532 -0
  10. purecdp-0.1.0/src/purecdp/_generator/model.py +175 -0
  11. purecdp-0.1.0/src/purecdp/_json.py +45 -0
  12. purecdp-0.1.0/src/purecdp/_loop.py +53 -0
  13. purecdp-0.1.0/src/purecdp/_shared.py +51 -0
  14. purecdp-0.1.0/src/purecdp/browser.py +363 -0
  15. purecdp-0.1.0/src/purecdp/connection.py +513 -0
  16. purecdp-0.1.0/src/purecdp/discovery.py +50 -0
  17. purecdp-0.1.0/src/purecdp/engine.py +192 -0
  18. purecdp-0.1.0/src/purecdp/errors.py +48 -0
  19. purecdp-0.1.0/src/purecdp/mcp.py +233 -0
  20. purecdp-0.1.0/src/purecdp/protocol/__init__.py +156 -0
  21. purecdp-0.1.0/src/purecdp/protocol/accessibility.py +585 -0
  22. purecdp-0.1.0/src/purecdp/protocol/ads.py +103 -0
  23. purecdp-0.1.0/src/purecdp/protocol/animation.py +385 -0
  24. purecdp-0.1.0/src/purecdp/protocol/audits.py +1925 -0
  25. purecdp-0.1.0/src/purecdp/protocol/autofill.py +250 -0
  26. purecdp-0.1.0/src/purecdp/protocol/background_service.py +177 -0
  27. purecdp-0.1.0/src/purecdp/protocol/bluetooth_emulation.py +520 -0
  28. purecdp-0.1.0/src/purecdp/protocol/browser.py +670 -0
  29. purecdp-0.1.0/src/purecdp/protocol/cache_storage.py +262 -0
  30. purecdp-0.1.0/src/purecdp/protocol/cast.py +131 -0
  31. purecdp-0.1.0/src/purecdp/protocol/console.py +87 -0
  32. purecdp-0.1.0/src/purecdp/protocol/crash_report_context.py +50 -0
  33. purecdp-0.1.0/src/purecdp/protocol/css.py +2336 -0
  34. purecdp-0.1.0/src/purecdp/protocol/debugger.py +1164 -0
  35. purecdp-0.1.0/src/purecdp/protocol/device_access.py +112 -0
  36. purecdp-0.1.0/src/purecdp/protocol/device_orientation.py +36 -0
  37. purecdp-0.1.0/src/purecdp/protocol/digital_credentials.py +57 -0
  38. purecdp-0.1.0/src/purecdp/protocol/dom.py +1869 -0
  39. purecdp-0.1.0/src/purecdp/protocol/dom_debugger.py +249 -0
  40. purecdp-0.1.0/src/purecdp/protocol/dom_snapshot.py +734 -0
  41. purecdp-0.1.0/src/purecdp/protocol/dom_storage.py +186 -0
  42. purecdp-0.1.0/src/purecdp/protocol/emulation.py +1443 -0
  43. purecdp-0.1.0/src/purecdp/protocol/event_breakpoints.py +46 -0
  44. purecdp-0.1.0/src/purecdp/protocol/extensions.py +189 -0
  45. purecdp-0.1.0/src/purecdp/protocol/fed_cm.py +234 -0
  46. purecdp-0.1.0/src/purecdp/protocol/fetch.py +433 -0
  47. purecdp-0.1.0/src/purecdp/protocol/file_system.py +103 -0
  48. purecdp-0.1.0/src/purecdp/protocol/headless_experimental.py +95 -0
  49. purecdp-0.1.0/src/purecdp/protocol/heap_profiler.py +317 -0
  50. purecdp-0.1.0/src/purecdp/protocol/indexed_db.py +447 -0
  51. purecdp-0.1.0/src/purecdp/protocol/input.py +610 -0
  52. purecdp-0.1.0/src/purecdp/protocol/inspector.py +70 -0
  53. purecdp-0.1.0/src/purecdp/protocol/io.py +83 -0
  54. purecdp-0.1.0/src/purecdp/protocol/layer_tree.py +379 -0
  55. purecdp-0.1.0/src/purecdp/protocol/log.py +150 -0
  56. purecdp-0.1.0/src/purecdp/protocol/media.py +271 -0
  57. purecdp-0.1.0/src/purecdp/protocol/memory.py +237 -0
  58. purecdp-0.1.0/src/purecdp/protocol/network.py +4730 -0
  59. purecdp-0.1.0/src/purecdp/protocol/overlay.py +1282 -0
  60. purecdp-0.1.0/src/purecdp/protocol/page.py +3477 -0
  61. purecdp-0.1.0/src/purecdp/protocol/performance.py +88 -0
  62. purecdp-0.1.0/src/purecdp/protocol/performance_timeline.py +176 -0
  63. purecdp-0.1.0/src/purecdp/protocol/preload.py +518 -0
  64. purecdp-0.1.0/src/purecdp/protocol/profiler.py +340 -0
  65. purecdp-0.1.0/src/purecdp/protocol/pwa.py +234 -0
  66. purecdp-0.1.0/src/purecdp/protocol/runtime.py +1344 -0
  67. purecdp-0.1.0/src/purecdp/protocol/schema.py +46 -0
  68. purecdp-0.1.0/src/purecdp/protocol/security.py +448 -0
  69. purecdp-0.1.0/src/purecdp/protocol/service_worker.py +309 -0
  70. purecdp-0.1.0/src/purecdp/protocol/smart_card_emulation.py +801 -0
  71. purecdp-0.1.0/src/purecdp/protocol/storage.py +1074 -0
  72. purecdp-0.1.0/src/purecdp/protocol/system_info.py +277 -0
  73. purecdp-0.1.0/src/purecdp/protocol/target.py +712 -0
  74. purecdp-0.1.0/src/purecdp/protocol/tethering.py +52 -0
  75. purecdp-0.1.0/src/purecdp/protocol/tracing.py +339 -0
  76. purecdp-0.1.0/src/purecdp/protocol/web_audio.py +524 -0
  77. purecdp-0.1.0/src/purecdp/protocol/web_authn.py +475 -0
  78. purecdp-0.1.0/src/purecdp/protocol/web_mcp.py +245 -0
  79. purecdp-0.1.0/src/purecdp/py.typed +0 -0
  80. purecdp-0.1.0/src/purecdp/testing/__init__.py +62 -0
  81. purecdp-0.1.0/src/purecdp/testing/_agent.py +241 -0
  82. purecdp-0.1.0/src/purecdp/testing/element.py +399 -0
  83. purecdp-0.1.0/src/purecdp/testing/frame.py +167 -0
  84. purecdp-0.1.0/src/purecdp/testing/human.py +189 -0
  85. purecdp-0.1.0/src/purecdp/testing/intercept.py +162 -0
  86. purecdp-0.1.0/src/purecdp/testing/page.py +1092 -0
  87. purecdp-0.1.0/src/purecdp/testing/pytest_plugin.py +79 -0
  88. purecdp-0.1.0/src/purecdp/testing/recorder.py +155 -0
  89. purecdp-0.1.0/src/purecdp/testing/snapshot.py +199 -0
  90. purecdp-0.1.0/src/purecdp/testing/sse.py +85 -0
  91. purecdp-0.1.0/src/purecdp/testing/stealth.py +338 -0
  92. purecdp-0.1.0/src/purecdp/testing/testcase.py +55 -0
  93. purecdp-0.1.0/src/purecdp/transport/__init__.py +10 -0
  94. purecdp-0.1.0/src/purecdp/transport/pipe.py +142 -0
  95. purecdp-0.1.0/src/purecdp/transport/websocket.py +214 -0
  96. purecdp-0.1.0/src/purecdp.egg-info/PKG-INFO +193 -0
  97. purecdp-0.1.0/src/purecdp.egg-info/SOURCES.txt +112 -0
  98. purecdp-0.1.0/src/purecdp.egg-info/dependency_links.txt +1 -0
  99. purecdp-0.1.0/src/purecdp.egg-info/entry_points.txt +2 -0
  100. purecdp-0.1.0/src/purecdp.egg-info/requires.txt +10 -0
  101. purecdp-0.1.0/src/purecdp.egg-info/top_level.txt +1 -0
  102. purecdp-0.1.0/tests/test_backends.py +68 -0
  103. purecdp-0.1.0/tests/test_connection.py +257 -0
  104. purecdp-0.1.0/tests/test_e2e.py +161 -0
  105. purecdp-0.1.0/tests/test_engine.py +158 -0
  106. purecdp-0.1.0/tests/test_generator.py +115 -0
  107. purecdp-0.1.0/tests/test_json.py +53 -0
  108. purecdp-0.1.0/tests/test_lifecycle.py +198 -0
  109. purecdp-0.1.0/tests/test_mcp.py +148 -0
  110. purecdp-0.1.0/tests/test_protocol.py +218 -0
  111. purecdp-0.1.0/tests/test_sse.py +80 -0
  112. purecdp-0.1.0/tests/test_testing.py +809 -0
  113. purecdp-0.1.0/tests/test_testing_e2e.py +1028 -0
  114. purecdp-0.1.0/tests/test_transports.py +161 -0
purecdp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: purecdp
3
+ Version: 0.1.0
4
+ Summary: Vanilla Python Chrome DevTools Protocol. Optional dependencies for accelerated performance
5
+ License-Expression: 0BSD
6
+ Project-URL: Homepage, https://github.com/h5rdly/purecdp
7
+ Keywords: cdp,chrome-devtools-protocol,chromium,browser,devtools,browser-automation
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Classifier: Topic :: Software Development :: Testing
11
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
12
+ Requires-Python: >=3.14
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: faster
15
+ Requires-Dist: orjson>=3.11; extra == "faster"
16
+ Requires-Dist: pybase64>=1.4.3; extra == "faster"
17
+ Requires-Dist: uvloop>=0.22; platform_system != "Windows" and extra == "faster"
18
+ Requires-Dist: winloop>=0.6; platform_system == "Windows" and extra == "faster"
19
+
20
+ # purecdp
21
+
22
+ Pure-Python (stdlib-only) library for the Chrome DevTools Protocol, targeting Python 3.14+.
23
+
24
+ Three things, layered:
25
+
26
+ 1. **`purecdp.protocol`** — low-level typed bindings, generated from the official
27
+ [devtools-protocol](https://github.com/ChromeDevTools/devtools-protocol) JSON specs.
28
+ 2. **`purecdp` core** — sans-I/O protocol engine + asyncio transports (websocket / pipe) + browser launcher.
29
+ 3. **`purecdp.testing`** — opinionated helpers for driving a browser in tests, incl. a pytest plugin.
30
+
31
+ See [Examples.md](Examples.md) for task-grouped recipes (basics, testing, stealth, LLM agents, frames),
32
+ [DESIGN.md](DESIGN.md) for architecture and rationale, and [ROADMAP.md](ROADMAP.md) for work order and status.
33
+
34
+ ## Quickstart
35
+
36
+ ```python
37
+ import asyncio
38
+ import purecdp
39
+ from purecdp.protocol import page, runtime
40
+
41
+ async def main():
42
+ async with await purecdp.launch() as browser: # finds Chromium/Chrome/Brave
43
+ session = await browser.new_page() # create target + attach
44
+
45
+ await session.execute(page.enable())
46
+ waiter = asyncio.create_task(session.wait_for(page.LoadEventFired))
47
+ await asyncio.sleep(0) # subscribe before triggering
48
+ await session.execute(page.navigate(url="https://example.com"))
49
+ await waiter
50
+
51
+ result, _ = await session.execute(
52
+ runtime.evaluate(expression="document.title", return_by_value=True))
53
+ print(result.value)
54
+
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ `purecdp.launch(pipe=True)` uses `--remote-debugging-pipe` instead of a websocket;
59
+ `purecdp.discovery.get_version(port=9222)` reaches a browser someone else started.
60
+ `browser.new_context()` gives an isolated cookies/storage context (cheap per-test
61
+ isolation); `connection.set_auto_attach()` auto-attaches popups/workers/OOPIFs,
62
+ resuming paused targets automatically.
63
+
64
+ ## Writing tests with purecdp
65
+
66
+ The stdlib way (`purecdp.testing.CDPTestCase` — fresh browser, isolated
67
+ context, and a `Page` per test):
68
+
69
+ ```python
70
+ from purecdp.testing import CDPTestCase, JSError
71
+
72
+ class MyAppTests(CDPTestCase):
73
+ async def test_stubbed_api(self):
74
+ async def api(request):
75
+ await request.fulfill(body='{"items": []}',
76
+ content_type="application/json")
77
+ await self.page.route("*/api/*", api)
78
+ await self.page.goto("https://myapp.example/", wait="idle")
79
+ await self.page.wait_for_selector("#empty-state")
80
+ assert not self.page.js_errors
81
+ ```
82
+
83
+ `Page` gives you `goto` (load/idle waits), `evaluate` (promises awaited,
84
+ `JSError` raised), `wait_for_selector`/`wait_for_function`, `click`,
85
+ `query`/`query_all` (held elements, text-filtered, stale-copy-proof),
86
+ `screenshot`, always-on `page.console` / `page.js_errors` capture, and
87
+ `page.route()` request stubbing/rewriting/aborting. For traffic-level
88
+ assertions there's `page.record()` (full request/response exchanges, with
89
+ `parse_sse()` for streamed bodies) and `page.expect_download()` (capture a
90
+ download's bytes without touching disk).
91
+
92
+ Prefer pytest? The optional plugin (auto-registered when installed, or
93
+ `-p purecdp.testing.pytest_plugin`) provides a session-scoped `cdp_browser`
94
+ and per-test `cdp_page`, and runs `async def` tests itself — no
95
+ pytest-asyncio required:
96
+
97
+ ```python
98
+ async def test_title(cdp_page):
99
+ await cdp_page.goto("data:text/html,<title>hi</title>")
100
+ assert await cdp_page.title() == "hi"
101
+ ```
102
+
103
+ ## Low-observability / stealth
104
+
105
+ For automation that shouldn't advertise itself, purecdp layers three opt-in
106
+ pieces (reduces common detection signals — not a promise of undetectability;
107
+ CreepJS-class and TLS/HTTP2 fingerprints still identify automation):
108
+
109
+ ```python
110
+ import purecdp
111
+ from purecdp.testing import Page, apply_stealth
112
+
113
+ async with await purecdp.launch(stealth=True) as browser: # AutomationControlled off, headless=new
114
+ session = await browser.new_page()
115
+ page = await Page.create(session, capture=False, track_network=False) # no Runtime.enable leak
116
+ await apply_stealth(page) # fingerprint init-scripts, before goto
117
+ await page.goto("https://example.com")
118
+ ```
119
+
120
+ `apply_stealth(page, evasions=[...])` selects a subset; keyword overrides
121
+ (`webgl_vendor`, `webgl_renderer`, `languages`, `vendor`,
122
+ `hardware_concurrency`) tune the spoofed values.
123
+
124
+ For behavioral realism (the more durable signal — how input *moves*, all over
125
+ trusted Input events):
126
+
127
+ ```python
128
+ el = await page.query("#submit")
129
+ await el.human_click() # curved, eased path to a near-center point
130
+ await page.human_type("hello@example.com") # per-key events, human cadence
131
+ await page.human_scroll(1200) # eased wheel steps, not one jump
132
+ ```
133
+
134
+ Since purecdp drives a *real* browser, its TLS/HTTP2 (JA3/JA4) fingerprint is
135
+ already authentically Chrome's — no spoofing needed, unlike browserless HTTP
136
+ scrapers.
137
+
138
+ ## Driving with an LLM (agent surface)
139
+
140
+ `page.snapshot()` returns a compact, ref-tagged view of the page from the
141
+ accessibility tree — small and stable enough to hand an LLM, instead of raw
142
+ HTML or screenshots. Each control gets a session ref (`e1`, `e2`, …); password
143
+ fields are masked.
144
+
145
+ ```python
146
+ snap = await page.snapshot()
147
+ print(snap)
148
+ # - RootWebArea "Login"
149
+ # - textbox "Email" [e1]
150
+ # - textbox "Password" = "••••••" [e2]
151
+ # - button "Sign in" [e3]
152
+
153
+ await page.act("e1", "fill", text="me@example.com") # fill/type/select/click/…
154
+ await page.act("e3", "click")
155
+ el = await page.element_for_ref("e1") # escape hatch: full Element API
156
+ ```
157
+
158
+ An optional MCP server exposes this to any Model Context Protocol host — no
159
+ extra dependency, stdlib JSON-RPC over stdio:
160
+
161
+ ```sh
162
+ python -m purecdp.mcp # tools: navigate, snapshot, act, evaluate
163
+ python -m purecdp.mcp --stealth # low-observability launch
164
+ ```
165
+
166
+ ## Layout
167
+
168
+ ```
169
+ spec/ vendored protocol JSON + PIN (source commit)
170
+ src/purecdp/ the library (src layout)
171
+ src/purecdp/_generator/ code generator (dev tool, reads spec/, writes src/purecdp/protocol/)
172
+ src/purecdp/protocol/ GENERATED bindings — do not edit by hand
173
+ tests/ tests and test utilities
174
+ ```
175
+
176
+ ## Regenerating the bindings
177
+
178
+ ```sh
179
+ python -m purecdp._generator # uses spec/ and src/purecdp/protocol/ defaults
180
+ ```
181
+
182
+ Update `spec/*.json` + `spec/PIN` from a devtools-protocol checkout first when moving the pin.
183
+
184
+ ## Running tests
185
+
186
+ Stdlib only — no test dependencies:
187
+
188
+ ```sh
189
+ python -m unittest # discovery from repo root
190
+ python tests/test_protocol.py # single file works too
191
+ ```
192
+
193
+ `python -m pytest` also works (nicer output, subtest counts) but is never required.
@@ -0,0 +1,174 @@
1
+ # purecdp
2
+
3
+ Pure-Python (stdlib-only) library for the Chrome DevTools Protocol, targeting Python 3.14+.
4
+
5
+ Three things, layered:
6
+
7
+ 1. **`purecdp.protocol`** — low-level typed bindings, generated from the official
8
+ [devtools-protocol](https://github.com/ChromeDevTools/devtools-protocol) JSON specs.
9
+ 2. **`purecdp` core** — sans-I/O protocol engine + asyncio transports (websocket / pipe) + browser launcher.
10
+ 3. **`purecdp.testing`** — opinionated helpers for driving a browser in tests, incl. a pytest plugin.
11
+
12
+ See [Examples.md](Examples.md) for task-grouped recipes (basics, testing, stealth, LLM agents, frames),
13
+ [DESIGN.md](DESIGN.md) for architecture and rationale, and [ROADMAP.md](ROADMAP.md) for work order and status.
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ import asyncio
19
+ import purecdp
20
+ from purecdp.protocol import page, runtime
21
+
22
+ async def main():
23
+ async with await purecdp.launch() as browser: # finds Chromium/Chrome/Brave
24
+ session = await browser.new_page() # create target + attach
25
+
26
+ await session.execute(page.enable())
27
+ waiter = asyncio.create_task(session.wait_for(page.LoadEventFired))
28
+ await asyncio.sleep(0) # subscribe before triggering
29
+ await session.execute(page.navigate(url="https://example.com"))
30
+ await waiter
31
+
32
+ result, _ = await session.execute(
33
+ runtime.evaluate(expression="document.title", return_by_value=True))
34
+ print(result.value)
35
+
36
+ asyncio.run(main())
37
+ ```
38
+
39
+ `purecdp.launch(pipe=True)` uses `--remote-debugging-pipe` instead of a websocket;
40
+ `purecdp.discovery.get_version(port=9222)` reaches a browser someone else started.
41
+ `browser.new_context()` gives an isolated cookies/storage context (cheap per-test
42
+ isolation); `connection.set_auto_attach()` auto-attaches popups/workers/OOPIFs,
43
+ resuming paused targets automatically.
44
+
45
+ ## Writing tests with purecdp
46
+
47
+ The stdlib way (`purecdp.testing.CDPTestCase` — fresh browser, isolated
48
+ context, and a `Page` per test):
49
+
50
+ ```python
51
+ from purecdp.testing import CDPTestCase, JSError
52
+
53
+ class MyAppTests(CDPTestCase):
54
+ async def test_stubbed_api(self):
55
+ async def api(request):
56
+ await request.fulfill(body='{"items": []}',
57
+ content_type="application/json")
58
+ await self.page.route("*/api/*", api)
59
+ await self.page.goto("https://myapp.example/", wait="idle")
60
+ await self.page.wait_for_selector("#empty-state")
61
+ assert not self.page.js_errors
62
+ ```
63
+
64
+ `Page` gives you `goto` (load/idle waits), `evaluate` (promises awaited,
65
+ `JSError` raised), `wait_for_selector`/`wait_for_function`, `click`,
66
+ `query`/`query_all` (held elements, text-filtered, stale-copy-proof),
67
+ `screenshot`, always-on `page.console` / `page.js_errors` capture, and
68
+ `page.route()` request stubbing/rewriting/aborting. For traffic-level
69
+ assertions there's `page.record()` (full request/response exchanges, with
70
+ `parse_sse()` for streamed bodies) and `page.expect_download()` (capture a
71
+ download's bytes without touching disk).
72
+
73
+ Prefer pytest? The optional plugin (auto-registered when installed, or
74
+ `-p purecdp.testing.pytest_plugin`) provides a session-scoped `cdp_browser`
75
+ and per-test `cdp_page`, and runs `async def` tests itself — no
76
+ pytest-asyncio required:
77
+
78
+ ```python
79
+ async def test_title(cdp_page):
80
+ await cdp_page.goto("data:text/html,<title>hi</title>")
81
+ assert await cdp_page.title() == "hi"
82
+ ```
83
+
84
+ ## Low-observability / stealth
85
+
86
+ For automation that shouldn't advertise itself, purecdp layers three opt-in
87
+ pieces (reduces common detection signals — not a promise of undetectability;
88
+ CreepJS-class and TLS/HTTP2 fingerprints still identify automation):
89
+
90
+ ```python
91
+ import purecdp
92
+ from purecdp.testing import Page, apply_stealth
93
+
94
+ async with await purecdp.launch(stealth=True) as browser: # AutomationControlled off, headless=new
95
+ session = await browser.new_page()
96
+ page = await Page.create(session, capture=False, track_network=False) # no Runtime.enable leak
97
+ await apply_stealth(page) # fingerprint init-scripts, before goto
98
+ await page.goto("https://example.com")
99
+ ```
100
+
101
+ `apply_stealth(page, evasions=[...])` selects a subset; keyword overrides
102
+ (`webgl_vendor`, `webgl_renderer`, `languages`, `vendor`,
103
+ `hardware_concurrency`) tune the spoofed values.
104
+
105
+ For behavioral realism (the more durable signal — how input *moves*, all over
106
+ trusted Input events):
107
+
108
+ ```python
109
+ el = await page.query("#submit")
110
+ await el.human_click() # curved, eased path to a near-center point
111
+ await page.human_type("hello@example.com") # per-key events, human cadence
112
+ await page.human_scroll(1200) # eased wheel steps, not one jump
113
+ ```
114
+
115
+ Since purecdp drives a *real* browser, its TLS/HTTP2 (JA3/JA4) fingerprint is
116
+ already authentically Chrome's — no spoofing needed, unlike browserless HTTP
117
+ scrapers.
118
+
119
+ ## Driving with an LLM (agent surface)
120
+
121
+ `page.snapshot()` returns a compact, ref-tagged view of the page from the
122
+ accessibility tree — small and stable enough to hand an LLM, instead of raw
123
+ HTML or screenshots. Each control gets a session ref (`e1`, `e2`, …); password
124
+ fields are masked.
125
+
126
+ ```python
127
+ snap = await page.snapshot()
128
+ print(snap)
129
+ # - RootWebArea "Login"
130
+ # - textbox "Email" [e1]
131
+ # - textbox "Password" = "••••••" [e2]
132
+ # - button "Sign in" [e3]
133
+
134
+ await page.act("e1", "fill", text="me@example.com") # fill/type/select/click/…
135
+ await page.act("e3", "click")
136
+ el = await page.element_for_ref("e1") # escape hatch: full Element API
137
+ ```
138
+
139
+ An optional MCP server exposes this to any Model Context Protocol host — no
140
+ extra dependency, stdlib JSON-RPC over stdio:
141
+
142
+ ```sh
143
+ python -m purecdp.mcp # tools: navigate, snapshot, act, evaluate
144
+ python -m purecdp.mcp --stealth # low-observability launch
145
+ ```
146
+
147
+ ## Layout
148
+
149
+ ```
150
+ spec/ vendored protocol JSON + PIN (source commit)
151
+ src/purecdp/ the library (src layout)
152
+ src/purecdp/_generator/ code generator (dev tool, reads spec/, writes src/purecdp/protocol/)
153
+ src/purecdp/protocol/ GENERATED bindings — do not edit by hand
154
+ tests/ tests and test utilities
155
+ ```
156
+
157
+ ## Regenerating the bindings
158
+
159
+ ```sh
160
+ python -m purecdp._generator # uses spec/ and src/purecdp/protocol/ defaults
161
+ ```
162
+
163
+ Update `spec/*.json` + `spec/PIN` from a devtools-protocol checkout first when moving the pin.
164
+
165
+ ## Running tests
166
+
167
+ Stdlib only — no test dependencies:
168
+
169
+ ```sh
170
+ python -m unittest # discovery from repo root
171
+ python tests/test_protocol.py # single file works too
172
+ ```
173
+
174
+ `python -m pytest` also works (nicer output, subtest counts) but is never required.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=83"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+
6
+ [project]
7
+ name = "purecdp"
8
+ dynamic = ["version"] # single source: purecdp.__version__
9
+ description = "Vanilla Python Chrome DevTools Protocol. Optional dependencies for accelerated performance"
10
+ readme = "Readme.md" # exact on-disk case — GitHub's Linux runner is case-sensitive
11
+ license = "0BSD"
12
+ requires-python = ">=3.14"
13
+
14
+
15
+ keywords = [
16
+ "cdp", "chrome-devtools-protocol", "chromium", "browser", "devtools", "browser-automation",
17
+ ]
18
+ classifiers = [
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Software Development :: Testing",
22
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ faster = [
27
+ "orjson>=3.11", # wire JSON dumps/loads
28
+ "pybase64>=1.4.3", # screenshot/PDF/body base64
29
+ "uvloop>=0.22; platform_system != 'Windows'", # faster event loop (POSIX)
30
+ "winloop>=0.6; platform_system == 'Windows'", # faster event loop (Windows)
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/h5rdly/purecdp"
35
+
36
+ [project.entry-points.pytest11]
37
+ purecdp = "purecdp.testing.pytest_plugin"
38
+
39
+ [tool.setuptools.dynamic]
40
+ version = { attr = "purecdp.__version__" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.setuptools.package-data]
46
+ purecdp = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,64 @@
1
+ '''Pure-Python Chrome DevTools Protocol library.
2
+
3
+ Layers (see DESIGN.md):
4
+ - purecdp.protocol — generated typed bindings, one module per CDP domain
5
+ - purecdp.engine — sans-I/O protocol engine (strings in, typed happenings out)
6
+ - purecdp.connection — asyncio Connection/Session over an abstract Transport
7
+ - purecdp.transport — stdlib websocket + pipe transports
8
+ - purecdp.browser — find/launch/clean-up a debuggable browser
9
+ - purecdp.discovery — /json/version, /json/list HTTP endpoints
10
+ - purecdp.errors — exception types
11
+ - purecdp._shared — support types shared by generated code and the engine
12
+ '''
13
+
14
+ from ._loop import run
15
+ from .browser import (
16
+ STEALTH_ARGS,
17
+ Browser,
18
+ BrowserContext,
19
+ close_page,
20
+ connect,
21
+ find_browser,
22
+ launch,
23
+ new_context,
24
+ new_page,
25
+ )
26
+ from .connection import Connection, EventStream, Session, Transport
27
+ from .errors import (
28
+ BrowserLaunchError,
29
+ CDPConnectionClosed,
30
+ CDPError,
31
+ CDPProtocolError,
32
+ CDPSessionClosed,
33
+ CDPTransportError,
34
+ ProtocolDriftWarning,
35
+ )
36
+ from .transport import PipeTransport, WebSocketTransport
37
+
38
+ __version__ = '0.1.0'
39
+
40
+ __all__ = [
41
+ 'Connection',
42
+ 'Session',
43
+ 'EventStream',
44
+ 'Transport',
45
+ 'WebSocketTransport',
46
+ 'PipeTransport',
47
+ 'Browser',
48
+ 'BrowserContext',
49
+ 'launch',
50
+ 'connect',
51
+ 'find_browser',
52
+ 'new_context',
53
+ 'new_page',
54
+ 'close_page',
55
+ 'STEALTH_ARGS',
56
+ 'run',
57
+ 'CDPError',
58
+ 'CDPProtocolError',
59
+ 'CDPConnectionClosed',
60
+ 'CDPSessionClosed',
61
+ 'CDPTransportError',
62
+ 'BrowserLaunchError',
63
+ 'ProtocolDriftWarning',
64
+ ]
@@ -0,0 +1,24 @@
1
+ '''base64 backend: pybase64 (SIMD) when installed, stdlib base64 otherwise.
2
+
3
+ CDP moves base64 in bulk — screenshots, PDFs, and recorded response bodies —
4
+ and pybase64 decodes them ~3-4x faster than the stdlib. Optional: purecdp
5
+ stays zero-dependency; these are drop-in replacements (identical signatures)
6
+ used only where large blobs are decoded/encoded.
7
+ '''
8
+
9
+ from __future__ import annotations
10
+
11
+ try:
12
+ import pybase64 as _backend
13
+
14
+ HAVE_PYBASE64 = True
15
+ except ImportError: # pragma: no cover - stdlib path exercised in CI
16
+ import base64 as _backend
17
+
18
+ HAVE_PYBASE64 = False
19
+
20
+ b64decode = _backend.b64decode
21
+ b64encode = _backend.b64encode
22
+
23
+ #: Name of the active backend, for diagnostics.
24
+ BACKEND = 'pybase64' if HAVE_PYBASE64 else 'base64'
@@ -0,0 +1,10 @@
1
+ '''Code generator: devtools-protocol JSON specs -> purecdp.protocol modules.
2
+
3
+ Dev-time tool only - not imported by the runtime library. Run with:
4
+
5
+ python -m purecdp._generator [--spec DIR] [--out DIR]
6
+
7
+ CDP bindings are generated from the Chrome DevTools Protocol JSON (BSD-3-Clause,
8
+ © Google), via the hyperiongray python-chrome-devtools-protocol (MIT) convention"
9
+
10
+ '''
@@ -0,0 +1,53 @@
1
+ '''CLI: python -m purecdp._generator [--spec DIR] [--out DIR]'''
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import pathlib
7
+ import sys
8
+
9
+ from . import emit, model
10
+
11
+
12
+ def repo_root() -> pathlib.Path:
13
+ # src/purecdp/_generator/__main__.py -> repo root is three levels above src/
14
+ return pathlib.Path(__file__).resolve().parents[3]
15
+
16
+
17
+ def main(argv: list[str] | None = None) -> int:
18
+ root = repo_root()
19
+ ap = argparse.ArgumentParser(prog='purecdp._generator')
20
+ ap.add_argument('--spec', type=pathlib.Path, default=root / 'spec',
21
+ help='directory containing browser_protocol.json, js_protocol.json, PIN')
22
+ ap.add_argument('--out', type=pathlib.Path,
23
+ default=root / 'src' / 'purecdp' / 'protocol',
24
+ help='output package directory (existing *.py files are removed)')
25
+ args = ap.parse_args(argv)
26
+
27
+ spec_files = [args.spec / 'browser_protocol.json', args.spec / 'js_protocol.json']
28
+ for f in spec_files:
29
+ if not f.is_file():
30
+ ap.error(f'spec file not found: {f}')
31
+ pin_file = args.spec / 'PIN'
32
+ pin = pin_file.read_text().split()[0][:12] if pin_file.is_file() else 'unpinned'
33
+
34
+ spec = model.load(spec_files)
35
+ files = emit.emit_all(spec, pin)
36
+
37
+ args.out.mkdir(parents=True, exist_ok=True)
38
+ for stale in args.out.glob('*.py'):
39
+ if stale.name not in files:
40
+ stale.unlink()
41
+ changed = 0
42
+ for name, source in sorted(files.items()):
43
+ path = args.out / name
44
+ if not path.is_file() or path.read_text(encoding='utf-8') != source:
45
+ path.write_text(source, encoding='utf-8')
46
+ changed += 1
47
+ print(f'{len(spec.domains)} domains -> {len(files)} files '
48
+ f'({changed} written) in {args.out}')
49
+ return 0
50
+
51
+
52
+ if __name__ == '__main__':
53
+ sys.exit(main())