alterlab 2.4.2__tar.gz → 2.4.4__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.
- alterlab-2.4.4/PKG-INFO +675 -0
- alterlab-2.4.4/README.md +651 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/__init__.py +9 -1
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/actions.py +444 -4
- alterlab-2.4.4/alterlab/artifacts.py +81 -0
- alterlab-2.4.4/alterlab/cli.py +232 -0
- alterlab-2.4.4/alterlab/cli_auth.py +135 -0
- alterlab-2.4.4/alterlab/cli_data.py +237 -0
- alterlab-2.4.4/alterlab/cli_hands.py +330 -0
- alterlab-2.4.4/alterlab/cli_local/__init__.py +276 -0
- alterlab-2.4.4/alterlab/cli_local/vendor_signatures.json +177 -0
- alterlab-2.4.4/alterlab/cli_routing.py +64 -0
- alterlab-2.4.4/alterlab/cli_runs.py +129 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/client.py +246 -22
- alterlab-2.4.4/alterlab/data/tool_definitions.json +69 -0
- alterlab-2.4.4/alterlab/hands_receipts.py +77 -0
- alterlab-2.4.4/alterlab/listings/__init__.py +271 -0
- alterlab-2.4.4/alterlab/mcp_stdio.py +142 -0
- alterlab-2.4.4/alterlab/research.py +171 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/types.py +12 -0
- alterlab-2.4.4/pyproject.toml +29 -0
- alterlab-2.4.2/PKG-INFO +0 -19
- alterlab-2.4.2/pyproject.toml +0 -23
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/_failure_backoff.py +0 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/crypto.py +0 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/document_types.py +0 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/requests_compat.py +0 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/resources/__init__.py +0 -0
- {alterlab-2.4.2 → alterlab-2.4.4}/alterlab/resources/sessions.py +0 -0
alterlab-2.4.4/PKG-INFO
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: alterlab
|
|
3
|
+
Version: 2.4.4
|
|
4
|
+
Summary: Official Python client for AlterLab: search, read and extract the live web, with automatic fallback from direct fetch to full browser rendering. Pay only for successful requests.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: AlterLab
|
|
7
|
+
Author-email: dev@alterlab.io
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Dist: httpx (>=0.25.2,<1.0)
|
|
20
|
+
Requires-Dist: pydantic (>=2.5.0,<3.0.0)
|
|
21
|
+
Requires-Dist: typing_extensions (>=3.7.4)
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# AlterLab Python SDK
|
|
25
|
+
|
|
26
|
+
Official Python client for [AlterLab](https://alterlab.io): search, read and extract the live web, with automatic fallback from direct fetch to full browser rendering. Pay only for successful requests.
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/alterlab/)
|
|
29
|
+
[](https://pypi.org/project/alterlab/)
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install alterlab
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import asyncio
|
|
41
|
+
from alterlab import AlterLab
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
async with AlterLab(api_key="sk_test_...") as client:
|
|
45
|
+
result = await client.scrape("https://example.com")
|
|
46
|
+
print(result["content"])
|
|
47
|
+
|
|
48
|
+
asyncio.run(main())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Output (the page text for `https://example.com`):
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
Example Domain
|
|
55
|
+
|
|
56
|
+
This domain is for use in documentation examples without needing permission. Avoid use in operations.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See [pricing](https://alterlab.io/pricing) and the [benchmark](https://alterlab.io/benchmark).
|
|
60
|
+
|
|
61
|
+
Prefer synchronous code? Use the sync wrapper:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from alterlab import AlterLabSync
|
|
65
|
+
|
|
66
|
+
with AlterLabSync(api_key="sk_test_...") as client:
|
|
67
|
+
result = client.scrape("https://example.com")
|
|
68
|
+
print(result["content"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Drop-in `requests` replacement
|
|
72
|
+
|
|
73
|
+
Already have code written against the [`requests`](https://requests.readthedocs.io/)
|
|
74
|
+
library? Swap the import line and keep the rest of your code unchanged — every call is
|
|
75
|
+
routed through AlterLab, with automatic fallback from direct fetch to full browser
|
|
76
|
+
rendering, instead of a bare socket connection, with no new backend endpoint involved.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from alterlab import requests # instead of: import requests
|
|
80
|
+
|
|
81
|
+
# Reads ALTERLAB_API_KEY from the environment on first call.
|
|
82
|
+
response = requests.get("https://example.com", headers={"X-Custom-Header": "value"})
|
|
83
|
+
print(response.status_code)
|
|
84
|
+
print(response.text)
|
|
85
|
+
|
|
86
|
+
data = requests.post(
|
|
87
|
+
"https://api.example.com/thing",
|
|
88
|
+
json={"key": "value"},
|
|
89
|
+
).json()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
No `ALTERLAB_API_KEY` in your environment, or want to set the key explicitly?
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from alterlab import requests
|
|
96
|
+
|
|
97
|
+
requests.configure(api_key="sk_test_...")
|
|
98
|
+
response = requests.get("https://example.com")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported: `requests.get`, `.post`, `.put`, `.patch`, `.delete`, `.request(method, url, ...)`,
|
|
102
|
+
with `params=`, `json=`, `data=`, `headers=`, and `timeout=` matching `requests`' own call
|
|
103
|
+
signature. The returned `Response` object exposes `.status_code`, `.text`, `.content`,
|
|
104
|
+
`.headers`, `.url`, `.json()`, `.ok`, and `.raise_for_status()`.
|
|
105
|
+
|
|
106
|
+
**Note**: AlterLab's own exceptions (`AlterLabAPIError` and subclasses like
|
|
107
|
+
`AuthenticationError`, `InsufficientFundsError`, `RateLimitError`) propagate directly from
|
|
108
|
+
these calls rather than being wrapped in `requests.exceptions` types — catch
|
|
109
|
+
`alterlab.AlterLabAPIError` instead of `requests.exceptions.HTTPError`. This includes
|
|
110
|
+
`.raise_for_status()`: for a 4xx/5xx status returned by the *target site* through an
|
|
111
|
+
otherwise-successful scrape, it raises `alterlab.AlterLabHTTPError` (a subclass of
|
|
112
|
+
`AlterLabAPIError`, with a `.status_code` attribute) — not `requests.exceptions.HTTPError`.
|
|
113
|
+
|
|
114
|
+
## Command line
|
|
115
|
+
|
|
116
|
+
The `alterlab` CLI ships with the SDK (MIT licensed). Install it with `pipx install alterlab`,
|
|
117
|
+
`uvx alterlab ...` or `pip install alterlab` (add `keyring` to store the key in the OS keychain).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
alterlab login --email you@example.com # a human approves in the browser; the key is never printed
|
|
121
|
+
alterlab scrape https://example.com -f md --estimate
|
|
122
|
+
alterlab crawl https://example.com --max 50 --max-cost 0.50 | jq .url
|
|
123
|
+
cat urls.txt | alterlab batch
|
|
124
|
+
alterlab search "web scraping" --content
|
|
125
|
+
alterlab extract https://shop.example/item --profile product
|
|
126
|
+
alterlab fetch https://example.com # direct fetch first, AlterLab only if blocked
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Data commands stream NDJSON (or markdown) to stdout and print the cost to stderr.
|
|
130
|
+
`--estimate` prints the quote and exits; `--max-cost USD` is a hard cap.
|
|
131
|
+
|
|
132
|
+
No account needed: `alterlab detect <url>`, `alterlab doctor <url>`, `alterlab md <file|url>`,
|
|
133
|
+
`alterlab robots <url>`.
|
|
134
|
+
|
|
135
|
+
Agents: `claude mcp add alterlab -- uvx alterlab mcp` runs a local stdio MCP server;
|
|
136
|
+
`alterlab skill install` installs the agent skill.
|
|
137
|
+
|
|
138
|
+
`alterlab routing-policy [--agent claude|codex|agents|cursor|copilot|windsurf] [--domains a.com,b.org] [--dir PATH]` writes the "use AlterLab for web tasks" policy into the client's project instructions file (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules/alterlab.mdc`, ...). Re-running replaces the marked block in place; all other content is kept.
|
|
139
|
+
|
|
140
|
+
## Methods
|
|
141
|
+
|
|
142
|
+
### `scrape(url, **kwargs)`
|
|
143
|
+
|
|
144
|
+
Scrape a single URL. Supports all scraping modes and automatic async job polling.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# Simple HTML scraping
|
|
148
|
+
result = await client.scrape("https://example.com")
|
|
149
|
+
|
|
150
|
+
# JavaScript rendering
|
|
151
|
+
result = await client.scrape("https://example.com", mode="js")
|
|
152
|
+
|
|
153
|
+
# With output formats
|
|
154
|
+
result = await client.scrape(
|
|
155
|
+
"https://example.com",
|
|
156
|
+
formats=["markdown", "text"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Structured extraction with a profile
|
|
160
|
+
result = await client.scrape(
|
|
161
|
+
"https://example.com/product",
|
|
162
|
+
extraction_profile="product",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Structured extraction with a JSON schema
|
|
166
|
+
result = await client.scrape(
|
|
167
|
+
"https://example.com/product",
|
|
168
|
+
extraction_schema={
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"title": {"type": "string"},
|
|
172
|
+
"price": {"type": "number"},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Parameters:**
|
|
179
|
+
|
|
180
|
+
| Parameter | Type | Default | Description |
|
|
181
|
+
|-----------|------|---------|-------------|
|
|
182
|
+
| `url` | `str` | required | URL to scrape |
|
|
183
|
+
| `mode` | `str` | `"auto"` | Scraping mode: `auto`, `html`, `js`, `pdf`, `ocr` |
|
|
184
|
+
| `sync` | `bool` | `True` | Wait for result (False returns job_id immediately) |
|
|
185
|
+
| `response` | `Optional[Literal["full", "minimal"]]` | `None` | `"minimal"` returns only content, status, url, cost and warnings (server default for MCP, agent and keyless keys) |
|
|
186
|
+
| `sync_policy` | `Literal["auto", "wait_until_deadline"]` | `"auto"` | `auto` preserves early 202 handoffs; `wait_until_deadline` waits through tier boundaries until terminal completion or the server's 85-second safe deadline (requires `sync=True`) |
|
|
187
|
+
| `formats` | `list` | `None` | Output formats: `text`, `json`, `json_v2`, `html`, `markdown`, `rag` |
|
|
188
|
+
| `extraction_schema` | `dict` | `None` | JSON Schema for structured extraction |
|
|
189
|
+
| `extraction_prompt` | `str` | `None` | Natural language extraction instructions |
|
|
190
|
+
| `extraction_profile` | `str` | `None` | Pre-built profile: `auto`, `product`, `article`, `job_posting`, `faq`, `recipe`, `event` |
|
|
191
|
+
| `advanced` | `AdvancedOptions` | `None` | Advanced scraping options (see below) |
|
|
192
|
+
| `cost_controls` | `CostControls` | `None` | Cost control parameters (see below) |
|
|
193
|
+
| `force_refresh` | `bool` | `False` | Bypass cache |
|
|
194
|
+
| `evidence` | `bool` | `False` | Include provenance for extracted fields |
|
|
195
|
+
|
|
196
|
+
### `crawl(url, **kwargs)`
|
|
197
|
+
|
|
198
|
+
Start a multi-page website crawl. Returns immediately with a `crawl_id` for polling.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
crawl = await client.crawl(
|
|
202
|
+
"https://example.com",
|
|
203
|
+
max_pages=200,
|
|
204
|
+
max_depth=2,
|
|
205
|
+
include_patterns=["/blog/*"],
|
|
206
|
+
formats=["markdown"],
|
|
207
|
+
)
|
|
208
|
+
print(f"Crawl ID: {crawl['crawl_id']}")
|
|
209
|
+
print(f"Estimated pages: {crawl['estimated_pages']}")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Parameters:**
|
|
213
|
+
|
|
214
|
+
| Parameter | Type | Default | Description |
|
|
215
|
+
|-----------|------|---------|-------------|
|
|
216
|
+
| `url` | `str` | required | Starting URL |
|
|
217
|
+
| `max_pages` | `int` | `50` | Maximum pages to scrape (1-100,000) |
|
|
218
|
+
| `max_depth` | `int` | `3` | Maximum link-following depth (0-50) |
|
|
219
|
+
| `include_patterns` | `list[str]` | `None` | Glob patterns to include (e.g. `["/products/*"]`) |
|
|
220
|
+
| `exclude_patterns` | `list[str]` | `None` | Glob patterns to exclude |
|
|
221
|
+
| `formats` | `list` | `None` | Output formats per page |
|
|
222
|
+
| `extraction_schema` | `dict` | `None` | JSON Schema for extraction on each page |
|
|
223
|
+
| `extraction_profile` | `str` | `None` | Extraction profile for each page |
|
|
224
|
+
| `webhook_url` | `str` | `None` | URL for `crawl.completed` webhook |
|
|
225
|
+
| `respect_robots` | `bool` | `True` | Respect robots.txt |
|
|
226
|
+
| `include_subdomains` | `bool` | `False` | Follow links to subdomains |
|
|
227
|
+
| `cost_controls` | `dict` | `None` | `{"max_credits": N, "max_tier": "2"}` |
|
|
228
|
+
|
|
229
|
+
### `crawl_and_wait(url, **kwargs)`
|
|
230
|
+
|
|
231
|
+
Start a crawl and poll until completion. Returns all page results.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
results = await client.crawl_and_wait(
|
|
235
|
+
"https://example.com",
|
|
236
|
+
max_pages=50,
|
|
237
|
+
formats=["markdown"],
|
|
238
|
+
poll_timeout=300.0,
|
|
239
|
+
)
|
|
240
|
+
print(f"Status: {results['status']}")
|
|
241
|
+
print(f"Pages scraped: {results['completed']}")
|
|
242
|
+
for page in results.get("pages", []):
|
|
243
|
+
print(f" {page['url']}: {page['status']}")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `get_crawl(crawl_id, include_results=False)`
|
|
247
|
+
|
|
248
|
+
Check crawl progress. Pass `include_results=True` to get per-page scrape results.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
status = await client.get_crawl(crawl_id)
|
|
252
|
+
print(f"{status['completed']}/{status['total']} pages done")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### `cancel_crawl(crawl_id)`
|
|
256
|
+
|
|
257
|
+
Cancel a running crawl. Credits for unprocessed pages are refunded.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
result = await client.cancel_crawl(crawl_id)
|
|
261
|
+
print(f"Refunded {result['credits_refunded']} credits")
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### `batch_scrape(requests, webhook_url=None)`
|
|
265
|
+
|
|
266
|
+
Submit multiple scrape requests as a batch.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
batch = await client.batch_scrape([
|
|
270
|
+
{"url": "https://example.com/page1", "mode": "html"},
|
|
271
|
+
{"url": "https://example.com/page2", "mode": "js"},
|
|
272
|
+
])
|
|
273
|
+
for job_id in batch["job_ids"]:
|
|
274
|
+
result = await client.wait_for_job(job_id)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `search(query, **kwargs)`
|
|
278
|
+
|
|
279
|
+
Unified, vendor-neutral search with page content by default. A plain `search("query")` runs
|
|
280
|
+
`engine="auto"` on `/v1/search` and returns each result page as concise markdown with a
|
|
281
|
+
per-result `content_status` (`scrape_results=True`, `concise=True`, `mode="standard"`). The unified
|
|
282
|
+
query is a flat $0.001; each fetched page bills at its fetch tier. Pass `scrape_results=False` for the
|
|
283
|
+
result list only. The SDK default engine is `auto`; the REST API's own default stays `duckduckgo`. An explicit `engine` keeps that engine's behaviour (no content unless asked for,
|
|
284
|
+
plain requests use `/v1/serp`).
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
results = client.search("headless browsers") # unified, with content
|
|
288
|
+
for item in results["results"]:
|
|
289
|
+
print(item["rank"], item["title"], item["url"], item["content_status"])
|
|
290
|
+
|
|
291
|
+
serp = client.search("headless browsers", engine="google", num=10, start=0, device="desktop")
|
|
292
|
+
for item in serp["organic_results"]:
|
|
293
|
+
print(item["position"], item["title"], item["url"])
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Engine | Price per query |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| `duckduckgo` | $0.001 |
|
|
299
|
+
| `google` | $0.004 |
|
|
300
|
+
| `bing` | $0.004 |
|
|
301
|
+
|
|
302
|
+
With `scrape_results`, pass `sync=True` to wait up to ~30s and get content in one call; on timeout the
|
|
303
|
+
response is a 202 with `next_action` (poll the search id).
|
|
304
|
+
|
|
305
|
+
`scrape_results`, `domain` and `time_range` are still served by the `/v1/search` alias, so
|
|
306
|
+
requests that use them are sent there. The response is the JSON of the endpoint used: `organic_results` from
|
|
307
|
+
`/v1/serp`, `results` from the alias. See the
|
|
308
|
+
[migration guide](https://alterlab.io/docs/migrations/search-to-serp).
|
|
309
|
+
|
|
310
|
+
### `research(query, **options)` and the research job methods
|
|
311
|
+
|
|
312
|
+
Budgeted deep research on your own model key (BYOK; a missing key returns `byok_key_required`
|
|
313
|
+
and charges nothing). Every claim in the result carries citations.
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
job = await client.research(
|
|
317
|
+
"Which EU countries ban fur farming?",
|
|
318
|
+
effort="standard",
|
|
319
|
+
max_cost_microcents=50_000,
|
|
320
|
+
output="both",
|
|
321
|
+
)
|
|
322
|
+
job = await client.wait_for_research(job["id"]) # returns on completed / completed_partial / failed / cancelled
|
|
323
|
+
print(job["status"], job["cost"], job["result"])
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Also: `get_research(id)`, `cancel_research(id)`, `research_events(id, last_event_id=None)`
|
|
327
|
+
(progress events, resumable), and for `plan_first=True` jobs `get_research_plan(id)`,
|
|
328
|
+
`update_research_plan(id, plan)` and `approve_research(id, plan=None)`. Every method has a sync
|
|
329
|
+
twin on `AlterLabSync`. See the [API reference](https://alterlab.io/docs/api/research).
|
|
330
|
+
|
|
331
|
+
### `batch_search(queries, **kwargs)` and `get_batch_search(batch_id)`
|
|
332
|
+
|
|
333
|
+
Submit and poll a batch of search queries. Each query is billed at the engine rate above.
|
|
334
|
+
|
|
335
|
+
### `feedback(category, tried, happened, expected=None, job_id=None)`
|
|
336
|
+
|
|
337
|
+
Tell AlterLab what was hard. Returns `cluster_key` and `status_url`. Never include customer data.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
result = client.feedback("blocked", "mode=auto", "empty body with 200", job_id="job_123")
|
|
341
|
+
print(result["cluster_key"], result["status_url"])
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
CLI (reads `ALTERLAB_API_KEY`; `--job-id` defaults to `$ALTERLAB_LAST_JOB_ID`):
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
alterlab feedback --category blocked --message "empty body with 200" --job-id job_123
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `estimate_cost(url, **kwargs)`
|
|
351
|
+
|
|
352
|
+
Get a cost estimate before scraping.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
estimate = await client.estimate_cost("https://example.com", mode="js")
|
|
356
|
+
print(f"Estimated: {estimate['estimated_credits']} credits")
|
|
357
|
+
print(f"Max possible: {estimate['max_possible_credits']} credits")
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### `get_usage()`
|
|
361
|
+
|
|
362
|
+
Check your balance and usage stats. All monetary values are in **microcents** (1,000,000 = $1.00).
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
usage = await client.get_usage()
|
|
366
|
+
balance = usage['balance_microcents'] / 1_000_000
|
|
367
|
+
print(f"Balance: ${balance:.4f}")
|
|
368
|
+
print(f"Account tier: {usage['account_tier']}")
|
|
369
|
+
print(f"Entitlement source: {usage['entitlement_source']}")
|
|
370
|
+
print(f"Used this month: ${usage['credits_used_month'] / 1_000_000:.4f}")
|
|
371
|
+
print(f"Period: {usage['period_start']} to {usage['period_end']}")
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### `wait_for_job(job_id, **kwargs)`
|
|
375
|
+
|
|
376
|
+
Poll an async job until completion with exponential backoff.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
# Start async scrape
|
|
380
|
+
response = await client.scrape("https://example.com", sync=False)
|
|
381
|
+
result = await client.wait_for_job(response["job_id"])
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### `get_job_status(job_id, wait=0)`
|
|
385
|
+
|
|
386
|
+
Check job status. Returns immediately by default; pass `wait` (seconds, `>= 0`) to
|
|
387
|
+
have the server hold the request until the job finishes or the window elapses.
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
status = await client.get_job_status(job_id)
|
|
391
|
+
print(f"Status: {status['status']}")
|
|
392
|
+
|
|
393
|
+
# Long-poll for up to 20 seconds
|
|
394
|
+
status = await client.get_job_status(job_id, wait=20)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Configuration
|
|
398
|
+
|
|
399
|
+
### `AdvancedOptions`
|
|
400
|
+
|
|
401
|
+
Fine-grained control over scraping behavior with credit cost implications.
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
from alterlab import AdvancedOptions
|
|
405
|
+
|
|
406
|
+
advanced = AdvancedOptions(
|
|
407
|
+
render_js=True, # +3 credits — headless browser rendering
|
|
408
|
+
screenshot=True, # +1 credit — full-page screenshot (requires render_js)
|
|
409
|
+
markdown=True, # Free — convert to markdown
|
|
410
|
+
generate_pdf=True, # +2 credits — generate PDF (requires render_js)
|
|
411
|
+
ocr=True, # +5 credits — extract text from images
|
|
412
|
+
use_proxy=True, # +1 credit — premium proxy
|
|
413
|
+
wait_condition="networkidle", # domcontentloaded | networkidle | load
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
result = await client.scrape("https://example.com", advanced=advanced)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### `CostControls`
|
|
420
|
+
|
|
421
|
+
Set spending limits and optimization preferences.
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
from alterlab import CostControls
|
|
425
|
+
|
|
426
|
+
controls = CostControls(
|
|
427
|
+
max_credits=5, # Hard credit limit for this request
|
|
428
|
+
max_tier="2", # Don't escalate beyond tier 2
|
|
429
|
+
prefer_cost=True, # Optimize for lowest cost
|
|
430
|
+
prefer_speed=True, # Optimize for fastest response
|
|
431
|
+
fail_fast=True, # Fail immediately if limits would be exceeded
|
|
432
|
+
fallback_on_block=False, # Opt-in: if a forced browser render hits a verification screen or timeout, retry once at T3/T3.5
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
result = await client.scrape("https://example.com", cost_controls=controls)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Browser actions
|
|
439
|
+
|
|
440
|
+
Run a sequence of browser actions after page load (max 20, requires `render_js=True`).
|
|
441
|
+
Build them with the typed `Actions` helpers; constraint violations raise `ValueError`
|
|
442
|
+
before the request is sent. Raw dicts such as `{"type": "click", "selector": "..."}`
|
|
443
|
+
are still accepted.
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from alterlab import Actions, AdvancedOptions
|
|
447
|
+
|
|
448
|
+
# Screenshot after actions: click, wait, then capture the resulting page
|
|
449
|
+
result = await client.scrape(
|
|
450
|
+
"https://example.com/products",
|
|
451
|
+
advanced=AdvancedOptions(render_js=True),
|
|
452
|
+
actions=[
|
|
453
|
+
Actions.dismiss_consent(),
|
|
454
|
+
Actions.click("button.load-more"),
|
|
455
|
+
Actions.wait(milliseconds=1000),
|
|
456
|
+
Actions.evaluate("document.title"),
|
|
457
|
+
Actions.screenshot(full_page=True),
|
|
458
|
+
],
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
for ar in result["action_results"] or []:
|
|
462
|
+
# Each entry is an ActionResult: a dict with typed attribute access
|
|
463
|
+
print(ar.index, ar.type, ar.success, ar.error)
|
|
464
|
+
|
|
465
|
+
title = result["action_results"][3].result
|
|
466
|
+
screenshot_url = result["action_results"][4].screenshot_url
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Available builders: `wait`, `wait_for`, `click`, `fill`, `press`, `scroll`,
|
|
470
|
+
`infinite_scroll`, `screenshot`, `evaluate`, `dismiss_consent`. Failed actions are
|
|
471
|
+
skipped; check `ar.success` and `ar.error` on each result.
|
|
472
|
+
|
|
473
|
+
### Downloading files
|
|
474
|
+
|
|
475
|
+
`Actions.download(selector=... | url=...)` captures a file download started by the
|
|
476
|
+
page (exactly one of `selector` or `url`; `timeout_ms` 1000-30000, optional
|
|
477
|
+
`max_bytes` up to 100 MiB, `frame` only with `selector`; at most 5 per request).
|
|
478
|
+
The file is stored privately and the matching action result carries
|
|
479
|
+
`ActionResult.artifact` metadata (`artifact_id`, `filename`, `media_type`,
|
|
480
|
+
`size_bytes`, `sha256`, `scan_status`, `expires_at`).
|
|
481
|
+
|
|
482
|
+
Multi-file: `max_files` (1-10) collects several files into
|
|
483
|
+
`ActionResult.artifacts`; `max_total_bytes` caps their total size, `quiet_ms`
|
|
484
|
+
(100-10000) ends collection after an idle period and `bundle="zip"` also stores
|
|
485
|
+
a ZIP, returned as `ActionResult.bundle` (the last three need `max_files > 1`).
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
result = client.scrape(
|
|
489
|
+
"https://example.com/reports",
|
|
490
|
+
advanced=AdvancedOptions(render_js=True),
|
|
491
|
+
actions=[Actions.click("#export"), Actions.download(selector="a.csv")],
|
|
492
|
+
)
|
|
493
|
+
artifact = result["action_results"][1].artifact
|
|
494
|
+
client.get_artifact(artifact.artifact_id) # metadata
|
|
495
|
+
client.get_artifact_url(artifact.artifact_id)["url"] # short-lived signed URL
|
|
496
|
+
client.download_artifact(artifact.artifact_id, "report.csv") # saves the file
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The same methods exist on `AlterLab` (awaitable) and `AlterLabSync`, and as
|
|
500
|
+
`alterlab.artifacts.get_artifact(client, id)` etc. The download is fetched
|
|
501
|
+
without your API key and written atomically (a failed download leaves no file).
|
|
502
|
+
|
|
503
|
+
## Convenience Methods
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
# HTML only (fastest, 1 credit base)
|
|
507
|
+
result = await client.scrape_html("https://example.com")
|
|
508
|
+
|
|
509
|
+
# JavaScript rendering
|
|
510
|
+
result = await client.scrape_js("https://example.com", screenshot=True)
|
|
511
|
+
|
|
512
|
+
# PDF extraction
|
|
513
|
+
result = await client.scrape_pdf("https://example.com/doc.pdf")
|
|
514
|
+
|
|
515
|
+
# OCR
|
|
516
|
+
result = await client.scrape_ocr("https://example.com/image.png", language="eng")
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Error Handling
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
from alterlab import (
|
|
523
|
+
AlterLabAPIError,
|
|
524
|
+
AlterLabTimeoutError,
|
|
525
|
+
AlterLabError,
|
|
526
|
+
AuthenticationError,
|
|
527
|
+
InsufficientFundsError,
|
|
528
|
+
RateLimitError,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
result = await client.scrape("https://example.com")
|
|
533
|
+
except AuthenticationError:
|
|
534
|
+
print("Check your API key at https://alterlab.io/dashboard/keys")
|
|
535
|
+
except InsufficientFundsError as e:
|
|
536
|
+
print(f"Add funds at: {e.billing_url}")
|
|
537
|
+
except RateLimitError as e:
|
|
538
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
539
|
+
except AlterLabAPIError as e:
|
|
540
|
+
print(f"API error {e.status_code}: {e.detail}")
|
|
541
|
+
except AlterLabTimeoutError as e:
|
|
542
|
+
print(f"Job timed out: {e}")
|
|
543
|
+
except AlterLabError as e:
|
|
544
|
+
print(f"SDK error: {e}")
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
| Exception | When |
|
|
548
|
+
|-----------|------|
|
|
549
|
+
| `AuthenticationError` | HTTP 401 — invalid or missing API key |
|
|
550
|
+
| `InsufficientFundsError` | HTTP 402 — insufficient balance (has `.billing_url`) |
|
|
551
|
+
| `RateLimitError` | HTTP 429 after retries exhausted or `max_retry_wait` reached (has `.retry_after`, `.scope`, `.limit`, `.account_tier`, `.docs_url`, `.rate_limit_*`, `.hint`; `None` for edge 429s without the canonical body). For `.error_code == "repeat_failure_backoff"` (the same request failed the same way several times in a row; no credits charged) it also has `.consecutive_failures`, `.outcome_class` and `.failure_code`: retry after `.retry_after`, with different request options, or with `force_refresh=True` |
|
|
552
|
+
| `AlterLabAPIError` | Other HTTP 4xx/5xx from the API (has `.status_code`, `.detail` and `.hint`) |
|
|
553
|
+
| `JobFailedError` | Polled job ended `failed`, `rejected` or `cancelled` (has `.job_id`, `.status`, `.hint`) |
|
|
554
|
+
| `AlterLabTimeoutError` | Async job polling exceeded timeout |
|
|
555
|
+
| `AlterLabValidationError` | Invalid parameters (e.g. `screenshot=True` without `render_js`) |
|
|
556
|
+
| `JobExpiredError` | Job result expired (1-hour TTL) — resubmit the request |
|
|
557
|
+
| `AlterLabHTTPError` | `requests`-compat `.raise_for_status()` — 4xx/5xx from the *target site*, not the API (has `.status_code`) |
|
|
558
|
+
| `AlterLabError` | Base class for all SDK errors |
|
|
559
|
+
|
|
560
|
+
All typed exceptions (`AuthenticationError`, `InsufficientFundsError`, `RateLimitError`,
|
|
561
|
+
`AlterLabHTTPError`) are subclasses of `AlterLabAPIError` — existing `except AlterLabAPIError`
|
|
562
|
+
blocks still catch them.
|
|
563
|
+
|
|
564
|
+
### Retry hints
|
|
565
|
+
|
|
566
|
+
When a request forces browser rendering (`render_js=True` or `force_tier="4"`) and the page
|
|
567
|
+
returns a verification screen or does not finish loading in time, the error carries a `hint`
|
|
568
|
+
(`{"code": "render_js_auto", "reason": "challenge" | "timeout", "message": ...}`). It is on
|
|
569
|
+
`AlterLabAPIError.hint` (failed sync scrape), `JobFailedError.hint` (failed polled job), and in
|
|
570
|
+
a failed crawl page's `error`. `get_error_hint()` reads any of them:
|
|
571
|
+
|
|
572
|
+
```python
|
|
573
|
+
from alterlab import AdvancedOptions, AlterLabAPIError, get_error_hint
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
result = await client.scrape(
|
|
577
|
+
"https://example.com", advanced=AdvancedOptions(render_js=True)
|
|
578
|
+
)
|
|
579
|
+
except AlterLabAPIError as e:
|
|
580
|
+
hint = get_error_hint(e)
|
|
581
|
+
if hint and hint["code"] == "render_js_auto":
|
|
582
|
+
print(hint["message"])
|
|
583
|
+
result = await client.scrape(
|
|
584
|
+
"https://example.com", advanced=AdvancedOptions(render_js="auto")
|
|
585
|
+
)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
For pages that only sometimes need a browser, prefer `render_js="auto"` from the start. Keep
|
|
589
|
+
`render_js=True` for `screenshot`, `generate_pdf` and browser `actions`.
|
|
590
|
+
|
|
591
|
+
## Client Options
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
client = AlterLab(
|
|
595
|
+
api_key="sk_test_...", # Optional — falls back to ALTERLAB_API_KEY env var
|
|
596
|
+
base_url="https://api.alterlab.io", # Default API URL
|
|
597
|
+
timeout=30, # Request timeout (seconds)
|
|
598
|
+
max_retries=3, # Retry count for transient failures
|
|
599
|
+
retry_delay=1.0, # Initial retry delay (jittered exponential backoff)
|
|
600
|
+
max_retry_wait=60.0, # Max total seconds spent waiting between retries (None = no cap)
|
|
601
|
+
)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
429 and 5xx responses are retried with jittered exponential backoff. A `Retry-After` header
|
|
605
|
+
is honoured as a minimum wait; if waiting it out would exceed `max_retry_wait`, the
|
|
606
|
+
`RateLimitError` is raised immediately with `.retry_after` set so you can schedule the retry.
|
|
607
|
+
|
|
608
|
+
If `api_key` is omitted, the client reads it from the `ALTERLAB_API_KEY` environment variable:
|
|
609
|
+
|
|
610
|
+
```python
|
|
611
|
+
# With ALTERLAB_API_KEY set in the environment
|
|
612
|
+
client = AlterLab()
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
A `ValueError` is raised only if neither `api_key` nor `ALTERLAB_API_KEY` is set. `AlterLabSync` accepts the same `api_key` parameter and env-var fallback.
|
|
616
|
+
|
|
617
|
+
The client automatically retries on 429 (rate limit) and 5xx errors with exponential backoff and `Retry-After` header support.
|
|
618
|
+
|
|
619
|
+
## Async Context Manager
|
|
620
|
+
|
|
621
|
+
Always use the client as a context manager to properly close connections:
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
async with AlterLab(api_key="sk_test_...") as client:
|
|
625
|
+
result = await client.scrape("https://example.com")
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Or close manually:
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
client = AlterLab(api_key="sk_test_...")
|
|
632
|
+
try:
|
|
633
|
+
result = await client.scrape("https://example.com")
|
|
634
|
+
finally:
|
|
635
|
+
await client.close()
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Billing & Cost Model
|
|
639
|
+
|
|
640
|
+
AlterLab uses a **dollar-balance** system. All values in the API are in **microcents** (1,000,000 microcents = $1.00).
|
|
641
|
+
|
|
642
|
+
### Free Trial
|
|
643
|
+
|
|
644
|
+
New accounts start with **$1.00** (1,000,000 microcents). No credit card required.
|
|
645
|
+
|
|
646
|
+
### Pricing by Tier
|
|
647
|
+
|
|
648
|
+
| Tier | Name | Cost per Request | Requests per $1 |
|
|
649
|
+
|------|------|-----------------|-----------------|
|
|
650
|
+
| 1 | Direct fetch | $0.0002 | 5,000 |
|
|
651
|
+
| 2 | Full HTTP client | $0.0003 | 3,333 |
|
|
652
|
+
| 3 | Managed sessions | $0.002 | 500 |
|
|
653
|
+
| 4 | Full browser rendering | $0.004 | 250 |
|
|
654
|
+
| 5 | Extended browser sessions | $0.02 | 50 |
|
|
655
|
+
|
|
656
|
+
Current prices: [alterlab.io/pricing](https://alterlab.io/pricing). Measured results: [alterlab.io/benchmark](https://alterlab.io/benchmark).
|
|
657
|
+
|
|
658
|
+
### Checking Your Balance
|
|
659
|
+
|
|
660
|
+
```python
|
|
661
|
+
usage = await client.get_usage()
|
|
662
|
+
balance_usd = usage['balance_microcents'] / 1_000_000
|
|
663
|
+
print(f"${balance_usd:.4f} remaining")
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Adding Balance
|
|
667
|
+
|
|
668
|
+
Add funds from the [Dashboard Billing page](https://alterlab.io/dashboard/billing).
|
|
669
|
+
|
|
670
|
+
## Links
|
|
671
|
+
|
|
672
|
+
- [Dashboard](https://alterlab.io/dashboard) — Get your API key
|
|
673
|
+
- [API Documentation](https://alterlab.io/docs) — Full API reference
|
|
674
|
+
- [npm package](https://www.npmjs.com/package/alterlab) — Node.js SDK
|
|
675
|
+
|