firecrawl-py 1.13.4__py3-none-any.whl → 1.14.0__py3-none-any.whl

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.

Potentially problematic release.


This version of firecrawl-py might be problematic. Click here for more details.

@@ -1,1275 +0,0 @@
1
- """
2
- FirecrawlApp Module
3
-
4
- This module provides a class `FirecrawlApp` for interacting with the Firecrawl API.
5
- It includes methods to scrape URLs, perform searches, initiate and monitor crawl jobs,
6
- and check the status of these jobs. The module uses requests for HTTP communication
7
- and handles retries for certain HTTP status codes.
8
-
9
- Classes:
10
- - FirecrawlApp: Main class for interacting with the Firecrawl API.
11
- """
12
- import logging
13
- import os
14
- import time
15
- from typing import Any, Dict, Optional, List, Union, Callable
16
- import json
17
-
18
- import requests
19
- import pydantic
20
- import websockets
21
-
22
- logger : logging.Logger = logging.getLogger("firecrawl")
23
-
24
- class SearchParams(pydantic.BaseModel):
25
- query: str
26
- limit: Optional[int] = 5
27
- tbs: Optional[str] = None
28
- filter: Optional[str] = None
29
- lang: Optional[str] = "en"
30
- country: Optional[str] = "us"
31
- location: Optional[str] = None
32
- origin: Optional[str] = "api"
33
- timeout: Optional[int] = 60000
34
- scrapeOptions: Optional[Dict[str, Any]] = None
35
-
36
- class GenerateLLMsTextParams(pydantic.BaseModel):
37
- """
38
- Parameters for the LLMs.txt generation operation.
39
- """
40
- maxUrls: Optional[int] = 10
41
- showFullText: Optional[bool] = False
42
- __experimental_stream: Optional[bool] = None
43
-
44
- class DeepResearchParams(pydantic.BaseModel):
45
- """
46
- Parameters for the deep research operation.
47
- """
48
- maxDepth: Optional[int] = 7
49
- timeLimit: Optional[int] = 270
50
- maxUrls: Optional[int] = 20
51
- __experimental_streamSteps: Optional[bool] = None
52
-
53
- class DeepResearchResponse(pydantic.BaseModel):
54
- """
55
- Response from the deep research operation.
56
- """
57
- success: bool
58
- id: str
59
- error: Optional[str] = None
60
-
61
- class DeepResearchStatusResponse(pydantic.BaseModel):
62
- """
63
- Status response from the deep research operation.
64
- """
65
- success: bool
66
- data: Optional[Dict[str, Any]] = None
67
- status: str
68
- error: Optional[str] = None
69
- expiresAt: str
70
- currentDepth: int
71
- maxDepth: int
72
- activities: List[Dict[str, Any]]
73
- sources: List[Dict[str, Any]]
74
- summaries: List[str]
75
-
76
- class FirecrawlApp:
77
- class SearchResponse(pydantic.BaseModel):
78
- """
79
- Response from the search operation.
80
- """
81
- success: bool
82
- data: List[Dict[str, Any]]
83
- warning: Optional[str] = None
84
- error: Optional[str] = None
85
-
86
- class ExtractParams(pydantic.BaseModel):
87
- """
88
- Parameters for the extract operation.
89
- """
90
- prompt: Optional[str] = None
91
- schema_: Optional[Any] = pydantic.Field(None, alias='schema')
92
- system_prompt: Optional[str] = None
93
- allow_external_links: Optional[bool] = False
94
- enable_web_search: Optional[bool] = False
95
- # Just for backwards compatibility
96
- enableWebSearch: Optional[bool] = False
97
- show_sources: Optional[bool] = False
98
-
99
-
100
-
101
-
102
- class ExtractResponse(pydantic.BaseModel):
103
- """
104
- Response from the extract operation.
105
- """
106
- success: bool
107
- data: Optional[Any] = None
108
- error: Optional[str] = None
109
-
110
- def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None) -> None:
111
- """
112
- Initialize the FirecrawlApp instance with API key, API URL.
113
-
114
- Args:
115
- api_key (Optional[str]): API key for authenticating with the Firecrawl API.
116
- api_url (Optional[str]): Base URL for the Firecrawl API.
117
- """
118
- self.api_key = api_key or os.getenv('FIRECRAWL_API_KEY')
119
- self.api_url = api_url or os.getenv('FIRECRAWL_API_URL', 'https://api.firecrawl.dev')
120
-
121
- # Only require API key when using cloud service
122
- if 'api.firecrawl.dev' in self.api_url and self.api_key is None:
123
- logger.warning("No API key provided for cloud service")
124
- raise ValueError('No API key provided')
125
-
126
- logger.debug(f"Initialized FirecrawlApp with API URL: {self.api_url}")
127
-
128
- def scrape_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
129
- """
130
- Scrape the specified URL using the Firecrawl API.
131
-
132
- Args:
133
- url (str): The URL to scrape.
134
- params (Optional[Dict[str, Any]]): Additional parameters for the scrape request.
135
-
136
- Returns:
137
- Any: The scraped data if the request is successful.
138
-
139
- Raises:
140
- Exception: If the scrape request fails.
141
- """
142
-
143
- headers = self._prepare_headers()
144
-
145
- # Prepare the base scrape parameters with the URL
146
- scrape_params = {'url': url}
147
-
148
- # If there are additional params, process them
149
- if params:
150
- # Handle extract (for v1)
151
- extract = params.get('extract', {})
152
- if extract:
153
- if 'schema' in extract and hasattr(extract['schema'], 'schema'):
154
- extract['schema'] = extract['schema'].schema()
155
- scrape_params['extract'] = extract
156
-
157
- # Include any other params directly at the top level of scrape_params
158
- for key, value in params.items():
159
- if key not in ['extract']:
160
- scrape_params[key] = value
161
-
162
- json = params.get("jsonOptions", {})
163
- if json:
164
- if 'schema' in json and hasattr(json['schema'], 'schema'):
165
- json['schema'] = json['schema'].schema()
166
- scrape_params['jsonOptions'] = json
167
-
168
- # Include any other params directly at the top level of scrape_params
169
- for key, value in params.items():
170
- if key not in ['jsonOptions']:
171
- scrape_params[key] = value
172
-
173
-
174
- endpoint = f'/v1/scrape'
175
- # Make the POST request with the prepared headers and JSON data
176
- response = requests.post(
177
- f'{self.api_url}{endpoint}',
178
- headers=headers,
179
- json=scrape_params,
180
- timeout=(scrape_params["timeout"] + 5000 if "timeout" in scrape_params else None),
181
- )
182
- if response.status_code == 200:
183
- try:
184
- response = response.json()
185
- except:
186
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
187
- if response['success'] and 'data' in response:
188
- return response['data']
189
- elif "error" in response:
190
- raise Exception(f'Failed to scrape URL. Error: {response["error"]}')
191
- else:
192
- raise Exception(f'Failed to scrape URL. Error: {response}')
193
- else:
194
- self._handle_error(response, 'scrape URL')
195
-
196
- def search(self, query: str, params: Optional[Union[Dict[str, Any], SearchParams]] = None) -> Dict[str, Any]:
197
- """
198
- Search for content using the Firecrawl API.
199
-
200
- Args:
201
- query (str): The search query string.
202
- params (Optional[Union[Dict[str, Any], SearchParams]]): Additional search parameters.
203
-
204
- Returns:
205
- Dict[str, Any]: The search response containing success status and search results.
206
- """
207
- if params is None:
208
- params = {}
209
-
210
- if isinstance(params, dict):
211
- search_params = SearchParams(query=query, **params)
212
- else:
213
- search_params = params
214
- search_params.query = query
215
-
216
- response = requests.post(
217
- f"{self.api_url}/v1/search",
218
- headers={"Authorization": f"Bearer {self.api_key}"},
219
- json=search_params.dict(exclude_none=True)
220
- )
221
-
222
- if response.status_code != 200:
223
- raise Exception(f"Request failed with status code {response.status_code}")
224
-
225
- try:
226
- return response.json()
227
- except:
228
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
229
-
230
- def crawl_url(self, url: str,
231
- params: Optional[Dict[str, Any]] = None,
232
- poll_interval: Optional[int] = 2,
233
- idempotency_key: Optional[str] = None) -> Any:
234
- """
235
- Initiate a crawl job for the specified URL using the Firecrawl API.
236
-
237
- Args:
238
- url (str): The URL to crawl.
239
- params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
240
- poll_interval (Optional[int]): Time in seconds between status checks when waiting for job completion. Defaults to 2 seconds.
241
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
242
-
243
- Returns:
244
- Dict[str, Any]: A dictionary containing the crawl results. The structure includes:
245
- - 'success' (bool): Indicates if the crawl was successful.
246
- - 'status' (str): The final status of the crawl job (e.g., 'completed').
247
- - 'completed' (int): Number of scraped pages that completed.
248
- - 'total' (int): Total number of scraped pages.
249
- - 'creditsUsed' (int): Estimated number of API credits used for this crawl.
250
- - 'expiresAt' (str): ISO 8601 formatted date-time string indicating when the crawl data expires.
251
- - 'data' (List[Dict]): List of all the scraped pages.
252
-
253
- Raises:
254
- Exception: If the crawl job initiation or monitoring fails.
255
- """
256
- endpoint = f'/v1/crawl'
257
- headers = self._prepare_headers(idempotency_key)
258
- json_data = {'url': url}
259
- if params:
260
- json_data.update(params)
261
- response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
262
- if response.status_code == 200:
263
- try:
264
- id = response.json().get('id')
265
- except:
266
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
267
- return self._monitor_job_status(id, headers, poll_interval)
268
-
269
- else:
270
- self._handle_error(response, 'start crawl job')
271
-
272
-
273
- def async_crawl_url(self, url: str, params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
274
- """
275
- Initiate a crawl job asynchronously.
276
-
277
- Args:
278
- url (str): The URL to crawl.
279
- params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
280
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
281
-
282
- Returns:
283
- Dict[str, Any]: A dictionary containing the crawl initiation response. The structure includes:
284
- - 'success' (bool): Indicates if the crawl initiation was successful.
285
- - 'id' (str): The unique identifier for the crawl job.
286
- - 'url' (str): The URL to check the status of the crawl job.
287
- """
288
- endpoint = f'/v1/crawl'
289
- headers = self._prepare_headers(idempotency_key)
290
- json_data = {'url': url}
291
- if params:
292
- json_data.update(params)
293
- response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
294
- if response.status_code == 200:
295
- try:
296
- return response.json()
297
- except:
298
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
299
- else:
300
- self._handle_error(response, 'start crawl job')
301
-
302
- def check_crawl_status(self, id: str) -> Any:
303
- """
304
- Check the status of a crawl job using the Firecrawl API.
305
-
306
- Args:
307
- id (str): The ID of the crawl job.
308
-
309
- Returns:
310
- Any: The status of the crawl job.
311
-
312
- Raises:
313
- Exception: If the status check request fails.
314
- """
315
- endpoint = f'/v1/crawl/{id}'
316
-
317
- headers = self._prepare_headers()
318
- response = self._get_request(f'{self.api_url}{endpoint}', headers)
319
- if response.status_code == 200:
320
- try:
321
- status_data = response.json()
322
- except:
323
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
324
- if status_data['status'] == 'completed':
325
- if 'data' in status_data:
326
- data = status_data['data']
327
- while 'next' in status_data:
328
- if len(status_data['data']) == 0:
329
- break
330
- next_url = status_data.get('next')
331
- if not next_url:
332
- logger.warning("Expected 'next' URL is missing.")
333
- break
334
- try:
335
- status_response = self._get_request(next_url, headers)
336
- if status_response.status_code != 200:
337
- logger.error(f"Failed to fetch next page: {status_response.status_code}")
338
- break
339
- try:
340
- next_data = status_response.json()
341
- except:
342
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
343
- data.extend(next_data.get('data', []))
344
- status_data = next_data
345
- except Exception as e:
346
- logger.error(f"Error during pagination request: {e}")
347
- break
348
- status_data['data'] = data
349
-
350
- response = {
351
- 'status': status_data.get('status'),
352
- 'total': status_data.get('total'),
353
- 'completed': status_data.get('completed'),
354
- 'creditsUsed': status_data.get('creditsUsed'),
355
- 'expiresAt': status_data.get('expiresAt'),
356
- 'data': status_data.get('data')
357
- }
358
-
359
- if 'error' in status_data:
360
- response['error'] = status_data['error']
361
-
362
- if 'next' in status_data:
363
- response['next'] = status_data['next']
364
-
365
- return {
366
- 'success': False if 'error' in status_data else True,
367
- **response
368
- }
369
- else:
370
- self._handle_error(response, 'check crawl status')
371
-
372
- def check_crawl_errors(self, id: str) -> Dict[str, Any]:
373
- """
374
- Returns information about crawl errors.
375
-
376
- Args:
377
- id (str): The ID of the crawl job.
378
-
379
- Returns:
380
- Dict[str, Any]: Information about crawl errors.
381
- """
382
- headers = self._prepare_headers()
383
- response = self._get_request(f'{self.api_url}/v1/crawl/{id}/errors', headers)
384
- if response.status_code == 200:
385
- try:
386
- return response.json()
387
- except:
388
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
389
- else:
390
- self._handle_error(response, "check crawl errors")
391
-
392
- def cancel_crawl(self, id: str) -> Dict[str, Any]:
393
- """
394
- Cancel an asynchronous crawl job using the Firecrawl API.
395
-
396
- Args:
397
- id (str): The ID of the crawl job to cancel.
398
-
399
- Returns:
400
- Dict[str, Any]: The response from the cancel crawl request.
401
- """
402
- headers = self._prepare_headers()
403
- response = self._delete_request(f'{self.api_url}/v1/crawl/{id}', headers)
404
- if response.status_code == 200:
405
- try:
406
- return response.json()
407
- except:
408
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
409
- else:
410
- self._handle_error(response, "cancel crawl job")
411
-
412
- def crawl_url_and_watch(self, url: str, params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> 'CrawlWatcher':
413
- """
414
- Initiate a crawl job and return a CrawlWatcher to monitor the job via WebSocket.
415
-
416
- Args:
417
- url (str): The URL to crawl.
418
- params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
419
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
420
-
421
- Returns:
422
- CrawlWatcher: An instance of CrawlWatcher to monitor the crawl job.
423
- """
424
- crawl_response = self.async_crawl_url(url, params, idempotency_key)
425
- if crawl_response['success'] and 'id' in crawl_response:
426
- return CrawlWatcher(crawl_response['id'], self)
427
- else:
428
- raise Exception("Crawl job failed to start")
429
-
430
- def map_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
431
- """
432
- Perform a map search using the Firecrawl API.
433
-
434
- Args:
435
- url (str): The URL to perform the map search on.
436
- params (Optional[Dict[str, Any]]): Additional parameters for the map search.
437
-
438
- Returns:
439
- List[str]: A list of URLs discovered during the map search.
440
- """
441
- endpoint = f'/v1/map'
442
- headers = self._prepare_headers()
443
-
444
- # Prepare the base scrape parameters with the URL
445
- json_data = {'url': url}
446
- if params:
447
- json_data.update(params)
448
-
449
- # Make the POST request with the prepared headers and JSON data
450
- response = requests.post(
451
- f'{self.api_url}{endpoint}',
452
- headers=headers,
453
- json=json_data,
454
- )
455
- if response.status_code == 200:
456
- try:
457
- response = response.json()
458
- except:
459
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
460
- if response['success'] and 'links' in response:
461
- return response
462
- elif 'error' in response:
463
- raise Exception(f'Failed to map URL. Error: {response["error"]}')
464
- else:
465
- raise Exception(f'Failed to map URL. Error: {response}')
466
- else:
467
- self._handle_error(response, 'map')
468
-
469
- def batch_scrape_urls(self, urls: List[str],
470
- params: Optional[Dict[str, Any]] = None,
471
- poll_interval: Optional[int] = 2,
472
- idempotency_key: Optional[str] = None) -> Any:
473
- """
474
- Initiate a batch scrape job for the specified URLs using the Firecrawl API.
475
-
476
- Args:
477
- urls (List[str]): The URLs to scrape.
478
- params (Optional[Dict[str, Any]]): Additional parameters for the scraper.
479
- poll_interval (Optional[int]): Time in seconds between status checks when waiting for job completion. Defaults to 2 seconds.
480
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
481
-
482
- Returns:
483
- Dict[str, Any]: A dictionary containing the scrape results. The structure includes:
484
- - 'success' (bool): Indicates if the batch scrape was successful.
485
- - 'status' (str): The final status of the batch scrape job (e.g., 'completed').
486
- - 'completed' (int): Number of scraped pages that completed.
487
- - 'total' (int): Total number of scraped pages.
488
- - 'creditsUsed' (int): Estimated number of API credits used for this batch scrape.
489
- - 'expiresAt' (str): ISO 8601 formatted date-time string indicating when the batch scrape data expires.
490
- - 'data' (List[Dict]): List of all the scraped pages.
491
-
492
- Raises:
493
- Exception: If the batch scrape job initiation or monitoring fails.
494
- """
495
- endpoint = f'/v1/batch/scrape'
496
- headers = self._prepare_headers(idempotency_key)
497
- json_data = {'urls': urls}
498
- if params:
499
- json_data.update(params)
500
- response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
501
- if response.status_code == 200:
502
- try:
503
- id = response.json().get('id')
504
- except:
505
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
506
- return self._monitor_job_status(id, headers, poll_interval)
507
-
508
- else:
509
- self._handle_error(response, 'start batch scrape job')
510
-
511
-
512
- def async_batch_scrape_urls(self, urls: List[str], params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
513
- """
514
- Initiate a crawl job asynchronously.
515
-
516
- Args:
517
- urls (List[str]): The URLs to scrape.
518
- params (Optional[Dict[str, Any]]): Additional parameters for the scraper.
519
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
520
-
521
- Returns:
522
- Dict[str, Any]: A dictionary containing the batch scrape initiation response. The structure includes:
523
- - 'success' (bool): Indicates if the batch scrape initiation was successful.
524
- - 'id' (str): The unique identifier for the batch scrape job.
525
- - 'url' (str): The URL to check the status of the batch scrape job.
526
- """
527
- endpoint = f'/v1/batch/scrape'
528
- headers = self._prepare_headers(idempotency_key)
529
- json_data = {'urls': urls}
530
- if params:
531
- json_data.update(params)
532
- response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
533
- if response.status_code == 200:
534
- try:
535
- return response.json()
536
- except:
537
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
538
- else:
539
- self._handle_error(response, 'start batch scrape job')
540
-
541
- def batch_scrape_urls_and_watch(self, urls: List[str], params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> 'CrawlWatcher':
542
- """
543
- Initiate a batch scrape job and return a CrawlWatcher to monitor the job via WebSocket.
544
-
545
- Args:
546
- urls (List[str]): The URLs to scrape.
547
- params (Optional[Dict[str, Any]]): Additional parameters for the scraper.
548
- idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
549
-
550
- Returns:
551
- CrawlWatcher: An instance of CrawlWatcher to monitor the batch scrape job.
552
- """
553
- crawl_response = self.async_batch_scrape_urls(urls, params, idempotency_key)
554
- if crawl_response['success'] and 'id' in crawl_response:
555
- return CrawlWatcher(crawl_response['id'], self)
556
- else:
557
- raise Exception("Batch scrape job failed to start")
558
-
559
- def check_batch_scrape_status(self, id: str) -> Any:
560
- """
561
- Check the status of a batch scrape job using the Firecrawl API.
562
-
563
- Args:
564
- id (str): The ID of the batch scrape job.
565
-
566
- Returns:
567
- Any: The status of the batch scrape job.
568
-
569
- Raises:
570
- Exception: If the status check request fails.
571
- """
572
- endpoint = f'/v1/batch/scrape/{id}'
573
-
574
- headers = self._prepare_headers()
575
- response = self._get_request(f'{self.api_url}{endpoint}', headers)
576
- if response.status_code == 200:
577
- try:
578
- status_data = response.json()
579
- except:
580
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
581
- if status_data['status'] == 'completed':
582
- if 'data' in status_data:
583
- data = status_data['data']
584
- while 'next' in status_data:
585
- if len(status_data['data']) == 0:
586
- break
587
- next_url = status_data.get('next')
588
- if not next_url:
589
- logger.warning("Expected 'next' URL is missing.")
590
- break
591
- try:
592
- status_response = self._get_request(next_url, headers)
593
- if status_response.status_code != 200:
594
- logger.error(f"Failed to fetch next page: {status_response.status_code}")
595
- break
596
- try:
597
- next_data = status_response.json()
598
- except:
599
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
600
- data.extend(next_data.get('data', []))
601
- status_data = next_data
602
- except Exception as e:
603
- logger.error(f"Error during pagination request: {e}")
604
- break
605
- status_data['data'] = data
606
-
607
- response = {
608
- 'status': status_data.get('status'),
609
- 'total': status_data.get('total'),
610
- 'completed': status_data.get('completed'),
611
- 'creditsUsed': status_data.get('creditsUsed'),
612
- 'expiresAt': status_data.get('expiresAt'),
613
- 'data': status_data.get('data')
614
- }
615
-
616
- if 'error' in status_data:
617
- response['error'] = status_data['error']
618
-
619
- if 'next' in status_data:
620
- response['next'] = status_data['next']
621
-
622
- return {
623
- 'success': False if 'error' in status_data else True,
624
- **response
625
- }
626
- else:
627
- self._handle_error(response, 'check batch scrape status')
628
-
629
- def check_batch_scrape_errors(self, id: str) -> Dict[str, Any]:
630
- """
631
- Returns information about batch scrape errors.
632
-
633
- Args:
634
- id (str): The ID of the crawl job.
635
-
636
- Returns:
637
- Dict[str, Any]: Information about crawl errors.
638
- """
639
- headers = self._prepare_headers()
640
- response = self._get_request(f'{self.api_url}/v1/batch/scrape/{id}/errors', headers)
641
- if response.status_code == 200:
642
- try:
643
- return response.json()
644
- except:
645
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
646
- else:
647
- self._handle_error(response, "check batch scrape errors")
648
-
649
- def extract(self, urls: List[str], params: Optional[ExtractParams] = None) -> Any:
650
- """
651
- Extracts information from a URL using the Firecrawl API.
652
-
653
- Args:
654
- urls (List[str]): The URLs to extract information from.
655
- params (Optional[ExtractParams]): Additional parameters for the extract request.
656
-
657
- Returns:
658
- Union[ExtractResponse, ErrorResponse]: The response from the extract operation.
659
- """
660
- headers = self._prepare_headers()
661
-
662
- if not params or (not params.get('prompt') and not params.get('schema')):
663
- raise ValueError("Either prompt or schema is required")
664
-
665
- schema = params.get('schema')
666
- if schema:
667
- if hasattr(schema, 'model_json_schema'):
668
- # Convert Pydantic model to JSON schema
669
- schema = schema.model_json_schema()
670
- # Otherwise assume it's already a JSON schema dict
671
-
672
- request_data = {
673
- 'urls': urls,
674
- 'prompt': params.get('prompt', None),
675
- 'allowExternalLinks': params.get('allow_external_links', params.get('allowExternalLinks', False)),
676
- 'enableWebSearch': params.get('enable_web_search', params.get('enableWebSearch', False)),
677
- 'showSources': params.get('show_sources', params.get('showSources', False)),
678
- 'systemPrompt': params.get('system_prompt', params.get('systemPrompt', None)),
679
- 'schema': schema,
680
- 'origin': 'api-sdk'
681
- }
682
-
683
- try:
684
- # Send the initial extract request
685
- response = self._post_request(
686
- f'{self.api_url}/v1/extract',
687
- request_data,
688
- headers
689
- )
690
- if response.status_code == 200:
691
- try:
692
- data = response.json()
693
- except:
694
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
695
- if data['success']:
696
- job_id = data.get('id')
697
- if not job_id:
698
- raise Exception('Job ID not returned from extract request.')
699
-
700
- # Poll for the extract status
701
- while True:
702
- status_response = self._get_request(
703
- f'{self.api_url}/v1/extract/{job_id}',
704
- headers
705
- )
706
- if status_response.status_code == 200:
707
- try:
708
- status_data = status_response.json()
709
- except:
710
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
711
- if status_data['status'] == 'completed':
712
- if status_data['success']:
713
- return status_data
714
- else:
715
- raise Exception(f'Failed to extract. Error: {status_data["error"]}')
716
- elif status_data['status'] in ['failed', 'cancelled']:
717
- raise Exception(f'Extract job {status_data["status"]}. Error: {status_data["error"]}')
718
- else:
719
- self._handle_error(status_response, "extract-status")
720
-
721
- time.sleep(2) # Polling interval
722
- else:
723
- raise Exception(f'Failed to extract. Error: {data["error"]}')
724
- else:
725
- self._handle_error(response, "extract")
726
- except Exception as e:
727
- raise ValueError(str(e), 500)
728
-
729
- return {'success': False, 'error': "Internal server error."}
730
-
731
- def get_extract_status(self, job_id: str) -> Dict[str, Any]:
732
- """
733
- Retrieve the status of an extract job.
734
-
735
- Args:
736
- job_id (str): The ID of the extract job.
737
-
738
- Returns:
739
- Dict[str, Any]: The status of the extract job.
740
-
741
- Raises:
742
- ValueError: If there is an error retrieving the status.
743
- """
744
- headers = self._prepare_headers()
745
- try:
746
- response = self._get_request(f'{self.api_url}/v1/extract/{job_id}', headers)
747
- if response.status_code == 200:
748
- try:
749
- return response.json()
750
- except:
751
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
752
- else:
753
- self._handle_error(response, "get extract status")
754
- except Exception as e:
755
- raise ValueError(str(e), 500)
756
-
757
- def async_extract(self, urls: List[str], params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
758
- """
759
- Initiate an asynchronous extract job.
760
-
761
- Args:
762
- urls (List[str]): The URLs to extract data from.
763
- params (Optional[Dict[str, Any]]): Additional parameters for the extract request.
764
- idempotency_key (Optional[str]): A unique key to ensure idempotency of requests.
765
-
766
- Returns:
767
- Dict[str, Any]: The response from the extract operation.
768
-
769
- Raises:
770
- ValueError: If there is an error initiating the extract job.
771
- """
772
- headers = self._prepare_headers(idempotency_key)
773
-
774
- schema = params.get('schema') if params else None
775
- if schema:
776
- if hasattr(schema, 'model_json_schema'):
777
- # Convert Pydantic model to JSON schema
778
- schema = schema.model_json_schema()
779
- # Otherwise assume it's already a JSON schema dict
780
-
781
- jsonData = {'urls': urls, **(params or {})}
782
- request_data = {
783
- **jsonData,
784
- 'allowExternalLinks': params.get('allow_external_links', False) if params else False,
785
- 'schema': schema,
786
- 'origin': 'api-sdk'
787
- }
788
-
789
- try:
790
- response = self._post_request(f'{self.api_url}/v1/extract', request_data, headers)
791
- if response.status_code == 200:
792
- try:
793
- return response.json()
794
- except:
795
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
796
- else:
797
- self._handle_error(response, "async extract")
798
- except Exception as e:
799
- raise ValueError(str(e), 500)
800
-
801
- def generate_llms_text(self, url: str, params: Optional[Union[Dict[str, Any], GenerateLLMsTextParams]] = None) -> Dict[str, Any]:
802
- """
803
- Generate LLMs.txt for a given URL and poll until completion.
804
-
805
- Args:
806
- url (str): The URL to generate LLMs.txt from.
807
- params (Optional[Union[Dict[str, Any], GenerateLLMsTextParams]]): Parameters for the LLMs.txt generation.
808
-
809
- Returns:
810
- Dict[str, Any]: A dictionary containing the generation results. The structure includes:
811
- - 'success' (bool): Indicates if the generation was successful.
812
- - 'status' (str): The final status of the generation job.
813
- - 'data' (Dict): The generated LLMs.txt data.
814
- - 'error' (Optional[str]): Error message if the generation failed.
815
- - 'expiresAt' (str): ISO 8601 formatted date-time string indicating when the data expires.
816
-
817
- Raises:
818
- Exception: If the generation job fails or an error occurs during status checks.
819
- """
820
- if params is None:
821
- params = {}
822
-
823
- if isinstance(params, dict):
824
- generation_params = GenerateLLMsTextParams(**params)
825
- else:
826
- generation_params = params
827
-
828
- response = self.async_generate_llms_text(url, generation_params)
829
- if not response.get('success') or 'id' not in response:
830
- return response
831
-
832
- job_id = response['id']
833
- while True:
834
- status = self.check_generate_llms_text_status(job_id)
835
-
836
- if status['status'] == 'completed':
837
- return status
838
- elif status['status'] == 'failed':
839
- raise Exception(f'LLMs.txt generation failed. Error: {status.get("error")}')
840
- elif status['status'] != 'processing':
841
- break
842
-
843
- time.sleep(2) # Polling interval
844
-
845
- return {'success': False, 'error': 'LLMs.txt generation job terminated unexpectedly'}
846
-
847
- def async_generate_llms_text(self, url: str, params: Optional[Union[Dict[str, Any], GenerateLLMsTextParams]] = None) -> Dict[str, Any]:
848
- """
849
- Initiate an asynchronous LLMs.txt generation operation.
850
-
851
- Args:
852
- url (str): The URL to generate LLMs.txt from.
853
- params (Optional[Union[Dict[str, Any], GenerateLLMsTextParams]]): Parameters for the LLMs.txt generation.
854
-
855
- Returns:
856
- Dict[str, Any]: A dictionary containing the generation initiation response. The structure includes:
857
- - 'success' (bool): Indicates if the generation initiation was successful.
858
- - 'id' (str): The unique identifier for the generation job.
859
-
860
- Raises:
861
- Exception: If the generation job initiation fails.
862
- """
863
- if params is None:
864
- params = {}
865
-
866
- if isinstance(params, dict):
867
- generation_params = GenerateLLMsTextParams(**params)
868
- else:
869
- generation_params = params
870
-
871
- headers = self._prepare_headers()
872
- json_data = {'url': url, **generation_params.dict(exclude_none=True)}
873
-
874
- try:
875
- response = self._post_request(f'{self.api_url}/v1/llmstxt', json_data, headers)
876
- if response.status_code == 200:
877
- try:
878
- return response.json()
879
- except:
880
- raise Exception('Failed to parse Firecrawl response as JSON.')
881
- else:
882
- self._handle_error(response, 'start LLMs.txt generation')
883
- except Exception as e:
884
- raise ValueError(str(e))
885
-
886
- return {'success': False, 'error': 'Internal server error'}
887
-
888
- def check_generate_llms_text_status(self, id: str) -> Dict[str, Any]:
889
- """
890
- Check the status of a LLMs.txt generation operation.
891
-
892
- Args:
893
- id (str): The ID of the LLMs.txt generation operation.
894
-
895
- Returns:
896
- Dict[str, Any]: The current status and results of the generation operation.
897
-
898
- Raises:
899
- Exception: If the status check fails.
900
- """
901
- headers = self._prepare_headers()
902
- try:
903
- response = self._get_request(f'{self.api_url}/v1/llmstxt/{id}', headers)
904
- if response.status_code == 200:
905
- try:
906
- return response.json()
907
- except:
908
- raise Exception('Failed to parse Firecrawl response as JSON.')
909
- elif response.status_code == 404:
910
- raise Exception('LLMs.txt generation job not found')
911
- else:
912
- self._handle_error(response, 'check LLMs.txt generation status')
913
- except Exception as e:
914
- raise ValueError(str(e))
915
-
916
- return {'success': False, 'error': 'Internal server error'}
917
-
918
- def _prepare_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
919
- """
920
- Prepare the headers for API requests.
921
-
922
- Args:
923
- idempotency_key (Optional[str]): A unique key to ensure idempotency of requests.
924
-
925
- Returns:
926
- Dict[str, str]: The headers including content type, authorization, and optionally idempotency key.
927
- """
928
- if idempotency_key:
929
- return {
930
- 'Content-Type': 'application/json',
931
- 'Authorization': f'Bearer {self.api_key}',
932
- 'x-idempotency-key': idempotency_key
933
- }
934
-
935
- return {
936
- 'Content-Type': 'application/json',
937
- 'Authorization': f'Bearer {self.api_key}',
938
- }
939
-
940
- def _post_request(self, url: str,
941
- data: Dict[str, Any],
942
- headers: Dict[str, str],
943
- retries: int = 3,
944
- backoff_factor: float = 0.5) -> requests.Response:
945
- """
946
- Make a POST request with retries.
947
-
948
- Args:
949
- url (str): The URL to send the POST request to.
950
- data (Dict[str, Any]): The JSON data to include in the POST request.
951
- headers (Dict[str, str]): The headers to include in the POST request.
952
- retries (int): Number of retries for the request.
953
- backoff_factor (float): Backoff factor for retries.
954
-
955
- Returns:
956
- requests.Response: The response from the POST request.
957
-
958
- Raises:
959
- requests.RequestException: If the request fails after the specified retries.
960
- """
961
- for attempt in range(retries):
962
- response = requests.post(url, headers=headers, json=data, timeout=((data["timeout"] + 5000) if "timeout" in data else None))
963
- if response.status_code == 502:
964
- time.sleep(backoff_factor * (2 ** attempt))
965
- else:
966
- return response
967
- return response
968
-
969
- def _get_request(self, url: str,
970
- headers: Dict[str, str],
971
- retries: int = 3,
972
- backoff_factor: float = 0.5) -> requests.Response:
973
- """
974
- Make a GET request with retries.
975
-
976
- Args:
977
- url (str): The URL to send the GET request to.
978
- headers (Dict[str, str]): The headers to include in the GET request.
979
- retries (int): Number of retries for the request.
980
- backoff_factor (float): Backoff factor for retries.
981
-
982
- Returns:
983
- requests.Response: The response from the GET request.
984
-
985
- Raises:
986
- requests.RequestException: If the request fails after the specified retries.
987
- """
988
- for attempt in range(retries):
989
- response = requests.get(url, headers=headers)
990
- if response.status_code == 502:
991
- time.sleep(backoff_factor * (2 ** attempt))
992
- else:
993
- return response
994
- return response
995
-
996
- def _delete_request(self, url: str,
997
- headers: Dict[str, str],
998
- retries: int = 3,
999
- backoff_factor: float = 0.5) -> requests.Response:
1000
- """
1001
- Make a DELETE request with retries.
1002
-
1003
- Args:
1004
- url (str): The URL to send the DELETE request to.
1005
- headers (Dict[str, str]): The headers to include in the DELETE request.
1006
- retries (int): Number of retries for the request.
1007
- backoff_factor (float): Backoff factor for retries.
1008
-
1009
- Returns:
1010
- requests.Response: The response from the DELETE request.
1011
-
1012
- Raises:
1013
- requests.RequestException: If the request fails after the specified retries.
1014
- """
1015
- for attempt in range(retries):
1016
- response = requests.delete(url, headers=headers)
1017
- if response.status_code == 502:
1018
- time.sleep(backoff_factor * (2 ** attempt))
1019
- else:
1020
- return response
1021
- return response
1022
-
1023
- def _monitor_job_status(self, id: str, headers: Dict[str, str], poll_interval: int) -> Any:
1024
- """
1025
- Monitor the status of a crawl job until completion.
1026
-
1027
- Args:
1028
- id (str): The ID of the crawl job.
1029
- headers (Dict[str, str]): The headers to include in the status check requests.
1030
- poll_interval (int): Secounds between status checks.
1031
- Returns:
1032
- Any: The crawl results if the job is completed successfully.
1033
-
1034
- Raises:
1035
- Exception: If the job fails or an error occurs during status checks.
1036
- """
1037
- while True:
1038
- api_url = f'{self.api_url}/v1/crawl/{id}'
1039
-
1040
- status_response = self._get_request(api_url, headers)
1041
- if status_response.status_code == 200:
1042
- try:
1043
- status_data = status_response.json()
1044
- except:
1045
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
1046
- if status_data['status'] == 'completed':
1047
- if 'data' in status_data:
1048
- data = status_data['data']
1049
- while 'next' in status_data:
1050
- if len(status_data['data']) == 0:
1051
- break
1052
- status_response = self._get_request(status_data['next'], headers)
1053
- try:
1054
- status_data = status_response.json()
1055
- except:
1056
- raise Exception(f'Failed to parse Firecrawl response as JSON.')
1057
- data.extend(status_data.get('data', []))
1058
- status_data['data'] = data
1059
- return status_data
1060
- else:
1061
- raise Exception('Crawl job completed but no data was returned')
1062
- elif status_data['status'] in ['active', 'paused', 'pending', 'queued', 'waiting', 'scraping']:
1063
- poll_interval=max(poll_interval,2)
1064
- time.sleep(poll_interval) # Wait for the specified interval before checking again
1065
- else:
1066
- raise Exception(f'Crawl job failed or was stopped. Status: {status_data["status"]}')
1067
- else:
1068
- self._handle_error(status_response, 'check crawl status')
1069
-
1070
- def _handle_error(self, response: requests.Response, action: str) -> None:
1071
- """
1072
- Handle errors from API responses.
1073
-
1074
- Args:
1075
- response (requests.Response): The response object from the API request.
1076
- action (str): Description of the action that was being performed.
1077
-
1078
- Raises:
1079
- Exception: An exception with a message containing the status code and error details from the response.
1080
- """
1081
- try:
1082
- error_message = response.json().get('error', 'No error message provided.')
1083
- error_details = response.json().get('details', 'No additional error details provided.')
1084
- except:
1085
- raise requests.exceptions.HTTPError(f'Failed to parse Firecrawl error response as JSON. Status code: {response.status_code}', response=response)
1086
-
1087
-
1088
- if response.status_code == 402:
1089
- message = f"Payment Required: Failed to {action}. {error_message} - {error_details}"
1090
- elif response.status_code == 408:
1091
- message = f"Request Timeout: Failed to {action} as the request timed out. {error_message} - {error_details}"
1092
- elif response.status_code == 409:
1093
- message = f"Conflict: Failed to {action} due to a conflict. {error_message} - {error_details}"
1094
- elif response.status_code == 500:
1095
- message = f"Internal Server Error: Failed to {action}. {error_message} - {error_details}"
1096
- else:
1097
- message = f"Unexpected error during {action}: Status code {response.status_code}. {error_message} - {error_details}"
1098
-
1099
- # Raise an HTTPError with the custom message and attach the response
1100
- raise requests.exceptions.HTTPError(message, response=response)
1101
-
1102
- def deep_research(self, query: str, params: Optional[Union[Dict[str, Any], DeepResearchParams]] = None,
1103
- on_activity: Optional[Callable[[Dict[str, Any]], None]] = None,
1104
- on_source: Optional[Callable[[Dict[str, Any]], None]] = None) -> Dict[str, Any]:
1105
- """
1106
- Initiates a deep research operation on a given query and polls until completion.
1107
-
1108
- Args:
1109
- query (str): The query to research.
1110
- params (Optional[Union[Dict[str, Any], DeepResearchParams]]): Parameters for the deep research operation.
1111
- on_activity (Optional[Callable[[Dict[str, Any]], None]]): Optional callback to receive activity updates in real-time.
1112
-
1113
- Returns:
1114
- Dict[str, Any]: The final research results.
1115
-
1116
- Raises:
1117
- Exception: If the research operation fails.
1118
- """
1119
- if params is None:
1120
- params = {}
1121
-
1122
- if isinstance(params, dict):
1123
- research_params = DeepResearchParams(**params)
1124
- else:
1125
- research_params = params
1126
-
1127
- response = self.async_deep_research(query, research_params)
1128
- if not response.get('success') or 'id' not in response:
1129
- return response
1130
-
1131
- job_id = response['id']
1132
- last_activity_count = 0
1133
- last_source_count = 0
1134
-
1135
- while True:
1136
- status = self.check_deep_research_status(job_id)
1137
-
1138
- if on_activity and 'activities' in status:
1139
- new_activities = status['activities'][last_activity_count:]
1140
- for activity in new_activities:
1141
- on_activity(activity)
1142
- last_activity_count = len(status['activities'])
1143
-
1144
- if on_source and 'sources' in status:
1145
- new_sources = status['sources'][last_source_count:]
1146
- for source in new_sources:
1147
- on_source(source)
1148
- last_source_count = len(status['sources'])
1149
-
1150
- if status['status'] == 'completed':
1151
- return status
1152
- elif status['status'] == 'failed':
1153
- raise Exception(f'Deep research failed. Error: {status.get("error")}')
1154
- elif status['status'] != 'processing':
1155
- break
1156
-
1157
- time.sleep(2) # Polling interval
1158
-
1159
- return {'success': False, 'error': 'Deep research job terminated unexpectedly'}
1160
-
1161
- def async_deep_research(self, query: str, params: Optional[Union[Dict[str, Any], DeepResearchParams]] = None) -> Dict[str, Any]:
1162
- """
1163
- Initiates an asynchronous deep research operation.
1164
-
1165
- Args:
1166
- query (str): The query to research.
1167
- params (Optional[Union[Dict[str, Any], DeepResearchParams]]): Parameters for the deep research operation.
1168
-
1169
- Returns:
1170
- Dict[str, Any]: The response from the deep research initiation.
1171
-
1172
- Raises:
1173
- Exception: If the research initiation fails.
1174
- """
1175
- if params is None:
1176
- params = {}
1177
-
1178
- if isinstance(params, dict):
1179
- research_params = DeepResearchParams(**params)
1180
- else:
1181
- research_params = params
1182
-
1183
- headers = self._prepare_headers()
1184
- json_data = {'query': query, **research_params.dict(exclude_none=True)}
1185
-
1186
- try:
1187
- response = self._post_request(f'{self.api_url}/v1/deep-research', json_data, headers)
1188
- if response.status_code == 200:
1189
- try:
1190
- return response.json()
1191
- except:
1192
- raise Exception('Failed to parse Firecrawl response as JSON.')
1193
- else:
1194
- self._handle_error(response, 'start deep research')
1195
- except Exception as e:
1196
- raise ValueError(str(e))
1197
-
1198
- return {'success': False, 'error': 'Internal server error'}
1199
-
1200
- def check_deep_research_status(self, id: str) -> Dict[str, Any]:
1201
- """
1202
- Check the status of a deep research operation.
1203
-
1204
- Args:
1205
- id (str): The ID of the deep research operation.
1206
-
1207
- Returns:
1208
- Dict[str, Any]: The current status and results of the research operation.
1209
-
1210
- Raises:
1211
- Exception: If the status check fails.
1212
- """
1213
- headers = self._prepare_headers()
1214
- try:
1215
- response = self._get_request(f'{self.api_url}/v1/deep-research/{id}', headers)
1216
- if response.status_code == 200:
1217
- try:
1218
- return response.json()
1219
- except:
1220
- raise Exception('Failed to parse Firecrawl response as JSON.')
1221
- elif response.status_code == 404:
1222
- raise Exception('Deep research job not found')
1223
- else:
1224
- self._handle_error(response, 'check deep research status')
1225
- except Exception as e:
1226
- raise ValueError(str(e))
1227
-
1228
- return {'success': False, 'error': 'Internal server error'}
1229
-
1230
- class CrawlWatcher:
1231
- def __init__(self, id: str, app: FirecrawlApp):
1232
- self.id = id
1233
- self.app = app
1234
- self.data: List[Dict[str, Any]] = []
1235
- self.status = "scraping"
1236
- self.ws_url = f"{app.api_url.replace('http', 'ws')}/v1/crawl/{id}"
1237
- self.event_handlers = {
1238
- 'done': [],
1239
- 'error': [],
1240
- 'document': []
1241
- }
1242
-
1243
- async def connect(self):
1244
- async with websockets.connect(self.ws_url, extra_headers={"Authorization": f"Bearer {self.app.api_key}"}) as websocket:
1245
- await self._listen(websocket)
1246
-
1247
- async def _listen(self, websocket):
1248
- async for message in websocket:
1249
- msg = json.loads(message)
1250
- await self._handle_message(msg)
1251
-
1252
- def add_event_listener(self, event_type: str, handler):
1253
- if event_type in self.event_handlers:
1254
- self.event_handlers[event_type].append(handler)
1255
-
1256
- def dispatch_event(self, event_type: str, detail: Dict[str, Any]):
1257
- if event_type in self.event_handlers:
1258
- for handler in self.event_handlers[event_type]:
1259
- handler(detail)
1260
-
1261
- async def _handle_message(self, msg: Dict[str, Any]):
1262
- if msg['type'] == 'done':
1263
- self.status = 'completed'
1264
- self.dispatch_event('done', {'status': self.status, 'data': self.data, 'id': self.id})
1265
- elif msg['type'] == 'error':
1266
- self.status = 'failed'
1267
- self.dispatch_event('error', {'status': self.status, 'data': self.data, 'error': msg['error'], 'id': self.id})
1268
- elif msg['type'] == 'catchup':
1269
- self.status = msg['data']['status']
1270
- self.data.extend(msg['data'].get('data', []))
1271
- for doc in self.data:
1272
- self.dispatch_event('document', {'data': doc, 'id': self.id})
1273
- elif msg['type'] == 'document':
1274
- self.data.append(msg['data'])
1275
- self.dispatch_event('document', {'data': msg['data'], 'id': self.id})