tavily-python 0.7.21__tar.gz → 0.7.23__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.
- {tavily_python-0.7.21 → tavily_python-0.7.23}/PKG-INFO +64 -2
- {tavily_python-0.7.21 → tavily_python-0.7.23}/README.md +63 -1
- {tavily_python-0.7.21 → tavily_python-0.7.23}/setup.py +1 -1
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/async_tavily.py +46 -27
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/hybrid_rag/hybrid_rag.py +6 -3
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/tavily.py +18 -7
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily_python.egg-info/PKG-INFO +64 -2
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily_python.egg-info/SOURCES.txt +1 -0
- tavily_python-0.7.23/tests/test_custom_session.py +410 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_search.py +57 -2
- {tavily_python-0.7.21 → tavily_python-0.7.23}/LICENSE +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/setup.cfg +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/__init__.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/config.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/errors.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/hybrid_rag/__init__.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily/utils.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily_python.egg-info/dependency_links.txt +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily_python.egg-info/requires.txt +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tavily_python.egg-info/top_level.txt +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_crawl.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_errors.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_map.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_research.py +0 -0
- {tavily_python-0.7.21 → tavily_python-0.7.23}/tests/test_session_pooling.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tavily-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.23
|
|
4
4
|
Summary: Python wrapper for the Tavily API
|
|
5
5
|
Home-page: https://github.com/tavily-ai/tavily-python
|
|
6
6
|
Author: Tavily AI
|
|
@@ -25,7 +25,7 @@ Dynamic: requires-dist
|
|
|
25
25
|
Dynamic: requires-python
|
|
26
26
|
Dynamic: summary
|
|
27
27
|
|
|
28
|
-
# Tavily Python
|
|
28
|
+
# Tavily Python SDK
|
|
29
29
|
|
|
30
30
|
[](https://github.com/tavily-ai/tavily-python/stargazers)
|
|
31
31
|
[](https://pypi.org/project/tavily-python/)
|
|
@@ -63,6 +63,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
63
63
|
print(response)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Using exact match to find specific names or phrases
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from tavily import TavilyClient
|
|
70
|
+
|
|
71
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
72
|
+
|
|
73
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
74
|
+
response = client.search(
|
|
75
|
+
query='"John Smith" CEO Acme Corp',
|
|
76
|
+
exact_match=True
|
|
77
|
+
)
|
|
78
|
+
print(response)
|
|
79
|
+
```
|
|
80
|
+
|
|
66
81
|
This is equivalent to directly querying our REST API.
|
|
67
82
|
|
|
68
83
|
### Generating context for a RAG Application
|
|
@@ -258,6 +273,53 @@ for chunk in stream:
|
|
|
258
273
|
print(chunk.decode('utf-8'))
|
|
259
274
|
```
|
|
260
275
|
|
|
276
|
+
## Advanced: Custom Session / Client Injection
|
|
277
|
+
|
|
278
|
+
For enterprise environments that proxy Tavily traffic through an API gateway (e.g., for centralized auth, logging, or policy enforcement), you can pass a pre-configured HTTP session instead of a Tavily API key.
|
|
279
|
+
|
|
280
|
+
### Sync (custom `requests.Session`)
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
import requests
|
|
284
|
+
from tavily import TavilyClient
|
|
285
|
+
|
|
286
|
+
# Pre-configure a session with your gateway's auth
|
|
287
|
+
session = requests.Session()
|
|
288
|
+
session.headers["Authorization"] = "Bearer your-gateway-token"
|
|
289
|
+
session.headers["X-Subscription-Key"] = "your-subscription-key"
|
|
290
|
+
|
|
291
|
+
# No Tavily API key needed — auth is handled by the session
|
|
292
|
+
client = TavilyClient(
|
|
293
|
+
session=session,
|
|
294
|
+
api_base_url="https://your-gateway.com/tavily",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
response = client.search("latest AI research")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Async (custom `httpx.AsyncClient`)
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
import httpx
|
|
304
|
+
from tavily import AsyncTavilyClient
|
|
305
|
+
|
|
306
|
+
# Pre-configure an async client with your gateway's auth
|
|
307
|
+
custom_client = httpx.AsyncClient(
|
|
308
|
+
headers={"Authorization": "Bearer your-gateway-token"},
|
|
309
|
+
base_url="https://your-gateway.com/tavily",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
client = AsyncTavilyClient(client=custom_client)
|
|
313
|
+
|
|
314
|
+
response = await client.search("latest AI research")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Key behaviors:**
|
|
318
|
+
- If a custom session/client is provided, `api_key` is optional
|
|
319
|
+
- Custom session headers take precedence over SDK defaults (e.g., your `Authorization` won't be overwritten)
|
|
320
|
+
- Custom session proxies take precedence over SDK proxy settings
|
|
321
|
+
- The SDK will **not** close externally-provided sessions — you manage the lifecycle
|
|
322
|
+
|
|
261
323
|
## Documentation
|
|
262
324
|
|
|
263
325
|
For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Tavily Python
|
|
1
|
+
# Tavily Python SDK
|
|
2
2
|
|
|
3
3
|
[](https://github.com/tavily-ai/tavily-python/stargazers)
|
|
4
4
|
[](https://pypi.org/project/tavily-python/)
|
|
@@ -36,6 +36,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
36
36
|
print(response)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Using exact match to find specific names or phrases
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from tavily import TavilyClient
|
|
43
|
+
|
|
44
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
45
|
+
|
|
46
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
47
|
+
response = client.search(
|
|
48
|
+
query='"John Smith" CEO Acme Corp',
|
|
49
|
+
exact_match=True
|
|
50
|
+
)
|
|
51
|
+
print(response)
|
|
52
|
+
```
|
|
53
|
+
|
|
39
54
|
This is equivalent to directly querying our REST API.
|
|
40
55
|
|
|
41
56
|
### Generating context for a RAG Application
|
|
@@ -231,6 +246,53 @@ for chunk in stream:
|
|
|
231
246
|
print(chunk.decode('utf-8'))
|
|
232
247
|
```
|
|
233
248
|
|
|
249
|
+
## Advanced: Custom Session / Client Injection
|
|
250
|
+
|
|
251
|
+
For enterprise environments that proxy Tavily traffic through an API gateway (e.g., for centralized auth, logging, or policy enforcement), you can pass a pre-configured HTTP session instead of a Tavily API key.
|
|
252
|
+
|
|
253
|
+
### Sync (custom `requests.Session`)
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
import requests
|
|
257
|
+
from tavily import TavilyClient
|
|
258
|
+
|
|
259
|
+
# Pre-configure a session with your gateway's auth
|
|
260
|
+
session = requests.Session()
|
|
261
|
+
session.headers["Authorization"] = "Bearer your-gateway-token"
|
|
262
|
+
session.headers["X-Subscription-Key"] = "your-subscription-key"
|
|
263
|
+
|
|
264
|
+
# No Tavily API key needed — auth is handled by the session
|
|
265
|
+
client = TavilyClient(
|
|
266
|
+
session=session,
|
|
267
|
+
api_base_url="https://your-gateway.com/tavily",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
response = client.search("latest AI research")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Async (custom `httpx.AsyncClient`)
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
import httpx
|
|
277
|
+
from tavily import AsyncTavilyClient
|
|
278
|
+
|
|
279
|
+
# Pre-configure an async client with your gateway's auth
|
|
280
|
+
custom_client = httpx.AsyncClient(
|
|
281
|
+
headers={"Authorization": "Bearer your-gateway-token"},
|
|
282
|
+
base_url="https://your-gateway.com/tavily",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
client = AsyncTavilyClient(client=custom_client)
|
|
286
|
+
|
|
287
|
+
response = await client.search("latest AI research")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Key behaviors:**
|
|
291
|
+
- If a custom session/client is provided, `api_key` is optional
|
|
292
|
+
- Custom session headers take precedence over SDK defaults (e.g., your `Authorization` won't be overwritten)
|
|
293
|
+
- Custom session proxies take precedence over SDK proxy settings
|
|
294
|
+
- The SDK will **not** close externally-provided sessions — you manage the lifecycle
|
|
295
|
+
|
|
234
296
|
## Documentation
|
|
235
297
|
|
|
236
298
|
For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
|
|
@@ -19,48 +19,63 @@ class AsyncTavilyClient:
|
|
|
19
19
|
proxies: Optional[dict[str, str]] = None,
|
|
20
20
|
api_base_url: Optional[str] = None,
|
|
21
21
|
client_source: Optional[str] = None,
|
|
22
|
-
project_id: Optional[str] = None
|
|
22
|
+
project_id: Optional[str] = None,
|
|
23
|
+
client: Optional[httpx.AsyncClient] = None):
|
|
23
24
|
if api_key is None:
|
|
24
25
|
api_key = os.getenv("TAVILY_API_KEY")
|
|
25
26
|
|
|
26
|
-
if not api_key:
|
|
27
|
+
if not api_key and client is None:
|
|
27
28
|
raise MissingAPIKeyError()
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
tavily_project = project_id or os.getenv("TAVILY_PROJECT")
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
self._api_base_url = api_base_url or "https://api.tavily.com"
|
|
33
|
+
self._company_info_tags = company_info_tags
|
|
34
|
+
|
|
35
|
+
default_headers = {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
**({"Authorization": f"Bearer {api_key}"} if api_key else {}),
|
|
38
|
+
"X-Client-Source": client_source or "tavily-python",
|
|
39
|
+
**({"X-Project-ID": tavily_project} if tavily_project else {})
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
self._external_client = client is not None
|
|
43
|
+
|
|
44
|
+
if client is not None:
|
|
45
|
+
self._client = client
|
|
46
|
+
# Only set headers that aren't already configured on the external client
|
|
47
|
+
for key, value in default_headers.items():
|
|
48
|
+
if key not in self._client.headers:
|
|
49
|
+
self._client.headers[key] = value
|
|
50
|
+
# Set base_url if the external client doesn't have one
|
|
51
|
+
if not str(self._client.base_url):
|
|
52
|
+
self._client.base_url = self._api_base_url
|
|
53
|
+
else:
|
|
54
|
+
proxies = proxies or {}
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
56
|
+
mapped_proxies = {
|
|
57
|
+
"http://": proxies.get("http", os.getenv("TAVILY_HTTP_PROXY")),
|
|
58
|
+
"https://": proxies.get("https", os.getenv("TAVILY_HTTPS_PROXY")),
|
|
59
|
+
}
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
mapped_proxies = {key: value for key, value in mapped_proxies.items() if value}
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
proxy_mounts = (
|
|
64
|
+
{scheme: httpx.AsyncHTTPTransport(proxy=proxy) for scheme, proxy in mapped_proxies.items()}
|
|
65
|
+
if mapped_proxies
|
|
66
|
+
else None
|
|
67
|
+
)
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"X-Client-Source": client_source or "tavily-python",
|
|
54
|
-
**({"X-Project-ID": tavily_project} if tavily_project else {})
|
|
55
|
-
},
|
|
56
|
-
base_url=self._api_base_url,
|
|
57
|
-
mounts=proxy_mounts
|
|
58
|
-
)
|
|
59
|
-
self._company_info_tags = company_info_tags
|
|
69
|
+
self._client = httpx.AsyncClient(
|
|
70
|
+
headers=default_headers,
|
|
71
|
+
base_url=self._api_base_url,
|
|
72
|
+
mounts=proxy_mounts
|
|
73
|
+
)
|
|
60
74
|
|
|
61
75
|
async def close(self):
|
|
62
76
|
"""Close the client and release connection pool resources."""
|
|
63
|
-
|
|
77
|
+
if not self._external_client:
|
|
78
|
+
await self._client.aclose()
|
|
64
79
|
|
|
65
80
|
async def __aenter__(self):
|
|
66
81
|
return self
|
|
@@ -88,6 +103,7 @@ class AsyncTavilyClient:
|
|
|
88
103
|
auto_parameters: bool = None,
|
|
89
104
|
include_favicon: bool = None,
|
|
90
105
|
include_usage: bool = None,
|
|
106
|
+
exact_match: bool = None,
|
|
91
107
|
**kwargs,
|
|
92
108
|
) -> dict:
|
|
93
109
|
"""
|
|
@@ -111,6 +127,7 @@ class AsyncTavilyClient:
|
|
|
111
127
|
"auto_parameters": auto_parameters,
|
|
112
128
|
"include_favicon": include_favicon,
|
|
113
129
|
"include_usage": include_usage,
|
|
130
|
+
"exact_match": exact_match,
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
data = {k: v for k, v in data.items() if v is not None}
|
|
@@ -164,6 +181,7 @@ class AsyncTavilyClient:
|
|
|
164
181
|
auto_parameters: bool = None,
|
|
165
182
|
include_favicon: bool = None,
|
|
166
183
|
include_usage: bool = None,
|
|
184
|
+
exact_match: bool = None,
|
|
167
185
|
**kwargs, # Accept custom arguments
|
|
168
186
|
) -> dict:
|
|
169
187
|
"""
|
|
@@ -188,6 +206,7 @@ class AsyncTavilyClient:
|
|
|
188
206
|
auto_parameters=auto_parameters,
|
|
189
207
|
include_favicon=include_favicon,
|
|
190
208
|
include_usage=include_usage,
|
|
209
|
+
exact_match=exact_match,
|
|
191
210
|
**kwargs,
|
|
192
211
|
)
|
|
193
212
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Union, Optional, Literal
|
|
3
3
|
|
|
4
|
+
import requests
|
|
4
5
|
from tavily import TavilyClient
|
|
5
6
|
|
|
6
7
|
try:
|
|
@@ -76,7 +77,8 @@ class TavilyHybridClient():
|
|
|
76
77
|
embeddings_field: str = 'embeddings',
|
|
77
78
|
content_field: str = 'content',
|
|
78
79
|
embedding_function: Optional[callable] = None,
|
|
79
|
-
ranking_function: Optional[callable] = None
|
|
80
|
+
ranking_function: Optional[callable] = None,
|
|
81
|
+
session: Optional[requests.Session] = None
|
|
80
82
|
):
|
|
81
83
|
'''
|
|
82
84
|
A client for performing hybrid RAG using both the Tavily API and a local database collection.
|
|
@@ -90,9 +92,10 @@ class TavilyHybridClient():
|
|
|
90
92
|
content_field (str): The name of the field in the collection that contains the content.
|
|
91
93
|
embedding_function (callable): If provided, this function will be used to generate embeddings for the search query and documents.
|
|
92
94
|
ranking_function (callable): If provided, this function will be used to rerank the combined results.
|
|
95
|
+
session (requests.Session): If provided, this pre-configured session will be used for HTTP requests. When set, api_key is optional.
|
|
93
96
|
'''
|
|
94
|
-
|
|
95
|
-
self.tavily = TavilyClient(api_key)
|
|
97
|
+
|
|
98
|
+
self.tavily = TavilyClient(api_key, session=session)
|
|
96
99
|
|
|
97
100
|
if db_provider != 'mongodb':
|
|
98
101
|
raise ValueError("Only MongoDB is currently supported as a database provider.")
|
|
@@ -11,11 +11,11 @@ class TavilyClient:
|
|
|
11
11
|
Tavily API client class.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
def __init__(self, api_key: Optional[str] = None, proxies: Optional[dict[str, str]] = None, api_base_url: Optional[str] = None, client_source: Optional[str] = None, project_id: Optional[str] = None):
|
|
14
|
+
def __init__(self, api_key: Optional[str] = None, proxies: Optional[dict[str, str]] = None, api_base_url: Optional[str] = None, client_source: Optional[str] = None, project_id: Optional[str] = None, session: Optional[requests.Session] = None):
|
|
15
15
|
if api_key is None:
|
|
16
16
|
api_key = os.getenv("TAVILY_API_KEY")
|
|
17
17
|
|
|
18
|
-
if not api_key:
|
|
18
|
+
if not api_key and session is None:
|
|
19
19
|
raise MissingAPIKeyError()
|
|
20
20
|
|
|
21
21
|
resolved_proxies = {
|
|
@@ -32,19 +32,26 @@ class TavilyClient:
|
|
|
32
32
|
|
|
33
33
|
self.headers = {
|
|
34
34
|
"Content-Type": "application/json",
|
|
35
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
35
|
+
**({"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}),
|
|
36
36
|
"X-Client-Source": client_source or "tavily-python",
|
|
37
37
|
**({"X-Project-ID": tavily_project} if tavily_project else {})
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
self.
|
|
41
|
-
self.session.
|
|
40
|
+
self._external_session = session is not None
|
|
41
|
+
self.session = session if session is not None else requests.Session()
|
|
42
|
+
# For external sessions, only set headers that aren't already configured
|
|
43
|
+
for key, value in self.headers.items():
|
|
44
|
+
if key not in self.session.headers:
|
|
45
|
+
self.session.headers[key] = value
|
|
42
46
|
if self.proxies:
|
|
43
|
-
self.
|
|
47
|
+
for protocol, url in self.proxies.items():
|
|
48
|
+
if protocol not in self.session.proxies:
|
|
49
|
+
self.session.proxies[protocol] = url
|
|
44
50
|
|
|
45
51
|
def close(self):
|
|
46
52
|
"""Close the session and release resources."""
|
|
47
|
-
self.
|
|
53
|
+
if not self._external_session:
|
|
54
|
+
self.session.close()
|
|
48
55
|
|
|
49
56
|
def __enter__(self):
|
|
50
57
|
return self
|
|
@@ -71,6 +78,7 @@ class TavilyClient:
|
|
|
71
78
|
auto_parameters: bool = None,
|
|
72
79
|
include_favicon: bool = None,
|
|
73
80
|
include_usage: bool = None,
|
|
81
|
+
exact_match: bool = None,
|
|
74
82
|
**kwargs
|
|
75
83
|
) -> dict:
|
|
76
84
|
"""
|
|
@@ -95,6 +103,7 @@ class TavilyClient:
|
|
|
95
103
|
"auto_parameters": auto_parameters,
|
|
96
104
|
"include_favicon": include_favicon,
|
|
97
105
|
"include_usage": include_usage,
|
|
106
|
+
"exact_match": exact_match,
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
data = {k: v for k, v in data.items() if v is not None}
|
|
@@ -151,6 +160,7 @@ class TavilyClient:
|
|
|
151
160
|
auto_parameters: bool = None,
|
|
152
161
|
include_favicon: bool = None,
|
|
153
162
|
include_usage: bool = None,
|
|
163
|
+
exact_match: bool = None,
|
|
154
164
|
**kwargs, # Accept custom arguments
|
|
155
165
|
) -> dict:
|
|
156
166
|
"""
|
|
@@ -175,6 +185,7 @@ class TavilyClient:
|
|
|
175
185
|
auto_parameters=auto_parameters,
|
|
176
186
|
include_favicon=include_favicon,
|
|
177
187
|
include_usage=include_usage,
|
|
188
|
+
exact_match=exact_match,
|
|
178
189
|
**kwargs)
|
|
179
190
|
response_dict.setdefault("results", [])
|
|
180
191
|
return response_dict
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tavily-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.23
|
|
4
4
|
Summary: Python wrapper for the Tavily API
|
|
5
5
|
Home-page: https://github.com/tavily-ai/tavily-python
|
|
6
6
|
Author: Tavily AI
|
|
@@ -25,7 +25,7 @@ Dynamic: requires-dist
|
|
|
25
25
|
Dynamic: requires-python
|
|
26
26
|
Dynamic: summary
|
|
27
27
|
|
|
28
|
-
# Tavily Python
|
|
28
|
+
# Tavily Python SDK
|
|
29
29
|
|
|
30
30
|
[](https://github.com/tavily-ai/tavily-python/stargazers)
|
|
31
31
|
[](https://pypi.org/project/tavily-python/)
|
|
@@ -63,6 +63,21 @@ response = tavily_client.search("Who is Leo Messi?")
|
|
|
63
63
|
print(response)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Using exact match to find specific names or phrases
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from tavily import TavilyClient
|
|
70
|
+
|
|
71
|
+
client = TavilyClient(api_key="tvly-YOUR_API_KEY")
|
|
72
|
+
|
|
73
|
+
# Use exact_match=True to only return results containing the exact phrase(s) inside quotes
|
|
74
|
+
response = client.search(
|
|
75
|
+
query='"John Smith" CEO Acme Corp',
|
|
76
|
+
exact_match=True
|
|
77
|
+
)
|
|
78
|
+
print(response)
|
|
79
|
+
```
|
|
80
|
+
|
|
66
81
|
This is equivalent to directly querying our REST API.
|
|
67
82
|
|
|
68
83
|
### Generating context for a RAG Application
|
|
@@ -258,6 +273,53 @@ for chunk in stream:
|
|
|
258
273
|
print(chunk.decode('utf-8'))
|
|
259
274
|
```
|
|
260
275
|
|
|
276
|
+
## Advanced: Custom Session / Client Injection
|
|
277
|
+
|
|
278
|
+
For enterprise environments that proxy Tavily traffic through an API gateway (e.g., for centralized auth, logging, or policy enforcement), you can pass a pre-configured HTTP session instead of a Tavily API key.
|
|
279
|
+
|
|
280
|
+
### Sync (custom `requests.Session`)
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
import requests
|
|
284
|
+
from tavily import TavilyClient
|
|
285
|
+
|
|
286
|
+
# Pre-configure a session with your gateway's auth
|
|
287
|
+
session = requests.Session()
|
|
288
|
+
session.headers["Authorization"] = "Bearer your-gateway-token"
|
|
289
|
+
session.headers["X-Subscription-Key"] = "your-subscription-key"
|
|
290
|
+
|
|
291
|
+
# No Tavily API key needed — auth is handled by the session
|
|
292
|
+
client = TavilyClient(
|
|
293
|
+
session=session,
|
|
294
|
+
api_base_url="https://your-gateway.com/tavily",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
response = client.search("latest AI research")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Async (custom `httpx.AsyncClient`)
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
import httpx
|
|
304
|
+
from tavily import AsyncTavilyClient
|
|
305
|
+
|
|
306
|
+
# Pre-configure an async client with your gateway's auth
|
|
307
|
+
custom_client = httpx.AsyncClient(
|
|
308
|
+
headers={"Authorization": "Bearer your-gateway-token"},
|
|
309
|
+
base_url="https://your-gateway.com/tavily",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
client = AsyncTavilyClient(client=custom_client)
|
|
313
|
+
|
|
314
|
+
response = await client.search("latest AI research")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Key behaviors:**
|
|
318
|
+
- If a custom session/client is provided, `api_key` is optional
|
|
319
|
+
- Custom session headers take precedence over SDK defaults (e.g., your `Authorization` won't be overwritten)
|
|
320
|
+
- Custom session proxies take precedence over SDK proxy settings
|
|
321
|
+
- The SDK will **not** close externally-provided sessions — you manage the lifecycle
|
|
322
|
+
|
|
261
323
|
## Documentation
|
|
262
324
|
|
|
263
325
|
For a complete guide on how to use the different endpoints and their parameters, please head to our [Python API Reference](https://docs.tavily.com/sdk/python/reference).
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
from tests.request_intercept import intercept_requests, clear_interceptor, MockSession
|
|
4
|
+
import tavily.tavily as sync_tavily
|
|
5
|
+
import tavily.async_tavily as async_tavily
|
|
6
|
+
import pytest
|
|
7
|
+
from tavily.errors import MissingAPIKeyError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def sync_interceptor():
|
|
12
|
+
yield intercept_requests(sync_tavily)
|
|
13
|
+
clear_interceptor(sync_tavily)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def async_interceptor():
|
|
18
|
+
yield intercept_requests(async_tavily)
|
|
19
|
+
clear_interceptor(async_tavily)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --- Sync TavilyClient tests ---
|
|
23
|
+
|
|
24
|
+
class TestSyncCustomSession:
|
|
25
|
+
def test_default_session_created_when_none_provided(self, sync_interceptor):
|
|
26
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test")
|
|
27
|
+
assert not client._external_session
|
|
28
|
+
|
|
29
|
+
def test_custom_session_used(self, sync_interceptor):
|
|
30
|
+
custom_session = MockSession(sync_interceptor)
|
|
31
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
32
|
+
assert client._external_session
|
|
33
|
+
assert client.session is custom_session
|
|
34
|
+
|
|
35
|
+
def test_custom_session_preserves_existing_headers(self, sync_interceptor):
|
|
36
|
+
custom_session = MockSession(sync_interceptor)
|
|
37
|
+
custom_session.headers["Authorization"] = "Bearer apim-token-123"
|
|
38
|
+
custom_session.headers["X-Custom"] = "custom-value"
|
|
39
|
+
|
|
40
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
41
|
+
|
|
42
|
+
# Custom Authorization should be preserved (not overwritten by Tavily's)
|
|
43
|
+
assert client.session.headers["Authorization"] == "Bearer apim-token-123"
|
|
44
|
+
# Custom header should be preserved
|
|
45
|
+
assert client.session.headers["X-Custom"] == "custom-value"
|
|
46
|
+
# Tavily defaults should fill in missing headers
|
|
47
|
+
assert client.session.headers["Content-Type"] == "application/json"
|
|
48
|
+
assert client.session.headers["X-Client-Source"] == "tavily-python"
|
|
49
|
+
|
|
50
|
+
def test_custom_session_gets_default_headers_when_empty(self, sync_interceptor):
|
|
51
|
+
custom_session = MockSession(sync_interceptor)
|
|
52
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
53
|
+
|
|
54
|
+
assert client.session.headers["Authorization"] == "Bearer tvly-test"
|
|
55
|
+
assert client.session.headers["Content-Type"] == "application/json"
|
|
56
|
+
|
|
57
|
+
def test_custom_session_preserves_existing_proxies(self, sync_interceptor):
|
|
58
|
+
custom_session = MockSession(sync_interceptor)
|
|
59
|
+
custom_session.proxies["https"] = "http://my-proxy:8080"
|
|
60
|
+
|
|
61
|
+
client = sync_tavily.TavilyClient(
|
|
62
|
+
api_key="tvly-test",
|
|
63
|
+
session=custom_session,
|
|
64
|
+
proxies={"https": "http://tavily-proxy:9090"},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Custom session proxy should take precedence
|
|
68
|
+
assert client.session.proxies["https"] == "http://my-proxy:8080"
|
|
69
|
+
|
|
70
|
+
def test_close_does_not_close_external_session(self, sync_interceptor):
|
|
71
|
+
closed = []
|
|
72
|
+
custom_session = MockSession(sync_interceptor)
|
|
73
|
+
custom_session.close = lambda: closed.append(True)
|
|
74
|
+
|
|
75
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
76
|
+
client.close()
|
|
77
|
+
assert len(closed) == 0
|
|
78
|
+
|
|
79
|
+
def test_close_closes_internal_session(self, sync_interceptor):
|
|
80
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test")
|
|
81
|
+
# Should not raise — just verifies close() is called on internal session
|
|
82
|
+
client.close()
|
|
83
|
+
|
|
84
|
+
def test_context_manager_does_not_close_external_session(self, sync_interceptor):
|
|
85
|
+
closed = []
|
|
86
|
+
custom_session = MockSession(sync_interceptor)
|
|
87
|
+
custom_session.close = lambda: closed.append(True)
|
|
88
|
+
|
|
89
|
+
with sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session):
|
|
90
|
+
pass
|
|
91
|
+
assert len(closed) == 0
|
|
92
|
+
|
|
93
|
+
def test_custom_session_sends_request(self, sync_interceptor):
|
|
94
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
95
|
+
custom_session = MockSession(sync_interceptor)
|
|
96
|
+
custom_session.headers["Authorization"] = "Bearer apim-token"
|
|
97
|
+
|
|
98
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
99
|
+
client.search("test query")
|
|
100
|
+
|
|
101
|
+
req = sync_interceptor.get_request()
|
|
102
|
+
assert req is not None
|
|
103
|
+
assert req.headers["Authorization"] == "Bearer apim-token"
|
|
104
|
+
|
|
105
|
+
# --- API key validation edge cases ---
|
|
106
|
+
|
|
107
|
+
def test_no_api_key_no_session_raises(self, monkeypatch):
|
|
108
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
109
|
+
with pytest.raises(MissingAPIKeyError):
|
|
110
|
+
sync_tavily.TavilyClient()
|
|
111
|
+
|
|
112
|
+
def test_no_api_key_with_session_allowed(self, sync_interceptor, monkeypatch):
|
|
113
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
114
|
+
custom_session = MockSession(sync_interceptor)
|
|
115
|
+
custom_session.headers["Authorization"] = "Bearer apim-token"
|
|
116
|
+
client = sync_tavily.TavilyClient(session=custom_session)
|
|
117
|
+
assert client.api_key is None
|
|
118
|
+
assert "Authorization" not in client.headers
|
|
119
|
+
assert client.session.headers["Authorization"] == "Bearer apim-token"
|
|
120
|
+
|
|
121
|
+
def test_no_api_key_with_session_no_auth_header_on_defaults(self, sync_interceptor, monkeypatch):
|
|
122
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
123
|
+
custom_session = MockSession(sync_interceptor)
|
|
124
|
+
client = sync_tavily.TavilyClient(session=custom_session)
|
|
125
|
+
# No api_key means no Authorization in defaults
|
|
126
|
+
assert "Authorization" not in client.headers
|
|
127
|
+
# Session shouldn't get an Authorization header either
|
|
128
|
+
assert "Authorization" not in client.session.headers
|
|
129
|
+
|
|
130
|
+
def test_no_api_key_with_session_sends_request(self, sync_interceptor, monkeypatch):
|
|
131
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
132
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
133
|
+
custom_session = MockSession(sync_interceptor)
|
|
134
|
+
custom_session.headers["Authorization"] = "Bearer apim-token"
|
|
135
|
+
|
|
136
|
+
client = sync_tavily.TavilyClient(session=custom_session)
|
|
137
|
+
client.search("test query")
|
|
138
|
+
|
|
139
|
+
req = sync_interceptor.get_request()
|
|
140
|
+
assert req is not None
|
|
141
|
+
assert req.headers["Authorization"] == "Bearer apim-token"
|
|
142
|
+
|
|
143
|
+
def test_empty_string_api_key_no_session_raises(self, monkeypatch):
|
|
144
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
145
|
+
with pytest.raises(MissingAPIKeyError):
|
|
146
|
+
sync_tavily.TavilyClient(api_key="")
|
|
147
|
+
|
|
148
|
+
def test_empty_string_api_key_with_session_allowed(self, sync_interceptor):
|
|
149
|
+
custom_session = MockSession(sync_interceptor)
|
|
150
|
+
client = sync_tavily.TavilyClient(api_key="", session=custom_session)
|
|
151
|
+
assert "Authorization" not in client.headers
|
|
152
|
+
|
|
153
|
+
def test_api_key_and_session_both_provided(self, sync_interceptor):
|
|
154
|
+
custom_session = MockSession(sync_interceptor)
|
|
155
|
+
client = sync_tavily.TavilyClient(api_key="tvly-test", session=custom_session)
|
|
156
|
+
# api_key provided and session has no Authorization, so default fills it in
|
|
157
|
+
assert client.session.headers["Authorization"] == "Bearer tvly-test"
|
|
158
|
+
|
|
159
|
+
def test_custom_session_with_all_endpoints(self, sync_interceptor):
|
|
160
|
+
custom_session = MockSession(sync_interceptor)
|
|
161
|
+
custom_session.headers["Authorization"] = "Bearer apim-token"
|
|
162
|
+
|
|
163
|
+
client = sync_tavily.TavilyClient(session=custom_session)
|
|
164
|
+
|
|
165
|
+
# search
|
|
166
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
167
|
+
client.search("test")
|
|
168
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
169
|
+
|
|
170
|
+
# extract
|
|
171
|
+
sync_interceptor.set_response(200, json={"results": [], "failed_results": []})
|
|
172
|
+
client.extract(urls=["https://example.com"])
|
|
173
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
174
|
+
|
|
175
|
+
# crawl
|
|
176
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
177
|
+
client.crawl(url="https://example.com")
|
|
178
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
179
|
+
|
|
180
|
+
# map
|
|
181
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
182
|
+
client.map(url="https://example.com")
|
|
183
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
184
|
+
|
|
185
|
+
def test_custom_session_with_custom_base_url(self, sync_interceptor):
|
|
186
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
187
|
+
custom_session = MockSession(sync_interceptor)
|
|
188
|
+
|
|
189
|
+
client = sync_tavily.TavilyClient(
|
|
190
|
+
api_key="tvly-test",
|
|
191
|
+
session=custom_session,
|
|
192
|
+
api_base_url="https://apim.corp.com/tavily",
|
|
193
|
+
)
|
|
194
|
+
client.search("test")
|
|
195
|
+
|
|
196
|
+
req = sync_interceptor.get_request()
|
|
197
|
+
assert req.url == "https://apim.corp.com/tavily/search"
|
|
198
|
+
|
|
199
|
+
def test_custom_session_proxies_fill_missing_protocols(self, sync_interceptor):
|
|
200
|
+
custom_session = MockSession(sync_interceptor)
|
|
201
|
+
custom_session.proxies["http"] = "http://session-proxy:8080"
|
|
202
|
+
|
|
203
|
+
client = sync_tavily.TavilyClient(
|
|
204
|
+
api_key="tvly-test",
|
|
205
|
+
session=custom_session,
|
|
206
|
+
proxies={"http": "http://arg-proxy:9090", "https": "http://arg-proxy:9091"},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# http: session proxy wins
|
|
210
|
+
assert client.session.proxies["http"] == "http://session-proxy:8080"
|
|
211
|
+
# https: session didn't have it, so arg fills it in
|
|
212
|
+
assert client.session.proxies["https"] == "http://arg-proxy:9091"
|
|
213
|
+
|
|
214
|
+
def test_custom_session_project_id_header(self, sync_interceptor):
|
|
215
|
+
custom_session = MockSession(sync_interceptor)
|
|
216
|
+
client = sync_tavily.TavilyClient(
|
|
217
|
+
api_key="tvly-test",
|
|
218
|
+
session=custom_session,
|
|
219
|
+
project_id="my-project",
|
|
220
|
+
)
|
|
221
|
+
assert client.session.headers["X-Project-ID"] == "my-project"
|
|
222
|
+
|
|
223
|
+
def test_shared_session_across_multiple_clients(self, sync_interceptor):
|
|
224
|
+
sync_interceptor.set_response(200, json={"results": []})
|
|
225
|
+
shared_session = MockSession(sync_interceptor)
|
|
226
|
+
shared_session.headers["Authorization"] = "Bearer shared-token"
|
|
227
|
+
|
|
228
|
+
client1 = sync_tavily.TavilyClient(session=shared_session)
|
|
229
|
+
client2 = sync_tavily.TavilyClient(session=shared_session)
|
|
230
|
+
|
|
231
|
+
assert client1.session is client2.session
|
|
232
|
+
|
|
233
|
+
client1.search("query1")
|
|
234
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer shared-token"
|
|
235
|
+
|
|
236
|
+
client2.search("query2")
|
|
237
|
+
assert sync_interceptor.get_request().headers["Authorization"] == "Bearer shared-token"
|
|
238
|
+
|
|
239
|
+
# Closing one client should not close the shared session
|
|
240
|
+
client1.close()
|
|
241
|
+
client2.search("query3")
|
|
242
|
+
assert sync_interceptor.get_request() is not None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# --- Async AsyncTavilyClient tests ---
|
|
246
|
+
|
|
247
|
+
class TestAsyncCustomClient:
|
|
248
|
+
def test_default_client_created_when_none_provided(self):
|
|
249
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test")
|
|
250
|
+
assert not client._external_client
|
|
251
|
+
|
|
252
|
+
def test_custom_client_used(self):
|
|
253
|
+
custom_client = httpx.AsyncClient()
|
|
254
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
255
|
+
assert client._external_client
|
|
256
|
+
assert client._client is custom_client
|
|
257
|
+
|
|
258
|
+
def test_custom_client_preserves_existing_headers(self):
|
|
259
|
+
custom_client = httpx.AsyncClient(headers={
|
|
260
|
+
"Authorization": "Bearer apim-token-123",
|
|
261
|
+
"X-Custom": "custom-value",
|
|
262
|
+
})
|
|
263
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
264
|
+
|
|
265
|
+
assert client._client.headers["Authorization"] == "Bearer apim-token-123"
|
|
266
|
+
assert client._client.headers["X-Custom"] == "custom-value"
|
|
267
|
+
assert client._client.headers["Content-Type"] == "application/json"
|
|
268
|
+
assert client._client.headers["X-Client-Source"] == "tavily-python"
|
|
269
|
+
|
|
270
|
+
def test_custom_client_gets_default_headers_when_empty(self):
|
|
271
|
+
custom_client = httpx.AsyncClient()
|
|
272
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
273
|
+
|
|
274
|
+
assert client._client.headers["Authorization"] == "Bearer tvly-test"
|
|
275
|
+
assert client._client.headers["Content-Type"] == "application/json"
|
|
276
|
+
|
|
277
|
+
def test_custom_client_base_url_set_when_missing(self):
|
|
278
|
+
custom_client = httpx.AsyncClient()
|
|
279
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
280
|
+
assert "api.tavily.com" in str(client._client.base_url)
|
|
281
|
+
|
|
282
|
+
def test_custom_client_base_url_preserved_when_set(self):
|
|
283
|
+
custom_client = httpx.AsyncClient(base_url="https://apim.example.com/tavily")
|
|
284
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
285
|
+
assert "apim.example.com" in str(client._client.base_url)
|
|
286
|
+
|
|
287
|
+
def test_close_does_not_close_external_client(self):
|
|
288
|
+
closed = []
|
|
289
|
+
custom_client = httpx.AsyncClient()
|
|
290
|
+
|
|
291
|
+
async def run():
|
|
292
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
293
|
+
original_aclose = custom_client.aclose
|
|
294
|
+
|
|
295
|
+
async def track_close():
|
|
296
|
+
closed.append(True)
|
|
297
|
+
await original_aclose()
|
|
298
|
+
|
|
299
|
+
custom_client.aclose = track_close
|
|
300
|
+
await client.close()
|
|
301
|
+
|
|
302
|
+
asyncio.run(run())
|
|
303
|
+
assert len(closed) == 0
|
|
304
|
+
|
|
305
|
+
def test_context_manager_does_not_close_external_client(self):
|
|
306
|
+
closed = []
|
|
307
|
+
custom_client = httpx.AsyncClient()
|
|
308
|
+
|
|
309
|
+
async def run():
|
|
310
|
+
original_aclose = custom_client.aclose
|
|
311
|
+
|
|
312
|
+
async def track_close():
|
|
313
|
+
closed.append(True)
|
|
314
|
+
await original_aclose()
|
|
315
|
+
|
|
316
|
+
custom_client.aclose = track_close
|
|
317
|
+
async with async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
asyncio.run(run())
|
|
321
|
+
assert len(closed) == 0
|
|
322
|
+
|
|
323
|
+
def test_custom_client_sends_request(self, async_interceptor):
|
|
324
|
+
async_interceptor.set_response(200, json={"results": []})
|
|
325
|
+
custom_client = httpx.AsyncClient(
|
|
326
|
+
headers={"Authorization": "Bearer apim-token"},
|
|
327
|
+
base_url="https://api.tavily.com",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
client = async_tavily.AsyncTavilyClient(api_key="tvly-test", client=custom_client)
|
|
331
|
+
asyncio.run(client.search("test query"))
|
|
332
|
+
|
|
333
|
+
req = async_interceptor.get_request()
|
|
334
|
+
assert req is not None
|
|
335
|
+
assert req.headers["Authorization"] == "Bearer apim-token"
|
|
336
|
+
|
|
337
|
+
# --- API key validation edge cases ---
|
|
338
|
+
|
|
339
|
+
def test_no_api_key_no_client_raises(self, monkeypatch):
|
|
340
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
341
|
+
with pytest.raises(MissingAPIKeyError):
|
|
342
|
+
async_tavily.AsyncTavilyClient()
|
|
343
|
+
|
|
344
|
+
def test_no_api_key_with_client_allowed(self, monkeypatch):
|
|
345
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
346
|
+
custom_client = httpx.AsyncClient(
|
|
347
|
+
headers={"Authorization": "Bearer apim-token"},
|
|
348
|
+
)
|
|
349
|
+
client = async_tavily.AsyncTavilyClient(client=custom_client)
|
|
350
|
+
assert client._client.headers["Authorization"] == "Bearer apim-token"
|
|
351
|
+
|
|
352
|
+
def test_no_api_key_with_client_no_auth_header_on_defaults(self, monkeypatch):
|
|
353
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
354
|
+
custom_client = httpx.AsyncClient()
|
|
355
|
+
client = async_tavily.AsyncTavilyClient(client=custom_client)
|
|
356
|
+
# httpx always has headers dict but Authorization shouldn't be added
|
|
357
|
+
assert "authorization" not in [k.lower() for k in client._client.headers.keys()
|
|
358
|
+
if k.lower() == "authorization"
|
|
359
|
+
and client._client.headers[k].startswith("Bearer None")]
|
|
360
|
+
|
|
361
|
+
def test_no_api_key_with_client_sends_request(self, async_interceptor, monkeypatch):
|
|
362
|
+
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
363
|
+
async_interceptor.set_response(200, json={"results": []})
|
|
364
|
+
custom_client = httpx.AsyncClient(
|
|
365
|
+
headers={"Authorization": "Bearer apim-token"},
|
|
366
|
+
base_url="https://api.tavily.com",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
client = async_tavily.AsyncTavilyClient(client=custom_client)
|
|
370
|
+
asyncio.run(client.search("test query"))
|
|
371
|
+
|
|
372
|
+
req = async_interceptor.get_request()
|
|
373
|
+
assert req is not None
|
|
374
|
+
assert req.headers["Authorization"] == "Bearer apim-token"
|
|
375
|
+
|
|
376
|
+
def test_custom_client_with_all_endpoints(self, async_interceptor):
|
|
377
|
+
custom_client = httpx.AsyncClient(
|
|
378
|
+
headers={"Authorization": "Bearer apim-token"},
|
|
379
|
+
base_url="https://api.tavily.com",
|
|
380
|
+
)
|
|
381
|
+
client = async_tavily.AsyncTavilyClient(client=custom_client)
|
|
382
|
+
|
|
383
|
+
# search
|
|
384
|
+
async_interceptor.set_response(200, json={"results": []})
|
|
385
|
+
asyncio.run(client.search("test"))
|
|
386
|
+
assert async_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
387
|
+
|
|
388
|
+
# extract
|
|
389
|
+
async_interceptor.set_response(200, json={"results": [], "failed_results": []})
|
|
390
|
+
asyncio.run(client.extract(urls=["https://example.com"]))
|
|
391
|
+
assert async_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
392
|
+
|
|
393
|
+
# crawl
|
|
394
|
+
async_interceptor.set_response(200, json={"results": []})
|
|
395
|
+
asyncio.run(client.crawl(url="https://example.com"))
|
|
396
|
+
assert async_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
397
|
+
|
|
398
|
+
# map
|
|
399
|
+
async_interceptor.set_response(200, json={"results": []})
|
|
400
|
+
asyncio.run(client.map(url="https://example.com"))
|
|
401
|
+
assert async_interceptor.get_request().headers["Authorization"] == "Bearer apim-token"
|
|
402
|
+
|
|
403
|
+
def test_custom_client_project_id_header(self):
|
|
404
|
+
custom_client = httpx.AsyncClient()
|
|
405
|
+
client = async_tavily.AsyncTavilyClient(
|
|
406
|
+
api_key="tvly-test",
|
|
407
|
+
client=custom_client,
|
|
408
|
+
project_id="my-project",
|
|
409
|
+
)
|
|
410
|
+
assert client._client.headers["X-Project-ID"] == "my-project"
|
|
@@ -32,7 +32,7 @@ def validate_specific(request, response):
|
|
|
32
32
|
assert request.headers["Authorization"] == "Bearer tvly-test"
|
|
33
33
|
assert request.headers["X-Client-Source"] == "tavily-python"
|
|
34
34
|
assert request.timeout == 10
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
request_json = request.json()
|
|
37
37
|
for key, value in {
|
|
38
38
|
"query": "What is Tavily?",
|
|
@@ -44,7 +44,8 @@ def validate_specific(request, response):
|
|
|
44
44
|
"exclude_domains": ["example.com"],
|
|
45
45
|
"include_answer": "advanced",
|
|
46
46
|
"include_raw_content": True,
|
|
47
|
-
"include_images": True
|
|
47
|
+
"include_images": True,
|
|
48
|
+
"exact_match": True
|
|
48
49
|
}.items():
|
|
49
50
|
assert request_json.get(key) == value
|
|
50
51
|
|
|
@@ -69,6 +70,7 @@ def test_sync_search_specific(sync_interceptor, sync_client):
|
|
|
69
70
|
include_answer="advanced",
|
|
70
71
|
include_raw_content=True,
|
|
71
72
|
include_images=True,
|
|
73
|
+
exact_match=True,
|
|
72
74
|
timeout=10
|
|
73
75
|
)
|
|
74
76
|
|
|
@@ -94,8 +96,61 @@ def test_async_search_specific(async_interceptor, async_client):
|
|
|
94
96
|
include_answer="advanced",
|
|
95
97
|
include_raw_content=True,
|
|
96
98
|
include_images=True,
|
|
99
|
+
exact_match=True,
|
|
97
100
|
timeout=10
|
|
98
101
|
))
|
|
99
102
|
|
|
100
103
|
request = async_interceptor.get_request()
|
|
101
104
|
validate_specific(request, response)
|
|
105
|
+
|
|
106
|
+
def test_sync_search_exact_match_not_sent_by_default(sync_interceptor, sync_client):
|
|
107
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
108
|
+
sync_client.search("What is Tavily?")
|
|
109
|
+
request = sync_interceptor.get_request()
|
|
110
|
+
assert "exact_match" not in request.json()
|
|
111
|
+
|
|
112
|
+
def test_sync_search_exact_match_true(sync_interceptor, sync_client):
|
|
113
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
114
|
+
sync_client.search("What is Tavily?", exact_match=True)
|
|
115
|
+
request = sync_interceptor.get_request()
|
|
116
|
+
assert request.json()["exact_match"] is True
|
|
117
|
+
|
|
118
|
+
def test_sync_search_exact_match_false(sync_interceptor, sync_client):
|
|
119
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
120
|
+
sync_client.search("What is Tavily?", exact_match=False)
|
|
121
|
+
request = sync_interceptor.get_request()
|
|
122
|
+
assert request.json()["exact_match"] is False
|
|
123
|
+
|
|
124
|
+
def test_async_search_exact_match_not_sent_by_default(async_interceptor, async_client):
|
|
125
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
126
|
+
asyncio.run(async_client.search("What is Tavily?"))
|
|
127
|
+
request = async_interceptor.get_request()
|
|
128
|
+
assert "exact_match" not in request.json()
|
|
129
|
+
|
|
130
|
+
def test_async_search_exact_match_true(async_interceptor, async_client):
|
|
131
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
132
|
+
asyncio.run(async_client.search("What is Tavily?", exact_match=True))
|
|
133
|
+
request = async_interceptor.get_request()
|
|
134
|
+
assert request.json()["exact_match"] is True
|
|
135
|
+
|
|
136
|
+
def test_async_search_exact_match_false(async_interceptor, async_client):
|
|
137
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
138
|
+
asyncio.run(async_client.search("What is Tavily?", exact_match=False))
|
|
139
|
+
request = async_interceptor.get_request()
|
|
140
|
+
assert request.json()["exact_match"] is False
|
|
141
|
+
|
|
142
|
+
def test_sync_search_exact_match_query_quotes_escaped_in_payload(sync_interceptor, sync_client):
|
|
143
|
+
sync_interceptor.set_response(200, json=dummy_response)
|
|
144
|
+
sync_client.search('"John Smith" CEO Acme Corp', exact_match=True)
|
|
145
|
+
request = sync_interceptor.get_request()
|
|
146
|
+
# The raw JSON payload should have escaped quotes for the quoted phrase
|
|
147
|
+
assert r'\"John Smith\"' in request.body
|
|
148
|
+
# But the parsed query should preserve the original quotes
|
|
149
|
+
assert request.json()["query"] == '"John Smith" CEO Acme Corp'
|
|
150
|
+
|
|
151
|
+
def test_async_search_exact_match_query_quotes_escaped_in_payload(async_interceptor, async_client):
|
|
152
|
+
async_interceptor.set_response(200, json=dummy_response)
|
|
153
|
+
asyncio.run(async_client.search('"John Smith" CEO Acme Corp', exact_match=True))
|
|
154
|
+
request = async_interceptor.get_request()
|
|
155
|
+
assert r'\"John Smith\"' in request.body
|
|
156
|
+
assert request.json()["query"] == '"John Smith" CEO Acme Corp'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|