webcrawlerapi 2.1.0__tar.gz → 2.1.3__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.
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/PKG-INFO +1 -1
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/setup.py +1 -1
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/tests/test_client.py +0 -32
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi/__init__.py +2 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi/client.py +126 -10
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi/models.py +25 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi.egg-info/PKG-INFO +1 -1
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/README.md +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/pyproject.toml +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/setup.cfg +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/tests/__init__.py +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/tests/test_models.py +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi.egg-info/SOURCES.txt +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi.egg-info/dependency_links.txt +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi.egg-info/requires.txt +0 -0
- {webcrawlerapi-2.1.0 → webcrawlerapi-2.1.3}/webcrawlerapi.egg-info/top_level.txt +0 -0
|
@@ -13,7 +13,6 @@ from webcrawlerapi.models import (
|
|
|
13
13
|
ScrapeId,
|
|
14
14
|
ScrapeResponse,
|
|
15
15
|
ScrapeResponseError,
|
|
16
|
-
UploadS3Action,
|
|
17
16
|
WebCrawlerApiError,
|
|
18
17
|
)
|
|
19
18
|
|
|
@@ -99,37 +98,6 @@ class TestWebCrawlerAPI:
|
|
|
99
98
|
assert payload["items_limit"] == 5
|
|
100
99
|
assert payload["respect_robots_txt"] is True
|
|
101
100
|
|
|
102
|
-
@responses.activate
|
|
103
|
-
def test_crawl_async_with_actions(self, client):
|
|
104
|
-
"""Test crawl_async with S3 upload action."""
|
|
105
|
-
responses.add(
|
|
106
|
-
responses.POST,
|
|
107
|
-
"https://api.test.com/v1/crawl",
|
|
108
|
-
json={"id": "crawl-456"},
|
|
109
|
-
status=200,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
s3_action = UploadS3Action(
|
|
113
|
-
path="crawl-results/",
|
|
114
|
-
access_key_id="AKIAEXAMPLE",
|
|
115
|
-
secret_access_key="secret123",
|
|
116
|
-
bucket="my-bucket",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
result = client.crawl_async(url="https://example.com", actions=[s3_action])
|
|
120
|
-
|
|
121
|
-
assert result.id == "crawl-456"
|
|
122
|
-
|
|
123
|
-
# Verify action in payload
|
|
124
|
-
request = responses.calls[0].request
|
|
125
|
-
import json
|
|
126
|
-
|
|
127
|
-
payload = json.loads(request.body)
|
|
128
|
-
assert "actions" in payload
|
|
129
|
-
assert len(payload["actions"]) == 1
|
|
130
|
-
assert payload["actions"][0]["type"] == "upload_s3"
|
|
131
|
-
assert payload["actions"][0]["bucket"] == "my-bucket"
|
|
132
|
-
|
|
133
101
|
@responses.activate
|
|
134
102
|
def test_crawl_async_with_max_age(self, client):
|
|
135
103
|
"""Test crawl_async includes max_age when provided."""
|
|
@@ -6,6 +6,7 @@ import requests
|
|
|
6
6
|
|
|
7
7
|
from .models import (
|
|
8
8
|
Action,
|
|
9
|
+
AgentRun,
|
|
9
10
|
CrawlResponse,
|
|
10
11
|
Job,
|
|
11
12
|
JobMarkdownResponse,
|
|
@@ -17,6 +18,7 @@ from .models import (
|
|
|
17
18
|
|
|
18
19
|
CRAWLER_VERSION = "v1"
|
|
19
20
|
SCRAPER_VERSION = "v2"
|
|
21
|
+
AGENT_TERMINAL_STATUSES = {"done", "error", "canceled"}
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class WebCrawlerAPI:
|
|
@@ -66,7 +68,6 @@ class WebCrawlerAPI:
|
|
|
66
68
|
webhook_url: Optional[str] = None,
|
|
67
69
|
whitelist_regexp: Optional[str] = None,
|
|
68
70
|
blacklist_regexp: Optional[str] = None,
|
|
69
|
-
actions: Optional[Union[Action, List[Action]]] = None,
|
|
70
71
|
respect_robots_txt: bool = False,
|
|
71
72
|
main_content_only: bool = False,
|
|
72
73
|
max_depth: Optional[int] = None,
|
|
@@ -85,7 +86,6 @@ class WebCrawlerAPI:
|
|
|
85
86
|
webhook_url (str, optional): URL for webhook notifications
|
|
86
87
|
whitelist_regexp (str, optional): Regex pattern for URL whitelist
|
|
87
88
|
blacklist_regexp (str, optional): Regex pattern for URL blacklist
|
|
88
|
-
actions (Action or List[Action], optional): Actions to perform during crawling
|
|
89
89
|
respect_robots_txt (bool): Whether to respect robots.txt (default: False)
|
|
90
90
|
main_content_only (bool): Whether to extract only main content (default: False)
|
|
91
91
|
max_depth (int, optional): Maximum crawl depth (0 = seed only, 1 = seed + direct links)
|
|
@@ -129,9 +129,6 @@ class WebCrawlerAPI:
|
|
|
129
129
|
payload["main_content_only"] = main_content_only
|
|
130
130
|
if keep_query_params is not None:
|
|
131
131
|
payload["keep_query_params"] = keep_query_params
|
|
132
|
-
if actions:
|
|
133
|
-
action_list = [actions] if not isinstance(actions, list) else actions
|
|
134
|
-
payload["actions"] = [vars(action) for action in action_list]
|
|
135
132
|
|
|
136
133
|
response = self.session.post(
|
|
137
134
|
urljoin(self.base_url, f"/{CRAWLER_VERSION}/crawl"), json=payload
|
|
@@ -235,7 +232,6 @@ class WebCrawlerAPI:
|
|
|
235
232
|
webhook_url: Optional[str] = None,
|
|
236
233
|
whitelist_regexp: Optional[str] = None,
|
|
237
234
|
blacklist_regexp: Optional[str] = None,
|
|
238
|
-
actions: Optional[Union[Action, List[Action]]] = None,
|
|
239
235
|
respect_robots_txt: bool = False,
|
|
240
236
|
main_content_only: bool = False,
|
|
241
237
|
max_depth: Optional[int] = None,
|
|
@@ -254,7 +250,6 @@ class WebCrawlerAPI:
|
|
|
254
250
|
webhook_url (str, optional): URL for webhook notifications
|
|
255
251
|
whitelist_regexp (str, optional): Regex pattern for URL whitelist
|
|
256
252
|
blacklist_regexp (str, optional): Regex pattern for URL blacklist
|
|
257
|
-
actions (Action or List[Action], optional): Actions to perform during crawling
|
|
258
253
|
respect_robots_txt (bool): Whether to respect robots.txt (default: False)
|
|
259
254
|
main_content_only (bool): Whether to extract only main content (default: False)
|
|
260
255
|
max_depth (int, optional): Maximum crawl depth
|
|
@@ -277,7 +272,6 @@ class WebCrawlerAPI:
|
|
|
277
272
|
webhook_url=webhook_url,
|
|
278
273
|
whitelist_regexp=whitelist_regexp,
|
|
279
274
|
blacklist_regexp=blacklist_regexp,
|
|
280
|
-
actions=actions,
|
|
281
275
|
respect_robots_txt=respect_robots_txt,
|
|
282
276
|
main_content_only=main_content_only,
|
|
283
277
|
max_depth=max_depth,
|
|
@@ -313,7 +307,6 @@ class WebCrawlerAPI:
|
|
|
313
307
|
webhook_url: Optional[str] = None,
|
|
314
308
|
whitelist_regexp: Optional[str] = None,
|
|
315
309
|
blacklist_regexp: Optional[str] = None,
|
|
316
|
-
actions: Optional[Union[Action, List[Action]]] = None,
|
|
317
310
|
respect_robots_txt: bool = False,
|
|
318
311
|
main_content_only: bool = False,
|
|
319
312
|
max_depth: Optional[int] = None,
|
|
@@ -338,7 +331,6 @@ class WebCrawlerAPI:
|
|
|
338
331
|
webhook_url=webhook_url,
|
|
339
332
|
whitelist_regexp=whitelist_regexp,
|
|
340
333
|
blacklist_regexp=blacklist_regexp,
|
|
341
|
-
actions=actions,
|
|
342
334
|
respect_robots_txt=respect_robots_txt,
|
|
343
335
|
main_content_only=main_content_only,
|
|
344
336
|
max_depth=max_depth,
|
|
@@ -355,6 +347,130 @@ class WebCrawlerAPI:
|
|
|
355
347
|
|
|
356
348
|
return self.get_job_markdown_content(job.id)
|
|
357
349
|
|
|
350
|
+
def run_agent_async(
|
|
351
|
+
self,
|
|
352
|
+
prompt: str,
|
|
353
|
+
max_spend_usd: float,
|
|
354
|
+
urls: Optional[List[str]] = None,
|
|
355
|
+
seed_urls_only: Optional[bool] = None,
|
|
356
|
+
output_schema: Optional[Dict[str, Any]] = None,
|
|
357
|
+
model: Optional[str] = None,
|
|
358
|
+
) -> AgentRun:
|
|
359
|
+
"""
|
|
360
|
+
Start a new agent run asynchronously.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
prompt (str): Natural-language task for the agent to complete
|
|
364
|
+
max_spend_usd (float): Maximum spend for this run in USD
|
|
365
|
+
urls (list, optional): Optional seed URLs the agent can use while running the task
|
|
366
|
+
seed_urls_only (bool, optional): If True, the agent only uses the provided seed URLs
|
|
367
|
+
output_schema (dict, optional): Optional JSON schema for structured agent output
|
|
368
|
+
model (str, optional): Model to use for the run. Omit to use the API default.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
AgentRun: The created agent run
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
WebCrawlerApiError: If the API returns an error response
|
|
375
|
+
requests.exceptions.RequestException: If the HTTP request fails
|
|
376
|
+
"""
|
|
377
|
+
payload: Dict[str, Any] = {"prompt": prompt, "max_spend_usd": max_spend_usd}
|
|
378
|
+
|
|
379
|
+
if urls is not None:
|
|
380
|
+
payload["urls"] = urls
|
|
381
|
+
if seed_urls_only is not None:
|
|
382
|
+
payload["seed_urls_only"] = seed_urls_only
|
|
383
|
+
if output_schema is not None:
|
|
384
|
+
payload["output_schema"] = output_schema
|
|
385
|
+
if model is not None:
|
|
386
|
+
payload["model"] = model
|
|
387
|
+
|
|
388
|
+
response = self.session.post(
|
|
389
|
+
urljoin(self.base_url, f"/{CRAWLER_VERSION}/agent"), json=payload
|
|
390
|
+
)
|
|
391
|
+
if not response.ok:
|
|
392
|
+
self._raise_for_error(response)
|
|
393
|
+
return AgentRun(response.json())
|
|
394
|
+
|
|
395
|
+
def get_agent_job(self, job_id: str) -> AgentRun:
|
|
396
|
+
"""
|
|
397
|
+
Get the status and result of a specific agent run.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
job_id (str): The unique identifier of the agent run
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
AgentRun: The agent run status and result
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
WebCrawlerApiError: If the API returns an error response
|
|
407
|
+
requests.exceptions.RequestException: If the HTTP request fails
|
|
408
|
+
"""
|
|
409
|
+
response = self.session.get(
|
|
410
|
+
urljoin(self.base_url, f"/{CRAWLER_VERSION}/agent/job/{job_id}"),
|
|
411
|
+
headers={"Cache-Control": "no-cache, no-store, must-revalidate"},
|
|
412
|
+
)
|
|
413
|
+
if not response.ok:
|
|
414
|
+
self._raise_for_error(response)
|
|
415
|
+
return AgentRun(response.json())
|
|
416
|
+
|
|
417
|
+
def run_agent(
|
|
418
|
+
self,
|
|
419
|
+
prompt: str,
|
|
420
|
+
max_spend_usd: float,
|
|
421
|
+
urls: Optional[List[str]] = None,
|
|
422
|
+
seed_urls_only: Optional[bool] = None,
|
|
423
|
+
output_schema: Optional[Dict[str, Any]] = None,
|
|
424
|
+
model: Optional[str] = None,
|
|
425
|
+
max_polls: int = 100,
|
|
426
|
+
) -> AgentRun:
|
|
427
|
+
"""
|
|
428
|
+
Start a new agent run and wait for its completion.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
prompt (str): Natural-language task for the agent to complete
|
|
432
|
+
max_spend_usd (float): Maximum spend for this run in USD
|
|
433
|
+
urls (list, optional): Optional seed URLs the agent can use while running the task
|
|
434
|
+
seed_urls_only (bool, optional): If True, the agent only uses the provided seed URLs
|
|
435
|
+
output_schema (dict, optional): Optional JSON schema for structured agent output
|
|
436
|
+
model (str, optional): Model to use for the run. Omit to use the API default.
|
|
437
|
+
max_polls (int): Maximum number of status checks before raising a timeout (default: 100)
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
AgentRun: The final agent run state after completion
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
WebCrawlerApiError: If the API returns an error response or the run times out
|
|
444
|
+
requests.exceptions.RequestException: If any API request fails
|
|
445
|
+
"""
|
|
446
|
+
run = self.run_agent_async(
|
|
447
|
+
prompt=prompt,
|
|
448
|
+
max_spend_usd=max_spend_usd,
|
|
449
|
+
urls=urls,
|
|
450
|
+
seed_urls_only=seed_urls_only,
|
|
451
|
+
output_schema=output_schema,
|
|
452
|
+
model=model,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if not run.id:
|
|
456
|
+
raise WebCrawlerApiError(
|
|
457
|
+
"invalid_response", "Failed to fetch agent job status"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if run.status in AGENT_TERMINAL_STATUSES:
|
|
461
|
+
return run
|
|
462
|
+
|
|
463
|
+
for _ in range(max_polls):
|
|
464
|
+
time.sleep(2)
|
|
465
|
+
agent_job = self.get_agent_job(run.id)
|
|
466
|
+
if agent_job.status in AGENT_TERMINAL_STATUSES:
|
|
467
|
+
return agent_job
|
|
468
|
+
|
|
469
|
+
raise WebCrawlerApiError(
|
|
470
|
+
"timeout",
|
|
471
|
+
"Agent run took too long, please retry or increase the number of polling retries",
|
|
472
|
+
)
|
|
473
|
+
|
|
358
474
|
def scrape_async(
|
|
359
475
|
self,
|
|
360
476
|
url: str,
|
|
@@ -94,6 +94,31 @@ class ScrapeResponseError:
|
|
|
94
94
|
status: Optional[str] = None
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
class AgentRun:
|
|
98
|
+
"""Represents an agent run job."""
|
|
99
|
+
|
|
100
|
+
TERMINAL_STATUSES = {"done", "error", "canceled"}
|
|
101
|
+
|
|
102
|
+
def __init__(self, data: Dict[str, Any]):
|
|
103
|
+
self.id: str = data["id"]
|
|
104
|
+
self.status: str = data["status"]
|
|
105
|
+
self.prompt: str = data["prompt"]
|
|
106
|
+
self.model: str = data["model"]
|
|
107
|
+
self.data: Optional[Any] = data.get("data")
|
|
108
|
+
self.error_reason: Optional[str] = data.get("error_reason")
|
|
109
|
+
self.balance_used_usd: float = data.get("balance_used_usd", 0)
|
|
110
|
+
self.max_spend_usd: Optional[float] = data.get("max_spend_usd")
|
|
111
|
+
self.urls: Optional[List[str]] = data.get("urls")
|
|
112
|
+
self.created_at: datetime = parse_datetime(data["created_at"])
|
|
113
|
+
self.updated_at: datetime = parse_datetime(data["updated_at"])
|
|
114
|
+
self.success: bool = data.get("success", False)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_terminal(self) -> bool:
|
|
118
|
+
"""Check if the agent run is in a terminal state (done, error, or canceled)."""
|
|
119
|
+
return self.status in self.TERMINAL_STATUSES
|
|
120
|
+
|
|
121
|
+
|
|
97
122
|
@dataclass
|
|
98
123
|
class Action:
|
|
99
124
|
"""Base class for actions that can be performed during crawling."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|