alterlab 2.1.0__tar.gz → 2.2.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.
- {alterlab-2.1.0 → alterlab-2.2.0}/PKG-INFO +1 -1
- {alterlab-2.1.0 → alterlab-2.2.0}/alterlab/__init__.py +1 -1
- {alterlab-2.1.0 → alterlab-2.2.0}/alterlab/client.py +745 -20
- alterlab-2.2.0/alterlab/types.py +93 -0
- {alterlab-2.1.0 → alterlab-2.2.0}/pyproject.toml +1 -1
- alterlab-2.1.0/alterlab/types.py +0 -48
- {alterlab-2.1.0 → alterlab-2.2.0}/alterlab/crypto.py +0 -0
- {alterlab-2.1.0 → alterlab-2.2.0}/alterlab/resources/__init__.py +0 -0
- {alterlab-2.1.0 → alterlab-2.2.0}/alterlab/resources/sessions.py +0 -0
|
@@ -41,7 +41,7 @@ from alterlab.client import (AdvancedOptions, AlterLab, AlterLabAPIError,
|
|
|
41
41
|
from alterlab.types import (SessionIntegration, SessionListResponse,
|
|
42
42
|
SessionValidation)
|
|
43
43
|
|
|
44
|
-
__version__ = "2.1.
|
|
44
|
+
__version__ = "2.1.1"
|
|
45
45
|
__all__ = [
|
|
46
46
|
"AlterLab",
|
|
47
47
|
"AlterLabSync",
|
|
@@ -11,93 +11,302 @@ This SDK provides full feature parity with the AlterLab API including:
|
|
|
11
11
|
- Batch scraping with webhooks
|
|
12
12
|
- Comprehensive error handling with retries
|
|
13
13
|
|
|
14
|
-
Version: 2.1.
|
|
14
|
+
Version: 2.1.1 (Adds X-AlterLab-Source header for ecosystem attribution)
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import logging
|
|
19
19
|
import time
|
|
20
20
|
import warnings
|
|
21
|
-
from typing import Any, Dict, List, Literal, Optional, Union
|
|
21
|
+
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
|
|
22
22
|
from urllib.parse import urljoin
|
|
23
23
|
|
|
24
24
|
import httpx
|
|
25
25
|
from alterlab.resources.sessions import AsyncSessions, SyncSessions
|
|
26
26
|
|
|
27
|
+
_SDK_VERSION = "2.1.1"
|
|
28
|
+
|
|
27
29
|
logger = logging.getLogger("alterlab")
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Response type hints (TypedDicts for IDE/type-checker support)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TierEscalationDetail(Dict[str, Any]):
|
|
38
|
+
"""Details of a single tier escalation attempt within a scrape request.
|
|
39
|
+
|
|
40
|
+
These fields are internal and may not appear in all responses.
|
|
41
|
+
|
|
42
|
+
Fields:
|
|
43
|
+
tier: Tier identifier (e.g. "1", "2", "3", "3.5", "4").
|
|
44
|
+
result: Outcome — ``"success"``, ``"failed"``, or ``"skipped"``.
|
|
45
|
+
credits: Credits charged for this tier attempt (microcents).
|
|
46
|
+
duration_ms: Time taken for this tier attempt in milliseconds.
|
|
47
|
+
error: Error message if the tier attempt failed.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BillingDetails(Dict[str, Any]):
|
|
52
|
+
"""Detailed billing breakdown returned in every scrape response.
|
|
53
|
+
|
|
54
|
+
Accessed via ``response["billing"]``.
|
|
55
|
+
|
|
56
|
+
Fields:
|
|
57
|
+
total_credits: Total credits charged for the request (microcents).
|
|
58
|
+
tier_used: Scraping tier that produced the final result (e.g. "1", "4").
|
|
59
|
+
escalations: List of :class:`TierEscalationDetail` for each tier attempted.
|
|
60
|
+
Internal — only present when ``x-internal`` fields are not filtered.
|
|
61
|
+
savings: Credits saved vs. always using the highest tier. Internal.
|
|
62
|
+
optimization_suggestion: Advisory on how to reduce cost next time. Internal.
|
|
63
|
+
byop_applied: Whether a Bring-Your-Own-Proxy discount was applied.
|
|
64
|
+
byop_discount_percent: Discount percentage applied (e.g. ``98.4``).
|
|
65
|
+
original_cost_microcents: Cost before BYOP discount in microcents.
|
|
66
|
+
final_cost_microcents: Cost after BYOP discount in microcents.
|
|
67
|
+
byos_applied: Whether Bring-Your-Own-Session cookies were injected.
|
|
68
|
+
byos_session_name: Name of the stored session used (e.g. ``"My Amazon Prime"``).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Valid keys and content types for section_filter validation.
|
|
73
|
+
_SECTION_FILTER_VALID_KEYS = {
|
|
74
|
+
"min_content_blocks",
|
|
75
|
+
"exclude_content_types",
|
|
76
|
+
"content_only",
|
|
77
|
+
}
|
|
78
|
+
_SECTION_FILTER_VALID_CONTENT_TYPES = {
|
|
79
|
+
"paragraph",
|
|
80
|
+
"list",
|
|
81
|
+
"table",
|
|
82
|
+
"image",
|
|
83
|
+
"code",
|
|
84
|
+
"blockquote",
|
|
85
|
+
"dl",
|
|
86
|
+
"details",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
30
90
|
class AdvancedOptions:
|
|
31
91
|
"""Advanced scraping options with credit costs.
|
|
32
92
|
|
|
33
93
|
Credit Costs:
|
|
34
|
-
- render_js:
|
|
94
|
+
- render_js: Forces Tier 4 minimum (browser rendering) — Tier 4 cost applies, no separate add-on charge
|
|
35
95
|
- screenshot: +1 credit (requires render_js)
|
|
36
96
|
- generate_pdf: +2 credits (requires render_js)
|
|
37
97
|
- ocr: +5 credits (refunded if no images found)
|
|
38
98
|
- use_proxy: +1 credit
|
|
39
99
|
- markdown: Free
|
|
40
100
|
- wait_condition: Free
|
|
101
|
+
- remove_cookie_banners: Free
|
|
41
102
|
"""
|
|
42
103
|
|
|
43
104
|
def __init__(
|
|
44
105
|
self,
|
|
45
|
-
render_js: bool = False,
|
|
106
|
+
render_js: Union[bool, Literal["auto"]] = False,
|
|
46
107
|
screenshot: bool = False,
|
|
47
108
|
markdown: bool = False,
|
|
48
109
|
generate_pdf: bool = False,
|
|
49
110
|
ocr: bool = False,
|
|
50
111
|
use_proxy: bool = False,
|
|
112
|
+
use_own_proxy: bool = False,
|
|
113
|
+
use_system_proxy: bool = False,
|
|
114
|
+
proxy_integration_id: Optional[str] = None,
|
|
115
|
+
proxy_country: Optional[str] = None,
|
|
116
|
+
remove_cookie_banners: bool = True,
|
|
51
117
|
wait_condition: str = "networkidle",
|
|
118
|
+
scroll_to_load: bool = False,
|
|
119
|
+
scroll_count: int = 3,
|
|
120
|
+
include_iframes: bool = False,
|
|
121
|
+
flatten_shadow_dom: bool = False,
|
|
122
|
+
section_filter: Optional[Dict[str, Any]] = None,
|
|
123
|
+
session_headers: Optional[Dict[str, str]] = None,
|
|
52
124
|
):
|
|
53
125
|
"""Initialize advanced options.
|
|
54
126
|
|
|
55
127
|
Args:
|
|
56
|
-
render_js: Render JavaScript using headless browser (
|
|
57
|
-
|
|
128
|
+
render_js: Render JavaScript using headless browser (forces Tier 4 minimum — no separate
|
|
129
|
+
add-on charge, Tier 4 cost applies). Set to ``"auto"`` for smart detection: probes
|
|
130
|
+
each page and only renders JS-heavy pages with a browser, using fast HTTP for static
|
|
131
|
+
pages. Saves 30–60% on mixed sites. Note: screenshot and generate_pdf require
|
|
132
|
+
``render_js=True`` (not ``"auto"``), since they need guaranteed browser rendering.
|
|
133
|
+
screenshot: Capture full-page screenshot (+1 credit, requires render_js=True)
|
|
58
134
|
markdown: Extract content as Markdown (free)
|
|
59
|
-
generate_pdf: Generate PDF of rendered page (+2 credits, requires render_js)
|
|
135
|
+
generate_pdf: Generate PDF of rendered page (+2 credits, requires render_js=True)
|
|
60
136
|
ocr: Extract text from images using OCR (+5 credits)
|
|
61
137
|
use_proxy: Route request through premium proxy (+1 credit)
|
|
138
|
+
use_own_proxy: Use your own integrated proxy instead of the system proxy
|
|
139
|
+
use_system_proxy: Override your default proxy integration and use AlterLab's system
|
|
140
|
+
proxy instead
|
|
141
|
+
proxy_integration_id: Specific proxy integration ID to use (requires
|
|
142
|
+
use_own_proxy=True)
|
|
143
|
+
proxy_country: Preferred proxy country code (e.g., ``"US"``, ``"DE"``) for
|
|
144
|
+
geo-targeting
|
|
145
|
+
remove_cookie_banners: Remove cookie consent banners from HTML before content
|
|
146
|
+
extraction (free, enabled by default)
|
|
62
147
|
wait_condition: Wait condition for JS rendering (domcontentloaded|networkidle|load)
|
|
148
|
+
scroll_to_load: Scroll page to trigger lazy-loaded content (requires render_js)
|
|
149
|
+
scroll_count: Number of scroll iterations when scroll_to_load is enabled (1-10)
|
|
150
|
+
include_iframes: Inline iframe content into the main document (free, requires render_js).
|
|
151
|
+
Same-origin iframes are read directly; cross-origin iframes are marked but not fetched.
|
|
152
|
+
flatten_shadow_dom: Flatten Shadow DOM roots into regular DOM for extraction (free,
|
|
153
|
+
requires render_js). Web Components with shadow roots become visible in the
|
|
154
|
+
serialized HTML.
|
|
155
|
+
section_filter: Filter options for json_v2 section tree output. Dict with optional keys:
|
|
156
|
+
- min_content_blocks (int): Minimum content blocks for a section (default 0)
|
|
157
|
+
- exclude_content_types (list[str]): Content types to exclude
|
|
158
|
+
(paragraph, list, table, image, code, blockquote, dl, details)
|
|
159
|
+
- content_only (bool): Strip headings, keep only content (default False)
|
|
160
|
+
session_headers: Inline auth headers (e.g. {"Authorization": "Bearer token"}).
|
|
161
|
+
Merged with session headers when session_id is also provided.
|
|
63
162
|
|
|
64
163
|
Raises:
|
|
65
|
-
ValueError: If
|
|
164
|
+
ValueError: If render_js is not a bool or the string ``"auto"``
|
|
165
|
+
ValueError: If screenshot or generate_pdf is True but render_js is not True
|
|
166
|
+
ValueError: If scroll_to_load is True but render_js is falsy
|
|
167
|
+
ValueError: If include_iframes or flatten_shadow_dom is True but render_js is falsy
|
|
168
|
+
ValueError: If scroll_count is not between 1 and 10
|
|
169
|
+
ValueError: If section_filter contains unknown keys or invalid value types
|
|
66
170
|
"""
|
|
67
|
-
if
|
|
68
|
-
raise ValueError(
|
|
69
|
-
if
|
|
70
|
-
raise ValueError(
|
|
171
|
+
if render_js not in (True, False, "auto"):
|
|
172
|
+
raise ValueError('render_js must be True, False, or "auto"')
|
|
173
|
+
if screenshot and render_js is not True:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"screenshot requires render_js=True "
|
|
176
|
+
'(render_js="auto" is not sufficient — screenshot needs guaranteed browser rendering)'
|
|
177
|
+
)
|
|
178
|
+
if generate_pdf and render_js is not True:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"generate_pdf requires render_js=True "
|
|
181
|
+
'(render_js="auto" is not sufficient — PDF generation needs guaranteed browser rendering)'
|
|
182
|
+
)
|
|
183
|
+
if scroll_to_load and not render_js:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"scroll_to_load requires render_js=True or render_js='auto'"
|
|
186
|
+
)
|
|
187
|
+
if include_iframes and not render_js:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
"include_iframes requires render_js=True or render_js='auto'"
|
|
190
|
+
)
|
|
191
|
+
if flatten_shadow_dom and not render_js:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
"flatten_shadow_dom requires render_js=True or render_js='auto'"
|
|
194
|
+
)
|
|
71
195
|
|
|
72
196
|
valid_conditions = ["domcontentloaded", "networkidle", "load"]
|
|
73
197
|
if wait_condition not in valid_conditions:
|
|
74
198
|
raise ValueError(f"wait_condition must be one of {valid_conditions}")
|
|
75
199
|
|
|
200
|
+
if not 1 <= scroll_count <= 10:
|
|
201
|
+
raise ValueError("scroll_count must be between 1 and 10")
|
|
202
|
+
|
|
76
203
|
self.render_js = render_js
|
|
77
204
|
self.screenshot = screenshot
|
|
78
205
|
self.markdown = markdown
|
|
79
206
|
self.generate_pdf = generate_pdf
|
|
80
207
|
self.ocr = ocr
|
|
81
208
|
self.use_proxy = use_proxy
|
|
209
|
+
self.use_own_proxy = use_own_proxy
|
|
210
|
+
self.use_system_proxy = use_system_proxy
|
|
211
|
+
self.proxy_integration_id = proxy_integration_id
|
|
212
|
+
self.proxy_country = proxy_country
|
|
213
|
+
self.remove_cookie_banners = remove_cookie_banners
|
|
82
214
|
self.wait_condition = wait_condition
|
|
215
|
+
self.scroll_to_load = scroll_to_load
|
|
216
|
+
self.scroll_count = scroll_count
|
|
217
|
+
self.include_iframes = include_iframes
|
|
218
|
+
self.flatten_shadow_dom = flatten_shadow_dom
|
|
219
|
+
self.session_headers = session_headers
|
|
220
|
+
|
|
221
|
+
if section_filter is not None:
|
|
222
|
+
self._validate_section_filter(section_filter)
|
|
223
|
+
self.section_filter = section_filter
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _validate_section_filter(sf: Dict[str, Any]) -> None:
|
|
227
|
+
"""Validate section_filter keys and value types.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValueError: If any key is not in the allowed set, or if value
|
|
231
|
+
types do not match the expected schema.
|
|
232
|
+
"""
|
|
233
|
+
if not isinstance(sf, dict):
|
|
234
|
+
raise ValueError("section_filter must be a dict")
|
|
235
|
+
|
|
236
|
+
unknown_keys = set(sf.keys()) - _SECTION_FILTER_VALID_KEYS
|
|
237
|
+
if unknown_keys:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"section_filter contains unknown keys: {sorted(unknown_keys)}. "
|
|
240
|
+
f"Allowed keys: {sorted(_SECTION_FILTER_VALID_KEYS)}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if "min_content_blocks" in sf:
|
|
244
|
+
val = sf["min_content_blocks"]
|
|
245
|
+
if not isinstance(val, int) or isinstance(val, bool) or val < 0:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"section_filter['min_content_blocks'] must be a non-negative integer"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if "exclude_content_types" in sf:
|
|
251
|
+
val = sf["exclude_content_types"]
|
|
252
|
+
if not isinstance(val, (list, tuple)):
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"section_filter['exclude_content_types'] must be a list of strings"
|
|
255
|
+
)
|
|
256
|
+
invalid = set(val) - _SECTION_FILTER_VALID_CONTENT_TYPES
|
|
257
|
+
if invalid:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"section_filter['exclude_content_types'] contains unknown types: "
|
|
260
|
+
f"{sorted(invalid)}. "
|
|
261
|
+
f"Allowed: {sorted(_SECTION_FILTER_VALID_CONTENT_TYPES)}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if "content_only" in sf:
|
|
265
|
+
val = sf["content_only"]
|
|
266
|
+
if not isinstance(val, bool):
|
|
267
|
+
raise ValueError("section_filter['content_only'] must be a bool")
|
|
83
268
|
|
|
84
269
|
def to_dict(self) -> Dict[str, Any]:
|
|
85
270
|
"""Convert to dictionary for API request."""
|
|
86
|
-
|
|
271
|
+
result: Dict[str, Any] = {
|
|
87
272
|
"render_js": self.render_js,
|
|
88
273
|
"screenshot": self.screenshot,
|
|
89
274
|
"markdown": self.markdown,
|
|
90
275
|
"generate_pdf": self.generate_pdf,
|
|
91
276
|
"ocr": self.ocr,
|
|
92
277
|
"use_proxy": self.use_proxy,
|
|
278
|
+
"use_own_proxy": self.use_own_proxy,
|
|
279
|
+
"use_system_proxy": self.use_system_proxy,
|
|
280
|
+
"remove_cookie_banners": self.remove_cookie_banners,
|
|
93
281
|
"wait_condition": self.wait_condition,
|
|
282
|
+
"scroll_to_load": self.scroll_to_load,
|
|
283
|
+
"scroll_count": self.scroll_count,
|
|
284
|
+
"include_iframes": self.include_iframes,
|
|
285
|
+
"flatten_shadow_dom": self.flatten_shadow_dom,
|
|
94
286
|
}
|
|
287
|
+
if self.scroll_to_load:
|
|
288
|
+
result["scroll_to_load"] = True
|
|
289
|
+
result["scroll_count"] = self.scroll_count
|
|
290
|
+
if self.section_filter is not None:
|
|
291
|
+
result["section_filter"] = self.section_filter
|
|
292
|
+
if self.session_headers is not None:
|
|
293
|
+
result["session_headers"] = self.session_headers
|
|
294
|
+
if self.proxy_integration_id is not None:
|
|
295
|
+
result["proxy_integration_id"] = self.proxy_integration_id
|
|
296
|
+
if self.proxy_country is not None:
|
|
297
|
+
result["proxy_country"] = self.proxy_country
|
|
298
|
+
return result
|
|
95
299
|
|
|
96
300
|
def calculate_credits(self) -> int:
|
|
97
|
-
"""Calculate additional credits for advanced options.
|
|
301
|
+
"""Calculate additional credits for advanced options.
|
|
302
|
+
|
|
303
|
+
Note: render_js="auto" does not add upfront credits — actual cost depends on
|
|
304
|
+
per-page decision (browser vs. HTTP). Treat as 0 for estimation purposes.
|
|
305
|
+
"""
|
|
98
306
|
credits = 0
|
|
99
|
-
if self.render_js:
|
|
307
|
+
if self.render_js is True:
|
|
100
308
|
credits += 3
|
|
309
|
+
# render_js="auto" doesn't add credits upfront — actual cost depends on per-page decision
|
|
101
310
|
if self.screenshot:
|
|
102
311
|
credits += 1
|
|
103
312
|
if self.generate_pdf:
|
|
@@ -127,7 +336,7 @@ class CostControls:
|
|
|
127
336
|
|
|
128
337
|
Args:
|
|
129
338
|
max_credits: Maximum credits to spend on this request
|
|
130
|
-
max_tier: Maximum tier to escalate to (
|
|
339
|
+
max_tier: Maximum tier to escalate to (1, 2, 3, 3.5, 4)
|
|
131
340
|
prefer_cost: Optimize for cost (try cheaper tiers first)
|
|
132
341
|
prefer_speed: Optimize for speed (skip to reliable tier)
|
|
133
342
|
fail_fast: Return error instead of escalating to expensive tiers
|
|
@@ -135,7 +344,7 @@ class CostControls:
|
|
|
135
344
|
Raises:
|
|
136
345
|
ValueError: If max_tier is not a valid tier value
|
|
137
346
|
"""
|
|
138
|
-
valid_tiers = ["
|
|
347
|
+
valid_tiers = ["1", "2", "3", "3.5", "4"]
|
|
139
348
|
if max_tier and max_tier not in valid_tiers:
|
|
140
349
|
raise ValueError(f"max_tier must be one of {valid_tiers}")
|
|
141
350
|
|
|
@@ -262,7 +471,8 @@ class AlterLab:
|
|
|
262
471
|
base_url=self.base_url,
|
|
263
472
|
headers={
|
|
264
473
|
"X-API-Key": api_key,
|
|
265
|
-
"User-Agent": "AlterLab-Python-SDK/
|
|
474
|
+
"User-Agent": f"AlterLab-Python-SDK/{_SDK_VERSION}",
|
|
475
|
+
"X-AlterLab-Source": f"python-sdk/{_SDK_VERSION}",
|
|
266
476
|
"Content-Type": "application/json",
|
|
267
477
|
},
|
|
268
478
|
timeout=httpx.Timeout(timeout),
|
|
@@ -417,6 +627,13 @@ class AlterLab:
|
|
|
417
627
|
ocr_language: str = "eng",
|
|
418
628
|
session_id: Optional[str] = None,
|
|
419
629
|
cookies: Optional[Dict[str, str]] = None,
|
|
630
|
+
actions: Optional[List[Dict[str, Any]]] = None,
|
|
631
|
+
headers: Optional[Dict[str, str]] = None,
|
|
632
|
+
location: Optional[Dict[str, str]] = None,
|
|
633
|
+
cache: bool = False,
|
|
634
|
+
cache_ttl: Optional[int] = None,
|
|
635
|
+
webhook_url: Optional[str] = None,
|
|
636
|
+
max_response_bytes: Optional[int] = None,
|
|
420
637
|
poll_interval: float = 1.0,
|
|
421
638
|
poll_timeout: float = 300.0,
|
|
422
639
|
poll_backoff_multiplier: float = 1.5,
|
|
@@ -452,18 +669,76 @@ class AlterLab:
|
|
|
452
669
|
Mutually exclusive with ``cookies``.
|
|
453
670
|
cookies: Inline cookie dict for one-off authenticated scraping (not stored).
|
|
454
671
|
Mutually exclusive with ``session_id``.
|
|
672
|
+
actions: Browser automation sequence to execute after page load (max 20 actions).
|
|
673
|
+
Requires ``advanced.render_js=True``. Each action is a dict with a ``type``
|
|
674
|
+
discriminator. Supported types: ``wait``, ``wait_for``, ``click``, ``fill``,
|
|
675
|
+
``press``, ``scroll``, ``infinite_scroll``, ``screenshot``, ``evaluate``.
|
|
676
|
+
Actions execute sequentially; failed actions are skipped. Results returned in
|
|
677
|
+
``action_results``. Examples::
|
|
678
|
+
|
|
679
|
+
[
|
|
680
|
+
{"type": "wait", "milliseconds": 1000},
|
|
681
|
+
{"type": "click", "selector": "button.load-more"},
|
|
682
|
+
{"type": "scroll", "direction": "down", "amount": 3},
|
|
683
|
+
{"type": "evaluate", "script": "document.title"},
|
|
684
|
+
]
|
|
685
|
+
|
|
686
|
+
headers: Custom HTTP request headers to forward to the target URL (max 50 headers).
|
|
687
|
+
Header names must be 1-256 characters and header values must be at most 8192
|
|
688
|
+
characters. Hop-by-hop and connection-control headers (e.g. ``Connection``,
|
|
689
|
+
``Transfer-Encoding``) are rejected by the API. Example::
|
|
690
|
+
|
|
691
|
+
{"Authorization": "Bearer token", "X-Custom-Header": "value"}
|
|
692
|
+
|
|
693
|
+
location: Geo-targeting parameters. Dict with optional keys:
|
|
694
|
+
- ``country`` (str): ISO 3166-1 alpha-2 country code (e.g., ``"DE"``, ``"JP"``).
|
|
695
|
+
Routes the request through a proxy in that country.
|
|
696
|
+
- ``language`` (str): ISO 639-1 language code (e.g., ``"de"``, ``"ja"``).
|
|
697
|
+
Sets the ``Accept-Language`` header and browser locale.
|
|
698
|
+
Example: ``{"country": "DE", "language": "de"}``
|
|
699
|
+
cache: Enable response caching. When ``True``, a cached result may be returned if
|
|
700
|
+
available. Defaults to ``False`` (no caching). Use ``force_refresh=True`` to bypass
|
|
701
|
+
an existing cache entry.
|
|
702
|
+
cache_ttl: Cache TTL in seconds (60–86400). Only applies when ``cache=True``.
|
|
703
|
+
Defaults to 3600 (1 hour) server-side when not specified.
|
|
704
|
+
webhook_url: URL to receive a ``job.completed`` or ``job.failed`` event when the
|
|
705
|
+
scrape finishes. This is a server-side notification side-effect and does not
|
|
706
|
+
affect client-side polling behavior — use ``sync=False`` to skip polling.
|
|
707
|
+
HTTPS is required in production (``http://localhost`` is allowed in dev).
|
|
708
|
+
max_response_bytes: Soft cap on raw response body size in bytes. When the
|
|
709
|
+
downloaded HTML exceeds this value it is truncated before extraction and
|
|
710
|
+
``content_truncated.truncation_reason`` is set to ``'response_body_cap'``.
|
|
711
|
+
Default: ``None`` (server default of 5 MB applies). Set to ``0`` for no
|
|
712
|
+
limit. Maximum: 50 MB (52_428_800).
|
|
455
713
|
poll_interval: Initial polling interval for async jobs (seconds)
|
|
456
714
|
poll_timeout: Maximum time to wait for async jobs (seconds)
|
|
457
715
|
poll_backoff_multiplier: Multiplier for exponential backoff (default: 1.5)
|
|
458
716
|
poll_max_interval: Maximum polling interval (seconds)
|
|
459
717
|
|
|
460
718
|
Returns:
|
|
461
|
-
Scrape response dictionary with content, metadata, and billing details
|
|
719
|
+
Scrape response dictionary with content, metadata, and billing details.
|
|
720
|
+
Key fields include:
|
|
721
|
+
- ``url``, ``status_code``, ``content``, ``headers``, ``cached``
|
|
722
|
+
- ``credits_used``, ``response_time_ms``, ``size_bytes``
|
|
723
|
+
- ``billing`` (:class:`BillingDetails`): Full billing breakdown including
|
|
724
|
+
``total_credits``, ``tier_used``, ``escalations``
|
|
725
|
+
(``List[TierEscalationDetail]`` — each entry has ``tier``, ``result``,
|
|
726
|
+
``credits: int``, ``duration_ms: Optional[int]``, ``error:
|
|
727
|
+
Optional[str]``), ``byop_applied``, and BYOP/BYOS discount fields.
|
|
728
|
+
- ``warning`` (Optional[str]): Actionable warning when auto-escalation
|
|
729
|
+
occurred, tier cap prevented escalation on thin content, or extraction
|
|
730
|
+
quality is low.
|
|
731
|
+
- ``content_quality`` (Optional[Dict]): Content quality metadata from
|
|
732
|
+
scrape diagnostics (e.g., word count, image count, quality score).
|
|
733
|
+
- ``note`` (Optional[str]): Advisory message (e.g. when repeat-request
|
|
734
|
+
detection identifies identical content across retries).
|
|
462
735
|
|
|
463
736
|
Raises:
|
|
464
737
|
AlterLabAPIError: On API errors
|
|
465
738
|
AlterLabTimeoutError: If async job polling times out
|
|
466
739
|
AlterLabValidationError: If parameters are invalid
|
|
740
|
+
AlterLabValidationError: If actions is provided but advanced.render_js is not set
|
|
741
|
+
AlterLabValidationError: If more than 20 actions are provided
|
|
467
742
|
|
|
468
743
|
Example:
|
|
469
744
|
# Simple HTML scraping
|
|
@@ -499,6 +774,33 @@ class AlterLab:
|
|
|
499
774
|
"https://amazon.com/dp/B0XXXXX",
|
|
500
775
|
session_id="uuid-of-stored-session"
|
|
501
776
|
)
|
|
777
|
+
|
|
778
|
+
# Geo-targeted scraping — retrieve German content via DE proxy
|
|
779
|
+
result = await client.scrape(
|
|
780
|
+
"https://example.com",
|
|
781
|
+
location={"country": "DE", "language": "de"}
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Smart JS detection — renders only JS-heavy pages (30-60% cost savings)
|
|
785
|
+
result = await client.scrape(
|
|
786
|
+
"https://example.com",
|
|
787
|
+
advanced=AdvancedOptions(render_js="auto")
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Cache-enabled scrape with custom TTL
|
|
791
|
+
result = await client.scrape(
|
|
792
|
+
"https://example.com",
|
|
793
|
+
cache=True,
|
|
794
|
+
cache_ttl=7200 # 2-hour cache
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
# Async scrape with webhook delivery (skips polling)
|
|
798
|
+
result = await client.scrape(
|
|
799
|
+
"https://example.com",
|
|
800
|
+
sync=False,
|
|
801
|
+
webhook_url="https://myapp.com/hooks/scrape-done"
|
|
802
|
+
)
|
|
803
|
+
# result contains {"job_id": "..."} — webhook delivers final result
|
|
502
804
|
"""
|
|
503
805
|
if session_id and cookies:
|
|
504
806
|
raise AlterLabValidationError(
|
|
@@ -506,6 +808,27 @@ class AlterLab:
|
|
|
506
808
|
"Use session_id to reference a stored session OR "
|
|
507
809
|
"provide inline cookies, not both."
|
|
508
810
|
)
|
|
811
|
+
if actions is not None:
|
|
812
|
+
if not advanced or not advanced.render_js:
|
|
813
|
+
raise AlterLabValidationError(
|
|
814
|
+
"actions requires advanced.render_js=True or render_js='auto' "
|
|
815
|
+
"(browser rendering must be enabled to execute browser actions)"
|
|
816
|
+
)
|
|
817
|
+
if len(actions) > 20:
|
|
818
|
+
raise AlterLabValidationError(
|
|
819
|
+
f"actions must contain at most 20 items, got {len(actions)}"
|
|
820
|
+
)
|
|
821
|
+
if screenshot and (advanced is None or advanced.render_js is not True):
|
|
822
|
+
raise AlterLabValidationError(
|
|
823
|
+
"screenshot requires render_js=True. "
|
|
824
|
+
"Pass advanced=AdvancedOptions(render_js=True, screenshot=True) "
|
|
825
|
+
"or use scrape_js(screenshot=True) instead."
|
|
826
|
+
)
|
|
827
|
+
if cache_ttl is not None and not cache:
|
|
828
|
+
raise AlterLabValidationError(
|
|
829
|
+
"cache_ttl requires cache=True. "
|
|
830
|
+
"Pass cache=True to enable caching, or omit cache_ttl."
|
|
831
|
+
)
|
|
509
832
|
|
|
510
833
|
# Build request payload
|
|
511
834
|
payload: Dict[str, Any] = {
|
|
@@ -530,6 +853,8 @@ class AlterLab:
|
|
|
530
853
|
payload["advanced"]["session_id"] = session_id
|
|
531
854
|
if cookies:
|
|
532
855
|
payload["advanced"]["cookies"] = cookies
|
|
856
|
+
if actions:
|
|
857
|
+
payload["advanced"]["actions"] = actions
|
|
533
858
|
|
|
534
859
|
# Remove empty advanced block
|
|
535
860
|
if not payload["advanced"]:
|
|
@@ -565,6 +890,28 @@ class AlterLab:
|
|
|
565
890
|
if mode == "ocr":
|
|
566
891
|
payload["ocr_language"] = ocr_language
|
|
567
892
|
|
|
893
|
+
# Custom request headers forwarded to target URL
|
|
894
|
+
if headers is not None:
|
|
895
|
+
payload["headers"] = headers
|
|
896
|
+
|
|
897
|
+
# Geo-targeting
|
|
898
|
+
if location is not None:
|
|
899
|
+
payload["location"] = location
|
|
900
|
+
|
|
901
|
+
# Cache control
|
|
902
|
+
if cache:
|
|
903
|
+
payload["cache"] = cache
|
|
904
|
+
if cache_ttl is not None:
|
|
905
|
+
payload["cache_ttl"] = cache_ttl
|
|
906
|
+
|
|
907
|
+
# Webhook URL for async notification
|
|
908
|
+
if webhook_url is not None:
|
|
909
|
+
payload["webhook_url"] = webhook_url
|
|
910
|
+
|
|
911
|
+
# Response body size cap
|
|
912
|
+
if max_response_bytes is not None:
|
|
913
|
+
payload["max_response_bytes"] = max_response_bytes
|
|
914
|
+
|
|
568
915
|
# Make request
|
|
569
916
|
response = await self._request_with_retry(
|
|
570
917
|
"POST", "/api/v1/scrape", json=payload
|
|
@@ -575,7 +922,10 @@ class AlterLab:
|
|
|
575
922
|
# Check if response is async (202 Accepted with job_id)
|
|
576
923
|
if response.status_code == 202 and "job_id" in result:
|
|
577
924
|
if not sync:
|
|
578
|
-
# Return job info immediately
|
|
925
|
+
# Return job info immediately in async mode.
|
|
926
|
+
# When webhook_url is also provided alongside sync=True, the server
|
|
927
|
+
# delivers the result via webhook as a side-effect, but polling still
|
|
928
|
+
# proceeds so the caller receives the scraped content directly.
|
|
579
929
|
return result
|
|
580
930
|
|
|
581
931
|
# Poll for job completion with exponential backoff
|
|
@@ -1080,6 +1430,91 @@ class AlterLab:
|
|
|
1080
1430
|
|
|
1081
1431
|
return response.json()
|
|
1082
1432
|
|
|
1433
|
+
async def crawl_stream(
|
|
1434
|
+
self,
|
|
1435
|
+
crawl_id: str,
|
|
1436
|
+
):
|
|
1437
|
+
"""Stream real-time crawl events via Server-Sent Events.
|
|
1438
|
+
|
|
1439
|
+
Returns an async iterator that yields parsed SSE events as
|
|
1440
|
+
dictionaries with ``event`` (str) and ``data`` (dict) keys.
|
|
1441
|
+
|
|
1442
|
+
Event types:
|
|
1443
|
+
- ``page.completed`` — per-page result with metadata, cost, tier
|
|
1444
|
+
- ``page.failed`` — per-page failure with error reason
|
|
1445
|
+
- ``crawl.progress`` — periodic summary (pages done/total, cost)
|
|
1446
|
+
- ``crawl.completed`` — final summary; iteration stops after this
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
crawl_id: Crawl ID returned from crawl()
|
|
1450
|
+
|
|
1451
|
+
Yields:
|
|
1452
|
+
Dict with 'event' (str) and 'data' (dict) keys
|
|
1453
|
+
|
|
1454
|
+
Raises:
|
|
1455
|
+
AlterLabAPIError: On HTTP errors (404 if crawl not found)
|
|
1456
|
+
|
|
1457
|
+
Example:
|
|
1458
|
+
crawl = await client.crawl("https://example.com", max_pages=10)
|
|
1459
|
+
async for event in client.crawl_stream(crawl["crawl_id"]):
|
|
1460
|
+
if event["event"] == "page.completed":
|
|
1461
|
+
print(f"Scraped: {event['data']['url']}")
|
|
1462
|
+
elif event["event"] == "crawl.completed":
|
|
1463
|
+
print(f"Done: {event['data']['completed']}/{event['data']['total']}")
|
|
1464
|
+
"""
|
|
1465
|
+
import json as _json
|
|
1466
|
+
|
|
1467
|
+
url = f"/api/v1/crawl/{crawl_id}/stream"
|
|
1468
|
+
headers = {
|
|
1469
|
+
"X-API-Key": self._api_key,
|
|
1470
|
+
"Accept": "text/event-stream",
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async with httpx.AsyncClient(
|
|
1474
|
+
base_url=self._base_url, timeout=None
|
|
1475
|
+
) as stream_client:
|
|
1476
|
+
async with stream_client.stream("GET", url, headers=headers) as response:
|
|
1477
|
+
if response.status_code != 200:
|
|
1478
|
+
body = await response.aread()
|
|
1479
|
+
raise AlterLabAPIError(
|
|
1480
|
+
f"SSE stream failed: {response.status_code}",
|
|
1481
|
+
status_code=response.status_code,
|
|
1482
|
+
response_body=body.decode("utf-8", errors="replace"),
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
current_event = "message"
|
|
1486
|
+
data_lines: list[str] = []
|
|
1487
|
+
|
|
1488
|
+
async for raw_line in response.aiter_lines():
|
|
1489
|
+
line = raw_line.rstrip("\n").rstrip("\r")
|
|
1490
|
+
|
|
1491
|
+
# Comment or keepalive
|
|
1492
|
+
if line.startswith(":"):
|
|
1493
|
+
continue
|
|
1494
|
+
|
|
1495
|
+
# Empty line = event boundary
|
|
1496
|
+
if not line:
|
|
1497
|
+
if data_lines:
|
|
1498
|
+
data_str = "\n".join(data_lines)
|
|
1499
|
+
try:
|
|
1500
|
+
parsed = _json.loads(data_str)
|
|
1501
|
+
except _json.JSONDecodeError:
|
|
1502
|
+
parsed = data_str
|
|
1503
|
+
yield {"event": current_event, "data": parsed}
|
|
1504
|
+
|
|
1505
|
+
# Terminal event — stop iteration
|
|
1506
|
+
if current_event == "crawl.completed":
|
|
1507
|
+
return
|
|
1508
|
+
|
|
1509
|
+
current_event = "message"
|
|
1510
|
+
data_lines = []
|
|
1511
|
+
continue
|
|
1512
|
+
|
|
1513
|
+
if line.startswith("event:"):
|
|
1514
|
+
current_event = line[6:].strip()
|
|
1515
|
+
elif line.startswith("data:"):
|
|
1516
|
+
data_lines.append(line[5:].strip())
|
|
1517
|
+
|
|
1083
1518
|
async def crawl_and_wait(
|
|
1084
1519
|
self,
|
|
1085
1520
|
url: str,
|
|
@@ -1186,6 +1621,280 @@ class AlterLab:
|
|
|
1186
1621
|
current_interval * poll_backoff_multiplier, poll_max_interval
|
|
1187
1622
|
)
|
|
1188
1623
|
|
|
1624
|
+
# ------------------------------------------------------------------
|
|
1625
|
+
# Search
|
|
1626
|
+
# ------------------------------------------------------------------
|
|
1627
|
+
|
|
1628
|
+
async def search(
|
|
1629
|
+
self,
|
|
1630
|
+
query: str,
|
|
1631
|
+
*,
|
|
1632
|
+
domain: Optional[str] = None,
|
|
1633
|
+
num_results: int = 10,
|
|
1634
|
+
page: int = 1,
|
|
1635
|
+
country: Optional[str] = None,
|
|
1636
|
+
language: Optional[str] = None,
|
|
1637
|
+
time_range: Optional[Literal["hour", "day", "week", "month", "year"]] = None,
|
|
1638
|
+
scrape_results: bool = False,
|
|
1639
|
+
formats: Optional[
|
|
1640
|
+
List[Literal["text", "json", "json_v2", "html", "markdown"]]
|
|
1641
|
+
] = None,
|
|
1642
|
+
extraction_schema: Optional[Dict[str, Any]] = None,
|
|
1643
|
+
) -> Dict[str, Any]:
|
|
1644
|
+
"""Execute a web search and return SERP results.
|
|
1645
|
+
|
|
1646
|
+
Args:
|
|
1647
|
+
query: Search terms (max 500 characters)
|
|
1648
|
+
domain: Restrict results to a specific domain (applied as site: prefix)
|
|
1649
|
+
num_results: Number of results to return (1-30, default 10)
|
|
1650
|
+
page: Result page number (1-indexed, default 1)
|
|
1651
|
+
country: ISO 3166-1 alpha-2 country code for geo-targeted results
|
|
1652
|
+
language: Language code for results (e.g., en, fr, de)
|
|
1653
|
+
time_range: Filter results by recency
|
|
1654
|
+
scrape_results: If True, scrape each result page and include content
|
|
1655
|
+
formats: Output formats when scrape_results=True
|
|
1656
|
+
extraction_schema: JSON schema for structured extraction (when scrape_results=True)
|
|
1657
|
+
|
|
1658
|
+
Returns:
|
|
1659
|
+
Search response with search_id, results, credits_used, etc.
|
|
1660
|
+
|
|
1661
|
+
Raises:
|
|
1662
|
+
AlterLabAPIError: On API errors
|
|
1663
|
+
|
|
1664
|
+
Example:
|
|
1665
|
+
results = await client.search("web scraping api python")
|
|
1666
|
+
for r in results["results"]:
|
|
1667
|
+
print(f"{r['position']}. {r['title']} — {r['url']}")
|
|
1668
|
+
"""
|
|
1669
|
+
payload: Dict[str, Any] = {
|
|
1670
|
+
"query": query,
|
|
1671
|
+
"num_results": num_results,
|
|
1672
|
+
"page": page,
|
|
1673
|
+
"scrape_results": scrape_results,
|
|
1674
|
+
}
|
|
1675
|
+
if domain is not None:
|
|
1676
|
+
payload["domain"] = domain
|
|
1677
|
+
if country is not None:
|
|
1678
|
+
payload["country"] = country
|
|
1679
|
+
if language is not None:
|
|
1680
|
+
payload["language"] = language
|
|
1681
|
+
if time_range is not None:
|
|
1682
|
+
payload["time_range"] = time_range
|
|
1683
|
+
if formats is not None:
|
|
1684
|
+
payload["formats"] = formats
|
|
1685
|
+
if extraction_schema is not None:
|
|
1686
|
+
payload["extraction_schema"] = extraction_schema
|
|
1687
|
+
|
|
1688
|
+
response = await self._request_with_retry(
|
|
1689
|
+
"POST", "/api/v1/search", json=payload
|
|
1690
|
+
)
|
|
1691
|
+
return response.json()
|
|
1692
|
+
|
|
1693
|
+
async def batch_search(
|
|
1694
|
+
self,
|
|
1695
|
+
queries: List[str],
|
|
1696
|
+
*,
|
|
1697
|
+
num_results: int = 10,
|
|
1698
|
+
country: Optional[str] = None,
|
|
1699
|
+
language: Optional[str] = None,
|
|
1700
|
+
time_range: Optional[Literal["hour", "day", "week", "month", "year"]] = None,
|
|
1701
|
+
scrape_results: bool = False,
|
|
1702
|
+
formats: Optional[
|
|
1703
|
+
List[Literal["text", "json", "json_v2", "html", "markdown"]]
|
|
1704
|
+
] = None,
|
|
1705
|
+
webhook_url: Optional[str] = None,
|
|
1706
|
+
) -> Dict[str, Any]:
|
|
1707
|
+
"""Submit a batch of search queries for async parallel processing.
|
|
1708
|
+
|
|
1709
|
+
Args:
|
|
1710
|
+
queries: List of search queries (2-100)
|
|
1711
|
+
num_results: Number of results per query (1-30, default 10)
|
|
1712
|
+
country: ISO 3166-1 alpha-2 country code
|
|
1713
|
+
language: Language code
|
|
1714
|
+
time_range: Filter results by recency
|
|
1715
|
+
scrape_results: Scrape each result page
|
|
1716
|
+
formats: Output formats when scrape_results=True
|
|
1717
|
+
webhook_url: URL to receive batch.search.completed webhook
|
|
1718
|
+
|
|
1719
|
+
Returns:
|
|
1720
|
+
Batch response with batch_id, job_ids, status, etc.
|
|
1721
|
+
|
|
1722
|
+
Raises:
|
|
1723
|
+
AlterLabAPIError: On API errors
|
|
1724
|
+
"""
|
|
1725
|
+
payload: Dict[str, Any] = {
|
|
1726
|
+
"queries": queries,
|
|
1727
|
+
"num_results": num_results,
|
|
1728
|
+
"scrape_results": scrape_results,
|
|
1729
|
+
}
|
|
1730
|
+
if country is not None:
|
|
1731
|
+
payload["country"] = country
|
|
1732
|
+
if language is not None:
|
|
1733
|
+
payload["language"] = language
|
|
1734
|
+
if time_range is not None:
|
|
1735
|
+
payload["time_range"] = time_range
|
|
1736
|
+
if formats is not None:
|
|
1737
|
+
payload["formats"] = formats
|
|
1738
|
+
if webhook_url is not None:
|
|
1739
|
+
payload["webhook_url"] = webhook_url
|
|
1740
|
+
|
|
1741
|
+
response = await self._request_with_retry(
|
|
1742
|
+
"POST", "/api/v1/search/batch", json=payload
|
|
1743
|
+
)
|
|
1744
|
+
return response.json()
|
|
1745
|
+
|
|
1746
|
+
# ------------------------------------------------------------------
|
|
1747
|
+
# Map
|
|
1748
|
+
# ------------------------------------------------------------------
|
|
1749
|
+
|
|
1750
|
+
async def map(
|
|
1751
|
+
self,
|
|
1752
|
+
url: str,
|
|
1753
|
+
*,
|
|
1754
|
+
max_pages: int = 500,
|
|
1755
|
+
max_depth: int = 3,
|
|
1756
|
+
include_patterns: Optional[List[str]] = None,
|
|
1757
|
+
exclude_patterns: Optional[List[str]] = None,
|
|
1758
|
+
search: Optional[str] = None,
|
|
1759
|
+
sitemap: Literal["skip", "include", "only"] = "include",
|
|
1760
|
+
sitemap_path: Optional[str] = None,
|
|
1761
|
+
include_metadata: bool = False,
|
|
1762
|
+
include_subdomains: bool = False,
|
|
1763
|
+
respect_robots: bool = True,
|
|
1764
|
+
) -> Dict[str, Any]:
|
|
1765
|
+
"""Discover URLs on a website via sitemap parsing and link extraction.
|
|
1766
|
+
|
|
1767
|
+
Lightweight site structure discovery without scraping content.
|
|
1768
|
+
Returns a flat list of discovered URLs with optional metadata.
|
|
1769
|
+
|
|
1770
|
+
Args:
|
|
1771
|
+
url: Starting URL for site discovery
|
|
1772
|
+
max_pages: Maximum URLs to discover (1-50000, default 500)
|
|
1773
|
+
max_depth: Link-following depth (0-10, default 3)
|
|
1774
|
+
include_patterns: Glob patterns — only include matching URLs
|
|
1775
|
+
exclude_patterns: Glob patterns — exclude matching URLs
|
|
1776
|
+
search: Query to filter/rank discovered URLs by relevance
|
|
1777
|
+
sitemap: Sitemap handling: skip, include (default), or only
|
|
1778
|
+
sitemap_path: Custom sitemap location (e.g., /custom-sitemap.xml)
|
|
1779
|
+
include_metadata: Fetch title and meta description for each URL
|
|
1780
|
+
include_subdomains: Include URLs from subdomains
|
|
1781
|
+
respect_robots: Respect robots.txt directives (default True)
|
|
1782
|
+
|
|
1783
|
+
Returns:
|
|
1784
|
+
Map response with map_id, urls, total_urls, credits_used, etc.
|
|
1785
|
+
|
|
1786
|
+
Raises:
|
|
1787
|
+
AlterLabAPIError: On API errors
|
|
1788
|
+
|
|
1789
|
+
Example:
|
|
1790
|
+
result = await client.map(
|
|
1791
|
+
"https://example.com",
|
|
1792
|
+
max_pages=1000,
|
|
1793
|
+
include_metadata=True,
|
|
1794
|
+
)
|
|
1795
|
+
for entry in result["urls"]:
|
|
1796
|
+
print(f"{entry['url']} (depth={entry['depth']})")
|
|
1797
|
+
"""
|
|
1798
|
+
payload: Dict[str, Any] = {
|
|
1799
|
+
"url": url,
|
|
1800
|
+
"max_pages": max_pages,
|
|
1801
|
+
"max_depth": max_depth,
|
|
1802
|
+
"sitemap": sitemap,
|
|
1803
|
+
"include_metadata": include_metadata,
|
|
1804
|
+
"include_subdomains": include_subdomains,
|
|
1805
|
+
"respect_robots": respect_robots,
|
|
1806
|
+
}
|
|
1807
|
+
if include_patterns is not None:
|
|
1808
|
+
payload["include_patterns"] = include_patterns
|
|
1809
|
+
if exclude_patterns is not None:
|
|
1810
|
+
payload["exclude_patterns"] = exclude_patterns
|
|
1811
|
+
if search is not None:
|
|
1812
|
+
payload["search"] = search
|
|
1813
|
+
if sitemap_path is not None:
|
|
1814
|
+
payload["sitemap_path"] = sitemap_path
|
|
1815
|
+
|
|
1816
|
+
response = await self._request_with_retry("POST", "/api/v1/map", json=payload)
|
|
1817
|
+
return response.json()
|
|
1818
|
+
|
|
1819
|
+
# ------------------------------------------------------------------
|
|
1820
|
+
# Extract
|
|
1821
|
+
# ------------------------------------------------------------------
|
|
1822
|
+
|
|
1823
|
+
async def extract(
|
|
1824
|
+
self,
|
|
1825
|
+
content: str,
|
|
1826
|
+
*,
|
|
1827
|
+
content_type: Literal["html", "text", "markdown"] = "html",
|
|
1828
|
+
extraction_schema: Optional[Dict[str, Any]] = None,
|
|
1829
|
+
extraction_profile: Optional[
|
|
1830
|
+
Literal[
|
|
1831
|
+
"auto",
|
|
1832
|
+
"product",
|
|
1833
|
+
"article",
|
|
1834
|
+
"job_posting",
|
|
1835
|
+
"faq",
|
|
1836
|
+
"recipe",
|
|
1837
|
+
"event",
|
|
1838
|
+
]
|
|
1839
|
+
] = None,
|
|
1840
|
+
extraction_prompt: Optional[str] = None,
|
|
1841
|
+
formats: Optional[
|
|
1842
|
+
List[Literal["text", "json", "json_v2", "html", "markdown", "rag"]]
|
|
1843
|
+
] = None,
|
|
1844
|
+
source_url: Optional[str] = None,
|
|
1845
|
+
evidence: bool = False,
|
|
1846
|
+
) -> Dict[str, Any]:
|
|
1847
|
+
"""Extract structured data from raw content without scraping.
|
|
1848
|
+
|
|
1849
|
+
Accepts HTML, text, or markdown content and runs the extraction pipeline
|
|
1850
|
+
including format conversion, schema-based filtering, and LLM extraction.
|
|
1851
|
+
|
|
1852
|
+
Args:
|
|
1853
|
+
content: Raw content to extract from (HTML, text, or markdown)
|
|
1854
|
+
content_type: Type of the provided content (default "html")
|
|
1855
|
+
extraction_schema: JSON Schema for structured extraction
|
|
1856
|
+
extraction_profile: Pre-defined extraction profile
|
|
1857
|
+
extraction_prompt: Natural language extraction instructions for LLM
|
|
1858
|
+
formats: Output formats (default ["json"])
|
|
1859
|
+
source_url: Original URL of the content (for context, not fetched)
|
|
1860
|
+
evidence: Include field provenance/evidence (default False)
|
|
1861
|
+
|
|
1862
|
+
Returns:
|
|
1863
|
+
Extract response with extract_id, formats, credits_used, etc.
|
|
1864
|
+
|
|
1865
|
+
Raises:
|
|
1866
|
+
AlterLabAPIError: On API errors
|
|
1867
|
+
|
|
1868
|
+
Example:
|
|
1869
|
+
result = await client.extract(
|
|
1870
|
+
content="<html><body><h1>Price: $29.99</h1></body></html>",
|
|
1871
|
+
content_type="html",
|
|
1872
|
+
extraction_prompt="Extract the price",
|
|
1873
|
+
formats=["json"],
|
|
1874
|
+
)
|
|
1875
|
+
print(result["formats"]["json"])
|
|
1876
|
+
"""
|
|
1877
|
+
payload: Dict[str, Any] = {
|
|
1878
|
+
"content": content,
|
|
1879
|
+
"content_type": content_type,
|
|
1880
|
+
"evidence": evidence,
|
|
1881
|
+
}
|
|
1882
|
+
if extraction_schema is not None:
|
|
1883
|
+
payload["extraction_schema"] = extraction_schema
|
|
1884
|
+
if extraction_profile is not None:
|
|
1885
|
+
payload["extraction_profile"] = extraction_profile
|
|
1886
|
+
if extraction_prompt is not None:
|
|
1887
|
+
payload["extraction_prompt"] = extraction_prompt
|
|
1888
|
+
if formats is not None:
|
|
1889
|
+
payload["formats"] = formats
|
|
1890
|
+
if source_url is not None:
|
|
1891
|
+
payload["source_url"] = source_url
|
|
1892
|
+
|
|
1893
|
+
response = await self._request_with_retry(
|
|
1894
|
+
"POST", "/api/v1/extract", json=payload
|
|
1895
|
+
)
|
|
1896
|
+
return response.json()
|
|
1897
|
+
|
|
1189
1898
|
# Deprecated methods for backwards compatibility
|
|
1190
1899
|
|
|
1191
1900
|
async def scrape_light(self, url: str, **kwargs) -> Dict[str, Any]:
|
|
@@ -1307,6 +2016,22 @@ class AlterLabSync:
|
|
|
1307
2016
|
"""Synchronous version of crawl_and_wait()."""
|
|
1308
2017
|
return self._run(self._client.crawl_and_wait(*args, **kwargs))
|
|
1309
2018
|
|
|
2019
|
+
def search(self, *args, **kwargs) -> Dict[str, Any]:
|
|
2020
|
+
"""Synchronous version of search()."""
|
|
2021
|
+
return self._run(self._client.search(*args, **kwargs))
|
|
2022
|
+
|
|
2023
|
+
def batch_search(self, *args, **kwargs) -> Dict[str, Any]:
|
|
2024
|
+
"""Synchronous version of batch_search()."""
|
|
2025
|
+
return self._run(self._client.batch_search(*args, **kwargs))
|
|
2026
|
+
|
|
2027
|
+
def map(self, *args, **kwargs) -> Dict[str, Any]:
|
|
2028
|
+
"""Synchronous version of map()."""
|
|
2029
|
+
return self._run(self._client.map(*args, **kwargs))
|
|
2030
|
+
|
|
2031
|
+
def extract(self, *args, **kwargs) -> Dict[str, Any]:
|
|
2032
|
+
"""Synchronous version of extract()."""
|
|
2033
|
+
return self._run(self._client.extract(*args, **kwargs))
|
|
2034
|
+
|
|
1310
2035
|
def scrape_html(self, *args, **kwargs) -> Dict[str, Any]:
|
|
1311
2036
|
"""Synchronous version of scrape_html()."""
|
|
1312
2037
|
return self._run(self._client.scrape_html(*args, **kwargs))
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Type definitions for AlterLab SDK session and cookie support."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
4
|
+
|
|
5
|
+
from typing_extensions import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TierInfo(TypedDict):
|
|
9
|
+
"""Rate-limit tier information derived from account balance.
|
|
10
|
+
|
|
11
|
+
Returned as part of the ``/auth/me`` response so that SDK consumers can
|
|
12
|
+
inspect their current tier without a separate API call.
|
|
13
|
+
|
|
14
|
+
Balance-based tier table:
|
|
15
|
+
|
|
16
|
+
+--------------+-----------+-------+-------------+
|
|
17
|
+
| Tier | Balance | RPM | Concurrent |
|
|
18
|
+
+==============+===========+=======+=============+
|
|
19
|
+
| Free | $0–$5 | 30 | 3 |
|
|
20
|
+
| Basic | $5–$50 | 60 | 10 |
|
|
21
|
+
| Growth | $50–$200 | 100 | 15 |
|
|
22
|
+
| Scale | $200–$500 | 300 | 25 |
|
|
23
|
+
| High Volume | $500+ | 500 | 50 |
|
|
24
|
+
| Enterprise | Custom | 10000 | 500 |
|
|
25
|
+
+--------------+-----------+-------+-------------+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
"""Human-readable tier name, e.g. ``'Free'``, ``'Basic'``, ``'Growth'``."""
|
|
30
|
+
|
|
31
|
+
slug: str
|
|
32
|
+
"""Machine-readable tier identifier, e.g. ``'free'``, ``'basic'``, ``'growth'``."""
|
|
33
|
+
|
|
34
|
+
requests_per_minute: int
|
|
35
|
+
"""Maximum API requests per minute allowed at this tier."""
|
|
36
|
+
|
|
37
|
+
concurrent_requests: int
|
|
38
|
+
"""Maximum number of concurrent scrape requests allowed at this tier."""
|
|
39
|
+
|
|
40
|
+
min_balance_usd: float
|
|
41
|
+
"""Minimum account balance (USD) required to qualify for this tier."""
|
|
42
|
+
|
|
43
|
+
max_balance_usd: Optional[float]
|
|
44
|
+
"""Upper balance threshold (USD) for this tier. ``None`` means no upper limit."""
|
|
45
|
+
|
|
46
|
+
next_tier_slug: Optional[str]
|
|
47
|
+
"""Slug of the next higher tier, or ``None`` if already at the top tier."""
|
|
48
|
+
|
|
49
|
+
next_tier_threshold_usd: Optional[float]
|
|
50
|
+
"""Balance (USD) needed to reach the next tier, or ``None`` if at top tier."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SessionIntegration(TypedDict, total=False):
|
|
54
|
+
"""A stored session integration. Cookie values are never exposed."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
name: str
|
|
58
|
+
domain: str
|
|
59
|
+
cookie_names: List[str]
|
|
60
|
+
header_names: Optional[List[str]]
|
|
61
|
+
status: Literal["active", "expired", "invalid", "pending_validation"]
|
|
62
|
+
expires_at: Optional[str]
|
|
63
|
+
last_validated_at: Optional[str]
|
|
64
|
+
validation_confidence: Optional[int]
|
|
65
|
+
consecutive_failures: int
|
|
66
|
+
last_used_at: Optional[str]
|
|
67
|
+
total_requests: int
|
|
68
|
+
successful_requests: int
|
|
69
|
+
success_rate: float
|
|
70
|
+
notes: Optional[str]
|
|
71
|
+
created_at: str
|
|
72
|
+
updated_at: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SessionListResponse(TypedDict):
|
|
76
|
+
"""Response from listing sessions."""
|
|
77
|
+
|
|
78
|
+
sessions: List[SessionIntegration]
|
|
79
|
+
total: int
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SessionValidation(TypedDict, total=False):
|
|
83
|
+
"""Result of a session validation check."""
|
|
84
|
+
|
|
85
|
+
is_valid: bool
|
|
86
|
+
confidence: int
|
|
87
|
+
http_status: Optional[int]
|
|
88
|
+
detected_user: Optional[str]
|
|
89
|
+
cookie_expiry_detected: Optional[str]
|
|
90
|
+
cookies_expiring_soon: bool
|
|
91
|
+
indicators_found: Optional[Dict[str, List[str]]]
|
|
92
|
+
consecutive_failures: int
|
|
93
|
+
details: str
|
alterlab-2.1.0/alterlab/types.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""Type definitions for AlterLab SDK session and cookie support."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict, List, Literal, Optional
|
|
4
|
-
|
|
5
|
-
from typing_extensions import TypedDict
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SessionIntegration(TypedDict, total=False):
|
|
9
|
-
"""A stored session integration. Cookie values are never exposed."""
|
|
10
|
-
|
|
11
|
-
id: str
|
|
12
|
-
name: str
|
|
13
|
-
domain: str
|
|
14
|
-
cookie_names: List[str]
|
|
15
|
-
header_names: Optional[List[str]]
|
|
16
|
-
status: Literal["active", "expired", "invalid", "pending_validation"]
|
|
17
|
-
expires_at: Optional[str]
|
|
18
|
-
last_validated_at: Optional[str]
|
|
19
|
-
validation_confidence: Optional[int]
|
|
20
|
-
consecutive_failures: int
|
|
21
|
-
last_used_at: Optional[str]
|
|
22
|
-
total_requests: int
|
|
23
|
-
successful_requests: int
|
|
24
|
-
success_rate: float
|
|
25
|
-
notes: Optional[str]
|
|
26
|
-
created_at: str
|
|
27
|
-
updated_at: str
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class SessionListResponse(TypedDict):
|
|
31
|
-
"""Response from listing sessions."""
|
|
32
|
-
|
|
33
|
-
sessions: List[SessionIntegration]
|
|
34
|
-
total: int
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class SessionValidation(TypedDict, total=False):
|
|
38
|
-
"""Result of a session validation check."""
|
|
39
|
-
|
|
40
|
-
is_valid: bool
|
|
41
|
-
confidence: int
|
|
42
|
-
http_status: Optional[int]
|
|
43
|
-
detected_user: Optional[str]
|
|
44
|
-
cookie_expiry_detected: Optional[str]
|
|
45
|
-
cookies_expiring_soon: bool
|
|
46
|
-
indicators_found: Optional[Dict[str, List[str]]]
|
|
47
|
-
consecutive_failures: int
|
|
48
|
-
details: str
|
|
File without changes
|
|
File without changes
|
|
File without changes
|