webcrawlerapi 2.0.11__tar.gz → 2.1.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.
- webcrawlerapi-2.1.0/PKG-INFO +84 -0
- webcrawlerapi-2.1.0/README.md +63 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/setup.py +1 -1
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/tests/test_client.py +41 -78
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi/__init__.py +4 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi/client.py +217 -136
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi/models.py +90 -29
- webcrawlerapi-2.1.0/webcrawlerapi.egg-info/PKG-INFO +84 -0
- webcrawlerapi-2.0.11/PKG-INFO +0 -205
- webcrawlerapi-2.0.11/README.md +0 -184
- webcrawlerapi-2.0.11/webcrawlerapi.egg-info/PKG-INFO +0 -205
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/pyproject.toml +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/setup.cfg +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/tests/__init__.py +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/tests/test_models.py +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi.egg-info/SOURCES.txt +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi.egg-info/dependency_links.txt +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi.egg-info/requires.txt +0 -0
- {webcrawlerapi-2.0.11 → webcrawlerapi-2.1.0}/webcrawlerapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webcrawlerapi
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Python SDK for WebCrawler API
|
|
5
|
+
Home-page: https://github.com/webcrawlerapi/webcrawlerapi-python-sdk
|
|
6
|
+
Author: Andrew
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.25.0
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# WebCrawler API Python SDK
|
|
23
|
+
|
|
24
|
+
A Python SDK for interacting with the [WebCrawlerAPI](https://webcrawlerapi.com).
|
|
25
|
+
|
|
26
|
+
Get your API key at [dash.webcrawlerapi.com/access](https://dash.webcrawlerapi.com/access).
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install webcrawlerapi
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Crawling
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from webcrawlerapi import WebCrawlerAPI
|
|
38
|
+
|
|
39
|
+
client = WebCrawlerAPI(api_key="your_api_key")
|
|
40
|
+
|
|
41
|
+
job = client.crawl(
|
|
42
|
+
url="https://example.com",
|
|
43
|
+
output_formats=["markdown"],
|
|
44
|
+
items_limit=10,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
for item in job.job_items:
|
|
48
|
+
if item.status == "done":
|
|
49
|
+
print(item.original_url)
|
|
50
|
+
print(item.content)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Scraping
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from webcrawlerapi import WebCrawlerAPI
|
|
57
|
+
|
|
58
|
+
client = WebCrawlerAPI(api_key="your_api_key")
|
|
59
|
+
|
|
60
|
+
result = client.scrape(
|
|
61
|
+
url="https://example.com",
|
|
62
|
+
output_formats=["markdown"],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if result.success:
|
|
66
|
+
print(result.markdown)
|
|
67
|
+
else:
|
|
68
|
+
print(f"Error: {result.error_code} — {result.error_message}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
- [Crawling — parameters, async, job/item responses, cancellation](docs/crawling.md)
|
|
74
|
+
- [Scraping — parameters, async, response fields](docs/scraping.md)
|
|
75
|
+
- [Advanced — output formats, structured outputs, webhooks, S3 actions](docs/advanced.md)
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Python 3.6+
|
|
80
|
+
- requests >= 2.25.0
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT License
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# WebCrawler API Python SDK
|
|
2
|
+
|
|
3
|
+
A Python SDK for interacting with the [WebCrawlerAPI](https://webcrawlerapi.com).
|
|
4
|
+
|
|
5
|
+
Get your API key at [dash.webcrawlerapi.com/access](https://dash.webcrawlerapi.com/access).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install webcrawlerapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Crawling
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from webcrawlerapi import WebCrawlerAPI
|
|
17
|
+
|
|
18
|
+
client = WebCrawlerAPI(api_key="your_api_key")
|
|
19
|
+
|
|
20
|
+
job = client.crawl(
|
|
21
|
+
url="https://example.com",
|
|
22
|
+
output_formats=["markdown"],
|
|
23
|
+
items_limit=10,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for item in job.job_items:
|
|
27
|
+
if item.status == "done":
|
|
28
|
+
print(item.original_url)
|
|
29
|
+
print(item.content)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Scraping
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from webcrawlerapi import WebCrawlerAPI
|
|
36
|
+
|
|
37
|
+
client = WebCrawlerAPI(api_key="your_api_key")
|
|
38
|
+
|
|
39
|
+
result = client.scrape(
|
|
40
|
+
url="https://example.com",
|
|
41
|
+
output_formats=["markdown"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if result.success:
|
|
45
|
+
print(result.markdown)
|
|
46
|
+
else:
|
|
47
|
+
print(f"Error: {result.error_code} — {result.error_message}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
- [Crawling — parameters, async, job/item responses, cancellation](docs/crawling.md)
|
|
53
|
+
- [Scraping — parameters, async, response fields](docs/scraping.md)
|
|
54
|
+
- [Advanced — output formats, structured outputs, webhooks, S3 actions](docs/advanced.md)
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- Python 3.6+
|
|
59
|
+
- requests >= 2.25.0
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT License
|
|
@@ -14,6 +14,7 @@ from webcrawlerapi.models import (
|
|
|
14
14
|
ScrapeResponse,
|
|
15
15
|
ScrapeResponseError,
|
|
16
16
|
UploadS3Action,
|
|
17
|
+
WebCrawlerApiError,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -159,7 +160,7 @@ class TestWebCrawlerAPI:
|
|
|
159
160
|
status=400,
|
|
160
161
|
)
|
|
161
162
|
|
|
162
|
-
with pytest.raises(
|
|
163
|
+
with pytest.raises(WebCrawlerApiError):
|
|
163
164
|
client.crawl_async(url="invalid-url")
|
|
164
165
|
|
|
165
166
|
@responses.activate
|
|
@@ -278,7 +279,6 @@ class TestWebCrawlerAPI:
|
|
|
278
279
|
output_format="cleaned",
|
|
279
280
|
clean_selectors=".ads, .footer",
|
|
280
281
|
prompt="Extract main content",
|
|
281
|
-
respect_robots_txt=True,
|
|
282
282
|
)
|
|
283
283
|
|
|
284
284
|
assert isinstance(result, ScrapeId)
|
|
@@ -293,7 +293,6 @@ class TestWebCrawlerAPI:
|
|
|
293
293
|
assert payload["output_format"] == "cleaned"
|
|
294
294
|
assert payload["clean_selectors"] == ".ads, .footer"
|
|
295
295
|
assert payload["prompt"] == "Extract main content"
|
|
296
|
-
assert payload["respect_robots_txt"] is True
|
|
297
296
|
|
|
298
297
|
@responses.activate
|
|
299
298
|
def test_scrape_async_error_response(self, client):
|
|
@@ -305,11 +304,11 @@ class TestWebCrawlerAPI:
|
|
|
305
304
|
status=400,
|
|
306
305
|
)
|
|
307
306
|
|
|
308
|
-
with pytest.raises(
|
|
307
|
+
with pytest.raises(WebCrawlerApiError) as exc_info:
|
|
309
308
|
client.scrape_async(url="invalid-url")
|
|
310
309
|
|
|
311
|
-
assert
|
|
312
|
-
assert "Invalid URL format" in
|
|
310
|
+
assert exc_info.value.status_code == 400
|
|
311
|
+
assert "Invalid URL format" in exc_info.value.error_message
|
|
313
312
|
|
|
314
313
|
@responses.activate
|
|
315
314
|
def test_get_scrape_success_done(self, client):
|
|
@@ -385,99 +384,63 @@ class TestWebCrawlerAPI:
|
|
|
385
384
|
|
|
386
385
|
@responses.activate
|
|
387
386
|
def test_scrape_with_polling_success(self, client):
|
|
388
|
-
"""Test scrape method
|
|
389
|
-
# Mock scrape_async response
|
|
387
|
+
"""Test scrape method returns done result from sync endpoint."""
|
|
390
388
|
responses.add(
|
|
391
389
|
responses.POST,
|
|
392
|
-
"https://api.test.com/v2/scrape
|
|
393
|
-
json={
|
|
390
|
+
"https://api.test.com/v2/scrape",
|
|
391
|
+
json={
|
|
392
|
+
"status": "done",
|
|
393
|
+
"success": True,
|
|
394
|
+
"markdown": "# Scraped Content",
|
|
395
|
+
},
|
|
394
396
|
status=200,
|
|
395
397
|
)
|
|
396
398
|
|
|
397
|
-
|
|
398
|
-
scrape_data = {
|
|
399
|
-
"status": "done",
|
|
400
|
-
"success": True,
|
|
401
|
-
"markdown": "# Scraped Content",
|
|
402
|
-
}
|
|
399
|
+
result = client.scrape(url="https://example.com")
|
|
403
400
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
json=scrape_data,
|
|
408
|
-
status=200,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
with patch("time.sleep") as mock_sleep:
|
|
412
|
-
result = client.scrape(url="https://example.com")
|
|
413
|
-
|
|
414
|
-
assert isinstance(result, ScrapeResponse)
|
|
415
|
-
assert result.status == "done"
|
|
416
|
-
assert result.markdown == "# Scraped Content"
|
|
417
|
-
# Should not sleep since scrape is already done
|
|
418
|
-
mock_sleep.assert_not_called()
|
|
401
|
+
assert isinstance(result, ScrapeResponse)
|
|
402
|
+
assert result.status == "done"
|
|
403
|
+
assert result.markdown == "# Scraped Content"
|
|
419
404
|
|
|
420
405
|
@responses.activate
|
|
421
406
|
def test_scrape_with_polling_error(self, client):
|
|
422
|
-
"""Test scrape method that
|
|
423
|
-
# Mock scrape_async response
|
|
407
|
+
"""Test scrape method that gets error from sync endpoint."""
|
|
424
408
|
responses.add(
|
|
425
409
|
responses.POST,
|
|
426
|
-
"https://api.test.com/v2/scrape
|
|
427
|
-
json={
|
|
410
|
+
"https://api.test.com/v2/scrape",
|
|
411
|
+
json={
|
|
412
|
+
"status": "error",
|
|
413
|
+
"success": False,
|
|
414
|
+
"error_code": "FETCH_ERROR",
|
|
415
|
+
"error_message": "Failed to fetch page",
|
|
416
|
+
},
|
|
428
417
|
status=200,
|
|
429
418
|
)
|
|
430
419
|
|
|
431
|
-
|
|
432
|
-
error_data = {
|
|
433
|
-
"status": "error",
|
|
434
|
-
"success": False,
|
|
435
|
-
"error_code": "FETCH_ERROR",
|
|
436
|
-
"error_message": "Failed to fetch page",
|
|
437
|
-
}
|
|
420
|
+
result = client.scrape(url="https://example.com")
|
|
438
421
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"https://api.test.com/v2/scrape/scrape-123",
|
|
442
|
-
json=error_data,
|
|
443
|
-
status=200,
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
with patch("time.sleep") as mock_sleep:
|
|
447
|
-
result = client.scrape(url="https://example.com")
|
|
448
|
-
|
|
449
|
-
assert isinstance(result, ScrapeResponseError)
|
|
450
|
-
assert result.error_code == "FETCH_ERROR"
|
|
451
|
-
# Should not sleep since error is immediate
|
|
452
|
-
mock_sleep.assert_not_called()
|
|
422
|
+
assert isinstance(result, ScrapeResponseError)
|
|
423
|
+
assert result.error_code == "FETCH_ERROR"
|
|
453
424
|
|
|
454
425
|
@responses.activate
|
|
455
426
|
def test_scrape_with_polling_max_polls(self, client):
|
|
456
|
-
"""Test scrape method
|
|
457
|
-
# Mock scrape_async response
|
|
427
|
+
"""Test scrape method returns full response data from sync endpoint."""
|
|
458
428
|
responses.add(
|
|
459
429
|
responses.POST,
|
|
460
|
-
"https://api.test.com/v2/scrape
|
|
461
|
-
json={
|
|
430
|
+
"https://api.test.com/v2/scrape",
|
|
431
|
+
json={
|
|
432
|
+
"status": "done",
|
|
433
|
+
"success": True,
|
|
434
|
+
"markdown": "# Content",
|
|
435
|
+
"page_status_code": 200,
|
|
436
|
+
"page_title": "Test Page",
|
|
437
|
+
},
|
|
462
438
|
status=200,
|
|
463
439
|
)
|
|
464
440
|
|
|
465
|
-
|
|
466
|
-
progress_data = {"status": "in_progress", "success": False}
|
|
467
|
-
|
|
468
|
-
for _ in range(3):
|
|
469
|
-
responses.add(
|
|
470
|
-
responses.GET,
|
|
471
|
-
"https://api.test.com/v2/scrape/scrape-123",
|
|
472
|
-
json=progress_data,
|
|
473
|
-
status=200,
|
|
474
|
-
)
|
|
441
|
+
result = client.scrape(url="https://example.com")
|
|
475
442
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
assert result.status == "in_progress"
|
|
481
|
-
# Should sleep 3 times (once after each poll)
|
|
482
|
-
assert mock_sleep.call_count == 3
|
|
483
|
-
mock_sleep.assert_called_with(5) # DEFAULT_POLL_DELAY_SECONDS
|
|
443
|
+
assert isinstance(result, ScrapeResponse)
|
|
444
|
+
assert result.status == "done"
|
|
445
|
+
assert result.page_status_code == 200
|
|
446
|
+
assert result.page_title == "Test Page"
|
|
@@ -28,17 +28,21 @@ from .models import (
|
|
|
28
28
|
CrawlResponse,
|
|
29
29
|
Job,
|
|
30
30
|
JobItem,
|
|
31
|
+
JobMarkdownResponse,
|
|
31
32
|
ScrapeId,
|
|
32
33
|
ScrapeResponse,
|
|
33
34
|
ScrapeResponseError,
|
|
34
35
|
UploadS3Action,
|
|
36
|
+
WebCrawlerApiError,
|
|
35
37
|
)
|
|
36
38
|
|
|
37
39
|
__version__ = "1.0.0"
|
|
38
40
|
__all__ = [
|
|
39
41
|
"WebCrawlerAPI",
|
|
42
|
+
"WebCrawlerApiError",
|
|
40
43
|
"Job",
|
|
41
44
|
"JobItem",
|
|
45
|
+
"JobMarkdownResponse",
|
|
42
46
|
"CrawlResponse",
|
|
43
47
|
"ScrapeId",
|
|
44
48
|
"ScrapeResponse",
|