Skip to content

Commit d4a25a0

Browse files
Improve APIResponse JSON parse diagnostics
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 57f0a74 commit d4a25a0

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

hyperbrowser/transport/base.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Mapping as MappingABC
12
from abc import ABC, abstractmethod
23
from typing import Generic, Mapping, Optional, Type, TypeVar, Union
34

@@ -16,12 +17,26 @@ def __init__(self, data: Optional[Union[dict, T]] = None, status_code: int = 200
1617
self.status_code = status_code
1718

1819
@classmethod
19-
def from_json(cls, json_data: dict, model: Type[T]) -> "APIResponse[T]":
20+
def from_json(
21+
cls, json_data: Mapping[str, object], model: Type[T]
22+
) -> "APIResponse[T]":
2023
"""Create an APIResponse from JSON data with a specific model."""
24+
model_name = getattr(model, "__name__", "response model")
25+
if not isinstance(json_data, MappingABC):
26+
actual_type_name = type(json_data).__name__
27+
raise HyperbrowserError(
28+
f"Failed to parse response data for {model_name}: "
29+
f"expected a mapping but received {actual_type_name}"
30+
)
2131
try:
2232
return cls(data=model(**json_data))
23-
except Exception as e:
24-
raise HyperbrowserError("Failed to parse response data", original_error=e)
33+
except HyperbrowserError:
34+
raise
35+
except Exception as exc:
36+
raise HyperbrowserError(
37+
f"Failed to parse response data for {model_name}",
38+
original_error=exc,
39+
) from exc
2540

2641
@classmethod
2742
def from_status(cls, status_code: int) -> "APIResponse[None]":

tests/test_transport_base.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import cast
2+
3+
import pytest
4+
from pydantic import BaseModel
5+
6+
from hyperbrowser.exceptions import HyperbrowserError
7+
from hyperbrowser.transport.base import APIResponse
8+
9+
10+
class _SampleResponseModel(BaseModel):
11+
name: str
12+
retries: int = 0
13+
14+
15+
class _RaisesHyperbrowserModel:
16+
def __init__(self, **kwargs):
17+
_ = kwargs
18+
raise HyperbrowserError("model validation failed")
19+
20+
21+
def test_api_response_from_json_parses_model_data() -> None:
22+
response = APIResponse.from_json(
23+
{"name": "job-1", "retries": 2}, _SampleResponseModel
24+
)
25+
26+
assert isinstance(response.data, _SampleResponseModel)
27+
assert response.status_code == 200
28+
assert response.data.name == "job-1"
29+
assert response.data.retries == 2
30+
31+
32+
def test_api_response_from_json_rejects_non_mapping_inputs() -> None:
33+
with pytest.raises(
34+
HyperbrowserError,
35+
match=(
36+
"Failed to parse response data for _SampleResponseModel: "
37+
"expected a mapping but received list"
38+
),
39+
):
40+
APIResponse.from_json(
41+
cast("dict[str, object]", ["not-a-mapping"]),
42+
_SampleResponseModel,
43+
)
44+
45+
46+
def test_api_response_from_json_wraps_non_hyperbrowser_errors() -> None:
47+
with pytest.raises(
48+
HyperbrowserError,
49+
match="Failed to parse response data for _SampleResponseModel",
50+
) as exc_info:
51+
APIResponse.from_json({"retries": 1}, _SampleResponseModel)
52+
53+
assert exc_info.value.original_error is not None
54+
55+
56+
def test_api_response_from_json_preserves_hyperbrowser_errors() -> None:
57+
with pytest.raises(HyperbrowserError, match="model validation failed") as exc_info:
58+
APIResponse.from_json({}, _RaisesHyperbrowserModel)
59+
60+
assert exc_info.value.original_error is None

0 commit comments

Comments
 (0)