webskrap 3.2.4__tar.gz → 3.3.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 (104) hide show
  1. {webskrap-3.2.4 → webskrap-3.3.0}/CHANGELOG.md +37 -1
  2. {webskrap-3.2.4 → webskrap-3.3.0}/PKG-INFO +41 -1
  3. {webskrap-3.2.4 → webskrap-3.3.0}/README.md +40 -0
  4. webskrap-3.3.0/docs/benchmarks/mcp-snapshots-playwright.json +83 -0
  5. webskrap-3.3.0/docs/benchmarks/mcp-snapshots-webskrap.json +140 -0
  6. webskrap-3.3.0/docs/benchmarks/mcp-snapshots.md +145 -0
  7. webskrap-3.3.0/docs/design/mcp-snapshots.md +92 -0
  8. {webskrap-3.2.4 → webskrap-3.3.0}/pyproject.toml +1 -1
  9. webskrap-3.3.0/scripts/benchmark_playwright_snapshots.py +125 -0
  10. webskrap-3.3.0/scripts/benchmark_snapshots.py +199 -0
  11. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/browser_session.py +8 -2
  12. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cdp.py +18 -2
  13. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cdp_dom.py +167 -31
  14. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cdp_setup.py +1 -0
  15. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cli.py +35 -15
  16. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/client.py +124 -35
  17. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/guide.md +71 -7
  18. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/mcp_server.py +66 -15
  19. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/models.py +6 -3
  20. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_browser_integration.py +125 -0
  21. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cdp.py +67 -0
  22. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cdp_child_targets.py +6 -3
  23. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cdp_session.py +1 -1
  24. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cli.py +20 -4
  25. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_client_unit.py +166 -13
  26. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_mcp.py +15 -1
  27. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_mcp_efficiency.py +68 -0
  28. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_parsing.py +1 -0
  29. webskrap-3.3.0/tests/test_snapshot_find.py +355 -0
  30. {webskrap-3.2.4 → webskrap-3.3.0}/uv.lock +166 -162
  31. {webskrap-3.2.4 → webskrap-3.3.0}/.gitattributes +0 -0
  32. {webskrap-3.2.4 → webskrap-3.3.0}/.gitignore +0 -0
  33. {webskrap-3.2.4/.package-dist.sya6Sl → webskrap-3.3.0/.package-dist.qx0gMJ}/.gitignore +0 -0
  34. {webskrap-3.2.4 → webskrap-3.3.0}/CONTRIBUTING.md +0 -0
  35. {webskrap-3.2.4 → webskrap-3.3.0}/LICENSE +0 -0
  36. {webskrap-3.2.4 → webskrap-3.3.0}/SECURITY.md +0 -0
  37. {webskrap-3.2.4 → webskrap-3.3.0}/benchmarks.py +0 -0
  38. {webskrap-3.2.4 → webskrap-3.3.0}/docs/design/cdp-driver.md +0 -0
  39. {webskrap-3.2.4 → webskrap-3.3.0}/docs/diagnostics/datadome.md +0 -0
  40. {webskrap-3.2.4 → webskrap-3.3.0}/docs/migration-3.0.md +0 -0
  41. {webskrap-3.2.4 → webskrap-3.3.0}/scripts/driver_parity.py +0 -0
  42. {webskrap-3.2.4 → webskrap-3.3.0}/scripts/live_stealth_report.py +0 -0
  43. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/__init__.py +0 -0
  44. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/browser_cli.py +0 -0
  45. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cdp_session.py +0 -0
  46. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/challenges.py +0 -0
  47. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/cli_output.py +0 -0
  48. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/consent.py +0 -0
  49. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/diagnostics.py +0 -0
  50. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/display.py +0 -0
  51. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/errors.py +0 -0
  52. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/fetch_runtime.py +0 -0
  53. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/human.py +0 -0
  54. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/install.py +0 -0
  55. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/parsing.py +0 -0
  56. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/paths.py +0 -0
  57. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/profiles.py +0 -0
  58. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/proxy_auth.py +0 -0
  59. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/py.typed +0 -0
  60. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/search.py +0 -0
  61. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/text_search.py +0 -0
  62. {webskrap-3.2.4 → webskrap-3.3.0}/src/webskrap/urls.py +0 -0
  63. {webskrap-3.2.4 → webskrap-3.3.0}/tests/conftest.py +0 -0
  64. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/accepted-202.html +0 -0
  65. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/akamai-invalid-url-400.html +0 -0
  66. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/akamai-reference-403.html +0 -0
  67. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/akamai-sec-cpt.html +0 -0
  68. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/api-403.html +0 -0
  69. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/article-access-denied.html +0 -0
  70. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/cloudflare-1020.html +0 -0
  71. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/cloudflare-custom-block.html +0 -0
  72. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/blocks/not-found-404.html +0 -0
  73. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/bing.html +0 -0
  74. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/bing_challenge.html +0 -0
  75. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/bing_no_results.html +0 -0
  76. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/ddg.html +0 -0
  77. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/ddg_challenge.html +0 -0
  78. {webskrap-3.2.4 → webskrap-3.3.0}/tests/fixtures/search/ddg_no_results.html +0 -0
  79. {webskrap-3.2.4 → webskrap-3.3.0}/tests/live_stealth_helpers.py +0 -0
  80. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_bot_detection.py +0 -0
  81. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_bot_detection_headless.py +0 -0
  82. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_browser_cli.py +0 -0
  83. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_browser_session_unit.py +0 -0
  84. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_browser_vision.py +0 -0
  85. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cdp_browser.py +0 -0
  86. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_cdp_setup.py +0 -0
  87. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_challenges.py +0 -0
  88. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_consent.py +0 -0
  89. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_consent_live.py +0 -0
  90. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_diagnostics.py +0 -0
  91. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_display.py +0 -0
  92. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_errors.py +0 -0
  93. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_fetch_runtime.py +0 -0
  94. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_human.py +0 -0
  95. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_install.py +0 -0
  96. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_live_stealth_helpers.py +0 -0
  97. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_live_stealth_report.py +0 -0
  98. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_models.py +0 -0
  99. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_paths.py +0 -0
  100. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_profiles.py +0 -0
  101. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_search.py +0 -0
  102. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_search_live.py +0 -0
  103. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_text_search.py +0 -0
  104. {webskrap-3.2.4 → webskrap-3.3.0}/tests/test_urls.py +0 -0
@@ -9,6 +9,41 @@ history, so they summarize each release rather than list every change.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [3.3.0] - 2026-11-14
13
+
14
+ ### Added
15
+
16
+ - MCP `browser_find` locates accessible elements and visible text with minimal
17
+ ancestor context. `browser_snapshot` accepts subtree targets and optional
18
+ bounding boxes. Existing interaction refs and response fields stay compatible.
19
+ - Named MCP fetch sessions reuse a browser across repeated requests. Close them
20
+ with `fetch_session_close`.
21
+
22
+ ### Changed
23
+
24
+ - Page extraction uses one browser evaluation.
25
+ - Fetch defaults use `domcontentloaded` and automatic resource policies:
26
+ lightweight resources for text, all resources for HTML. Use explicit
27
+ `networkidle` or `resource_policy="all"` when a task needs them.
28
+ - Refresh the Python dependency lock and update setup-uv to 10.3.3.
29
+
30
+ ### Fixed
31
+
32
+ - Preserve empty HTTP error responses instead of reporting a navigation failure.
33
+ - Keep CLI schema output compatible with Typer 0.29 type descriptors and older
34
+ Typer parameter types.
35
+ - Accessibility searches retain inline paragraph, status and form-value text.
36
+ Snapshots follow the focused dialog when native modal dialogs are stacked.
37
+ - Wait for the local cross-site fixture's image request before checking it in
38
+ browser tests.
39
+
40
+ ### Security
41
+
42
+ - Omit password input values from accessibility snapshots. Escape URL metadata
43
+ and normalize line separators so page attributes cannot inject fake snapshot
44
+ lines. Detect detached refs before interaction.
45
+ - Update locked OpenTelemetry API to 1.45.2 for GHSA-mf5v-w3w3-f6cg.
46
+
12
47
  ## [3.2.4] - 2026-11-01
13
48
 
14
49
  ### Added
@@ -1001,7 +1036,8 @@ entries below.
1001
1036
 
1002
1037
  Earlier releases (0.1.0 - 0.4.9) are recorded in the git history and tags only.
1003
1038
 
