From 17601feb62a6d950d814735b20777993ac8a02c9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:25:03 +0000 Subject: [PATCH] fix: add error field to ContentStatus for failed content retrievals The API returns an 'error' field in status objects when content retrieval fails, but the ContentStatus dataclass was missing this field, causing TypeError when parsing responses with errors. - Add ContentStatusError dataclass with http_status_code and tag fields - Add optional error field to ContentStatus - Make source field optional (it's not present on error statuses) - Update both sync and async get_contents to parse error field - Add test for ContentStatus with error parsing Co-Authored-By: ishan@exa.ai --- exa_py/api.py | 27 ++++++++++++++++++++++++++- tests/unit/test_search_api.py | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/exa_py/api.py b/exa_py/api.py index 724e26a..c1e5b61 100644 --- a/exa_py/api.py +++ b/exa_py/api.py @@ -1160,13 +1160,22 @@ def close(self) -> None: T = TypeVar("T") +@dataclass +class ContentStatusError: + """Error details for a failed content retrieval.""" + + http_status_code: Optional[int] = None + tag: Optional[str] = None + + @dataclass class ContentStatus: """A class representing the status of a content retrieval operation.""" id: str status: str - source: str + source: Optional[str] = None + error: Optional[ContentStatusError] = None @dataclass @@ -1671,11 +1680,19 @@ def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwargs): cost_dollars = parse_cost_dollars(data.get("costDollars")) statuses = [] for status in data.get("statuses", []): + error_data = status.get("error") + error = None + if error_data is not None: + error = ContentStatusError( + http_status_code=error_data.get("httpStatusCode"), + tag=error_data.get("tag"), + ) statuses.append( ContentStatus( id=status.get("id"), status=status.get("status"), source=status.get("source"), + error=error, ) ) results = [] @@ -2625,11 +2642,19 @@ async def get_contents(self, urls: Union[str, List[str], List[_Result]], **kwarg cost_dollars = parse_cost_dollars(data.get("costDollars")) statuses = [] for status in data.get("statuses", []): + error_data = status.get("error") + error = None + if error_data is not None: + error = ContentStatusError( + http_status_code=error_data.get("httpStatusCode"), + tag=error_data.get("tag"), + ) statuses.append( ContentStatus( id=status.get("id"), status=status.get("status"), source=status.get("source"), + error=error, ) ) results = [] diff --git a/tests/unit/test_search_api.py b/tests/unit/test_search_api.py index ef35d63..03baa7d 100644 --- a/tests/unit/test_search_api.py +++ b/tests/unit/test_search_api.py @@ -22,6 +22,27 @@ def test_contentstatus_parsing_offline(): payload_status = {"id": "u", "status": "success", "source": "cached"} cs = exa_api.ContentStatus(**payload_status) assert cs.id == "u" and cs.status == "success" and cs.source == "cached" + assert cs.error is None + + +def test_contentstatus_with_error_parsing_offline(): + payload_status = { + "id": "u", + "status": "error", + "error": {"httpStatusCode": 404, "tag": "NOT_FOUND"}, + } + cs = exa_api.ContentStatus( + id=payload_status["id"], + status=payload_status["status"], + error=exa_api.ContentStatusError( + http_status_code=payload_status["error"]["httpStatusCode"], + tag=payload_status["error"]["tag"], + ), + ) + assert cs.id == "u" and cs.status == "error" + assert cs.error is not None + assert cs.error.http_status_code == 404 + assert cs.error.tag == "NOT_FOUND" def test_answerresponse_accepts_dict():