Skip to content

Commit a8bd95d

Browse files
committed
Updated generic resource and tests
1 parent 546f0db commit a8bd95d

File tree

3 files changed

+61
-111
lines changed

3 files changed

+61
-111
lines changed

mpt_api_client/http/models.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ def next_offset(self) -> int:
3737

3838
@dataclass
3939
class Meta:
40-
"""Provides meta information about the pagination, ignored fields and the response."""
40+
"""Provides meta-information about the pagination, ignored fields and the response."""
4141

42+
response: Response
4243
pagination: Pagination = field(default_factory=Pagination)
4344
ignored: list[str] = field(default_factory=list)
44-
response: Response | None = None
4545

4646
@classmethod
4747
def from_response(cls, response: Response) -> Self:
4848
"""Creates a meta object from response."""
49-
meta_data = response.json().get("$meta")
49+
meta_data = response.json().get("$meta", {})
5050
if not isinstance(meta_data, dict):
5151
raise TypeError("Response $meta must be a dict.")
5252

@@ -57,30 +57,34 @@ def from_response(cls, response: Response) -> Self:
5757
)
5858

5959

60-
class GenericResource(Box):
60+
ResourceType = dict[str, Any] | None
61+
62+
63+
class GenericResource:
6164
"""Provides a base resource to interact with api data using fluent interfaces."""
65+
_data_key = "data"
66+
67+
def __init__(self, resource: ResourceType = None, meta: Meta | None = None) -> None:
68+
resource = resource or {}
69+
self.meta: Meta | None = meta
70+
self._resource: Box = Box(resource, camel_killer_box=True, default_box=False)
6271

63-
def __init__(self, *args: Any, **kwargs: Any) -> None:
64-
super().__init__(*args, **kwargs)
65-
self.__post_init__()
72+
def __getattr__(self, attribute: str) -> Box | Any:
73+
"""Returns the resource data."""
74+
return self._resource.__getattr__(attribute) # type: ignore[no-untyped-call]
6675

67-
def __post_init__(self) -> None:
68-
"""Initializes meta information."""
69-
meta = self.get("$meta", None) # type: ignore[no-untyped-call]
70-
if meta:
71-
self._meta = Meta(**meta)
76+
def to_dict(self) -> dict[str, Any]:
77+
"""Returns the resource as a dictionary."""
78+
return self._resource.to_dict()
7279

7380
@classmethod
7481
def from_response(cls, response: Response) -> Self:
7582
"""Creates a resource from a response.
7683
7784
Expected a Response with json data with two keys: data and $meta.
7885
"""
79-
response_data = response.json().get("data")
86+
response_data = response.json().get(cls._data_key)
8087
if not isinstance(response_data, dict):
8188
raise TypeError("Response data must be a dict.")
8289
meta = Meta.from_response(response)
83-
meta.response = response
84-
resource = cls(response_data)
85-
resource._meta = meta
86-
return resource
90+
return cls(response_data, meta)
Lines changed: 34 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import re
21

32
import pytest
43
from httpx import Response
@@ -11,100 +10,56 @@ def meta_data():
1110
return {"pagination": {"limit": 10, "offset": 20, "total": 100}, "ignored": ["one"]} # noqa: WPS226
1211

1312

14-
class TestGenericResource: # noqa: WPS214
15-
def test_generic_resource_empty(self):
16-
resource = GenericResource()
17-
with pytest.raises(AttributeError):
18-
_ = resource._meta
13+
def test_generic_resource_empty():
14+
resource = GenericResource()
15+
assert resource.meta is None
16+
assert resource.to_dict() == {}
1917

20-
def test_initialization_with_data(self):
21-
resource = GenericResource(name="test", value=123)
2218

23-
assert resource.name == "test"
24-
assert resource.value == 123
19+
def test_from_response(meta_data):
20+
record_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}}
21+
response = Response(200, json={"data": record_data, "$meta": meta_data})
22+
expected_meta = Meta.from_response(response)
2523

26-
def test_init(self, meta_data):
27-
resource = {"$meta": meta_data, "key": "value"} # noqa: WPS445 WPS517
28-
init_one = GenericResource(resource)
29-
init_two = GenericResource(**resource)
30-
assert init_one == init_two
24+
resource = GenericResource.from_response(response)
3125

32-
def test_generic_resource_meta_property_with_data(self, meta_data):
33-
resource = GenericResource({"$meta": meta_data})
34-
assert resource._meta == Meta(**meta_data)
26+
assert resource.to_dict() == record_data
27+
assert resource.meta == expected_meta
3528

36-
def test_generic_resource_box_functionality(self):
37-
resource = GenericResource(id=1, name="test_resource", nested={"key": "value"})
3829

39-
assert resource.id == 1
40-
assert resource.name == "test_resource"
41-
assert resource.nested.key == "value"
30+
def test_attribute_access():
31+
record_data = {"id": 1, "name": {"given": "Albert", "family": "Einstein"}}
32+
meta = Meta.from_response(Response(200, json={"$meta": {}}))
33+
resource = GenericResource(resource=record_data, meta=meta)
4234

43-
def test_with_both_meta_and_response(self, meta_data):
44-
response = Response(200, json={})
45-
meta_data["response"] = response
46-
meta_object = Meta(**meta_data)
35+
assert resource.meta == meta
4736

48-
resource = GenericResource(
49-
data="test_data",
50-
**{"$meta": meta_data}, # noqa: WPS445 WPS517
51-
)
37+
assert resource.id == 1
5238

53-
assert resource.data == "test_data"
54-
assert resource._meta == meta_object
39+
with pytest.raises(AttributeError, match=r"'Box' object has no attribute 'address'"):
40+
_ = resource.address # noqa: WPS122
5541

56-
def test_dynamic_attribute_access(self):
57-
resource = GenericResource()
42+
with pytest.raises(AttributeError, match=r"'Box' object has no attribute 'surname'"):
43+
_ = resource.name.surname # noqa: WPS122
5844

59-
resource.dynamic_field = "dynamic_value"
60-
resource.nested_object = {"inner": "data"}
45+
assert resource.name.given == "Albert"
46+
assert resource.name.to_dict() == record_data["name"]
6147

62-
assert resource.dynamic_field == "dynamic_value"
63-
assert resource.nested_object.inner == "data"
6448

49+
def test_wrong_data_type():
50+
with pytest.raises(TypeError, match=r"Response data must be a dict."):
51+
GenericResource.from_response(Response(200, json={"data": 1}))
6552

66-
class TestGenericResourceFromResponse:
67-
@pytest.fixture
68-
def meta_data_single(self):
69-
return {"ignored": ["one"]} # noqa: WPS226
7053

71-
@pytest.fixture
72-
def meta_data_two_resources(self):
73-
return {"pagination": {"limit": 10, "offset": 0, "total": 2}, "ignored": ["one"]} # noqa: WPS226
54+
class ChargeResourceMock(GenericResource):
55+
_data_key = "charge"
7456

75-
@pytest.fixture
76-
def meta_data_multiple(self):
77-
return {"ignored": ["one", "two"]} # noqa: WPS226
7857

79-
@pytest.fixture
80-
def single_resource_data(self):
81-
return {"id": 1, "name": "test"}
58+
def test_custom_data_key():
59+
record_data = {"id": 1, "amount": 100}
60+
response = Response(200, json={"charge": record_data})
8261

83-
@pytest.fixture
84-
def single_resource_response(self, single_resource_data, meta_data_single):
85-
return Response(200, json={"data": single_resource_data, "$meta": meta_data_single})
62+
resource = ChargeResourceMock.from_response(response)
8663

87-
@pytest.fixture
88-
def multiple_resource_response(self, single_resource_data, meta_data_two_resources):
89-
return Response(
90-
200,
91-
json={
92-
"data": [single_resource_data, single_resource_data],
93-
"$meta": meta_data_two_resources,
94-
},
95-
)
96-
97-
def test_malformed_meta_response(self):
98-
with pytest.raises(TypeError, match=re.escape("Response $meta must be a dict.")):
99-
_resource = GenericResource.from_response(Response(200, json={"data": {}, "$meta": 4}))
100-
101-
def test_single_resource(self, single_resource_response):
102-
resource = GenericResource.from_response(single_resource_response)
103-
assert resource.id == 1
104-
assert resource.name == "test"
105-
assert isinstance(resource._meta, Meta)
106-
assert resource._meta.response == single_resource_response
107-
108-
def test_two_resources(self, multiple_resource_response, single_resource_data):
109-
with pytest.raises(TypeError, match=r"Response data must be a dict."):
110-
_resource = GenericResource.from_response(multiple_resource_response)
64+
assert resource.id == 1
65+
assert resource.amount == 100

tests/http/models/test_meta.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,34 @@
55

66

77
class TestMeta:
8-
98
@pytest.fixture
109
def responses_fixture(self):
1110
response_data = {
1211
"$meta": {
1312
"ignored": ["ignored"],
14-
"pagination": {"limit": 25, "offset": 50, "total": 300}
15-
13+
"pagination": {"limit": 25, "offset": 50, "total": 300},
1614
}
1715
}
1816
return Response(status_code=200, json=response_data)
1917

2018
@pytest.fixture
2119
def invalid_response_fixture(self):
22-
response_data = {
23-
"$meta": "invalid_meta"
24-
}
20+
response_data = {"$meta": "invalid_meta"}
2521
return Response(status_code=200, json=response_data)
2622

27-
def test_meta_initialization_empty(self):
28-
meta = Meta()
29-
assert meta.pagination == Pagination(limit=0, offset=0, total=0)
30-
3123
def test_meta_from_response(self, responses_fixture):
3224
meta = Meta.from_response(responses_fixture)
3325

3426
assert isinstance(meta.pagination, Pagination)
35-
assert meta.pagination.limit == 25
36-
assert meta.pagination.offset == 50
37-
assert meta.pagination.total == 300
27+
assert meta.pagination == Pagination(limit=25, offset=50, total=300)
3828

3929
def test_invalid_meta_from_response(self, invalid_response_fixture):
40-
with pytest.raises(TypeError):
30+
with pytest.raises(TypeError, match=r"Response \$meta must be a dict."):
4131
Meta.from_response(invalid_response_fixture)
4232

4333
def test_meta_with_pagination_object(self):
34+
response = Response(status_code=200, json={})
4435
pagination = Pagination(limit=10, offset=0, total=100)
45-
meta = Meta(pagination=pagination)
36+
meta = Meta(response=response, pagination=pagination)
4637

4738
assert meta.pagination == Pagination(limit=10, offset=0, total=100)

0 commit comments

Comments
 (0)