firecrawl-haystack 1.1.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firecrawl_haystack-2.0.0/CHANGELOG.md +50 -0
- firecrawl_haystack-1.1.0/LICENSE → firecrawl_haystack-2.0.0/LICENSE.txt +1 -1
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/PKG-INFO +8 -7
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/README.md +2 -2
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/pyproject.toml +18 -4
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/fetchers/firecrawl/firecrawl_crawler.py +27 -59
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/websearch/firecrawl/firecrawl_websearch.py +13 -12
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/tests/test_firecrawl_crawler.py +85 -61
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/tests/test_firecrawl_websearch.py +96 -71
- firecrawl_haystack-1.1.0/CHANGELOG.md +0 -15
- firecrawl_haystack-1.1.0/tests/__init__.py +0 -3
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/.gitignore +0 -0
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/pydoc/config_docusaurus.yml +0 -0
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/fetchers/firecrawl/__init__.py +0 -0
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/fetchers/py.typed +0 -0
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/websearch/firecrawl/__init__.py +0 -0
- {firecrawl_haystack-1.1.0 → firecrawl_haystack-2.0.0}/src/haystack_integrations/components/websearch/py.typed +0 -0
- {firecrawl_haystack-1.1.0/src/haystack_integrations/components/websearch → firecrawl_haystack-2.0.0/tests}/__init__.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [integrations/firecrawl-v1.2.0] - 2026-09-04
|
|
4
|
+
|
|
5
|
+
### 🐛 Bug Fixes
|
|
6
|
+
|
|
7
|
+
- Fix new issues raised by ruff 0.16.0 (#3670)
|
|
8
|
+
- Standardize license files (#3771)
|
|
9
|
+
|
|
10
|
+
### 🚜 Refactor
|
|
11
|
+
|
|
12
|
+
- Firecrawl - decouple sync/async clients creation (#3914)
|
|
13
|
+
|
|
14
|
+
### 📚 Documentation
|
|
15
|
+
|
|
16
|
+
- Fix FirecrawlCrawler wrong name in docstrings (#3560)
|
|
17
|
+
|
|
18
|
+
### 🧪 Testing
|
|
19
|
+
|
|
20
|
+
- Test compatible integrations with python 3.14; update pyproject (#3001)
|
|
21
|
+
- Track test coverage for all integrations (#3065)
|
|
22
|
+
|
|
23
|
+
### 🧹 Chores
|
|
24
|
+
|
|
25
|
+
- Add ANN type annotations to faiss, fastembed, firecrawl, github, google_ai (#2988)
|
|
26
|
+
- Enforce ruff docstring rules (D102/D103/D205/D209/D213/D417/D419) in integrations 11-20 (#3009)
|
|
27
|
+
- Improve consistency of integrations folder structure (#3430)
|
|
28
|
+
- Re-standardize READMEs (#3768)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## [integrations/firecrawl-v1.1.0] - 2026-03-09
|
|
32
|
+
|
|
33
|
+
### 🚀 Features
|
|
34
|
+
|
|
35
|
+
- Add FirecrawlWebSearch component (#2888)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## [integrations/firecrawl-v1.0.1] - 2026-02-24
|
|
39
|
+
|
|
40
|
+
### 🌀 Miscellaneous
|
|
41
|
+
|
|
42
|
+
- Docs: Update Firecrawl module path in Docusaurus configuration (#2869)
|
|
43
|
+
|
|
44
|
+
## [integrations/firecrawl-v1.0.0] - 2026-02-24
|
|
45
|
+
|
|
46
|
+
### 🚀 Features
|
|
47
|
+
|
|
48
|
+
- Add FirecrawlCrawler (#2859)
|
|
49
|
+
|
|
50
|
+
<!-- generated by git-cliff -->
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2023 deepset GmbH
|
|
189
|
+
Copyright 2023-present deepset GmbH
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: firecrawl-haystack
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Haystack integration for Firecrawl
|
|
5
5
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/firecrawl#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
7
7
|
Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/firecrawl
|
|
8
8
|
Author-email: deepset GmbH <info@deepset.ai>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
|
-
License-File: LICENSE
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
11
|
Keywords: Firecrawl,Haystack,Web Crawler,Web Scraping,Web Search
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
20
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.11
|
|
21
22
|
Requires-Dist: firecrawl-py>=4.0.0
|
|
22
23
|
Requires-Dist: haystack-ai>=2.24.1
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
@@ -27,6 +28,7 @@ Description-Content-Type: text/markdown
|
|
|
27
28
|
[](https://pypi.org/project/firecrawl-haystack)
|
|
28
29
|
[](https://pypi.org/project/firecrawl-haystack)
|
|
29
30
|
|
|
31
|
+
- [Integration page](https://haystack.deepset.ai/integrations/firecrawl)
|
|
30
32
|
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/firecrawl/CHANGELOG.md)
|
|
31
33
|
|
|
32
34
|
---
|
|
@@ -35,5 +37,4 @@ Description-Content-Type: text/markdown
|
|
|
35
37
|
|
|
36
38
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
To run integration tests locally, you need to export the `FIRECRAWL_API_KEY` environment variable.
|
|
40
|
+
To run integration tests locally, you need to export the `FIRECRAWL_API_KEY` environment variable.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://pypi.org/project/firecrawl-haystack)
|
|
4
4
|
[](https://pypi.org/project/firecrawl-haystack)
|
|
5
5
|
|
|
6
|
+
- [Integration page](https://haystack.deepset.ai/integrations/firecrawl)
|
|
6
7
|
- [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/firecrawl/CHANGELOG.md)
|
|
7
8
|
|
|
8
9
|
---
|
|
@@ -11,5 +12,4 @@
|
|
|
11
12
|
|
|
12
13
|
Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
To run integration tests locally, you need to export the `FIRECRAWL_API_KEY` environment variable.
|
|
15
|
+
To run integration tests locally, you need to export the `FIRECRAWL_API_KEY` environment variable.
|
|
@@ -7,7 +7,7 @@ name = "firecrawl-haystack"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Haystack integration for Firecrawl"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
keywords = [
|
|
13
13
|
"Firecrawl",
|
|
@@ -23,10 +23,11 @@ classifiers = [
|
|
|
23
23
|
"License :: OSI Approved :: Apache Software License",
|
|
24
24
|
"Development Status :: 4 - Beta",
|
|
25
25
|
"Programming Language :: Python",
|
|
26
|
-
"Programming Language :: Python :: 3.10",
|
|
27
26
|
"Programming Language :: Python :: 3.11",
|
|
28
27
|
"Programming Language :: Python :: 3.12",
|
|
29
28
|
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Programming Language :: Python :: 3.15",
|
|
30
31
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
31
32
|
]
|
|
32
33
|
dependencies = [
|
|
@@ -73,7 +74,8 @@ dependencies = [
|
|
|
73
74
|
unit = 'pytest -m "not integration" {args:tests}'
|
|
74
75
|
integration = 'pytest -m "integration" {args:tests}'
|
|
75
76
|
all = 'pytest {args:tests}'
|
|
76
|
-
cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
|
|
77
|
+
unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
|
|
78
|
+
integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
|
|
77
79
|
types = "mypy -p haystack_integrations.components.fetchers.firecrawl -p haystack_integrations.components.websearch.firecrawl {args}"
|
|
78
80
|
|
|
79
81
|
[tool.mypy]
|
|
@@ -88,9 +90,17 @@ line-length = 120
|
|
|
88
90
|
[tool.ruff.lint]
|
|
89
91
|
select = [
|
|
90
92
|
"A",
|
|
93
|
+
"ANN",
|
|
91
94
|
"ARG",
|
|
92
95
|
"B",
|
|
93
96
|
"C",
|
|
97
|
+
"D102", # Missing docstring in public method
|
|
98
|
+
"D103", # Missing docstring in public function
|
|
99
|
+
"D205", # 1 blank line required between summary line and description
|
|
100
|
+
"D209", # Closing triple quotes go to new line
|
|
101
|
+
"D213", # summary lines must be positioned on the second physical line of the docstring
|
|
102
|
+
"D417", # Missing argument descriptions in the docstring
|
|
103
|
+
"D419", # Docstring is empty
|
|
94
104
|
"DTZ",
|
|
95
105
|
"E",
|
|
96
106
|
"EM",
|
|
@@ -123,12 +133,15 @@ ignore = [
|
|
|
123
133
|
"S105",
|
|
124
134
|
"S106",
|
|
125
135
|
"S107",
|
|
136
|
+
# Allow `Any` - used legitimately for dynamic types and SDK boundaries
|
|
137
|
+
"ANN401",
|
|
126
138
|
# Ignore complexity
|
|
127
139
|
"C901",
|
|
128
140
|
"PLR0911",
|
|
129
141
|
"PLR0912",
|
|
130
142
|
"PLR0913",
|
|
131
143
|
"PLR0915",
|
|
144
|
+
"PLR0917",
|
|
132
145
|
]
|
|
133
146
|
unfixable = [
|
|
134
147
|
# Don't touch unused imports
|
|
@@ -143,11 +156,12 @@ ban-relative-imports = "parents"
|
|
|
143
156
|
|
|
144
157
|
[tool.ruff.lint.per-file-ignores]
|
|
145
158
|
# Tests can use magic values, assertions, and relative imports
|
|
146
|
-
"tests/**/*" = ["PLR2004", "S101", "TID252"]
|
|
159
|
+
"tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
|
|
147
160
|
|
|
148
161
|
[tool.coverage.run]
|
|
149
162
|
source = ["haystack_integrations"]
|
|
150
163
|
branch = true
|
|
164
|
+
relative_files = true
|
|
151
165
|
parallel = false
|
|
152
166
|
|
|
153
167
|
[tool.coverage.report]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from haystack import Document, component, logging
|
|
@@ -27,15 +27,15 @@ class FirecrawlCrawler:
|
|
|
27
27
|
### Usage example
|
|
28
28
|
|
|
29
29
|
```python
|
|
30
|
-
from haystack_integrations.components.fetchers.firecrawl import
|
|
30
|
+
from haystack_integrations.components.fetchers.firecrawl import FirecrawlCrawler
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
crawler = FirecrawlCrawler(
|
|
33
33
|
api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
|
|
34
34
|
params={"limit": 5},
|
|
35
35
|
)
|
|
36
|
-
|
|
36
|
+
crawler.warm_up()
|
|
37
37
|
|
|
38
|
-
result =
|
|
38
|
+
result = crawler.run(urls=["https://docs.haystack.deepset.ai/docs/intro"])
|
|
39
39
|
documents = result["documents"]
|
|
40
40
|
```
|
|
41
41
|
"""
|
|
@@ -46,7 +46,7 @@ class FirecrawlCrawler:
|
|
|
46
46
|
params: dict[str, Any] | None = None,
|
|
47
47
|
) -> None:
|
|
48
48
|
"""
|
|
49
|
-
Initialize the
|
|
49
|
+
Initialize the FirecrawlCrawler.
|
|
50
50
|
|
|
51
51
|
:param api_key:
|
|
52
52
|
API key for Firecrawl.
|
|
@@ -83,14 +83,18 @@ class FirecrawlCrawler:
|
|
|
83
83
|
:returns: A dictionary with the following keys:
|
|
84
84
|
- `documents`: List of documents, one for each URL crawled.
|
|
85
85
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
self.warm_up()
|
|
87
|
+
assert self._firecrawl_client is not None # noqa: S101
|
|
88
88
|
|
|
89
89
|
current_params = params if params is not None else self._params
|
|
90
90
|
documents: list[Document] = []
|
|
91
91
|
for url in urls:
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
try:
|
|
93
|
+
crawl_response = self._firecrawl_client.crawl(url=url, **current_params)
|
|
94
|
+
except Exception as error:
|
|
95
|
+
logger.exception(f"Failed to crawl website {url}: {error}")
|
|
96
|
+
continue
|
|
97
|
+
documents.extend(self._documents_from_crawl_response(url=url, crawl_response=crawl_response))
|
|
94
98
|
|
|
95
99
|
return {"documents": documents}
|
|
96
100
|
|
|
@@ -111,67 +115,31 @@ class FirecrawlCrawler:
|
|
|
111
115
|
:returns: A dictionary with the following keys:
|
|
112
116
|
- `documents`: List of documents, one for each URL crawled.
|
|
113
117
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
await self.warm_up_async()
|
|
119
|
+
assert self._async_firecrawl_client is not None # noqa: S101
|
|
116
120
|
|
|
117
121
|
current_params = params if params is not None else self._params
|
|
118
122
|
documents: list[Document] = []
|
|
119
123
|
for url in urls:
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
try:
|
|
125
|
+
crawl_response = await self._async_firecrawl_client.crawl(url=url, **current_params)
|
|
126
|
+
except Exception as error:
|
|
127
|
+
logger.exception(f"Failed to crawl website {url}: {error}")
|
|
128
|
+
continue
|
|
129
|
+
documents.extend(self._documents_from_crawl_response(url=url, crawl_response=crawl_response))
|
|
122
130
|
|
|
123
131
|
return {"documents": documents}
|
|
124
132
|
|
|
125
133
|
def warm_up(self) -> None:
|
|
126
|
-
"""
|
|
127
|
-
Warm up the Firecrawl client by initializing the clients.
|
|
128
|
-
This is useful to avoid cold start delays when crawling many URLs.
|
|
129
|
-
"""
|
|
134
|
+
"""Warm up the synchronous Firecrawl client."""
|
|
130
135
|
if self._firecrawl_client is None:
|
|
131
136
|
self._firecrawl_client = Firecrawl(api_key=self.api_key.resolve_value())
|
|
137
|
+
|
|
138
|
+
async def warm_up_async(self) -> None:
|
|
139
|
+
"""Warm up the asynchronous Firecrawl client."""
|
|
132
140
|
if self._async_firecrawl_client is None:
|
|
133
141
|
self._async_firecrawl_client = AsyncFirecrawl(api_key=self.api_key.resolve_value())
|
|
134
142
|
|
|
135
|
-
def _crawl_url(self, url: str, params: dict[str, Any]) -> list[Document]:
|
|
136
|
-
"""
|
|
137
|
-
Crawl a single URL and return Documents.
|
|
138
|
-
|
|
139
|
-
:param url: URL to crawl.
|
|
140
|
-
:param params: Crawl request parameters.
|
|
141
|
-
:return: List of Documents from the crawl result.
|
|
142
|
-
"""
|
|
143
|
-
try:
|
|
144
|
-
# Ignoring type because the firecrawl client is initialized as None and only set during warm_up
|
|
145
|
-
crawl_response = self._firecrawl_client.crawl( # type: ignore[union-attr]
|
|
146
|
-
url=url,
|
|
147
|
-
**params,
|
|
148
|
-
)
|
|
149
|
-
except Exception as error:
|
|
150
|
-
logger.exception(f"Failed to crawl website {url}: {error}")
|
|
151
|
-
return []
|
|
152
|
-
|
|
153
|
-
return self._documents_from_crawl_response(url=url, crawl_response=crawl_response)
|
|
154
|
-
|
|
155
|
-
async def _crawl_url_async(self, url: str, params: dict[str, Any]) -> list[Document]:
|
|
156
|
-
"""
|
|
157
|
-
Asynchronously crawl a single URL and return Documents.
|
|
158
|
-
|
|
159
|
-
:param url: URL to crawl.
|
|
160
|
-
:param params: Crawl request parameters.
|
|
161
|
-
:return: List of Documents from the crawl result.
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
# Ignoring type because the firecrawl client is initialized as None and only set during warm_up
|
|
165
|
-
crawl_response = await self._async_firecrawl_client.crawl( # type: ignore[union-attr]
|
|
166
|
-
url=url,
|
|
167
|
-
**params,
|
|
168
|
-
)
|
|
169
|
-
except Exception as error:
|
|
170
|
-
logger.exception(f"Failed to crawl website {url}: {error}")
|
|
171
|
-
return []
|
|
172
|
-
|
|
173
|
-
return self._documents_from_crawl_response(url=url, crawl_response=crawl_response)
|
|
174
|
-
|
|
175
143
|
def _documents_from_crawl_response(self, url: str, crawl_response: Any) -> list[Document]:
|
|
176
144
|
"""
|
|
177
145
|
Convert a Firecrawl crawl response to Haystack Documents.
|
|
@@ -189,7 +157,7 @@ class FirecrawlCrawler:
|
|
|
189
157
|
doc = Document(
|
|
190
158
|
content=page.markdown,
|
|
191
159
|
meta={
|
|
192
|
-
"timestamp": datetime.now(tz=
|
|
160
|
+
"timestamp": datetime.now(tz=UTC).isoformat(),
|
|
193
161
|
**metadata,
|
|
194
162
|
},
|
|
195
163
|
)
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from firecrawl import AsyncFirecrawl, Firecrawl # type: ignore[import-untyped]
|
|
8
|
-
from firecrawl.types import SearchData # type: ignore[import-untyped]
|
|
9
7
|
from haystack import Document, component, logging
|
|
10
8
|
from haystack.utils import Secret
|
|
11
9
|
|
|
10
|
+
from firecrawl import AsyncFirecrawl, Firecrawl # type: ignore[import-untyped]
|
|
11
|
+
from firecrawl.types import SearchData # type: ignore[import-untyped]
|
|
12
|
+
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -69,12 +70,12 @@ class FirecrawlWebSearch:
|
|
|
69
70
|
self._async_firecrawl_client: AsyncFirecrawl | None = None
|
|
70
71
|
|
|
71
72
|
def warm_up(self) -> None:
|
|
72
|
-
"""
|
|
73
|
-
Warm up the Firecrawl clients by initializing the sync and async clients.
|
|
74
|
-
This is useful to avoid cold start delays when performing searches.
|
|
75
|
-
"""
|
|
73
|
+
"""Warm up the synchronous Firecrawl client."""
|
|
76
74
|
if self._firecrawl_client is None:
|
|
77
75
|
self._firecrawl_client = Firecrawl(api_key=self.api_key.resolve_value())
|
|
76
|
+
|
|
77
|
+
async def warm_up_async(self) -> None:
|
|
78
|
+
"""Warm up the asynchronous Firecrawl client."""
|
|
78
79
|
if self._async_firecrawl_client is None:
|
|
79
80
|
self._async_firecrawl_client = AsyncFirecrawl(api_key=self.api_key.resolve_value())
|
|
80
81
|
|
|
@@ -95,8 +96,8 @@ class FirecrawlWebSearch:
|
|
|
95
96
|
- `documents`: List of documents with search result content.
|
|
96
97
|
- `links`: List of URLs from the search results.
|
|
97
98
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
self.warm_up()
|
|
100
|
+
assert self._firecrawl_client is not None # noqa: S101
|
|
100
101
|
|
|
101
102
|
current_params = search_params if search_params is not None else self._search_params
|
|
102
103
|
params = current_params.copy()
|
|
@@ -104,7 +105,7 @@ class FirecrawlWebSearch:
|
|
|
104
105
|
params["limit"] = self.top_k
|
|
105
106
|
|
|
106
107
|
try:
|
|
107
|
-
search_response = self._firecrawl_client.search(
|
|
108
|
+
search_response = self._firecrawl_client.search(
|
|
108
109
|
query=query,
|
|
109
110
|
**params,
|
|
110
111
|
)
|
|
@@ -132,8 +133,8 @@ class FirecrawlWebSearch:
|
|
|
132
133
|
- `documents`: List of documents with search result content.
|
|
133
134
|
- `links`: List of URLs from the search results.
|
|
134
135
|
"""
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
await self.warm_up_async()
|
|
137
|
+
assert self._async_firecrawl_client is not None # noqa: S101
|
|
137
138
|
|
|
138
139
|
current_params = search_params if search_params is not None else self._search_params
|
|
139
140
|
params = current_params.copy()
|
|
@@ -141,7 +142,7 @@ class FirecrawlWebSearch:
|
|
|
141
142
|
params["limit"] = self.top_k
|
|
142
143
|
|
|
143
144
|
try:
|
|
144
|
-
search_response = await self._async_firecrawl_client.search(
|
|
145
|
+
search_response = await self._async_firecrawl_client.search(
|
|
145
146
|
query=query,
|
|
146
147
|
**params,
|
|
147
148
|
)
|
|
@@ -14,38 +14,44 @@ from haystack.utils import Secret
|
|
|
14
14
|
from haystack_integrations.components.fetchers.firecrawl import FirecrawlCrawler
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return page
|
|
24
|
-
|
|
25
|
-
@pytest.fixture
|
|
26
|
-
def crawl_response(self, page) -> MagicMock:
|
|
27
|
-
crawl_response = MagicMock()
|
|
28
|
-
crawl_response.status = "completed"
|
|
29
|
-
crawl_response.data = [page]
|
|
30
|
-
return crawl_response
|
|
31
|
-
|
|
32
|
-
@pytest.fixture
|
|
33
|
-
def mock_client(self, crawl_response) -> MagicMock:
|
|
34
|
-
mock_client = MagicMock()
|
|
35
|
-
mock_client.crawl.return_value = crawl_response
|
|
36
|
-
return mock_client
|
|
37
|
-
|
|
38
|
-
@pytest.fixture
|
|
39
|
-
def mock_async_client(self, crawl_response) -> MagicMock:
|
|
40
|
-
mock_async_client = MagicMock()
|
|
41
|
-
mock_async_client.crawl = AsyncMock(return_value=crawl_response)
|
|
42
|
-
return mock_async_client
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def page() -> MagicMock:
|
|
19
|
+
page = MagicMock()
|
|
20
|
+
page.markdown = "Page content"
|
|
21
|
+
page.metadata_dict = {"url": "https://example.com"}
|
|
22
|
+
return page
|
|
43
23
|
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def crawl_response(page) -> MagicMock:
|
|
27
|
+
crawl_response = MagicMock()
|
|
28
|
+
crawl_response.status = "completed"
|
|
29
|
+
crawl_response.data = [page]
|
|
30
|
+
return crawl_response
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_client(crawl_response) -> MagicMock:
|
|
35
|
+
mock_client = MagicMock()
|
|
36
|
+
mock_client.crawl.return_value = crawl_response
|
|
37
|
+
return mock_client
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def mock_async_client(crawl_response) -> MagicMock:
|
|
42
|
+
mock_async_client = MagicMock()
|
|
43
|
+
mock_async_client.crawl = AsyncMock(return_value=crawl_response)
|
|
44
|
+
return mock_async_client
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestInit:
|
|
44
48
|
def test_init_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
45
49
|
monkeypatch.setenv("FIRECRAWL_API_KEY", "test-key")
|
|
46
50
|
fetcher = FirecrawlCrawler()
|
|
47
51
|
assert fetcher._params == {"limit": 1, "scrape_options": {"formats": ["markdown"]}}
|
|
48
52
|
assert fetcher.api_key.resolve_value() == "test-key"
|
|
53
|
+
assert fetcher._firecrawl_client is None
|
|
54
|
+
assert fetcher._async_firecrawl_client is None
|
|
49
55
|
|
|
50
56
|
def test_init_with_params(self) -> None:
|
|
51
57
|
fetcher = FirecrawlCrawler(
|
|
@@ -55,13 +61,14 @@ class TestFirecrawlCrawler:
|
|
|
55
61
|
assert fetcher._params == {"limit": 10, "scrape_options": {"formats": ["markdown"]}}
|
|
56
62
|
assert fetcher.api_key.resolve_value() == "custom-key"
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
|
|
65
|
+
class TestSerialization:
|
|
66
|
+
def test_to_dict(self) -> None:
|
|
60
67
|
fetcher = FirecrawlCrawler(
|
|
61
68
|
api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
|
|
62
69
|
params={"limit": 5},
|
|
63
70
|
)
|
|
64
|
-
data = component_to_dict(fetcher, "
|
|
71
|
+
data = component_to_dict(fetcher, "FirecrawlCrawler")
|
|
65
72
|
assert data["type"] == "haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.FirecrawlCrawler"
|
|
66
73
|
assert data["init_parameters"]["params"] == {"limit": 5}
|
|
67
74
|
assert "api_key" in data["init_parameters"]
|
|
@@ -75,10 +82,46 @@ class TestFirecrawlCrawler:
|
|
|
75
82
|
"api_key": {"env_vars": ["FIRECRAWL_API_KEY"], "strict": True, "type": "env_var"},
|
|
76
83
|
},
|
|
77
84
|
}
|
|
78
|
-
fetcher = component_from_dict(FirecrawlCrawler, data, "
|
|
85
|
+
fetcher = component_from_dict(FirecrawlCrawler, data, "FirecrawlCrawler")
|
|
79
86
|
assert fetcher.params == {"limit": 3}
|
|
80
87
|
assert fetcher.api_key.resolve_value() == "test-key"
|
|
81
88
|
|
|
89
|
+
|
|
90
|
+
class TestComponentLifecycle:
|
|
91
|
+
def test_key_resolved_at_warm_up_not_init(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
92
|
+
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
|
|
93
|
+
fetcher = FirecrawlCrawler()
|
|
94
|
+
|
|
95
|
+
with pytest.raises(ValueError, match="None of the following authentication environment variables are set"):
|
|
96
|
+
fetcher.warm_up()
|
|
97
|
+
|
|
98
|
+
def test_warm_up(self) -> None:
|
|
99
|
+
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
100
|
+
|
|
101
|
+
with patch("haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.Firecrawl") as mock_client:
|
|
102
|
+
fetcher.warm_up()
|
|
103
|
+
fetcher.warm_up()
|
|
104
|
+
|
|
105
|
+
assert fetcher._firecrawl_client is mock_client.return_value
|
|
106
|
+
assert fetcher._async_firecrawl_client is None
|
|
107
|
+
mock_client.assert_called_once_with(api_key="test-key")
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_warm_up_async(self) -> None:
|
|
111
|
+
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
112
|
+
|
|
113
|
+
with patch(
|
|
114
|
+
"haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.AsyncFirecrawl"
|
|
115
|
+
) as mock_async_client:
|
|
116
|
+
await fetcher.warm_up_async()
|
|
117
|
+
await fetcher.warm_up_async()
|
|
118
|
+
|
|
119
|
+
assert fetcher._firecrawl_client is None
|
|
120
|
+
assert fetcher._async_firecrawl_client is mock_async_client.return_value
|
|
121
|
+
mock_async_client.assert_called_once_with(api_key="test-key")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestRun:
|
|
82
125
|
def test_run_with_mocked_firecrawl_client(self, mock_client) -> None:
|
|
83
126
|
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
84
127
|
|
|
@@ -135,12 +178,9 @@ class TestFirecrawlCrawler:
|
|
|
135
178
|
)
|
|
136
179
|
|
|
137
180
|
def test_run_calls_warm_up(self, crawl_response) -> None:
|
|
138
|
-
with (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
) as mock_firecrawl_client,
|
|
142
|
-
patch("haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.AsyncFirecrawl"),
|
|
143
|
-
):
|
|
181
|
+
with patch(
|
|
182
|
+
"haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.Firecrawl"
|
|
183
|
+
) as mock_firecrawl_client:
|
|
144
184
|
mock_firecrawl_client.return_value.crawl.return_value = crawl_response
|
|
145
185
|
|
|
146
186
|
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
@@ -151,12 +191,9 @@ class TestFirecrawlCrawler:
|
|
|
151
191
|
|
|
152
192
|
@pytest.mark.asyncio
|
|
153
193
|
async def test_run_async_calls_warm_up(self, crawl_response) -> None:
|
|
154
|
-
with (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.AsyncFirecrawl"
|
|
158
|
-
) as mock_async_firecrawl_client,
|
|
159
|
-
):
|
|
194
|
+
with patch(
|
|
195
|
+
"haystack_integrations.components.fetchers.firecrawl.firecrawl_crawler.AsyncFirecrawl"
|
|
196
|
+
) as mock_async_firecrawl_client:
|
|
160
197
|
mock_async_firecrawl_client.return_value.crawl = AsyncMock(return_value=crawl_response)
|
|
161
198
|
|
|
162
199
|
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
@@ -222,21 +259,13 @@ class TestFirecrawlCrawler:
|
|
|
222
259
|
|
|
223
260
|
assert "Failed to crawl website https://example.com: failed" in caplog.text
|
|
224
261
|
|
|
225
|
-
def test_warm_up_initializes_clients(self) -> None:
|
|
226
|
-
fetcher = FirecrawlCrawler(api_key=Secret.from_token("test-key"))
|
|
227
|
-
assert fetcher._firecrawl_client is None
|
|
228
|
-
assert fetcher._async_firecrawl_client is None
|
|
229
|
-
|
|
230
|
-
fetcher.warm_up()
|
|
231
|
-
|
|
232
|
-
assert fetcher._firecrawl_client is not None
|
|
233
|
-
assert fetcher._async_firecrawl_client is not None
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
263
|
+
@pytest.mark.skipif(
|
|
264
|
+
not os.environ.get("FIRECRAWL_API_KEY"),
|
|
265
|
+
reason="Export FIRECRAWL_API_KEY to run integration tests.",
|
|
266
|
+
)
|
|
267
|
+
@pytest.mark.integration
|
|
268
|
+
class TestIntegration:
|
|
240
269
|
def test_run_integration(self) -> None:
|
|
241
270
|
fetcher = FirecrawlCrawler(api_key=Secret.from_env_var("FIRECRAWL_API_KEY"), params={"limit": 1})
|
|
242
271
|
result = fetcher.run(urls=["https://docs.haystack.deepset.ai/docs/intro"], params={"limit": 1})
|
|
@@ -247,11 +276,6 @@ class TestFirecrawlCrawler:
|
|
|
247
276
|
assert "Haystack" in result["documents"][0].content
|
|
248
277
|
assert "https://docs.haystack.deepset.ai/docs/intro" == result["documents"][0].meta["url"]
|
|
249
278
|
|
|
250
|
-
@pytest.mark.skipif(
|
|
251
|
-
not os.environ.get("FIRECRAWL_API_KEY"),
|
|
252
|
-
reason="Export FIRECRAWL_API_KEY to run integration tests.",
|
|
253
|
-
)
|
|
254
|
-
@pytest.mark.integration
|
|
255
279
|
@pytest.mark.asyncio
|
|
256
280
|
async def test_run_async_integration(self) -> None:
|
|
257
281
|
fetcher = FirecrawlCrawler(api_key=Secret.from_env_var("FIRECRAWL_API_KEY"), params={"limit": 1})
|
|
@@ -13,50 +13,57 @@ from haystack.utils import Secret
|
|
|
13
13
|
from haystack_integrations.components.websearch.firecrawl import FirecrawlWebSearch
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return result
|
|
25
|
-
|
|
26
|
-
@pytest.fixture
|
|
27
|
-
def search_result_document(self) -> MagicMock:
|
|
28
|
-
"""A Document-like object (with markdown, from scrapeOptions)."""
|
|
29
|
-
result = MagicMock(spec=["markdown", "metadata_dict", "url"])
|
|
30
|
-
result.markdown = "# Full page content\nSome markdown text."
|
|
31
|
-
result.metadata_dict = {"url": "https://example.com/page", "title": "Page Title"}
|
|
32
|
-
result.url = "https://example.com/page"
|
|
33
|
-
return result
|
|
34
|
-
|
|
35
|
-
@pytest.fixture
|
|
36
|
-
def search_response(self, search_result_web) -> MagicMock:
|
|
37
|
-
"""Standard search response with web results."""
|
|
38
|
-
response = MagicMock()
|
|
39
|
-
response.web = [search_result_web]
|
|
40
|
-
return response
|
|
41
|
-
|
|
42
|
-
@pytest.fixture
|
|
43
|
-
def mock_client(self, search_response) -> MagicMock:
|
|
44
|
-
client = MagicMock()
|
|
45
|
-
client.search.return_value = search_response
|
|
46
|
-
return client
|
|
47
|
-
|
|
48
|
-
@pytest.fixture
|
|
49
|
-
def mock_async_client(self, search_response) -> MagicMock:
|
|
50
|
-
client = MagicMock()
|
|
51
|
-
client.search = AsyncMock(return_value=search_response)
|
|
52
|
-
return client
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def search_result_web() -> MagicMock:
|
|
18
|
+
"""A SearchResultWeb-like object (no markdown, just search metadata)."""
|
|
19
|
+
result = MagicMock(spec=["url", "title", "description"])
|
|
20
|
+
result.url = "https://example.com"
|
|
21
|
+
result.title = "Example Title"
|
|
22
|
+
result.description = "Example description snippet"
|
|
23
|
+
return result
|
|
53
24
|
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def search_result_document() -> MagicMock:
|
|
28
|
+
"""A Document-like object (with markdown, from scrapeOptions)."""
|
|
29
|
+
result = MagicMock(spec=["markdown", "metadata_dict", "url"])
|
|
30
|
+
result.markdown = "# Full page content\nSome markdown text."
|
|
31
|
+
result.metadata_dict = {"url": "https://example.com/page", "title": "Page Title"}
|
|
32
|
+
result.url = "https://example.com/page"
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def search_response(search_result_web) -> MagicMock:
|
|
38
|
+
"""Standard search response with web results."""
|
|
39
|
+
response = MagicMock()
|
|
40
|
+
response.web = [search_result_web]
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def mock_client(search_response) -> MagicMock:
|
|
46
|
+
client = MagicMock()
|
|
47
|
+
client.search.return_value = search_response
|
|
48
|
+
return client
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def mock_async_client(search_response) -> MagicMock:
|
|
53
|
+
client = MagicMock()
|
|
54
|
+
client.search = AsyncMock(return_value=search_response)
|
|
55
|
+
return client
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestInit:
|
|
54
59
|
def test_init_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
55
60
|
monkeypatch.setenv("FIRECRAWL_API_KEY", "test-key")
|
|
56
61
|
ws = FirecrawlWebSearch()
|
|
57
62
|
assert ws.top_k == 10
|
|
58
63
|
assert ws._search_params == {}
|
|
59
64
|
assert ws.api_key.resolve_value() == "test-key"
|
|
65
|
+
assert ws._firecrawl_client is None
|
|
66
|
+
assert ws._async_firecrawl_client is None
|
|
60
67
|
|
|
61
68
|
def test_init_with_params(self) -> None:
|
|
62
69
|
ws = FirecrawlWebSearch(
|
|
@@ -68,8 +75,9 @@ class TestFirecrawlWebSearch:
|
|
|
68
75
|
assert ws._search_params == {"tbs": "qdr:d", "location": "US"}
|
|
69
76
|
assert ws.api_key.resolve_value() == "custom-key"
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
|
|
79
|
+
class TestSerialization:
|
|
80
|
+
def test_to_dict(self) -> None:
|
|
73
81
|
ws = FirecrawlWebSearch(top_k=5, search_params={"tbs": "qdr:d"})
|
|
74
82
|
data = component_to_dict(ws, "FirecrawlWebSearch")
|
|
75
83
|
assert (
|
|
@@ -94,6 +102,42 @@ class TestFirecrawlWebSearch:
|
|
|
94
102
|
assert ws.search_params == {"location": "UK"}
|
|
95
103
|
assert ws.api_key.resolve_value() == "test-key"
|
|
96
104
|
|
|
105
|
+
|
|
106
|
+
class TestComponentLifecycle:
|
|
107
|
+
def test_key_resolved_at_warm_up_not_init(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
108
|
+
monkeypatch.delenv("FIRECRAWL_API_KEY", raising=False)
|
|
109
|
+
ws = FirecrawlWebSearch()
|
|
110
|
+
|
|
111
|
+
with pytest.raises(ValueError, match="None of the following authentication environment variables are set"):
|
|
112
|
+
ws.warm_up()
|
|
113
|
+
|
|
114
|
+
def test_warm_up(self) -> None:
|
|
115
|
+
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
|
|
116
|
+
|
|
117
|
+
with patch("haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.Firecrawl") as mock_client:
|
|
118
|
+
ws.warm_up()
|
|
119
|
+
ws.warm_up()
|
|
120
|
+
|
|
121
|
+
assert ws._firecrawl_client is mock_client.return_value
|
|
122
|
+
assert ws._async_firecrawl_client is None
|
|
123
|
+
mock_client.assert_called_once_with(api_key="test-key")
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_warm_up_async(self) -> None:
|
|
127
|
+
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
|
|
128
|
+
|
|
129
|
+
with patch(
|
|
130
|
+
"haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"
|
|
131
|
+
) as mock_async_client:
|
|
132
|
+
await ws.warm_up_async()
|
|
133
|
+
await ws.warm_up_async()
|
|
134
|
+
|
|
135
|
+
assert ws._firecrawl_client is None
|
|
136
|
+
assert ws._async_firecrawl_client is mock_async_client.return_value
|
|
137
|
+
mock_async_client.assert_called_once_with(api_key="test-key")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestRun:
|
|
97
141
|
def test_run_returns_documents_and_links(self, mock_client) -> None:
|
|
98
142
|
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"), top_k=10)
|
|
99
143
|
ws._firecrawl_client = mock_client
|
|
@@ -175,12 +219,9 @@ class TestFirecrawlWebSearch:
|
|
|
175
219
|
assert result["links"] == []
|
|
176
220
|
|
|
177
221
|
def test_run_calls_warm_up(self, search_response) -> None:
|
|
178
|
-
with (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
) as mock_firecrawl_client,
|
|
182
|
-
patch("haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"),
|
|
183
|
-
):
|
|
222
|
+
with patch(
|
|
223
|
+
"haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.Firecrawl"
|
|
224
|
+
) as mock_firecrawl_client:
|
|
184
225
|
mock_firecrawl_client.return_value.search.return_value = search_response
|
|
185
226
|
|
|
186
227
|
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
|
|
@@ -191,12 +232,9 @@ class TestFirecrawlWebSearch:
|
|
|
191
232
|
|
|
192
233
|
@pytest.mark.asyncio
|
|
193
234
|
async def test_run_async_calls_warm_up(self, search_response) -> None:
|
|
194
|
-
with (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"
|
|
198
|
-
) as mock_async_firecrawl_client,
|
|
199
|
-
):
|
|
235
|
+
with patch(
|
|
236
|
+
"haystack_integrations.components.websearch.firecrawl.firecrawl_websearch.AsyncFirecrawl"
|
|
237
|
+
) as mock_async_firecrawl_client:
|
|
200
238
|
mock_async_firecrawl_client.return_value.search = AsyncMock(return_value=search_response)
|
|
201
239
|
|
|
202
240
|
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
|
|
@@ -205,16 +243,6 @@ class TestFirecrawlWebSearch:
|
|
|
205
243
|
assert ws._async_firecrawl_client is mock_async_firecrawl_client.return_value
|
|
206
244
|
mock_async_firecrawl_client.assert_called_once_with(api_key="test-key")
|
|
207
245
|
|
|
208
|
-
def test_warm_up_initializes_clients(self) -> None:
|
|
209
|
-
ws = FirecrawlWebSearch(api_key=Secret.from_token("test-key"))
|
|
210
|
-
assert ws._firecrawl_client is None
|
|
211
|
-
assert ws._async_firecrawl_client is None
|
|
212
|
-
|
|
213
|
-
ws.warm_up()
|
|
214
|
-
|
|
215
|
-
assert ws._firecrawl_client is not None
|
|
216
|
-
assert ws._async_firecrawl_client is not None
|
|
217
|
-
|
|
218
246
|
def test_run_empty_web_results(self, mock_client) -> None:
|
|
219
247
|
empty_response = MagicMock()
|
|
220
248
|
empty_response.web = []
|
|
@@ -237,11 +265,13 @@ class TestFirecrawlWebSearch:
|
|
|
237
265
|
assert result["documents"] == []
|
|
238
266
|
assert result["links"] == []
|
|
239
267
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
|
|
269
|
+
@pytest.mark.skipif(
|
|
270
|
+
not os.environ.get("FIRECRAWL_API_KEY"),
|
|
271
|
+
reason="Export FIRECRAWL_API_KEY to run integration tests.",
|
|
272
|
+
)
|
|
273
|
+
@pytest.mark.integration
|
|
274
|
+
class TestIntegration:
|
|
245
275
|
def test_run_integration(self) -> None:
|
|
246
276
|
ws = FirecrawlWebSearch(
|
|
247
277
|
api_key=Secret.from_env_var("FIRECRAWL_API_KEY"),
|
|
@@ -257,11 +287,6 @@ class TestFirecrawlWebSearch:
|
|
|
257
287
|
assert isinstance(result["documents"][0], Document)
|
|
258
288
|
assert result["documents"][0].content
|
|
259
289
|
|
|
260
|
-
@pytest.mark.skipif(
|
|
261
|
-
not os.environ.get("FIRECRAWL_API_KEY"),
|
|
262
|
-
reason="Export FIRECRAWL_API_KEY to run integration tests.",
|
|
263
|
-
)
|
|
264
|
-
@pytest.mark.integration
|
|
265
290
|
@pytest.mark.asyncio
|
|
266
291
|
async def test_run_async_integration(self) -> None:
|
|
267
292
|
ws = FirecrawlWebSearch(
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [integrations/firecrawl-v1.0.1] - 2026-02-24
|
|
4
|
-
|
|
5
|
-
### 🌀 Miscellaneous
|
|
6
|
-
|
|
7
|
-
- Docs: Update Firecrawl module path in Docusaurus configuration (#2869)
|
|
8
|
-
|
|
9
|
-
## [integrations/firecrawl-v1.0.0] - 2026-02-24
|
|
10
|
-
|
|
11
|
-
### 🚀 Features
|
|
12
|
-
|
|
13
|
-
- Add FirecrawlCrawler (#2859)
|
|
14
|
-
|
|
15
|
-
<!-- generated by git-cliff -->
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|