scraper-rust 0.4.0__tar.gz → 0.4.2__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 (37) hide show
  1. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/.github/workflows/release.yml +37 -13
  2. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/Cargo.lock +25 -3
  3. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/Cargo.toml +2 -1
  4. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/PKG-INFO +2 -1
  5. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/README.md +1 -0
  6. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/pyproject.toml +1 -1
  7. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/scraper_rs/__init__.py +2 -0
  8. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/scraper_rs.pyi +21 -0
  9. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/src/lib.rs +512 -1
  10. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/tests/test_scraper.py +52 -0
  11. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/uv.lock +35 -35
  12. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/.github/workflows/benchmark.yml +0 -0
  13. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/.github/workflows/bump-version.yml +0 -0
  14. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/.github/workflows/tests.yml +0 -0
  15. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/.gitignore +0 -0
  16. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/AGENTS.md +0 -0
  17. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/LICENSE +0 -0
  18. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/benchmarks/README.md +0 -0
  19. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/benchmarks/bench_parse_memory.py +0 -0
  20. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/benchmarks/bench_sync_async.py +0 -0
  21. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/benchmarks/bench_vs_markupever.py +0 -0
  22. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/README.md +0 -0
  23. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/api.md +0 -0
  24. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/architecture.md +0 -0
  25. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/async-api.md +0 -0
  26. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/development.md +0 -0
  27. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/docs/limits-and-errors.md +0 -0
  28. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/examples/demo.py +0 -0
  29. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/examples/demo_async_document.py +0 -0
  30. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/examples/demo_asyncio.py +0 -0
  31. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/examples/demo_prettify_url.py +0 -0
  32. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/justfile +0 -0
  33. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/py.typed +0 -0
  34. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/scraper_rs/asyncio.py +0 -0
  35. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/scraper_rs/asyncio.pyi +0 -0
  36. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/tests/test_asyncio.py +0 -0
  37. {scraper_rust-0.4.0 → scraper_rust-0.4.2}/tests/test_performance.py +0 -0
@@ -10,12 +10,24 @@ permissions:
10
10
 
11
11
  jobs:
12
12
  build-wheels:
13
- name: Build wheels on ${{ matrix.os }}
14
- runs-on: ${{ matrix.os }}
13
+ name: Build wheels on ${{ matrix.name }}
14
+ runs-on: ${{ matrix.runner }}
15
15
  strategy:
16
16
  fail-fast: false
17
17
  matrix:
18
- os: [ubuntu-latest, windows-latest, macos-latest]
18
+ include:
19
+ - name: linux-x86_64
20
+ runner: ubuntu-latest
21
+ platform: linux
22
+ - name: linux-arm64
23
+ runner: ubuntu-24.04-arm
24
+ platform: linux
25
+ - name: windows
26
+ runner: windows-latest
27
+ platform: windows
28
+ - name: macos
29
+ runner: macos-latest
30
+ platform: macos
19
31
 
20
32
  steps:
21
33
  - name: Checkout repository
@@ -27,7 +39,7 @@ jobs:
27
39
  python-version: '3.10'
28
40
 
29
41
  - name: Build wheels (Linux)
30
- if: matrix.os == 'ubuntu-latest'
42
+ if: matrix.platform == 'linux'
31
43
  uses: PyO3/maturin-action@v1
32
44
  with:
33
45
  command: build
@@ -35,7 +47,7 @@ jobs:
35
47
  manylinux: auto
36
48
 
37
49
  - name: Build wheels (Windows/macOS)
38
- if: matrix.os == 'windows-latest' || matrix.os == 'macos-latest'
50
+ if: matrix.platform == 'windows' || matrix.platform == 'macos'
39
51
  uses: PyO3/maturin-action@v1
40
52
  with:
41
53
  command: build
@@ -44,7 +56,7 @@ jobs:
44
56
  - name: Upload wheels as artifacts
45
57
  uses: actions/upload-artifact@v7
46
58
  with:
47
- name: wheels-${{ matrix.os }}
59
+ name: wheels-${{ matrix.name }}
48
60
  path: dist/*.whl
49
61
 
50
62
  - name: Upload wheels to release
@@ -56,13 +68,25 @@ jobs:
56
68
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57
69
 
58
70
  build-free-threaded-wheels:
59
- name: Build free-threaded wheels on ${{ matrix.os }} (Python ${{ matrix.python-version }})
60
- runs-on: ${{ matrix.os }}
71
+ name: Build free-threaded wheels on ${{ matrix.name }} (Python ${{ matrix.python-version }})
72
+ runs-on: ${{ matrix.runner }}
61
73
  strategy:
62
74
  fail-fast: false
63
75
  matrix:
64
- os: [ubuntu-latest, windows-latest, macos-latest]
65
76
  python-version: ['3.13t', '3.14t']
77
+ include:
78
+ - name: linux-x86_64
79
+ runner: ubuntu-latest
80
+ platform: linux
81
+ - name: linux-arm64
82
+ runner: ubuntu-24.04-arm
83
+ platform: linux
84
+ - name: windows
85
+ runner: windows-latest
86
+ platform: windows
87
+ - name: macos
88
+ runner: macos-latest
89
+ platform: macos
66
90
 
67
91
  steps:
68
92
  - name: Checkout repository
@@ -75,20 +99,20 @@ jobs:
75
99
  allow-prereleases: true
76
100
 
77
101
  - name: Install zig (for Linux manylinux builds)
78
- if: matrix.os == 'ubuntu-latest'
102
+ if: matrix.platform == 'linux'
79
103
  uses: goto-bus-stop/setup-zig@v2
80
104
  with:
81
105
  version: 0.11.0
82
106
 
83
107
  - name: Build free-threaded wheels (Linux - using zig for manylinux)
84
- if: matrix.os == 'ubuntu-latest'
108
+ if: matrix.platform == 'linux'
85
109
  uses: PyO3/maturin-action@v1
86
110
  with:
87
111
  command: build
88
112
  args: --release --strip --out dist --zig
89
113
 
90
114
  - name: Build free-threaded wheels (Windows/macOS)
91
- if: matrix.os == 'windows-latest' || matrix.os == 'macos-latest'
115
+ if: matrix.platform == 'windows' || matrix.platform == 'macos'
92
116
  uses: PyO3/maturin-action@v1
93
117
  with:
94
118
  command: build
@@ -97,7 +121,7 @@ jobs:
97
121
  - name: Upload free-threaded wheels as artifacts
98
122
  uses: actions/upload-artifact@v7
99
123
  with:
100
- name: wheels-free-threaded-${{ matrix.os }}-${{ matrix.python-version }}
124
+ name: wheels-free-threaded-${{ matrix.name }}-${{ matrix.python-version }}
101
125
  path: dist/*.whl
102
126
 
103
127
  - name: Upload free-threaded wheels to release
@@ -545,7 +545,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
545
545
  checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
546
546
  dependencies = [
547
547
  "log",
548
- "markup5ever",
548
+ "markup5ever 0.38.0",
549
+ ]
550
+
551
+ [[package]]
552
+ name = "html5ever"
553
+ version = "0.39.0"
554
+ source = "registry+https://github.com/rust-lang/crates.io-index"
555
+ checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
556
+ dependencies = [
557
+ "log",
558
+ "markup5ever 0.39.0",
549
559
  ]
550
560
 
551
561
  [[package]]
@@ -1215,6 +1225,17 @@ dependencies = [
1215
1225
  "web_atoms",
1216
1226
  ]
1217
1227
 
1228
+ [[package]]
1229
+ name = "markup5ever"
1230
+ version = "0.39.0"
1231
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1232
+ checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
1233
+ dependencies = [
1234
+ "log",
1235
+ "tendril",
1236
+ "web_atoms",
1237
+ ]
1238
+
1218
1239
  [[package]]
1219
1240
  name = "memchr"
1220
1241
  version = "2.8.0"
@@ -1820,7 +1841,7 @@ dependencies = [
1820
1841
  "cssparser",
1821
1842
  "ego-tree",
1822
1843
  "getopts",
1823
- "html5ever",
1844
+ "html5ever 0.38.0",
1824
1845
  "precomputed-hash",
1825
1846
  "selectors",
1826
1847
  "tendril",
@@ -1828,8 +1849,9 @@ dependencies = [
1828
1849
 
1829
1850
  [[package]]
1830
1851
  name = "scraper-rs"
1831
- version = "0.4.0"
1852
+ version = "0.4.2"
1832
1853
  dependencies = [
1854
+ "html5ever 0.39.0",
1833
1855
  "pyo3",
1834
1856
  "pyo3-async-runtimes",
1835
1857
  "scraper",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "scraper-rs"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -20,6 +20,7 @@ tokio = { version = "1", features = ["rt", "macros"] }
20
20
  scraper = { version = "0.25.0", git = "https://github.com/rust-scraper/scraper", features = ["atomic"], rev = "d07444effb9ca192d9b3919fb920ab9c6c701c4f" }
21
21
 
22
22
  xee-xpath = "0.1.5"
23
+ html5ever = "0.39.0"
23
24
 
24
25
  [profile.release]
25
26
  lto = "fat"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scraper-rust
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -166,6 +166,7 @@ If you have `just` installed, the repo includes helpers: `just build` (local whe
166
166
  ## Projects Using scraper-rs
167
167
 
168
168
  - [**silkworm**](https://github.com/BitingSnakes/silkworm) - Async web scraping framework on top of Rust
169
+ - [**silkworm-mcp**](https://github.com/BitingSnakes/silkworm-mcp) - An MCP server for silkworm
169
170
 
170
171
  ## Development
171
172
 
@@ -139,6 +139,7 @@ If you have `just` installed, the repo includes helpers: `just build` (local whe
139
139
  ## Projects Using scraper-rs
140
140
 
141
141
  - [**silkworm**](https://github.com/BitingSnakes/silkworm) - Async web scraping framework on top of Rust
142
+ - [**silkworm-mcp**](https://github.com/BitingSnakes/silkworm-mcp) - An MCP server for silkworm
142
143
 
143
144
  ## Development
144
145
 
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scraper-rust"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Python bindings around rust-scraper/scraper with PyO3"
9
9
  authors = [{ name = "Yehor Smoliakov", email = "egorsmkv@gmail.com" }]
10
10
  requires-python = ">=3.10"
@@ -9,6 +9,8 @@ __all__ = [
9
9
  "__version__",
10
10
  "first",
11
11
  "parse",
12
+ "parse_document",
13
+ "parse_fragment",
12
14
  "prettify",
13
15
  "select",
14
16
  "select_first",
@@ -8,6 +8,21 @@ class ElementDict(TypedDict):
8
8
  html: str
9
9
  attrs: dict[str, str]
10
10
 
11
+ class HtmlNodeDict(TypedDict, total=False):
12
+ node_type: str
13
+ tag: str
14
+ namespace: str
15
+ attrs: dict[str, str]
16
+ children: list[HtmlNodeDict]
17
+ text: str
18
+ quirks_mode: str
19
+ errors: list[str]
20
+ name: str
21
+ public_id: str
22
+ system_id: str
23
+ target: str
24
+ data: str
25
+
11
26
  class Element:
12
27
  tag: str
13
28
  text: str
@@ -62,6 +77,12 @@ class Document:
62
77
  def parse(
63
78
  html: str, *, max_size_bytes: int | None = ..., truncate_on_limit: bool = False
64
79
  ) -> Document: ...
80
+ def parse_document(
81
+ html: str, *, max_size_bytes: int | None = ..., truncate_on_limit: bool = False
82
+ ) -> HtmlNodeDict: ...
83
+ def parse_fragment(
84
+ html: str, *, max_size_bytes: int | None = ..., truncate_on_limit: bool = False
85
+ ) -> HtmlNodeDict: ...
65
86
  def prettify(
66
87
  html: str, *, max_size_bytes: int | None = ..., truncate_on_limit: bool = False
67
88
  ) -> str: ...
@@ -5,10 +5,17 @@ use std::future::Future;
5
5
  use std::rc::Rc;
6
6
  use std::sync::{Arc, Mutex, OnceLock};
7
7
 
8
+ use html5ever::interface::{ElementFlags, NodeOrText, QuirksMode};
9
+ use html5ever::tendril::{StrTendril, TendrilSink};
10
+ use html5ever::tree_builder::TreeSink;
11
+ use html5ever::{
12
+ Attribute, ParseOpts, QualName, local_name, ns, parse_document as html5ever_parse_document,
13
+ parse_fragment as html5ever_parse_fragment,
14
+ };
8
15
  use pyo3::IntoPyObject;
9
16
  use pyo3::exceptions::PyValueError;
10
17
  use pyo3::prelude::*;
11
- use pyo3::types::PyDict;
18
+ use pyo3::types::{PyDict, PyList};
12
19
  use pyo3::wrap_pyfunction;
13
20
  use scraper::{Html, Selector, element_ref::ElementRef};
14
21
  use xee_xpath::context::StaticContextBuilder;
@@ -110,6 +117,484 @@ fn ensure_within_size_limit(
110
117
  Ok(Cow::Borrowed(html))
111
118
  }
112
119
 
120
+ #[derive(Clone)]
121
+ enum Html5DictNodeData {
122
+ Document,
123
+ Doctype {
124
+ name: String,
125
+ public_id: String,
126
+ system_id: String,
127
+ },
128
+ Text {
129
+ text: String,
130
+ },
131
+ Comment {
132
+ text: String,
133
+ },
134
+ Element {
135
+ name: QualName,
136
+ attrs: Vec<Attribute>,
137
+ template_contents: Option<usize>,
138
+ },
139
+ ProcessingInstruction {
140
+ target: String,
141
+ data: String,
142
+ },
143
+ }
144
+
145
+ #[derive(Clone)]
146
+ struct Html5DictNode {
147
+ parent: Option<usize>,
148
+ children: Vec<usize>,
149
+ data: Html5DictNodeData,
150
+ }
151
+
152
+ #[derive(Clone, Copy)]
153
+ enum Html5ParseKind {
154
+ Document,
155
+ Fragment,
156
+ }
157
+
158
+ #[derive(Debug)]
159
+ struct Html5ElemName {
160
+ ns: html5ever::Namespace,
161
+ local_name: html5ever::LocalName,
162
+ }
163
+
164
+ impl html5ever::interface::ElemName for Html5ElemName {
165
+ fn ns(&self) -> &html5ever::Namespace {
166
+ &self.ns
167
+ }
168
+
169
+ fn local_name(&self) -> &html5ever::LocalName {
170
+ &self.local_name
171
+ }
172
+ }
173
+
174
+ struct Html5DictTree {
175
+ nodes: Vec<Html5DictNode>,
176
+ quirks_mode: QuirksMode,
177
+ errors: Vec<String>,
178
+ parse_kind: Html5ParseKind,
179
+ }
180
+
181
+ struct Html5DictSink {
182
+ nodes: RefCell<Vec<Html5DictNode>>,
183
+ quirks_mode: std::cell::Cell<QuirksMode>,
184
+ errors: RefCell<Vec<String>>,
185
+ parse_kind: Html5ParseKind,
186
+ }
187
+
188
+ impl Html5DictSink {
189
+ fn new(parse_kind: Html5ParseKind) -> Self {
190
+ Self {
191
+ nodes: RefCell::new(vec![Html5DictNode {
192
+ parent: None,
193
+ children: Vec::new(),
194
+ data: Html5DictNodeData::Document,
195
+ }]),
196
+ quirks_mode: std::cell::Cell::new(QuirksMode::NoQuirks),
197
+ errors: RefCell::new(Vec::new()),
198
+ parse_kind,
199
+ }
200
+ }
201
+
202
+ fn create_node(&self, data: Html5DictNodeData) -> usize {
203
+ let mut nodes = self.nodes.borrow_mut();
204
+ let id = nodes.len();
205
+ nodes.push(Html5DictNode {
206
+ parent: None,
207
+ children: Vec::new(),
208
+ data,
209
+ });
210
+ id
211
+ }
212
+
213
+ fn remove_from_parent_impl(nodes: &mut [Html5DictNode], target: usize) {
214
+ let Some(parent) = nodes[target].parent.take() else {
215
+ return;
216
+ };
217
+ if let Some(position) = nodes[parent]
218
+ .children
219
+ .iter()
220
+ .position(|&child| child == target)
221
+ {
222
+ nodes[parent].children.remove(position);
223
+ }
224
+ }
225
+
226
+ fn append_text_node(&self, nodes: &mut Vec<Html5DictNode>, parent: usize, text: StrTendril) {
227
+ if let Some(&last_child) = nodes[parent].children.last()
228
+ && let Html5DictNodeData::Text { text: existing } = &mut nodes[last_child].data
229
+ {
230
+ existing.push_str(text.as_ref());
231
+ return;
232
+ }
233
+
234
+ let id = nodes.len();
235
+ nodes.push(Html5DictNode {
236
+ parent: Some(parent),
237
+ children: Vec::new(),
238
+ data: Html5DictNodeData::Text {
239
+ text: text.to_string(),
240
+ },
241
+ });
242
+ nodes[parent].children.push(id);
243
+ }
244
+
245
+ fn append_node(&self, nodes: &mut [Html5DictNode], parent: usize, child: usize) {
246
+ Self::remove_from_parent_impl(nodes, child);
247
+ nodes[child].parent = Some(parent);
248
+ nodes[parent].children.push(child);
249
+ }
250
+ }
251
+
252
+ impl TreeSink for Html5DictSink {
253
+ type Handle = usize;
254
+ type Output = Html5DictTree;
255
+ type ElemName<'a>
256
+ = Html5ElemName
257
+ where
258
+ Self: 'a;
259
+
260
+ fn finish(self) -> Self::Output {
261
+ Html5DictTree {
262
+ nodes: self.nodes.into_inner(),
263
+ quirks_mode: self.quirks_mode.get(),
264
+ errors: self.errors.into_inner(),
265
+ parse_kind: self.parse_kind,
266
+ }
267
+ }
268
+
269
+ fn parse_error(&self, msg: Cow<'static, str>) {
270
+ self.errors.borrow_mut().push(msg.into_owned());
271
+ }
272
+
273
+ fn get_document(&self) -> Self::Handle {
274
+ 0
275
+ }
276
+
277
+ fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
278
+ let nodes = self.nodes.borrow();
279
+ match &nodes[*target].data {
280
+ Html5DictNodeData::Element { name, .. } => Html5ElemName {
281
+ ns: name.ns.clone(),
282
+ local_name: name.local.clone(),
283
+ },
284
+ _ => panic!("elem_name called on a non-element node"),
285
+ }
286
+ }
287
+
288
+ fn create_element(
289
+ &self,
290
+ name: QualName,
291
+ attrs: Vec<Attribute>,
292
+ flags: ElementFlags,
293
+ ) -> Self::Handle {
294
+ let template_contents = if flags.template {
295
+ Some(self.create_node(Html5DictNodeData::Document))
296
+ } else {
297
+ None
298
+ };
299
+
300
+ self.create_node(Html5DictNodeData::Element {
301
+ name,
302
+ attrs,
303
+ template_contents,
304
+ })
305
+ }
306
+
307
+ fn create_comment(&self, text: StrTendril) -> Self::Handle {
308
+ self.create_node(Html5DictNodeData::Comment {
309
+ text: text.to_string(),
310
+ })
311
+ }
312
+
313
+ fn create_pi(&self, target: StrTendril, data: StrTendril) -> Self::Handle {
314
+ self.create_node(Html5DictNodeData::ProcessingInstruction {
315
+ target: target.to_string(),
316
+ data: data.to_string(),
317
+ })
318
+ }
319
+
320
+ fn append(&self, parent: &Self::Handle, child: NodeOrText<Self::Handle>) {
321
+ let mut nodes = self.nodes.borrow_mut();
322
+ match child {
323
+ NodeOrText::AppendText(text) => self.append_text_node(&mut nodes, *parent, text),
324
+ NodeOrText::AppendNode(child) => self.append_node(&mut nodes, *parent, child),
325
+ }
326
+ }
327
+
328
+ fn append_based_on_parent_node(
329
+ &self,
330
+ element: &Self::Handle,
331
+ prev_element: &Self::Handle,
332
+ child: NodeOrText<Self::Handle>,
333
+ ) {
334
+ let has_parent = self.nodes.borrow()[*element].parent.is_some();
335
+ if has_parent {
336
+ self.append_before_sibling(element, child);
337
+ } else {
338
+ self.append(prev_element, child);
339
+ }
340
+ }
341
+
342
+ fn append_doctype_to_document(
343
+ &self,
344
+ name: StrTendril,
345
+ public_id: StrTendril,
346
+ system_id: StrTendril,
347
+ ) {
348
+ let id = self.create_node(Html5DictNodeData::Doctype {
349
+ name: name.to_string(),
350
+ public_id: public_id.to_string(),
351
+ system_id: system_id.to_string(),
352
+ });
353
+ self.append(&0, NodeOrText::AppendNode(id));
354
+ }
355
+
356
+ fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
357
+ let nodes = self.nodes.borrow();
358
+ match &nodes[*target].data {
359
+ Html5DictNodeData::Element {
360
+ template_contents: Some(contents),
361
+ ..
362
+ } => *contents,
363
+ _ => panic!("get_template_contents called on a non-template element"),
364
+ }
365
+ }
366
+
367
+ fn same_node(&self, x: &Self::Handle, y: &Self::Handle) -> bool {
368
+ x == y
369
+ }
370
+
371
+ fn set_quirks_mode(&self, mode: QuirksMode) {
372
+ self.quirks_mode.set(mode);
373
+ }
374
+
375
+ fn append_before_sibling(&self, sibling: &Self::Handle, new_node: NodeOrText<Self::Handle>) {
376
+ let mut nodes = self.nodes.borrow_mut();
377
+ let Some(parent) = nodes[*sibling].parent else {
378
+ return;
379
+ };
380
+ let Some(position) = nodes[parent]
381
+ .children
382
+ .iter()
383
+ .position(|&child| child == *sibling)
384
+ else {
385
+ return;
386
+ };
387
+
388
+ match new_node {
389
+ NodeOrText::AppendText(text) => {
390
+ if position > 0 {
391
+ let previous = nodes[parent].children[position - 1];
392
+ if let Html5DictNodeData::Text { text: existing } = &mut nodes[previous].data {
393
+ existing.push_str(text.as_ref());
394
+ return;
395
+ }
396
+ }
397
+
398
+ if let Html5DictNodeData::Text { text: existing } = &mut nodes[*sibling].data {
399
+ existing.insert_str(0, text.as_ref());
400
+ return;
401
+ }
402
+
403
+ let id = nodes.len();
404
+ nodes.push(Html5DictNode {
405
+ parent: Some(parent),
406
+ children: Vec::new(),
407
+ data: Html5DictNodeData::Text {
408
+ text: text.to_string(),
409
+ },
410
+ });
411
+ nodes[parent].children.insert(position, id);
412
+ }
413
+ NodeOrText::AppendNode(child) => {
414
+ Self::remove_from_parent_impl(&mut nodes, child);
415
+ nodes[child].parent = Some(parent);
416
+ let Some(new_position) = nodes[parent]
417
+ .children
418
+ .iter()
419
+ .position(|&node| node == *sibling)
420
+ else {
421
+ return;
422
+ };
423
+ nodes[parent].children.insert(new_position, child);
424
+ }
425
+ }
426
+ }
427
+
428
+ fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec<Attribute>) {
429
+ let mut nodes = self.nodes.borrow_mut();
430
+ let Html5DictNodeData::Element {
431
+ attrs: existing_attrs,
432
+ ..
433
+ } = &mut nodes[*target].data
434
+ else {
435
+ panic!("add_attrs_if_missing called on a non-element node");
436
+ };
437
+
438
+ for attr in attrs {
439
+ if existing_attrs
440
+ .iter()
441
+ .all(|existing| existing.name != attr.name)
442
+ {
443
+ existing_attrs.push(attr);
444
+ }
445
+ }
446
+ }
447
+
448
+ fn remove_from_parent(&self, target: &Self::Handle) {
449
+ let mut nodes = self.nodes.borrow_mut();
450
+ Self::remove_from_parent_impl(&mut nodes, *target);
451
+ }
452
+
453
+ fn reparent_children(&self, node: &Self::Handle, new_parent: &Self::Handle) {
454
+ let children = {
455
+ let mut nodes = self.nodes.borrow_mut();
456
+ std::mem::take(&mut nodes[*node].children)
457
+ };
458
+
459
+ for child in children {
460
+ self.append(new_parent, NodeOrText::AppendNode(child));
461
+ }
462
+ }
463
+ }
464
+
465
+ fn quirks_mode_name(mode: QuirksMode) -> &'static str {
466
+ match mode {
467
+ QuirksMode::Quirks => "quirks",
468
+ QuirksMode::LimitedQuirks => "limited-quirks",
469
+ QuirksMode::NoQuirks => "no-quirks",
470
+ }
471
+ }
472
+
473
+ fn qual_name_to_string(name: &QualName) -> String {
474
+ name.local.to_string()
475
+ }
476
+
477
+ fn attrs_to_map(attrs: &[Attribute]) -> HashMap<String, String> {
478
+ attrs
479
+ .iter()
480
+ .map(|attr| (attr.name.local.to_string(), attr.value.to_string()))
481
+ .collect()
482
+ }
483
+
484
+ fn html5_dict_node_to_py(
485
+ py: Python<'_>,
486
+ tree: &Html5DictTree,
487
+ node_id: usize,
488
+ root_override: Option<&str>,
489
+ ) -> PyResult<Py<PyDict>> {
490
+ let node = &tree.nodes[node_id];
491
+ let dict = PyDict::new(py);
492
+
493
+ let node_type = root_override.unwrap_or(match &node.data {
494
+ Html5DictNodeData::Document => "document",
495
+ Html5DictNodeData::Doctype { .. } => "doctype",
496
+ Html5DictNodeData::Text { .. } => "text",
497
+ Html5DictNodeData::Comment { .. } => "comment",
498
+ Html5DictNodeData::Element { .. } => "element",
499
+ Html5DictNodeData::ProcessingInstruction { .. } => "processing_instruction",
500
+ });
501
+ dict.set_item("node_type", node_type)?;
502
+
503
+ match &node.data {
504
+ Html5DictNodeData::Document => {}
505
+ Html5DictNodeData::Doctype {
506
+ name,
507
+ public_id,
508
+ system_id,
509
+ } => {
510
+ dict.set_item("name", name)?;
511
+ dict.set_item("public_id", public_id)?;
512
+ dict.set_item("system_id", system_id)?;
513
+ }
514
+ Html5DictNodeData::Text { text } | Html5DictNodeData::Comment { text } => {
515
+ dict.set_item("text", text)?;
516
+ }
517
+ Html5DictNodeData::Element { name, attrs, .. } => {
518
+ dict.set_item("tag", qual_name_to_string(name))?;
519
+ dict.set_item("namespace", name.ns.to_string())?;
520
+ dict.set_item("attrs", attrs_to_map(attrs))?;
521
+ }
522
+ Html5DictNodeData::ProcessingInstruction { target, data } => {
523
+ dict.set_item("target", target)?;
524
+ dict.set_item("data", data)?;
525
+ }
526
+ }
527
+
528
+ let child_ids = if node_id == 0 && matches!(tree.parse_kind, Html5ParseKind::Fragment) {
529
+ let mut flattened = Vec::new();
530
+ for &child in &node.children {
531
+ if let Html5DictNodeData::Element { name, .. } = &tree.nodes[child].data
532
+ && name.local.as_ref() == "html"
533
+ {
534
+ flattened.extend(tree.nodes[child].children.iter().copied());
535
+ } else {
536
+ flattened.push(child);
537
+ }
538
+ }
539
+ flattened
540
+ } else {
541
+ node.children.clone()
542
+ };
543
+
544
+ let children = PyList::empty(py);
545
+ for child in child_ids {
546
+ children.append(html5_dict_node_to_py(py, tree, child, None)?)?;
547
+ }
548
+ dict.set_item("children", children)?;
549
+
550
+ if node_id == 0 {
551
+ dict.set_item("quirks_mode", quirks_mode_name(tree.quirks_mode))?;
552
+ dict.set_item("errors", &tree.errors)?;
553
+ }
554
+
555
+ Ok(dict.into())
556
+ }
557
+
558
+ fn parse_document_to_dict_tree(
559
+ html: &str,
560
+ max_size_bytes: Option<usize>,
561
+ truncate_on_limit: bool,
562
+ ) -> PyResult<Html5DictTree> {
563
+ let max_size_bytes = max_size_bytes.unwrap_or(DEFAULT_MAX_PARSE_BYTES);
564
+ let html_to_parse = ensure_within_size_limit(html, max_size_bytes, truncate_on_limit)?;
565
+ Ok(html5ever_parse_document(
566
+ Html5DictSink::new(Html5ParseKind::Document),
567
+ ParseOpts::default(),
568
+ )
569
+ .one(html_to_parse.as_ref()))
570
+ }
571
+
572
+ fn parse_fragment_to_dict_tree(
573
+ html: &str,
574
+ max_size_bytes: Option<usize>,
575
+ truncate_on_limit: bool,
576
+ ) -> PyResult<Html5DictTree> {
577
+ let max_size_bytes = max_size_bytes.unwrap_or(DEFAULT_MAX_PARSE_BYTES);
578
+ let html_to_parse = ensure_within_size_limit(html, max_size_bytes, truncate_on_limit)?;
579
+ let context_name = QualName::new(None, ns!(html), local_name!("div"));
580
+ Ok(html5ever_parse_fragment(
581
+ Html5DictSink::new(Html5ParseKind::Fragment),
582
+ ParseOpts::default(),
583
+ context_name,
584
+ Vec::new(),
585
+ false,
586
+ )
587
+ .one(html_to_parse.as_ref()))
588
+ }
589
+
590
+ fn html5_tree_to_py_dict(py: Python<'_>, tree: Html5DictTree) -> PyResult<Py<PyDict>> {
591
+ let root_override = match tree.parse_kind {
592
+ Html5ParseKind::Document => Some("document"),
593
+ Html5ParseKind::Fragment => Some("document_fragment"),
594
+ };
595
+ html5_dict_node_to_py(py, &tree, 0, root_override)
596
+ }
597
+
113
598
  /// Tiny helper to truncate text in __repr__.
114
599
  fn truncate_for_repr(s: &str, max_chars: usize) -> String {
115
600
  let mut out = String::new();
@@ -1413,6 +1898,30 @@ fn parse(html: &str, max_size_bytes: Option<usize>, truncate_on_limit: bool) ->
1413
1898
  Document::from_html(html, max_size_bytes, truncate_on_limit)
1414
1899
  }
1415
1900
 
1901
+ #[pyfunction(name = "parse_document")]
1902
+ #[pyo3(signature = (html, *, max_size_bytes=None, truncate_on_limit=false))]
1903
+ fn parse_document_dict(
1904
+ py: Python<'_>,
1905
+ html: &str,
1906
+ max_size_bytes: Option<usize>,
1907
+ truncate_on_limit: bool,
1908
+ ) -> PyResult<Py<PyDict>> {
1909
+ let tree = parse_document_to_dict_tree(html, max_size_bytes, truncate_on_limit)?;
1910
+ html5_tree_to_py_dict(py, tree)
1911
+ }
1912
+
1913
+ #[pyfunction(name = "parse_fragment")]
1914
+ #[pyo3(signature = (html, *, max_size_bytes=None, truncate_on_limit=false))]
1915
+ fn parse_fragment_dict(
1916
+ py: Python<'_>,
1917
+ html: &str,
1918
+ max_size_bytes: Option<usize>,
1919
+ truncate_on_limit: bool,
1920
+ ) -> PyResult<Py<PyDict>> {
1921
+ let tree = parse_fragment_to_dict_tree(html, max_size_bytes, truncate_on_limit)?;
1922
+ html5_tree_to_py_dict(py, tree)
1923
+ }
1924
+
1416
1925
  #[pyfunction]
1417
1926
  #[pyo3(signature = (html, *, max_size_bytes=None, truncate_on_limit=false))]
1418
1927
  fn prettify(
@@ -1629,6 +2138,8 @@ fn scraper_rs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
1629
2138
 
1630
2139
  // Top-level functions
1631
2140
  m.add_function(wrap_pyfunction!(parse, m)?)?;
2141
+ m.add_function(wrap_pyfunction!(parse_document_dict, m)?)?;
2142
+ m.add_function(wrap_pyfunction!(parse_fragment_dict, m)?)?;
1632
2143
  m.add_function(wrap_pyfunction!(prettify, m)?)?;
1633
2144
  m.add_function(wrap_pyfunction!(select, m)?)?;
1634
2145
  m.add_function(wrap_pyfunction!(select_first, m)?)?;
@@ -7,6 +7,8 @@ from scraper_rs import (
7
7
  __version__,
8
8
  first,
9
9
  parse,
10
+ parse_document,
11
+ parse_fragment,
10
12
  prettify,
11
13
  select,
12
14
  select_first,
@@ -204,6 +206,56 @@ def test_top_level_parse_and_select(sample_html: str) -> None:
204
206
  assert xpath_first(sample_html, "//div[@data-id='1']/a").text == "First"
205
207
 
206
208
 
209
+ def test_parse_document_to_dict(sample_html: str) -> None:
210
+ parsed = parse_document(sample_html)
211
+
212
+ assert parsed["node_type"] == "document"
213
+ assert parsed["quirks_mode"] in {"quirks", "limited-quirks", "no-quirks"}
214
+ assert isinstance(parsed["errors"], list)
215
+
216
+ html_node = next(
217
+ child
218
+ for child in parsed["children"]
219
+ if child["node_type"] == "element" and child["tag"] == "html"
220
+ )
221
+ body_node = next(
222
+ child
223
+ for child in html_node["children"]
224
+ if child["node_type"] == "element" and child["tag"] == "body"
225
+ )
226
+ first_div = next(
227
+ child
228
+ for child in body_node["children"]
229
+ if child["node_type"] == "element" and child["tag"] == "div"
230
+ )
231
+
232
+ assert first_div["attrs"]["class"] == "item"
233
+ assert first_div["attrs"]["data-id"] == "1"
234
+
235
+
236
+ def test_parse_fragment_to_dict() -> None:
237
+ parsed = parse_fragment(
238
+ '<div class="item">Hello <span>world</span><!-- note --></div>'
239
+ )
240
+
241
+ assert parsed["node_type"] == "document_fragment"
242
+ assert parsed["errors"] == []
243
+
244
+ div_node = next(
245
+ child
246
+ for child in parsed["children"]
247
+ if child["node_type"] == "element" and child["tag"] == "div"
248
+ )
249
+
250
+ assert div_node["attrs"]["class"] == "item"
251
+ assert div_node["children"][0]["node_type"] == "text"
252
+ assert div_node["children"][0]["text"] == "Hello "
253
+ assert div_node["children"][1]["tag"] == "span"
254
+ assert div_node["children"][1]["children"][0]["text"] == "world"
255
+ assert div_node["children"][2]["node_type"] == "comment"
256
+ assert div_node["children"][2]["text"] == " note "
257
+
258
+
207
259
  def test_css_alias_and_invalid_selector(sample_html: str) -> None:
208
260
  doc = Document(sample_html)
209
261
 
@@ -43,26 +43,26 @@ wheels = [
43
43
 
44
44
  [[package]]
45
45
  name = "maturin"
46
- version = "1.12.4"
46
+ version = "1.12.6"
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/2f/a6/54e73f0ec0224488ae25196ce8b4df298cae613b099ad0c4f39dd7e3a8d2/maturin-1.12.4.tar.gz", hash = "sha256:06f6438be7e723aaf4b412fb34839854b540a1350f7614fadf5bd1db2b98d5f7", size = 262134, upload-time = "2026-02-21T10:24:25.64Z" }
51
+ sdist = { url = "https://files.pythonhosted.org/packages/0c/18/8b2eebd3ea086a5ec73d7081f95ec64918ceda1900075902fc296ea3ad55/maturin-1.12.6.tar.gz", hash = "sha256:d37be3a811a7f2ee28a0fa0964187efa50e90f21da0c6135c27787fa0b6a89db", size = 269165, upload-time = "2026-03-01T14:54:04.21Z" }
52
52
  wheels = [
53
- { url = "https://files.pythonhosted.org/packages/e1/cd/8285f37bf968b8485e3c7eb43349a5adbccfddfc487cd4327fb9104578cc/maturin-1.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cf8a0eddef9ab8773bc823c77aed3de9a5c85fb760c86448048a79ef89794c81", size = 9758449, upload-time = "2026-02-21T10:24:35.382Z" },
54
- { url = "https://files.pythonhosted.org/packages/d9/91/f51191db83735f77bc988c8034730bb63b750a4a1a04f9c8cba10f44ad45/maturin-1.12.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:eba1bd1c1513d00fec75228da98622c68a9f50f9693aaa6fb7dacb244e7bbf26", size = 18938848, upload-time = "2026-02-21T10:24:10.701Z" },
55
- { url = "https://files.pythonhosted.org/packages/65/47/03c422adeac93b903354b322bba632754fdb134b27ace71b5603feba5906/maturin-1.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89749cfc0e6baf5517fa370729a98955552e42fefc406b95732d5c8e85bc90c0", size = 9791641, upload-time = "2026-02-21T10:24:21.72Z" },
56
- { url = "https://files.pythonhosted.org/packages/5e/30/dd78acf6afc48d358512b5ed928fd24e2bc6b68db69b1f6bba3ffd7bcaed/maturin-1.12.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:4d68664e5b81f282144a3b717a7e8593ec94ac87d7ae563a4c464e93d6cde877", size = 9811625, upload-time = "2026-02-21T10:24:08.152Z" },
57
- { url = "https://files.pythonhosted.org/packages/e3/9a/a6e358a18815ab090ef55187da0066df01a955c7c44a61fb83b127055f23/maturin-1.12.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:88e09e6c386b08974fab0c7e4c07d7c7c50a0ba63095d31e930d80568488e1be", size = 10255812, upload-time = "2026-02-21T10:24:15.117Z" },
58
- { url = "https://files.pythonhosted.org/packages/4a/c5/84dfcce1f3475237cba6e6201a1939980025afbb41c076aa5147b10ac202/maturin-1.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5cc56481b0f360571587c35a1d960ce6d0a0258d49aebb6af98fff9db837c337", size = 9645462, upload-time = "2026-02-21T10:24:28.814Z" },
59
- { url = "https://files.pythonhosted.org/packages/de/82/0845fff86ea044028302db17bc611e9bfe1b7b2c992756162cbe71267df5/maturin-1.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:8fd7eb0c9bb017e98d81aa86a1d440b912fe4f7f219571035dd6ab330c82071c", size = 9593649, upload-time = "2026-02-21T10:24:33.376Z" },
60
- { url = "https://files.pythonhosted.org/packages/2b/14/6e8969cd48c7c8ea27d7638e572d46eeba9aa0cb370d3031eb6a3f10ff8d/maturin-1.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:5bb07c349dd066277a61e017a6d6e0860cd54b7b33f8ead10b9e5a4ffb740a0a", size = 12681515, upload-time = "2026-02-21T10:24:31.097Z" },
61
- { url = "https://files.pythonhosted.org/packages/ac/8d/2ad86623dca3cfa394049f4220188dececa6e4cefd73ac1f1385fc79c876/maturin-1.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c21baaed066b5bec893db2d261bfe3b9da054d99c018326f0bdcf1dc4c3a1eb9", size = 10448453, upload-time = "2026-02-21T10:24:26.827Z" },
62
- { url = "https://files.pythonhosted.org/packages/9c/eb/c66e2d3272e74dd590ae81bb51590bd98c3cd4e3f6629d4e4218bd6a5c28/maturin-1.12.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:939c4c57efa8ea982a991ee3ccb3992364622e9cbd1ede922b5cfb0f652bf517", size = 9970879, upload-time = "2026-02-21T10:24:12.881Z" },
63
- { url = "https://files.pythonhosted.org/packages/38/a0/998f8063d67fa19639179af7e8ea46016ceaa12f85b9720a2e4846449f43/maturin-1.12.4-py3-none-win32.whl", hash = "sha256:d72f626616292cb3e283941f47835ffc608207ebd8f95f4c50523a6631ffcb2e", size = 8518146, upload-time = "2026-02-21T10:24:17.296Z" },
64
- { url = "https://files.pythonhosted.org/packages/69/14/6ceea315db6e47093442ec70c2d01bb011d69f5243de5fc0e6a5fab97513/maturin-1.12.4-py3-none-win_amd64.whl", hash = "sha256:ab32c5ff7579a549421cae03e6297d3b03d7b81fa2934e3bdf24a102d99eb378", size = 9863686, upload-time = "2026-02-21T10:24:19.35Z" },
65
- { url = "https://files.pythonhosted.org/packages/d4/28/73e14739c6f7605ff9b9d108726d3ff529d4f91a7838739b4dd0afd33ec1/maturin-1.12.4-py3-none-win_arm64.whl", hash = "sha256:b8c05d24209af50ed9ae9e5de473c84866b9676c637fcfad123ee57f4a9ed098", size = 8557843, upload-time = "2026-02-21T10:24:23.894Z" },
53
+ { url = "https://files.pythonhosted.org/packages/71/8b/9ddfde8a485489e3ebdc50ee3042ef1c854f00dfea776b951068f6ffe451/maturin-1.12.6-py3-none-linux_armv6l.whl", hash = "sha256:6892b4176992fcc143f9d1c1c874a816e9a041248eef46433db87b0f0aff4278", size = 9789847, upload-time = "2026-03-01T14:54:09.172Z" },
54
+ { url = "https://files.pythonhosted.org/packages/ef/e8/5f7fd3763f214a77ac0388dbcc71cc30aec5490016bd0c8e6bd729fc7b0a/maturin-1.12.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c0c742beeeef7fb93b6a81bd53e75507887e396fd1003c45117658d063812dad", size = 19023833, upload-time = "2026-03-01T14:53:46.743Z" },
55
+ { url = "https://files.pythonhosted.org/packages/e0/7f/706ff3839c8b2046436d4c2bc97596c558728264d18abc298a1ad862a4be/maturin-1.12.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cb41139295eed6411d3cdafc7430738094c2721f34b7eeb44f33cac516115dc", size = 9821620, upload-time = "2026-03-01T14:54:12.04Z" },
56
+ { url = "https://files.pythonhosted.org/packages/0e/9c/70917fb123c8dd6b595e913616c9c72d730cbf4a2b6cac8077dc02a12586/maturin-1.12.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:351f3af1488a7cbdcff3b6d8482c17164273ac981378a13a4a9937a49aec7d71", size = 9849107, upload-time = "2026-03-01T14:53:48.971Z" },
57
+ { url = "https://files.pythonhosted.org/packages/59/ea/f1d6ad95c0a12fbe761a7c28a57540341f188564dbe8ad730a4d1788cd32/maturin-1.12.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:6dbddfe4dc7ddee60bbac854870bd7cfec660acb54d015d24597d59a1c828f61", size = 10242855, upload-time = "2026-03-01T14:53:44.605Z" },
58
+ { url = "https://files.pythonhosted.org/packages/93/1b/2419843a4f1d2fb4747f3dc3d9c4a2881cd97a3274dd94738fcdf0835e79/maturin-1.12.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8fdb0f63e77ee3df0f027a120e9af78dbc31edf0eb0f263d55783c250c33b728", size = 9674972, upload-time = "2026-03-01T14:53:52.763Z" },
59
+ { url = "https://files.pythonhosted.org/packages/71/46/b60ab2fc996d904b40e55bd475599dcdccd8f7ad3e649bf95e87970df466/maturin-1.12.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:fa84b7493a2e80759cacc2e668fa5b444d55b9994e90707c42904f55d6322c1e", size = 9645755, upload-time = "2026-03-01T14:53:58.497Z" },
60
+ { url = "https://files.pythonhosted.org/packages/a4/96/03f2b55a8c226805115232fc23c4a4f33f0c9d39e11efab8166dc440f80d/maturin-1.12.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:e90dc12bc6a38e9495692a36c9e231c4d7e0c9bfde60719468ab7d8673db3c45", size = 12737612, upload-time = "2026-03-01T14:54:05.393Z" },
61
+ { url = "https://files.pythonhosted.org/packages/2b/c2/648667022c5b53cdccefa67c245e8a984970f3045820f00c2e23bdb2aff4/maturin-1.12.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06fc8d089f98623ce924c669b70911dfed30f9a29956c362945f727f9abc546b", size = 10455028, upload-time = "2026-03-01T14:54:07.349Z" },
62
+ { url = "https://files.pythonhosted.org/packages/63/d6/5b5efe3ca0c043357ed3f8d2b2d556169fdbf1ff75e50e8e597708a359d2/maturin-1.12.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:75133e56274d43b9227fd49dca9a86e32f1fd56a7b55544910c4ce978c2bb5aa", size = 10014531, upload-time = "2026-03-01T14:53:54.548Z" },
63
+ { url = "https://files.pythonhosted.org/packages/68/d5/39c594c27b1a8b32a0cb95fff9ad60b888c4352d1d1c389ac1bd20dc1e16/maturin-1.12.6-py3-none-win32.whl", hash = "sha256:3f32e0a3720b81423c9d35c14e728cb1f954678124749776dc72d533ea1115e8", size = 8553012, upload-time = "2026-03-01T14:53:50.706Z" },
64
+ { url = "https://files.pythonhosted.org/packages/94/66/b262832a91747e04051e21f986bd01a8af81fbffafacc7d66a11e79aab5f/maturin-1.12.6-py3-none-win_amd64.whl", hash = "sha256:977290159d252db946054a0555263c59b3d0c7957135c69e690f4b1558ee9983", size = 9890470, upload-time = "2026-03-01T14:53:56.659Z" },
65
+ { url = "https://files.pythonhosted.org/packages/e3/47/76b8ca470ddc8d7d36aa8c15f5a6aed1841806bb93a0f4ead8ee61e9a088/maturin-1.12.6-py3-none-win_arm64.whl", hash = "sha256:bae91976cdc8148038e13c881e1e844e5c63e58e026e8b9945aa2d19b3b4ae89", size = 8606158, upload-time = "2026-03-01T14:54:02.423Z" },
66
66
  ]
67
67
 
68
68
  [[package]]
@@ -126,32 +126,32 @@ wheels = [
126
126
 
127
127
  [[package]]
128
128
  name = "ruff"
129
- version = "0.15.2"
129
+ version = "0.15.7"
130
130
  source = { registry = "https://pypi.org/simple" }
131
- sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
132
132
  wheels = [
133
- { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
134
- { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
135
- { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
136
- { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
137
- { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
138
- { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
139
- { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
140
- { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
141
- { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
142
- { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
143
- { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
144
- { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
145
- { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
146
- { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
147
- { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
148
- { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
149
- { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
133
+ { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
134
+ { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
135
+ { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
136
+ { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
137
+ { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
138
+ { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
139
+ { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
140
+ { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
141
+ { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
142
+ { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
143
+ { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
144
+ { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
145
+ { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
146
+ { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
147
+ { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
148
+ { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
149
+ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
150
150
  ]
151
151
 
152
152
  [[package]]
153
153
  name = "scraper-rust"
154
- version = "0.4.0"
154
+ version = "0.4.2"
155
155
  source = { editable = "." }
156
156
 
157
157
  [package.optional-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes