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.
Files changed (55) hide show
  1. scrapenest_sdk-1.0.0/.gitignore +12 -0
  2. scrapenest_sdk-1.0.0/CHANGELOG.md +38 -0
  3. scrapenest_sdk-1.0.0/LICENSE +21 -0
  4. scrapenest_sdk-1.0.0/PKG-INFO +309 -0
  5. scrapenest_sdk-1.0.0/README.md +275 -0
  6. scrapenest_sdk-1.0.0/examples/01_quickstart_product.py +68 -0
  7. scrapenest_sdk-1.0.0/examples/02_extract_listing.py +66 -0
  8. scrapenest_sdk-1.0.0/examples/03_json_api_jsonpath.py +48 -0
  9. scrapenest_sdk-1.0.0/examples/04_dataset_to_csv.py +57 -0
  10. scrapenest_sdk-1.0.0/examples/05_browser_infinite_scroll.py +56 -0
  11. scrapenest_sdk-1.0.0/examples/06_stealth_screenshot.py +54 -0
  12. scrapenest_sdk-1.0.0/examples/07_async_crawl.py +53 -0
  13. scrapenest_sdk-1.0.0/examples/08_monitor.py +48 -0
  14. scrapenest_sdk-1.0.0/examples/09_change_detection.py +55 -0
  15. scrapenest_sdk-1.0.0/examples/09_webhook_endpoints.py +60 -0
  16. scrapenest_sdk-1.0.0/examples/10_api_key_hygiene.py +48 -0
  17. scrapenest_sdk-1.0.0/examples/11_retention_and_holds.py +52 -0
  18. scrapenest_sdk-1.0.0/examples/README.md +57 -0
  19. scrapenest_sdk-1.0.0/examples/_common.py +67 -0
  20. scrapenest_sdk-1.0.0/examples/webhook_receiver.py +29 -0
  21. scrapenest_sdk-1.0.0/pyproject.toml +83 -0
  22. scrapenest_sdk-1.0.0/src/scrapenest/__init__.py +132 -0
  23. scrapenest_sdk-1.0.0/src/scrapenest/_async_client.py +168 -0
  24. scrapenest_sdk-1.0.0/src/scrapenest/_client.py +167 -0
  25. scrapenest_sdk-1.0.0/src/scrapenest/_ops.py +512 -0
  26. scrapenest_sdk-1.0.0/src/scrapenest/_parsing.py +542 -0
  27. scrapenest_sdk-1.0.0/src/scrapenest/_retry.py +45 -0
  28. scrapenest_sdk-1.0.0/src/scrapenest/_transport.py +103 -0
  29. scrapenest_sdk-1.0.0/src/scrapenest/_version.py +1 -0
  30. scrapenest_sdk-1.0.0/src/scrapenest/exceptions.py +156 -0
  31. scrapenest_sdk-1.0.0/src/scrapenest/models.py +524 -0
  32. scrapenest_sdk-1.0.0/src/scrapenest/py.typed +1 -0
  33. scrapenest_sdk-1.0.0/src/scrapenest/resources/__init__.py +1 -0
  34. scrapenest_sdk-1.0.0/src/scrapenest/resources/api_keys.py +113 -0
  35. scrapenest_sdk-1.0.0/src/scrapenest/resources/artifacts.py +78 -0
  36. scrapenest_sdk-1.0.0/src/scrapenest/resources/audit_logs.py +103 -0
  37. scrapenest_sdk-1.0.0/src/scrapenest/resources/jobs.py +207 -0
  38. scrapenest_sdk-1.0.0/src/scrapenest/resources/monitors.py +256 -0
  39. scrapenest_sdk-1.0.0/src/scrapenest/resources/org.py +42 -0
  40. scrapenest_sdk-1.0.0/src/scrapenest/resources/retention.py +96 -0
  41. scrapenest_sdk-1.0.0/src/scrapenest/resources/webhook_endpoints.py +190 -0
  42. scrapenest_sdk-1.0.0/src/scrapenest/types.py +229 -0
  43. scrapenest_sdk-1.0.0/src/scrapenest/webhooks.py +105 -0
  44. scrapenest_sdk-1.0.0/tests/conftest.py +29 -0
  45. scrapenest_sdk-1.0.0/tests/test_api_keys.py +134 -0
  46. scrapenest_sdk-1.0.0/tests/test_async.py +119 -0
  47. scrapenest_sdk-1.0.0/tests/test_audit_logs.py +80 -0
  48. scrapenest_sdk-1.0.0/tests/test_client.py +269 -0
  49. scrapenest_sdk-1.0.0/tests/test_exceptions.py +69 -0
  50. scrapenest_sdk-1.0.0/tests/test_monitors.py +248 -0
  51. scrapenest_sdk-1.0.0/tests/test_pagination_manifest.py +133 -0
  52. scrapenest_sdk-1.0.0/tests/test_retention_org.py +144 -0
  53. scrapenest_sdk-1.0.0/tests/test_retry.py +140 -0
  54. scrapenest_sdk-1.0.0/tests/test_webhook_endpoints.py +254 -0
  55. scrapenest_sdk-1.0.0/tests/test_webhooks.py +74 -0
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ __pycache__/
9
+
10
+ # example run outputs
11
+ examples/*.png
12
+ examples/*.csv
@@ -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).