exa-py 1.14.3__tar.gz → 1.14.4__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.
- {exa_py-1.14.3 → exa_py-1.14.4}/PKG-INFO +1 -1
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/api.py +17 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py.egg-info/PKG-INFO +1 -1
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py.egg-info/SOURCES.txt +1 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/pyproject.toml +1 -1
- {exa_py-1.14.3 → exa_py-1.14.4}/setup.py +1 -1
- exa_py-1.14.4/tests/test_search_api.py +107 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/README.md +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/py.typed +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/research/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/research/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/research/models.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/utils.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/core/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/core/base.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/enrichments/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/enrichments/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/items/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/items/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/monitors/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/monitors/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/monitors/runs/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/monitors/runs/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/searches/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/searches/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/types.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/webhooks/__init__.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py/websets/webhooks/client.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py.egg-info/dependency_links.txt +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py.egg-info/requires.txt +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/exa_py.egg-info/top_level.txt +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/setup.cfg +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/tests/test_monitors.py +0 -0
- {exa_py-1.14.3 → exa_py-1.14.4}/tests/test_websets.py +0 -0
|
@@ -788,6 +788,15 @@ class AsyncStreamAnswerResponse:
|
|
|
788
788
|
|
|
789
789
|
T = TypeVar("T")
|
|
790
790
|
|
|
791
|
+
@dataclass
|
|
792
|
+
class ContentStatus:
|
|
793
|
+
"""A class representing the status of a content retrieval operation."""
|
|
794
|
+
|
|
795
|
+
id: str
|
|
796
|
+
status: str
|
|
797
|
+
source: str
|
|
798
|
+
|
|
799
|
+
|
|
791
800
|
|
|
792
801
|
@dataclass
|
|
793
802
|
class SearchResponse(Generic[T]):
|
|
@@ -804,6 +813,7 @@ class SearchResponse(Generic[T]):
|
|
|
804
813
|
autoprompt_string: Optional[str]
|
|
805
814
|
resolved_search_type: Optional[str]
|
|
806
815
|
auto_date: Optional[str]
|
|
816
|
+
statuses: Optional[List[ContentStatus]] = None
|
|
807
817
|
cost_dollars: Optional[CostDollars] = None
|
|
808
818
|
|
|
809
819
|
def __str__(self):
|
|
@@ -818,6 +828,8 @@ class SearchResponse(Generic[T]):
|
|
|
818
828
|
output += f"\n - search: {self.cost_dollars.search}"
|
|
819
829
|
if self.cost_dollars.contents:
|
|
820
830
|
output += f"\n - contents: {self.cost_dollars.contents}"
|
|
831
|
+
if self.statuses:
|
|
832
|
+
output += f"\nStatuses: {self.statuses}"
|
|
821
833
|
return output
|
|
822
834
|
|
|
823
835
|
|
|
@@ -1402,15 +1414,18 @@ class Exa:
|
|
|
1402
1414
|
options,
|
|
1403
1415
|
{**CONTENTS_OPTIONS_TYPES, **CONTENTS_ENDPOINT_OPTIONS_TYPES},
|
|
1404
1416
|
)
|
|
1417
|
+
|
|
1405
1418
|
options = to_camel_case(options)
|
|
1406
1419
|
data = self.request("/contents", options)
|
|
1407
1420
|
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
1421
|
+
statuses = [ContentStatus(**status) for status in data.get("statuses", [])]
|
|
1408
1422
|
return SearchResponse(
|
|
1409
1423
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
1410
1424
|
data.get("autopromptString"),
|
|
1411
1425
|
data.get("resolvedSearchType"),
|
|
1412
1426
|
data.get("autoDate"),
|
|
1413
1427
|
cost_dollars=cost_dollars,
|
|
1428
|
+
statuses=statuses,
|
|
1414
1429
|
)
|
|
1415
1430
|
|
|
1416
1431
|
def find_similar(
|
|
@@ -2092,12 +2107,14 @@ class AsyncExa(Exa):
|
|
|
2092
2107
|
options = to_camel_case(options)
|
|
2093
2108
|
data = await self.async_request("/contents", options)
|
|
2094
2109
|
cost_dollars = parse_cost_dollars(data.get("costDollars"))
|
|
2110
|
+
statuses = [ContentStatus(**status) for status in data.get("statuses", [])]
|
|
2095
2111
|
return SearchResponse(
|
|
2096
2112
|
[Result(**to_snake_case(result)) for result in data["results"]],
|
|
2097
2113
|
data.get("autopromptString"),
|
|
2098
2114
|
data.get("resolvedSearchType"),
|
|
2099
2115
|
data.get("autoDate"),
|
|
2100
2116
|
cost_dollars=cost_dollars,
|
|
2117
|
+
statuses=statuses,
|
|
2101
2118
|
)
|
|
2102
2119
|
|
|
2103
2120
|
async def find_similar(
|
|
@@ -0,0 +1,107 @@
|
|
|
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.ContentResponse)
|
|
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
|
|
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
|
|
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
|