scrapeunblocker-haystack 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/CHANGELOG.md +18 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/PKG-INFO +8 -1
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/README.md +7 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/src/scrapeunblocker_haystack/__init__.py +2 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/src/scrapeunblocker_haystack/fetcher.py +19 -1
- scrapeunblocker_haystack-0.3.0/src/scrapeunblocker_haystack/version.py +1 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/tests/test_components.py +48 -0
- scrapeunblocker_haystack-0.2.1/src/scrapeunblocker_haystack/version.py +0 -1
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/.github/workflows/publish.yml +0 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/.github/workflows/test.yml +0 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/.gitignore +0 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/LICENSE +0 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/pyproject.toml +0 -0
- {scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/src/scrapeunblocker_haystack/search.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- With `parsed_data=True`, a page that loads but holds no structured data is
|
|
6
|
+
now a Document instead of being skipped. The API answers it as HTTP 200 with
|
|
7
|
+
`data_extracted: false` and the rendered HTML (billed like a plain fetch),
|
|
8
|
+
replacing the 422 `no_data_extracted` handled in 0.2.2. The Document's JSON
|
|
9
|
+
carries that HTML, and `meta["data_extracted"]` is `False` (`True` for a
|
|
10
|
+
successful parse). `NoDataExtractedError` is no longer raised; it stays
|
|
11
|
+
importable so existing `except` clauses keep working.
|
|
12
|
+
|
|
13
|
+
## 0.2.2
|
|
14
|
+
|
|
15
|
+
- With `parsed_data=True`, a page that loads but holds no structured data
|
|
16
|
+
(the API answers HTTP 422 `no_data_extracted`, not billed, no HTML) no longer
|
|
17
|
+
shows up as a generic request failure. The fetcher skips it with a clear
|
|
18
|
+
warning, or raises the new `NoDataExtractedError` when
|
|
19
|
+
`raise_on_failure=True`. Fetch without `parsed_data` to get the HTML.
|
|
20
|
+
|
|
3
21
|
## 0.2.1
|
|
4
22
|
|
|
5
23
|
- A URL whose page does not exist (the site answers HTTP 404 or 410) no longer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scrapeunblocker-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Haystack integration for ScrapeUnblocker - scrape pages behind anti-bot protections
|
|
5
5
|
Project-URL: Homepage, https://www.scrapeunblocker.com
|
|
6
6
|
Project-URL: Documentation, https://docs.scrapeunblocker.com
|
|
@@ -120,6 +120,13 @@ Steps are **non-idempotent**. If a step fails, the fetch raises
|
|
|
120
120
|
which is logged and skipped by default, or re-raised when
|
|
121
121
|
`raise_on_failure=True`.
|
|
122
122
|
|
|
123
|
+
#### Pages with nothing to parse
|
|
124
|
+
|
|
125
|
+
With `parsed_data=True`, a page that loads but holds no structured data still
|
|
126
|
+
yields a Document. Its JSON carries `"data_extracted": false` and the rendered
|
|
127
|
+
page in `html`, and `meta["data_extracted"]` is `False`, so you can route it to
|
|
128
|
+
an HTML converter. The call is billed like a plain HTML fetch.
|
|
129
|
+
|
|
123
130
|
#### Pages that do not exist
|
|
124
131
|
|
|
125
132
|
If the site answers HTTP 404 or 410, there is no page to index: the URL is
|
|
@@ -95,6 +95,13 @@ Steps are **non-idempotent**. If a step fails, the fetch raises
|
|
|
95
95
|
which is logged and skipped by default, or re-raised when
|
|
96
96
|
`raise_on_failure=True`.
|
|
97
97
|
|
|
98
|
+
#### Pages with nothing to parse
|
|
99
|
+
|
|
100
|
+
With `parsed_data=True`, a page that loads but holds no structured data still
|
|
101
|
+
yields a Document. Its JSON carries `"data_extracted": false` and the rendered
|
|
102
|
+
page in `html`, and `meta["data_extracted"]` is `False`, so you can route it to
|
|
103
|
+
an HTML converter. The call is billed like a plain HTML fetch.
|
|
104
|
+
|
|
98
105
|
#### Pages that do not exist
|
|
99
106
|
|
|
100
107
|
If the site answers HTTP 404 or 410, there is no page to index: the URL is
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from scrapeunblocker_haystack.fetcher import (
|
|
2
|
+
NoDataExtractedError,
|
|
2
3
|
ScrapeUnblockerFetcher,
|
|
3
4
|
StepExecutionError,
|
|
4
5
|
TargetNotFoundError,
|
|
@@ -6,6 +7,7 @@ from scrapeunblocker_haystack.fetcher import (
|
|
|
6
7
|
from scrapeunblocker_haystack.search import ScrapeUnblockerWebSearch
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
10
|
+
"NoDataExtractedError",
|
|
9
11
|
"ScrapeUnblockerFetcher",
|
|
10
12
|
"ScrapeUnblockerWebSearch",
|
|
11
13
|
"StepExecutionError",
|
|
@@ -35,6 +35,19 @@ class TargetNotFoundError(Exception):
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class NoDataExtractedError(Exception):
|
|
39
|
+
"""No longer raised; kept so existing ``except`` clauses still import.
|
|
40
|
+
|
|
41
|
+
A ``parsed_data`` page with no structured data comes back as a billed
|
|
42
|
+
HTTP 200 carrying ``data_extracted: false`` and the rendered HTML, so the
|
|
43
|
+
fetcher returns it as a Document (``meta["data_extracted"]`` is False).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, url: str) -> None:
|
|
47
|
+
self.url = url
|
|
48
|
+
super().__init__(f"No structured data could be extracted from {url}.")
|
|
49
|
+
|
|
50
|
+
|
|
38
51
|
def _target_gone_status(response: requests.Response) -> Optional[int]:
|
|
39
52
|
"""The target's own 404/410 answer, or None.
|
|
40
53
|
|
|
@@ -225,7 +238,10 @@ class ScrapeUnblockerFetcher:
|
|
|
225
238
|
Fetch each URL and return one Document per successfully fetched page.
|
|
226
239
|
|
|
227
240
|
A URL whose page does not exist (the site answers 404/410) yields no
|
|
228
|
-
Document; with `raise_on_failure` it raises `TargetNotFoundError`.
|
|
241
|
+
Document; with `raise_on_failure` it raises `TargetNotFoundError`. With
|
|
242
|
+
`parsed_data`, a page holding no structured data still yields a
|
|
243
|
+
Document: its JSON carries `data_extracted: false` and the rendered
|
|
244
|
+
`html`, and `meta["data_extracted"]` is False.
|
|
229
245
|
|
|
230
246
|
:param urls: URLs to fetch.
|
|
231
247
|
:returns: A dictionary with a `documents` key holding the fetched pages.
|
|
@@ -265,6 +281,8 @@ class ScrapeUnblockerFetcher:
|
|
|
265
281
|
content = json.dumps(payload, ensure_ascii=False)
|
|
266
282
|
if self.list_elements and isinstance(payload, dict):
|
|
267
283
|
meta["count"] = payload.get("count")
|
|
284
|
+
elif self.parsed_data and isinstance(payload, dict):
|
|
285
|
+
meta["data_extracted"] = payload.get("data_extracted") is not False
|
|
268
286
|
except ValueError:
|
|
269
287
|
content = response.text
|
|
270
288
|
else:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -10,6 +10,7 @@ from haystack.utils import Secret
|
|
|
10
10
|
from scrapeunblocker_haystack import (
|
|
11
11
|
ScrapeUnblockerFetcher,
|
|
12
12
|
ScrapeUnblockerWebSearch,
|
|
13
|
+
NoDataExtractedError,
|
|
13
14
|
StepExecutionError,
|
|
14
15
|
TargetNotFoundError,
|
|
15
16
|
)
|
|
@@ -297,3 +298,50 @@ class TestTargetNotFound:
|
|
|
297
298
|
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY)
|
|
298
299
|
|
|
299
300
|
assert len(fetcher.run(urls=["https://example.com"])["documents"]) == 1
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class TestNoDataExtracted:
|
|
304
|
+
BODY = {
|
|
305
|
+
"data": {"page_type": "unknown", "data": {}},
|
|
306
|
+
"data_extracted": False,
|
|
307
|
+
"detail": "The page was rendered, but no structured data could be extracted from it.",
|
|
308
|
+
"html": "<html><title>Example Domain</title></html>",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
312
|
+
def test_empty_parse_is_a_document_with_the_html(self, mock_post):
|
|
313
|
+
mock_post.side_effect = [
|
|
314
|
+
_response(json_data=self.BODY),
|
|
315
|
+
_response(json_data={"data": {"page_type": "product", "data": {"title": "x"}}}),
|
|
316
|
+
]
|
|
317
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, parsed_data=True)
|
|
318
|
+
|
|
319
|
+
empty, parsed = fetcher.run(urls=["https://example.com/empty", "https://example.com"])[
|
|
320
|
+
"documents"
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
assert empty.meta["data_extracted"] is False
|
|
324
|
+
assert json.loads(empty.content)["html"] == self.BODY["html"]
|
|
325
|
+
assert parsed.meta["data_extracted"] is True
|
|
326
|
+
|
|
327
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
328
|
+
def test_empty_parse_does_not_raise(self, mock_post):
|
|
329
|
+
mock_post.return_value = _response(json_data=self.BODY)
|
|
330
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, parsed_data=True, raise_on_failure=True)
|
|
331
|
+
|
|
332
|
+
assert len(fetcher.run(urls=["https://example.com/empty"])["documents"]) == 1
|
|
333
|
+
|
|
334
|
+
def test_error_class_still_importable(self):
|
|
335
|
+
assert issubclass(NoDataExtractedError, Exception)
|
|
336
|
+
|
|
337
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
338
|
+
def test_dead_url_with_parsed_body_is_skipped(self, mock_post):
|
|
339
|
+
response = _response(
|
|
340
|
+
status_code=404, json_data={"data": {"page_type": "not_found", "data": {}}}
|
|
341
|
+
)
|
|
342
|
+
response.text = '{"data":{"page_type":"not_found","data":{}}}'
|
|
343
|
+
response.headers = {"Content-Type": "application/json", "X-Origin-Status": "404"}
|
|
344
|
+
mock_post.return_value = response
|
|
345
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, parsed_data=True)
|
|
346
|
+
|
|
347
|
+
assert fetcher.run(urls=["https://example.com/gone"])["documents"] == []
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.1"
|
{scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
{scrapeunblocker_haystack-0.2.1 → scrapeunblocker_haystack-0.3.0}/.github/workflows/test.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|