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.
@@ -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.2.1
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"