tavily-python 0.7.20__tar.gz → 0.7.22__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.
Files changed (24) hide show
  1. {tavily_python-0.7.20 → tavily_python-0.7.22}/PKG-INFO +16 -1
  2. {tavily_python-0.7.20 → tavily_python-0.7.22}/README.md +15 -0
  3. {tavily_python-0.7.20 → tavily_python-0.7.22}/setup.py +1 -1
  4. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/async_tavily.py +149 -141
  5. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/tavily.py +4 -0
  6. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/PKG-INFO +16 -1
  7. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/SOURCES.txt +2 -1
  8. {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_errors.py +4 -1
  9. {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_search.py +57 -2
  10. tavily_python-0.7.22/tests/test_session_pooling.py +152 -0
  11. {tavily_python-0.7.20 → tavily_python-0.7.22}/LICENSE +0 -0
  12. {tavily_python-0.7.20 → tavily_python-0.7.22}/setup.cfg +0 -0
  13. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/__init__.py +0 -0
  14. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/config.py +0 -0
  15. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/errors.py +0 -0
  16. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/hybrid_rag/__init__.py +0 -0
  17. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/hybrid_rag/hybrid_rag.py +0 -0
  18. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily/utils.py +0 -0
  19. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/dependency_links.txt +0 -0
  20. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/requires.txt +0 -0
  21. {tavily_python-0.7.20 → tavily_python-0.7.22}/tavily_python.egg-info/top_level.txt +0 -0
  22. {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_crawl.py +0 -0
  23. {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_map.py +0 -0
  24. {tavily_python-0.7.20 → tavily_python-0.7.22}/tests/test_research.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tavily-python
3
- Version: 0.7.20
3
+ Version: 0.7.22
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
@@ -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
@@ -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
@@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
5
5
 
6
6
  setup(
7
7
  name='tavily-python',
8
- version='0.7.20',
8
+ version='0.7.22',
9
9
  url='https://github.com/tavily-ai/tavily-python',
10
10
  author='Tavily AI',
11
11
  author_email='support@tavily.com',
@@ -44,8 +44,9 @@ class AsyncTavilyClient:
44
44
  tavily_project = project_id or os.getenv("TAVILY_PROJECT")
45
45
 
46
46
  self._api_base_url = api_base_url or "https://api.tavily.com"
47
-
48
- self._client_creator = lambda: httpx.AsyncClient(
47
+
48
+ # Create a persistent client for connection pooling
49
+ self._client = httpx.AsyncClient(
49
50
  headers={
50
51
  "Content-Type": "application/json",
51
52
  "Authorization": f"Bearer {api_key}",
@@ -57,6 +58,16 @@ class AsyncTavilyClient:
57
58
  )
58
59
  self._company_info_tags = company_info_tags
59
60
 
61
+ async def close(self):
62
+ """Close the client and release connection pool resources."""
63
+ await self._client.aclose()
64
+
65
+ async def __aenter__(self):
66
+ return self
67
+
68
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
69
+ await self.close()
70
+
60
71
  async def _search(
61
72
  self,
62
73
  query: str,
@@ -77,6 +88,7 @@ class AsyncTavilyClient:
77
88
  auto_parameters: bool = None,
78
89
  include_favicon: bool = None,
79
90
  include_usage: bool = None,
91
+ exact_match: bool = None,
80
92
  **kwargs,
81
93
  ) -> dict:
82
94
  """
@@ -100,6 +112,7 @@ class AsyncTavilyClient:
100
112
  "auto_parameters": auto_parameters,
101
113
  "include_favicon": include_favicon,
102
114
  "include_usage": include_usage,
115
+ "exact_match": exact_match,
103
116
  }
104
117
 
105
118
  data = {k: v for k, v in data.items() if v is not None}
@@ -109,11 +122,10 @@ class AsyncTavilyClient:
109
122
 
110
123
  timeout = min(timeout, 120)
111
124
 
112
- async with self._client_creator() as client:
113
- try:
114
- response = await client.post("/search", content=json.dumps(data), timeout=timeout)
115
- except httpx.TimeoutException:
116
- raise TimeoutError(timeout)
125
+ try:
126
+ response = await self._client.post("/search", content=json.dumps(data), timeout=timeout)
127
+ except httpx.TimeoutException:
128
+ raise TimeoutError(timeout)
117
129
 
118
130
  if response.status_code == 200:
119
131
  return response.json()
@@ -154,6 +166,7 @@ class AsyncTavilyClient:
154
166
  auto_parameters: bool = None,
155
167
  include_favicon: bool = None,
156
168
  include_usage: bool = None,
169
+ exact_match: bool = None,
157
170
  **kwargs, # Accept custom arguments
158
171
  ) -> dict:
159
172
  """
@@ -178,6 +191,7 @@ class AsyncTavilyClient:
178
191
  auto_parameters=auto_parameters,
179
192
  include_favicon=include_favicon,
180
193
  include_usage=include_usage,
194
+ exact_match=exact_match,
181
195
  **kwargs,
182
196
  )
183
197
 
@@ -221,11 +235,10 @@ class AsyncTavilyClient:
221
235
  if kwargs:
222
236
  data.update(kwargs)
223
237
 
224
- async with self._client_creator() as client:
225
- try:
226
- response = await client.post("/extract", content=json.dumps(data), timeout=timeout)
227
- except httpx.TimeoutException:
228
- raise TimeoutError(timeout)
238
+ try:
239
+ response = await self._client.post("/extract", content=json.dumps(data), timeout=timeout)
240
+ except httpx.TimeoutException:
241
+ raise TimeoutError(timeout)
229
242
 
230
243
  if response.status_code == 200:
231
244
  return response.json()
@@ -283,7 +296,7 @@ class AsyncTavilyClient:
283
296
  response_dict["failed_results"] = failed_results
284
297
 
285
298
  return response_dict
286
-
299
+
287
300
  async def _crawl(self,
288
301
  url: str,
289
302
  max_depth: int = None,
@@ -332,31 +345,30 @@ class AsyncTavilyClient:
332
345
 
333
346
  data = {k: v for k, v in data.items() if v is not None}
334
347
 
335
- async with self._client_creator() as client:
348
+ try:
349
+ response = await self._client.post("/crawl", content=json.dumps(data), timeout=timeout)
350
+ except httpx.TimeoutException:
351
+ raise TimeoutError(timeout)
352
+
353
+ if response.status_code == 200:
354
+ return response.json()
355
+ else:
356
+ detail = ""
336
357
  try:
337
- response = await client.post("/crawl", content=json.dumps(data), timeout=timeout)
338
- except httpx.TimeoutException:
339
- raise TimeoutError(timeout)
358
+ detail = response.json().get("detail", {}).get("error", None)
359
+ except Exception:
360
+ pass
340
361
 
341
- if response.status_code == 200:
342
- return response.json()
362
+ if response.status_code == 429:
363
+ raise UsageLimitExceededError(detail)
364
+ elif response.status_code in [403,432,433]:
365
+ raise ForbiddenError(detail)
366
+ elif response.status_code == 401:
367
+ raise InvalidAPIKeyError(detail)
368
+ elif response.status_code == 400:
369
+ raise BadRequestError(detail)
343
370
  else:
344
- detail = ""
345
- try:
346
- detail = response.json().get("detail", {}).get("error", None)
347
- except Exception:
348
- pass
349
-
350
- if response.status_code == 429:
351
- raise UsageLimitExceededError(detail)
352
- elif response.status_code in [403,432,433]:
353
- raise ForbiddenError(detail)
354
- elif response.status_code == 401:
355
- raise InvalidAPIKeyError(detail)
356
- elif response.status_code == 400:
357
- raise BadRequestError(detail)
358
- else:
359
- raise response.raise_for_status()
371
+ raise response.raise_for_status()
360
372
 
361
373
  async def crawl(self,
362
374
  url: str,
@@ -380,7 +392,7 @@ class AsyncTavilyClient:
380
392
  ) -> dict:
381
393
  """
382
394
  Combined crawl method.
383
-
395
+
384
396
  """
385
397
  response_dict = await self._crawl(url,
386
398
  max_depth=max_depth,
@@ -402,7 +414,7 @@ class AsyncTavilyClient:
402
414
  **kwargs)
403
415
 
404
416
  return response_dict
405
-
417
+
406
418
  async def _map(self,
407
419
  url: str,
408
420
  max_depth: int = None,
@@ -443,31 +455,30 @@ class AsyncTavilyClient:
443
455
 
444
456
  data = {k: v for k, v in data.items() if v is not None}
445
457
 
446
- async with self._client_creator() as client:
458
+ try:
459
+ response = await self._client.post("/map", content=json.dumps(data), timeout=timeout)
460
+ except httpx.TimeoutException:
461
+ raise TimeoutError(timeout)
462
+
463
+ if response.status_code == 200:
464
+ return response.json()
465
+ else:
466
+ detail = ""
447
467
  try:
448
- response = await client.post("/map", content=json.dumps(data), timeout=timeout)
449
- except httpx.TimeoutException:
450
- raise TimeoutError(timeout)
468
+ detail = response.json().get("detail", {}).get("error", None)
469
+ except Exception:
470
+ pass
451
471
 
452
- if response.status_code == 200:
453
- return response.json()
472
+ if response.status_code == 429:
473
+ raise UsageLimitExceededError(detail)
474
+ elif response.status_code in [403,432,433]:
475
+ raise ForbiddenError(detail)
476
+ elif response.status_code == 401:
477
+ raise InvalidAPIKeyError(detail)
478
+ elif response.status_code == 400:
479
+ raise BadRequestError(detail)
454
480
  else:
455
- detail = ""
456
- try:
457
- detail = response.json().get("detail", {}).get("error", None)
458
- except Exception:
459
- pass
460
-
461
- if response.status_code == 429:
462
- raise UsageLimitExceededError(detail)
463
- elif response.status_code in [403,432,433]:
464
- raise ForbiddenError(detail)
465
- elif response.status_code == 401:
466
- raise InvalidAPIKeyError(detail)
467
- elif response.status_code == 400:
468
- raise BadRequestError(detail)
469
- else:
470
- raise response.raise_for_status()
481
+ raise response.raise_for_status()
471
482
 
472
483
  async def map(self,
473
484
  url: str,
@@ -639,68 +650,66 @@ class AsyncTavilyClient:
639
650
  if stream:
640
651
  async def stream_generator() -> AsyncGenerator[bytes, None]:
641
652
  try:
642
- async with self._client_creator() as client:
643
- async with client.stream(
644
- "POST",
645
- "/research",
646
- content=json.dumps(data),
647
- timeout=timeout
648
- ) as response:
649
- if response.status_code != 200:
650
- try:
651
- error_text = await response.aread()
652
- error_text = error_text.decode('utf-8') if isinstance(error_text, bytes) else error_text
653
- except Exception:
654
- error_text = "Unknown error"
655
-
656
- if response.status_code == 429:
657
- raise UsageLimitExceededError(error_text)
658
- elif response.status_code in [403,432,433]:
659
- raise ForbiddenError(error_text)
660
- elif response.status_code == 401:
661
- raise InvalidAPIKeyError(error_text)
662
- elif response.status_code == 400:
663
- raise BadRequestError(error_text)
664
- else:
665
- raise Exception(f"Error {response.status_code}: {error_text}")
666
-
667
- async for chunk in response.aiter_bytes():
668
- if chunk:
669
- yield chunk
653
+ async with self._client.stream(
654
+ "POST",
655
+ "/research",
656
+ content=json.dumps(data),
657
+ timeout=timeout
658
+ ) as response:
659
+ if response.status_code != 200:
660
+ try:
661
+ error_text = await response.aread()
662
+ error_text = error_text.decode('utf-8') if isinstance(error_text, bytes) else error_text
663
+ except Exception:
664
+ error_text = "Unknown error"
665
+
666
+ if response.status_code == 429:
667
+ raise UsageLimitExceededError(error_text)
668
+ elif response.status_code in [403,432,433]:
669
+ raise ForbiddenError(error_text)
670
+ elif response.status_code == 401:
671
+ raise InvalidAPIKeyError(error_text)
672
+ elif response.status_code == 400:
673
+ raise BadRequestError(error_text)
674
+ else:
675
+ raise Exception(f"Error {response.status_code}: {error_text}")
676
+
677
+ async for chunk in response.aiter_bytes():
678
+ if chunk:
679
+ yield chunk
670
680
  except httpx.TimeoutException:
671
681
  raise TimeoutError(timeout)
672
682
  except Exception as e:
673
683
  raise Exception(f"Error during research stream: {str(e)}")
674
-
684
+
675
685
  return stream_generator()
676
686
  else:
677
687
  async def _make_request():
678
- async with self._client_creator() as client:
679
- try:
680
- response = await client.post("/research", content=json.dumps(data), timeout=timeout)
681
- except httpx.TimeoutException:
682
- raise TimeoutError(timeout)
688
+ try:
689
+ response = await self._client.post("/research", content=json.dumps(data), timeout=timeout)
690
+ except httpx.TimeoutException:
691
+ raise TimeoutError(timeout)
683
692
 
684
- if response.status_code == 200:
685
- return response.json()
693
+ if response.status_code == 200:
694
+ return response.json()
695
+ else:
696
+ detail = ""
697
+ try:
698
+ detail = response.json().get("detail", {}).get("error", None)
699
+ except Exception:
700
+ pass
701
+
702
+ if response.status_code == 429:
703
+ raise UsageLimitExceededError(detail)
704
+ elif response.status_code in [403,432,433]:
705
+ raise ForbiddenError(detail)
706
+ elif response.status_code == 401:
707
+ raise InvalidAPIKeyError(detail)
708
+ elif response.status_code == 400:
709
+ raise BadRequestError(detail)
686
710
  else:
687
- detail = ""
688
- try:
689
- detail = response.json().get("detail", {}).get("error", None)
690
- except Exception:
691
- pass
692
-
693
- if response.status_code == 429:
694
- raise UsageLimitExceededError(detail)
695
- elif response.status_code in [403,432,433]:
696
- raise ForbiddenError(detail)
697
- elif response.status_code == 401:
698
- raise InvalidAPIKeyError(detail)
699
- elif response.status_code == 400:
700
- raise BadRequestError(detail)
701
- else:
702
- raise response.raise_for_status()
703
-
711
+ raise response.raise_for_status()
712
+
704
713
  return _make_request()
705
714
 
706
715
  async def research(self,
@@ -714,16 +723,16 @@ class AsyncTavilyClient:
714
723
  ) -> Union[dict, AsyncGenerator[bytes, None]]:
715
724
  """
716
725
  Research method to create a research task.
717
-
726
+
718
727
  Args:
719
728
  input: The research task description (required).
720
729
  model: Research depth - must be either 'mini', 'pro', or 'auto'.
721
730
  output_schema: Schema for the 'structured_output' response format (JSON Schema dict).
722
731
  stream: Whether to stream the research task.
723
732
  citation_format: Citation format - must be either 'numbered', 'mla', 'apa', or 'chicago'.
724
- timeout: Optional HTTP request timeout in seconds.
733
+ timeout: Optional HTTP request timeout in seconds.
725
734
  **kwargs: Additional custom arguments.
726
-
735
+
727
736
  Returns:
728
737
  When stream=False: dict - the response dictionary.
729
738
  When stream=True: AsyncGenerator[bytes, None] - iterate over this to get streaming chunks.
@@ -740,43 +749,42 @@ class AsyncTavilyClient:
740
749
  if stream:
741
750
  return result # Don't await the result, it's an AsyncGenerator that will be lazy and only execute when iterated over with async for
742
751
  else:
743
- return await result
752
+ return await result
744
753
 
745
754
  async def get_research(self,
746
755
  request_id: str
747
756
  ) -> dict:
748
757
  """
749
758
  Get research results by request_id.
750
-
759
+
751
760
  Args:
752
761
  request_id: The research request ID.
753
-
762
+
754
763
  Returns:
755
764
  dict: Research response containing request_id, created_at, completed_at, status, content, and sources.
756
765
  """
757
- async with self._client_creator() as client:
766
+ try:
767
+ response = await self._client.get(f"/research/{request_id}")
768
+ except Exception as e:
769
+ raise Exception(f"Error getting research: {e}")
770
+
771
+ if response.status_code in (200, 202):
772
+ data = response.json()
773
+ return data
774
+ else:
775
+ detail = ""
758
776
  try:
759
- response = await client.get(f"/research/{request_id}")
760
- except Exception as e:
761
- raise Exception(f"Error getting research: {e}")
777
+ detail = response.json().get("detail", {}).get("error", None)
778
+ except Exception:
779
+ pass
762
780
 
763
- if response.status_code in (200, 202):
764
- data = response.json()
765
- return data
781
+ if response.status_code == 429:
782
+ raise UsageLimitExceededError(detail)
783
+ elif response.status_code in [403,432,433]:
784
+ raise ForbiddenError(detail)
785
+ elif response.status_code == 401:
786
+ raise InvalidAPIKeyError(detail)
787
+ elif response.status_code == 400:
788
+ raise BadRequestError(detail)
766
789
  else:
767
- detail = ""
768
- try:
769
- detail = response.json().get("detail", {}).get("error", None)
770
- except Exception:
771
- pass
772
-
773
- if response.status_code == 429:
774
- raise UsageLimitExceededError(detail)
775
- elif response.status_code in [403,432,433]:
776
- raise ForbiddenError(detail)
777
- elif response.status_code == 401:
778
- raise InvalidAPIKeyError(detail)
779
- elif response.status_code == 400:
780
- raise BadRequestError(detail)
781
- else:
782
- raise response.raise_for_status()
790
+ raise response.raise_for_status()
@@ -71,6 +71,7 @@ class TavilyClient:
71
71
  auto_parameters: bool = None,
72
72
  include_favicon: bool = None,
73
73
  include_usage: bool = None,
74
+ exact_match: bool = None,
74
75
  **kwargs
75
76
  ) -> dict:
76
77
  """
@@ -95,6 +96,7 @@ class TavilyClient:
95
96
  "auto_parameters": auto_parameters,
96
97
  "include_favicon": include_favicon,
97
98
  "include_usage": include_usage,
99
+ "exact_match": exact_match,
98
100
  }
99
101
 
100
102
  data = {k: v for k, v in data.items() if v is not None}
@@ -151,6 +153,7 @@ class TavilyClient:
151
153
  auto_parameters: bool = None,
152
154
  include_favicon: bool = None,
153
155
  include_usage: bool = None,
156
+ exact_match: bool = None,
154
157
  **kwargs, # Accept custom arguments
155
158
  ) -> dict:
156
159
  """
@@ -175,6 +178,7 @@ class TavilyClient:
175
178
  auto_parameters=auto_parameters,
176
179
  include_favicon=include_favicon,
177
180
  include_usage=include_usage,
181
+ exact_match=exact_match,
178
182
  **kwargs)
179
183
  response_dict.setdefault("results", [])
180
184
  return response_dict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tavily-python
3
- Version: 0.7.20
3
+ Version: 0.7.22
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
@@ -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
@@ -18,4 +18,5 @@ tests/test_crawl.py
18
18
  tests/test_errors.py
19
19
  tests/test_map.py
20
20
  tests/test_research.py
21
- tests/test_search.py
21
+ tests/test_search.py
22
+ tests/test_session_pooling.py
@@ -11,7 +11,10 @@ def set_api_key():
11
11
  old_key = os.getenv("TAVILY_API_KEY")
12
12
  os.environ["TAVILY_API_KEY"] = "test_api_key"
13
13
  yield
14
- os.environ["TAVILY_API_KEY"] = old_key
14
+ if old_key is not None:
15
+ os.environ["TAVILY_API_KEY"] = old_key
16
+ elif "TAVILY_API_KEY" in os.environ:
17
+ del os.environ["TAVILY_API_KEY"]
15
18
 
16
19
  @pytest.fixture
17
20
  def clear_api_key():
@@ -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'
@@ -0,0 +1,152 @@
1
+ """
2
+ Tests for session pooling functionality in both sync and async clients.
3
+ """
4
+ import asyncio
5
+ import pytest
6
+
7
+ import tavily.tavily as sync_tavily
8
+ import tavily.async_tavily as async_tavily
9
+ from tests.request_intercept import intercept_requests, clear_interceptor
10
+
11
+
12
+ dummy_response = {
13
+ "query": "test",
14
+ "results": [{"title": "Test", "url": "https://test.com", "content": "Test content", "score": 0.99}],
15
+ "response_time": 0.1
16
+ }
17
+
18
+
19
+ # =============================================================================
20
+ # SYNC CLIENT TESTS
21
+ # =============================================================================
22
+
23
+ class TestSyncSessionPooling:
24
+ """Test sync client session pooling and lifecycle."""
25
+
26
+ @pytest.fixture
27
+ def interceptor(self):
28
+ yield intercept_requests(sync_tavily)
29
+ clear_interceptor(sync_tavily)
30
+
31
+ def test_context_manager(self, interceptor):
32
+ """Test that sync client works with context manager."""
33
+ interceptor.set_response(200, json=dummy_response)
34
+
35
+ with sync_tavily.TavilyClient(api_key="tvly-test") as client:
36
+ response = client.search("test query")
37
+ assert response["results"][0]["title"] == "Test"
38
+
39
+ def test_close_method(self, interceptor):
40
+ """Test that close() method works without error."""
41
+ interceptor.set_response(200, json=dummy_response)
42
+
43
+ client = sync_tavily.TavilyClient(api_key="tvly-test")
44
+ response = client.search("test query")
45
+ assert response["results"][0]["title"] == "Test"
46
+
47
+ # close() should not raise
48
+ client.close()
49
+
50
+ def test_multiple_sequential_requests(self, interceptor):
51
+ """Test that multiple requests work with same client (connection reuse)."""
52
+ interceptor.set_response(200, json=dummy_response)
53
+
54
+ client = sync_tavily.TavilyClient(api_key="tvly-test")
55
+
56
+ # Make multiple requests with same client
57
+ for i in range(3):
58
+ response = client.search(f"test query {i}")
59
+ assert response["results"][0]["title"] == "Test"
60
+
61
+ client.close()
62
+
63
+ def test_context_manager_multiple_requests(self, interceptor):
64
+ """Test multiple requests within context manager."""
65
+ interceptor.set_response(200, json=dummy_response)
66
+
67
+ with sync_tavily.TavilyClient(api_key="tvly-test") as client:
68
+ for i in range(3):
69
+ response = client.search(f"test query {i}")
70
+ assert response["results"][0]["title"] == "Test"
71
+
72
+
73
+ # =============================================================================
74
+ # ASYNC CLIENT TESTS
75
+ # =============================================================================
76
+
77
+ class TestAsyncSessionPooling:
78
+ """Test async client session pooling and lifecycle."""
79
+
80
+ @pytest.fixture
81
+ def interceptor(self):
82
+ yield intercept_requests(async_tavily)
83
+ clear_interceptor(async_tavily)
84
+
85
+ def test_context_manager(self, interceptor):
86
+ """Test that async client works with async context manager."""
87
+ interceptor.set_response(200, json=dummy_response)
88
+
89
+ async def run():
90
+ async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
91
+ response = await client.search("test query")
92
+ assert response["results"][0]["title"] == "Test"
93
+
94
+ asyncio.run(run())
95
+
96
+ def test_close_method(self, interceptor):
97
+ """Test that close() method works without error."""
98
+ interceptor.set_response(200, json=dummy_response)
99
+
100
+ async def run():
101
+ client = async_tavily.AsyncTavilyClient(api_key="tvly-test")
102
+ response = await client.search("test query")
103
+ assert response["results"][0]["title"] == "Test"
104
+
105
+ # close() should not raise
106
+ await client.close()
107
+
108
+ asyncio.run(run())
109
+
110
+ def test_multiple_sequential_requests(self, interceptor):
111
+ """Test that multiple requests work with same client (connection reuse)."""
112
+ interceptor.set_response(200, json=dummy_response)
113
+
114
+ async def run():
115
+ client = async_tavily.AsyncTavilyClient(api_key="tvly-test")
116
+
117
+ # Make multiple requests with same client
118
+ for i in range(3):
119
+ response = await client.search(f"test query {i}")
120
+ assert response["results"][0]["title"] == "Test"
121
+
122
+ await client.close()
123
+
124
+ asyncio.run(run())
125
+
126
+ def test_context_manager_multiple_requests(self, interceptor):
127
+ """Test multiple requests within async context manager."""
128
+ interceptor.set_response(200, json=dummy_response)
129
+
130
+ async def run():
131
+ async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
132
+ for i in range(3):
133
+ response = await client.search(f"test query {i}")
134
+ assert response["results"][0]["title"] == "Test"
135
+
136
+ asyncio.run(run())
137
+
138
+ def test_concurrent_requests_same_client(self, interceptor):
139
+ """Test concurrent requests using same client."""
140
+ interceptor.set_response(200, json=dummy_response)
141
+
142
+ async def run():
143
+ async with async_tavily.AsyncTavilyClient(api_key="tvly-test") as client:
144
+ # Run multiple searches concurrently
145
+ tasks = [client.search(f"query {i}") for i in range(3)]
146
+ results = await asyncio.gather(*tasks)
147
+
148
+ assert len(results) == 3
149
+ for result in results:
150
+ assert result["results"][0]["title"] == "Test"
151
+
152
+ asyncio.run(run())
File without changes
File without changes