exa-py 1.14.3__tar.gz → 1.14.5__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.

Potentially problematic release.


This version of exa-py might be problematic. Click here for more details.

Files changed (37) hide show
  1. {exa_py-1.14.3 → exa_py-1.14.5}/PKG-INFO +1 -1
  2. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/api.py +43 -0
  3. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py.egg-info/PKG-INFO +1 -1
  4. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py.egg-info/SOURCES.txt +1 -0
  5. {exa_py-1.14.3 → exa_py-1.14.5}/pyproject.toml +1 -8
  6. {exa_py-1.14.3 → exa_py-1.14.5}/setup.py +1 -1
  7. exa_py-1.14.5/tests/test_search_api.py +136 -0
  8. {exa_py-1.14.3 → exa_py-1.14.5}/README.md +0 -0
  9. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/__init__.py +0 -0
  10. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/py.typed +0 -0
  11. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/research/__init__.py +0 -0
  12. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/research/client.py +0 -0
  13. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/research/models.py +0 -0
  14. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/utils.py +0 -0
  15. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/__init__.py +0 -0
  16. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/client.py +0 -0
  17. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/core/__init__.py +0 -0
  18. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/core/base.py +0 -0
  19. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/enrichments/__init__.py +0 -0
  20. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/enrichments/client.py +0 -0
  21. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/items/__init__.py +0 -0
  22. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/items/client.py +0 -0
  23. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/monitors/__init__.py +0 -0
  24. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/monitors/client.py +0 -0
  25. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/monitors/runs/__init__.py +0 -0
  26. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/monitors/runs/client.py +0 -0
  27. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/searches/__init__.py +0 -0
  28. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/searches/client.py +0 -0
  29. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/types.py +0 -0
  30. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/webhooks/__init__.py +0 -0
  31. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py/websets/webhooks/client.py +0 -0
  32. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py.egg-info/dependency_links.txt +0 -0
  33. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py.egg-info/requires.txt +0 -0
  34. {exa_py-1.14.3 → exa_py-1.14.5}/exa_py.egg-info/top_level.txt +0 -0
  35. {exa_py-1.14.3 → exa_py-1.14.5}/setup.cfg +0 -0
  36. {exa_py-1.14.3 → exa_py-1.14.5}/tests/test_monitors.py +0 -0
  37. {exa_py-1.14.3 → exa_py-1.14.5}/tests/test_websets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exa-py
3
- Version: 1.14.3
3
+ Version: 1.14.5
4
4
  Summary: Python SDK for Exa API.
5
5
  Home-page: https://github.com/exa-labs/exa-py
6
6
  Author: Exa
