scrapeunblocker 0.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.
- scrapeunblocker-0.1.0/.github/workflows/ci.yml +21 -0
- scrapeunblocker-0.1.0/.github/workflows/publish.yml +41 -0
- scrapeunblocker-0.1.0/.gitignore +22 -0
- scrapeunblocker-0.1.0/CHANGELOG.md +10 -0
- scrapeunblocker-0.1.0/LICENSE +21 -0
- scrapeunblocker-0.1.0/PKG-INFO +208 -0
- scrapeunblocker-0.1.0/README.md +173 -0
- scrapeunblocker-0.1.0/pyproject.toml +55 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/__init__.py +47 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/_async_client.py +201 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/_base.py +109 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/_client.py +248 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/exceptions.py +70 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/models.py +52 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/py.typed +0 -0
- scrapeunblocker-0.1.0/src/scrapeunblocker/version.py +1 -0
- scrapeunblocker-0.1.0/tests/test_client.py +169 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.8", "3.11", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: python -m pip install --upgrade pip
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: pytest -q
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes on every GitHub Release. Uses PyPI Trusted Publishing (OIDC) -
|
|
4
|
+
# no API tokens stored anywhere. Configure the trusted publisher once at
|
|
5
|
+
# https://pypi.org/manage/account/publishing/ pointing at this repo +
|
|
6
|
+
# workflow (publish.yml) and environment "pypi".
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
release:
|
|
10
|
+
types: [published]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
- run: python -m pip install --upgrade build
|
|
24
|
+
- run: python -m build
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write # required for trusted publishing
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Tooling
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# OS / editor
|
|
20
|
+
.DS_Store
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Sync `Client` and async `AsyncClient`.
|
|
8
|
+
- `get_page_source`, `get_parsed`, `get_page_with_cookies`, `serp`, `get_image`.
|
|
9
|
+
- Skyscanner plugins: flights, hotels, car hire (quotes + locations).
|
|
10
|
+
- Typed exception hierarchy and automatic retry on transient failures.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScrapeUnblocker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scrapeunblocker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the ScrapeUnblocker web scraping API - JavaScript-rendered pages that bypass Cloudflare, DataDome, PerimeterX, Akamai and more.
|
|
5
|
+
Project-URL: Homepage, https://scrapeunblocker.com
|
|
6
|
+
Project-URL: Documentation, https://developers.scrapeunblocker.com
|
|
7
|
+
Project-URL: Source, https://github.com/ScrapeUnblocker/scrapeunblocker-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/ScrapeUnblocker/scrapeunblocker-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: ScrapeUnblocker <support@scrapeunblocker.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: anti-bot,captcha,cloudflare,crawler,datadome,proxy,scraping api,web scraping
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Requires-Dist: httpx>=0.23
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# ScrapeUnblocker Python client
|
|
37
|
+
|
|
38
|
+
Official Python client for the [ScrapeUnblocker](https://scrapeunblocker.com) web scraping API.
|
|
39
|
+
|
|
40
|
+
Every request is fully JavaScript-rendered in a real browser and routed through premium proxies, so it bypasses Cloudflare, DataDome, PerimeterX, Akamai, Kasada and similar anti-bot systems - from one simple call. You are only billed for successful requests.
|
|
41
|
+
|
|
42
|
+
- **Highest success rate on the market** (95%+ on live production traffic)
|
|
43
|
+
- **Rendered HTML or parsed JSON** - no per-site parsers to maintain
|
|
44
|
+
- Sync **and** async clients, fully type-hinted
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install scrapeunblocker
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.8+.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from scrapeunblocker import Client
|
|
58
|
+
|
|
59
|
+
su = Client(api_key="su_live_...") # or set the SCRAPEUNBLOCKER_KEY env var
|
|
60
|
+
|
|
61
|
+
# Rendered HTML for any URL
|
|
62
|
+
html = su.get_page_source("https://example.com")
|
|
63
|
+
|
|
64
|
+
# Structured JSON instead of HTML (products, listings, search results, ...)
|
|
65
|
+
product = su.get_parsed("https://www.amazon.com/dp/B08N5WRWNW")
|
|
66
|
+
print(product.page_type) # "product"
|
|
67
|
+
print(product.data) # {...}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Get your API key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com). The free trial does not require a credit card.
|
|
71
|
+
|
|
72
|
+
## Authentication
|
|
73
|
+
|
|
74
|
+
Pass the key directly, or set an environment variable and omit it:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export SCRAPEUNBLOCKER_KEY="su_live_..."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from scrapeunblocker import Client
|
|
82
|
+
su = Client() # reads SCRAPEUNBLOCKER_KEY
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Fetch rendered HTML
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
html = su.get_page_source(
|
|
89
|
+
"https://www.nordstrom.com/browse/women/clothing/dresses",
|
|
90
|
+
proxy_country="US", # route through a specific country
|
|
91
|
+
time_sleep=3, # wait extra seconds after load
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Get parsed JSON
|
|
96
|
+
|
|
97
|
+
Pass a URL and get back structured data extracted via Schema.org, `__NEXT_DATA__` or AI-generated rules:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
result = su.get_parsed("https://www.walmart.com/ip/12345")
|
|
101
|
+
print(result.page_type) # e.g. "product"
|
|
102
|
+
print(result.source) # how it was extracted
|
|
103
|
+
print(result.data) # the fields
|
|
104
|
+
|
|
105
|
+
# If a parse ever comes back wrong, force a fresh set of rules:
|
|
106
|
+
result = su.get_parsed(url, refresh_rules=True, rules_hint="price is missing")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Google search (SERP)
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
serp = su.serp("web scraping api", pages_to_check=2, proxy_country="US")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Cookies and the serving proxy
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
page = su.get_page_with_cookies("https://example.com")
|
|
119
|
+
print(page.html, page.cookies, page.proxy)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Images
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
data = su.get_image("https://example.com/photo.jpg")
|
|
126
|
+
open("photo.jpg", "wb").write(data)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Skyscanner plugins
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Resolve a place name to entity IDs, then search
|
|
133
|
+
locs = su.skyscanner.flight_locations("London")
|
|
134
|
+
flights = su.skyscanner.flights(
|
|
135
|
+
origin="London", dest="New York",
|
|
136
|
+
depart_date="2026-09-01", adults=1, currency="USD",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
hotels = su.skyscanner.hotels(destination="Madrid", checkin="2026-09-01", checkout="2026-09-03")
|
|
140
|
+
cars = su.skyscanner.carhire(pickup="Madrid", pickup_datetime="2026-09-01T10:00", dropoff_datetime="2026-09-03T10:00")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Async
|
|
144
|
+
|
|
145
|
+
Every method has an async twin on `AsyncClient`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import asyncio
|
|
149
|
+
from scrapeunblocker import AsyncClient
|
|
150
|
+
|
|
151
|
+
async def main():
|
|
152
|
+
async with AsyncClient(api_key="su_live_...") as su:
|
|
153
|
+
html = await su.get_page_source("https://example.com")
|
|
154
|
+
|
|
155
|
+
asyncio.run(main())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Error handling
|
|
159
|
+
|
|
160
|
+
Non-2xx responses raise typed exceptions, all subclasses of `ScrapeUnblockerError`:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from scrapeunblocker import Client, BlockedError, RateLimitError, UpstreamOutageError
|
|
164
|
+
|
|
165
|
+
su = Client()
|
|
166
|
+
try:
|
|
167
|
+
html = su.get_page_source("https://example.com")
|
|
168
|
+
except BlockedError:
|
|
169
|
+
... # 403: the target blocked every bypass path (not billed)
|
|
170
|
+
except RateLimitError:
|
|
171
|
+
... # 429: slow down
|
|
172
|
+
except UpstreamOutageError:
|
|
173
|
+
... # 503: the target site itself is down - retry later
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Exception | Status | Meaning |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `InvalidRequestError` | 400 | Bad URL or unsupported scheme |
|
|
179
|
+
| `AuthenticationError` | 401 | Missing or invalid API key |
|
|
180
|
+
| `BlockedError` | 403 | Blocked by bot protection on every path |
|
|
181
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
182
|
+
| `UpstreamOutageError` | 503 | The target origin is down |
|
|
183
|
+
| `ServerError` | 5xx | Unexpected server error |
|
|
184
|
+
| `ScrapeTimeoutError` | - | Request exceeded the timeout |
|
|
185
|
+
| `ConnectionError` | - | Could not reach the API |
|
|
186
|
+
|
|
187
|
+
Transient failures (429, 502, 503, 504 and network errors) are retried automatically with exponential backoff; tune with `Client(max_retries=...)`.
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
Client(
|
|
193
|
+
api_key=None, # or SCRAPEUNBLOCKER_KEY env var
|
|
194
|
+
base_url="https://api.scrapeunblocker.com",
|
|
195
|
+
timeout=180.0, # seconds; protected pages can be slow
|
|
196
|
+
max_retries=2,
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Links
|
|
201
|
+
|
|
202
|
+
- Documentation: https://developers.scrapeunblocker.com
|
|
203
|
+
- Website: https://scrapeunblocker.com
|
|
204
|
+
- Dashboard: https://app.scrapeunblocker.com
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# ScrapeUnblocker Python client
|
|
2
|
+
|
|
3
|
+
Official Python client for the [ScrapeUnblocker](https://scrapeunblocker.com) web scraping API.
|
|
4
|
+
|
|
5
|
+
Every request is fully JavaScript-rendered in a real browser and routed through premium proxies, so it bypasses Cloudflare, DataDome, PerimeterX, Akamai, Kasada and similar anti-bot systems - from one simple call. You are only billed for successful requests.
|
|
6
|
+
|
|
7
|
+
- **Highest success rate on the market** (95%+ on live production traffic)
|
|
8
|
+
- **Rendered HTML or parsed JSON** - no per-site parsers to maintain
|
|
9
|
+
- Sync **and** async clients, fully type-hinted
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install scrapeunblocker
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.8+.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from scrapeunblocker import Client
|
|
23
|
+
|
|
24
|
+
su = Client(api_key="su_live_...") # or set the SCRAPEUNBLOCKER_KEY env var
|
|
25
|
+
|
|
26
|
+
# Rendered HTML for any URL
|
|
27
|
+
html = su.get_page_source("https://example.com")
|
|
28
|
+
|
|
29
|
+
# Structured JSON instead of HTML (products, listings, search results, ...)
|
|
30
|
+
product = su.get_parsed("https://www.amazon.com/dp/B08N5WRWNW")
|
|
31
|
+
print(product.page_type) # "product"
|
|
32
|
+
print(product.data) # {...}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Get your API key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com). The free trial does not require a credit card.
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Pass the key directly, or set an environment variable and omit it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export SCRAPEUNBLOCKER_KEY="su_live_..."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from scrapeunblocker import Client
|
|
47
|
+
su = Client() # reads SCRAPEUNBLOCKER_KEY
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Fetch rendered HTML
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
html = su.get_page_source(
|
|
54
|
+
"https://www.nordstrom.com/browse/women/clothing/dresses",
|
|
55
|
+
proxy_country="US", # route through a specific country
|
|
56
|
+
time_sleep=3, # wait extra seconds after load
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Get parsed JSON
|
|
61
|
+
|
|
62
|
+
Pass a URL and get back structured data extracted via Schema.org, `__NEXT_DATA__` or AI-generated rules:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
result = su.get_parsed("https://www.walmart.com/ip/12345")
|
|
66
|
+
print(result.page_type) # e.g. "product"
|
|
67
|
+
print(result.source) # how it was extracted
|
|
68
|
+
print(result.data) # the fields
|
|
69
|
+
|
|
70
|
+
# If a parse ever comes back wrong, force a fresh set of rules:
|
|
71
|
+
result = su.get_parsed(url, refresh_rules=True, rules_hint="price is missing")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Google search (SERP)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
serp = su.serp("web scraping api", pages_to_check=2, proxy_country="US")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Cookies and the serving proxy
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
page = su.get_page_with_cookies("https://example.com")
|
|
84
|
+
print(page.html, page.cookies, page.proxy)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Images
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
data = su.get_image("https://example.com/photo.jpg")
|
|
91
|
+
open("photo.jpg", "wb").write(data)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Skyscanner plugins
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Resolve a place name to entity IDs, then search
|
|
98
|
+
locs = su.skyscanner.flight_locations("London")
|
|
99
|
+
flights = su.skyscanner.flights(
|
|
100
|
+
origin="London", dest="New York",
|
|
101
|
+
depart_date="2026-09-01", adults=1, currency="USD",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
hotels = su.skyscanner.hotels(destination="Madrid", checkin="2026-09-01", checkout="2026-09-03")
|
|
105
|
+
cars = su.skyscanner.carhire(pickup="Madrid", pickup_datetime="2026-09-01T10:00", dropoff_datetime="2026-09-03T10:00")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Async
|
|
109
|
+
|
|
110
|
+
Every method has an async twin on `AsyncClient`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import asyncio
|
|
114
|
+
from scrapeunblocker import AsyncClient
|
|
115
|
+
|
|
116
|
+
async def main():
|
|
117
|
+
async with AsyncClient(api_key="su_live_...") as su:
|
|
118
|
+
html = await su.get_page_source("https://example.com")
|
|
119
|
+
|
|
120
|
+
asyncio.run(main())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Error handling
|
|
124
|
+
|
|
125
|
+
Non-2xx responses raise typed exceptions, all subclasses of `ScrapeUnblockerError`:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from scrapeunblocker import Client, BlockedError, RateLimitError, UpstreamOutageError
|
|
129
|
+
|
|
130
|
+
su = Client()
|
|
131
|
+
try:
|
|
132
|
+
html = su.get_page_source("https://example.com")
|
|
133
|
+
except BlockedError:
|
|
134
|
+
... # 403: the target blocked every bypass path (not billed)
|
|
135
|
+
except RateLimitError:
|
|
136
|
+
... # 429: slow down
|
|
137
|
+
except UpstreamOutageError:
|
|
138
|
+
... # 503: the target site itself is down - retry later
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Exception | Status | Meaning |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `InvalidRequestError` | 400 | Bad URL or unsupported scheme |
|
|
144
|
+
| `AuthenticationError` | 401 | Missing or invalid API key |
|
|
145
|
+
| `BlockedError` | 403 | Blocked by bot protection on every path |
|
|
146
|
+
| `RateLimitError` | 429 | Too many requests |
|
|
147
|
+
| `UpstreamOutageError` | 503 | The target origin is down |
|
|
148
|
+
| `ServerError` | 5xx | Unexpected server error |
|
|
149
|
+
| `ScrapeTimeoutError` | - | Request exceeded the timeout |
|
|
150
|
+
| `ConnectionError` | - | Could not reach the API |
|
|
151
|
+
|
|
152
|
+
Transient failures (429, 502, 503, 504 and network errors) are retried automatically with exponential backoff; tune with `Client(max_retries=...)`.
|
|
153
|
+
|
|
154
|
+
## Configuration
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
Client(
|
|
158
|
+
api_key=None, # or SCRAPEUNBLOCKER_KEY env var
|
|
159
|
+
base_url="https://api.scrapeunblocker.com",
|
|
160
|
+
timeout=180.0, # seconds; protected pages can be slow
|
|
161
|
+
max_retries=2,
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Links
|
|
166
|
+
|
|
167
|
+
- Documentation: https://developers.scrapeunblocker.com
|
|
168
|
+
- Website: https://scrapeunblocker.com
|
|
169
|
+
- Dashboard: https://app.scrapeunblocker.com
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scrapeunblocker"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the ScrapeUnblocker web scraping API - JavaScript-rendered pages that bypass Cloudflare, DataDome, PerimeterX, Akamai and more."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "ScrapeUnblocker", email = "support@scrapeunblocker.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"web scraping",
|
|
15
|
+
"scraping api",
|
|
16
|
+
"anti-bot",
|
|
17
|
+
"proxy",
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"datadome",
|
|
20
|
+
"captcha",
|
|
21
|
+
"crawler",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = ["httpx>=0.23"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://scrapeunblocker.com"
|
|
43
|
+
Documentation = "https://developers.scrapeunblocker.com"
|
|
44
|
+
Source = "https://github.com/ScrapeUnblocker/scrapeunblocker-python"
|
|
45
|
+
Changelog = "https://github.com/ScrapeUnblocker/scrapeunblocker-python/blob/main/CHANGELOG.md"
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.21", "respx>=0.20", "mypy>=1.0"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/scrapeunblocker"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Official Python client for the ScrapeUnblocker web scraping API.
|
|
2
|
+
|
|
3
|
+
Basic usage::
|
|
4
|
+
|
|
5
|
+
from scrapeunblocker import Client
|
|
6
|
+
|
|
7
|
+
su = Client(api_key="su_live_...") # or set SCRAPEUNBLOCKER_KEY
|
|
8
|
+
html = su.get_page_source("https://example.com")
|
|
9
|
+
product = su.get_parsed("https://www.amazon.com/dp/B08N5WRWNW")
|
|
10
|
+
|
|
11
|
+
See https://developers.scrapeunblocker.com for the full API reference.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._async_client import AsyncClient
|
|
15
|
+
from ._client import Client
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
APIError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
BlockedError,
|
|
20
|
+
ConnectionError,
|
|
21
|
+
InvalidRequestError,
|
|
22
|
+
RateLimitError,
|
|
23
|
+
ScrapeTimeoutError,
|
|
24
|
+
ScrapeUnblockerError,
|
|
25
|
+
ServerError,
|
|
26
|
+
UpstreamOutageError,
|
|
27
|
+
)
|
|
28
|
+
from .models import PageResult, ParsedPage
|
|
29
|
+
from .version import __version__
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Client",
|
|
33
|
+
"AsyncClient",
|
|
34
|
+
"ParsedPage",
|
|
35
|
+
"PageResult",
|
|
36
|
+
"ScrapeUnblockerError",
|
|
37
|
+
"APIError",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"InvalidRequestError",
|
|
40
|
+
"BlockedError",
|
|
41
|
+
"RateLimitError",
|
|
42
|
+
"UpstreamOutageError",
|
|
43
|
+
"ServerError",
|
|
44
|
+
"ScrapeTimeoutError",
|
|
45
|
+
"ConnectionError",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|