scraper-rust 0.6.0__tar.gz → 0.7.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 (52) hide show
  1. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/.github/workflows/release.yml +43 -72
  2. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/.github/workflows/tests.yml +3 -6
  3. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/AGENTS.md +9 -8
  4. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/Cargo.lock +53 -54
  5. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/Cargo.toml +4 -4
  6. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/PKG-INFO +16 -5
  7. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/README.md +15 -4
  8. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/api.md +1 -1
  9. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/async-api.md +2 -0
  10. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/pyproject.toml +1 -1
  11. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/scraper_rs/asyncio.py +11 -11
  12. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/prettify.rs +42 -4
  13. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/tests.rs +4 -0
  14. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/xpath.rs +8 -12
  15. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/uv.lock +41 -41
  16. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/.github/workflows/benchmark.yml +0 -0
  17. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/.github/workflows/bump-version.yml +0 -0
  18. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/.gitignore +0 -0
  19. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/LICENSE +0 -0
  20. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/benchmarks/README.md +0 -0
  21. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/benchmarks/common.rs +0 -0
  22. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/benchmarks/parse_scaling.rs +0 -0
  23. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/benchmarks/sync_async.rs +0 -0
  24. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/build.rs +0 -0
  25. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/README.md +0 -0
  26. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/architecture.md +0 -0
  27. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/development.md +0 -0
  28. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/docs/limits-and-errors.md +0 -0
  29. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/examples/demo.py +0 -0
  30. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/examples/demo_async_document.py +0 -0
  31. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/examples/demo_asyncio.py +0 -0
  32. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/examples/demo_prettify_url.py +0 -0
  33. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/justfile +0 -0
  34. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/py.typed +0 -0
  35. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/scraper_rs/__init__.py +0 -0
  36. /scraper_rust-0.6.0/scraper_rs.pyi → /scraper_rust-0.7.0/scraper_rs/__init__.pyi +0 -0
  37. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/scraper_rs/asyncio.pyi +0 -0
  38. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/async_core.rs +0 -0
  39. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/cache.rs +0 -0
  40. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/document.rs +0 -0
  41. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/element.rs +0 -0
  42. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/functions.rs +0 -0
  43. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/html5_dict.rs +0 -0
  44. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/lib.rs +0 -0
  45. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/limits.rs +0 -0
  46. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/runtime.rs +0 -0
  47. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/selectors.rs +0 -0
  48. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/text.rs +0 -0
  49. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/src/tl_dom.rs +0 -0
  50. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/tests/test_asyncio.py +0 -0
  51. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/tests/test_performance.py +0 -0
  52. {scraper_rust-0.6.0 → scraper_rust-0.7.0}/tests/test_scraper.py +0 -0
@@ -82,6 +82,7 @@ jobs:
82
82
  with:
83
83
  name: wheels-${{ matrix.name }}
84
84
  path: dist/*.whl
85
+ if-no-files-found: error
85
86
 
86
87
  - name: Upload wheels to release
87
88
  uses: softprops/action-gh-release@v2
@@ -91,71 +92,6 @@ jobs:
91
92
  env:
92
93
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93
94
 
94
- build-free-threaded-wheels:
95
- name: Build free-threaded wheels on ${{ matrix.name }} (Python ${{ matrix.python-version }})
96
- runs-on: ${{ matrix.runner }}
97
- strategy:
98
- fail-fast: false
99
- matrix:
100
- python-version: ['3.13t', '3.14t']
101
- include:
102
- - name: linux-x86_64
103
- runner: ubuntu-latest
104
- platform: linux
105
- - name: linux-arm64
106
- runner: ubuntu-24.04-arm
107
- platform: linux
108
- - name: windows
109
- runner: windows-latest
110
- platform: windows
111
- - name: macos
112
- runner: macos-latest
113
- platform: macos
114
-
115
- steps:
116
- - name: Checkout repository
117
- uses: actions/checkout@v6
118
-
119
- - name: Set up Python
120
- uses: actions/setup-python@v6
121
- with:
122
- python-version: ${{ matrix.python-version }}
123
- allow-prereleases: true
124
-
125
- - name: Install zig (for Linux manylinux builds)
126
- if: matrix.platform == 'linux'
127
- uses: goto-bus-stop/setup-zig@v2
128
- with:
129
- version: 0.11.0
130
-
131
- - name: Build free-threaded wheels (Linux - using zig for manylinux)
132
- if: matrix.platform == 'linux'
133
- uses: PyO3/maturin-action@v1
134
- with:
135
- command: build
136
- args: --release --strip --out dist --zig --interpreter python
137
-
138
- - name: Build free-threaded wheels (Windows/macOS)
139
- if: matrix.platform == 'windows' || matrix.platform == 'macos'
140
- uses: PyO3/maturin-action@v1
141
- with:
142
- command: build
143
- args: --release --strip --out dist
144
-
145
- - name: Upload free-threaded wheels as artifacts
146
- uses: actions/upload-artifact@v7
147
- with:
148
- name: wheels-free-threaded-${{ matrix.name }}-${{ matrix.python-version }}
149
- path: dist/*.whl
150
-
151
- - name: Upload free-threaded wheels to release
152
- uses: softprops/action-gh-release@v2
153
- with:
154
- files: dist/*.whl
155
- fail_on_unmatched_files: true
156
- env:
157
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
158
-
159
95
  build-sdist:
160
96
  name: Build source distribution
161
97
  runs-on: ubuntu-latest
@@ -174,6 +110,7 @@ jobs:
174
110
  with:
175
111
  name: sdist
176
112
  path: dist/*.tar.gz
113
+ if-no-files-found: error
177
114
 
178
115
  - name: Upload sdist to release
179
116
  uses: softprops/action-gh-release@v2
@@ -186,7 +123,7 @@ jobs:
186
123
  publish-to-pypi:
187
124
  name: Publish to PyPI
188
125
  runs-on: ubuntu-latest
189
- needs: [build-wheels, build-free-threaded-wheels, build-sdist]
126
+ needs: [build-wheels, build-sdist]
190
127
  environment:
191
128
  name: pypi
192
129
  url: https://pypi.org/p/scraper-rust
@@ -208,14 +145,48 @@ jobs:
208
145
  pattern: 'sdist'
209
146
  merge-multiple: true
210
147
 
211
- - name: List all files to be uploaded
148
+ - name: Validate distributions
212
149
  run: |
213
- echo "Files in dist directory:"
214
150
  ls -lah dist/
215
- echo "Total wheel files:"
216
- ls -1 dist/*.whl | wc -l
217
- echo "All wheel files:"
218
- ls -1 dist/*.whl
151
+ python - <<'PY'
152
+ from pathlib import Path
153
+ import struct
154
+ import tarfile
155
+ import zipfile
156
+
157
+ dist = Path("dist")
158
+ wheels = sorted(dist.glob("*.whl"))
159
+ sdists = sorted(dist.glob("*.tar.gz"))
160
+
161
+ if len(wheels) != 4:
162
+ raise SystemExit(f"Expected 4 platform wheels, found {len(wheels)}")
163
+ if len(sdists) != 1:
164
+ raise SystemExit(f"Expected 1 source distribution, found {len(sdists)}")
165
+
166
+ for wheel in wheels:
167
+ data = wheel.read_bytes()
168
+ eocd = data.rfind(b"PK\x05\x06")
169
+ if eocd < 0 or eocd + 22 > len(data):
170
+ raise SystemExit(f"{wheel}: missing or truncated ZIP end record")
171
+
172
+ comment_length = struct.unpack_from("<H", data, eocd + 20)[0]
173
+ archive_end = eocd + 22 + comment_length
174
+ if comment_length:
175
+ raise SystemExit(f"{wheel}: ZIP comments are not accepted by PyPI")
176
+ if archive_end != len(data):
177
+ trailing = len(data) - archive_end
178
+ raise SystemExit(f"{wheel}: contains {trailing} trailing bytes")
179
+
180
+ with zipfile.ZipFile(wheel) as archive:
181
+ bad_member = archive.testzip()
182
+ if bad_member is not None:
183
+ raise SystemExit(f"{wheel}: corrupt member {bad_member}")
184
+
185
+ with tarfile.open(sdists[0], mode="r:gz") as archive:
186
+ archive.getmembers()
187
+
188
+ print(f"Validated {len(wheels)} wheels and {len(sdists)} source distribution")
189
+ PY
219
190
 
220
191
  - name: Publish to PyPI
221
192
  uses: pypa/gh-action-pypi-publish@release/v1
@@ -16,7 +16,8 @@ jobs:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- python-version: ${{ fromJSON(github.event.inputs.python-version != '' && format('["{0}"]', github.event.inputs.python-version) || '["3.10","3.11","3.12","3.13","3.13t","3.14","3.14t"]') }}
19
+ # PyO3 0.29 supports free-threaded CPython starting with 3.14.
20
+ python-version: ${{ fromJSON(github.event.inputs.python-version != '' && format('["{0}"]', github.event.inputs.python-version) || '["3.10","3.11","3.12","3.13","3.14","3.14t"]') }}
20
21
 
21
22
  steps:
22
23
  - name: Checkout repository
@@ -44,11 +45,7 @@ jobs:
44
45
  - name: Build extension module
45
46
  run: |
46
47
  source .venv/bin/activate
47
- if [[ "${{ matrix.python-version }}" == "3.13t" || "${{ matrix.python-version }}" == "3.14t" ]]; then
48
- maturin develop --release --locked
49
- else
50
- maturin develop --release --locked
51
- fi
48
+ maturin develop --release --locked
52
49
 
53
50
  - name: Run tests
54
51
  run: |
@@ -23,7 +23,7 @@ This file contains essential information for Large Language Models (LLMs) to eff
23
23
  - `pyo3-async-runtimes` v0.28 with `tokio-runtime` feature
24
24
  - `tokio` v1 (rt, macros) for async wrappers
25
25
  - **Python** 3.10+
26
- - Type annotations and stub files (`scraper_rs.pyi`, `py.typed`)
26
+ - Type annotations and stub files (`__init__.pyi`, `py.typed`)
27
27
  - Async support via `asyncio` module
28
28
  - **Build System**
29
29
  - `maturin` - Build PyO3 wheels
@@ -59,8 +59,9 @@ scraper-rs/
59
59
  │ ├── demo_asyncio.py # Async usage examples
60
60
  │ ├── demo_async_document.py
61
61
  │ └── demo_prettify_url.py
62
- ├── scraper_rs/ # Python package directory (async wrappers + import glue)
63
- ├── scraper_rs.pyi # Type stub file
62
+ ├── scraper_rs/
63
+ ├── __init__.pyi # Type stub file
64
+ │ └── asyncio.pyi # async wrappers
64
65
  ├── py.typed # PEP 561 marker for type support
65
66
  ├── Cargo.toml # Rust package configuration
66
67
  ├── pyproject.toml # Python package configuration
@@ -77,7 +78,7 @@ scraper-rs/
77
78
  - **`src/functions.rs`**: Top-level helpers: `parse()`, `parse_document()`, `parse_fragment()`, `prettify()`, `select()`, `select_first()`, `first()`, `xpath()`, `xpath_first()`
78
79
  - **`src/selectors.rs`**, **`src/xpath.rs`**, **`src/limits.rs`**: selector caches, XPath conversion, size limit enforcement, and UTF-8-safe truncation
79
80
 
80
- - **`scraper_rs.pyi`**: Type stubs for IDE support and type checking
81
+ - **`__init__.pyi`**: Type stubs for IDE support and type checking
81
82
 
82
83
  - **`Cargo.toml`**: Rust crate configuration
83
84
  - `crate-type = ["cdylib"]` for Python extension module
@@ -240,7 +241,7 @@ Module: `scraper_rs.asyncio`
240
241
  - Release build optimizations in `Cargo.toml` for smaller binary size
241
242
 
242
243
  ### Python Code
243
- - Type annotations required (uses `scraper_rs.pyi` stub file)
244
+ - Type annotations required (uses `__init__.pyi` stub file)
244
245
  - Follow PEP 8 style guidelines
245
246
  - Use descriptive variable names
246
247
  - Include docstrings for public functions
@@ -258,7 +259,7 @@ Module: `scraper_rs.asyncio`
258
259
  ### Adding a New Method to Document
259
260
  1. Add Rust implementation in `src/document.rs` under `impl Document`
260
261
  2. Add `#[pymethods]` annotation if needed
261
- 3. Update `scraper_rs.pyi` with type signature
262
+ 3. Update `__init__.pyi` with type signature
262
263
  4. Add tests in `tests/test_scraper.py`
263
264
  5. If it should be async, update `scraper_rs/asyncio.py` and `scraper_rs/asyncio.pyi`
264
265
  6. Update README.md with usage example
@@ -269,7 +270,7 @@ Module: `scraper_rs.asyncio`
269
270
  1. Add Rust function in `src/functions.rs` with `#[pyfunction]`
270
271
  2. Register with `m.add_function(wrap_pyfunction!(function_name, m)?)?` in `scraper_rs` module
271
272
  3. Export in `__all__` if applicable
272
- 4. Update `scraper_rs.pyi`
273
+ 4. Update `__init__.pyi`
273
274
  5. Add async wrapper in `scraper_rs/asyncio.py` and `scraper_rs/asyncio.pyi` if needed
274
275
  6. Add tests
275
276
  7. Update documentation
@@ -296,7 +297,7 @@ See `.github/workflows/release.yml` and `.github/workflows/bump-version.yml`
296
297
  4. **UTF-8 safety**: Truncation must respect character boundaries
297
298
  5. **Async wrappers**: Public async wrappers live in `scraper_rs/asyncio.py`; keep their stubs and tests aligned with sync behavior
298
299
  6. **Rust modules are split by responsibility**: `src/lib.rs` is the module entry point; add core behavior to the focused module that owns it
299
- 7. **Type stubs are critical**: Always update `scraper_rs.pyi` when changing APIs
300
+ 7. **Type stubs are critical**: Always update `__init__.pyi` when changing APIs
300
301
  8. **Tests are comprehensive**: Follow existing test patterns in `tests/`
301
302
  9. **Build artifacts**: `target/` and `.venv/` should not be committed
302
303
  10. **Use `just` commands**: They handle common workflows correctly
@@ -124,7 +124,7 @@ checksum = "56791e4bd64c99fc361e01008f45c984baa93f12a0957d1b3c51dd2c6baab453"
124
124
  dependencies = [
125
125
  "proc-macro2",
126
126
  "quote",
127
- "syn 2.0.117",
127
+ "syn 2.0.119",
128
128
  ]
129
129
 
130
130
  [[package]]
@@ -148,7 +148,7 @@ dependencies = [
148
148
  "proc-macro-crate",
149
149
  "proc-macro2",
150
150
  "quote",
151
- "syn 2.0.117",
151
+ "syn 2.0.119",
152
152
  ]
153
153
 
154
154
  [[package]]
@@ -392,7 +392,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
392
392
  dependencies = [
393
393
  "proc-macro2",
394
394
  "quote",
395
- "syn 2.0.117",
395
+ "syn 2.0.119",
396
396
  ]
397
397
 
398
398
  [[package]]
@@ -419,7 +419,7 @@ dependencies = [
419
419
  "once_cell",
420
420
  "proc-macro2",
421
421
  "quote",
422
- "syn 2.0.117",
422
+ "syn 2.0.119",
423
423
  ]
424
424
 
425
425
  [[package]]
@@ -465,41 +465,41 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
465
465
 
466
466
  [[package]]
467
467
  name = "futures-channel"
468
- version = "0.3.32"
468
+ version = "0.3.33"
469
469
  source = "registry+https://github.com/rust-lang/crates.io-index"
470
- checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
470
+ checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
471
471
  dependencies = [
472
472
  "futures-core",
473
473
  ]
474
474
 
475
475
  [[package]]
476
476
  name = "futures-core"
477
- version = "0.3.32"
477
+ version = "0.3.33"
478
478
  source = "registry+https://github.com/rust-lang/crates.io-index"
479
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
479
+ checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
480
480
 
481
481
  [[package]]
482
482
  name = "futures-macro"
483
- version = "0.3.32"
483
+ version = "0.3.33"
484
484
  source = "registry+https://github.com/rust-lang/crates.io-index"
485
- checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
485
+ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
486
486
  dependencies = [
487
487
  "proc-macro2",
488
488
  "quote",
489
- "syn 2.0.117",
489
+ "syn 2.0.119",
490
490
  ]
491
491
 
492
492
  [[package]]
493
493
  name = "futures-task"
494
- version = "0.3.32"
494
+ version = "0.3.33"
495
495
  source = "registry+https://github.com/rust-lang/crates.io-index"
496
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
496
+ checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
497
497
 
498
498
  [[package]]
499
499
  name = "futures-util"
500
- version = "0.3.32"
500
+ version = "0.3.33"
501
501
  source = "registry+https://github.com/rust-lang/crates.io-index"
502
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
502
+ checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
503
503
  dependencies = [
504
504
  "futures-core",
505
505
  "futures-macro",
@@ -1006,7 +1006,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
1006
1006
  dependencies = [
1007
1007
  "proc-macro2",
1008
1008
  "quote",
1009
- "syn 2.0.117",
1009
+ "syn 2.0.119",
1010
1010
  ]
1011
1011
 
1012
1012
  [[package]]
@@ -1082,7 +1082,7 @@ dependencies = [
1082
1082
  "proc-macro2",
1083
1083
  "quote",
1084
1084
  "strum",
1085
- "syn 2.0.117",
1085
+ "syn 2.0.119",
1086
1086
  "thiserror",
1087
1087
  ]
1088
1088
 
@@ -1256,7 +1256,7 @@ dependencies = [
1256
1256
  "quote",
1257
1257
  "regex-syntax 0.8.10",
1258
1258
  "rustc_version",
1259
- "syn 2.0.117",
1259
+ "syn 2.0.119",
1260
1260
  ]
1261
1261
 
1262
1262
  [[package]]
@@ -1336,7 +1336,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
1336
1336
  dependencies = [
1337
1337
  "proc-macro2",
1338
1338
  "quote",
1339
- "syn 2.0.117",
1339
+ "syn 2.0.119",
1340
1340
  ]
1341
1341
 
1342
1342
  [[package]]
@@ -1556,9 +1556,9 @@ dependencies = [
1556
1556
 
1557
1557
  [[package]]
1558
1558
  name = "pyo3"
1559
- version = "0.28.3"
1559
+ version = "0.29.0"
1560
1560
  source = "registry+https://github.com/rust-lang/crates.io-index"
1561
- checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
1561
+ checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
1562
1562
  dependencies = [
1563
1563
  "libc",
1564
1564
  "once_cell",
@@ -1570,9 +1570,9 @@ dependencies = [
1570
1570
 
1571
1571
  [[package]]
1572
1572
  name = "pyo3-async-runtimes"
1573
- version = "0.28.0"
1573
+ version = "0.29.0"
1574
1574
  source = "registry+https://github.com/rust-lang/crates.io-index"
1575
- checksum = "9e7364a95bf00e8377bbf9b0f09d7ff9715a29d8fcf93b47d1a967363b973178"
1575
+ checksum = "b3ef68daa7316a3fac65e5e18b2203f010346de1c1c53456811a2624673ab046"
1576
1576
  dependencies = [
1577
1577
  "futures-channel",
1578
1578
  "futures-util",
@@ -1584,18 +1584,18 @@ dependencies = [
1584
1584
 
1585
1585
  [[package]]
1586
1586
  name = "pyo3-build-config"
1587
- version = "0.28.3"
1587
+ version = "0.29.0"
1588
1588
  source = "registry+https://github.com/rust-lang/crates.io-index"
1589
- checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
1589
+ checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
1590
1590
  dependencies = [
1591
1591
  "target-lexicon",
1592
1592
  ]
1593
1593
 
1594
1594
  [[package]]
1595
1595
  name = "pyo3-ffi"
1596
- version = "0.28.3"
1596
+ version = "0.29.0"
1597
1597
  source = "registry+https://github.com/rust-lang/crates.io-index"
1598
- checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
1598
+ checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
1599
1599
  dependencies = [
1600
1600
  "libc",
1601
1601
  "pyo3-build-config",
@@ -1603,34 +1603,33 @@ dependencies = [
1603
1603
 
1604
1604
  [[package]]
1605
1605
  name = "pyo3-macros"
1606
- version = "0.28.3"
1606
+ version = "0.29.0"
1607
1607
  source = "registry+https://github.com/rust-lang/crates.io-index"
1608
- checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
1608
+ checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
1609
1609
  dependencies = [
1610
1610
  "proc-macro2",
1611
1611
  "pyo3-macros-backend",
1612
1612
  "quote",
1613
- "syn 2.0.117",
1613
+ "syn 2.0.119",
1614
1614
  ]
1615
1615
 
1616
1616
  [[package]]
1617
1617
  name = "pyo3-macros-backend"
1618
- version = "0.28.3"
1618
+ version = "0.29.0"
1619
1619
  source = "registry+https://github.com/rust-lang/crates.io-index"
1620
- checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
1620
+ checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
1621
1621
  dependencies = [
1622
1622
  "heck",
1623
1623
  "proc-macro2",
1624
- "pyo3-build-config",
1625
1624
  "quote",
1626
- "syn 2.0.117",
1625
+ "syn 2.0.119",
1627
1626
  ]
1628
1627
 
1629
1628
  [[package]]
1630
1629
  name = "quote"
1631
- version = "1.0.45"
1630
+ version = "1.0.46"
1632
1631
  source = "registry+https://github.com/rust-lang/crates.io-index"
1633
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
1632
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
1634
1633
  dependencies = [
1635
1634
  "proc-macro2",
1636
1635
  ]
@@ -1864,7 +1863,7 @@ dependencies = [
1864
1863
 
1865
1864
  [[package]]
1866
1865
  name = "scraper-rs"
1867
- version = "0.6.0"
1866
+ version = "0.7.0"
1868
1867
  dependencies = [
1869
1868
  "criterion",
1870
1869
  "pyo3",
@@ -1915,7 +1914,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1915
1914
  dependencies = [
1916
1915
  "proc-macro2",
1917
1916
  "quote",
1918
- "syn 2.0.117",
1917
+ "syn 2.0.119",
1919
1918
  ]
1920
1919
 
1921
1920
  [[package]]
@@ -1998,7 +1997,7 @@ dependencies = [
1998
1997
  "heck",
1999
1998
  "proc-macro2",
2000
1999
  "quote",
2001
- "syn 2.0.117",
2000
+ "syn 2.0.119",
2002
2001
  ]
2003
2002
 
2004
2003
  [[package]]
@@ -2014,9 +2013,9 @@ dependencies = [
2014
2013
 
2015
2014
  [[package]]
2016
2015
  name = "syn"
2017
- version = "2.0.117"
2016
+ version = "2.0.119"
2018
2017
  source = "registry+https://github.com/rust-lang/crates.io-index"
2019
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
2018
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
2020
2019
  dependencies = [
2021
2020
  "proc-macro2",
2022
2021
  "quote",
@@ -2042,7 +2041,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
2042
2041
  dependencies = [
2043
2042
  "proc-macro2",
2044
2043
  "quote",
2045
- "syn 2.0.117",
2044
+ "syn 2.0.119",
2046
2045
  ]
2047
2046
 
2048
2047
  [[package]]
@@ -2074,7 +2073,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
2074
2073
  dependencies = [
2075
2074
  "proc-macro2",
2076
2075
  "quote",
2077
- "syn 2.0.117",
2076
+ "syn 2.0.119",
2078
2077
  ]
2079
2078
 
2080
2079
  [[package]]
@@ -2114,9 +2113,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2114
2113
 
2115
2114
  [[package]]
2116
2115
  name = "tokio"
2117
- version = "1.52.3"
2116
+ version = "1.53.0"
2118
2117
  source = "registry+https://github.com/rust-lang/crates.io-index"
2119
- checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
2118
+ checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
2120
2119
  dependencies = [
2121
2120
  "pin-project-lite",
2122
2121
  "tokio-macros",
@@ -2130,7 +2129,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
2130
2129
  dependencies = [
2131
2130
  "proc-macro2",
2132
2131
  "quote",
2133
- "syn 2.0.117",
2132
+ "syn 2.0.119",
2134
2133
  ]
2135
2134
 
2136
2135
  [[package]]
@@ -2273,7 +2272,7 @@ dependencies = [
2273
2272
  "bumpalo",
2274
2273
  "proc-macro2",
2275
2274
  "quote",
2276
- "syn 2.0.117",
2275
+ "syn 2.0.119",
2277
2276
  "wasm-bindgen-shared",
2278
2277
  ]
2279
2278
 
@@ -2348,7 +2347,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2348
2347
  dependencies = [
2349
2348
  "proc-macro2",
2350
2349
  "quote",
2351
- "syn 2.0.117",
2350
+ "syn 2.0.119",
2352
2351
  ]
2353
2352
 
2354
2353
  [[package]]
@@ -2359,7 +2358,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2359
2358
  dependencies = [
2360
2359
  "proc-macro2",
2361
2360
  "quote",
2362
- "syn 2.0.117",
2361
+ "syn 2.0.119",
2363
2362
  ]
2364
2363
 
2365
2364
  [[package]]
@@ -2589,7 +2588,7 @@ checksum = "30d233fc2ef8ab1a9f9bf7af9cc1c7f7e7d75eb9848f731a99d8f1d4c2259fe3"
2589
2588
  dependencies = [
2590
2589
  "proc-macro2",
2591
2590
  "quote",
2592
- "syn 2.0.117",
2591
+ "syn 2.0.119",
2593
2592
  "xee-schema-type",
2594
2593
  "xee-xpath-ast",
2595
2594
  "xot",
@@ -2652,7 +2651,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
2652
2651
  dependencies = [
2653
2652
  "proc-macro2",
2654
2653
  "quote",
2655
- "syn 2.0.117",
2654
+ "syn 2.0.119",
2656
2655
  "synstructure",
2657
2656
  ]
2658
2657
 
@@ -2673,7 +2672,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
2673
2672
  dependencies = [
2674
2673
  "proc-macro2",
2675
2674
  "quote",
2676
- "syn 2.0.117",
2675
+ "syn 2.0.119",
2677
2676
  ]
2678
2677
 
2679
2678
  [[package]]
@@ -2693,7 +2692,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
2693
2692
  dependencies = [
2694
2693
  "proc-macro2",
2695
2694
  "quote",
2696
- "syn 2.0.117",
2695
+ "syn 2.0.119",
2697
2696
  "synstructure",
2698
2697
  ]
2699
2698
 
@@ -2727,7 +2726,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
2727
2726
  dependencies = [
2728
2727
  "proc-macro2",
2729
2728
  "quote",
2730
- "syn 2.0.117",
2729
+ "syn 2.0.119",
2731
2730
  ]
2732
2731
 
2733
2732
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "scraper-rs"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -15,8 +15,8 @@ default = []
15
15
  extension-module = ["pyo3/extension-module"]
16
16
 
17
17
  [dependencies]
18
- pyo3 = { version = "0.28.3", features = ["abi3-py310"] }
19
- pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
18
+ pyo3 = { version = "0.29.0", features = ["abi3-py310"] }
19
+ pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
20
20
  rayon = "1"
21
21
  tokio = { version = "1", features = ["rt", "macros", "sync"] }
22
22
 
@@ -28,7 +28,7 @@ criterion = { version = "0.8", features = ["async_tokio"] }
28
28
  tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
29
29
 
30
30
  [build-dependencies]
31
- pyo3-build-config = { version = "0.28.3", features = ["resolve-config"] }
31
+ pyo3-build-config = { version = "0.29.0", features = ["resolve-config"] }
32
32
 
33
33
  [[bench]]
34
34
  name = "sync_async"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scraper-rust
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -33,6 +33,15 @@ Project-URL: Repository, https://github.com/RustedBytes/scraper-rs
33
33
 
34
34
  Python bindings for high-performance Rust HTML parsing via PyO3. It provides a lightweight `Document`/`Element` API with CSS selectors backed by `rustedbytes-tl`, XPath (via `xee-xpath`), handy helpers, and zero Python-side parsing work.
35
35
 
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install scraper-rust
40
+
41
+ # or using uv
42
+ uv add scraper-rust
43
+ ```
44
+
36
45
  ## Quick start
37
46
 
38
47
  ```py
@@ -50,7 +59,7 @@ print(doc.text) # "First Second"
50
59
 
51
60
  items = doc.select(".item")
52
61
  print(items[0].attr("data-id")) # "1"
53
- print(items[0].to_dict()) # {"tag": "div", "text": "First", "html": "<a...>", ...}
62
+ print(items[0].to_dict()) # {"tag": "div", "text": "First", "html": "<a...>", ...}
54
63
 
55
64
  first_link = doc.select_first("a[href]") # alias: doc.find(...)
56
65
  print(first_link.text, first_link.attr("href")) # First /a
@@ -65,9 +74,11 @@ print(doc.xpath_first("//div[@data-id='1']/a").attr("href")) # "/a"
65
74
  # Functional helpers
66
75
  links = select(html, "a[href]")
67
76
  print([link.attr("href") for link in links]) # ["/a", "/b"]
68
- print(first(html, "a[href]").text) # First
69
- print(select_first(html, "a[href]").text) # First
70
- print([link.text for link in xpath(html, "//div[@class='item']/a")]) # ["First", "Second"]
77
+ print(first(html, "a[href]").text) # First
78
+ print(select_first(html, "a[href]").text) # First
79
+ print(
80
+ [link.text for link in xpath(html, "//div[@class='item']/a")]
81
+ ) # ["First", "Second"]
71
82
  print(prettify(html))
72
83
  ```
73
84
 
@@ -6,6 +6,15 @@
6
6
 
7
7
  Python bindings for high-performance Rust HTML parsing via PyO3. It provides a lightweight `Document`/`Element` API with CSS selectors backed by `rustedbytes-tl`, XPath (via `xee-xpath`), handy helpers, and zero Python-side parsing work.
8
8
 
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install scraper-rust
13
+
14
+ # or using uv
15
+ uv add scraper-rust
16
+ ```
17
+
9
18
  ## Quick start
10
19
 
11
20
  ```py
@@ -23,7 +32,7 @@ print(doc.text) # "First Second"
23
32
 
24
33
  items = doc.select(".item")
25
34
  print(items[0].attr("data-id")) # "1"
26
- print(items[0].to_dict()) # {"tag": "div", "text": "First", "html": "<a...>", ...}
35
+ print(items[0].to_dict()) # {"tag": "div", "text": "First", "html": "<a...>", ...}
27
36
 
28
37
  first_link = doc.select_first("a[href]") # alias: doc.find(...)
29
38
  print(first_link.text, first_link.attr("href")) # First /a
@@ -38,9 +47,11 @@ print(doc.xpath_first("//div[@data-id='1']/a").attr("href")) # "/a"
38
47
  # Functional helpers
39
48
  links = select(html, "a[href]")
40
49
  print([link.attr("href") for link in links]) # ["/a", "/b"]
41
- print(first(html, "a[href]").text) # First
42
- print(select_first(html, "a[href]").text) # First
43
- print([link.text for link in xpath(html, "//div[@class='item']/a")]) # ["First", "Second"]
50
+ print(first(html, "a[href]").text) # First
51
+ print(select_first(html, "a[href]").text) # First
52
+ print(
53
+ [link.text for link in xpath(html, "//div[@class='item']/a")]
54
+ ) # ["First", "Second"]
44
55
  print(prettify(html))
45
56
  ```
46
57
 
@@ -11,7 +11,7 @@ from scraper_rs import Document
11
11
 
12
12
  doc = Document(
13
13
  html,
14
- max_size_bytes=None, # default: 1 GiB
14
+ max_size_bytes=None, # default: 1 GiB
15
15
  truncate_on_limit=False, # default: error on oversized HTML
16
16
  )
17
17
  ```
@@ -31,12 +31,14 @@ from scraper_rs import asyncio as async_scraper
31
31
 
32
32
  html = "<div class='item'><a href='/a'>First</a></div>"
33
33
 
34
+
34
35
  async def main():
35
36
  async with await async_scraper.parse(html) as doc:
36
37
  items = await doc.select(".item")
37
38
  first_link = await items[0].select_first("a[href]")
38
39
  print(first_link.text, first_link.attr("href"))
39
40
 
41
+
40
42
  asyncio.run(main())
41
43
  ```
42
44
 
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scraper-rust"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Python bindings for high-performance HTML parsing and selection with PyO3"
9
9
  authors = [{ name = "Yehor Smoliakov", email = "egorsmkv@gmail.com" }]
10
10
  requires-python = ">=3.10"
@@ -2,17 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .scraper_rs import (
6
- _AsyncDocumentCore,
7
- _AsyncElementCore,
8
- first_async as _first_async,
9
- parse_async as _parse_async,
10
- prettify_async as _prettify_async,
11
- select_async as _select_async,
12
- select_first_async as _select_first_async,
13
- xpath_async as _xpath_async,
14
- xpath_first_async as _xpath_first_async,
15
- )
5
+ from . import scraper_rs as _core
6
+
7
+ _AsyncDocumentCore = _core._AsyncDocumentCore
8
+ _AsyncElementCore = _core._AsyncElementCore
9
+ _first_async = _core.first_async
10
+ _parse_async = _core.parse_async
11
+ _prettify_async = _core.prettify_async
12
+ _select_async = _core.select_async
13
+ _select_first_async = _core.select_first_async
14
+ _xpath_async = _core.xpath_async
15
+ _xpath_first_async = _core.xpath_first_async
16
16
 
17
17
 
18
18
  def _wrap_element(element: _AsyncElementCore | None) -> "AsyncElement | None":
@@ -19,6 +19,44 @@ pub(crate) fn escape_html(value: &str) -> String {
19
19
  escaped
20
20
  }
21
21
 
22
+ #[inline]
23
+ fn entity_reference_len(value: &str) -> Option<usize> {
24
+ let semicolon = value.find(';')?;
25
+ let reference = &value[..semicolon];
26
+ let valid = if let Some(numeric) = reference.strip_prefix('#') {
27
+ if let Some(hex) = numeric
28
+ .strip_prefix('x')
29
+ .or_else(|| numeric.strip_prefix('X'))
30
+ {
31
+ !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
32
+ } else {
33
+ !numeric.is_empty() && numeric.bytes().all(|byte| byte.is_ascii_digit())
34
+ }
35
+ } else {
36
+ !reference.is_empty() && reference.bytes().all(|byte| byte.is_ascii_alphanumeric())
37
+ };
38
+ valid.then_some(semicolon + 1)
39
+ }
40
+
41
+ #[inline]
42
+ fn escape_html_text(value: &str) -> String {
43
+ let mut escaped = String::with_capacity(value.len());
44
+ let mut rest = value;
45
+ while let Some((before, after_ampersand)) = rest.split_once('&') {
46
+ escaped.push_str(&escape_html(before));
47
+ rest = &rest[before.len() + 1..];
48
+ if let Some(reference_len) = entity_reference_len(after_ampersand) {
49
+ escaped.push('&');
50
+ escaped.push_str(&after_ampersand[..reference_len]);
51
+ rest = &after_ampersand[reference_len..];
52
+ } else {
53
+ escaped.push_str("&amp;");
54
+ }
55
+ }
56
+ escaped.push_str(&escape_html(rest));
57
+ escaped
58
+ }
59
+
22
60
  #[inline]
23
61
  fn push_indent(out: &mut String, level: usize, indent_size: usize) {
24
62
  let spaces = level.saturating_mul(indent_size);
@@ -135,7 +173,7 @@ fn serialize_pretty_element_into(
135
173
  && let PrettyChild::Text(text) = &children[0]
136
174
  {
137
175
  out.push('>');
138
- out.push_str(&escape_html(text));
176
+ out.push_str(&escape_html_text(text));
139
177
  out.push_str("</");
140
178
  out.push_str(&name);
141
179
  out.push_str(">\n");
@@ -160,7 +198,7 @@ fn serialize_pretty_element_into(
160
198
  }
161
199
  PrettyChild::Text(text) => {
162
200
  push_indent(out, level + 1, indent_size);
163
- out.push_str(&escape_html(&text));
201
+ out.push_str(&escape_html_text(&text));
164
202
  out.push('\n');
165
203
  }
166
204
  }
@@ -191,7 +229,7 @@ pub(crate) fn prettify_document_html(html: &str) -> String {
191
229
  tl::Node::Raw(text) => {
192
230
  let normalized = normalized_text(&bytes_to_string(text));
193
231
  if !normalized.is_empty() {
194
- out.push_str(&escape_html(&normalized));
232
+ out.push_str(&escape_html_text(&normalized));
195
233
  out.push('\n');
196
234
  }
197
235
  }
@@ -226,7 +264,7 @@ pub(crate) fn prettify_fragment_html(html: &str) -> PyResult<String> {
226
264
  if normalized.is_empty() {
227
265
  continue;
228
266
  }
229
- out.push_str(&escape_html(&normalized));
267
+ out.push_str(&escape_html_text(&normalized));
230
268
  out.push('\n');
231
269
  }
232
270
  tl::Node::Comment(_) => {}
@@ -233,6 +233,10 @@ fn prettify_helpers_format_document_and_fragment() {
233
233
  assert!(doc_pretty.contains("<span>Hi</span>"));
234
234
  assert!(doc_pretty.contains("<p>A &amp; B</p>"));
235
235
  assert!(doc_pretty.contains("<br>"));
236
+ assert_eq!(
237
+ prettify_document_html("<p>&gt; &amp; &#65; &#x41; &</p>"),
238
+ "<p>&gt; &amp; &#65; &#x41; &amp;</p>"
239
+ );
236
240
 
237
241
  let fragment_pretty =
238
242
  prettify_fragment_html(r#"hello <span data-x="1"> world </span>"#).unwrap();
@@ -141,6 +141,9 @@ pub(crate) fn xpath_with_limit(
141
141
  ) -> PyResult<Vec<Element>> {
142
142
  let max_size_bytes = max_size_bytes.unwrap_or(DEFAULT_MAX_PARSE_BYTES);
143
143
  let html_to_parse = ensure_within_size_limit(html, max_size_bytes, truncate_on_limit)?;
144
+ if html_to_parse.len() < html.len() {
145
+ return evaluate_fragment_xpath_with_fallback(html_to_parse.as_ref(), expr);
146
+ }
144
147
  match parse_xpath_documents(html_to_parse.as_ref(), "HTML document") {
145
148
  Ok((mut documents, document_handle)) => {
146
149
  evaluate_xpath_elements(&mut documents, document_handle, expr)
@@ -160,6 +163,9 @@ pub(crate) fn xpath_first_with_limit(
160
163
  ) -> PyResult<Option<Element>> {
161
164
  let max_size_bytes = max_size_bytes.unwrap_or(DEFAULT_MAX_PARSE_BYTES);
162
165
  let html_to_parse = ensure_within_size_limit(html, max_size_bytes, truncate_on_limit)?;
166
+ if html_to_parse.len() < html.len() {
167
+ return evaluate_fragment_xpath_first_with_fallback(html_to_parse.as_ref(), expr);
168
+ }
163
169
  match parse_xpath_documents(html_to_parse.as_ref(), "HTML document") {
164
170
  Ok((mut documents, document_handle)) => {
165
171
  evaluate_xpath_first_element(&mut documents, document_handle, expr)
@@ -219,12 +225,7 @@ pub(crate) fn evaluate_fragment_xpath_with_fallback(
219
225
  if normalized.is_empty() {
220
226
  return Ok(Vec::new());
221
227
  }
222
- match parse_xpath_documents(&normalized, "HTML document") {
223
- Ok((mut documents, document_handle)) => {
224
- evaluate_xpath_elements(&mut documents, document_handle, expr)
225
- }
226
- Err(_) => Ok(Vec::new()),
227
- }
228
+ evaluate_fragment_xpath(&normalized, expr)
228
229
  }
229
230
 
230
231
  pub(crate) fn evaluate_fragment_xpath_first_with_fallback(
@@ -235,12 +236,7 @@ pub(crate) fn evaluate_fragment_xpath_first_with_fallback(
235
236
  if normalized.is_empty() {
236
237
  return Ok(None);
237
238
  }
238
- match parse_xpath_documents(&normalized, "HTML document") {
239
- Ok((mut documents, document_handle)) => {
240
- evaluate_xpath_first_element(&mut documents, document_handle, expr)
241
- }
242
- Err(_) => Ok(None),
243
- }
239
+ evaluate_fragment_xpath_first(&normalized, expr)
244
240
  }
245
241
 
246
242
  pub(crate) struct XPathDocumentState {
@@ -43,26 +43,26 @@ wheels = [
43
43
 
44
44
  [[package]]
45
45
  name = "maturin"
46
- version = "1.13.3"
46
+ version = "1.14.1"
47
47
  source = { registry = "https://pypi.org/simple" }
48
48
  dependencies = [
49
49
  { name = "tomli", marker = "python_full_version < '3.11'" },
50
50
  ]
51
- sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/612d23d33ec21b9ae7ece7b3f0dd5f9dfd57b4009e9d2938165869ebd6ae/maturin-1.13.3.tar.gz", hash = "sha256:771e1e9e71a278e56db01552e0d1acfd1464259f9575b6e72842f893cd299079", size = 357934, upload-time = "2026-05-11T07:43:39.027Z" }
51
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" }
52
52
  wheels = [
53
- { url = "https://files.pythonhosted.org/packages/71/66/18c2aaac0b2a5dea9f1db5984ce83b905ad205cfc7c02d0091e707c0c2e7/maturin-1.13.3-py3-none-linux_armv6l.whl", hash = "sha256:3cc13929ca82aefa4adbf0f2c35419369796213c6fb0eb24e914945f50ef5d8c", size = 10190971, upload-time = "2026-05-11T07:43:10.431Z" },
54
- { url = "https://files.pythonhosted.org/packages/bc/71/26a988d092e4fd6a9523d46d44400a46cad7cdf3fd206ce702240c748aee/maturin-1.13.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:53b08bd075649ce96513ad9abf241a43cb685ed6e9e7790f8dbc2d66e95d8323", size = 19716714, upload-time = "2026-05-11T07:43:36.911Z" },
55
- { url = "https://files.pythonhosted.org/packages/82/5c/f3fd0e184255d9fc7e272c62af3dfa84c617b2577ef83af9ce615f5279cc/maturin-1.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4cd478e6e4c56251e48ed079b8efd55b30bc5c09cf695a1bdafaeb582ee735a0", size = 10194726, upload-time = "2026-05-11T07:43:07.05Z" },
56
- { url = "https://files.pythonhosted.org/packages/a9/e1/f4edb69fb647b77c4769a9bfd4d6fb62961e653d164bc277ecdffac3ab61/maturin-1.13.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:a2675e25f313034ae6f57388cf14818f87d8961c4a96795287f3e155f59beb11", size = 10172781, upload-time = "2026-05-11T07:43:40.796Z" },
57
- { url = "https://files.pythonhosted.org/packages/c7/7d/a1be934690cdcc3c6609769ceaad322ab7501c2ee5bafcac1b14d609e403/maturin-1.13.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:4667ef609ab446c1b5e0bfe4f9fb99699ab6d8548433f8d1a684256e0b67217f", size = 10682670, upload-time = "2026-05-11T07:43:13.132Z" },
58
- { url = "https://files.pythonhosted.org/packages/18/f5/372ae19b72ce8f6e37e5864ae4dc5b252ee9fce0619ccc3aa366aa3a7f97/maturin-1.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3db93337ed97e60ffc878aa8b493cd7ae44d3a5e1a37256db3a4491f57565018", size = 10060363, upload-time = "2026-05-11T07:43:21.107Z" },
59
- { url = "https://files.pythonhosted.org/packages/cb/5b/c68340cca09368af0df80965dfabed4234205a492a93da00793c7b9aae20/maturin-1.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:1cc0a110b224ca90406b668a3e3c1f5a515062e59e26292f6dbaf5fd4909c6f3", size = 10017551, upload-time = "2026-05-11T07:43:33.916Z" },
60
- { url = "https://files.pythonhosted.org/packages/28/1e/f90fb2b000bad9e6d850cd5afb88b2f1e2a279cfb4de02ea40078484690e/maturin-1.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:c00ea6428dea17bf616fe93770837634454b28c2de1a876e42ef8036c616079a", size = 13301712, upload-time = "2026-05-11T07:43:26.492Z" },
61
- { url = "https://files.pythonhosted.org/packages/be/58/1670f68a8f04ccd7b90df11047bd9a046585310e84e1967cc9849cd1c5a3/maturin-1.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49fd6ab08da28098ccf37afca24cdba72376ba9c1eedf9dd25ff82ed771961ff", size = 10946765, upload-time = "2026-05-11T07:43:16.135Z" },
62
- { url = "https://files.pythonhosted.org/packages/4b/ac/00c955c2ef134817b1a7bdaa76b0309e9c5291eb17d9ff88069eecd08bc2/maturin-1.13.3-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:b6741d7bf4af97da937528fd1e523c6ab54f53d9a21870fa735d6e67fd88e273", size = 10388661, upload-time = "2026-05-11T07:43:18.727Z" },
63
- { url = "https://files.pythonhosted.org/packages/97/c6/cbf8a51dde19c19aeba0d9b075095a2effb9b31fd312b1aae3ac79f8aea2/maturin-1.13.3-py3-none-win32.whl", hash = "sha256:0ef257e692cc756c87af5bea95ddfe7d3ac49d3376a7a87f728d63f06e7b6f8b", size = 8901838, upload-time = "2026-05-11T07:43:23.76Z" },
64
- { url = "https://files.pythonhosted.org/packages/a1/ff/c6a50a59dc8313097d43ac5f4d74df6a500c8cb62b0dc9e054f53e203a48/maturin-1.13.3-py3-none-win_amd64.whl", hash = "sha256:def4a435ea9d2ee93b18ba579dc8c9cf898889a66f312cd379b5e374ec3e3ad6", size = 10340801, upload-time = "2026-05-11T07:43:29.239Z" },
65
- { url = "https://files.pythonhosted.org/packages/6c/93/e32e79333f0902ba292b996f504f5f06be59587f7d02ab8d5ed1e3066445/maturin-1.13.3-py3-none-win_arm64.whl", hash = "sha256:2389fe92d017cea9d94e521fa0175314a4c52f79a1057b901fbc9f8686ef7d0b", size = 9706562, upload-time = "2026-05-11T07:43:31.743Z" },
53
+ { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" },
54
+ { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" },
55
+ { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" },
56
+ { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" },
57
+ { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" },
58
+ { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" },
59
+ { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" },
60
+ { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" },
61
+ { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" },
62
+ { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" },
63
+ { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" },
64
+ { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" },
65
+ { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" },
66
66
  ]
67
67
 
68
68
  [[package]]
@@ -94,7 +94,7 @@ wheels = [
94
94
 
95
95
  [[package]]
96
96
  name = "pytest"
97
- version = "9.0.3"
97
+ version = "9.1.1"
98
98
  source = { registry = "https://pypi.org/simple" }
99
99
  dependencies = [
100
100
  { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -105,9 +105,9 @@ dependencies = [
105
105
  { name = "pygments" },
106
106
  { name = "tomli", marker = "python_full_version < '3.11'" },
107
107
  ]
108
- sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
108
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
109
109
  wheels = [
110
- { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
110
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
111
111
  ]
112
112
 
113
113
  [[package]]
@@ -126,32 +126,32 @@ wheels = [
126
126
 
127
127
  [[package]]
128
128
  name = "ruff"
129
- version = "0.15.15"
129
+ version = "0.16.0"
130
130
  source = { registry = "https://pypi.org/simple" }
131
- sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
132
132
  wheels = [
133
- { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
134
- { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
135
- { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
136
- { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
137
- { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
138
- { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
139
- { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
140
- { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
141
- { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
142
- { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
143
- { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
144
- { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
145
- { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
146
- { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
147
- { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
148
- { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
149
- { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
133
+ { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
134
+ { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
135
+ { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
136
+ { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
137
+ { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
138
+ { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
139
+ { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
140
+ { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
141
+ { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
142
+ { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
143
+ { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
144
+ { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
145
+ { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
146
+ { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
147
+ { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
148
+ { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
149
+ { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
150
150
  ]
151
151
 
152
152
  [[package]]
153
153
  name = "scraper-rust"
154
- version = "0.6.0"
154
+ version = "0.7.0"
155
155
  source = { editable = "." }
156
156
 
157
157
  [package.optional-dependencies]
@@ -239,9 +239,9 @@ wheels = [
239
239
 
240
240
  [[package]]
241
241
  name = "typing-extensions"
242
- version = "4.15.0"
242
+ version = "4.16.0"
243
243
  source = { registry = "https://pypi.org/simple" }
244
- sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
244
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
245
245
  wheels = [
246
- { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
246
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
247
247
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes