scraper-rust 0.5.0__tar.gz → 0.5.1__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 (50) hide show
  1. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/Cargo.lock +47 -1
  2. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/Cargo.toml +3 -2
  3. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/PKG-INFO +2 -1
  4. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/README.md +1 -0
  5. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/pyproject.toml +1 -1
  6. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/scraper_rs/asyncio.py +41 -80
  7. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/runtime.rs +6 -1
  8. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/uv.lock +1 -1
  9. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/.github/workflows/benchmark.yml +0 -0
  10. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/.github/workflows/bump-version.yml +0 -0
  11. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/.github/workflows/release.yml +0 -0
  12. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/.github/workflows/tests.yml +0 -0
  13. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/.gitignore +0 -0
  14. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/AGENTS.md +0 -0
  15. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/LICENSE +0 -0
  16. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/benchmarks/README.md +0 -0
  17. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/benchmarks/bench_parse_memory.py +0 -0
  18. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/benchmarks/bench_sync_async.py +0 -0
  19. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/benchmarks/bench_vs_markupever.py +0 -0
  20. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/README.md +0 -0
  21. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/api.md +0 -0
  22. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/architecture.md +0 -0
  23. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/async-api.md +0 -0
  24. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/development.md +0 -0
  25. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/docs/limits-and-errors.md +0 -0
  26. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/examples/demo.py +0 -0
  27. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/examples/demo_async_document.py +0 -0
  28. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/examples/demo_asyncio.py +0 -0
  29. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/examples/demo_prettify_url.py +0 -0
  30. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/justfile +0 -0
  31. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/py.typed +0 -0
  32. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/scraper_rs/__init__.py +0 -0
  33. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/scraper_rs/asyncio.pyi +0 -0
  34. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/scraper_rs.pyi +0 -0
  35. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/async_core.rs +0 -0
  36. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/cache.rs +0 -0
  37. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/document.rs +0 -0
  38. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/element.rs +0 -0
  39. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/functions.rs +0 -0
  40. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/html5_dict.rs +0 -0
  41. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/lib.rs +0 -0
  42. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/limits.rs +0 -0
  43. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/prettify.rs +0 -0
  44. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/selectors.rs +0 -0
  45. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/tests.rs +0 -0
  46. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/text.rs +0 -0
  47. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/src/xpath.rs +0 -0
  48. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/tests/test_asyncio.py +0 -0
  49. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/tests/test_performance.py +0 -0
  50. {scraper_rust-0.5.0 → scraper_rust-0.5.1}/tests/test_scraper.py +0 -0
@@ -244,6 +244,31 @@ dependencies = [
244
244
  "libm",
245
245
  ]
246
246
 
247
+ [[package]]
248
+ name = "crossbeam-deque"
249
+ version = "0.8.6"
250
+ source = "registry+https://github.com/rust-lang/crates.io-index"
251
+ checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
252
+ dependencies = [
253
+ "crossbeam-epoch",
254
+ "crossbeam-utils",
255
+ ]
256
+
257
+ [[package]]
258
+ name = "crossbeam-epoch"
259
+ version = "0.9.18"
260
+ source = "registry+https://github.com/rust-lang/crates.io-index"
261
+ checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
262
+ dependencies = [
263
+ "crossbeam-utils",
264
+ ]
265
+
266
+ [[package]]
267
+ name = "crossbeam-utils"
268
+ version = "0.8.21"
269
+ source = "registry+https://github.com/rust-lang/crates.io-index"
270
+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
271
+
247
272
  [[package]]
248
273
  name = "cssparser"
249
274
  version = "0.37.0"
@@ -1680,6 +1705,26 @@ dependencies = [
1680
1705
  "rand_core",
1681
1706
  ]
1682
1707
 
1708
+ [[package]]
1709
+ name = "rayon"
1710
+ version = "1.12.0"
1711
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1712
+ checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
1713
+ dependencies = [
1714
+ "either",
1715
+ "rayon-core",
1716
+ ]
1717
+
1718
+ [[package]]
1719
+ name = "rayon-core"
1720
+ version = "1.13.0"
1721
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1722
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
1723
+ dependencies = [
1724
+ "crossbeam-deque",
1725
+ "crossbeam-utils",
1726
+ ]
1727
+
1683
1728
  [[package]]
1684
1729
  name = "redox_syscall"
1685
1730
  version = "0.5.18"
@@ -1832,11 +1877,12 @@ dependencies = [
1832
1877
 
1833
1878
  [[package]]
1834
1879
  name = "scraper-rs"
1835
- version = "0.5.0"
1880
+ version = "0.5.1"
1836
1881
  dependencies = [
1837
1882
  "html5ever",
1838
1883
  "pyo3",
1839
1884
  "pyo3-async-runtimes",
1885
+ "rayon",
1840
1886
  "scraper",
1841
1887
  "tokio",
1842
1888
  "xee-xpath",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "scraper-rs"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -15,7 +15,8 @@ extension-module = ["pyo3/extension-module"]
15
15
  [dependencies]
16
16
  pyo3 = { version = "0.28.3", features = ["abi3-py310"] }
17
17
  pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
18
- tokio = { version = "1", features = ["rt", "macros"] }
18
+ rayon = "1"
19
+ tokio = { version = "1", features = ["rt", "macros", "sync"] }
19
20
 
20
21
  scraper = { version = "0.27.0", git = "https://github.com/rust-scraper/scraper", features = ["atomic"], rev = "9c1eff304e45a8bccb463968268ce6758703e821" }
21
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scraper-rust
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -103,6 +103,7 @@ asyncio.run(main())
103
103
  ```
104
104
 
105
105
  All async functions accept the same keyword arguments as their sync counterparts (`max_size_bytes`, `truncate_on_limit`, etc.).
106
+ CPU-bound async parsing and selection work is offloaded through the Rust async core onto Rayon worker threads so concurrent coroutines do not run that work on the event-loop thread.
106
107
  `AsyncDocument` supports `async with` for automatic cleanup in coroutine code, and `AsyncElement` / `AsyncDocument` both expose async `.prettify()` helpers.
107
108
 
108
109
  ### Large documents and memory safety
@@ -76,6 +76,7 @@ asyncio.run(main())
76
76
  ```
77
77
 
78
78
  All async functions accept the same keyword arguments as their sync counterparts (`max_size_bytes`, `truncate_on_limit`, etc.).
79
+ CPU-bound async parsing and selection work is offloaded through the Rust async core onto Rayon worker threads so concurrent coroutines do not run that work on the event-loop thread.
79
80
  `AsyncDocument` supports `async with` for automatic cleanup in coroutine code, and `AsyncElement` / `AsyncDocument` both expose async `.prettify()` helpers.
80
81
 
81
82
  ### Large documents and memory safety
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scraper-rust"
7
- version = "0.5.0"
7
+ version = "0.5.1"
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"
@@ -2,44 +2,35 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
6
-
7
5
  from .scraper_rs import (
8
- Document as _Document,
9
- Element as _Element,
10
- first as _first_sync,
11
- prettify as _prettify_sync,
12
- select as _select_sync,
13
- select_first as _select_first_sync,
14
- xpath as _xpath_sync,
15
- xpath_first as _xpath_first_sync,
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,
16
15
  )
17
16
 
18
17
 
19
- def _wrap_element(element: _Element | None) -> "AsyncElement | None":
18
+ def _wrap_element(element: _AsyncElementCore | None) -> "AsyncElement | None":
20
19
  if element is None:
21
20
  return None
22
21
  return AsyncElement(element)
23
22
 
24
23
 
25
- def _wrap_elements(elements: list[_Element]) -> list["AsyncElement"]:
24
+ def _wrap_elements(elements: list[_AsyncElementCore]) -> list["AsyncElement"]:
26
25
  return [AsyncElement(element) for element in elements]
27
26
 
28
27
 
29
- def _parse_state(html: str, kwargs: dict[str, int | bool | None]) -> tuple[str, str]:
30
- document = _Document(html, **kwargs)
31
- try:
32
- return document.html, document.text
33
- finally:
34
- document.close()
35
-
36
-
37
28
  class AsyncElement:
38
29
  """Async wrapper for immutable element snapshots."""
39
30
 
40
31
  __slots__ = ("_element",)
41
32
 
42
- def __init__(self, element: _Element) -> None:
33
+ def __init__(self, element: _AsyncElementCore) -> None:
43
34
  self._element = element
44
35
 
45
36
  @property
@@ -65,32 +56,25 @@ class AsyncElement:
65
56
  return self._element.get(name, default)
66
57
 
67
58
  async def select(self, css: str) -> list["AsyncElement"]:
68
- await asyncio.sleep(0)
69
- return _wrap_elements(self._element.select(css))
59
+ return _wrap_elements(await self._element.select(css))
70
60
 
71
61
  async def select_first(self, css: str) -> "AsyncElement | None":
72
- await asyncio.sleep(0)
73
- return _wrap_element(self._element.select_first(css))
62
+ return _wrap_element(await self._element.select_first(css))
74
63
 
75
64
  async def find(self, css: str) -> "AsyncElement | None":
76
- await asyncio.sleep(0)
77
- return _wrap_element(self._element.find(css))
65
+ return _wrap_element(await self._element.find(css))
78
66
 
79
67
  async def css(self, css: str) -> list["AsyncElement"]:
80
- await asyncio.sleep(0)
81
- return _wrap_elements(self._element.css(css))
68
+ return _wrap_elements(await self._element.css(css))
82
69
 
83
70
  async def xpath(self, expr: str) -> list["AsyncElement"]:
84
- await asyncio.sleep(0)
85
- return _wrap_elements(self._element.xpath(expr))
71
+ return _wrap_elements(await self._element.xpath(expr))
86
72
 
87
73
  async def xpath_first(self, expr: str) -> "AsyncElement | None":
88
- await asyncio.sleep(0)
89
- return _wrap_element(self._element.xpath_first(expr))
74
+ return _wrap_element(await self._element.xpath_first(expr))
90
75
 
91
76
  async def prettify(self) -> str:
92
- await asyncio.sleep(0)
93
- return self._element.prettify()
77
+ return await self._element.prettify()
94
78
 
95
79
  def to_dict(self) -> dict[str, str | dict[str, str]]:
96
80
  return self._element.to_dict()
@@ -100,59 +84,44 @@ class AsyncElement:
100
84
 
101
85
 
102
86
  class AsyncDocument:
103
- """Async wrapper that stores only shareable document state."""
87
+ """Async wrapper around Rust-owned document state."""
104
88
 
105
- __slots__ = ("_html", "_text", "_closed")
89
+ __slots__ = ("_document",)
106
90
 
107
- def __init__(self, html: str, text: str) -> None:
108
- self._html = html
109
- self._text = text
110
- self._closed = False
91
+ def __init__(self, document: _AsyncDocumentCore) -> None:
92
+ self._document = document
111
93
 
112
94
  @property
113
95
  def html(self) -> str:
114
- return "" if self._closed else self._html
96
+ return self._document.html
115
97
 
116
98
  @property
117
99
  def text(self) -> str:
118
- return "" if self._closed else self._text
100
+ return self._document.text
119
101
 
120
102
  async def select(self, css: str) -> list[AsyncElement]:
121
- if self._closed:
122
- return []
123
- return await select(self._html, css)
103
+ return _wrap_elements(await self._document.select(css))
124
104
 
125
105
  async def select_first(self, css: str) -> AsyncElement | None:
126
- if self._closed:
127
- return None
128
- return await select_first(self._html, css)
106
+ return _wrap_element(await self._document.select_first(css))
129
107
 
130
108
  async def find(self, css: str) -> AsyncElement | None:
131
- return await self.select_first(css)
109
+ return _wrap_element(await self._document.find(css))
132
110
 
133
111
  async def css(self, css: str) -> list[AsyncElement]:
134
- return await self.select(css)
112
+ return _wrap_elements(await self._document.css(css))
135
113
 
136
114
  async def xpath(self, expr: str) -> list[AsyncElement]:
137
- if self._closed:
138
- return []
139
- return await xpath(self._html, expr)
115
+ return _wrap_elements(await self._document.xpath(expr))
140
116
 
141
117
  async def xpath_first(self, expr: str) -> AsyncElement | None:
142
- if self._closed:
143
- return None
144
- return await xpath_first(self._html, expr)
118
+ return _wrap_element(await self._document.xpath_first(expr))
145
119
 
146
120
  async def prettify(self) -> str:
147
- if self._closed:
148
- return ""
149
- await asyncio.sleep(0)
150
- return _prettify_sync(self._html)
121
+ return await self._document.prettify()
151
122
 
152
123
  def close(self) -> None:
153
- self._closed = True
154
- self._html = ""
155
- self._text = ""
124
+ self._document.close()
156
125
 
157
126
  def __enter__(self) -> "AsyncDocument":
158
127
  return self
@@ -167,43 +136,35 @@ class AsyncDocument:
167
136
  self.close()
168
137
 
169
138
  def __repr__(self) -> str:
170
- return f"<AsyncDocument len_html={len(self.html)}>"
139
+ return repr(self._document)
171
140
 
172
141
 
173
142
  async def parse(html: str, **kwargs) -> "AsyncDocument":
174
- await asyncio.sleep(0)
175
- parsed_html, text = _parse_state(html, kwargs)
176
- return AsyncDocument(parsed_html, text)
143
+ return AsyncDocument(await _parse_async(html, **kwargs))
177
144
 
178
145
 
179
146
  async def select(html: str, css: str, **kwargs) -> list["AsyncElement"]:
180
- await asyncio.sleep(0)
181
- return _wrap_elements(_select_sync(html, css, **kwargs))
147
+ return _wrap_elements(await _select_async(html, css, **kwargs))
182
148
 
183
149
 
184
150
  async def select_first(html: str, css: str, **kwargs) -> "AsyncElement | None":
185
- await asyncio.sleep(0)
186
- return _wrap_element(_select_first_sync(html, css, **kwargs))
151
+ return _wrap_element(await _select_first_async(html, css, **kwargs))
187
152
 
188
153
 
189
154
  async def first(html: str, css: str, **kwargs) -> "AsyncElement | None":
190
- await asyncio.sleep(0)
191
- return _wrap_element(_first_sync(html, css, **kwargs))
155
+ return _wrap_element(await _first_async(html, css, **kwargs))
192
156
 
193
157
 
194
158
  async def xpath(html: str, expr: str, **kwargs) -> list["AsyncElement"]:
195
- await asyncio.sleep(0)
196
- return _wrap_elements(_xpath_sync(html, expr, **kwargs))
159
+ return _wrap_elements(await _xpath_async(html, expr, **kwargs))
197
160
 
198
161
 
199
162
  async def xpath_first(html: str, expr: str, **kwargs) -> "AsyncElement | None":
200
- await asyncio.sleep(0)
201
- return _wrap_element(_xpath_first_sync(html, expr, **kwargs))
163
+ return _wrap_element(await _xpath_first_async(html, expr, **kwargs))
202
164
 
203
165
 
204
166
  async def prettify(html: str, **kwargs) -> str:
205
- await asyncio.sleep(0)
206
- return _prettify_sync(html, **kwargs)
167
+ return await _prettify_async(html, **kwargs)
207
168
 
208
169
 
209
170
  __all__ = [
@@ -21,7 +21,12 @@ where
21
21
  F: FnOnce() -> PyResult<T> + Send + 'static,
22
22
  T: Send + 'static,
23
23
  {
24
- tokio::task::spawn_blocking(work)
24
+ let (sender, receiver) = tokio::sync::oneshot::channel();
25
+ rayon::spawn(move || {
26
+ let _ = sender.send(work());
27
+ });
28
+
29
+ receiver
25
30
  .await
26
31
  .map_err(|e| PyValueError::new_err(format!("Task join error: {e}")))?
27
32
  }
@@ -151,7 +151,7 @@ wheels = [
151
151
 
152
152
  [[package]]
153
153
  name = "scraper-rust"
154
- version = "0.5.0"
154
+ version = "0.5.1"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes