scrapenest-sdk 1.0.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.
- scrapenest_sdk-1.0.0/.gitignore +12 -0
- scrapenest_sdk-1.0.0/CHANGELOG.md +38 -0
- scrapenest_sdk-1.0.0/LICENSE +21 -0
- scrapenest_sdk-1.0.0/PKG-INFO +309 -0
- scrapenest_sdk-1.0.0/README.md +275 -0
- scrapenest_sdk-1.0.0/examples/01_quickstart_product.py +68 -0
- scrapenest_sdk-1.0.0/examples/02_extract_listing.py +66 -0
- scrapenest_sdk-1.0.0/examples/03_json_api_jsonpath.py +48 -0
- scrapenest_sdk-1.0.0/examples/04_dataset_to_csv.py +57 -0
- scrapenest_sdk-1.0.0/examples/05_browser_infinite_scroll.py +56 -0
- scrapenest_sdk-1.0.0/examples/06_stealth_screenshot.py +54 -0
- scrapenest_sdk-1.0.0/examples/07_async_crawl.py +53 -0
- scrapenest_sdk-1.0.0/examples/08_monitor.py +48 -0
- scrapenest_sdk-1.0.0/examples/09_change_detection.py +55 -0
- scrapenest_sdk-1.0.0/examples/09_webhook_endpoints.py +60 -0
- scrapenest_sdk-1.0.0/examples/10_api_key_hygiene.py +48 -0
- scrapenest_sdk-1.0.0/examples/11_retention_and_holds.py +52 -0
- scrapenest_sdk-1.0.0/examples/README.md +57 -0
- scrapenest_sdk-1.0.0/examples/_common.py +67 -0
- scrapenest_sdk-1.0.0/examples/webhook_receiver.py +29 -0
- scrapenest_sdk-1.0.0/pyproject.toml +83 -0
- scrapenest_sdk-1.0.0/src/scrapenest/__init__.py +132 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_async_client.py +168 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_client.py +167 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_ops.py +512 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_parsing.py +542 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_retry.py +45 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_transport.py +103 -0
- scrapenest_sdk-1.0.0/src/scrapenest/_version.py +1 -0
- scrapenest_sdk-1.0.0/src/scrapenest/exceptions.py +156 -0
- scrapenest_sdk-1.0.0/src/scrapenest/models.py +524 -0
- scrapenest_sdk-1.0.0/src/scrapenest/py.typed +1 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/__init__.py +1 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/api_keys.py +113 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/artifacts.py +78 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/audit_logs.py +103 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/jobs.py +207 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/monitors.py +256 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/org.py +42 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/retention.py +96 -0
- scrapenest_sdk-1.0.0/src/scrapenest/resources/webhook_endpoints.py +190 -0
- scrapenest_sdk-1.0.0/src/scrapenest/types.py +229 -0
- scrapenest_sdk-1.0.0/src/scrapenest/webhooks.py +105 -0
- scrapenest_sdk-1.0.0/tests/conftest.py +29 -0
- scrapenest_sdk-1.0.0/tests/test_api_keys.py +134 -0
- scrapenest_sdk-1.0.0/tests/test_async.py +119 -0
- scrapenest_sdk-1.0.0/tests/test_audit_logs.py +80 -0
- scrapenest_sdk-1.0.0/tests/test_client.py +269 -0
- scrapenest_sdk-1.0.0/tests/test_exceptions.py +69 -0
- scrapenest_sdk-1.0.0/tests/test_monitors.py +248 -0
- scrapenest_sdk-1.0.0/tests/test_pagination_manifest.py +133 -0
- scrapenest_sdk-1.0.0/tests/test_retention_org.py +144 -0
- scrapenest_sdk-1.0.0/tests/test_retry.py +140 -0
- scrapenest_sdk-1.0.0/tests/test_webhook_endpoints.py +254 -0
- scrapenest_sdk-1.0.0/tests/test_webhooks.py +74 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-07-15
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Sync (`ScrapeNestClient`) and async (`AsyncScrapeNestClient`) clients with identical surfaces.
|
|
13
|
+
- `client.scrape(...)` (submit and wait) and `client.submit(...)` (fire and collect later).
|
|
14
|
+
- Typed job-create overloads per tier (`light`/`standard`/`stealth`) with autocomplete for
|
|
15
|
+
extraction hooks, browser actions, proxy, retry policy, viewport, and artifact options.
|
|
16
|
+
- `client.jobs`: create, get, list, `iter()` pagination, `get_manifest()` (outcome, credits, block, timing).
|
|
17
|
+
- `client.artifacts`: presigned download URLs plus `download_bytes` / `download_text` helpers.
|
|
18
|
+
- `client.monitors`: recurring cron scrapes - create, update, pause/resume, delete, run history
|
|
19
|
+
(`runs` / `iter_runs`).
|
|
20
|
+
- Change detection: `detection=` on `monitors.create/update`, `monitors.changes(...)` /
|
|
21
|
+
`iter_changes(...)`, and `Monitor.detection` with `MonitorChange` /
|
|
22
|
+
`MonitorChangeListResponse` models.
|
|
23
|
+
- `client.webhook_endpoints`: endpoint CRUD, secret rotation, pause/resume, delivery messages
|
|
24
|
+
and per-attempt forensics with cursor pagination.
|
|
25
|
+
- `client.api_keys`: list, create (scopes, expiry, per-key limits, IP allowlist), rotate, revoke.
|
|
26
|
+
- `client.retention`: retention policy read/update, legal holds (create, list, release).
|
|
27
|
+
- `client.org`: organization IP allowlist read/replace.
|
|
28
|
+
- `client.audit_logs`: read the organization audit trail with `severity` / `result` /
|
|
29
|
+
`event_type` filters and transparent pagination.
|
|
30
|
+
- Automatic retries with jittered exponential backoff on `429`/`5xx` and transport errors,
|
|
31
|
+
honoring `Retry-After`; configurable via `max_retries` / `backoff_factor`.
|
|
32
|
+
- Automatic `idempotency_token` on job creation so retries never double-submit; `CreateJobResponse.idempotent_replay`.
|
|
33
|
+
- Typed exception taxonomy: `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`,
|
|
34
|
+
`NotFoundError`, `ConflictError`, `ValidationError`, `RateLimitError`, `ServerError`,
|
|
35
|
+
`ScrapeNestConnectionError`.
|
|
36
|
+
- `verify_webhook()` / `verify_signature()` for Svix-scheme webhook signature verification.
|
|
37
|
+
|
|
38
|
+
[1.0.0]: https://github.com/scrapenest/scrapenest-py-sdk/releases/tag/v1.0.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) ScrapeNest
|
|
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,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scrapenest-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the ScrapeNest scraping API
|
|
5
|
+
Project-URL: Documentation, https://docs.scrapenest.com
|
|
6
|
+
Project-URL: Homepage, https://scrapenest.com
|
|
7
|
+
Project-URL: Source, https://github.com/scrapenest/scrapenest-py-sdk
|
|
8
|
+
Project-URL: Changelog, https://github.com/scrapenest/scrapenest-py-sdk/blob/main/CHANGELOG.md
|
|
9
|
+
Author: ScrapeNest
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,crawler,scrapenest,scraping,sdk,web-scraping
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: typing-extensions>=4.6; python_version < '3.11'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# ScrapeNest Python SDK
|
|
36
|
+
|
|
37
|
+
The official Python client for the [ScrapeNest](https://scrapenest.com) scraping API. Submit
|
|
38
|
+
scraping jobs, wait for or collect their results, download artifacts, manage monitors, webhook
|
|
39
|
+
endpoints, and API keys - everything the API offers, fully typed.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install scrapenest-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import scrapenest
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10+. The only runtime dependency is [httpx](https://www.python-httpx.org/).
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from scrapenest import ScrapeNestClient
|
|
55
|
+
|
|
56
|
+
client = ScrapeNestClient(api_key="sn_live_...")
|
|
57
|
+
|
|
58
|
+
# Submit and wait (polls until terminal, up to `timeout` seconds)
|
|
59
|
+
result = client.scrape(job_type="light", target_url="https://example.com")
|
|
60
|
+
print(result.status, result.artifact_count)
|
|
61
|
+
|
|
62
|
+
# Fetch the rendered HTML artifact
|
|
63
|
+
job = client.jobs.get(result.job_id)
|
|
64
|
+
html_artifact = next(a for a in job.artifacts if a.artifact_type == "html")
|
|
65
|
+
html = client.artifacts.download_text(html_artifact.artifact_id)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Authentication uses your API key, sent automatically in the `X-API-Key` header. Create and manage
|
|
69
|
+
keys in the dashboard (or with `client.api_keys`, below).
|
|
70
|
+
|
|
71
|
+
## Job tiers
|
|
72
|
+
|
|
73
|
+
`job_type` selects the worker tier; the SDK gives you autocomplete for each tier's options:
|
|
74
|
+
|
|
75
|
+
| Tier | What it does | Tier-specific options |
|
|
76
|
+
|------|--------------|-----------------------|
|
|
77
|
+
| `light` | Fast HTTP-only fetch | `method`, `body`, `follow_redirects`, `retry_policy` |
|
|
78
|
+
| `standard` | Headless Chromium | `actions`, `viewport`, `wait_until`, `locale`, ... |
|
|
79
|
+
| `stealth` | Anti-blocking browser | everything in standard plus `os_name`, `proxy`, `browser_extensions` |
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Stealth job with structured extraction
|
|
83
|
+
job = client.submit(
|
|
84
|
+
job_type="stealth",
|
|
85
|
+
target_url="https://example.com/pricing",
|
|
86
|
+
artifact_options={"include_screenshot": True, "include_extraction": True},
|
|
87
|
+
extraction={
|
|
88
|
+
"hooks": [
|
|
89
|
+
{"hook_id": "price", "type": "css", "selector": ".price", "all_matches": True},
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
print(job.job_id, "idempotent replay:", job.idempotent_replay)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Waiting vs collecting later
|
|
97
|
+
|
|
98
|
+
The ScrapeNest API is asynchronous: you submit a job and collect the result once it runs.
|
|
99
|
+
|
|
100
|
+
- `client.scrape(...)` submits and blocks (polling) until the job reaches a terminal state.
|
|
101
|
+
- `client.submit(...)` returns an acknowledgement immediately; collect the result later via
|
|
102
|
+
`client.jobs.get(...)` or a webhook.
|
|
103
|
+
|
|
104
|
+
Every job creation automatically attaches an `idempotency_token`, so transparent retries never
|
|
105
|
+
create duplicate jobs. Pass your own `idempotency_token` to make a call safe to repeat across
|
|
106
|
+
processes.
|
|
107
|
+
|
|
108
|
+
## Async client
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
from scrapenest import AsyncScrapeNestClient
|
|
113
|
+
|
|
114
|
+
async def main():
|
|
115
|
+
async with AsyncScrapeNestClient(api_key="sn_live_...") as client:
|
|
116
|
+
result = await client.scrape(job_type="light", target_url="https://example.com")
|
|
117
|
+
print(result.status)
|
|
118
|
+
|
|
119
|
+
async for summary in client.jobs.iter(status="succeeded"):
|
|
120
|
+
print(summary.job_id)
|
|
121
|
+
|
|
122
|
+
asyncio.run(main())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The async client mirrors the sync API one-to-one across every resource.
|
|
126
|
+
|
|
127
|
+
## Listing and paginating jobs
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
page = client.jobs.list(status="succeeded", limit=50)
|
|
131
|
+
print(page.total)
|
|
132
|
+
|
|
133
|
+
# Or iterate every match, transparently walking pages:
|
|
134
|
+
for summary in client.jobs.iter(job_type="stealth"):
|
|
135
|
+
print(summary.job_id)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Job outcome and credits (manifest)
|
|
139
|
+
|
|
140
|
+
A job's `status` tells you whether it ran (`succeeded`/`failed`). To see whether the target
|
|
141
|
+
*blocked* you and what it cost, fetch the forensics manifest:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
manifest = client.jobs.get_manifest(job_id)
|
|
145
|
+
if manifest:
|
|
146
|
+
print(manifest.outcome) # "success" | "blocked" | "failed"
|
|
147
|
+
print(manifest.blocked) # True when the target challenged/denied us
|
|
148
|
+
print(manifest.credits.amount) # credits charged (0 when not charged)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Monitors
|
|
152
|
+
|
|
153
|
+
A monitor runs a scrape on a cron cadence; every fire mints a normal job. Add a
|
|
154
|
+
`detection` block to also get notified when the watched content changes.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
monitor = client.monitors.create(
|
|
158
|
+
name="hourly-prices",
|
|
159
|
+
cron="0 * * * *",
|
|
160
|
+
timezone="Europe/Paris",
|
|
161
|
+
job_type="light",
|
|
162
|
+
target_url="https://example.com/pricing",
|
|
163
|
+
# optional: watch for changes and alert by webhook + email
|
|
164
|
+
detection={"enabled": True, "mode": "selector", "selector": ".price",
|
|
165
|
+
"notify": {"email": ["alerts@acme.eu"]}},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
client.monitors.pause(monitor.id)
|
|
169
|
+
client.monitors.resume(monitor.id)
|
|
170
|
+
|
|
171
|
+
for run in client.monitors.iter_runs(monitor.id):
|
|
172
|
+
print(run.fired_at, run.status, run.job_id)
|
|
173
|
+
|
|
174
|
+
# When detection is on, review what changed (newest first):
|
|
175
|
+
for change in client.monitors.iter_changes(monitor.id):
|
|
176
|
+
print(change.detected_at, change.change_ratio, change.notified_channels)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`update()` replaces the whole monitor definition - pass every field you want to keep.
|
|
180
|
+
|
|
181
|
+
## Webhook endpoints
|
|
182
|
+
|
|
183
|
+
Register an HTTPS endpoint once and ScrapeNest pushes job events to it - no polling:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
endpoint = client.webhook_endpoints.create(
|
|
187
|
+
name="prod-receiver",
|
|
188
|
+
url="https://hooks.your-app.com/scrapenest",
|
|
189
|
+
)
|
|
190
|
+
print(endpoint.secret) # signing secret - shown once only, store it now
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Debug deliveries without leaving Python:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
for message in client.webhook_endpoints.iter_messages(endpoint.endpoint_id):
|
|
197
|
+
print(message.event_type, message.status)
|
|
198
|
+
if message.status == "failed":
|
|
199
|
+
attempts = client.webhook_endpoints.attempts(endpoint.endpoint_id, message.message_id)
|
|
200
|
+
for attempt in attempts.items:
|
|
201
|
+
print(f" HTTP {attempt.status_code} in {attempt.latency_ms}ms: {attempt.response_body}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`rotate_secret()`, `pause()`, `resume()`, `update()`, and `delete()` complete the lifecycle.
|
|
205
|
+
Rotation invalidates the old secret immediately - update your receiver first.
|
|
206
|
+
|
|
207
|
+
### Verifying deliveries
|
|
208
|
+
|
|
209
|
+
Verify the signature of incoming webhook deliveries before trusting them:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from scrapenest import verify_webhook, WebhookVerificationError
|
|
213
|
+
|
|
214
|
+
# `payload` is the raw request body (bytes); `headers` carries the svix-* headers.
|
|
215
|
+
try:
|
|
216
|
+
event = verify_webhook(payload, headers, secret="whsec_...")
|
|
217
|
+
except WebhookVerificationError:
|
|
218
|
+
return 400 # reject
|
|
219
|
+
handle(event)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
See [`examples/webhook_receiver.py`](examples/webhook_receiver.py) for a full FastAPI handler.
|
|
223
|
+
|
|
224
|
+
## API keys
|
|
225
|
+
|
|
226
|
+
Provision least-privilege keys per consumer (requires the `api_keys.manage` scope):
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
created = client.api_keys.create(
|
|
230
|
+
name="ci-pipeline",
|
|
231
|
+
scopes=["jobs.create", "jobs.read", "artifacts.read"],
|
|
232
|
+
allowed_cidrs=["203.0.113.0/24"],
|
|
233
|
+
rate_limit_rpm=60,
|
|
234
|
+
)
|
|
235
|
+
print(created.token) # plaintext key - shown once only
|
|
236
|
+
|
|
237
|
+
client.api_keys.rotate(created.api_key_id) # old token stops working immediately
|
|
238
|
+
client.api_keys.revoke(created.api_key_id, reason="pipeline retired")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Retention and legal holds
|
|
242
|
+
|
|
243
|
+
Artifacts are purged after your retention window unless a hold protects them
|
|
244
|
+
(requires the `retention.manage` scope):
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
policy = client.retention.get_policy()
|
|
248
|
+
print(policy.retention_days, policy.max_retention_days)
|
|
249
|
+
|
|
250
|
+
hold = client.retention.create_hold(
|
|
251
|
+
scope_type="job",
|
|
252
|
+
scope_ref=job_id,
|
|
253
|
+
justification="Dispute #4242 - preserve evidence",
|
|
254
|
+
)
|
|
255
|
+
client.retention.release_hold(hold.hold_id, release_notes="Dispute closed")
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Organization IP allowlist
|
|
259
|
+
|
|
260
|
+
Restrict API access to known networks (requires the `org.manage` scope):
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
client.org.set_ip_allowlist(["203.0.113.0/24", "2001:db8::/32"])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The effective allowlist for a key is the org list intersected with any non-empty key-level list.
|
|
267
|
+
|
|
268
|
+
## Errors
|
|
269
|
+
|
|
270
|
+
Every non-2xx response raises a typed exception, all subclasses of `ScrapeNestAPIError`:
|
|
271
|
+
|
|
272
|
+
| Status | Exception |
|
|
273
|
+
|--------|-----------|
|
|
274
|
+
| 400 | `BadRequestError` |
|
|
275
|
+
| 401 | `AuthenticationError` |
|
|
276
|
+
| 403 | `PermissionDeniedError` |
|
|
277
|
+
| 404 | `NotFoundError` |
|
|
278
|
+
| 409 | `ConflictError` |
|
|
279
|
+
| 422 | `ValidationError` (see `.details`) |
|
|
280
|
+
| 429 | `RateLimitError` (see `.retry_after`) |
|
|
281
|
+
| 5xx | `ServerError` |
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from scrapenest import RateLimitError, ValidationError
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
client.submit(job_type="light", target_url="not-a-url")
|
|
288
|
+
except ValidationError as exc:
|
|
289
|
+
for problem in exc.details:
|
|
290
|
+
print(problem["field"], problem["message"])
|
|
291
|
+
except RateLimitError as exc:
|
|
292
|
+
print("retry after", exc.retry_after, "seconds")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Every `ScrapeNestAPIError` carries `request_id` - include it when contacting support.
|
|
296
|
+
|
|
297
|
+
`429` and `5xx` responses are retried automatically with jittered backoff (honoring `Retry-After`);
|
|
298
|
+
configure with `ScrapeNestClient(..., max_retries=2, backoff_factor=0.5)`. Transport failures after
|
|
299
|
+
retries raise `ScrapeNestConnectionError`.
|
|
300
|
+
|
|
301
|
+
## Examples
|
|
302
|
+
|
|
303
|
+
The [`examples/`](examples/) directory contains runnable end-to-end scripts: quickstart, structured
|
|
304
|
+
extraction, browser and stealth jobs, an async crawl, monitors and change detection, webhook endpoint
|
|
305
|
+
management, API key hygiene, and retention holds.
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# ScrapeNest Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python client for the [ScrapeNest](https://scrapenest.com) scraping API. Submit
|
|
4
|
+
scraping jobs, wait for or collect their results, download artifacts, manage monitors, webhook
|
|
5
|
+
endpoints, and API keys - everything the API offers, fully typed.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install scrapenest-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import scrapenest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.10+. The only runtime dependency is [httpx](https://www.python-httpx.org/).
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from scrapenest import ScrapeNestClient
|
|
21
|
+
|
|
22
|
+
client = ScrapeNestClient(api_key="sn_live_...")
|
|
23
|
+
|
|
24
|
+
# Submit and wait (polls until terminal, up to `timeout` seconds)
|
|
25
|
+
result = client.scrape(job_type="light", target_url="https://example.com")
|
|
26
|
+
print(result.status, result.artifact_count)
|
|
27
|
+
|
|
28
|
+
# Fetch the rendered HTML artifact
|
|
29
|
+
job = client.jobs.get(result.job_id)
|
|
30
|
+
html_artifact = next(a for a in job.artifacts if a.artifact_type == "html")
|
|
31
|
+
html = client.artifacts.download_text(html_artifact.artifact_id)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Authentication uses your API key, sent automatically in the `X-API-Key` header. Create and manage
|
|
35
|
+
keys in the dashboard (or with `client.api_keys`, below).
|
|
36
|
+
|
|
37
|
+
## Job tiers
|
|
38
|
+
|
|
39
|
+
`job_type` selects the worker tier; the SDK gives you autocomplete for each tier's options:
|
|
40
|
+
|
|
41
|
+
| Tier | What it does | Tier-specific options |
|
|
42
|
+
|------|--------------|-----------------------|
|
|
43
|
+
| `light` | Fast HTTP-only fetch | `method`, `body`, `follow_redirects`, `retry_policy` |
|
|
44
|
+
| `standard` | Headless Chromium | `actions`, `viewport`, `wait_until`, `locale`, ... |
|
|
45
|
+
| `stealth` | Anti-blocking browser | everything in standard plus `os_name`, `proxy`, `browser_extensions` |
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Stealth job with structured extraction
|
|
49
|
+
job = client.submit(
|
|
50
|
+
job_type="stealth",
|
|
51
|
+
target_url="https://example.com/pricing",
|
|
52
|
+
artifact_options={"include_screenshot": True, "include_extraction": True},
|
|
53
|
+
extraction={
|
|
54
|
+
"hooks": [
|
|
55
|
+
{"hook_id": "price", "type": "css", "selector": ".price", "all_matches": True},
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
print(job.job_id, "idempotent replay:", job.idempotent_replay)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Waiting vs collecting later
|
|
63
|
+
|
|
64
|
+
The ScrapeNest API is asynchronous: you submit a job and collect the result once it runs.
|
|
65
|
+
|
|
66
|
+
- `client.scrape(...)` submits and blocks (polling) until the job reaches a terminal state.
|
|
67
|
+
- `client.submit(...)` returns an acknowledgement immediately; collect the result later via
|
|
68
|
+
`client.jobs.get(...)` or a webhook.
|
|
69
|
+
|
|
70
|
+
Every job creation automatically attaches an `idempotency_token`, so transparent retries never
|
|
71
|
+
create duplicate jobs. Pass your own `idempotency_token` to make a call safe to repeat across
|
|
72
|
+
processes.
|
|
73
|
+
|
|
74
|
+
## Async client
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import asyncio
|
|
78
|
+
from scrapenest import AsyncScrapeNestClient
|
|
79
|
+
|
|
80
|
+
async def main():
|
|
81
|
+
async with AsyncScrapeNestClient(api_key="sn_live_...") as client:
|
|
82
|
+
result = await client.scrape(job_type="light", target_url="https://example.com")
|
|
83
|
+
print(result.status)
|
|
84
|
+
|
|
85
|
+
async for summary in client.jobs.iter(status="succeeded"):
|
|
86
|
+
print(summary.job_id)
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The async client mirrors the sync API one-to-one across every resource.
|
|
92
|
+
|
|
93
|
+
## Listing and paginating jobs
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
page = client.jobs.list(status="succeeded", limit=50)
|
|
97
|
+
print(page.total)
|
|
98
|
+
|
|
99
|
+
# Or iterate every match, transparently walking pages:
|
|
100
|
+
for summary in client.jobs.iter(job_type="stealth"):
|
|
101
|
+
print(summary.job_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Job outcome and credits (manifest)
|
|
105
|
+
|
|
106
|
+
A job's `status` tells you whether it ran (`succeeded`/`failed`). To see whether the target
|
|
107
|
+
*blocked* you and what it cost, fetch the forensics manifest:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
manifest = client.jobs.get_manifest(job_id)
|
|
111
|
+
if manifest:
|
|
112
|
+
print(manifest.outcome) # "success" | "blocked" | "failed"
|
|
113
|
+
print(manifest.blocked) # True when the target challenged/denied us
|
|
114
|
+
print(manifest.credits.amount) # credits charged (0 when not charged)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Monitors
|
|
118
|
+
|
|
119
|
+
A monitor runs a scrape on a cron cadence; every fire mints a normal job. Add a
|
|
120
|
+
`detection` block to also get notified when the watched content changes.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
monitor = client.monitors.create(
|
|
124
|
+
name="hourly-prices",
|
|
125
|
+
cron="0 * * * *",
|
|
126
|
+
timezone="Europe/Paris",
|
|
127
|
+
job_type="light",
|
|
128
|
+
target_url="https://example.com/pricing",
|
|
129
|
+
# optional: watch for changes and alert by webhook + email
|
|
130
|
+
detection={"enabled": True, "mode": "selector", "selector": ".price",
|
|
131
|
+
"notify": {"email": ["alerts@acme.eu"]}},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
client.monitors.pause(monitor.id)
|
|
135
|
+
client.monitors.resume(monitor.id)
|
|
136
|
+
|
|
137
|
+
for run in client.monitors.iter_runs(monitor.id):
|
|
138
|
+
print(run.fired_at, run.status, run.job_id)
|
|
139
|
+
|
|
140
|
+
# When detection is on, review what changed (newest first):
|
|
141
|
+
for change in client.monitors.iter_changes(monitor.id):
|
|
142
|
+
print(change.detected_at, change.change_ratio, change.notified_channels)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`update()` replaces the whole monitor definition - pass every field you want to keep.
|
|
146
|
+
|
|
147
|
+
## Webhook endpoints
|
|
148
|
+
|
|
149
|
+
Register an HTTPS endpoint once and ScrapeNest pushes job events to it - no polling:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
endpoint = client.webhook_endpoints.create(
|
|
153
|
+
name="prod-receiver",
|
|
154
|
+
url="https://hooks.your-app.com/scrapenest",
|
|
155
|
+
)
|
|
156
|
+
print(endpoint.secret) # signing secret - shown once only, store it now
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Debug deliveries without leaving Python:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
for message in client.webhook_endpoints.iter_messages(endpoint.endpoint_id):
|
|
163
|
+
print(message.event_type, message.status)
|
|
164
|
+
if message.status == "failed":
|
|
165
|
+
attempts = client.webhook_endpoints.attempts(endpoint.endpoint_id, message.message_id)
|
|
166
|
+
for attempt in attempts.items:
|
|
167
|
+
print(f" HTTP {attempt.status_code} in {attempt.latency_ms}ms: {attempt.response_body}")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`rotate_secret()`, `pause()`, `resume()`, `update()`, and `delete()` complete the lifecycle.
|
|
171
|
+
Rotation invalidates the old secret immediately - update your receiver first.
|
|
172
|
+
|
|
173
|
+
### Verifying deliveries
|
|
174
|
+
|
|
175
|
+
Verify the signature of incoming webhook deliveries before trusting them:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from scrapenest import verify_webhook, WebhookVerificationError
|
|
179
|
+
|
|
180
|
+
# `payload` is the raw request body (bytes); `headers` carries the svix-* headers.
|
|
181
|
+
try:
|
|
182
|
+
event = verify_webhook(payload, headers, secret="whsec_...")
|
|
183
|
+
except WebhookVerificationError:
|
|
184
|
+
return 400 # reject
|
|
185
|
+
handle(event)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
See [`examples/webhook_receiver.py`](examples/webhook_receiver.py) for a full FastAPI handler.
|
|
189
|
+
|
|
190
|
+
## API keys
|
|
191
|
+
|
|
192
|
+
Provision least-privilege keys per consumer (requires the `api_keys.manage` scope):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
created = client.api_keys.create(
|
|
196
|
+
name="ci-pipeline",
|
|
197
|
+
scopes=["jobs.create", "jobs.read", "artifacts.read"],
|
|
198
|
+
allowed_cidrs=["203.0.113.0/24"],
|
|
199
|
+
rate_limit_rpm=60,
|
|
200
|
+
)
|
|
201
|
+
print(created.token) # plaintext key - shown once only
|
|
202
|
+
|
|
203
|
+
client.api_keys.rotate(created.api_key_id) # old token stops working immediately
|
|
204
|
+
client.api_keys.revoke(created.api_key_id, reason="pipeline retired")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Retention and legal holds
|
|
208
|
+
|
|
209
|
+
Artifacts are purged after your retention window unless a hold protects them
|
|
210
|
+
(requires the `retention.manage` scope):
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
policy = client.retention.get_policy()
|
|
214
|
+
print(policy.retention_days, policy.max_retention_days)
|
|
215
|
+
|
|
216
|
+
hold = client.retention.create_hold(
|
|
217
|
+
scope_type="job",
|
|
218
|
+
scope_ref=job_id,
|
|
219
|
+
justification="Dispute #4242 - preserve evidence",
|
|
220
|
+
)
|
|
221
|
+
client.retention.release_hold(hold.hold_id, release_notes="Dispute closed")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Organization IP allowlist
|
|
225
|
+
|
|
226
|
+
Restrict API access to known networks (requires the `org.manage` scope):
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
client.org.set_ip_allowlist(["203.0.113.0/24", "2001:db8::/32"])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The effective allowlist for a key is the org list intersected with any non-empty key-level list.
|
|
233
|
+
|
|
234
|
+
## Errors
|
|
235
|
+
|
|
236
|
+
Every non-2xx response raises a typed exception, all subclasses of `ScrapeNestAPIError`:
|
|
237
|
+
|
|
238
|
+
| Status | Exception |
|
|
239
|
+
|--------|-----------|
|
|
240
|
+
| 400 | `BadRequestError` |
|
|
241
|
+
| 401 | `AuthenticationError` |
|
|
242
|
+
| 403 | `PermissionDeniedError` |
|
|
243
|
+
| 404 | `NotFoundError` |
|
|
244
|
+
| 409 | `ConflictError` |
|
|
245
|
+
| 422 | `ValidationError` (see `.details`) |
|
|
246
|
+
| 429 | `RateLimitError` (see `.retry_after`) |
|
|
247
|
+
| 5xx | `ServerError` |
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from scrapenest import RateLimitError, ValidationError
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
client.submit(job_type="light", target_url="not-a-url")
|
|
254
|
+
except ValidationError as exc:
|
|
255
|
+
for problem in exc.details:
|
|
256
|
+
print(problem["field"], problem["message"])
|
|
257
|
+
except RateLimitError as exc:
|
|
258
|
+
print("retry after", exc.retry_after, "seconds")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Every `ScrapeNestAPIError` carries `request_id` - include it when contacting support.
|
|
262
|
+
|
|
263
|
+
`429` and `5xx` responses are retried automatically with jittered backoff (honoring `Retry-After`);
|
|
264
|
+
configure with `ScrapeNestClient(..., max_retries=2, backoff_factor=0.5)`. Transport failures after
|
|
265
|
+
retries raise `ScrapeNestConnectionError`.
|
|
266
|
+
|
|
267
|
+
## Examples
|
|
268
|
+
|
|
269
|
+
The [`examples/`](examples/) directory contains runnable end-to-end scripts: quickstart, structured
|
|
270
|
+
extraction, browser and stealth jobs, an async crawl, monitors and change detection, webhook endpoint
|
|
271
|
+
management, API key hygiene, and retention holds.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT. See [LICENSE](LICENSE).
|