-
Notifications
You must be signed in to change notification settings - Fork 0
#MPT-12328 Single result resource #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import math | ||
| from dataclasses import dataclass, field | ||
| from typing import Any, Self | ||
|
|
||
| from box import Box | ||
| from httpx import Response | ||
|
|
||
|
|
||
| @dataclass | ||
| class Pagination: | ||
| """Provides pagination information.""" | ||
|
|
||
| limit: int = 0 | ||
| offset: int = 0 | ||
| total: int = 0 | ||
|
|
||
| def has_next(self) -> bool: | ||
| """Returns True if there is a next page.""" | ||
| return self.offset + self.limit < self.total | ||
|
|
||
| def num_page(self) -> int: | ||
| """Returns the current page number.""" | ||
| if self.limit == 0: | ||
| return 0 | ||
| return (self.offset // self.limit) + 1 | ||
|
|
||
| def total_pages(self) -> int: | ||
| """Returns the total number of pages.""" | ||
| if self.limit == 0: | ||
| return 0 | ||
| return math.ceil(self.total / self.limit) | ||
|
|
||
| def next_offset(self) -> int: | ||
| """Returns the next offset as an integer for the next page.""" | ||
| return self.offset + self.limit | ||
|
svazquezco marked this conversation as resolved.
|
||
|
|
||
|
|
||
| @dataclass | ||
| class Meta: | ||
| """Provides meta information about the pagination, ignored fields and the response.""" | ||
|
|
||
| pagination: Pagination = field(default_factory=Pagination) | ||
| ignored: list[str] = field(default_factory=list) | ||
| response: Response | None = None | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
|
|
||
| @classmethod | ||
| def from_response(cls, response: Response) -> Self: | ||
| """Creates a meta object from response.""" | ||
| meta_data = response.json().get("$meta") | ||
| if not isinstance(meta_data, dict): | ||
| raise TypeError("Response $meta must be a dict.") | ||
|
|
||
| return cls( | ||
| ignored=meta_data.get("ignored", []), | ||
| pagination=Pagination(**meta_data.get("pagination", {})), | ||
| response=response, | ||
| ) | ||
|
|
||
|
|
||
| class GenericResource(Box): | ||
| """Provides a base resource to interact with api data using fluent interfaces.""" | ||
|
|
||
|
albertsola marked this conversation as resolved.
|
||
| def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
| super().__init__(*args, **kwargs) | ||
| self.__post_init__() | ||
|
|
||
| def __post_init__(self) -> None: | ||
| """Initializes meta information.""" | ||
| meta = self.get("$meta", None) # type: ignore[no-untyped-call] | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
| if meta: | ||
| self._meta = Meta(**meta) | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
|
|
||
| @classmethod | ||
| def from_response(cls, response: Response) -> Self: | ||
| """Creates a resource from a response. | ||
|
|
||
| Expected a Response with json data with two keys: data and $meta. | ||
| """ | ||
| response_data = response.json().get("data") | ||
| if not isinstance(response_data, dict): | ||
| raise TypeError("Response data must be a dict.") | ||
| meta = Meta.from_response(response) | ||
| meta.response = response | ||
| resource = cls(response_data) | ||
| resource._meta = meta | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
| return resource | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import re | ||
|
|
||
| import pytest | ||
| from httpx import Response | ||
|
|
||
| from mpt_api_client.http.models import GenericResource, Meta | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def meta_data(): | ||
| return {"pagination": {"limit": 10, "offset": 20, "total": 100}, "ignored": ["one"]} # noqa: WPS226 | ||
|
|
||
|
|
||
| class TestGenericResource: # noqa: WPS214 | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
| def test_generic_resource_empty(self): | ||
| resource = GenericResource() | ||
| with pytest.raises(AttributeError): | ||
| _ = resource._meta | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
|
|
||
| def test_initialization_with_data(self): | ||
| resource = GenericResource(name="test", value=123) | ||
|
|
||
| assert resource.name == "test" | ||
| assert resource.value == 123 | ||
|
|
||
| def test_init(self, meta_data): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you need to check the |
||
| resource = {"$meta": meta_data, "key": "value"} # noqa: WPS445 WPS517 | ||
| init_one = GenericResource(resource) | ||
| init_two = GenericResource(**resource) | ||
| assert init_one == init_two | ||
|
|
||
| def test_generic_resource_meta_property_with_data(self, meta_data): | ||
| resource = GenericResource({"$meta": meta_data}) | ||
| assert resource._meta == Meta(**meta_data) | ||
|
|
||
| def test_generic_resource_box_functionality(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really I didn't get why we should check here library functionality? |
||
| resource = GenericResource(id=1, name="test_resource", nested={"key": "value"}) | ||
|
|
||
| assert resource.id == 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Three separate test cases, should be done either with 3 different tests or with parametrized |
||
| assert resource.name == "test_resource" | ||
| assert resource.nested.key == "value" | ||
|
|
||
| def test_with_both_meta_and_response(self, meta_data): | ||
| response = Response(200, json={}) | ||
| meta_data["response"] = response | ||
| meta_object = Meta(**meta_data) | ||
|
|
||
| resource = GenericResource( | ||
| data="test_data", | ||
| **{"$meta": meta_data}, # noqa: WPS445 WPS517 | ||
| ) | ||
|
|
||
| assert resource.data == "test_data" | ||
| assert resource._meta == meta_object | ||
|
|
||
| def test_dynamic_attribute_access(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, that's library functionality :-) |
||
| resource = GenericResource() | ||
|
|
||
| resource.dynamic_field = "dynamic_value" | ||
| resource.nested_object = {"inner": "data"} | ||
|
|
||
| assert resource.dynamic_field == "dynamic_value" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two separate test cases |
||
| assert resource.nested_object.inner == "data" | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| class TestGenericResourceFromResponse: | ||
| @pytest.fixture | ||
| def meta_data_single(self): | ||
| return {"ignored": ["one"]} # noqa: WPS226 | ||
|
|
||
| @pytest.fixture | ||
| def meta_data_two_resources(self): | ||
| return {"pagination": {"limit": 10, "offset": 0, "total": 2}, "ignored": ["one"]} # noqa: WPS226 | ||
|
|
||
| @pytest.fixture | ||
| def meta_data_multiple(self): | ||
| return {"ignored": ["one", "two"]} # noqa: WPS226 | ||
|
|
||
| @pytest.fixture | ||
| def single_resource_data(self): | ||
| return {"id": 1, "name": "test"} | ||
|
|
||
| @pytest.fixture | ||
| def single_resource_response(self, single_resource_data, meta_data_single): | ||
| return Response(200, json={"data": single_resource_data, "$meta": meta_data_single}) | ||
|
|
||
| @pytest.fixture | ||
| def multiple_resource_response(self, single_resource_data, meta_data_two_resources): | ||
| return Response( | ||
| 200, | ||
| json={ | ||
| "data": [single_resource_data, single_resource_data], | ||
| "$meta": meta_data_two_resources, | ||
| }, | ||
| ) | ||
|
|
||
| def test_malformed_meta_response(self): | ||
| with pytest.raises(TypeError, match=re.escape("Response $meta must be a dict.")): | ||
| _resource = GenericResource.from_response(Response(200, json={"data": {}, "$meta": 4})) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just |
||
|
|
||
| def test_single_resource(self, single_resource_response): | ||
| resource = GenericResource.from_response(single_resource_response) | ||
| assert resource.id == 1 | ||
| assert resource.name == "test" | ||
| assert isinstance(resource._meta, Meta) | ||
| assert resource._meta.response == single_resource_response | ||
|
|
||
| def test_two_resources(self, multiple_resource_response, single_resource_data): | ||
| with pytest.raises(TypeError, match=r"Response data must be a dict."): | ||
| _resource = GenericResource.from_response(multiple_resource_response) | ||
|
albertsola marked this conversation as resolved.
Outdated
albertsola marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import pytest | ||
| from httpx import Response | ||
|
|
||
| from mpt_api_client.http.models import Meta, Pagination | ||
|
|
||
|
|
||
| class TestMeta: | ||
|
|
||
| @pytest.fixture | ||
| def responses_fixture(self): | ||
| response_data = { | ||
| "$meta": { | ||
| "ignored": ["ignored"], | ||
| "pagination": {"limit": 25, "offset": 50, "total": 300} | ||
|
|
||
| } | ||
| } | ||
| return Response(status_code=200, json=response_data) | ||
|
|
||
| @pytest.fixture | ||
| def invalid_response_fixture(self): | ||
| response_data = { | ||
| "$meta": "invalid_meta" | ||
| } | ||
| return Response(status_code=200, json=response_data) | ||
|
|
||
| def test_meta_initialization_empty(self): | ||
| meta = Meta() | ||
| assert meta.pagination == Pagination(limit=0, offset=0, total=0) | ||
|
|
||
| def test_meta_from_response(self, responses_fixture): | ||
| meta = Meta.from_response(responses_fixture) | ||
|
|
||
| assert isinstance(meta.pagination, Pagination) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? |
||
| assert meta.pagination.limit == 25 | ||
| assert meta.pagination.offset == 50 | ||
| assert meta.pagination.total == 300 | ||
|
|
||
| def test_invalid_meta_from_response(self, invalid_response_fixture): | ||
| with pytest.raises(TypeError): | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
| Meta.from_response(invalid_response_fixture) | ||
|
|
||
| def test_meta_with_pagination_object(self): | ||
| pagination = Pagination(limit=10, offset=0, total=100) | ||
| meta = Meta(pagination=pagination) | ||
|
|
||
| assert meta.pagination == Pagination(limit=10, offset=0, total=100) | ||
|
albertsola marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.