@@ -166,6 +166,7 @@ CONTENTS_OPTIONS_TYPES = {
166
166
  "text": [dict, bool],
167
167
  "highlights": [dict, bool],
168
168
  "summary": [dict, bool],
169
+ "context": [dict, bool],
169
170
  "metadata": [dict, bool],
170
171
  "livecrawl_timeout": [int],
171
172
  "livecrawl": [LIVECRAWL_OPTIONS],
@@ -292,6 +293,16 @@ class SummaryContentsOptions(TypedDict, total=False):
292
293
  schema: JSONSchema
293
294
 
294
295
 
296
+ class ContextContentsOptions(TypedDict, total=False):
297
+ """Options for retrieving aggregated context from a set of search results.
298
+
299
+ Attributes:
300
+ max_characters (int): The maximum number of characters to include in the context string.
301
+ """
302
+
303
+ max_characters: int
304
+
305
+
295
306
  class ExtrasOptions(TypedDict, total=False):
296
307
  """A class representing additional extraction fields (e.g. links, images)"""
297
308
 
@@ -788,6 +799,15 @@ class AsyncStreamAnswerResponse:
788
799
 
789
800
  T = TypeVar("T")
790
801
 
802
+ @dataclass
803
+ class ContentStatus:
804
+ """A class representing the status of a content retrieval operation."""
805
+
806
+ id: str
807
+ status: str
808
+ source: str
809
+
810
+
791
811
 
792
812
  @dataclass
793
813
  class SearchResponse(Generic[T]):
@@ -798,16 +818,23 @@ class SearchResponse(Generic[T]):
798
818
  autoprompt_string (str, optional): The Exa query created by autoprompt.
799
819
  resolved_search_type (str, optional): 'neural' or 'keyword' if auto.
800
820
  auto_date (str, optional): A date for filtering if autoprompt found one.
821
+ context (str, optional): Combined context string when requested via contents.context.
822
+ statuses (List[ContentStatus], optional): Status list from get_contents.
823
+ cost_dollars (CostDollars, optional): Cost breakdown.
801
824
  """
802
825
 
803
826
  results: List[T]
804
827
  autoprompt_string: Optional[str]
805
828
  resolved_search_type: Optional[str]
806
829
  auto_date: Optional[str]
830
+ context: Optional[str] = None
831
+ statuses: Optional[List[ContentStatus]] = None
807
832
  cost_dollars: Optional[CostDollars] = None
808
833
 
809
834
  def __str__(self):
810
835
  output = "\n\n".join(str(result) for result in self.results)
836
+ if self.context:
837
+ output += f"\nContext: {self.context}"
811
838
  if self.autoprompt_string:
812
839
  output += f"\n\nAutoprompt String: {self.autoprompt_string}"
813
840
  if self.resolved_search_type:
@@ -818,6 +845,8 @@ class SearchResponse(Generic[T]):
818
845
  output += f"\n - search: {self.cost_dollars.search}"
819
846
  if self.cost_dollars.contents:
820
847
  output += f"\n - contents: {self.cost_dollars.contents}"
848
+ if self.statuses:
849
+ output += f"\nStatuses: {self.statuses}"
821
850
  return output
822
851
 
823
852
 
@@ -1242,6 +1271,7 @@ class Exa:
1242
1271
  "text",
1243
1272
  "highlights",
1244
1273
  "summary",
1274
+ "context",
1245
1275
  "subpages",
1246
1276
  "subpage_target",
1247
1277
  "livecrawl",
@@ -1258,6 +1288,7 @@ class Exa:
1258
1288
  data["autopromptString"] if "autopromptString" in data else None,
1259
1289
  data["resolvedSearchType"] if "resolvedSearchType" in data else None,
1260
1290
  data["autoDate"] if "autoDate" in data else None,
1291
+ context=data.get("context"),
1261
1292
  cost_dollars=cost_dollars,
1262
1293
  )
1263
1294
 
@@ -1405,12 +1436,15 @@ class Exa:
1405
1436
  options = to_camel_case(options)
1406
1437
  data = self.request("/contents", options)
1407
1438
  cost_dollars = parse_cost_dollars(data.get("costDollars"))
1439
+ statuses = [ContentStatus(**status) for status in data.get("statuses", [])]
1408
1440
  return SearchResponse(
1409
1441
  [Result(**to_snake_case(result)) for result in data["results"]],
1410
1442
  data.get("autopromptString"),
1411
1443
  data.get("resolvedSearchType"),
1412
1444
  data.get("autoDate"),
1445
+ context=data.get("context"),
1413
1446
  cost_dollars=cost_dollars,
1447
+ statuses=statuses,
1414
1448
  )
1415
1449
 
1416
1450
  def find_similar(
@@ -1700,6 +1734,7 @@ class Exa:
1700
1734
  "text",
1701
1735
  "highlights",
1702
1736
  "summary",
1737
+ "context",
1703
1738
  "subpages",
1704
1739
  "subpage_target",
1705
1740
  "livecrawl",
@@ -1716,6 +1751,7 @@ class Exa:
1716
1751
  data.get("autopromptString"),
1717
1752
  data.get("resolvedSearchType"),
1718
1753
  data.get("autoDate"),
1754
+ context=data.get("context"),
1719
1755
  cost_dollars=cost_dollars,
1720
1756
  )
1721
1757
 
@@ -2052,6 +2088,7 @@ class AsyncExa(Exa):
2052
2088
  "text",
2053
2089
  "highlights",
2054
2090
  "summary",
2091
+ "context",
2055
2092
  "subpages",
2056
2093
  "subpage_target",
2057
2094
  "livecrawl",
@@ -2068,6 +2105,7 @@ class AsyncExa(Exa):
2068
2105
  data["autopromptString"] if "autopromptString" in data else None,
2069
2106
  data["resolvedSearchType"] if "resolvedSearchType" in data else None,
2070
2107
  data["autoDate"] if "autoDate" in data else None,
2108
+ context=data.get("context"),
2071
2109
  cost_dollars=cost_dollars,
2072
2110
  )
2073
2111
 
@@ -2092,12 +2130,15 @@ class AsyncExa(Exa):
2092
2130
  options = to_camel_case(options)
2093
2131
  data = await self.async_request("/contents", options)
2094
2132
  cost_dollars = parse_cost_dollars(data.get("costDollars"))
2133
+ statuses = [ContentStatus(**status) for status in data.get("statuses", [])]
2095
2134
  return SearchResponse(
2096
2135
  [Result(**to_snake_case(result)) for result in data["results"]],
2097
2136
  data.get("autopromptString"),
2098
2137
  data.get("resolvedSearchType"),
2099
2138
  data.get("autoDate"),
2139
+ context=data.get("context"),
2100
2140
  cost_dollars=cost_dollars,
2141
+ statuses=statuses,
2101
2142
  )
2102
2143
 
2103
2144
  async def find_similar(
@@ -2175,6 +2216,7 @@ class AsyncExa(Exa):
2175
2216
  "text",
2176
2217
  "highlights",
2177
2218
  "summary",
2219
+ "context",
2178
2220
  "subpages",
2179
2221
  "subpage_target",
2180
2222
  "livecrawl",
@@ -2191,6 +2233,7 @@ class AsyncExa(Exa):
2191
2233
  data.get("autopromptString"),
2192
2234
  data.get("resolvedSearchType"),
2193
2235
  data.get("autoDate"),
2236
+ context=data.get("context"),
2194
2237
  cost_dollars=cost_dollars,
2195
2238
  )
2196
2239
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exa-py
3
- Version: 1.14.3
3
+ Version: 1.14.5
4
4
  Summary: Python SDK for Exa API.
5
5
  Home-page: https://github.com/exa-labs/exa-py
6
6
  Author: Exa
@@ -31,4 +31,5 @@ exa_py/websets/searches/client.py
31
31
  exa_py/websets/webhooks/__init__.py
32
32
  exa_py/websets/webhooks/client.py
33
33
  tests/test_monitors.py
34
+ tests/test_search_api.py
34
35
  tests/test_websets.py
@@ -1,10 +1,3 @@
1
- [tool.poetry]
2
- name = "exa-py"
3
- version = "1.14.2"
4
- description = "Python SDK for Exa API."
5
- authors = ["Exa AI <hello@exa.ai>"]
6
- readme = "README.md"
7
-
8
1
  [tool.poetry.dependencies]
9
2
  python = "^3.9"
10
3
  requests = "^2.32.3"
@@ -32,7 +25,7 @@ in-project = true
32
25
 
33
26
  [project]
34
27
  name = "exa-py"
35
- version = "1.14.3"
28
+ version = "1.14.5"
36
29
  description = "Python SDK for Exa API."
37
30
  readme = "README.md"
38
31
  requires-python = ">=3.9"
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="exa_py",
5
- version="1.12.4",
5
+ version="1.14.4",
6
6
  description="Python SDK for Exa API.",
7
7
  long_description_content_type="text/markdown",
8
8
  long_description=open("README.md").read(),
@@ -0,0 +1,136 @@
1
+ import os
2
+ from unittest.mock import AsyncMock, patch
3
+
4
+ import httpx
5
+ import pytest
6
+
7
+ from exa_py import Exa, AsyncExa, api as exa_api
8
+
9
+ API_KEY = os.getenv("EXA_API_KEY", "test-key")
10
+
11
+
12
+ def _have_real_key() -> bool:
13
+ return API_KEY != "test-key" and len(API_KEY) > 10
14
+
15
+ ########################################
16
+ # Offline unit tests (no network)
17
+ ########################################
18
+
19
+ def test_contentstatus_parsing_offline():
20
+ payload_status = {"id": "u", "status": "success", "source": "cached"}
21
+ cs = exa_api.ContentStatus(**payload_status)
22
+ assert cs.id == "u" and cs.status == "success" and cs.source == "cached"
23
+
24
+
25
+ def test_answerresponse_accepts_dict():
26
+ dummy = exa_api.AnswerResult(id="1", url="u", title="t")
27
+ resp = exa_api.AnswerResponse(answer={"foo": "bar"}, citations=[dummy])
28
+ assert isinstance(resp.answer, dict) and resp.answer["foo"] == "bar"
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_async_request_accepts_201():
33
+ ax = AsyncExa(API_KEY)
34
+
35
+ async def _fake_post(url, json, headers):
36
+ return httpx.Response(201, json={"ok": True})
37
+
38
+ with patch.object(ax.client, "post", new=AsyncMock(side_effect=_fake_post)):
39
+ data = await ax.async_request("/dummy", {})
40
+ assert data == {"ok": True}
41
+
42
+
43
+ ########################################
44
+ # Live integration tests (skipped without key)
45
+ ########################################
46
+
47
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
48
+ def test_user_agent_header():
49
+ exa = Exa(API_KEY)
50
+ assert exa.headers["User-Agent"] == "exa-py 1.12.4"
51
+
52
+
53
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
54
+ def test_research_client_attrs():
55
+ exa = Exa(API_KEY)
56
+ aexa = AsyncExa(API_KEY)
57
+ assert hasattr(exa, "research") and hasattr(aexa, "research")
58
+
59
+
60
+ # ---- Core live endpoint smoke checks ----
61
+
62
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
63
+ def test_get_contents_live_preferred():
64
+ exa = Exa(API_KEY)
65
+ resp = exa.get_contents(urls=["https://techcrunch.com"], text=True, livecrawl="preferred")
66
+ assert isinstance(resp, exa_api.SearchResponse)
67
+ # statuses may be empty when cached – still fine
68
+ assert len(resp.results) >= 1
69
+
70
+
71
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
72
+ def test_search_and_contents_live():
73
+ exa = Exa(API_KEY)
74
+ resp = exa.search_and_contents("openai", num_results=1, text=True)
75
+ assert resp.results and resp.results[0].text
76
+
77
+
78
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
79
+ def test_find_similar_live():
80
+ exa = Exa(API_KEY)
81
+ resp = exa.find_similar("https://www.openai.com", num_results=1)
82
+ assert resp.results
83
+
84
+
85
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
86
+ def test_get_contents_sync_live():
87
+ exa = Exa(API_KEY)
88
+ resp = exa.get_contents(urls=["https://example.com"], text=True, livecrawl="never")
89
+ assert resp.results
90
+
91
+
92
+ @pytest.mark.asyncio
93
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
94
+ async def test_get_contents_async_live():
95
+ ax = AsyncExa(API_KEY)
96
+ resp = await ax.get_contents(urls=["https://example.com"], text=True, livecrawl="never")
97
+ assert resp.results
98
+
99
+
100
+ # researchTask endpoint is still beta; mark as xfail if 404 returned
101
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
102
+ @pytest.mark.xfail(strict=False)
103
+ def test_research_task_live():
104
+ exa = Exa(API_KEY)
105
+ schema = {"type": "object", "properties": {"answer": {"type": "string"}}, "required": ["answer"]}
106
+ resp = exa.researchTask(input_instructions="Return the string 'pong'", output_schema=schema)
107
+ assert resp.id
108
+
109
+ ########################################
110
+ # Live tests for new context / statuses features
111
+ ########################################
112
+
113
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
114
+ def test_search_and_contents_context_live():
115
+ """search_and_contents with context=True should return non-empty context string."""
116
+ exa = Exa(API_KEY)
117
+ resp = exa.search_and_contents("openai research", num_results=3, context=True, text=False)
118
+ assert resp.context is not None and isinstance(resp.context, str) and len(resp.context) > 0
119
+
120
+
121
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
122
+ def test_find_similar_and_contents_context_live():
123
+ """find_similar_and_contents with context flag should include context string."""
124
+ exa = Exa(API_KEY)
125
+ resp = exa.find_similar_and_contents("https://www.openai.com", num_results=3, context=True, text=False)
126
+ # context may be empty depending on backend, but attribute should exist (None or str)
127
+ assert hasattr(resp, "context")
128
+
129
+
130
+ @pytest.mark.skipif(not _have_real_key(), reason="EXA_API_KEY not provided")
131
+ def test_get_contents_statuses_live():
132
+ """get_contents should expose statuses list (possibly empty)."""
133
+ exa = Exa(API_KEY)
134
+ resp = exa.get_contents(urls=["https://techcrunch.com"], text=True, livecrawl="never")
135
+ # statuses attribute exists; ensure it's a list
136
+ assert isinstance(resp.statuses, list)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes