tavily-python 0.7.23__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.23 → tavily_python-0.7.25}/PKG-INFO +47 -1
- {tavily_python-0.7.23 → tavily_python-0.7.25}/README.md +46 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/setup.py +1 -1
- tavily_python-0.7.25/tavily/__init__.py +11 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/async_tavily.py +143 -118
- tavily_python-0.7.25/tavily/errors.py +71 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/tavily.py +139 -129
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily_python.egg-info/PKG-INFO +47 -1
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_custom_session.py +17 -10
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_errors.py +12 -13
- tavily_python-0.7.23/tavily/__init__.py +0 -4
- tavily_python-0.7.23/tavily/errors.py +0 -30
- {tavily_python-0.7.23 → tavily_python-0.7.25}/LICENSE +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/setup.cfg +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/config.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/hybrid_rag/__init__.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/hybrid_rag/hybrid_rag.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily/utils.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily_python.egg-info/SOURCES.txt +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily_python.egg-info/dependency_links.txt +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily_python.egg-info/requires.txt +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tavily_python.egg-info/top_level.txt +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_crawl.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_map.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_research.py +0 -0
- {tavily_python-0.7.23 → tavily_python-0.7.25}/tests/test_search.py +0 -0
- {tavily_python-0.7.23 → 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.
|
|
@@ -320,6 +343,29 @@ response = await client.search("latest AI research")
|
|
|
320
343
|
- Custom session proxies take precedence over SDK proxy settings
|
|
321
344
|
- The SDK will **not** close externally-provided sessions — you manage the lifecycle
|
|
322
345
|
|
|
346
|
+
## Session & User Tracking
|
|
347
|
+
|
|
348
|
+
`session_id`, `human_id`, and `client_name` are optional identifiers that help attribute requests to a logical session, an end user, and a named client. All three are sent as HTTP headers (`X-Session-Id`, `X-Human-Id`, `X-Client-Name`) and are never persisted in raw form — `human_id` is hashed server-side.
|
|
349
|
+
|
|
350
|
+
Set them once at client init, or per-call (per-call wins):
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from tavily import TavilyClient
|
|
354
|
+
|
|
355
|
+
# Client-level — applied to every request
|
|
356
|
+
client = TavilyClient(
|
|
357
|
+
api_key="tvly-YOUR_API_KEY",
|
|
358
|
+
session_id="my-session-123",
|
|
359
|
+
human_id="internal-user-id-42",
|
|
360
|
+
client_name="my-app",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Per-call override
|
|
364
|
+
client.search("hello", session_id="ad-hoc-session")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
All three are opt-in. Leave them unset and the SDK sends nothing — behavior is identical to earlier versions.
|
|
368
|
+
|
|
323
369
|
## Documentation
|
|
324
370
|
|
|
325
371
|
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).
|
|
@@ -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.
|
|
@@ -293,6 +316,29 @@ response = await client.search("latest AI research")
|
|
|
293
316
|
- Custom session proxies take precedence over SDK proxy settings
|
|
294
317
|
- The SDK will **not** close externally-provided sessions — you manage the lifecycle
|
|
295
318
|
|
|
319
|
+
## Session & User Tracking
|
|
320
|
+
|
|
321
|
+
`session_id`, `human_id`, and `client_name` are optional identifiers that help attribute requests to a logical session, an end user, and a named client. All three are sent as HTTP headers (`X-Session-Id`, `X-Human-Id`, `X-Client-Name`) and are never persisted in raw form — `human_id` is hashed server-side.
|
|
322
|
+
|
|
323
|
+
Set them once at client init, or per-call (per-call wins):
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
from tavily import TavilyClient
|
|
327
|
+
|
|
328
|
+
# Client-level — applied to every request
|
|
329
|
+
client = TavilyClient(
|
|
330
|
+
api_key="tvly-YOUR_API_KEY",
|
|
331
|
+
session_id="my-session-123",
|
|
332
|
+
human_id="internal-user-id-42",
|
|
333
|
+
client_name="my-app",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Per-call override
|
|
337
|
+
client.search("hello", session_id="ad-hoc-session")
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
All three are opt-in. Leave them unset and the SDK sends nothing — behavior is identical to earlier versions.
|
|
341
|
+
|
|
296
342
|
## Documentation
|
|
297
343
|
|
|
298
344
|
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,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:
|
|
@@ -20,23 +50,39 @@ class AsyncTavilyClient:
|
|
|
20
50
|
api_base_url: Optional[str] = None,
|
|
21
51
|
client_source: Optional[str] = None,
|
|
22
52
|
project_id: Optional[str] = None,
|
|
53
|
+
session_id: Optional[str] = None,
|
|
54
|
+
human_id: Optional[str] = None,
|
|
55
|
+
client_name: Optional[str] = None,
|
|
23
56
|
client: Optional[httpx.AsyncClient] = None):
|
|
24
57
|
if api_key is None:
|
|
25
58
|
api_key = os.getenv("TAVILY_API_KEY")
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
29
63
|
|
|
30
64
|
tavily_project = project_id or os.getenv("TAVILY_PROJECT")
|
|
31
65
|
|
|
32
66
|
self._api_base_url = api_base_url or "https://api.tavily.com"
|
|
33
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"
|
|
34
76
|
|
|
35
77
|
default_headers = {
|
|
36
78
|
"Content-Type": "application/json",
|
|
37
79
|
**({"Authorization": f"Bearer {api_key}"} if api_key else {}),
|
|
38
|
-
"X-
|
|
39
|
-
|
|
80
|
+
**({"X-Tavily-Access-Mode": "keyless"} if self._keyless else {}),
|
|
81
|
+
"X-Client-Source": client_source_header,
|
|
82
|
+
**({"X-Project-ID": tavily_project} if tavily_project else {}),
|
|
83
|
+
**({"X-Session-Id": session_id} if session_id else {}),
|
|
84
|
+
**({"X-Human-Id": human_id} if human_id else {}),
|
|
85
|
+
**({"X-Client-Name": client_name} if client_name else {}),
|
|
40
86
|
}
|
|
41
87
|
|
|
42
88
|
self._external_client = client is not None
|
|
@@ -83,6 +129,66 @@ class AsyncTavilyClient:
|
|
|
83
129
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
84
130
|
await self.close()
|
|
85
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
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _pop_request_headers(kwargs: dict) -> Optional[dict]:
|
|
177
|
+
"""Pop session_id, human_id, and client_name from kwargs and return them as headers.
|
|
178
|
+
|
|
179
|
+
Returns None when no overrides are provided so callers can omit the headers kwarg.
|
|
180
|
+
"""
|
|
181
|
+
overrides = {}
|
|
182
|
+
for key, header_name in (
|
|
183
|
+
("session_id", "X-Session-Id"),
|
|
184
|
+
("human_id", "X-Human-Id"),
|
|
185
|
+
("client_name", "X-Client-Name"),
|
|
186
|
+
):
|
|
187
|
+
value = kwargs.pop(key, None)
|
|
188
|
+
if value is not None:
|
|
189
|
+
overrides[header_name] = str(value)
|
|
190
|
+
return overrides or None
|
|
191
|
+
|
|
86
192
|
async def _search(
|
|
87
193
|
self,
|
|
88
194
|
query: str,
|
|
@@ -132,35 +238,21 @@ class AsyncTavilyClient:
|
|
|
132
238
|
|
|
133
239
|
data = {k: v for k, v in data.items() if v is not None}
|
|
134
240
|
|
|
241
|
+
override_headers = self._pop_request_headers(kwargs)
|
|
135
242
|
if kwargs:
|
|
136
243
|
data.update(kwargs)
|
|
137
244
|
|
|
138
245
|
timeout = min(timeout, 120)
|
|
139
246
|
|
|
140
247
|
try:
|
|
141
|
-
response = await self._client.post("/search", content=json.dumps(data), timeout=timeout)
|
|
248
|
+
response = await self._client.post("/search", content=json.dumps(data), timeout=timeout, **({"headers": override_headers} if override_headers else {}))
|
|
142
249
|
except httpx.TimeoutException:
|
|
143
250
|
raise TimeoutError(timeout)
|
|
144
251
|
|
|
145
252
|
if response.status_code == 200:
|
|
146
253
|
return response.json()
|
|
147
254
|
else:
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
151
|
-
except Exception:
|
|
152
|
-
pass
|
|
153
|
-
|
|
154
|
-
if response.status_code == 429:
|
|
155
|
-
raise UsageLimitExceededError(detail)
|
|
156
|
-
elif response.status_code in [403,432,433]:
|
|
157
|
-
raise ForbiddenError(detail)
|
|
158
|
-
elif response.status_code == 401:
|
|
159
|
-
raise InvalidAPIKeyError(detail)
|
|
160
|
-
elif response.status_code == 400:
|
|
161
|
-
raise BadRequestError(detail)
|
|
162
|
-
else:
|
|
163
|
-
raise response.raise_for_status()
|
|
255
|
+
self._handle_error_response(response)
|
|
164
256
|
|
|
165
257
|
async def search(self,
|
|
166
258
|
query: str,
|
|
@@ -247,34 +339,19 @@ class AsyncTavilyClient:
|
|
|
247
339
|
|
|
248
340
|
data = {k: v for k, v in data.items() if v is not None}
|
|
249
341
|
|
|
342
|
+
override_headers = self._pop_request_headers(kwargs)
|
|
250
343
|
if kwargs:
|
|
251
344
|
data.update(kwargs)
|
|
252
345
|
|
|
253
346
|
try:
|
|
254
|
-
response = await self._client.post("/extract", content=json.dumps(data), timeout=timeout)
|
|
347
|
+
response = await self._client.post("/extract", content=json.dumps(data), timeout=timeout, **({"headers": override_headers} if override_headers else {}))
|
|
255
348
|
except httpx.TimeoutException:
|
|
256
349
|
raise TimeoutError(timeout)
|
|
257
350
|
|
|
258
351
|
if response.status_code == 200:
|
|
259
352
|
return response.json()
|
|
260
353
|
else:
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
264
|
-
except Exception:
|
|
265
|
-
pass
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if response.status_code == 429:
|
|
269
|
-
raise UsageLimitExceededError(detail)
|
|
270
|
-
elif response.status_code in [403,432,433]:
|
|
271
|
-
raise ForbiddenError(detail)
|
|
272
|
-
elif response.status_code == 401:
|
|
273
|
-
raise InvalidAPIKeyError(detail)
|
|
274
|
-
elif response.status_code == 400:
|
|
275
|
-
raise BadRequestError(detail)
|
|
276
|
-
else:
|
|
277
|
-
raise response.raise_for_status()
|
|
354
|
+
self._handle_error_response(response)
|
|
278
355
|
|
|
279
356
|
async def extract(self,
|
|
280
357
|
urls: Union[List[str], str], # Accept a list of URLs or a single URL
|
|
@@ -355,35 +432,21 @@ class AsyncTavilyClient:
|
|
|
355
432
|
"chunks_per_source": chunks_per_source,
|
|
356
433
|
}
|
|
357
434
|
|
|
435
|
+
override_headers = self._pop_request_headers(kwargs)
|
|
358
436
|
if kwargs:
|
|
359
437
|
data.update(kwargs)
|
|
360
438
|
|
|
361
439
|
data = {k: v for k, v in data.items() if v is not None}
|
|
362
440
|
|
|
363
441
|
try:
|
|
364
|
-
response = await self._client.post("/crawl", content=json.dumps(data), timeout=timeout)
|
|
442
|
+
response = await self._client.post("/crawl", content=json.dumps(data), timeout=timeout, **({"headers": override_headers} if override_headers else {}))
|
|
365
443
|
except httpx.TimeoutException:
|
|
366
444
|
raise TimeoutError(timeout)
|
|
367
445
|
|
|
368
446
|
if response.status_code == 200:
|
|
369
447
|
return response.json()
|
|
370
448
|
else:
|
|
371
|
-
|
|
372
|
-
try:
|
|
373
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
374
|
-
except Exception:
|
|
375
|
-
pass
|
|
376
|
-
|
|
377
|
-
if response.status_code == 429:
|
|
378
|
-
raise UsageLimitExceededError(detail)
|
|
379
|
-
elif response.status_code in [403,432,433]:
|
|
380
|
-
raise ForbiddenError(detail)
|
|
381
|
-
elif response.status_code == 401:
|
|
382
|
-
raise InvalidAPIKeyError(detail)
|
|
383
|
-
elif response.status_code == 400:
|
|
384
|
-
raise BadRequestError(detail)
|
|
385
|
-
else:
|
|
386
|
-
raise response.raise_for_status()
|
|
449
|
+
self._handle_error_response(response)
|
|
387
450
|
|
|
388
451
|
async def crawl(self,
|
|
389
452
|
url: str,
|
|
@@ -409,6 +472,7 @@ class AsyncTavilyClient:
|
|
|
409
472
|
Combined crawl method.
|
|
410
473
|
|
|
411
474
|
"""
|
|
475
|
+
self._check_keyless_supported("crawl")
|
|
412
476
|
response_dict = await self._crawl(url,
|
|
413
477
|
max_depth=max_depth,
|
|
414
478
|
max_breadth=max_breadth,
|
|
@@ -465,35 +529,21 @@ class AsyncTavilyClient:
|
|
|
465
529
|
"include_usage": include_usage,
|
|
466
530
|
}
|
|
467
531
|
|
|
532
|
+
override_headers = self._pop_request_headers(kwargs)
|
|
468
533
|
if kwargs:
|
|
469
534
|
data.update(kwargs)
|
|
470
535
|
|
|
471
536
|
data = {k: v for k, v in data.items() if v is not None}
|
|
472
537
|
|
|
473
538
|
try:
|
|
474
|
-
response = await self._client.post("/map", content=json.dumps(data), timeout=timeout)
|
|
539
|
+
response = await self._client.post("/map", content=json.dumps(data), timeout=timeout, **({"headers": override_headers} if override_headers else {}))
|
|
475
540
|
except httpx.TimeoutException:
|
|
476
541
|
raise TimeoutError(timeout)
|
|
477
542
|
|
|
478
543
|
if response.status_code == 200:
|
|
479
544
|
return response.json()
|
|
480
545
|
else:
|
|
481
|
-
|
|
482
|
-
try:
|
|
483
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
484
|
-
except Exception:
|
|
485
|
-
pass
|
|
486
|
-
|
|
487
|
-
if response.status_code == 429:
|
|
488
|
-
raise UsageLimitExceededError(detail)
|
|
489
|
-
elif response.status_code in [403,432,433]:
|
|
490
|
-
raise ForbiddenError(detail)
|
|
491
|
-
elif response.status_code == 401:
|
|
492
|
-
raise InvalidAPIKeyError(detail)
|
|
493
|
-
elif response.status_code == 400:
|
|
494
|
-
raise BadRequestError(detail)
|
|
495
|
-
else:
|
|
496
|
-
raise response.raise_for_status()
|
|
546
|
+
self._handle_error_response(response)
|
|
497
547
|
|
|
498
548
|
async def map(self,
|
|
499
549
|
url: str,
|
|
@@ -515,6 +565,7 @@ class AsyncTavilyClient:
|
|
|
515
565
|
Combined map method.
|
|
516
566
|
|
|
517
567
|
"""
|
|
568
|
+
self._check_keyless_supported("map")
|
|
518
569
|
response_dict = await self._map(url,
|
|
519
570
|
max_depth=max_depth,
|
|
520
571
|
max_breadth=max_breadth,
|
|
@@ -554,6 +605,7 @@ class AsyncTavilyClient:
|
|
|
554
605
|
|
|
555
606
|
Returns a string of JSON containing the search context up to context limit.
|
|
556
607
|
"""
|
|
608
|
+
self._check_keyless_supported("get_search_context")
|
|
557
609
|
timeout = min(timeout, 120)
|
|
558
610
|
response_dict = await self._search(query,
|
|
559
611
|
search_depth=search_depth,
|
|
@@ -590,6 +642,7 @@ class AsyncTavilyClient:
|
|
|
590
642
|
"""
|
|
591
643
|
Q&A search method. Search depth is advanced by default to get the best answer.
|
|
592
644
|
"""
|
|
645
|
+
self._check_keyless_supported("qna_search")
|
|
593
646
|
timeout = min(timeout, 120)
|
|
594
647
|
response_dict = await self._search(query,
|
|
595
648
|
search_depth=search_depth,
|
|
@@ -616,6 +669,7 @@ class AsyncTavilyClient:
|
|
|
616
669
|
country: str = None,
|
|
617
670
|
) -> Sequence[dict]:
|
|
618
671
|
""" Company information search method. Search depth is advanced by default to get the best answer. """
|
|
672
|
+
self._check_keyless_supported("get_company_info")
|
|
619
673
|
timeout = min(timeout, 120)
|
|
620
674
|
|
|
621
675
|
async def _perform_search(topic: str):
|
|
@@ -659,6 +713,7 @@ class AsyncTavilyClient:
|
|
|
659
713
|
|
|
660
714
|
data = {k: v for k, v in data.items() if v is not None}
|
|
661
715
|
|
|
716
|
+
override_headers = self._pop_request_headers(kwargs)
|
|
662
717
|
if kwargs:
|
|
663
718
|
data.update(kwargs)
|
|
664
719
|
|
|
@@ -669,7 +724,8 @@ class AsyncTavilyClient:
|
|
|
669
724
|
"POST",
|
|
670
725
|
"/research",
|
|
671
726
|
content=json.dumps(data),
|
|
672
|
-
timeout=timeout
|
|
727
|
+
timeout=timeout,
|
|
728
|
+
**({"headers": override_headers} if override_headers else {})
|
|
673
729
|
) as response:
|
|
674
730
|
if response.status_code != 200:
|
|
675
731
|
try:
|
|
@@ -678,16 +734,13 @@ class AsyncTavilyClient:
|
|
|
678
734
|
except Exception:
|
|
679
735
|
error_text = "Unknown error"
|
|
680
736
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
raise BadRequestError(error_text)
|
|
689
|
-
else:
|
|
690
|
-
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)
|
|
691
744
|
|
|
692
745
|
async for chunk in response.aiter_bytes():
|
|
693
746
|
if chunk:
|
|
@@ -701,29 +754,14 @@ class AsyncTavilyClient:
|
|
|
701
754
|
else:
|
|
702
755
|
async def _make_request():
|
|
703
756
|
try:
|
|
704
|
-
response = await self._client.post("/research", content=json.dumps(data), timeout=timeout)
|
|
757
|
+
response = await self._client.post("/research", content=json.dumps(data), timeout=timeout, **({"headers": override_headers} if override_headers else {}))
|
|
705
758
|
except httpx.TimeoutException:
|
|
706
759
|
raise TimeoutError(timeout)
|
|
707
760
|
|
|
708
761
|
if response.status_code == 200:
|
|
709
762
|
return response.json()
|
|
710
763
|
else:
|
|
711
|
-
|
|
712
|
-
try:
|
|
713
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
714
|
-
except Exception:
|
|
715
|
-
pass
|
|
716
|
-
|
|
717
|
-
if response.status_code == 429:
|
|
718
|
-
raise UsageLimitExceededError(detail)
|
|
719
|
-
elif response.status_code in [403,432,433]:
|
|
720
|
-
raise ForbiddenError(detail)
|
|
721
|
-
elif response.status_code == 401:
|
|
722
|
-
raise InvalidAPIKeyError(detail)
|
|
723
|
-
elif response.status_code == 400:
|
|
724
|
-
raise BadRequestError(detail)
|
|
725
|
-
else:
|
|
726
|
-
raise response.raise_for_status()
|
|
764
|
+
self._handle_error_response(response)
|
|
727
765
|
|
|
728
766
|
return _make_request()
|
|
729
767
|
|
|
@@ -752,6 +790,7 @@ class AsyncTavilyClient:
|
|
|
752
790
|
When stream=False: dict - the response dictionary.
|
|
753
791
|
When stream=True: AsyncGenerator[bytes, None] - iterate over this to get streaming chunks.
|
|
754
792
|
"""
|
|
793
|
+
self._check_keyless_supported("research")
|
|
755
794
|
result = self._research(
|
|
756
795
|
input=input,
|
|
757
796
|
model=model,
|
|
@@ -778,6 +817,7 @@ class AsyncTavilyClient:
|
|
|
778
817
|
Returns:
|
|
779
818
|
dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
|
|
780
819
|
"""
|
|
820
|
+
self._check_keyless_supported("get_research")
|
|
781
821
|
try:
|
|
782
822
|
response = await self._client.get(f"/research/{request_id}")
|
|
783
823
|
except Exception as e:
|
|
@@ -787,19 +827,4 @@ class AsyncTavilyClient:
|
|
|
787
827
|
data = response.json()
|
|
788
828
|
return data
|
|
789
829
|
else:
|
|
790
|
-
|
|
791
|
-
try:
|
|
792
|
-
detail = response.json().get("detail", {}).get("error", None)
|
|
793
|
-
except Exception:
|
|
794
|
-
pass
|
|
795
|
-
|
|
796
|
-
if response.status_code == 429:
|
|
797
|
-
raise UsageLimitExceededError(detail)
|
|
798
|
-
elif response.status_code in [403,432,433]:
|
|
799
|
-
raise ForbiddenError(detail)
|
|
800
|
-
elif response.status_code == 401:
|
|
801
|
-
raise InvalidAPIKeyError(detail)
|
|
802
|
-
elif response.status_code == 400:
|
|
803
|
-
raise BadRequestError(detail)
|
|
804
|
-
else:
|
|
805
|
-
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
|