0
+ assert 'metadata' in status_response['data'][0]
+ assert 'title' in status_response['data'][0]['metadata']
+ assert 'description' in status_response['data'][0]['metadata']
+ assert 'language' in status_response['data'][0]['metadata']
+ assert 'sourceURL' in status_response['data'][0]['metadata']
+ assert 'statusCode' in status_response['data'][0]['metadata']
+ assert 'error' not in status_response['data'][0]['metadata']
+
+def test_invalid_api_key_on_map():
+ invalid_app = FirecrawlApp(api_key="invalid_api_key", api_url=API_URL)
+ with pytest.raises(Exception) as excinfo:
+ invalid_app.map_url('https://roastmywebsite.ai')
+ assert "Unauthorized: Invalid token" in str(excinfo.value)
+
+def test_blocklisted_url_on_map():
+ app = FirecrawlApp(api_key=TEST_API_KEY, api_url=API_URL)
+ blocklisted_url = "https://facebook.com/fake-test"
+ with pytest.raises(Exception) as excinfo:
+ app.map_url(blocklisted_url)
+ assert "URL is blocked. Firecrawl currently does not support social media scraping due to policy restrictions." in str(excinfo.value)
+
+def test_successful_response_with_valid_preview_token_on_map():
+ app = FirecrawlApp(api_key="this_is_just_a_preview_token", api_url=API_URL)
+ response = app.map_url('https://roastmywebsite.ai')
+ assert response is not None
+ assert len(response) > 0
+
+def test_successful_response_for_valid_map():
+ app = FirecrawlApp(api_key=TEST_API_KEY, api_url=API_URL)
+ response = app.map_url('https://roastmywebsite.ai')
+ assert response is not None
+ assert len(response) > 0
+ assert any("https://" in link for link in response)
+ filtered_links = [link for link in response if "roastmywebsite.ai" in link]
+ assert len(filtered_links) > 0
+
+def test_search_e2e():
+ app = FirecrawlApp(api_url=API_URL, api_key=TEST_API_KEY)
+ with pytest.raises(NotImplementedError) as excinfo:
+ app.search("test query")
+ assert "Search is not supported in v1" in str(excinfo.value)
+
+# def test_llm_extraction():
+# app = FirecrawlApp(api_url=API_URL, api_key=TEST_API_KEY)
+# response = app.scrape_url("https://mendable.ai", {
+# 'extractorOptions': {
+# 'mode': 'llm-extraction',
+# 'extractionPrompt': "Based on the information on the page, find what the company's mission is and whether it supports SSO, and whether it is open source",
+# 'extractionSchema': {
+# 'type': 'object',
+# 'properties': {
+# 'company_mission': {'type': 'string'},
+# 'supports_sso': {'type': 'boolean'},
+# 'is_open_source': {'type': 'boolean'}
+# },
+# 'required': ['company_mission', 'supports_sso', 'is_open_source']
+# }
+# }
+# })
+# assert response is not None
+# assert 'llm_extraction' in response
+# llm_extraction = response['llm_extraction']
+# assert 'company_mission' in llm_extraction
+# assert isinstance(llm_extraction['supports_sso'], bool)
+# assert isinstance(llm_extraction['is_open_source'], bool)
+
+
+
\ No newline at end of file
diff --git a/apps/python-sdk/firecrawl/firecrawl.py b/apps/python-sdk/firecrawl/firecrawl.py
index 7ec0d33f..3961631e 100644
--- a/apps/python-sdk/firecrawl/firecrawl.py
+++ b/apps/python-sdk/firecrawl/firecrawl.py
@@ -12,31 +12,29 @@ Classes:
import logging
import os
import time
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, List
+import json
import requests
+import websockets
logger : logging.Logger = logging.getLogger("firecrawl")
class FirecrawlApp:
- """
- Initialize the FirecrawlApp instance.
-
- Args:
- api_key (Optional[str]): API key for authenticating with the Firecrawl API.
- api_url (Optional[str]): Base URL for the Firecrawl API.
- """
def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None) -> None:
- self.api_key = api_key or os.getenv('FIRECRAWL_API_KEY')
- if self.api_key is None:
- logger.warning("No API key provided")
- raise ValueError('No API key provided')
- else:
- logger.debug("Initialized FirecrawlApp with API key: %s", self.api_key)
+ """
+ Initialize the FirecrawlApp instance with API key, API URL.
- self.api_url = api_url or os.getenv('FIRECRAWL_API_URL', 'https://api.firecrawl.dev')
- if self.api_url != 'https://api.firecrawl.dev':
- logger.debug("Initialized FirecrawlApp with API URL: %s", self.api_url)
+ Args:
+ api_key (Optional[str]): API key for authenticating with the Firecrawl API.
+ api_url (Optional[str]): Base URL for the Firecrawl API.
+ """
+ self.api_key = api_key or os.getenv('FIRECRAWL_API_KEY')
+ self.api_url = api_url or os.getenv('FIRECRAWL_API_URL', 'https://api.firecrawl.dev')
+ if self.api_key is None:
+ logger.warning("No API key provided")
+ raise ValueError('No API key provided')
+ logger.debug(f"Initialized FirecrawlApp with API key: {self.api_key}")
def scrape_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
"""
@@ -60,24 +58,22 @@ class FirecrawlApp:
# If there are additional params, process them
if params:
- # Initialize extractorOptions if present
- extractor_options = params.get('extractorOptions', {})
- # Check and convert the extractionSchema if it's a Pydantic model
- if 'extractionSchema' in extractor_options:
- if hasattr(extractor_options['extractionSchema'], 'schema'):
- extractor_options['extractionSchema'] = extractor_options['extractionSchema'].schema()
- # Ensure 'mode' is set, defaulting to 'llm-extraction' if not explicitly provided
- extractor_options['mode'] = extractor_options.get('mode', 'llm-extraction')
- # Update the scrape_params with the processed extractorOptions
- scrape_params['extractorOptions'] = extractor_options
+ # Handle extract (for v1)
+ extract = params.get('extract', {})
+ if extract:
+ if 'schema' in extract and hasattr(extract['schema'], 'schema'):
+ extract['schema'] = extract['schema'].schema()
+ scrape_params['extract'] = extract
# Include any other params directly at the top level of scrape_params
for key, value in params.items():
- if key != 'extractorOptions':
+ if key not in ['extract']:
scrape_params[key] = value
+
+ endpoint = f'/v1/scrape'
# Make the POST request with the prepared headers and JSON data
response = requests.post(
- f'{self.api_url}/v0/scrape',
+ f'{self.api_url}{endpoint}',
headers=headers,
json=scrape_params,
)
@@ -102,32 +98,14 @@ class FirecrawlApp:
Any: The search results if the request is successful.
Raises:
+ NotImplementedError: If the search request is attempted on API version v1.
Exception: If the search request fails.
"""
- headers = self._prepare_headers()
- json_data = {'query': query}
- if params:
- json_data.update(params)
- response = requests.post(
- f'{self.api_url}/v0/search',
- headers=headers,
- json=json_data
- )
- if response.status_code == 200:
- response = response.json()
-
- if response['success'] and 'data' in response:
- return response['data']
- else:
- raise Exception(f'Failed to search. Error: {response["error"]}')
-
- else:
- self._handle_error(response, 'search')
+ raise NotImplementedError("Search is not supported in v1.")
def crawl_url(self, url: str,
params: Optional[Dict[str, Any]] = None,
- wait_until_done: bool = True,
- poll_interval: int = 2,
+ poll_interval: Optional[int] = 2,
idempotency_key: Optional[str] = None) -> Any:
"""
Initiate a crawl job for the specified URL using the Firecrawl API.
@@ -135,8 +113,7 @@ class FirecrawlApp:
Args:
url (str): The URL to crawl.
params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
- wait_until_done (bool): Whether to wait until the crawl job is completed.
- poll_interval (int): Time in seconds between status checks when waiting for job completion.
+ poll_interval (Optional[int]): Time in seconds between status checks when waiting for job completion. Defaults to 2 seconds.
idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
Returns:
@@ -145,26 +122,49 @@ class FirecrawlApp:
Raises:
Exception: If the crawl job initiation or monitoring fails.
"""
+ endpoint = f'/v1/crawl'
headers = self._prepare_headers(idempotency_key)
json_data = {'url': url}
if params:
json_data.update(params)
- response = self._post_request(f'{self.api_url}/v0/crawl', json_data, headers)
+ response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
if response.status_code == 200:
- job_id = response.json().get('jobId')
- if wait_until_done:
- return self._monitor_job_status(job_id, headers, poll_interval)
- else:
- return {'jobId': job_id}
+ id = response.json().get('id')
+ return self._monitor_job_status(id, headers, poll_interval)
+
else:
self._handle_error(response, 'start crawl job')
- def check_crawl_status(self, job_id: str) -> Any:
+
+ def async_crawl_url(self, url: str, params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
+ """
+ Initiate a crawl job asynchronously.
+
+ Args:
+ url (str): The URL to crawl.
+ params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
+ idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
+
+ Returns:
+ Dict[str, Any]: The response from the crawl initiation request.
+ """
+ endpoint = f'/v1/crawl'
+ headers = self._prepare_headers(idempotency_key)
+ json_data = {'url': url}
+ if params:
+ json_data.update(params)
+ response = self._post_request(f'{self.api_url}{endpoint}', json_data, headers)
+ if response.status_code == 200:
+ return response.json()
+ else:
+ self._handle_error(response, 'start crawl job')
+
+ def check_crawl_status(self, id: str) -> Any:
"""
Check the status of a crawl job using the Firecrawl API.
Args:
- job_id (str): The ID of the crawl job.
+ id (str): The ID of the crawl job.
Returns:
Any: The status of the crawl job.
@@ -172,13 +172,78 @@ class FirecrawlApp:
Raises:
Exception: If the status check request fails.
"""
+ endpoint = f'/v1/crawl/{id}'
+
headers = self._prepare_headers()
- response = self._get_request(f'{self.api_url}/v0/crawl/status/{job_id}', headers)
+ response = self._get_request(f'{self.api_url}{endpoint}', headers)
if response.status_code == 200:
- return response.json()
+ data = response.json()
+ return {
+ 'success': True,
+ 'status': data.get('status'),
+ 'total': data.get('total'),
+ 'completed': data.get('completed'),
+ 'creditsUsed': data.get('creditsUsed'),
+ 'expiresAt': data.get('expiresAt'),
+ 'next': data.get('next'),
+ 'data': data.get('data'),
+ 'error': data.get('error')
+ }
else:
self._handle_error(response, 'check crawl status')
+ def crawl_url_and_watch(self, url: str, params: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> 'CrawlWatcher':
+ """
+ Initiate a crawl job and return a CrawlWatcher to monitor the job via WebSocket.
+
+ Args:
+ url (str): The URL to crawl.
+ params (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
+ idempotency_key (Optional[str]): A unique uuid key to ensure idempotency of requests.
+
+ Returns:
+ CrawlWatcher: An instance of CrawlWatcher to monitor the crawl job.
+ """
+ crawl_response = self.async_crawl_url(url, params, idempotency_key)
+ if crawl_response['success'] and 'id' in crawl_response:
+ return CrawlWatcher(crawl_response['id'], self)
+ else:
+ raise Exception("Crawl job failed to start")
+
+ def map_url(self, url: str, params: Optional[Dict[str, Any]] = None) -> Any:
+ """
+ Perform a map search using the Firecrawl API.
+
+ Args:
+ url (str): The URL to perform the map search on.
+ params (Optional[Dict[str, Any]]): Additional parameters for the map search.
+
+ Returns:
+ Any: The result of the map search, typically a dictionary containing mapping data.
+ """
+ endpoint = f'/v1/map'
+ headers = self._prepare_headers()
+
+ # Prepare the base scrape parameters with the URL
+ json_data = {'url': url}
+ if params:
+ json_data.update(params)
+
+ # Make the POST request with the prepared headers and JSON data
+ response = requests.post(
+ f'{self.api_url}{endpoint}',
+ headers=headers,
+ json=json_data,
+ )
+ if response.status_code == 200:
+ response = response.json()
+ if response['success'] and 'links' in response:
+ return response['links']
+ else:
+ raise Exception(f'Failed to map URL. Error: {response["error"]}')
+ else:
+ self._handle_error(response, 'map')
+
def _prepare_headers(self, idempotency_key: Optional[str] = None) -> Dict[str, str]:
"""
Prepare the headers for API requests.
@@ -257,15 +322,14 @@ class FirecrawlApp:
return response
return response
- def _monitor_job_status(self, job_id: str, headers: Dict[str, str], poll_interval: int) -> Any:
+ def _monitor_job_status(self, id: str, headers: Dict[str, str], poll_interval: int) -> Any:
"""
Monitor the status of a crawl job until completion.
Args:
- job_id (str): The ID of the crawl job.
+ id (str): The ID of the crawl job.
headers (Dict[str, str]): The headers to include in the status check requests.
poll_interval (int): Secounds between status checks.
-
Returns:
Any: The crawl results if the job is completed successfully.
@@ -273,15 +337,23 @@ class FirecrawlApp:
Exception: If the job fails or an error occurs during status checks.
"""
while True:
- status_response = self._get_request(f'{self.api_url}/v0/crawl/status/{job_id}', headers)
+ api_url = f'{self.api_url}/v1/crawl/{id}'
+
+ status_response = self._get_request(api_url, headers)
if status_response.status_code == 200:
status_data = status_response.json()
if status_data['status'] == 'completed':
if 'data' in status_data:
- return status_data['data']
+ data = status_data['data']
+ while 'next' in status_data:
+ status_response = self._get_request(status_data['next'], headers)
+ status_data = status_response.json()
+ data.extend(status_data['data'])
+ status_data['data'] = data
+ return status_data
else:
raise Exception('Crawl job completed but no data was returned')
- elif status_data['status'] in ['active', 'paused', 'pending', 'queued', 'waiting']:
+ elif status_data['status'] in ['active', 'paused', 'pending', 'queued', 'waiting', 'scraping']:
poll_interval=max(poll_interval,2)
time.sleep(poll_interval) # Wait for the specified interval before checking again
else:
@@ -300,19 +372,66 @@ class FirecrawlApp:
Raises:
Exception: An exception with a message containing the status code and error details from the response.
"""
- error_message = response.json().get('error', 'No additional error details provided.')
+ error_message = response.json().get('error', 'No error message provided.')
+ error_details = response.json().get('details', 'No additional error details provided.')
if response.status_code == 402:
- message = f"Payment Required: Failed to {action}. {error_message}"
+ message = f"Payment Required: Failed to {action}. {error_message} - {error_details}"
elif response.status_code == 408:
- message = f"Request Timeout: Failed to {action} as the request timed out. {error_message}"
+ message = f"Request Timeout: Failed to {action} as the request timed out. {error_message} - {error_details}"
elif response.status_code == 409:
- message = f"Conflict: Failed to {action} due to a conflict. {error_message}"
+ message = f"Conflict: Failed to {action} due to a conflict. {error_message} - {error_details}"
elif response.status_code == 500:
- message = f"Internal Server Error: Failed to {action}. {error_message}"
+ message = f"Internal Server Error: Failed to {action}. {error_message} - {error_details}"
else:
- message = f"Unexpected error during {action}: Status code {response.status_code}. {error_message}"
+ message = f"Unexpected error during {action}: Status code {response.status_code}. {error_message} - {error_details}"
# Raise an HTTPError with the custom message and attach the response
raise requests.exceptions.HTTPError(message, response=response)
-
\ No newline at end of file
+
+class CrawlWatcher:
+ def __init__(self, id: str, app: FirecrawlApp):
+ self.id = id
+ self.app = app
+ self.data: List[Dict[str, Any]] = []
+ self.status = "scraping"
+ self.ws_url = f"{app.api_url.replace('http', 'ws')}/v1/crawl/{id}"
+ self.event_handlers = {
+ 'done': [],
+ 'error': [],
+ 'document': []
+ }
+
+ async def connect(self):
+ async with websockets.connect(self.ws_url, extra_headers={"Authorization": f"Bearer {self.app.api_key}"}) as websocket:
+ await self._listen(websocket)
+
+ async def _listen(self, websocket):
+ async for message in websocket:
+ msg = json.loads(message)
+ await self._handle_message(msg)
+
+ def add_event_listener(self, event_type: str, handler):
+ if event_type in self.event_handlers:
+ self.event_handlers[event_type].append(handler)
+
+ def dispatch_event(self, event_type: str, detail: Dict[str, Any]):
+ if event_type in self.event_handlers:
+ for handler in self.event_handlers[event_type]:
+ handler(detail)
+
+ async def _handle_message(self, msg: Dict[str, Any]):
+ if msg['type'] == 'done':
+ self.status = 'completed'
+ self.dispatch_event('done', {'status': self.status, 'data': self.data})
+ elif msg['type'] == 'error':
+ self.status = 'failed'
+ self.dispatch_event('error', {'status': self.status, 'data': self.data, 'error': msg['error']})
+ elif msg['type'] == 'catchup':
+ self.status = msg['data']['status']
+ self.data.extend(msg['data'].get('data', []))
+ for doc in self.data:
+ self.dispatch_event('document', doc)
+ elif msg['type'] == 'document':
+ self.data.append(msg['data'])
+ self.dispatch_event('document', msg['data'])
\ No newline at end of file
diff --git a/apps/python-sdk/firecrawl_py.egg-info/PKG-INFO b/apps/python-sdk/firecrawl_py.egg-info/PKG-INFO
deleted file mode 100644
index 288eb7a5..00000000
--- a/apps/python-sdk/firecrawl_py.egg-info/PKG-INFO
+++ /dev/null
@@ -1,179 +0,0 @@
-Metadata-Version: 2.1
-Name: firecrawl-py
-Version: 0.0.12
-Summary: Python SDK for Firecrawl API
-Home-page: https://github.com/mendableai/firecrawl
-Author: Mendable.ai
-Author-email: nick@mendable.ai
-License: GNU General Public License v3 (GPLv3)
-Project-URL: Documentation, https://docs.firecrawl.dev
-Project-URL: Source, https://github.com/mendableai/firecrawl
-Project-URL: Tracker, https://github.com/mendableai/firecrawl/issues
-Keywords: SDK API firecrawl
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
-Classifier: Natural Language :: English
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Topic :: Internet
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
-Classifier: Topic :: Software Development
-Classifier: Topic :: Software Development :: Libraries
-Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Classifier: Topic :: Text Processing
-Classifier: Topic :: Text Processing :: Indexing
-Requires-Python: >=3.8
-Description-Content-Type: text/markdown
-
-# Firecrawl Python SDK
-
-The Firecrawl Python SDK is a library that allows you to easily scrape and crawl websites, and output the data in a format ready for use with language models (LLMs). It provides a simple and intuitive interface for interacting with the Firecrawl API.
-
-## Installation
-
-To install the Firecrawl Python SDK, you can use pip:
-
-```bash
-pip install firecrawl-py
-```
-
-## Usage
-
-1. Get an API key from [firecrawl.dev](https://firecrawl.dev)
-2. Set the API key as an environment variable named `FIRECRAWL_API_KEY` or pass it as a parameter to the `FirecrawlApp` class.
-
-
-Here's an example of how to use the SDK:
-
-```python
-from firecrawl import FirecrawlApp
-
-# Initialize the FirecrawlApp with your API key
-app = FirecrawlApp(api_key='your_api_key')
-
-# Scrape a single URL
-url = 'https://mendable.ai'
-scraped_data = app.scrape_url(url)
-
-# Crawl a website
-crawl_url = 'https://mendable.ai'
-params = {
- 'pageOptions': {
- 'onlyMainContent': True
- }
-}
-crawl_result = app.crawl_url(crawl_url, params=params)
-```
-
-### Scraping a URL
-
-To scrape a single URL, use the `scrape_url` method. It takes the URL as a parameter and returns the scraped data as a dictionary.
-
-```python
-url = 'https://example.com'
-scraped_data = app.scrape_url(url)
-```
-### Extracting structured data from a URL
-
-With LLM extraction, you can easily extract structured data from any URL. We support pydantic schemas to make it easier for you too. Here is how you to use it:
-
-```python
-class ArticleSchema(BaseModel):
- title: str
- points: int
- by: str
- commentsURL: str
-
-class TopArticlesSchema(BaseModel):
- top: List[ArticleSchema] = Field(..., max_items=5, description="Top 5 stories")
-
-data = app.scrape_url('https://news.ycombinator.com', {
- 'extractorOptions': {
- 'extractionSchema': TopArticlesSchema.model_json_schema(),
- 'mode': 'llm-extraction'
- },
- 'pageOptions':{
- 'onlyMainContent': True
- }
-})
-print(data["llm_extraction"])
-```
-
-### Search for a query
-
-Used to search the web, get the most relevant results, scrap each page and return the markdown.
-
-```python
-query = 'what is mendable?'
-search_result = app.search(query)
-```
-
-### Crawling a Website
-
-To crawl a website, use the `crawl_url` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
-
-The `wait_until_done` parameter determines whether the method should wait for the crawl job to complete before returning the result. If set to `True`, the method will periodically check the status of the crawl job until it is completed or the specified `timeout` (in seconds) is reached. If set to `False`, the method will return immediately with the job ID, and you can manually check the status of the crawl job using the `check_crawl_status` method.
-
-```python
-crawl_url = 'https://example.com'
-params = {
- 'crawlerOptions': {
- 'excludes': ['blog/*'],
- 'includes': [], # leave empty for all pages
- 'limit': 1000,
- },
- 'pageOptions': {
- 'onlyMainContent': True
- }
-}
-crawl_result = app.crawl_url(crawl_url, params=params, wait_until_done=True, timeout=5)
-```
-
-If `wait_until_done` is set to `True`, the `crawl_url` method will return the crawl result once the job is completed. If the job fails or is stopped, an exception will be raised.
-
-### Checking Crawl Status
-
-To check the status of a crawl job, use the `check_crawl_status` method. It takes the job ID as a parameter and returns the current status of the crawl job.
-
-```python
-job_id = crawl_result['jobId']
-status = app.check_crawl_status(job_id)
-```
-
-## Error Handling
-
-The SDK handles errors returned by the Firecrawl API and raises appropriate exceptions. If an error occurs during a request, an exception will be raised with a descriptive error message.
-
-## Running the Tests with Pytest
-
-To ensure the functionality of the Firecrawl Python SDK, we have included end-to-end tests using `pytest`. These tests cover various aspects of the SDK, including URL scraping, web searching, and website crawling.
-
-### Running the Tests
-
-To run the tests, execute the following commands:
-
-Install pytest:
-```bash
-pip install pytest
-```
-
-Run:
-```bash
-pytest firecrawl/__tests__/e2e_withAuth/test.py
-```
-
-
-## Contributing
-
-Contributions to the Firecrawl Python SDK are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the GitHub repository.
-
-## License
-
-The Firecrawl Python SDK is open-source and released under the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/apps/python-sdk/firecrawl_py.egg-info/SOURCES.txt b/apps/python-sdk/firecrawl_py.egg-info/SOURCES.txt
deleted file mode 100644
index c25567c5..00000000
--- a/apps/python-sdk/firecrawl_py.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-README.md
-setup.py
-firecrawl/__init__.py
-firecrawl/firecrawl.py
-firecrawl_py.egg-info/PKG-INFO
-firecrawl_py.egg-info/SOURCES.txt
-firecrawl_py.egg-info/dependency_links.txt
-firecrawl_py.egg-info/requires.txt
-firecrawl_py.egg-info/top_level.txt
\ No newline at end of file
diff --git a/apps/python-sdk/firecrawl_py.egg-info/dependency_links.txt b/apps/python-sdk/firecrawl_py.egg-info/dependency_links.txt
deleted file mode 100644
index 8b137891..00000000
--- a/apps/python-sdk/firecrawl_py.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/apps/python-sdk/firecrawl_py.egg-info/requires.txt b/apps/python-sdk/firecrawl_py.egg-info/requires.txt
deleted file mode 100644
index c8d341f5..00000000
--- a/apps/python-sdk/firecrawl_py.egg-info/requires.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-requests
-pytest
-python-dotenv
diff --git a/apps/python-sdk/firecrawl_py.egg-info/top_level.txt b/apps/python-sdk/firecrawl_py.egg-info/top_level.txt
deleted file mode 100644
index 8bce1a1f..00000000
--- a/apps/python-sdk/firecrawl_py.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-firecrawl
diff --git a/apps/python-sdk/pyproject.toml b/apps/python-sdk/pyproject.toml
index 0a732c43..87cb91f1 100644
--- a/apps/python-sdk/pyproject.toml
+++ b/apps/python-sdk/pyproject.toml
@@ -10,6 +10,9 @@ readme = {file="README.md", content-type = "text/markdown"}
requires-python = ">=3.8"
dependencies = [
"requests",
+ "python-dotenv",
+ "websockets",
+ "nest-asyncio"
]
authors = [{name = "Mendable.ai",email = "nick@mendable.ai"}]
maintainers = [{name = "Mendable.ai",email = "nick@mendable.ai"}]
diff --git a/apps/python-sdk/requirements.txt b/apps/python-sdk/requirements.txt
index 1bed5881..db67ceeb 100644
--- a/apps/python-sdk/requirements.txt
+++ b/apps/python-sdk/requirements.txt
@@ -1,3 +1,5 @@
requests
pytest
-python-dotenv
\ No newline at end of file
+python-dotenv
+websockets
+nest-asyncio
\ No newline at end of file
diff --git a/apps/python-sdk/setup.py b/apps/python-sdk/setup.py
index 4978559b..8a67d1fd 100644
--- a/apps/python-sdk/setup.py
+++ b/apps/python-sdk/setup.py
@@ -30,6 +30,9 @@ setup(
'requests',
'pytest',
'python-dotenv',
+ 'websockets',
+ 'asyncio',
+ 'nest-asyncio'
],
python_requires=">=3.8",
classifiers=[
diff --git a/apps/rust-sdk/.gitignore b/apps/rust-sdk/.gitignore
new file mode 100644
index 00000000..2f7896d1
--- /dev/null
+++ b/apps/rust-sdk/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/apps/rust-sdk/CHANGELOG.md b/apps/rust-sdk/CHANGELOG.md
new file mode 100644
index 00000000..8342b9fa
--- /dev/null
+++ b/apps/rust-sdk/CHANGELOG.md
@@ -0,0 +1,7 @@
+## CHANGELOG
+
+## [0.1]
+
+### Added
+
+- [feat] Firecrawl rust sdk.
diff --git a/apps/rust-sdk/Cargo.lock b/apps/rust-sdk/Cargo.lock
new file mode 100644
index 00000000..c2b71d6d
--- /dev/null
+++ b/apps/rust-sdk/Cargo.lock
@@ -0,0 +1,1999 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "0.6.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.3.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "blake2b_simd"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+
+[[package]]
+name = "cc"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clippy"
+version = "0.0.302"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d911ee15579a3f50880d8c1d59ef6e79f9533127a3bd342462f5d584f5e8c294"
+dependencies = [
+ "term 0.5.2",
+]
+
+[[package]]
+name = "cloudabi"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "dirs"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ddf21e73e016298f5cb37d6ef8e8da8e39f91f9ec8b0df44b7deb16a9f8cd5b"
+dependencies = [
+ "log 0.3.9",
+ "regex",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "extprim"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b1a357c911c352439b460d7b375b5c85977b9db395b703dfee5a94dfb4d66a2"
+dependencies = [
+ "num-traits",
+ "rand",
+ "rustc_version",
+ "semver",
+ "serde",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
+name = "firecrawl"
+version = "0.1.0"
+dependencies = [
+ "assert_matches",
+ "clippy",
+ "dotenv",
+ "log 0.4.22",
+ "reqwest",
+ "rustfmt",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
+name = "h2"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+
+[[package]]
+name = "hyper"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
+dependencies = [
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg 1.3.0",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
+dependencies = [
+ "log 0.4.22",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc",
+ "log 0.4.22",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg 1.3.0",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.2",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
+dependencies = [
+ "autocfg 0.1.8",
+ "libc",
+ "rand_chacha",
+ "rand_core 0.4.2",
+ "rand_hc",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+dependencies = [
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
+
+[[package]]
+name = "rand_hc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_isaac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rand_jitter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
+dependencies = [
+ "libc",
+ "rand_core 0.4.2",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_os"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
+dependencies = [
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
+dependencies = [
+ "autocfg 0.1.8",
+ "rand_core 0.4.2",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+dependencies = [
+ "rand_core 0.3.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
+dependencies = [
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
+ "rust-argon2",
+]
+
+[[package]]
+name = "regex"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+ "utf8-ranges",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
+dependencies = [
+ "ucd-util",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log 0.4.22",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.15",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rust-argon2"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
+dependencies = [
+ "base64 0.13.1",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustfmt"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec940eed814db0fb7ab928c5f5025f97dc55d1c0e345e39dda2ce9f945557500"
+dependencies = [
+ "diff",
+ "env_logger",
+ "getopts",
+ "kernel32-sys",
+ "libc",
+ "log 0.3.9",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "strings",
+ "syntex_errors",
+ "syntex_syntax",
+ "term 0.4.6",
+ "toml",
+ "unicode-segmentation",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.6.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64 0.22.1",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.102.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.204"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.204"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg 1.3.0",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "strings"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa481ee1bc42fc3df8195f91f7cb43cf8f2b71b48bac40bf5381cfaf7e481f3c"
+dependencies = [
+ "log 0.3.9",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "syntex_errors"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3133289179676c9f5c5b2845bf5a2e127769f4889fcbada43035ef6bd662605e"
+dependencies = [
+ "libc",
+ "serde",
+ "serde_derive",
+ "syntex_pos",
+ "term 0.4.6",
+ "unicode-xid",
+]
+
+[[package]]
+name = "syntex_pos"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ab669fa003d208c681f874bbc76d91cc3d32550d16b5d9d2087cf477316470"
+dependencies = [
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "syntex_syntax"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03815b9f04d95828770d9c974aa39c6e1f6ef3114eb77a3ce09008a0d15dd142"
+dependencies = [
+ "bitflags 0.9.1",
+ "extprim",
+ "log 0.3.9",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "syntex_errors",
+ "syntex_pos",
+ "unicode-xid",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "term"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1"
+dependencies = [
+ "kernel32-sys",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "term"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
+dependencies = [
+ "byteorder",
+ "dirs",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "ucd-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf8-ranges"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
+
+[[package]]
+name = "uuid"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+dependencies = [
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log 0.4.22",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
diff --git a/apps/rust-sdk/Cargo.toml b/apps/rust-sdk/Cargo.toml
new file mode 100644
index 00000000..685545e2
--- /dev/null
+++ b/apps/rust-sdk/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "firecrawl"
+author="Mendable.ai"
+version = "0.1.0"
+edition = "2021"
+license = "GPL-2.0-or-later"
+homepage = "https://www.firecrawl.dev/"
+repository ="https://github.com/mendableai/firecrawl"
+description = "Rust SDK for Firecrawl API."
+authors = ["sanix-darker
"]
+
+[lib]
+path = "src/lib.rs"
+name = "firecrawl"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[dependencies]
+reqwest = { version = "^0.12", features = ["json", "blocking"] }
+serde = { version = "^1.0", features = ["derive"] }
+serde_json = "^1.0"
+log = "^0.4"
+thiserror = "^1.0"
+uuid = { version = "^1.10", features = ["v4"] }
+tokio = { version = "^1", features = ["full"] }
+
+[dev-dependencies]
+clippy = "^0.0.302"
+rustfmt = "^0.10"
+assert_matches = "^1.5"
+dotenv = "^0.15"
+tokio = { version = "1", features = ["full"] }
+
+[build-dependencies]
+tokio = { version = "1", features = ["full"] }
diff --git a/apps/rust-sdk/README.md b/apps/rust-sdk/README.md
new file mode 100644
index 00000000..54ad9097
--- /dev/null
+++ b/apps/rust-sdk/README.md
@@ -0,0 +1,181 @@
+# Firecrawl Rust SDK
+
+The Firecrawl Rust SDK is a library that allows you to easily scrape and crawl websites, and output the data in a format ready for use with language models (LLMs). It provides a simple and intuitive interface for interacting with the Firecrawl API.
+
+## Installation
+
+To install the Firecrawl Rust SDK, add the following to your `Cargo.toml`:
+
+```toml
+[dependencies]
+firecrawl = "^0.1"
+tokio = { version = "^1", features = ["full"] }
+serde = { version = "^1.0", features = ["derive"] }
+serde_json = "^1.0"
+uuid = { version = "^1.10", features = ["v4"] }
+
+[build-dependencies]
+tokio = { version = "1", features = ["full"] }
+```
+
+To add it in your codebase.
+
+## Usage
+
+1. Get an API key from [firecrawl.dev](https://firecrawl.dev)
+2. Set the API key as an environment variable named `FIRECRAWL_API_KEY` or pass it as a parameter to the `FirecrawlApp` struct.
+
+Here's an example of how to use the SDK in [example.rs](./examples/example.rs):
+All below example can start with :
+```rust
+use firecrawl::FirecrawlApp;
+
+#[tokio::main]
+async fn main() {
+ // Initialize the FirecrawlApp with the API key
+ let api_key = ...;
+ let api_url = ...;
+ let app = FirecrawlApp::new(api_key, api_url).expect("Failed to initialize FirecrawlApp");
+
+ // your code here...
+}
+```
+
+### Scraping a URL
+
+To scrape a single URL, use the `scrape_url` method. It takes the URL as a parameter and returns the scraped data as a `serde_json::Value`.
+
+```rust
+// Example scrape code...
+let scrape_result = app.scrape_url("https://example.com", None).await;
+match scrape_result {
+ Ok(data) => println!("Scrape Result:\n{}", data["markdown"]),
+ Err(e) => eprintln!("Scrape failed: {}", e),
+}
+```
+
+### Extracting structured data from a URL
+
+With LLM extraction, you can easily extract structured data from any URL. We support Serde for JSON schema validation to make it easier for you too. Here is how you use it:
+
+```rust
+let json_schema = json!({
+ "type": "object",
+ "properties": {
+ "top": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {"type": "string"},
+ "points": {"type": "number"},
+ "by": {"type": "string"},
+ "commentsURL": {"type": "string"}
+ },
+ "required": ["title", "points", "by", "commentsURL"]
+ },
+ "minItems": 5,
+ "maxItems": 5,
+ "description": "Top 5 stories on Hacker News"
+ }
+ },
+ "required": ["top"]
+});
+
+let llm_extraction_params = json!({
+ "extractorOptions": {
+ "extractionSchema": json_schema,
+ "mode": "llm-extraction"
+ },
+ "pageOptions": {
+ "onlyMainContent": true
+ }
+});
+
+// Example scrape code...
+let llm_extraction_result = app
+ .scrape_url("https://news.ycombinator.com", Some(llm_extraction_params))
+ .await;
+match llm_extraction_result {
+ Ok(data) => println!("LLM Extraction Result:\n{}", data["llm_extraction"]),
+ Err(e) => eprintln!("LLM Extraction failed: {}", e),
+}
+```
+
+### Search for a query
+
+Used to search the web, get the most relevant results, scrape each page, and return the markdown.
+
+```rust
+// Example query search code...
+let query = "what is mendable?";
+let search_result = app.search(query).await;
+match search_result {
+ Ok(data) => println!("Search Result:\n{}", data),
+ Err(e) => eprintln!("Search failed: {}", e),
+}
+```
+
+### Crawling a Website
+
+To crawl a website, use the `crawl_url` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
+
+The `wait_until_done` parameter determines whether the method should wait for the crawl job to complete before returning the result. If set to `true`, the method will periodically check the status of the crawl job until it is completed or the specified `timeout` (in seconds) is reached. If set to `false`, the method will return immediately with the job ID, and you can manually check the status of the crawl job using the `check_crawl_status` method.
+
+```rust
+let random_uuid = String::from(Uuid::new_v4());
+let idempotency_key = Some(random_uuid); // optional idempotency key
+let crawl_params = json!({
+ "crawlerOptions": {
+ "excludes": ["blog/*"]
+ }
+});
+
+// Example crawl code...
+let crawl_result = app
+ .crawl_url("https://example.com", Some(crawl_params), true, 2, idempotency_key)
+ .await;
+match crawl_result {
+ Ok(data) => println!("Crawl Result:\n{}", data),
+ Err(e) => eprintln!("Crawl failed: {}", e),
+}
+```
+
+If `wait_until_done` is set to `true`, the `crawl_url` method will return the crawl result once the job is completed. If the job fails or is stopped, an exception will be raised.
+
+### Checking Crawl Status
+
+To check the status of a crawl job, use the `check_crawl_status` method. It takes the job ID as a parameter and returns the current status of the crawl job.
+
+```rust
+let job_id = crawl_result["jobId"].as_str().expect("Job ID not found");
+let status = app.check_crawl_status(job_id).await;
+match status {
+ Ok(data) => println!("Crawl Status:\n{}", data),
+ Err(e) => eprintln!("Failed to check crawl status: {}", e),
+}
+```
+
+## Error Handling
+
+The SDK handles errors returned by the Firecrawl API and raises appropriate exceptions. If an error occurs during a request, an exception will be raised with a descriptive error message.
+
+## Running the Tests with Cargo
+
+To ensure the functionality of the Firecrawl Rust SDK, we have included end-to-end tests using `cargo`. These tests cover various aspects of the SDK, including URL scraping, web searching, and website crawling.
+
+### Running the Tests
+
+To run the tests, execute the following commands:
+```bash
+$ export $(xargs < ./tests/.env)
+$ cargo test --test e2e_with_auth
+```
+
+## Contributing
+
+Contributions to the Firecrawl Rust SDK are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the GitHub repository.
+
+## License
+
+The Firecrawl Rust SDK is open-source and released under the [AGPL License](https://www.gnu.org/licenses/agpl-3.0.en.html).
diff --git a/apps/rust-sdk/examples/example.rs b/apps/rust-sdk/examples/example.rs
new file mode 100644
index 00000000..c6b96b78
--- /dev/null
+++ b/apps/rust-sdk/examples/example.rs
@@ -0,0 +1,82 @@
+use firecrawl::FirecrawlApp;
+use serde_json::json;
+use uuid::Uuid;
+
+#[tokio::main]
+async fn main() {
+ // Initialize the FirecrawlApp with the API key
+ let api_key = Some("fc-YOUR_API_KEY".to_string());
+ let api_url = Some("http://0.0.0.0:3002".to_string());
+ let app = FirecrawlApp::new(api_key, api_url).expect("Failed to initialize FirecrawlApp");
+
+ // Scrape a website
+ let scrape_result = app.scrape_url("https://firecrawl.dev", None).await;
+ match scrape_result {
+ Ok(data) => println!("Scrape Result:\n{}", data["markdown"]),
+ Err(e) => eprintln!("Scrape failed: {}", e),
+ }
+
+ // Crawl a website
+ let random_uuid = String::from(Uuid::new_v4());
+ let idempotency_key = Some(random_uuid); // optional idempotency key
+ let crawl_params = json!({
+ "crawlerOptions": {
+ "excludes": ["blog/*"]
+ }
+ });
+ let crawl_result = app
+ .crawl_url(
+ "https://mendable.ai",
+ Some(crawl_params),
+ true,
+ 2,
+ idempotency_key,
+ )
+ .await;
+ match crawl_result {
+ Ok(data) => println!("Crawl Result:\n{}", data),
+ Err(e) => eprintln!("Crawl failed: {}", e),
+ }
+
+ // LLM Extraction with a JSON schema
+ let json_schema = json!({
+ "type": "object",
+ "properties": {
+ "top": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {"type": "string"},
+ "points": {"type": "number"},
+ "by": {"type": "string"},
+ "commentsURL": {"type": "string"}
+ },
+ "required": ["title", "points", "by", "commentsURL"]
+ },
+ "minItems": 5,
+ "maxItems": 5,
+ "description": "Top 5 stories on Hacker News"
+ }
+ },
+ "required": ["top"]
+ });
+
+ let llm_extraction_params = json!({
+ "extractorOptions": {
+ "extractionSchema": json_schema,
+ "mode": "llm-extraction"
+ },
+ "pageOptions": {
+ "onlyMainContent": true
+ }
+ });
+
+ let llm_extraction_result = app
+ .scrape_url("https://news.ycombinator.com", Some(llm_extraction_params))
+ .await;
+ match llm_extraction_result {
+ Ok(data) => println!("LLM Extraction Result:\n{}", data["llm_extraction"]),
+ Err(e) => eprintln!("LLM Extraction failed: {}", e),
+ }
+}
diff --git a/apps/rust-sdk/src/lib.rs b/apps/rust-sdk/src/lib.rs
new file mode 100644
index 00000000..a2ca75ad
--- /dev/null
+++ b/apps/rust-sdk/src/lib.rs
@@ -0,0 +1,373 @@
+/*
+*
+* - Structs and Enums:
+* FirecrawlError: Custom error enum for handling various errors.
+* FirecrawlApp: Main struct for the application, holding API key, URL, and HTTP client.
+*
+* - Initialization:
+*
+* FirecrawlApp::new initializes the struct, fetching the API key and URL from environment variables if not provided.
+*
+* - API Methods:
+* scrape_url, search, crawl_url, check_crawl_status:
+* Methods for interacting with the Firecrawl API, similar to the Python methods.
+* monitor_job_status: Polls the API to monitor the status of a crawl job until completion.
+*/
+
+use std::env;
+use std::thread;
+use std::time::Duration;
+
+use log::debug;
+use reqwest::{Client, Response};
+use serde_json::json;
+use serde_json::Value;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum FirecrawlError {
+ #[error("HTTP request failed: {0}")]
+ HttpRequestFailed(String),
+ #[error("API key not provided")]
+ ApiKeyNotProvided,
+ #[error("Failed to parse response: {0}")]
+ ResponseParseError(String),
+ #[error("Crawl job failed or stopped: {0}")]
+ CrawlJobFailed(String),
+}
+
+#[derive(Clone, Debug)]
+pub struct FirecrawlApp {
+ api_key: String,
+ api_url: String,
+ client: Client,
+}
+// the api verstion of firecrawl
+const API_VERSION: &str = "/v0";
+
+impl FirecrawlApp {
+ /// Initialize the FirecrawlApp instance.
+ ///
+ /// # Arguments:
+ /// * `api_key` (Optional[str]): API key for authenticating with the Firecrawl API.
+ /// * `api_url` (Optional[str]): Base URL for the Firecrawl API.
+ pub fn new(api_key: Option, api_url: Option) -> Result {
+ let api_key = api_key
+ .or_else(|| env::var("FIRECRAWL_API_KEY").ok())
+ .ok_or(FirecrawlError::ApiKeyNotProvided)?;
+ let api_url = api_url.unwrap_or_else(|| {
+ env::var("FIRECRAWL_API_URL")
+ .unwrap_or_else(|_| "https://api.firecrawl.dev".to_string())
+ });
+
+ debug!("Initialized FirecrawlApp with API key: {}", api_key);
+ debug!("Initialized FirecrawlApp with API URL: {}", api_url);
+
+ Ok(FirecrawlApp {
+ api_key,
+ api_url,
+ client: Client::new(),
+ })
+ }
+
+ /// Scrape the specified URL using the Firecrawl API.
+ ///
+ /// # Arguments:
+ /// * `url` (str): The URL to scrape.
+ /// * `params` (Optional[Dict[str, Any]]): Additional parameters for the scrape request.
+ ///
+ /// # Returns:
+ /// * `Any`: The scraped data if the request is successful.
+ ///
+ /// # Raises:
+ /// * `Exception`: If the scrape request fails.
+ pub async fn scrape_url(
+ &self,
+ url: &str,
+ params: Option,
+ ) -> Result {
+ let headers = self.prepare_headers(None);
+ let mut scrape_params = json!({"url": url});
+
+ if let Some(mut params) = params {
+ if let Some(extractor_options) = params.get_mut("extractorOptions") {
+ if let Some(extraction_schema) = extractor_options.get_mut("extractionSchema") {
+ if extraction_schema.is_object() && extraction_schema.get("schema").is_some() {
+ extractor_options["extractionSchema"] = extraction_schema["schema"].clone();
+ }
+ extractor_options["mode"] = extractor_options
+ .get("mode")
+ .cloned()
+ .unwrap_or_else(|| json!("llm-extraction"));
+ }
+ scrape_params["extractorOptions"] = extractor_options.clone();
+ }
+ for (key, value) in params.as_object().unwrap() {
+ if key != "extractorOptions" {
+ scrape_params[key] = value.clone();
+ }
+ }
+ }
+
+ let response = self
+ .client
+ .post(&format!("{}{}/scrape", self.api_url, API_VERSION))
+ .headers(headers)
+ .json(&scrape_params)
+ .send()
+ .await
+ .map_err(|e| FirecrawlError::HttpRequestFailed(e.to_string()))?;
+
+ self.handle_response(response, "scrape URL").await
+ }
+
+ /// Perform a search using the Firecrawl API.
+ ///
+ /// # Arguments:
+ /// * `query` (str): The search query.
+ /// * `params` (Optional[Dict[str, Any]]): Additional parameters for the search request.
+ ///
+ /// # Returns:
+ /// * `Any`: The search results if the request is successful.
+ ///
+ /// # Raises:
+ /// * `Exception`: If the search request fails.
+ pub async fn search(
+ &self,
+ query: &str,
+ params: Option,
+ ) -> Result {
+ let headers = self.prepare_headers(None);
+ let mut json_data = json!({"query": query});
+ if let Some(params) = params {
+ for (key, value) in params.as_object().unwrap() {
+ json_data[key] = value.clone();
+ }
+ }
+
+ let response = self
+ .client
+ .post(&format!("{}{}/search", self.api_url, API_VERSION))
+ .headers(headers)
+ .json(&json_data)
+ .send()
+ .await
+ .map_err(|e| FirecrawlError::HttpRequestFailed(e.to_string()))?;
+
+ self.handle_response(response, "search").await
+ }
+
+ /// Initiate a crawl job for the specified URL using the Firecrawl API.
+ ///
+ /// # Arguments:
+ /// * `url` (str): The URL to crawl.
+ /// * `params` (Optional[Dict[str, Any]]): Additional parameters for the crawl request.
+ /// * `wait_until_done` (bool): Whether to wait until the crawl job is completed.
+ /// * `poll_interval` (int): Time in seconds between status checks when waiting for job completion.
+ /// * `idempotency_key` (Optional[str]): A unique uuid key to ensure idempotency of requests.
+ ///
+ /// # Returns:
+ /// * `Any`: The crawl job ID or the crawl results if waiting until completion.
+ ///
+ /// # `Raises`:
+ /// * `Exception`: If the crawl job initiation or monitoring fails.
+ pub async fn crawl_url(
+ &self,
+ url: &str,
+ params: Option,
+ wait_until_done: bool,
+ poll_interval: u64,
+ idempotency_key: Option,
+ ) -> Result {
+ let headers = self.prepare_headers(idempotency_key);
+ let mut json_data = json!({"url": url});
+ if let Some(params) = params {
+ for (key, value) in params.as_object().unwrap() {
+ json_data[key] = value.clone();
+ }
+ }
+
+ let response = self
+ .client
+ .post(&format!("{}{}/crawl", self.api_url, API_VERSION))
+ .headers(headers.clone())
+ .json(&json_data)
+ .send()
+ .await
+ .map_err(|e| FirecrawlError::HttpRequestFailed(e.to_string()))?;
+
+ let response_json = self.handle_response(response, "start crawl job").await?;
+ let job_id = response_json["jobId"].as_str().unwrap().to_string();
+
+ if wait_until_done {
+ self.monitor_job_status(&job_id, headers, poll_interval)
+ .await
+ } else {
+ Ok(json!({"jobId": job_id}))
+ }
+ }
+
+ /// Check the status of a crawl job using the Firecrawl API.
+ ///
+ /// # Arguments:
+ /// * `job_id` (str): The ID of the crawl job.
+ ///
+ /// # Returns:
+ /// * `Any`: The status of the crawl job.
+ ///
+ /// # Raises:
+ /// * `Exception`: If the status check request fails.
+ pub async fn check_crawl_status(&self, job_id: &str) -> Result {
+ let headers = self.prepare_headers(None);
+ let response = self
+ .client
+ .get(&format!(
+ "{}{}/crawl/status/{}",
+ self.api_url, API_VERSION, job_id
+ ))
+ .headers(headers)
+ .send()
+ .await
+ .map_err(|e| FirecrawlError::HttpRequestFailed(e.to_string()))?;
+
+ self.handle_response(response, "check crawl status").await
+ }
+
+ /// Monitor the status of a crawl job until completion.
+ ///
+ /// # Arguments:
+ /// * `job_id` (str): The ID of the crawl job.
+ /// * `headers` (Dict[str, str]): The headers to include in the status check requests.
+ /// * `poll_interval` (int): Secounds between status checks.
+ ///
+ /// # Returns:
+ /// * `Any`: The crawl results if the job is completed successfully.
+ ///
+ /// # Raises:
+ /// Exception: If the job fails or an error occurs during status checks.
+ async fn monitor_job_status(
+ &self,
+ job_id: &str,
+ headers: reqwest::header::HeaderMap,
+ poll_interval: u64,
+ ) -> Result {
+ loop {
+ let response = self
+ .client
+ .get(&format!(
+ "{}{}/crawl/status/{}",
+ self.api_url, API_VERSION, job_id
+ ))
+ .headers(headers.clone())
+ .send()
+ .await
+ .map_err(|e| FirecrawlError::HttpRequestFailed(e.to_string()))?;
+
+ let status_data = self.handle_response(response, "check crawl status").await?;
+ match status_data["status"].as_str() {
+ Some("completed") => {
+ if status_data["data"].is_object() {
+ return Ok(status_data["data"].clone());
+ } else {
+ return Err(FirecrawlError::CrawlJobFailed(
+ "Crawl job completed but no data was returned".to_string(),
+ ));
+ }
+ }
+ Some("active") | Some("paused") | Some("pending") | Some("queued")
+ | Some("waiting") => {
+ thread::sleep(Duration::from_secs(poll_interval));
+ }
+ Some(status) => {
+ return Err(FirecrawlError::CrawlJobFailed(format!(
+ "Crawl job failed or was stopped. Status: {}",
+ status
+ )));
+ }
+ None => {
+ return Err(FirecrawlError::CrawlJobFailed(
+ "Unexpected response: no status field".to_string(),
+ ));
+ }
+ }
+ }
+ }
+
+ /// Prepare the headers for API requests.
+ ///
+ /// # Arguments:
+ /// `idempotency_key` (Optional[str]): A unique key to ensure idempotency of requests.
+ ///
+ /// # Returns:
+ /// Dict[str, str]: The headers including content type, authorization, and optionally idempotency key.
+ fn prepare_headers(&self, idempotency_key: Option) -> reqwest::header::HeaderMap {
+ let mut headers = reqwest::header::HeaderMap::new();
+ headers.insert("Content-Type", "application/json".parse().unwrap());
+ headers.insert(
+ "Authorization",
+ format!("Bearer {}", self.api_key).parse().unwrap(),
+ );
+ if let Some(key) = idempotency_key {
+ headers.insert("x-idempotency-key", key.parse().unwrap());
+ }
+ headers
+ }
+
+ /// Handle errors from API responses.
+ ///
+ /// # Arguments:
+ /// * `response` (requests.Response): The response object from the API request.
+ /// * `action` (str): Description of the action that was being performed.
+ ///
+ /// # Raises:
+ /// Exception: An exception with a message containing the status code and error details from the response.
+ async fn handle_response(
+ &self,
+ response: Response,
+ action: &str,
+ ) -> Result {
+ if response.status().is_success() {
+ let response_json: Value = response
+ .json()
+ .await
+ .map_err(|e| FirecrawlError::ResponseParseError(e.to_string()))?;
+ if response_json["success"].as_bool().unwrap_or(false) {
+ Ok(response_json["data"].clone())
+ } else {
+ Err(FirecrawlError::HttpRequestFailed(format!(
+ "Failed to {}: {}",
+ action, response_json["error"]
+ )))
+ }
+ } else {
+ let status_code = response.status().as_u16();
+ let error_message = response
+ .json::()
+ .await
+ .unwrap_or_else(|_| json!({"error": "No additional error details provided."}));
+ let message = match status_code {
+ 402 => format!(
+ "Payment Required: Failed to {}. {}",
+ action, error_message["error"]
+ ),
+ 408 => format!(
+ "Request Timeout: Failed to {} as the request timed out. {}",
+ action, error_message["error"]
+ ),
+ 409 => format!(
+ "Conflict: Failed to {} due to a conflict. {}",
+ action, error_message["error"]
+ ),
+ 500 => format!(
+ "Internal Server Error: Failed to {}. {}",
+ action, error_message["error"]
+ ),
+ _ => format!(
+ "Unexpected error during {}: Status code {}. {}",
+ action, status_code, error_message["error"]
+ ),
+ };
+ Err(FirecrawlError::HttpRequestFailed(message))
+ }
+ }
+}
diff --git a/apps/go-sdk/firecrawl/.env.example b/apps/rust-sdk/tests/.env.example
similarity index 50%
rename from apps/go-sdk/firecrawl/.env.example
rename to apps/rust-sdk/tests/.env.example
index 772a6243..5aa1cb11 100644
--- a/apps/go-sdk/firecrawl/.env.example
+++ b/apps/rust-sdk/tests/.env.example
@@ -1,2 +1,2 @@
API_URL=http://localhost:3002
-TEST_API_KEY=fc-YOUR-API-KEY
+TEST_API_KEY=fc-YOUR_API_KEY
diff --git a/apps/rust-sdk/tests/e2e_with_auth.rs b/apps/rust-sdk/tests/e2e_with_auth.rs
new file mode 100644
index 00000000..ac9dc1d3
--- /dev/null
+++ b/apps/rust-sdk/tests/e2e_with_auth.rs
@@ -0,0 +1,174 @@
+use assert_matches::assert_matches;
+use dotenv::dotenv;
+use firecrawl::FirecrawlApp;
+use serde_json::json;
+use std::env;
+use std::time::Duration;
+use tokio::time::sleep;
+
+#[tokio::test]
+async fn test_no_api_key() {
+ dotenv().ok();
+ let api_url = env::var("API_URL").expect("API_URL environment variable is not set");
+ assert_matches!(FirecrawlApp::new(None, Some(api_url)), Err(e) if e.to_string() == "API key not provided");
+}
+
+#[tokio::test]
+async fn test_blocklisted_url() {
+ dotenv().ok();
+ let api_url = env::var("API_URL").unwrap();
+ let api_key = env::var("TEST_API_KEY").unwrap();
+ let app = FirecrawlApp::new(Some(api_key), Some(api_url)).unwrap();
+ let blocklisted_url = "https://facebook.com/fake-test";
+ let result = app.scrape_url(blocklisted_url, None).await;
+
+ assert_matches!(
+ result,
+ Err(e) if e.to_string().contains("Firecrawl currently does not support social media scraping due to policy restrictions")
+ );
+}
+
+#[tokio::test]
+async fn test_successful_response_with_valid_preview_token() {
+ dotenv().ok();
+ let api_url = env::var("API_URL").unwrap();
+ let app = FirecrawlApp::new(
+ Some("this_is_just_a_preview_token".to_string()),
+ Some(api_url),
+ )
+ .unwrap();
+ let result = app
+ .scrape_url("https://roastmywebsite.ai", None)
+ .await
+ .unwrap();
+ assert!(result.as_object().unwrap().contains_key("content"));
+ assert!(result["content"].as_str().unwrap().contains("_Roast_"));
+}
+
+#[tokio::test]
+async fn test_scrape_url_e2e() {
+ dotenv().ok();
+ let api_url = env::var("API_URL").unwrap();
+ let api_key = env::var("TEST_API_KEY").unwrap();
+ let app = FirecrawlApp::new(Some(api_key), Some(api_url)).unwrap();
+ let result = app
+ .scrape_url("https://roastmywebsite.ai", None)
+ .await
+ .unwrap();
+ assert!(result.as_object().unwrap().contains_key("content"));
+ assert!(result.as_object().unwrap().contains_key("markdown"));
+ assert!(result.as_object().unwrap().contains_key("metadata"));
+ assert!(!result.as_object().unwrap().contains_key("html"));
+ assert!(result["content"].as_str().unwrap().contains("_Roast_"));
+}
+
+#[tokio::test]
+async fn test_successful_response_with_valid_api_key_and_include_html() {
+ dotenv().ok();
+ let api_url = env::var("API_URL").unwrap();
+ let api_key = env::var("TEST_API_KEY").unwrap();
+ let app = FirecrawlApp::new(Some(api_key), Some(api_url)).unwrap();
+ let params = json!({
+ "pageOptions": {
+ "includeHtml": true
+ }
+ });
+ let result = app
+ .scrape_url("https://roastmywebsite.ai", Some(params))
+ .await
+ .unwrap();
+ assert!(result.as_object().unwrap().contains_key("content"));
+ assert!(result.as_object().unwrap().contains_key("markdown"));
+ assert!(result.as_object().unwrap().contains_key("html"));
+ assert!(result.as_object().unwrap().contains_key("metadata"));
+ assert!(result["content"].as_str().unwrap().contains("_Roast_"));
+ assert!(result["markdown"].as_str().unwrap().contains("_Roast_"));
+ assert!(result["html"].as_str().unwrap().contains(" {
describe("Scraping website tests with a dataset", () => {
it("Should scrape the website and prompt it against OpenAI", async () => {
+ let totalTimeTaken = 0;
let passedTests = 0;
const batchSize = 15; // Adjusted to comply with the rate limit of 15 per minute
const batchPromises = [];
@@ -51,11 +52,16 @@ describe("Scraping Checkup (E2E)", () => {
const batchPromise = Promise.all(
batch.map(async (websiteData: WebsiteData) => {
try {
+ const startTime = new Date().getTime();
const scrapedContent = await request(TEST_URL || "")
- .post("/v0/scrape")
+ .post("/v1/scrape")
.set("Content-Type", "application/json")
.set("Authorization", `Bearer ${process.env.TEST_API_KEY}`)
- .send({ url: websiteData.website, pageOptions: { onlyMainContent: true } });
+ .send({ url: websiteData.website });
+
+ const endTime = new Date().getTime();
+ const timeTaken = endTime - startTime;
+ totalTimeTaken += timeTaken;
if (scrapedContent.statusCode !== 200) {
console.error(`Failed to scrape ${websiteData.website} ${scrapedContent.statusCode}`);
@@ -165,6 +171,7 @@ describe("Scraping Checkup (E2E)", () => {
const timeTaken = (endTime - startTime) / 1000;
console.log(`Score: ${score}%`);
console.log(`Total tokens: ${totalTokens}`);
+ console.log(`Total time taken: ${totalTimeTaken} miliseconds`);
await logErrors(errorLog, timeTaken, totalTokens, score, websitesData.length);
diff --git a/apps/test-suite/utils/supabase.ts b/apps/test-suite/utils/supabase.ts
index abf7fd78..a1549e24 100644
--- a/apps/test-suite/utils/supabase.ts
+++ b/apps/test-suite/utils/supabase.ts
@@ -1,5 +1,6 @@
import { createClient, SupabaseClient } from "@supabase/supabase-js";
-import "dotenv/config";
+import { configDotenv } from "dotenv";
+configDotenv();
// SupabaseService class initializes the Supabase client conditionally based on environment variables.
class SupabaseService {
@@ -9,7 +10,8 @@ class SupabaseService {
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceToken = process.env.SUPABASE_SERVICE_TOKEN;
// Only initialize the Supabase client if both URL and Service Token are provided.
- if (process.env.USE_DB_AUTHENTICATION === "false") {
+ const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true';
+ if (!useDbAuthentication) {
// Warn the user that Authentication is disabled by setting the client to null
console.warn(
"Authentication is disabled. Supabase client will not be initialized."
@@ -36,7 +38,8 @@ export const supabase_service: SupabaseClient = new Proxy(
new SupabaseService(),
{
get: function (target, prop, receiver) {
- if (process.env.USE_DB_AUTHENTICATION === "false") {
+ const useDbAuthentication = process.env.USE_DB_AUTHENTICATION === 'true';
+ if (!useDbAuthentication) {
console.debug(
"Attempted to access Supabase client when it's not configured."
);
diff --git a/apps/ui/ingestion-ui/README.md b/apps/ui/ingestion-ui/README.md
index e6b49b95..61f9f983 100644
--- a/apps/ui/ingestion-ui/README.md
+++ b/apps/ui/ingestion-ui/README.md
@@ -20,7 +20,7 @@ This template provides an easy way to spin up a UI for Firecrawl using React. It
```
2. Set up your Firecrawl API key:
- Open `src/components/FirecrawlComponent.tsx` and replace the placeholder API key:
+ Open `src/components/ingestion.tsx` and replace the placeholder API key:
```typescript
const FIRECRAWL_API_KEY = "your-api-key-here";
@@ -36,7 +36,7 @@ This template provides an easy way to spin up a UI for Firecrawl using React. It
## Customization
-The main Firecrawl component is located in `src/components/FirecrawlComponent.tsx`. You can modify this file to customize the UI or add additional features.
+The main Firecrawl component is located in `src/components/ingestion.tsx`. You can modify this file to customize the UI or add additional features.
## Security Considerations
diff --git a/apps/ui/ingestion-ui/package-lock.json b/apps/ui/ingestion-ui/package-lock.json
index 7038a1f2..e48e99b8 100644
--- a/apps/ui/ingestion-ui/package-lock.json
+++ b/apps/ui/ingestion-ui/package-lock.json
@@ -11,6 +11,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -1192,6 +1193,32 @@
}
}
},
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
+ "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
@@ -1220,6 +1247,21 @@
}
}
},
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
@@ -1304,6 +1346,69 @@
}
}
},
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz",
+ "integrity": "sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-presence": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-roving-focus": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
+ "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
diff --git a/apps/ui/ingestion-ui/package.json b/apps/ui/ingestion-ui/package.json
index 48009648..01a754b2 100644
--- a/apps/ui/ingestion-ui/package.json
+++ b/apps/ui/ingestion-ui/package.json
@@ -13,6 +13,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
diff --git a/apps/ui/ingestion-ui/src/App.tsx b/apps/ui/ingestion-ui/src/App.tsx
index eb0e6954..b80a5ad8 100644
--- a/apps/ui/ingestion-ui/src/App.tsx
+++ b/apps/ui/ingestion-ui/src/App.tsx
@@ -1,9 +1,35 @@
+import { useState } from "react";
import FirecrawlComponent from "./components/ingestion";
+import FirecrawlComponentV1 from "./components/ingestionV1";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Label } from "@/components/ui/label";
function App() {
+ const [selectedComponent, setSelectedComponent] = useState<"v0" | "v1">("v1");
+
return (
<>
-
+
+
setSelectedComponent(value as "v0" | "v1")}
+ className="flex space-x-6 mt-6"
+ >
+
+
+ Firecrawl Component V0
+
+
+
+ Firecrawl Component V1
+
+
+
+ {selectedComponent === "v1" ? (
+
+ ) : (
+
+ )}
>
);
}
diff --git a/apps/ui/ingestion-ui/src/components/ingestionV1.tsx b/apps/ui/ingestion-ui/src/components/ingestionV1.tsx
new file mode 100644
index 00000000..b34c0d6b
--- /dev/null
+++ b/apps/ui/ingestion-ui/src/components/ingestionV1.tsx
@@ -0,0 +1,603 @@
+import { useState, ChangeEvent, FormEvent, useEffect } from "react";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardContent,
+ CardFooter,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
+
+//! Hardcoded values (not recommended for production)
+//! Highly recommended to move all Firecrawl API calls to the backend (e.g. Next.js API route)
+const FIRECRAWL_API_URL = "https://api.firecrawl.dev"; // Replace with your actual API URL whether it is local or using Firecrawl Cloud
+const FIRECRAWL_API_KEY = "fc-YOUR_API_KEY"; // Replace with your actual API key
+
+interface FormData {
+ url: string;
+ crawlSubPages: boolean;
+ search: string;
+ limit: string;
+ maxDepth: string;
+ excludePaths: string;
+ includePaths: string;
+ extractMainContent: boolean;
+}
+
+interface CrawlerOptions {
+ includes?: string[];
+ excludes?: string[];
+ maxDepth?: number;
+ limit?: number;
+ returnOnlyUrls: boolean;
+}
+
+interface ScrapeOptions {
+ formats?: string[];
+ onlyMainContent?: boolean;
+}
+
+interface PageOptions {
+ onlyMainContent: boolean;
+}
+
+interface RequestBody {
+ url: string;
+ crawlerOptions?: CrawlerOptions;
+ pageOptions?: PageOptions;
+ search?: string;
+ excludePaths?: string[];
+ includePaths?: string[];
+ maxDepth?: number;
+ limit?: number;
+ scrapeOptions?: ScrapeOptions;
+ formats?: string[];
+}
+
+interface ScrapeResultMetadata {
+ title: string;
+ description: string;
+ language: string;
+ sourceURL: string;
+ pageStatusCode: number;
+ pageError?: string;
+ [key: string]: string | number | undefined;
+}
+
+interface ScrapeResultData {
+ markdown: string;
+ content: string;
+ html: string;
+ rawHtml: string;
+ metadata: ScrapeResultMetadata;
+ llm_extraction: Record;
+ warning?: string;
+}
+
+interface ScrapeResult {
+ success: boolean;
+ data: ScrapeResultData;
+}
+
+export default function FirecrawlComponentV1() {
+ const [formData, setFormData] = useState({
+ url: "",
+ crawlSubPages: false,
+ search: "",
+ limit: "",
+ maxDepth: "",
+ excludePaths: "",
+ includePaths: "",
+ extractMainContent: false,
+ });
+ const [loading, setLoading] = useState(false);
+ const [scrapingSelectedLoading, setScrapingSelectedLoading] =
+ useState(false);
+ const [crawledUrls, setCrawledUrls] = useState([]);
+ const [selectedUrls, setSelectedUrls] = useState([]);
+ const [scrapeResults, setScrapeResults] = useState<
+ Record
+ >({});
+ const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(true);
+ const [crawlStatus, setCrawlStatus] = useState<{
+ current: number;
+ total: number | null;
+ }>({ current: 0, total: null });
+ const [elapsedTime, setElapsedTime] = useState(0);
+ const [showCrawlStatus, setShowCrawlStatus] = useState(false);
+ const [isScraping, setIsScraping] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const urlsPerPage = 10;
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (loading) {
+ setShowCrawlStatus(true);
+ timer = setInterval(() => {
+ setElapsedTime((prevTime) => prevTime + 1);
+ }, 1000);
+ }
+ return () => {
+ if (timer) clearInterval(timer);
+ };
+ }, [loading]);
+
+ const handleChange = (e: ChangeEvent) => {
+ const { name, value, type, checked } = e.target;
+ setFormData((prevData) => {
+ const newData = {
+ ...prevData,
+ [name]: type === "checkbox" ? checked : value,
+ };
+
+ // Automatically check "Crawl Sub-pages" if limit or search have content
+ if (name === "limit" || name === "search") {
+ newData.crawlSubPages = !!value || !!newData.limit || !!newData.search;
+ }
+
+ return newData;
+ });
+ };
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setIsCollapsibleOpen(false);
+ setElapsedTime(0);
+ setCrawlStatus({ current: 0, total: null });
+ setIsScraping(!formData.crawlSubPages);
+ setCrawledUrls([]);
+ setSelectedUrls([]);
+ setScrapeResults({});
+ setScrapingSelectedLoading(false);
+ setShowCrawlStatus(false);
+
+ try {
+ const endpoint = `${FIRECRAWL_API_URL}/v1/${
+ formData.crawlSubPages ? "map" : "scrape"
+ }`;
+
+ const requestBody: RequestBody = formData.crawlSubPages
+ ? {
+ url: formData.url,
+ search: formData.search || undefined,
+ limit: formData.limit ? parseInt(formData.limit) : undefined,
+ }
+ : {
+ url: formData.url,
+ formats: ["markdown"],
+ };
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${FIRECRAWL_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ if (formData.crawlSubPages) {
+ if (data.success === true && Array.isArray(data.links)) {
+ setCrawledUrls(data.links);
+ setSelectedUrls(data.links);
+ setCrawlStatus({
+ current: data.links.length,
+ total: data.links.length,
+ });
+
+ // Set scrape results with the links
+ const linkResults: Record = {};
+ data.links.forEach((link: string) => {
+ linkResults[link] = {
+ success: true,
+ data: {
+ metadata: {
+ sourceURL: link,
+ title: "",
+ description: "",
+ language: "",
+ pageStatusCode: 200,
+ },
+ markdown: "",
+ content: "",
+ html: "",
+ rawHtml: "",
+ llm_extraction: {},
+ },
+ };
+ });
+ } else {
+ console.error("Unexpected response format from map endpoint");
+ console.log(data);
+ }
+ } else {
+ setScrapeResults({ [formData.url]: data });
+ setCrawlStatus({ current: 1, total: 1 });
+ }
+ } catch (error) {
+ console.error("Error:", error);
+ setScrapeResults({
+ error: {
+ success: false,
+ data: {
+ metadata: {
+ pageError: "Error occurred while fetching data",
+ title: "",
+ description: "",
+ language: "",
+ sourceURL: "",
+ pageStatusCode: 0,
+ },
+ markdown: "",
+ content: "",
+ html: "",
+ rawHtml: "",
+ llm_extraction: {},
+ },
+ },
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleScrapeSelected = async () => {
+ setLoading(true);
+ setElapsedTime(0);
+ setCrawlStatus({ current: 0, total: selectedUrls.length });
+ setIsScraping(true);
+ setScrapingSelectedLoading(true);
+ const newScrapeResults: Record = {};
+
+ for (const [index, url] of selectedUrls.entries()) {
+ try {
+ const response = await fetch(`${FIRECRAWL_API_URL}/v1/scrape`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${FIRECRAWL_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ url: url,
+ formats: ["markdown"],
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data: ScrapeResult = await response.json();
+ newScrapeResults[url] = data;
+ setCrawlStatus((prev) => ({ ...prev, current: index + 1 }));
+ setScrapeResults({ ...scrapeResults, ...newScrapeResults });
+ } catch (error) {
+ console.error(`Error scraping ${url}:`, error);
+ newScrapeResults[url] = {
+ success: false,
+ data: {
+ markdown: "",
+ content: "",
+ html: "",
+ rawHtml: "",
+ metadata: {
+ title: "",
+ description: "",
+ language: "",
+ sourceURL: url,
+ pageStatusCode: 0,
+ pageError: (error as Error).message,
+ },
+ llm_extraction: {},
+ },
+ };
+ }
+ }
+
+ setLoading(false);
+ setIsScraping(false);
+ };
+
+ const handlePageChange = (newPage: number) => {
+ setCurrentPage(newPage);
+ };
+
+ const paginatedUrls = crawledUrls.slice(
+ (currentPage - 1) * urlsPerPage,
+ currentPage * urlsPerPage
+ );
+
+ return (
+
+
+
+
+ Extract web content (V1)
+
+ Powered by Firecrawl 🔥
+
+
+
+ Use this component to quickly give your users the ability to connect
+ their AI apps to web data with Firecrawl. Learn more on the{" "}
+
+ Firecrawl docs!
+
+
+
+
+
+ {showCrawlStatus && (
+
+
+ {!isScraping &&
+ crawledUrls.length > 0 &&
+ !scrapingSelectedLoading && (
+ <>
+ {
+ if (checked) {
+ setSelectedUrls([...crawledUrls]);
+ } else {
+ setSelectedUrls([]);
+ }
+ }}
+ />
+
+ {selectedUrls.length === crawledUrls.length
+ ? `Unselect All (${selectedUrls.length})`
+ : `Select All (${selectedUrls.length})`}
+
+ >
+ )}
+
+
+ {isScraping
+ ? `Scraped ${crawlStatus.current} page(s) in ${elapsedTime}s`
+ : `Crawled ${crawlStatus.current} pages in ${elapsedTime}s`}
+
+
+ )}
+
+ {crawledUrls.length > 0 &&
+ !scrapingSelectedLoading &&
+ !isScraping && (
+ <>
+
+ {paginatedUrls.map((url, index) => (
+
+
+ setSelectedUrls((prev) =>
+ prev.includes(url)
+ ? prev.filter((u) => u !== url)
+ : [...prev, url]
+ )
+ }
+ />
+
+ {url.length > 70 ? `${url.slice(0, 70)}...` : url}
+
+
+ ))}
+
+
+ handlePageChange(currentPage - 1)}
+ disabled={currentPage === 1}
+ >
+
+
+
+ Page {currentPage} of{" "}
+ {Math.ceil(crawledUrls.length / urlsPerPage)}
+
+ handlePageChange(currentPage + 1)}
+ disabled={currentPage * urlsPerPage >= crawledUrls.length}
+ >
+
+
+
+ >
+ )}
+
+
+ {crawledUrls.length > 0 && !scrapingSelectedLoading && (
+
+ Scrape Selected URLs
+
+ )}
+
+
+
+ {Object.keys(scrapeResults).length > 0 && (
+
+
Scrape Results
+
+ You can do whatever you want with the scrape results. Here is a
+ basic showcase of the markdown.
+
+
+ {Object.entries(scrapeResults).map(([url, result]) => (
+
+
+ {result.data.metadata.title}
+
+ {url
+ .replace(/^(https?:\/\/)?(www\.)?/, "")
+ .replace(/\/$/, "")}
+
+
+
+
+ {result.success ? (
+ <>
+
+ {result.data.markdown.trim()}
+
+ >
+ ) : (
+ <>
+
+ Failed to scrape this URL
+
+
+ {result.toString()}
+
+ >
+ )}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/apps/ui/ingestion-ui/src/components/ui/radio-group.tsx b/apps/ui/ingestion-ui/src/components/ui/radio-group.tsx
new file mode 100644
index 00000000..43b43b48
--- /dev/null
+++ b/apps/ui/ingestion-ui/src/components/ui/radio-group.tsx
@@ -0,0 +1,42 @@
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 8c160f4a..24b51762 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -15,7 +15,6 @@ x-common-service: &common-service
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
- MODEL_NAME=${MODEL_NAME:-gpt-4o}
- SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL}
- - SERPER_API_KEY=${SERPER_API_KEY}
- LLAMAPARSE_API_KEY=${LLAMAPARSE_API_KEY}
- LOGTAIL_KEY=${LOGTAIL_KEY}
- BULL_AUTH_KEY=${BULL_AUTH_KEY}
diff --git a/examples/kubernetes/cluster-install/secret.yaml b/examples/kubernetes/cluster-install/secret.yaml
index 2be96320..6d8eed3b 100644
--- a/examples/kubernetes/cluster-install/secret.yaml
+++ b/examples/kubernetes/cluster-install/secret.yaml
@@ -6,7 +6,6 @@ type: Opaque
data:
OPENAI_API_KEY: ""
SLACK_WEBHOOK_URL: ""
- SERPER_API_KEY: ""
LLAMAPARSE_API_KEY: ""
LOGTAIL_KEY: ""
BULL_AUTH_KEY: ""
diff --git a/examples/simple_web_data_extraction_with_claude/simple_web_data_extraction_with_claude.ipynb b/examples/simple_web_data_extraction_with_claude/simple_web_data_extraction_with_claude.ipynb
new file mode 100644
index 00000000..ee14f147
--- /dev/null
+++ b/examples/simple_web_data_extraction_with_claude/simple_web_data_extraction_with_claude.ipynb
@@ -0,0 +1,259 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Web Scraping and Extraction with Firecrawl and Claude\n",
+ "\n",
+ "This notebook demonstrates how to use Firecrawl to scrape web content and Claude to extract structured data from it."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 1: Import Required Libraries"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "True"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import os\n",
+ "import json\n",
+ "from firecrawl import FirecrawlApp\n",
+ "from anthropic import Anthropic\n",
+ "from dotenv import load_dotenv\n",
+ "\n",
+ "# Load environment variables\n",
+ "load_dotenv()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 2: Set Up API Keys and URL"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "URL to scrape: https://mendable.ai\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Retrieve API keys from environment variables\n",
+ "anthropic_api_key = os.getenv(\"ANTHROPIC_API_KEY\")\n",
+ "firecrawl_api_key = os.getenv(\"FIRECRAWL_API_KEY\")\n",
+ "\n",
+ "# Set the URL to scrape\n",
+ "url = \"https://mendable.ai\" # Replace with the actual URL you want to scrape\n",
+ "\n",
+ "print(f\"URL to scrape: {url}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 3: Initialize Firecrawl and Anthropic Clients"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Firecrawl and Anthropic clients initialized.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Initialize FirecrawlApp and Anthropic client\n",
+ "firecrawl_app = FirecrawlApp(api_key=firecrawl_api_key)\n",
+ "anthropic_client = Anthropic(api_key=anthropic_api_key)\n",
+ "\n",
+ "print(\"Firecrawl and Anthropic clients initialized.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 4: Scrape the URL using Firecrawl"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Page content scraped. Length: 16199 characters\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Scrape the URL using Firecrawl\n",
+ "page_content = firecrawl_app.scrape_url(url, params={\"pageOptions\": {\"onlyMainContent\": True}})\n",
+ "\n",
+ "print(f\"Page content scraped. Length: {len(page_content['content'])} characters\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 5: Prepare the Prompt for Claude"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Prompt prepared for Claude.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Prepare the prompt for Claude\n",
+ "prompt = f\"\"\"Analyze the following webpage content and extract the following information:\n",
+ "1. The title of the page\n",
+ "2. Whether the company is part of Y Combinator (YC)\n",
+ "3. Whether the company/product is open source\n",
+ "\n",
+ "Return the information in JSON format with the following schema:\n",
+ "{{\n",
+ " \"main_header_title\": string,\n",
+ " \"is_yc_company\": boolean,\n",
+ " \"is_open_source\": boolean\n",
+ "}}\n",
+ "\n",
+ "Webpage content:\n",
+ "{page_content['content']}\n",
+ "\n",
+ "Return only the JSON, nothing else.\"\"\"\n",
+ "\n",
+ "print(\"Prompt prepared for Claude.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 6: Query Claude"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Claude response received.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Query Claude\n",
+ "response = anthropic_client.messages.create(\n",
+ " model=\"claude-3-opus-20240229\",\n",
+ " max_tokens=1000,\n",
+ " messages=[\n",
+ " {\"role\": \"user\", \"content\": prompt}\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "print(\"Claude response received.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Step 7: Parse and Display the Result"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{\n",
+ " \"title\": \"Just in time answers for Sales and Support\",\n",
+ " \"is_yc_company\": true,\n",
+ " \"is_open_source\": false\n",
+ "}\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Parse and print the result\n",
+ "result = json.loads(response.content[0].text)\n",
+ "print(json.dumps(result, indent=2))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/img/firecrawl_logo.png b/img/firecrawl_logo.png
new file mode 100644
index 00000000..bd723222
Binary files /dev/null and b/img/firecrawl_logo.png differ