scrapeunblocker-haystack 0.3.0__tar.gz → 0.4.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.3.0 → scrapeunblocker_haystack-0.4.0}/CHANGELOG.md +16 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/PKG-INFO +36 -7
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/README.md +35 -6
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/src/scrapeunblocker_haystack/fetcher.py +40 -1
- scrapeunblocker_haystack-0.4.0/src/scrapeunblocker_haystack/version.py +1 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/tests/test_components.py +140 -0
- scrapeunblocker_haystack-0.3.0/src/scrapeunblocker_haystack/version.py +0 -1
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/.github/workflows/publish.yml +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/.github/workflows/test.yml +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/.gitignore +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/LICENSE +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/pyproject.toml +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/src/scrapeunblocker_haystack/__init__.py +0 -0
- {scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/src/scrapeunblocker_haystack/search.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- `ScrapeUnblockerFetcher` can return pages as clean Markdown for LLMs and RAG.
|
|
6
|
+
With `markdown=True` each Document's content is the page as Markdown
|
|
7
|
+
(headings, lists, GitHub-flavored tables, absolute links, images with alt
|
|
8
|
+
text, fenced code; no scripts, styles, navigation, footers or forms), ready
|
|
9
|
+
for a prompt or a splitter without an HTML converter. `main_content_only`
|
|
10
|
+
(default `True`) keeps only the main content; `False` converts the whole
|
|
11
|
+
page. `meta["markdown"]` is `True` for these Documents (`False` otherwise)
|
|
12
|
+
and `meta["main_content_only"]` records the setting. Billed like an HTML
|
|
13
|
+
fetch. `markdown` cannot be combined with `parsed_data` or `list_elements`
|
|
14
|
+
(the API refuses it), so that raises `ValueError`. Defaults are unchanged.
|
|
15
|
+
- README: the pipeline example fetches Markdown and passes it straight to the
|
|
16
|
+
prompt. The previous example connected the fetcher's Documents to
|
|
17
|
+
`HTMLToDocument`, which only accepts sources, so the pipeline did not build.
|
|
18
|
+
|
|
3
19
|
## 0.3.0
|
|
4
20
|
|
|
5
21
|
- With `parsed_data=True`, a page that loads but holds no structured data is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scrapeunblocker-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -73,6 +73,8 @@ print(result["documents"][0].content[:200])
|
|
|
73
73
|
| `time_sleep` | `None` | Seconds to wait after load before capturing |
|
|
74
74
|
| `steps` | `None` | Browser actions to run before the HTML is captured (see below) |
|
|
75
75
|
| `list_elements` | `False` | Return the parsed elements JSON instead of HTML (see below) |
|
|
76
|
+
| `markdown` | `False` | Return the page as clean Markdown instead of HTML (see below) |
|
|
77
|
+
| `main_content_only` | `True` | With `markdown`, keep only the main content (drop navigation, header, footer, sidebars) |
|
|
76
78
|
| `base_url` | `https://api.scrapeunblocker.com` | API base URL |
|
|
77
79
|
| `timeout` | `180` | HTTP timeout in seconds |
|
|
78
80
|
| `raise_on_failure` | `False` | Raise instead of skipping a URL that fails |
|
|
@@ -80,6 +82,36 @@ print(result["documents"][0].content[:200])
|
|
|
80
82
|
By default a URL that cannot be fetched is logged and skipped, so one bad URL
|
|
81
83
|
does not discard the rest of the batch.
|
|
82
84
|
|
|
85
|
+
#### Markdown for LLMs and RAG
|
|
86
|
+
|
|
87
|
+
Set `markdown=True` to get each page as clean Markdown instead of HTML:
|
|
88
|
+
headings, lists, tables (GitHub-flavored), absolute links, images with their
|
|
89
|
+
alt text and fenced code blocks, without scripts, styles, navigation, footers,
|
|
90
|
+
forms or hidden elements. The Markdown goes straight into a prompt or a
|
|
91
|
+
splitter, with no HTML converter, and is usually a small fraction of the
|
|
92
|
+
HTML's size. It is billed like an HTML fetch.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from scrapeunblocker_haystack import ScrapeUnblockerFetcher
|
|
96
|
+
|
|
97
|
+
fetcher = ScrapeUnblockerFetcher(markdown=True)
|
|
98
|
+
result = fetcher.run(
|
|
99
|
+
urls=["https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
doc = result["documents"][0]
|
|
103
|
+
print("# A Light in the Attic" in doc.content) # True
|
|
104
|
+
print(doc.meta["content_type"]) # text/markdown; charset=utf-8
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
By default only the page's main content is kept: navigation, the site header,
|
|
108
|
+
footer and sidebars are dropped. Pass `main_content_only=False` to convert the
|
|
109
|
+
whole page. `markdown` works with `proxy_country`, `time_sleep` and `steps`,
|
|
110
|
+
but not with `parsed_data` or `list_elements`, which return their own JSON
|
|
111
|
+
formats; combining them raises `ValueError`. A page that does not exist
|
|
112
|
+
(404/410) is skipped or raises `TargetNotFoundError`, as below, with the
|
|
113
|
+
site's own page as Markdown in `body`.
|
|
114
|
+
|
|
83
115
|
#### Browser steps
|
|
84
116
|
|
|
85
117
|
Pass `steps` to drive the page in a real browser before its HTML is captured -
|
|
@@ -174,12 +206,11 @@ for doc in result["documents"]:
|
|
|
174
206
|
|
|
175
207
|
## In a pipeline
|
|
176
208
|
|
|
177
|
-
|
|
209
|
+
Fetch pages as Markdown and answer a question from them:
|
|
178
210
|
|
|
179
211
|
```python
|
|
180
212
|
from haystack import Pipeline
|
|
181
213
|
from haystack.components.builders import ChatPromptBuilder
|
|
182
|
-
from haystack.components.converters import HTMLToDocument
|
|
183
214
|
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
184
215
|
from haystack.dataclasses import ChatMessage
|
|
185
216
|
|
|
@@ -194,13 +225,11 @@ prompt = [
|
|
|
194
225
|
]
|
|
195
226
|
|
|
196
227
|
pipe = Pipeline()
|
|
197
|
-
pipe.add_component("fetcher", ScrapeUnblockerFetcher())
|
|
198
|
-
pipe.add_component("converter", HTMLToDocument())
|
|
228
|
+
pipe.add_component("fetcher", ScrapeUnblockerFetcher(markdown=True))
|
|
199
229
|
pipe.add_component("prompt_builder", ChatPromptBuilder(template=prompt, required_variables="*"))
|
|
200
230
|
pipe.add_component("llm", OpenAIChatGenerator())
|
|
201
231
|
|
|
202
|
-
pipe.connect("fetcher.documents", "
|
|
203
|
-
pipe.connect("converter.documents", "prompt_builder.documents")
|
|
232
|
+
pipe.connect("fetcher.documents", "prompt_builder.documents")
|
|
204
233
|
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
205
234
|
|
|
206
235
|
result = pipe.run(
|
|
@@ -48,6 +48,8 @@ print(result["documents"][0].content[:200])
|
|
|
48
48
|
| `time_sleep` | `None` | Seconds to wait after load before capturing |
|
|
49
49
|
| `steps` | `None` | Browser actions to run before the HTML is captured (see below) |
|
|
50
50
|
| `list_elements` | `False` | Return the parsed elements JSON instead of HTML (see below) |
|
|
51
|
+
| `markdown` | `False` | Return the page as clean Markdown instead of HTML (see below) |
|
|
52
|
+
| `main_content_only` | `True` | With `markdown`, keep only the main content (drop navigation, header, footer, sidebars) |
|
|
51
53
|
| `base_url` | `https://api.scrapeunblocker.com` | API base URL |
|
|
52
54
|
| `timeout` | `180` | HTTP timeout in seconds |
|
|
53
55
|
| `raise_on_failure` | `False` | Raise instead of skipping a URL that fails |
|
|
@@ -55,6 +57,36 @@ print(result["documents"][0].content[:200])
|
|
|
55
57
|
By default a URL that cannot be fetched is logged and skipped, so one bad URL
|
|
56
58
|
does not discard the rest of the batch.
|
|
57
59
|
|
|
60
|
+
#### Markdown for LLMs and RAG
|
|
61
|
+
|
|
62
|
+
Set `markdown=True` to get each page as clean Markdown instead of HTML:
|
|
63
|
+
headings, lists, tables (GitHub-flavored), absolute links, images with their
|
|
64
|
+
alt text and fenced code blocks, without scripts, styles, navigation, footers,
|
|
65
|
+
forms or hidden elements. The Markdown goes straight into a prompt or a
|
|
66
|
+
splitter, with no HTML converter, and is usually a small fraction of the
|
|
67
|
+
HTML's size. It is billed like an HTML fetch.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from scrapeunblocker_haystack import ScrapeUnblockerFetcher
|
|
71
|
+
|
|
72
|
+
fetcher = ScrapeUnblockerFetcher(markdown=True)
|
|
73
|
+
result = fetcher.run(
|
|
74
|
+
urls=["https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
doc = result["documents"][0]
|
|
78
|
+
print("# A Light in the Attic" in doc.content) # True
|
|
79
|
+
print(doc.meta["content_type"]) # text/markdown; charset=utf-8
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
By default only the page's main content is kept: navigation, the site header,
|
|
83
|
+
footer and sidebars are dropped. Pass `main_content_only=False` to convert the
|
|
84
|
+
whole page. `markdown` works with `proxy_country`, `time_sleep` and `steps`,
|
|
85
|
+
but not with `parsed_data` or `list_elements`, which return their own JSON
|
|
86
|
+
formats; combining them raises `ValueError`. A page that does not exist
|
|
87
|
+
(404/410) is skipped or raises `TargetNotFoundError`, as below, with the
|
|
88
|
+
site's own page as Markdown in `body`.
|
|
89
|
+
|
|
58
90
|
#### Browser steps
|
|
59
91
|
|
|
60
92
|
Pass `steps` to drive the page in a real browser before its HTML is captured -
|
|
@@ -149,12 +181,11 @@ for doc in result["documents"]:
|
|
|
149
181
|
|
|
150
182
|
## In a pipeline
|
|
151
183
|
|
|
152
|
-
|
|
184
|
+
Fetch pages as Markdown and answer a question from them:
|
|
153
185
|
|
|
154
186
|
```python
|
|
155
187
|
from haystack import Pipeline
|
|
156
188
|
from haystack.components.builders import ChatPromptBuilder
|
|
157
|
-
from haystack.components.converters import HTMLToDocument
|
|
158
189
|
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
159
190
|
from haystack.dataclasses import ChatMessage
|
|
160
191
|
|
|
@@ -169,13 +200,11 @@ prompt = [
|
|
|
169
200
|
]
|
|
170
201
|
|
|
171
202
|
pipe = Pipeline()
|
|
172
|
-
pipe.add_component("fetcher", ScrapeUnblockerFetcher())
|
|
173
|
-
pipe.add_component("converter", HTMLToDocument())
|
|
203
|
+
pipe.add_component("fetcher", ScrapeUnblockerFetcher(markdown=True))
|
|
174
204
|
pipe.add_component("prompt_builder", ChatPromptBuilder(template=prompt, required_variables="*"))
|
|
175
205
|
pipe.add_component("llm", OpenAIChatGenerator())
|
|
176
206
|
|
|
177
|
-
pipe.connect("fetcher.documents", "
|
|
178
|
-
pipe.connect("converter.documents", "prompt_builder.documents")
|
|
207
|
+
pipe.connect("fetcher.documents", "prompt_builder.documents")
|
|
179
208
|
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
180
209
|
|
|
181
210
|
result = pipe.run(
|
|
@@ -103,6 +103,18 @@ class ScrapeUnblockerFetcher:
|
|
|
103
103
|
print(result["documents"][0].content[:200])
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### Markdown
|
|
107
|
+
|
|
108
|
+
Set `markdown=True` to get each page as clean Markdown, ready for an LLM or a
|
|
109
|
+
RAG pipeline, instead of HTML. By default only the main content is kept;
|
|
110
|
+
`main_content_only=False` converts the whole page:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
fetcher = ScrapeUnblockerFetcher(markdown=True)
|
|
114
|
+
result = fetcher.run(urls=["https://example.com"])
|
|
115
|
+
print(result["documents"][0].content[:200])
|
|
116
|
+
```
|
|
117
|
+
|
|
106
118
|
### Browser steps
|
|
107
119
|
|
|
108
120
|
Drive the page in a real browser before the HTML is captured - click, type,
|
|
@@ -135,6 +147,8 @@ class ScrapeUnblockerFetcher:
|
|
|
135
147
|
base_url: str = DEFAULT_BASE_URL,
|
|
136
148
|
timeout: int = DEFAULT_TIMEOUT,
|
|
137
149
|
raise_on_failure: bool = False,
|
|
150
|
+
markdown: bool = False,
|
|
151
|
+
main_content_only: bool = True,
|
|
138
152
|
) -> None:
|
|
139
153
|
"""
|
|
140
154
|
:param api_key: ScrapeUnblocker API key. Read from `SCRAPEUNBLOCKER_API_KEY` by default.
|
|
@@ -152,7 +166,21 @@ class ScrapeUnblockerFetcher:
|
|
|
152
166
|
:param timeout: HTTP timeout in seconds.
|
|
153
167
|
:param raise_on_failure: Raise instead of skipping when a URL cannot be fetched
|
|
154
168
|
(including when a browser step fails or the page does not exist).
|
|
169
|
+
:param markdown: Return each page as clean Markdown instead of HTML: headings,
|
|
170
|
+
lists, tables, absolute links, images with alt text and code blocks, without
|
|
171
|
+
scripts, styles, navigation, footers or forms. Billed like an HTML fetch.
|
|
172
|
+
Cannot be combined with `parsed_data` or `list_elements`.
|
|
173
|
+
:param main_content_only: With `markdown`, keep only the page's main content and
|
|
174
|
+
drop navigation, the site header, footer and sidebars. `False` converts the
|
|
175
|
+
whole page. Ignored without `markdown`.
|
|
176
|
+
:raises ValueError: If `markdown` is combined with `parsed_data` or `list_elements`.
|
|
155
177
|
"""
|
|
178
|
+
if markdown and (parsed_data or list_elements):
|
|
179
|
+
# The API refuses these combinations (HTTP 422): each returns its own format.
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"markdown=True cannot be combined with parsed_data or list_elements: "
|
|
182
|
+
"each returns its own format."
|
|
183
|
+
)
|
|
156
184
|
self.api_key = api_key
|
|
157
185
|
self.parsed_data = parsed_data
|
|
158
186
|
self.proxy_country = proxy_country
|
|
@@ -162,6 +190,8 @@ class ScrapeUnblockerFetcher:
|
|
|
162
190
|
self.base_url = base_url.rstrip("/")
|
|
163
191
|
self.timeout = timeout
|
|
164
192
|
self.raise_on_failure = raise_on_failure
|
|
193
|
+
self.markdown = markdown
|
|
194
|
+
self.main_content_only = main_content_only
|
|
165
195
|
|
|
166
196
|
def to_dict(self) -> Dict[str, Any]:
|
|
167
197
|
"""Serialize this component to a dictionary."""
|
|
@@ -176,6 +206,8 @@ class ScrapeUnblockerFetcher:
|
|
|
176
206
|
base_url=self.base_url,
|
|
177
207
|
timeout=self.timeout,
|
|
178
208
|
raise_on_failure=self.raise_on_failure,
|
|
209
|
+
markdown=self.markdown,
|
|
210
|
+
main_content_only=self.main_content_only,
|
|
179
211
|
)
|
|
180
212
|
|
|
181
213
|
@classmethod
|
|
@@ -196,6 +228,9 @@ class ScrapeUnblockerFetcher:
|
|
|
196
228
|
params["steps"] = json.dumps(self.steps)
|
|
197
229
|
if self.list_elements:
|
|
198
230
|
params["list_elements"] = True
|
|
231
|
+
if self.markdown:
|
|
232
|
+
params["markdown"] = True
|
|
233
|
+
params["main_content_only"] = self.main_content_only
|
|
199
234
|
|
|
200
235
|
response = requests.post(
|
|
201
236
|
f"{self.base_url}/getPageSource",
|
|
@@ -241,7 +276,8 @@ class ScrapeUnblockerFetcher:
|
|
|
241
276
|
Document; with `raise_on_failure` it raises `TargetNotFoundError`. With
|
|
242
277
|
`parsed_data`, a page holding no structured data still yields a
|
|
243
278
|
Document: its JSON carries `data_extracted: false` and the rendered
|
|
244
|
-
`html`, and `meta["data_extracted"]` is False.
|
|
279
|
+
`html`, and `meta["data_extracted"]` is False. With `markdown`, the
|
|
280
|
+
Document's content is the page as Markdown and `meta["markdown"]` is True.
|
|
245
281
|
|
|
246
282
|
:param urls: URLs to fetch.
|
|
247
283
|
:returns: A dictionary with a `documents` key holding the fetched pages.
|
|
@@ -273,7 +309,10 @@ class ScrapeUnblockerFetcher:
|
|
|
273
309
|
"content_type": response.headers.get("Content-Type", ""),
|
|
274
310
|
"parsed_data": self.parsed_data,
|
|
275
311
|
"list_elements": self.list_elements,
|
|
312
|
+
"markdown": self.markdown,
|
|
276
313
|
}
|
|
314
|
+
if self.markdown:
|
|
315
|
+
meta["main_content_only"] = self.main_content_only
|
|
277
316
|
|
|
278
317
|
if json_mode:
|
|
279
318
|
try:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -345,3 +345,143 @@ class TestNoDataExtracted:
|
|
|
345
345
|
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, parsed_data=True)
|
|
346
346
|
|
|
347
347
|
assert fetcher.run(urls=["https://example.com/gone"])["documents"] == []
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TestMarkdown:
|
|
351
|
+
MARKDOWN = "# A Light in the Attic\n\n£51.77\n\n| UPC | a897fe39b1053632 |\n"
|
|
352
|
+
|
|
353
|
+
def _markdown(self, text: str = MARKDOWN, status_code: int = 200, origin=None) -> MagicMock:
|
|
354
|
+
response = _response(text=text, status_code=status_code)
|
|
355
|
+
response.headers = {"Content-Type": "text/markdown; charset=utf-8"}
|
|
356
|
+
if origin is not None:
|
|
357
|
+
response.headers["X-Origin-Status"] = origin
|
|
358
|
+
return response
|
|
359
|
+
|
|
360
|
+
def test_off_by_default(self):
|
|
361
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY)
|
|
362
|
+
assert fetcher.markdown is False
|
|
363
|
+
assert fetcher.main_content_only is True
|
|
364
|
+
|
|
365
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
366
|
+
def test_markdown_query(self, mock_post):
|
|
367
|
+
mock_post.return_value = self._markdown()
|
|
368
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True)
|
|
369
|
+
|
|
370
|
+
fetcher.run(urls=["https://example.com"])
|
|
371
|
+
|
|
372
|
+
_, kwargs = mock_post.call_args
|
|
373
|
+
assert kwargs["params"] == {
|
|
374
|
+
"url": "https://example.com",
|
|
375
|
+
"markdown": True,
|
|
376
|
+
"main_content_only": True,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
380
|
+
def test_whole_page(self, mock_post):
|
|
381
|
+
mock_post.return_value = self._markdown()
|
|
382
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True, main_content_only=False)
|
|
383
|
+
|
|
384
|
+
fetcher.run(urls=["https://example.com"])
|
|
385
|
+
|
|
386
|
+
_, kwargs = mock_post.call_args
|
|
387
|
+
assert kwargs["params"]["main_content_only"] is False
|
|
388
|
+
|
|
389
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
390
|
+
def test_main_content_only_not_sent_without_markdown(self, mock_post):
|
|
391
|
+
mock_post.return_value = _response()
|
|
392
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, main_content_only=False)
|
|
393
|
+
|
|
394
|
+
fetcher.run(urls=["https://example.com"])
|
|
395
|
+
|
|
396
|
+
_, kwargs = mock_post.call_args
|
|
397
|
+
assert kwargs["params"] == {"url": "https://example.com"}
|
|
398
|
+
|
|
399
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
400
|
+
def test_markdown_document(self, mock_post):
|
|
401
|
+
mock_post.return_value = self._markdown()
|
|
402
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True, proxy_country="gb")
|
|
403
|
+
|
|
404
|
+
doc = fetcher.run(urls=["https://example.com/book"])["documents"][0]
|
|
405
|
+
|
|
406
|
+
assert doc.content == self.MARKDOWN
|
|
407
|
+
assert doc.meta["url"] == "https://example.com/book"
|
|
408
|
+
assert doc.meta["markdown"] is True
|
|
409
|
+
assert doc.meta["main_content_only"] is True
|
|
410
|
+
assert doc.meta["content_type"].startswith("text/markdown")
|
|
411
|
+
_, kwargs = mock_post.call_args
|
|
412
|
+
assert kwargs["params"]["proxy_country"] == "gb"
|
|
413
|
+
|
|
414
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
415
|
+
def test_markdown_that_looks_like_json_stays_text(self, mock_post):
|
|
416
|
+
mock_post.return_value = self._markdown(text='{"a": 1}')
|
|
417
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True)
|
|
418
|
+
|
|
419
|
+
assert fetcher.run(urls=["https://example.com"])["documents"][0].content == '{"a": 1}'
|
|
420
|
+
|
|
421
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
422
|
+
def test_html_document_meta_says_not_markdown(self, mock_post):
|
|
423
|
+
mock_post.return_value = _response()
|
|
424
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY)
|
|
425
|
+
|
|
426
|
+
doc = fetcher.run(urls=["https://example.com"])["documents"][0]
|
|
427
|
+
|
|
428
|
+
assert doc.meta["markdown"] is False
|
|
429
|
+
assert "main_content_only" not in doc.meta
|
|
430
|
+
|
|
431
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
432
|
+
def test_markdown_with_steps(self, mock_post):
|
|
433
|
+
mock_post.return_value = self._markdown()
|
|
434
|
+
steps = [{"action": "click", "selector": "#more"}]
|
|
435
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True, steps=steps)
|
|
436
|
+
|
|
437
|
+
fetcher.run(urls=["https://example.com"])
|
|
438
|
+
|
|
439
|
+
_, kwargs = mock_post.call_args
|
|
440
|
+
assert json.loads(kwargs["params"]["steps"]) == steps
|
|
441
|
+
assert kwargs["params"]["markdown"] is True
|
|
442
|
+
|
|
443
|
+
@pytest.mark.parametrize("other", [{"parsed_data": True}, {"list_elements": True}])
|
|
444
|
+
def test_rejects_json_formats(self, other):
|
|
445
|
+
with pytest.raises(ValueError, match="cannot be combined"):
|
|
446
|
+
ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True, **other)
|
|
447
|
+
|
|
448
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
449
|
+
def test_missing_page_is_skipped(self, mock_post):
|
|
450
|
+
mock_post.side_effect = [
|
|
451
|
+
self._markdown(text="# 404 Not Found", status_code=404, origin="404"),
|
|
452
|
+
self._markdown(),
|
|
453
|
+
]
|
|
454
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True)
|
|
455
|
+
|
|
456
|
+
result = fetcher.run(urls=["https://example.com/gone", "https://example.com"])
|
|
457
|
+
|
|
458
|
+
assert [d.meta["url"] for d in result["documents"]] == ["https://example.com"]
|
|
459
|
+
|
|
460
|
+
@patch("scrapeunblocker_haystack.fetcher.requests.post")
|
|
461
|
+
def test_missing_page_raises_with_markdown_body(self, mock_post):
|
|
462
|
+
mock_post.return_value = self._markdown(text="# 404 Not Found", status_code=404, origin="404")
|
|
463
|
+
fetcher = ScrapeUnblockerFetcher(api_key=API_KEY, markdown=True, raise_on_failure=True)
|
|
464
|
+
|
|
465
|
+
with pytest.raises(TargetNotFoundError) as exc:
|
|
466
|
+
fetcher.run(urls=["https://example.com/gone"])
|
|
467
|
+
assert exc.value.origin_status == 404
|
|
468
|
+
assert exc.value.body == "# 404 Not Found"
|
|
469
|
+
|
|
470
|
+
def test_to_dict_and_back(self):
|
|
471
|
+
fetcher = ScrapeUnblockerFetcher(api_key=ENV_KEY, markdown=True, main_content_only=False)
|
|
472
|
+
data = fetcher.to_dict()
|
|
473
|
+
assert data["init_parameters"]["markdown"] is True
|
|
474
|
+
assert data["init_parameters"]["main_content_only"] is False
|
|
475
|
+
|
|
476
|
+
restored = ScrapeUnblockerFetcher.from_dict(data)
|
|
477
|
+
assert restored.markdown is True
|
|
478
|
+
assert restored.main_content_only is False
|
|
479
|
+
|
|
480
|
+
def test_from_dict_of_older_version(self):
|
|
481
|
+
data = ScrapeUnblockerFetcher(api_key=ENV_KEY).to_dict()
|
|
482
|
+
del data["init_parameters"]["markdown"]
|
|
483
|
+
del data["init_parameters"]["main_content_only"]
|
|
484
|
+
|
|
485
|
+
restored = ScrapeUnblockerFetcher.from_dict(data)
|
|
486
|
+
assert restored.markdown is False
|
|
487
|
+
assert restored.main_content_only is True
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
{scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
{scrapeunblocker_haystack-0.3.0 → scrapeunblocker_haystack-0.4.0}/.github/workflows/test.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|