1004
- [Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.2.4...HEAD
1039
+ [Unreleased]: https://github.com/kacigaya/webskrap/compare/v3.3.0...HEAD
1040
+ [3.3.0]: https://github.com/kacigaya/webskrap/compare/v3.2.4...v3.3.0
1005
1041
  [3.2.4]: https://github.com/kacigaya/webskrap/compare/v3.2.3...v3.2.4
1006
1042
  [3.2.3]: https://github.com/kacigaya/webskrap/compare/v3.2.2...v3.2.3
1007
1043
  [3.2.2]: https://github.com/kacigaya/webskrap/compare/v3.2.1...v3.2.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webskrap
3
- Version: 3.2.4
3
+ Version: 3.3.0
4
4
  Summary: Async Python scraping on Chromium over its own CDP client, with coherent browser profiles and session controls.
5
5
  Project-URL: Homepage, https://github.com/kacigaya/webskrap
6
6
  Project-URL: Repository, https://github.com/kacigaya/webskrap
@@ -87,6 +87,9 @@ Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
87
87
  pages (DataDome, Cloudflare, Akamai) raise `blocked`. WebSkrap waits for device
88
88
  checks that reload the page. It does not retry refusals or solve CAPTCHAs.
89
89
  Ordinary HTTP errors still return `ok=False`.
90
+ For an empty HTTP error body, Chromium can show its own error page. The returned
91
+ text or HTML is that rendered page. The status, headers, and `final_url` still
92
+ come from the HTTP response.
90
93
 
91
94
  For manual CAPTCHA completion, open a persistent browser with
92
95
  `webskrap browser open --headed --session manual --proxy http://localhost:8888 URL`.
@@ -99,6 +102,30 @@ Keep `profile_mode="native"` for the browser's own identity. On Linux with Xvfb,
99
102
  Full profile emulation is useful for device tests, but cannot replace every
100
103
  worker or iframe value. See the [identity limits](src/webskrap/guide.md).
101
104
 
105
+ ## Python batches
106
+
107
+ Use `client.fetch_many(urls)` to fetch a finite batch in order with one browser:
108
+
109
+ ```python
110
+ async with WebSkrapClient() as client:
111
+ results = await client.fetch_many(
112
+ ["https://example.com", "https://example.org"], text_only=True
113
+ )
114
+ ```
115
+
116
+ Each batch shares cookies and storage, then closes its browser. Separate batches
117
+ and `client.fetch()` calls use fresh sessions. An explicit `user_data_dir` can
118
+ still share state on disk. Empty batches do not launch a browser. The first page
119
+ error stops the batch and raises; no partial list is returned. Ordinary HTTP
120
+ errors remain results with `ok=False`. `screenshot=True` uses a separate generated
121
+ PNG filename for each page.
122
+
123
+ For large inputs, per-page options, or state across batches, create a session
124
+ with `session = await client.session("crawl")`. Call `await session.fetch(url)`
125
+ for each URL. Close it with `await client.close_session("crawl")`, or let the
126
+ client context close it. Session names belong to one client. Use separate
127
+ sessions for unrelated callers.
128
+
102
129
  ## Search
103
130
 
104
131
  A search loads the engine's results page in the same stealth browser and
@@ -144,6 +171,19 @@ Coordinates are CSS pixels from the original image's top-left. Take a fresh
144
171
  view after scrolling or page changes. Headless Chromium needs no Xvfb for
145
172
  this; the agent's client and model must support image input.
146
173
 
174
+ ## Load only what the task needs
175
+ The default resource policy is `auto`. Text-only fetches skip images, fonts
176
+ and media. HTML fetches, screenshots and interactive pages load all resources.
177
+ Set `resource_policy="all"`, `"lite"`, or `"documents"` to override this choice.
178
+ Use `documents` only when removing CSS preserves the required result.
179
+ Fetches use `domcontentloaded`; set `ready_selector` for deferred content.
180
+ Use `browser_find(text="Save")` to locate a known control with ancestor context.
181
+ Use `browser_snapshot(target="e15", depth=2)` for one subtree; add `boxes=true`
182
+ only when geometry matters. Reuse returned refs with `browser_interact`.
183
+ Use `networkidle`, screenshots and full snapshots only when the task needs them.
184
+ CLI and MCP reject cookies by default. Disable this only for known pages where
185
+ it is unnecessary. In Python, enable rejection with `SessionConfig(decline_cookies=True)`.
186
+
147
187
  ## Documentation
148
188
 
149
189
  - [Agent skill](.agents/skills/webskrap/SKILL.md)
@@ -52,6 +52,9 @@ Known challenge pages (DataDome, Cloudflare, Akamai, AWS WAF) and provider block
52
52
  pages (DataDome, Cloudflare, Akamai) raise `blocked`. WebSkrap waits for device
53
53
  checks that reload the page. It does not retry refusals or solve CAPTCHAs.
54
54
  Ordinary HTTP errors still return `ok=False`.
55
+ For an empty HTTP error body, Chromium can show its own error page. The returned
56
+ text or HTML is that rendered page. The status, headers, and `final_url` still
57
+ come from the HTTP response.
55
58
 
56
59
  For manual CAPTCHA completion, open a persistent browser with
57
60
  `webskrap browser open --headed --session manual --proxy http://localhost:8888 URL`.
@@ -64,6 +67,30 @@ Keep `profile_mode="native"` for the browser's own identity. On Linux with Xvfb,
64
67
  Full profile emulation is useful for device tests, but cannot replace every
65
68
  worker or iframe value. See the [identity limits](src/webskrap/guide.md).
66
69
 
70
+ ## Python batches
71
+
72
+ Use `client.fetch_many(urls)` to fetch a finite batch in order with one browser:
73
+
74
+ ```python
75
+ async with WebSkrapClient() as client:
76
+ results = await client.fetch_many(
77
+ ["https://example.com", "https://example.org"], text_only=True
78
+ )
79
+ ```
80
+
81
+ Each batch shares cookies and storage, then closes its browser. Separate batches
82
+ and `client.fetch()` calls use fresh sessions. An explicit `user_data_dir` can
83
+ still share state on disk. Empty batches do not launch a browser. The first page
84
+ error stops the batch and raises; no partial list is returned. Ordinary HTTP
85
+ errors remain results with `ok=False`. `screenshot=True` uses a separate generated
86
+ PNG filename for each page.
87
+
88
+ For large inputs, per-page options, or state across batches, create a session
89
+ with `session = await client.session("crawl")`. Call `await session.fetch(url)`
90
+ for each URL. Close it with `await client.close_session("crawl")`, or let the
91
+ client context close it. Session names belong to one client. Use separate
92
+ sessions for unrelated callers.
93
+
67
94
  ## Search
68
95
 
69
96
  A search loads the engine's results page in the same stealth browser and
@@ -109,6 +136,19 @@ Coordinates are CSS pixels from the original image's top-left. Take a fresh
109
136
  view after scrolling or page changes. Headless Chromium needs no Xvfb for
110
137
  this; the agent's client and model must support image input.
111
138
 
139
+ ## Load only what the task needs
140
+ The default resource policy is `auto`. Text-only fetches skip images, fonts
141
+ and media. HTML fetches, screenshots and interactive pages load all resources.
142
+ Set `resource_policy="all"`, `"lite"`, or `"documents"` to override this choice.
143
+ Use `documents` only when removing CSS preserves the required result.
144
+ Fetches use `domcontentloaded`; set `ready_selector` for deferred content.
145
+ Use `browser_find(text="Save")` to locate a known control with ancestor context.
146
+ Use `browser_snapshot(target="e15", depth=2)` for one subtree; add `boxes=true`
147
+ only when geometry matters. Reuse returned refs with `browser_interact`.
148
+ Use `networkidle`, screenshots and full snapshots only when the task needs them.
149
+ CLI and MCP reject cookies by default. Disable this only for known pages where
150
+ it is unnecessary. In Python, enable rejection with `SessionConfig(decline_cookies=True)`.
151
+
112
152
  ## Documentation
113
153
 
114
154
  - [Agent skill](.agents/skills/webskrap/SKILL.md)
@@ -0,0 +1,83 @@
1
+ {
2
+ "package": "@playwright/mcp@0.0.85",
3
+ "runs": [
4
+ {
5
+ "mode": "full",
6
+ "repeat": 0,
7
+ "success": true,
8
+ "tool_calls": 6,
9
+ "request_tokens": 91,
10
+ "response_tokens": 479,
11
+ "response_bytes": 1508,
12
+ "snapshot_ms_median": 74.09905100939795,
13
+ "task_ms": 2093.8407619833015,
14
+ "cpu_seconds": 0.6999999999999993,
15
+ "rss_mib_after": 1396.22265625
16
+ },
17
+ {
18
+ "mode": "full",
19
+ "repeat": 1,
20
+ "success": true,
21
+ "tool_calls": 6,
22
+ "request_tokens": 91,
23
+ "response_tokens": 479,
24
+ "response_bytes": 1508,
25
+ "snapshot_ms_median": 109.93575002066791,
26
+ "task_ms": 2686.555687978398,
27
+ "cpu_seconds": 0.7799999999999994,
28
+ "rss_mib_after": 1335.69921875
29
+ },
30
+ {
31
+ "mode": "full",
32
+ "repeat": 2,
33
+ "success": true,
34
+ "tool_calls": 6,
35
+ "request_tokens": 91,
36
+ "response_tokens": 479,
37
+ "response_bytes": 1508,
38
+ "snapshot_ms_median": 90.022451011464,
39
+ "task_ms": 2248.4715349855833,
40
+ "cpu_seconds": 0.7300000000000004,
41
+ "rss_mib_after": 1335.19140625
42
+ },
43
+ {
44
+ "mode": "none",
45
+ "repeat": 0,
46
+ "success": true,
47
+ "tool_calls": 6,
48
+ "request_tokens": 91,
49
+ "response_tokens": 351,
50
+ "response_bytes": 1162,
51
+ "snapshot_ms_median": 130.02865097951144,
52
+ "task_ms": 2340.031432977412,
53
+ "cpu_seconds": 0.7799999999999998,
54
+ "rss_mib_after": 1314.16796875
55
+ },
56
+ {
57
+ "mode": "none",
58
+ "repeat": 1,
59
+ "success": true,
60
+ "tool_calls": 6,
61
+ "request_tokens": 91,
62
+ "response_tokens": 351,
63
+ "response_bytes": 1162,
64
+ "snapshot_ms_median": 77.6378289447166,
65
+ "task_ms": 1987.0279059978202,
66
+ "cpu_seconds": 0.7399999999999998,
67
+ "rss_mib_after": 1312.30859375
68
+ },
69
+ {
70
+ "mode": "none",
71
+ "repeat": 2,
72
+ "success": true,
73
+ "tool_calls": 6,
74
+ "request_tokens": 91,
75
+ "response_tokens": 351,
76
+ "response_bytes": 1162,
77
+ "snapshot_ms_median": 73.93148995470256,
78
+ "task_ms": 2026.7483039642684,
79
+ "cpu_seconds": 0.75,
80
+ "rss_mib_after": 1314.65625
81
+ }
82
+ ]
83
+ }
@@ -0,0 +1,140 @@
1
+ {
2
+ "tokenizer": "tiktoken 0.14.0 o200k_base",
3
+ "runs": [
4
+ {
5
+ "mode": "baseline",
6
+ "repeat": 0,
7
+ "success": true,
8
+ "tool_calls": 15,
9
+ "request_tokens": 245,
10
+ "response_tokens": 32636,
11
+ "response_bytes": 101972,
12
+ "snapshot_ms_median": 276.9699060008861,
13
+ "task_ms": 15578.733453992754,
14
+ "cpu_seconds": 4.199999999999999,
15
+ "rss_mib_after": 1158.53515625,
16
+ "describe_calls": 8688,
17
+ "stealth_sensitive_calls": 0
18
+ },
19
+ {
20
+ "mode": "snapshot",
21
+ "repeat": 0,
22
+ "success": true,
23
+ "tool_calls": 9,
24
+ "request_tokens": 161,
25
+ "response_tokens": 11054,
26
+ "response_bytes": 34568,
27
+ "snapshot_ms_median": 132.4927629902959,
28
+ "task_ms": 13070.251779980026,
29
+ "cpu_seconds": 2.4899999999999998,
30
+ "rss_mib_after": 1248.1875,
31
+ "describe_calls": 2894,
32
+ "stealth_sensitive_calls": 0
33
+ },
34
+ {
35
+ "mode": "find",
36
+ "repeat": 0,
37
+ "success": true,
38
+ "tool_calls": 6,
39
+ "request_tokens": 123,
40
+ "response_tokens": 302,
41
+ "response_bytes": 974,
42
+ "snapshot_ms_median": 10.353491990827024,
43
+ "task_ms": 12541.241928993259,
44
+ "cpu_seconds": 2.02,
45
+ "rss_mib_after": 1241.4296875,
46
+ "describe_calls": 7,
47
+ "stealth_sensitive_calls": 0
48
+ },
49
+ {
50
+ "mode": "baseline",
51
+ "repeat": 1,
52
+ "success": true,
53
+ "tool_calls": 15,
54
+ "request_tokens": 245,
55
+ "response_tokens": 32636,
56
+ "response_bytes": 101972,
57
+ "snapshot_ms_median": 189.5280875323806,
58
+ "task_ms": 14903.852140996605,
59
+ "cpu_seconds": 4.0600000000000005,
60
+ "rss_mib_after": 1160.078125,
61
+ "describe_calls": 8688,
62
+ "stealth_sensitive_calls": 0
63
+ },
64
+ {
65
+ "mode": "snapshot",
66
+ "repeat": 1,
67
+ "success": true,
68
+ "tool_calls": 9,
69
+ "request_tokens": 161,
70
+ "response_tokens": 11054,
71
+ "response_bytes": 34568,
72
+ "snapshot_ms_median": 135.92701949528418,
73
+ "task_ms": 13055.62697298592,
74
+ "cpu_seconds": 2.8599999999999994,
75
+ "rss_mib_after": 1247.55078125,
76
+ "describe_calls": 2894,
77
+ "stealth_sensitive_calls": 0
78
+ },
79
+ {
80
+ "mode": "find",
81
+ "repeat": 1,
82
+ "success": true,
83
+ "tool_calls": 6,
84
+ "request_tokens": 123,
85
+ "response_tokens": 302,
86
+ "response_bytes": 974,
87
+ "snapshot_ms_median": 15.616477990988642,
88
+ "task_ms": 12361.839258985128,
89
+ "cpu_seconds": 1.96,
90
+ "rss_mib_after": 1244.92578125,
91
+ "describe_calls": 7,
92
+ "stealth_sensitive_calls": 0
93
+ },
94
+ {
95
+ "mode": "baseline",
96
+ "repeat": 2,
97
+ "success": true,
98
+ "tool_calls": 15,
99
+ "request_tokens": 245,
100
+ "response_tokens": 32636,
101
+ "response_bytes": 101972,
102
+ "snapshot_ms_median": 151.91562002291903,
103
+ "task_ms": 13915.481942996848,
104
+ "cpu_seconds": 4.12,
105
+ "rss_mib_after": 1257.5,
106
+ "describe_calls": 8688,
107
+ "stealth_sensitive_calls": 0
108
+ },
109
+ {
110
+ "mode": "snapshot",
111
+ "repeat": 2,
112
+ "success": true,
113
+ "tool_calls": 9,
114
+ "request_tokens": 161,
115
+ "response_tokens": 11054,
116
+ "response_bytes": 34568,
117
+ "snapshot_ms_median": 131.0603755118791,
118
+ "task_ms": 12102.267072012182,
119
+ "cpu_seconds": 2.6100000000000003,
120
+ "rss_mib_after": 1245.76171875,
121
+ "describe_calls": 2894,
122
+ "stealth_sensitive_calls": 0
123
+ },
124
+ {
125
+ "mode": "find",
126
+ "repeat": 2,
127
+ "success": true,
128
+ "tool_calls": 6,
129
+ "request_tokens": 123,
130
+ "response_tokens": 302,
131
+ "response_bytes": 974,
132
+ "snapshot_ms_median": 11.524417961481959,
133
+ "task_ms": 11286.688952008262,
134
+ "cpu_seconds": 1.9299999999999997,
135
+ "rss_mib_after": 1244.19140625,
136
+ "describe_calls": 7,
137
+ "stealth_sensitive_calls": 0
138
+ }
139
+ ]
140
+ }
@@ -0,0 +1,145 @@
1
+ # MCP snapshot workflow measurements
2
+
3
+ Measured on 2026-11-14 on Linux ARM64, Python 3.14.7, uv 0.13.5 and
4
+ Chrome for Testing 156.0.8078.47, headless with sandbox explicitly disabled.
5
+ Baseline revision: `914e24576ebe92e08d46c2867d065343b1b71fdc`.
6
+ The measurements precede the review fixes for inline-text search, URL escaping
7
+ and stacked dialogs. Those fixes have separate regression coverage; the
8
+ benchmark has not been rerun for them.
9
+ No production dependency was added.
10
+
11
+ ## Method
12
+
13
+ The task page has 180 records, headings, descriptions and edit buttons. The
14
+ workflow locates record 179, opens its native modal editor, locates and fills
15
+ the Title field, locates Save changes, clicks it and checks the saved value.
16
+ This measures a three-interaction workflow, including DOM changes and modal
17
+ state, rather than an isolated snapshot. It is one synthetic task, not a
18
+ representative sample of the web or an actual model inference run.
19
+
20
+ Each WebSkrap mode runs three times in alternating order with a fresh browser.
21
+ Baseline loads the original CDP module from the revision above. Snapshot mode
22
+ uses the new code with default depth and character limits. Both page through
23
+ 8,000-character responses until the named control is present. Find mode uses
24
+ literal name search. All modes use the same native WebSkrap interactions.
25
+
26
+ The Playwright comparison uses `@playwright/mcp@0.0.85` in npm's external cache,
27
+ the same Chromium executable and HTML, and the same names and actions. It
28
+ runs three times each with default automatic snapshots and with
29
+ `--snapshot-mode=none`. Both use `browser_find`. Playwright can save large snapshots to local files;
30
+ these counts cover returned text, not unrequested file contents. The task needs
31
+ only find results, so it does not read those files. Playwright loads a temporary
32
+ file; WebSkrap injects the document into about:blank. Loading and browser
33
+ startup are excluded. Verification reads are excluded from tool/token counts.
34
+
35
+ Token counts use tiktoken 0.14.0, `o200k_base`. Request counts encode JSON tool
36
+ names and arguments. Response counts encode each WebSkrap JSON payload once,
37
+ or Playwright's returned text content once. These are measured payload tokens,
38
+ not billed model input/output usage. They exclude JSON-RPC envelopes, duplicate
39
+ MCP text/structured serialization, tool schemas, model reasoning and replayed
40
+ conversation context. A client that sends both structured and text payloads to
41
+ a model can consume more tokens. No model was invoked to select actions.
42
+
43
+ Snapshot latency covers each read plus shaping; the reported number is the
44
+ median of each run's median. WebSkrap calls Python handlers directly;
45
+ Playwright uses MCP stdio, so cross-driver latency includes different transport
46
+ costs. Task time includes trusted interactions and verification. WebSkrap's
47
+ human cursor, scroll and typing timing differs from Playwright's interaction
48
+ implementation. Compare read latency separately from total task time.
49
+
50
+ CPU is the `/proc` CPU-time delta for Python plus its current descendants.
51
+ Memory is their summed RSS after the task, not peak memory or unique physical
52
+ memory; shared pages can be counted more than once. `/proc` sampling and
53
+ regex-worker lifecycle are approximate. Runs shared the host with tests.
54
+ Timing and memory are observations, not isolated performance guarantees.
55
+
56
+ ## Results
57
+
58
+ All rows are medians over three successful runs. Each driver completed 3/3
59
+ workflows per mode with the expected saved value.
60
+
61
+ | Mode | Request tokens | Response tokens | Response bytes | Tool calls | Read ms | Task ms | CPU s | Summed RSS MiB |
62
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
63
+ | WebSkrap baseline, paged snapshot | 245 | 32,636 | 101,972 | 15 | 189.53 | 14,903.85 | 4.12 | 1,160.08 |
64
+ | WebSkrap new, paged snapshot | 161 | 11,054 | 34,568 | 9 | 132.49 | 13,055.63 | 2.61 | 1,247.55 |
65
+ | WebSkrap new, targeted find | 123 | 302 | 974 | 6 | 11.52 | 12,361.84 | 1.96 | 1,244.19 |
66
+ | Playwright MCP, find + default snapshots | 91 | 479 | 1,508 | 6 | 90.02 | 2,248.47 | 0.73 | 1,335.70 |
67
+ | Playwright MCP, find + no automatic snapshots | 91 | 351 | 1,162 | 6 | 77.64 | 2,026.75 | 0.75 | 1,314.17 |
68
+
69
+ The new snapshot path saves tokens in this task mainly because it omits
70
+ background content behind the modal. It preserves the existing format for
71
+ ordinary nonmodal pages. Targeted find reduces response tokens by 99.1% against
72
+ the baseline workflow and uses six calls instead of fifteen. This reduction is
73
+ specific to a known target near the end of a long page. Small pages or tasks
74
+ that need broad context will yield different results.
75
+
76
+ Targeted find makes seven `DOM.describeNode` calls per task, compared with
77
+ 8,688 in the baseline and 2,894 for the new paged snapshots. All WebSkrap modes
78
+ made zero `Runtime.enable`, `Console.enable`, `Accessibility.enable` or
79
+ `Accessibility.getFullAXTree` calls during the measured task. This is a CDP
80
+ regression indicator, not evidence that a protected website accepts the browser.
81
+ No new public anti-bot acceptance test was run.
82
+
83
+ Playwright completes trusted interactions faster in this task. WebSkrap's
84
+ read-only find is faster in these samples, but its total task is slower.
85
+ Summed RSS is higher than the old baseline in these samples; this method does
86
+ not establish a memory improvement. Literal find avoids regex subprocesses;
87
+ caller-supplied regex still pays worker startup and timeout protection costs.
88
+
89
+ The server instruction text is 656 tokens versus 658 before this change.
90
+ The new find tool definition is 425 tokens when encoded as its full MCP tool
91
+ record. A client that includes all tool schemas in each model prompt pays
92
+ that extra context cost, which is excluded from the task table.
93
+
94
+ ## Reproduce
95
+
96
+ 1. Run `git show 914e24576ebe92e08d46c2867d065343b1b71fdc:src/webskrap/cdp_dom.py > /tmp/webskrap-snapshot-baseline.py`.
97
+ 2. Run `uv run --with tiktoken==0.14.0 python scripts/benchmark_snapshots.py --baseline /tmp/webskrap-snapshot-baseline.py`.
98
+ 3. For the optional external comparison, run `uv run --with tiktoken==0.14.0 python scripts/benchmark_playwright_snapshots.py`.
99
+
100
+ The first script accepts `--repeats`. The second requires npx and installs only
101
+ into its external cache. Neither adds a WebSkrap runtime dependency. Both
102
+ explicitly disable Chromium's sandbox for this host. Use a sandbox on hosts
103
+ that support it. Load only a trusted baseline source file.
104
+
105
+ Raw measurements: [WebSkrap](mcp-snapshots-webskrap.json) and
106
+ [Playwright MCP](mcp-snapshots-playwright.json). Implementation decisions and
107
+ semantic limits are in [the design note](../design/mcp-snapshots.md).
108
+
109
+ ## Validation and review
110
+
111
+ The complete local non-live suite passed: 966 tests, 30 live tests deselected,
112
+ 88.20% coverage. Review then found that the existing frame-navigation error
113
+ handler could swallow an iframe regex-worker timeout. The handler now propagates
114
+ search usage/internal errors. The final focused run passed 47 tests with appended
115
+ coverage of 88.34%, including an independent regression test for that error path.
116
+
117
+ Final checks passed: Ruff lint and format, Pyright 1.1.411 for Linux, Windows
118
+ and Darwin, Bandit, source/wheel build and Twine metadata validation. The review
119
+ was performed locally. Public live tests and Windows/macOS runtime tests were
120
+ not run on this Linux host. Cross-platform typing does not replace those tests.
121
+
122
+ The lockfile pins `opentelemetry-api==1.45.2`, which fixes
123
+ [GHSA-mf5v-w3w3-f6cg](https://osv.dev/vulnerability/GHSA-mf5v-w3w3-f6cg)
124
+ (CVE-2026-126360, traceparent parsing denial of service). The previous locked
125
+ version, 1.45.1, failed the CI audit. Only that package's version and artifact
126
+ metadata changed. Both pip-audit and the OSV scan of 51 locked public packages
127
+ now report no known vulnerabilities. No new dependency was added.
128
+
129
+ After the audit repair, the full local non-live suite passed 967 tests with
130
+ 89.15% coverage. Windows CI also exposed a fixture race: challenge detection
131
+ can return before an iframe's image request reaches the proxy. The local test
132
+ now defers that request and waits for its actual arrival with a five-second
133
+ deadline. The old immediate assertion failed against the delayed fixture;
134
+ all six transition/authentication cases pass with the bounded wait. The final
135
+ combined frame/snapshot run passed 30 tests. Production challenge handling is
136
+ unchanged.
137
+
138
+ Review follow-up validation: the full non-live run passed 980 tests with 89.15%
139
+ coverage. The final focused run passed 31 tests, including additional Unicode
140
+ separator cases. The broader session/MCP regression run passed 81 tests. New
141
+ regressions cover inline paragraph/status/value searches, ref-like visible text,
142
+ URL and ARIA line injection, and stacked modal opening/closing order. Ruff,
143
+ three-platform Pyright, Bandit, pip-audit, OSV, build and Twine passed. Public
144
+ live tests were excluded; local cross-platform runtime tests remain delegated
145
+ to CI. No benchmark numbers were regenerated for this follow-up.
@@ -0,0 +1,92 @@
1
+ # Targeted MCP accessibility reads
2
+
3
+ WebSkrap keeps its native CDP DOM walker. It does not enable Chromium's
4
+ Accessibility domain or Runtime event domain to generate snapshots.
5
+
6
+ ## Existing behavior and changes
7
+
8
+ The starting implementation already had implicit ARIA roles, common accessible
9
+ name rules, states, wrapper elision, text merging, depth limits, open shadow
10
+ roots, iframe traversal, backend-node refs and character pagination. Actions
11
+ already returned page state without an automatic snapshot.
12
+
13
+ [Playwright MCP snapshots](https://playwright.dev/mcp/snapshots) describes
14
+ subtree targets, depth limits, optional boxes and matching nodes under ancestor
15
+ paths. WebSkrap adopts these features while keeping its selector and ref
16
+ contracts. Playwright locator expressions are not accepted. A CSS selector
17
+ must uniquely match an element in the main document. A frame ref selects that
18
+ frame; an element ref also works inside an open shadow root.
19
+
20
+ `browser_snapshot` adds optional `target` and `boxes` arguments after the
21
+ existing positional arguments. Its response fields remain unchanged. Depth is
22
+ relative to the selected root. Zero and null retain the unlimited-depth
23
+ meaning. Negative depth returns a usage error. Boxes are rounded CSS pixels
24
+ relative to the top-level viewport, including iframe offsets.
25
+
26
+ `browser_find` returns the same snapshot-window fields, plus match counts.
27
+ It searches semantic lines, including text and non-password values appended
28
+ after refs, and retains their ancestor paths. Only structural ref and box
29
+ tokens are removed for matching; ref-like text in names or content is kept.
30
+ It omits sibling subtrees and link URL metadata. Search takes either case-insensitive literal
31
+ text or Python regex. Literal text is escaped before compilation. Caller regex
32
+ uses the existing subprocess worker and two-second timeout. Regex never runs
33
+ in the page. Search has a match cap and independent character pagination.
34
+
35
+ ## Ref and response cost
36
+
37
+ The DOM walk runs in an isolated world and returns semantic text plus remote
38
+ node objects. Search filters the text before issuing `DOM.describeNode`.
39
+ Only matching nodes, their ancestors and iframe owners need backend IDs.
40
+ Iframe owners remain available while child frames are searched. A final pass
41
+ removes nonmatching frame paths and applies the global match limit.
42
+
43
+ The walker still builds each available frame's semantic tree. This avoids a
44
+ second naming implementation and keeps semantics consistent with snapshots.
45
+ It does not make search traversal proportional only to the number of matches.
46
+ There is no persistent tree cache. Remote object groups are released on errors
47
+ and successful reads. Ref replacement uses one substitution pass. Per-call
48
+ random placeholders prevent page text that resembles a placeholder from being
49
+ used as a remote-object index.
50
+
51
+ Refs keep Chromium's backend-node identity. Repeated snapshots, find calls and
52
+ SPA updates preserve refs when they preserve the underlying node. A detached
53
+ node or a changed document returns `stale_ref`. There is no snapshot-generation
54
+ counter and no model-specific ref remapping. Existing interaction mechanisms
55
+ remain in use.
56
+
57
+ Responses do not repeat snapshots after actions. A caller can find, interact,
58
+ wait for a condition and request only the next needed control. A diff cache
59
+ was not added: this change measures explicit targeted reads, without adding a
60
+ second state lifetime or requiring clients to reconstruct trees.
61
+
62
+ ## Semantics and limits
63
+
64
+ Link URL metadata uses quoted JSON strings, including escaped line separators.
65
+ A URL cannot add structural lines or refs to the snapshot. Search still omits
66
+ URL metadata. Dynamic ARIA state text is normalized to one line.
67
+
68
+ With stacked native modal dialogs, snapshots select the modal containing
69
+ `document.activeElement`. If focus does not identify a modal, the last modal in
70
+ document order is used. That fallback is an approximation of top-layer order.
71
+ Scope checks apply to the selected modal, and closing it restores the lower
72
+ modal's context.
73
+
74
+ The shared walker now also omits password input values, inert content, closed
75
+ details content and background content behind native modal dialogs. It resolves
76
+ `aria-labelledby` in the element's document or shadow root. Disabled fieldsets,
77
+ expanded=false, mixed pressed state, required, readonly, invalid and busy state
78
+ are represented. Existing compact role/name and common naming rules remain;
79
+ this is not a complete implementation of the browser accessibility algorithm.
80
+
81
+ Closed shadow roots and nested frames inside out-of-process iframes retain the
82
+ driver's existing limits. Frame navigation during traversal can omit that frame.
83
+ A snapshot is not an atomic DOM transaction. Continuation offsets are valid
84
+ only while the DOM remains unchanged. A character window can split a line or
85
+ ref. Match caps precede character caps. There are no sibling gap markers;
86
+ ancestor paths are intentionally partial trees.
87
+
88
+ Password redaction does not remove secrets from visible text, labels, URLs or
89
+ other field values. Snapshot and find output must receive the same privacy
90
+ handling as other browser reads.
91
+
92
+ See [measured workflows](../benchmarks/mcp-snapshots.md) for results and limits.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "webskrap"
7
- version = "3.2.4"
7
+ version = "3.3.0"
8
8
  description = "Async Python scraping on Chromium over its own CDP client, with coherent browser profiles and session controls."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"