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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webcrawlerapi
3
- Version: 2.1.0
3
+ Version: 2.1.3
4
4
  Summary: Python SDK for WebCrawler API
5
5
  Home-page: https://github.com/webcrawlerapi/webcrawlerapi-python-sdk
6
6
  Author: Andrew
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="webcrawlerapi",
5
- version="2.1.0",
5
+ version="2.1.3",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "requests>=2.25.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."""
@@ -25,6 +25,7 @@ Basic usage:
25
25
  from .client import WebCrawlerAPI
26
26
  from .models import (
27
27
  Action,
28
+ AgentRun,
28
29
  CrawlResponse,
29
30
  Job,
30
31
  JobItem,
@@ -49,4 +50,5 @@ __all__ = [
49
50
  "ScrapeResponseError",
50
51
  "Action",
51
52
  "UploadS3Action",
53
+ "AgentRun",
52
54
  ]
@@ -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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webcrawlerapi
3
- Version: 2.1.0
3
+ Version: 2.1.3
4
4
  Summary: Python SDK for WebCrawler API
5
5
  Home-page: https://github.com/webcrawlerapi/webcrawlerapi-python-sdk
6
6
  Author: Andrew
File without changes
File without changes