tavily-python 0.7.24__tar.gz → 0.7.25__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.24 → tavily_python-0.7.25}/PKG-INFO +24 -1
- {tavily_python-0.7.24 → tavily_python-0.7.25}/README.md +23 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/setup.py +1 -1
- tavily_python-0.7.25/tavily/__init__.py +11 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/async_tavily.py +107 -111
- tavily_python-0.7.25/tavily/errors.py +71 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/tavily.py +89 -117
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily_python.egg-info/PKG-INFO +24 -1
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_custom_session.py +17 -10
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_errors.py +12 -13
- tavily_python-0.7.24/tavily/__init__.py +0 -4
- tavily_python-0.7.24/tavily/errors.py +0 -30
- {tavily_python-0.7.24 → tavily_python-0.7.25}/LICENSE +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/setup.cfg +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/config.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/hybrid_rag/__init__.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/hybrid_rag/hybrid_rag.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily/utils.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily_python.egg-info/SOURCES.txt +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily_python.egg-info/dependency_links.txt +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily_python.egg-info/requires.txt +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tavily_python.egg-info/top_level.txt +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_crawl.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_map.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_research.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/tests/test_search.py +0 -0
- {tavily_python-0.7.24 → tavily_python-0.7.25}/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.25
|
|
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
|
|
@@ -40,6 +40,29 @@ The Tavily Python wrapper allows for easy interaction with the Tavily API, offer
|
|
|
40
40
|
pip install tavily-python
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Keyless mode
|
|
44
|
+
|
|
45
|
+
You can try Tavily without an API key. Instantiate `TavilyClient()` with no arguments and the SDK runs in keyless mode against the public Tavily API. Keyless mode supports `search()` and `extract()` only; other methods raise an error explaining that an API key is required.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tavily import TavilyClient, TavilyKeylessLimitError
|
|
49
|
+
|
|
50
|
+
# No API key needed
|
|
51
|
+
client = TavilyClient()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = client.search("Who is Leo Messi?")
|
|
55
|
+
print(response)
|
|
56
|
+
except TavilyKeylessLimitError as e:
|
|
57
|
+
# Rate-limit cap reached. The exception carries the human-readable
|
|
58
|
+
# message plus structured fields (code, window, retry_after_seconds,
|
|
59
|
+
# next_actions) returned by the Tavily API.
|
|
60
|
+
print(e)
|
|
61
|
+
print("retry after:", e.retry_after_seconds, "seconds")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Keyless usage is rate-limited. For higher limits and the full set of endpoints (including `crawl`, `map`, and `research`), [sign up for a Tavily API key](https://tavily.com) and pass it as `TavilyClient(api_key="tvly-...")`.
|
|
65
|
+
|
|
43
66
|
# Tavily Search
|
|
44
67
|
|
|
45
68
|
Search lets you search the web for a given query.
|
|
@@ -13,6 +13,29 @@ The Tavily Python wrapper allows for easy interaction with the Tavily API, offer
|
|
|
13
13
|
pip install tavily-python
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Keyless mode
|
|
17
|
+
|
|
18
|
+
You can try Tavily without an API key. Instantiate `TavilyClient()` with no arguments and the SDK runs in keyless mode against the public Tavily API. Keyless mode supports `search()` and `extract()` only; other methods raise an error explaining that an API key is required.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from tavily import TavilyClient, TavilyKeylessLimitError
|
|
22
|
+
|
|
23
|
+
# No API key needed
|
|
24
|
+
client = TavilyClient()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
response = client.search("Who is Leo Messi?")
|
|
28
|
+
print(response)
|
|
29
|
+
except TavilyKeylessLimitError as e:
|
|
30
|
+
# Rate-limit cap reached. The exception carries the human-readable
|
|
31
|
+
# message plus structured fields (code, window, retry_after_seconds,
|
|
32
|
+
# next_actions) returned by the Tavily API.
|
|
33
|
+
print(e)
|
|
34
|
+
print("retry after:", e.retry_after_seconds, "seconds")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keyless usage is rate-limited. For higher limits and the full set of endpoints (including `crawl`, `map`, and `research`), [sign up for a Tavily API key](https://tavily.com) and pass it as `TavilyClient(api_key="tvly-...")`.
|
|
38
|
+
|
|
16
39
|
# Tavily Search
|
|
17
40
|
|
|
18
41
|
Search lets you search the web for a given query.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .async_tavily import AsyncTavilyClient
|
|
2
|
+
from .tavily import Client, TavilyClient
|
|
3
|
+
from .errors import (
|
|
4
|
+
InvalidAPIKeyError,
|
|
5
|
+
UsageLimitExceededError,
|
|
6
|
+
MissingAPIKeyError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
TavilyKeylessLimitError,
|
|
9
|
+
KeylessUnsupportedEndpointError,
|
|
10
|
+
)
|
|
11
|
+
from .hybrid_rag import TavilyHybridClient
|
|
@@ -6,7 +6,37 @@ from typing import Literal, Sequence, Optional, List, Union, AsyncGenerator, Awa
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
8
|
from .utils import get_max_items_from_list
|
|
9
|
-
from .errors import
|
|
9
|
+
from .errors import (
|
|
10
|
+
UsageLimitExceededError,
|
|
11
|
+
InvalidAPIKeyError,
|
|
12
|
+
MissingAPIKeyError,
|
|
13
|
+
BadRequestError,
|
|
14
|
+
ForbiddenError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
TavilyKeylessLimitError,
|
|
17
|
+
KeylessUnsupportedEndpointError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_keyless_envelope(body) -> bool:
|
|
22
|
+
"""Return True when the response body matches the Tavily recoverable-error envelope shape."""
|
|
23
|
+
return (
|
|
24
|
+
isinstance(body, dict)
|
|
25
|
+
and isinstance(body.get("error"), dict)
|
|
26
|
+
and isinstance(body["error"].get("code"), str)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _raise_keyless_envelope(body) -> None:
|
|
31
|
+
"""Raise ``TavilyKeylessLimitError`` from an envelope-shaped response body."""
|
|
32
|
+
err = body["error"]
|
|
33
|
+
raise TavilyKeylessLimitError(
|
|
34
|
+
message=err.get("message") or "",
|
|
35
|
+
code=err.get("code"),
|
|
36
|
+
window=err.get("window"),
|
|
37
|
+
retry_after_seconds=err.get("retry_after_seconds"),
|
|
38
|
+
next_actions=err.get("next_actions") or [],
|
|
39
|
+
)
|
|
10
40
|
|
|
11
41
|
|
|
12
42
|
class AsyncTavilyClient:
|
|
@@ -27,18 +57,28 @@ class AsyncTavilyClient:
|
|
|
27
57
|
if api_key is None:
|
|
28
58
|
api_key = os.getenv("TAVILY_API_KEY")
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
# api_key is optional: when absent, the client runs in keyless mode.
|
|
61
|
+
# Only `search` and `extract` are available in keyless mode.
|
|
62
|
+
api_key = api_key or None
|
|
32
63
|
|
|
33
64
|
tavily_project = project_id or os.getenv("TAVILY_PROJECT")
|
|
34
65
|
|
|
35
66
|
self._api_base_url = api_base_url or "https://api.tavily.com"
|
|
36
67
|
self._company_info_tags = company_info_tags
|
|
68
|
+
self._keyless = api_key is None and client is None
|
|
69
|
+
|
|
70
|
+
if self._keyless:
|
|
71
|
+
# Honor an explicit client_source so non-SDK keyless surfaces
|
|
72
|
+
# (e.g. tavily-cli-keyless) are attributed correctly by the server.
|
|
73
|
+
client_source_header = client_source or "tavily-python-keyless"
|
|
74
|
+
else:
|
|
75
|
+
client_source_header = client_source or "tavily-python"
|
|
37
76
|
|
|
38
77
|
default_headers = {
|
|
39
78
|
"Content-Type": "application/json",
|
|
40
79
|
**({"Authorization": f"Bearer {api_key}"} if api_key else {}),
|
|
41
|
-
"X-
|
|
80
|
+
**({"X-Tavily-Access-Mode": "keyless"} if self._keyless else {}),
|
|
81
|
+
"X-Client-Source": client_source_header,
|
|
42
82
|
**({"X-Project-ID": tavily_project} if tavily_project else {}),
|
|
43
83
|
**({"X-Session-Id": session_id} if session_id else {}),
|
|
44
84
|
**({"X-Human-Id": human_id} if human_id else {}),
|
|
@@ -89,6 +129,49 @@ class AsyncTavilyClient:
|
|
|
89
129
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
90
130
|
await self.close()
|
|
91
131
|
|
|
132
|
+
def _handle_error_response(self, response, body_override=None) -> None:
|
|
133
|
+
"""Raise an appropriate exception for a non-2xx httpx response.
|
|
134
|
+
|
|
135
|
+
On keyless calls, checks the response body for the recoverable-error
|
|
136
|
+
envelope shape and raises ``TavilyKeylessLimitError`` with the
|
|
137
|
+
envelope's structured fields when present.
|
|
138
|
+
"""
|
|
139
|
+
if body_override is not None:
|
|
140
|
+
body = body_override
|
|
141
|
+
else:
|
|
142
|
+
try:
|
|
143
|
+
body = response.json()
|
|
144
|
+
except Exception:
|
|
145
|
+
body = None
|
|
146
|
+
|
|
147
|
+
if self._keyless and _is_keyless_envelope(body):
|
|
148
|
+
_raise_keyless_envelope(body)
|
|
149
|
+
|
|
150
|
+
detail = ""
|
|
151
|
+
if isinstance(body, dict):
|
|
152
|
+
try:
|
|
153
|
+
detail = body.get("detail", {}).get("error", None) or ""
|
|
154
|
+
except Exception:
|
|
155
|
+
detail = ""
|
|
156
|
+
elif isinstance(body, str):
|
|
157
|
+
detail = body
|
|
158
|
+
|
|
159
|
+
if response.status_code == 429:
|
|
160
|
+
raise UsageLimitExceededError(detail)
|
|
161
|
+
elif response.status_code in [403, 432, 433]:
|
|
162
|
+
raise ForbiddenError(detail)
|
|
163
|
+
elif response.status_code == 401:
|
|
164
|
+
raise InvalidAPIKeyError(detail)
|
|
165
|
+
elif response.status_code == 400:
|
|
166
|
+
raise BadRequestError(detail)
|
|
167
|
+
else:
|
|
168
|
+
raise response.raise_for_status()
|
|
169
|
+
|
|
170
|
+
def _check_keyless_supported(self, method: str) -> None:
|
|
171
|
+
"""Raise ``KeylessUnsupportedEndpointError`` when running in keyless mode."""
|
|
172
|
+
if self._keyless:
|
|
173
|
+
raise KeylessUnsupportedEndpointError(method)
|
|
174
|
+
|
|
92
175
|
@staticmethod
|
|
93
176
|
def _pop_request_headers(kwargs: dict) -> Optional[dict]:
|
|
94
177
|
"""Pop session_id, human_id, and client_name from kwargs and return them as headers.
|
|
@@ -169,22 +252,7 @@ class AsyncTavilyClient:
|
|
|
169
252
|
if response.status_code == 200:
|
|
170
253
|
return response.json()
|
|
171
254
|
else:
|
|
172
|
-
|
|
173
|
-
try:
|
|
174
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
175
|
-
except Exception:
|
|
176
|
-
pass
|
|
177
|
-
|
|
178
|
-
if response.status_code == 429:
|
|
179
|
-
raise UsageLimitExceededError(detail)
|
|
180
|
-
elif response.status_code in [403,432,433]:
|
|
181
|
-
raise ForbiddenError(detail)
|
|
182
|
-
elif response.status_code == 401:
|
|
183
|
-
raise InvalidAPIKeyError(detail)
|
|
184
|
-
elif response.status_code == 400:
|
|
185
|
-
raise BadRequestError(detail)
|
|
186
|
-
else:
|
|
187
|
-
raise response.raise_for_status()
|
|
255
|
+
self._handle_error_response(response)
|
|
188
256
|
|
|
189
257
|
async def search(self,
|
|
190
258
|
query: str,
|
|
@@ -283,23 +351,7 @@ class AsyncTavilyClient:
|
|
|
283
351
|
if response.status_code == 200:
|
|
284
352
|
return response.json()
|
|
285
353
|
else:
|
|
286
|
-
|
|
287
|
-
try:
|
|
288
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
289
|
-
except Exception:
|
|
290
|
-
pass
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if response.status_code == 429:
|
|
294
|
-
raise UsageLimitExceededError(detail)
|
|
295
|
-
elif response.status_code in [403,432,433]:
|
|
296
|
-
raise ForbiddenError(detail)
|
|
297
|
-
elif response.status_code == 401:
|
|
298
|
-
raise InvalidAPIKeyError(detail)
|
|
299
|
-
elif response.status_code == 400:
|
|
300
|
-
raise BadRequestError(detail)
|
|
301
|
-
else:
|
|
302
|
-
raise response.raise_for_status()
|
|
354
|
+
self._handle_error_response(response)
|
|
303
355
|
|
|
304
356
|
async def extract(self,
|
|
305
357
|
urls: Union[List[str], str], # Accept a list of URLs or a single URL
|
|
@@ -394,22 +446,7 @@ class AsyncTavilyClient:
|
|
|
394
446
|
if response.status_code == 200:
|
|
395
447
|
return response.json()
|
|
396
448
|
else:
|
|
397
|
-
|
|
398
|
-
try:
|
|
399
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
400
|
-
except Exception:
|
|
401
|
-
pass
|
|
402
|
-
|
|
403
|
-
if response.status_code == 429:
|
|
404
|
-
raise UsageLimitExceededError(detail)
|
|
405
|
-
elif response.status_code in [403,432,433]:
|
|
406
|
-
raise ForbiddenError(detail)
|
|
407
|
-
elif response.status_code == 401:
|
|
408
|
-
raise InvalidAPIKeyError(detail)
|
|
409
|
-
elif response.status_code == 400:
|
|
410
|
-
raise BadRequestError(detail)
|
|
411
|
-
else:
|
|
412
|
-
raise response.raise_for_status()
|
|
449
|
+
self._handle_error_response(response)
|
|
413
450
|
|
|
414
451
|
async def crawl(self,
|
|
415
452
|
url: str,
|
|
@@ -435,6 +472,7 @@ class AsyncTavilyClient:
|
|
|
435
472
|
Combined crawl method.
|
|
436
473
|
|
|
437
474
|
"""
|
|
475
|
+
self._check_keyless_supported("crawl")
|
|
438
476
|
response_dict = await self._crawl(url,
|
|
439
477
|
max_depth=max_depth,
|
|
440
478
|
max_breadth=max_breadth,
|
|
@@ -505,22 +543,7 @@ class AsyncTavilyClient:
|
|
|
505
543
|
if response.status_code == 200:
|
|
506
544
|
return response.json()
|
|
507
545
|
else:
|
|
508
|
-
|
|
509
|
-
try:
|
|
510
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
511
|
-
except Exception:
|
|
512
|
-
pass
|
|
513
|
-
|
|
514
|
-
if response.status_code == 429:
|
|
515
|
-
raise UsageLimitExceededError(detail)
|
|
516
|
-
elif response.status_code in [403,432,433]:
|
|
517
|
-
raise ForbiddenError(detail)
|
|
518
|
-
elif response.status_code == 401:
|
|
519
|
-
raise InvalidAPIKeyError(detail)
|
|
520
|
-
elif response.status_code == 400:
|
|
521
|
-
raise BadRequestError(detail)
|
|
522
|
-
else:
|
|
523
|
-
raise response.raise_for_status()
|
|
546
|
+
self._handle_error_response(response)
|
|
524
547
|
|
|
525
548
|
async def map(self,
|
|
526
549
|
url: str,
|
|
@@ -542,6 +565,7 @@ class AsyncTavilyClient:
|
|
|
542
565
|
Combined map method.
|
|
543
566
|
|
|
544
567
|
"""
|
|
568
|
+
self._check_keyless_supported("map")
|
|
545
569
|
response_dict = await self._map(url,
|
|
546
570
|
max_depth=max_depth,
|
|
547
571
|
max_breadth=max_breadth,
|
|
@@ -581,6 +605,7 @@ class AsyncTavilyClient:
|
|
|
581
605
|
|
|
582
606
|
Returns a string of JSON containing the search context up to context limit.
|
|
583
607
|
"""
|
|
608
|
+
self._check_keyless_supported("get_search_context")
|
|
584
609
|
timeout = min(timeout, 120)
|
|
585
610
|
response_dict = await self._search(query,
|
|
586
611
|
search_depth=search_depth,
|
|
@@ -617,6 +642,7 @@ class AsyncTavilyClient:
|
|
|
617
642
|
"""
|
|
618
643
|
Q&A search method. Search depth is advanced by default to get the best answer.
|
|
619
644
|
"""
|
|
645
|
+
self._check_keyless_supported("qna_search")
|
|
620
646
|
timeout = min(timeout, 120)
|
|
621
647
|
response_dict = await self._search(query,
|
|
622
648
|
search_depth=search_depth,
|
|
@@ -643,6 +669,7 @@ class AsyncTavilyClient:
|
|
|
643
669
|
country: str = None,
|
|
644
670
|
) -> Sequence[dict]:
|
|
645
671
|
""" Company information search method. Search depth is advanced by default to get the best answer. """
|
|
672
|
+
self._check_keyless_supported("get_company_info")
|
|
646
673
|
timeout = min(timeout, 120)
|
|
647
674
|
|
|
648
675
|
async def _perform_search(topic: str):
|
|
@@ -707,16 +734,13 @@ class AsyncTavilyClient:
|
|
|
707
734
|
except Exception:
|
|
708
735
|
error_text = "Unknown error"
|
|
709
736
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
raise BadRequestError(error_text)
|
|
718
|
-
else:
|
|
719
|
-
raise Exception(f"Error {response.status_code}: {error_text}")
|
|
737
|
+
body_override = None
|
|
738
|
+
try:
|
|
739
|
+
body_override = json.loads(error_text)
|
|
740
|
+
except Exception:
|
|
741
|
+
body_override = error_text
|
|
742
|
+
|
|
743
|
+
self._handle_error_response(response, body_override=body_override)
|
|
720
744
|
|
|
721
745
|
async for chunk in response.aiter_bytes():
|
|
722
746
|
if chunk:
|
|
@@ -737,22 +761,7 @@ class AsyncTavilyClient:
|
|
|
737
761
|
if response.status_code == 200:
|
|
738
762
|
return response.json()
|
|
739
763
|
else:
|
|
740
|
-
|
|
741
|
-
try:
|
|
742
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
743
|
-
except Exception:
|
|
744
|
-
pass
|
|
745
|
-
|
|
746
|
-
if response.status_code == 429:
|
|
747
|
-
raise UsageLimitExceededError(detail)
|
|
748
|
-
elif response.status_code in [403,432,433]:
|
|
749
|
-
raise ForbiddenError(detail)
|
|
750
|
-
elif response.status_code == 401:
|
|
751
|
-
raise InvalidAPIKeyError(detail)
|
|
752
|
-
elif response.status_code == 400:
|
|
753
|
-
raise BadRequestError(detail)
|
|
754
|
-
else:
|
|
755
|
-
raise response.raise_for_status()
|
|
764
|
+
self._handle_error_response(response)
|
|
756
765
|
|
|
757
766
|
return _make_request()
|
|
758
767
|
|
|
@@ -781,6 +790,7 @@ class AsyncTavilyClient:
|
|
|
781
790
|
When stream=False: dict - the response dictionary.
|
|
782
791
|
When stream=True: AsyncGenerator[bytes, None] - iterate over this to get streaming chunks.
|
|
783
792
|
"""
|
|
793
|
+
self._check_keyless_supported("research")
|
|
784
794
|
result = self._research(
|
|
785
795
|
input=input,
|
|
786
796
|
model=model,
|
|
@@ -807,6 +817,7 @@ class AsyncTavilyClient:
|
|
|
807
817
|
Returns:
|
|
808
818
|
dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
|
|
809
819
|
"""
|
|
820
|
+
self._check_keyless_supported("get_research")
|
|
810
821
|
try:
|
|
811
822
|
response = await self._client.get(f"/research/{request_id}")
|
|
812
823
|
except Exception as e:
|
|
@@ -816,19 +827,4 @@ class AsyncTavilyClient:
|
|
|
816
827
|
data = response.json()
|
|
817
828
|
return data
|
|
818
829
|
else:
|
|
819
|
-
|
|
820
|
-
try:
|
|
821
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
822
|
-
except Exception:
|
|
823
|
-
pass
|
|
824
|
-
|
|
825
|
-
if response.status_code == 429:
|
|
826
|
-
raise UsageLimitExceededError(detail)
|
|
827
|
-
elif response.status_code in [403,432,433]:
|
|
828
|
-
raise ForbiddenError(detail)
|
|
829
|
-
elif response.status_code == 401:
|
|
830
|
-
raise InvalidAPIKeyError(detail)
|
|
831
|
-
elif response.status_code == 400:
|
|
832
|
-
raise BadRequestError(detail)
|
|
833
|
-
else:
|
|
834
|
-
raise response.raise_for_status()
|
|
830
|
+
self._handle_error_response(response)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UsageLimitExceededError(Exception):
|
|
5
|
+
def __init__(self, message: str):
|
|
6
|
+
super().__init__(message)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TavilyKeylessLimitError(UsageLimitExceededError):
|
|
10
|
+
"""Raised when a keyless request is rejected by the Tavily API."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
code: Optional[str] = None,
|
|
16
|
+
window: Optional[str] = None,
|
|
17
|
+
retry_after_seconds: Optional[int] = None,
|
|
18
|
+
next_actions: Optional[List[Any]] = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.code = code
|
|
23
|
+
self.window = window
|
|
24
|
+
self.retry_after_seconds = retry_after_seconds
|
|
25
|
+
self.next_actions = next_actions or []
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.message or ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BadRequestError(Exception):
|
|
32
|
+
def __init__(self, message: str):
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ForbiddenError(Exception):
|
|
37
|
+
def __init__(self, message: str):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InvalidAPIKeyError(Exception):
|
|
42
|
+
def __init__(self, message: str):
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TimeoutError(Exception):
|
|
47
|
+
def __init__(self, timeout: float):
|
|
48
|
+
super().__init__(f"Request timed out after {timeout} seconds.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MissingAPIKeyError(Exception):
|
|
52
|
+
def __init__(self):
|
|
53
|
+
super().__init__(
|
|
54
|
+
"No API key provided. Please provide the api_key attribute or set the TAVILY_API_KEY environment variable."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KeylessUnsupportedEndpointError(Exception):
|
|
59
|
+
"""Raised when a method is called without an API key but does not support keyless mode.
|
|
60
|
+
|
|
61
|
+
Only ``search`` and ``extract`` support keyless mode. Calling other methods
|
|
62
|
+
keyless raises this error before any network request is sent.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, method: str):
|
|
66
|
+
super().__init__(
|
|
67
|
+
f"`{method}` is not available in keyless mode. "
|
|
68
|
+
"Only `search` and `extract` can be called without an API key. "
|
|
69
|
+
"Pass an `api_key` to TavilyClient (or set TAVILY_API_KEY) to use this method."
|
|
70
|
+
)
|
|
71
|
+
self.method = method
|
|
@@ -4,7 +4,38 @@ import os
|
|
|
4
4
|
import warnings
|
|
5
5
|
from typing import Literal, Sequence, Optional, List, Union, Generator
|
|
6
6
|
from .utils import get_max_items_from_list
|
|
7
|
-
from .errors import
|
|
7
|
+
from .errors import (
|
|
8
|
+
UsageLimitExceededError,
|
|
9
|
+
InvalidAPIKeyError,
|
|
10
|
+
MissingAPIKeyError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
TavilyKeylessLimitError,
|
|
15
|
+
KeylessUnsupportedEndpointError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_keyless_envelope(body) -> bool:
|
|
20
|
+
"""Return True when the response body matches the Tavily recoverable-error envelope shape."""
|
|
21
|
+
return (
|
|
22
|
+
isinstance(body, dict)
|
|
23
|
+
and isinstance(body.get("error"), dict)
|
|
24
|
+
and isinstance(body["error"].get("code"), str)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _raise_keyless_envelope(body) -> None:
|
|
29
|
+
"""Raise ``TavilyKeylessLimitError`` from an envelope-shaped response body."""
|
|
30
|
+
err = body["error"]
|
|
31
|
+
raise TavilyKeylessLimitError(
|
|
32
|
+
message=err.get("message") or "",
|
|
33
|
+
code=err.get("code"),
|
|
34
|
+
window=err.get("window"),
|
|
35
|
+
retry_after_seconds=err.get("retry_after_seconds"),
|
|
36
|
+
next_actions=err.get("next_actions") or [],
|
|
37
|
+
)
|
|
38
|
+
|
|
8
39
|
|
|
9
40
|
class TavilyClient:
|
|
10
41
|
"""
|
|
@@ -26,8 +57,7 @@ class TavilyClient:
|
|
|
26
57
|
if api_key is None:
|
|
27
58
|
api_key = os.getenv("TAVILY_API_KEY")
|
|
28
59
|
|
|
29
|
-
|
|
30
|
-
raise MissingAPIKeyError()
|
|
60
|
+
api_key = api_key or None
|
|
31
61
|
|
|
32
62
|
resolved_proxies = {
|
|
33
63
|
"http": proxies.get("http") if proxies else os.getenv("TAVILY_HTTP_PROXY"),
|
|
@@ -39,12 +69,19 @@ class TavilyClient:
|
|
|
39
69
|
|
|
40
70
|
self.base_url = api_base_url or "https://api.tavily.com"
|
|
41
71
|
self.api_key = api_key
|
|
72
|
+
self._keyless = api_key is None and session is None
|
|
42
73
|
self.proxies = resolved_proxies
|
|
43
74
|
|
|
75
|
+
if self._keyless:
|
|
76
|
+
client_source_header = client_source or "tavily-python-keyless"
|
|
77
|
+
else:
|
|
78
|
+
client_source_header = client_source or "tavily-python"
|
|
79
|
+
|
|
44
80
|
self.headers = {
|
|
45
81
|
"Content-Type": "application/json",
|
|
46
82
|
**({"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}),
|
|
47
|
-
"X-
|
|
83
|
+
**({"X-Tavily-Access-Mode": "keyless"} if self._keyless else {}),
|
|
84
|
+
"X-Client-Source": client_source_header,
|
|
48
85
|
**({"X-Project-ID": tavily_project} if tavily_project else {}),
|
|
49
86
|
**({"X-Session-Id": session_id} if session_id else {}),
|
|
50
87
|
**({"X-Human-Id": human_id} if human_id else {}),
|
|
@@ -73,6 +110,40 @@ class TavilyClient:
|
|
|
73
110
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
74
111
|
self.close()
|
|
75
112
|
|
|
113
|
+
def _handle_error_response(self, response) -> None:
|
|
114
|
+
"""Raise an appropriate exception for a non-2xx response."""
|
|
115
|
+
body = None
|
|
116
|
+
try:
|
|
117
|
+
body = response.json()
|
|
118
|
+
except Exception:
|
|
119
|
+
body = None
|
|
120
|
+
|
|
121
|
+
if self._keyless and _is_keyless_envelope(body):
|
|
122
|
+
_raise_keyless_envelope(body)
|
|
123
|
+
|
|
124
|
+
detail = ""
|
|
125
|
+
if isinstance(body, dict):
|
|
126
|
+
try:
|
|
127
|
+
detail = body.get("detail", {}).get("error", None) or ""
|
|
128
|
+
except Exception:
|
|
129
|
+
detail = ""
|
|
130
|
+
|
|
131
|
+
if response.status_code == 429:
|
|
132
|
+
raise UsageLimitExceededError(detail)
|
|
133
|
+
elif response.status_code in [403, 432, 433]:
|
|
134
|
+
raise ForbiddenError(detail)
|
|
135
|
+
elif response.status_code == 401:
|
|
136
|
+
raise InvalidAPIKeyError(detail)
|
|
137
|
+
elif response.status_code == 400:
|
|
138
|
+
raise BadRequestError(detail)
|
|
139
|
+
else:
|
|
140
|
+
raise response.raise_for_status()
|
|
141
|
+
|
|
142
|
+
def _check_keyless_supported(self, method: str) -> None:
|
|
143
|
+
"""Raise ``KeylessUnsupportedEndpointError`` when running in keyless mode."""
|
|
144
|
+
if self._keyless:
|
|
145
|
+
raise KeylessUnsupportedEndpointError(method)
|
|
146
|
+
|
|
76
147
|
@staticmethod
|
|
77
148
|
def _pop_request_headers(kwargs: dict) -> Optional[dict]:
|
|
78
149
|
"""Pop session_id, human_id, and client_name from kwargs and return them as headers.
|
|
@@ -155,22 +226,7 @@ class TavilyClient:
|
|
|
155
226
|
if response.status_code == 200:
|
|
156
227
|
return response.json()
|
|
157
228
|
else:
|
|
158
|
-
|
|
159
|
-
try:
|
|
160
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
161
|
-
except Exception:
|
|
162
|
-
pass
|
|
163
|
-
|
|
164
|
-
if response.status_code == 429:
|
|
165
|
-
raise UsageLimitExceededError(detail)
|
|
166
|
-
elif response.status_code in [403, 432, 433]:
|
|
167
|
-
raise ForbiddenError(detail)
|
|
168
|
-
elif response.status_code == 401:
|
|
169
|
-
raise InvalidAPIKeyError(detail)
|
|
170
|
-
elif response.status_code == 400:
|
|
171
|
-
raise BadRequestError(detail)
|
|
172
|
-
else:
|
|
173
|
-
raise response.raise_for_status()
|
|
229
|
+
self._handle_error_response(response)
|
|
174
230
|
|
|
175
231
|
|
|
176
232
|
def search(self,
|
|
@@ -263,22 +319,7 @@ class TavilyClient:
|
|
|
263
319
|
if response.status_code == 200:
|
|
264
320
|
return response.json()
|
|
265
321
|
else:
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
269
|
-
except Exception:
|
|
270
|
-
pass
|
|
271
|
-
|
|
272
|
-
if response.status_code == 429:
|
|
273
|
-
raise UsageLimitExceededError(detail)
|
|
274
|
-
elif response.status_code in [403, 432, 433]:
|
|
275
|
-
raise ForbiddenError(detail)
|
|
276
|
-
elif response.status_code == 401:
|
|
277
|
-
raise InvalidAPIKeyError(detail)
|
|
278
|
-
elif response.status_code == 400:
|
|
279
|
-
raise BadRequestError(detail)
|
|
280
|
-
else:
|
|
281
|
-
raise response.raise_for_status()
|
|
322
|
+
self._handle_error_response(response)
|
|
282
323
|
|
|
283
324
|
def extract(self,
|
|
284
325
|
urls: Union[List[str], str], # Accept a list of URLs or a single URL
|
|
@@ -367,22 +408,7 @@ class TavilyClient:
|
|
|
367
408
|
if response.status_code == 200:
|
|
368
409
|
return response.json()
|
|
369
410
|
else:
|
|
370
|
-
|
|
371
|
-
try:
|
|
372
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
373
|
-
except Exception:
|
|
374
|
-
pass
|
|
375
|
-
|
|
376
|
-
if response.status_code == 429:
|
|
377
|
-
raise UsageLimitExceededError(detail)
|
|
378
|
-
elif response.status_code in [403, 432, 433]:
|
|
379
|
-
raise ForbiddenError(detail)
|
|
380
|
-
elif response.status_code == 401:
|
|
381
|
-
raise InvalidAPIKeyError(detail)
|
|
382
|
-
elif response.status_code == 400:
|
|
383
|
-
raise BadRequestError(detail)
|
|
384
|
-
else:
|
|
385
|
-
raise response.raise_for_status()
|
|
411
|
+
self._handle_error_response(response)
|
|
386
412
|
|
|
387
413
|
def crawl(self,
|
|
388
414
|
url: str,
|
|
@@ -408,6 +434,7 @@ class TavilyClient:
|
|
|
408
434
|
Combined crawl method.
|
|
409
435
|
include_favicon: If True, include the favicon in the crawl results.
|
|
410
436
|
"""
|
|
437
|
+
self._check_keyless_supported("crawl")
|
|
411
438
|
return self._crawl(url,
|
|
412
439
|
max_depth=max_depth,
|
|
413
440
|
max_breadth=max_breadth,
|
|
@@ -476,22 +503,7 @@ class TavilyClient:
|
|
|
476
503
|
if response.status_code == 200:
|
|
477
504
|
return response.json()
|
|
478
505
|
else:
|
|
479
|
-
|
|
480
|
-
try:
|
|
481
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
482
|
-
except Exception:
|
|
483
|
-
pass
|
|
484
|
-
|
|
485
|
-
if response.status_code == 429:
|
|
486
|
-
raise UsageLimitExceededError(detail)
|
|
487
|
-
elif response.status_code in [403, 432, 433]:
|
|
488
|
-
raise ForbiddenError(detail)
|
|
489
|
-
elif response.status_code == 401:
|
|
490
|
-
raise InvalidAPIKeyError(detail)
|
|
491
|
-
elif response.status_code == 400:
|
|
492
|
-
raise BadRequestError(detail)
|
|
493
|
-
else:
|
|
494
|
-
raise response.raise_for_status()
|
|
506
|
+
self._handle_error_response(response)
|
|
495
507
|
|
|
496
508
|
def map(self,
|
|
497
509
|
url: str,
|
|
@@ -511,8 +523,9 @@ class TavilyClient:
|
|
|
511
523
|
) -> dict:
|
|
512
524
|
"""
|
|
513
525
|
Combined map method.
|
|
514
|
-
|
|
526
|
+
|
|
515
527
|
"""
|
|
528
|
+
self._check_keyless_supported("map")
|
|
516
529
|
return self._map(url,
|
|
517
530
|
max_depth=max_depth,
|
|
518
531
|
max_breadth=max_breadth,
|
|
@@ -550,6 +563,7 @@ class TavilyClient:
|
|
|
550
563
|
|
|
551
564
|
Returns a string of JSON containing the search context up to context limit.
|
|
552
565
|
"""
|
|
566
|
+
self._check_keyless_supported("get_search_context")
|
|
553
567
|
warnings.warn("get_search_context is deprecated and will be removed in future versions.",
|
|
554
568
|
DeprecationWarning, stacklevel=2)
|
|
555
569
|
|
|
@@ -589,6 +603,7 @@ class TavilyClient:
|
|
|
589
603
|
"""
|
|
590
604
|
Q&A search method. Search depth is advanced by default to get the best answer.
|
|
591
605
|
"""
|
|
606
|
+
self._check_keyless_supported("qna_search")
|
|
592
607
|
warnings.warn("qna_search is deprecated and will be removed in future versions.",
|
|
593
608
|
DeprecationWarning, stacklevel=2)
|
|
594
609
|
response_dict = self._search(query,
|
|
@@ -647,22 +662,7 @@ class TavilyClient:
|
|
|
647
662
|
raise TimeoutError(timeout)
|
|
648
663
|
|
|
649
664
|
if response.status_code != 200:
|
|
650
|
-
|
|
651
|
-
try:
|
|
652
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
653
|
-
except Exception:
|
|
654
|
-
pass
|
|
655
|
-
|
|
656
|
-
if response.status_code == 429:
|
|
657
|
-
raise UsageLimitExceededError(detail)
|
|
658
|
-
elif response.status_code in [403, 432, 433]:
|
|
659
|
-
raise ForbiddenError(detail)
|
|
660
|
-
elif response.status_code == 401:
|
|
661
|
-
raise InvalidAPIKeyError(detail)
|
|
662
|
-
elif response.status_code == 400:
|
|
663
|
-
raise BadRequestError(detail)
|
|
664
|
-
else:
|
|
665
|
-
raise response.raise_for_status()
|
|
665
|
+
self._handle_error_response(response)
|
|
666
666
|
|
|
667
667
|
def stream_generator() -> Generator[bytes, None, None]:
|
|
668
668
|
try:
|
|
@@ -687,22 +687,7 @@ class TavilyClient:
|
|
|
687
687
|
if response.status_code == 200:
|
|
688
688
|
return response.json()
|
|
689
689
|
else:
|
|
690
|
-
|
|
691
|
-
try:
|
|
692
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
693
|
-
except Exception:
|
|
694
|
-
pass
|
|
695
|
-
|
|
696
|
-
if response.status_code == 429:
|
|
697
|
-
raise UsageLimitExceededError(detail)
|
|
698
|
-
elif response.status_code in [403, 432, 433]:
|
|
699
|
-
raise ForbiddenError(detail)
|
|
700
|
-
elif response.status_code == 401:
|
|
701
|
-
raise InvalidAPIKeyError(detail)
|
|
702
|
-
elif response.status_code == 400:
|
|
703
|
-
raise BadRequestError(detail)
|
|
704
|
-
else:
|
|
705
|
-
raise response.raise_for_status()
|
|
690
|
+
self._handle_error_response(response)
|
|
706
691
|
|
|
707
692
|
def research(self,
|
|
708
693
|
input: str,
|
|
@@ -728,6 +713,7 @@ class TavilyClient:
|
|
|
728
713
|
Returns:
|
|
729
714
|
dict: Response containing request_id, created_at, status, input, and model.
|
|
730
715
|
"""
|
|
716
|
+
self._check_keyless_supported("research")
|
|
731
717
|
|
|
732
718
|
return self._research(
|
|
733
719
|
input=input,
|
|
@@ -751,6 +737,7 @@ class TavilyClient:
|
|
|
751
737
|
Returns:
|
|
752
738
|
dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
|
|
753
739
|
"""
|
|
740
|
+
self._check_keyless_supported("get_research")
|
|
754
741
|
try:
|
|
755
742
|
response = self.session.get(self.base_url + f"/research/{request_id}")
|
|
756
743
|
except Exception as e:
|
|
@@ -759,22 +746,7 @@ class TavilyClient:
|
|
|
759
746
|
if response.status_code in (200, 202):
|
|
760
747
|
return response.json()
|
|
761
748
|
else:
|
|
762
|
-
|
|
763
|
-
try:
|
|
764
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
765
|
-
except Exception:
|
|
766
|
-
pass
|
|
767
|
-
|
|
768
|
-
if response.status_code == 429:
|
|
769
|
-
raise UsageLimitExceededError(detail)
|
|
770
|
-
elif response.status_code in [403, 432, 433]:
|
|
771
|
-
raise ForbiddenError(detail)
|
|
772
|
-
elif response.status_code == 401:
|
|
773
|
-
raise InvalidAPIKeyError(detail)
|
|
774
|
-
elif response.status_code == 400:
|
|
775
|
-
raise BadRequestError(detail)
|
|
776
|
-
else:
|
|
777
|
-
raise response.raise_for_status()
|
|
749
|
+
self._handle_error_response(response)
|
|
778
750
|
|
|
779
751
|
|
|
780
752
|
class Client(TavilyClient):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tavily-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.25
|
|
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
|
|
@@ -40,6 +40,29 @@ The Tavily Python wrapper allows for easy interaction with the Tavily API, offer
|
|
|
40
40
|
pip install tavily-python
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Keyless mode
|
|
44
|
+
|
|
45
|
+
You can try Tavily without an API key. Instantiate `TavilyClient()` with no arguments and the SDK runs in keyless mode against the public Tavily API. Keyless mode supports `search()` and `extract()` only; other methods raise an error explaining that an API key is required.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tavily import TavilyClient, TavilyKeylessLimitError
|
|
49
|
+
|
|
50
|
+
# No API key needed
|
|
51
|
+
client = TavilyClient()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = client.search("Who is Leo Messi?")
|
|
55
|
+
print(response)
|
|
56
|
+
except TavilyKeylessLimitError as e:
|
|
57
|
+
# Rate-limit cap reached. The exception carries the human-readable
|
|
58
|
+
# message plus structured fields (code, window, retry_after_seconds,
|
|
59
|
+
# next_actions) returned by the Tavily API.
|
|
60
|
+
print(e)
|
|
61
|
+
print("retry after:", e.retry_after_seconds, "seconds")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Keyless usage is rate-limited. For higher limits and the full set of endpoints (including `crawl`, `map`, and `research`), [sign up for a Tavily API key](https://tavily.com) and pass it as `TavilyClient(api_key="tvly-...")`.
|
|
65
|
+
|
|
43
66
|
# Tavily Search
|
|
44
67
|
|
|
45
68
|
Search lets you search the web for a given query.
|
|
@@ -4,7 +4,6 @@ from tests.request_intercept import intercept_requests, clear_interceptor, MockS
|
|
|
4
4
|
import tavily.tavily as sync_tavily
|
|
5
5
|
import tavily.async_tavily as async_tavily
|
|
6
6
|
import pytest
|
|
7
|
-
from tavily.errors import MissingAPIKeyError
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
@pytest.fixture
|
|
@@ -104,10 +103,14 @@ class TestSyncCustomSession:
|
|
|
104
103
|
|
|
105
104
|
# --- API key validation edge cases ---
|
|
106
105
|
|
|
107
|
-
def
|
|
106
|
+
def test_no_api_key_no_session_creates_keyless_client(self, monkeypatch):
|
|
107
|
+
"""No api_key + no custom session -> keyless mode (search/extract only)."""
|
|
108
108
|
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
client = sync_tavily.TavilyClient()
|
|
110
|
+
assert client._keyless is True
|
|
111
|
+
assert "Authorization" not in client.headers
|
|
112
|
+
assert client.headers.get("X-Tavily-Access-Mode") == "keyless"
|
|
113
|
+
assert client.headers.get("X-Client-Source") == "tavily-python-keyless"
|
|
111
114
|
|
|
112
115
|
def test_no_api_key_with_session_allowed(self, sync_interceptor, monkeypatch):
|
|
113
116
|
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
@@ -140,10 +143,13 @@ class TestSyncCustomSession:
|
|
|
140
143
|
assert req is not None
|
|
141
144
|
assert req.headers["Authorization"] == "Bearer apim-token"
|
|
142
145
|
|
|
143
|
-
def
|
|
146
|
+
def test_empty_string_api_key_no_session_creates_keyless_client(self, monkeypatch):
|
|
147
|
+
"""Empty-string api_key + no custom session -> keyless mode."""
|
|
144
148
|
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
client = sync_tavily.TavilyClient(api_key="")
|
|
150
|
+
assert client._keyless is True
|
|
151
|
+
assert "Authorization" not in client.headers
|
|
152
|
+
assert client.headers.get("X-Tavily-Access-Mode") == "keyless"
|
|
147
153
|
|
|
148
154
|
def test_empty_string_api_key_with_session_allowed(self, sync_interceptor):
|
|
149
155
|
custom_session = MockSession(sync_interceptor)
|
|
@@ -336,10 +342,11 @@ class TestAsyncCustomClient:
|
|
|
336
342
|
|
|
337
343
|
# --- API key validation edge cases ---
|
|
338
344
|
|
|
339
|
-
def
|
|
345
|
+
def test_no_api_key_no_client_creates_keyless_client(self, monkeypatch):
|
|
346
|
+
"""Async: no api_key + no custom client -> keyless mode."""
|
|
340
347
|
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
client = async_tavily.AsyncTavilyClient()
|
|
349
|
+
assert client._keyless is True
|
|
343
350
|
|
|
344
351
|
def test_no_api_key_with_client_allowed(self, monkeypatch):
|
|
345
352
|
monkeypatch.delenv("TAVILY_API_KEY", raising=False)
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
|
|
5
5
|
import tavily.tavily as sync_tavily
|
|
6
6
|
import tavily.async_tavily as async_tavily
|
|
7
|
-
from tavily.errors import
|
|
7
|
+
from tavily.errors import InvalidAPIKeyError
|
|
8
8
|
|
|
9
9
|
@pytest.fixture
|
|
10
10
|
def set_api_key():
|
|
@@ -31,18 +31,17 @@ def test_load_key_from_env(set_api_key):
|
|
|
31
31
|
|
|
32
32
|
# No error should be raised
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async_tavily.AsyncTavilyClient()
|
|
34
|
+
def test_no_api_key_creates_keyless_client(clear_api_key):
|
|
35
|
+
"""With no API key (None or empty string) the client constructs in keyless mode."""
|
|
36
|
+
for ctor_args in [{"api_key": ""}, {}, {"api_key": None}]:
|
|
37
|
+
sync_client = sync_tavily.TavilyClient(**ctor_args)
|
|
38
|
+
assert sync_client._keyless is True
|
|
39
|
+
assert "Authorization" not in sync_client.headers
|
|
40
|
+
assert sync_client.headers.get("X-Tavily-Access-Mode") == "keyless"
|
|
41
|
+
assert sync_client.headers.get("X-Client-Source") == "tavily-python-keyless"
|
|
42
|
+
|
|
43
|
+
async_client = async_tavily.AsyncTavilyClient(**ctor_args)
|
|
44
|
+
assert async_client._keyless is True
|
|
46
45
|
|
|
47
46
|
def test_invalid_api_key():
|
|
48
47
|
with pytest.raises(InvalidAPIKeyError):
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
class UsageLimitExceededError(Exception):
|
|
2
|
-
def __init__(self, message: str):
|
|
3
|
-
super().__init__(message)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BadRequestError(Exception):
|
|
7
|
-
def __init__(self, message: str):
|
|
8
|
-
super().__init__(message)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ForbiddenError(Exception):
|
|
12
|
-
def __init__(self, message: str):
|
|
13
|
-
super().__init__(message)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class InvalidAPIKeyError(Exception):
|
|
17
|
-
def __init__(self, message: str):
|
|
18
|
-
super().__init__(message)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class TimeoutError(Exception):
|
|
22
|
-
def __init__(self, timeout: float):
|
|
23
|
-
super().__init__(f"Request timed out after {timeout} seconds.")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class MissingAPIKeyError(Exception):
|
|
27
|
-
def __init__(self):
|
|
28
|
-
super().__init__(
|
|
29
|
-
"No API key provided. Please provide the api_key attribute or set the TAVILY_API_KEY environment variable."
|
|
30
|
-
)
|
|
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
|