From a3752a3d2b09867fabcf5e961cbef61ba47f654c Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Fri, 26 Apr 2024 22:34:01 +0300 Subject: [PATCH] Add: coverage 100%! --- README.md | 106 +++++++++++++++ docs/CHANGELOG.md | 2 +- examples/crud.py | 90 +++++++++++++ resty/managers/managers.py | 16 ++- resty/managers/types.py | 16 +++ resty/middlewares/status.py | 11 +- resty/types.py | 6 +- tests/managers/conftest.py | 14 +- tests/managers/test_manager.py | 233 ++++++++++++++++++++++++++++++++- 9 files changed, 479 insertions(+), 15 deletions(-) create mode 100644 examples/crud.py diff --git a/README.md b/README.md index a00c6a2..8b5416c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,112 @@ poetry add resty-client ## Getting-Started +See [examples](examples) for more. + +### Schemas + +```python +from resty.types import Schema + + +class UserCreateSchema(Schema): + username: str + email: str + password: str + age: int + + +class UserReadSchema(Schema): + id: int + username: str + email: str + age: int + + +class UserUpdateSchema(Schema): + username: str = None + email: str = None +``` + +### Manager + +```python +from resty.managers import Manager +from resty.enums import Endpoint, Field + + +class UserManager(Manager): + endpoints = { + Endpoint.CREATE: "users/", + Endpoint.READ: "users/", + Endpoint.READ_ONE: "users/{pk}", + Endpoint.UPDATE: "users/{pk}", + Endpoint.DELETE: "users/{pk}", + } + fields = { + Field.PRIMARY: "id", + } +``` + +### CRUD + +```python +import asyncio + +import httpx + +from resty.clients.httpx import RESTClient + + +async def main(): + client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) + + response = await UserManager.create( + client=client, + obj=UserCreateSchema( + username="admin", + email="admin@admin.com", + password="admin", + age=19, + ), + response_type=UserReadSchema, + ) + print(response) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.read( + client=client, + response_type=UserReadSchema, + ) + + for obj in response: + print(obj) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.read_one( + client=client, + obj_or_pk=1, + response_type=UserReadSchema, + ) + + print(response) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.update( + client=client, + obj=UserUpdateSchema(id=1, username="admin123", ), + response_type=UserReadSchema, + ) + + print(response) # id=1 username='admin123' email='admin@admin.com' age=19 + + await UserManager.delete( + client=client, + obj_or_pk=1, + expected_status=204, + ) + + +if __name__ == "__main__": + asyncio.run(main()) +``` ## Status diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a42f18e..e026815 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,4 +22,4 @@ - Improved test coverage to 100% - Improved architecture -- Expanded Readme docs & examples +- Added examples diff --git a/examples/crud.py b/examples/crud.py new file mode 100644 index 0000000..b257b78 --- /dev/null +++ b/examples/crud.py @@ -0,0 +1,90 @@ +import asyncio + +import httpx + +from resty.enums import Endpoint, Field +from resty.types import Schema +from resty.managers import Manager +from resty.clients.httpx import RESTClient + + +class UserCreateSchema(Schema): + username: str + email: str + password: str + age: int + + +class UserReadSchema(Schema): + id: int + username: str + email: str + age: int + + +class UserUpdateSchema(Schema): + username: str = None + email: str = None + + +class UserManager(Manager): + endpoints = { + Endpoint.CREATE: "users/", + Endpoint.READ: "users/", + Endpoint.READ_ONE: "users/{pk}", + Endpoint.UPDATE: "users/{pk}", + Endpoint.DELETE: "users/{pk}", + } + fields = { + Field.PRIMARY: "id", + } + + +async def main(): + client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) + + response = await UserManager.create( + client=client, + obj=UserCreateSchema( + username="admin", + email="admin@admin.com", + password="admin", + age=19, + ), + response_type=UserReadSchema, + ) + print(response) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.read( + client=client, + response_type=UserReadSchema, + ) + + for obj in response: + print(obj) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.read_one( + client=client, + obj_or_pk=1, + response_type=UserReadSchema, + ) + + print(response) # id=1 username='admin' email='admin@admin.com' age=19 + + response = await UserManager.update( + client=client, + obj=UserUpdateSchema(id=1, username="admin123", ), + response_type=UserReadSchema, + ) + + print(response) # id=1 username='admin123' email='admin@admin.com' age=19 + + await UserManager.delete( + client=client, + obj_or_pk=1, + expected_status=204, + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/resty/managers/managers.py b/resty/managers/managers.py index 3faa0b1..70029e0 100644 --- a/resty/managers/managers.py +++ b/resty/managers/managers.py @@ -55,6 +55,12 @@ def get_pk(cls, obj: Schema | Mapping) -> any: return getattr(obj, field, None) + @classmethod + def _get_pk(cls, obj_or_pk: any) -> any: + if isinstance(obj_or_pk, Mapping | Schema): + return cls.get_pk(obj=obj_or_pk) + return obj_or_pk + @classmethod def _deserialize(cls, schema: type[Schema], data: any, **kwargs): serializer = cls.get_serializer(**kwargs) @@ -114,6 +120,9 @@ def _prepare_request(cls, endpoint: Endpoint, **kwargs) -> Request: @classmethod def _handle_response(cls, response: Response, response_type: ResponseType, **kwargs) -> any: + if not response: + return + if inspect.isclass(response_type): if issubclass(response_type, dict | list | tuple | set): return response_type(response.json) @@ -144,9 +153,10 @@ async def read[T: Schema](cls, client: BaseRESTClient, response_type: ResponseTy @classmethod async def read_one[T: Schema](cls, client: BaseRESTClient, obj_or_pk: Schema | Mapping | any, response_type: ResponseType = None, **kwargs) -> T: + request = cls._prepare_request( endpoint=Endpoint.READ_ONE, - pk=kwargs.pop('pk', cls.get_pk(obj_or_pk)), + pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs ) response = await cls._make_request(client=client, request=request) @@ -156,7 +166,7 @@ async def read_one[T: Schema](cls, client: BaseRESTClient, obj_or_pk: Schema | M async def update[T: Schema](cls, client: BaseRESTClient, obj: Schema | Mapping, response_type: ResponseType = None, **kwargs) -> T | None: request = cls._prepare_request( - endpoint=Endpoint.READ_ONE, + endpoint=Endpoint.UPDATE, pk=kwargs.pop('pk', cls.get_pk(obj)), obj=obj, **kwargs @@ -170,7 +180,7 @@ async def delete[T: Schema](cls, client: BaseRESTClient, obj_or_pk: Schema | Map **kwargs) -> T | None: request = cls._prepare_request( endpoint=Endpoint.DELETE, - pk=kwargs.pop('pk', cls.get_pk(obj_or_pk)), + pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs ) response = await cls._make_request(client=client, request=request) diff --git a/resty/managers/types.py b/resty/managers/types.py index 8fe1c3e..19acd51 100644 --- a/resty/managers/types.py +++ b/resty/managers/types.py @@ -38,6 +38,22 @@ class BaseManager(ABC): serializer_class: BaseSerializer url_builder_class: BaseURLBuilder + @classmethod + @abstractmethod + def get_serializer(cls, **kwargs) -> type[BaseSerializer]: ... + + @classmethod + @abstractmethod + def get_method(cls, endpoint: Endpoint, **kwargs) -> Method: ... + + @classmethod + @abstractmethod + def get_field(cls, field: Field) -> str: ... + + @classmethod + @abstractmethod + def get_pk(cls, obj: Schema | Mapping) -> any: ... + @classmethod @abstractmethod async def create[T: Schema]( diff --git a/resty/middlewares/status.py b/resty/middlewares/status.py index 075438f..d4980ae 100644 --- a/resty/middlewares/status.py +++ b/resty/middlewares/status.py @@ -8,9 +8,9 @@ class StatusCheckingMiddleware(BaseResponseMiddleware): def __init__( - self, - errors: Mapping[int, type[Exception]] = None, - default_error: type[Exception] = HTTPError, + self, + errors: Mapping[int, type[Exception]] = None, + default_error: type[Exception] = HTTPError, ): self._errors = errors or STATUS_ERRORS self._default_error = default_error @@ -33,10 +33,9 @@ async def __call__(self, response: Response, **kwargs): actual_status = response.status expected_status = kwargs.pop( "expected_status", - { - 200, - }, + {200, 201}, ) + check_status = kwargs.pop("check_status", True) if check_status and not self._check_status(actual_status, expected_status): diff --git a/resty/types.py b/resty/types.py index 9b46710..e3567db 100644 --- a/resty/types.py +++ b/resty/types.py @@ -27,7 +27,7 @@ class Request: class Response: request: Request status: int - content: bytes - text: str - json: list | dict + content: bytes = None + text: str = None + json: list | dict = None middleware_options: dict = field(default_factory=dict) diff --git a/tests/managers/conftest.py b/tests/managers/conftest.py index 158b09b..e8c9477 100644 --- a/tests/managers/conftest.py +++ b/tests/managers/conftest.py @@ -1,3 +1,5 @@ +import pytest + from resty.clients import BaseRESTClient from resty.types import Request, Response @@ -9,6 +11,16 @@ def __init__(self, response: Response = None, **expected): async def request(self, request: Request) -> Response: for key, value in self.expected.items(): - assert getattr(request, key) != value + assert getattr(request, key) == value return self.response + + +@pytest.fixture +def client(request): # pragma: nocover + response, expected = request.param + + return RESTClientMock( + response=response, + **expected, + ) diff --git a/tests/managers/test_manager.py b/tests/managers/test_manager.py index 6896632..5a83fdb 100644 --- a/tests/managers/test_manager.py +++ b/tests/managers/test_manager.py @@ -1,7 +1,238 @@ +import pytest + +from resty.enums import Method, Endpoint, Field from resty.managers import Manager +from resty.types import Response, Request, Schema from tests.managers.conftest import RESTClientMock -def test_create(): +class UserCreate(Schema): + username: str + + +class UserRead(Schema): + id: int + username: str + + +class UserUpdate(Schema): + id: int + username: str + + +class UserManager(Manager): + endpoints = { + Endpoint.CREATE: "users/", + Endpoint.READ: "users/", + Endpoint.READ_ONE: "users/{pk}", + Endpoint.UPDATE: "users/{pk}", + Endpoint.DELETE: "users/{pk}", + + } + fields = { + Field.PRIMARY: 'id', + } + + +class UserManagerForURLBuilding(Manager): + endpoints = { + Endpoint.READ_ONE: "users/{pk}/{abc}", + } + fields = { + Field.PRIMARY: 'id', + } + + +class ManagerWithoutSerializer(Manager): + serializer_class = None + + +class ManagerWithInvalidSerializer(Manager): + serializer_class = 123 + + +class ManagerWithUnspecifiedMethods(Manager): + methods = {} + + +class ManagerWithUnspecifiedFields(Manager): pass + + +class ManagerWithPkField(Manager): + fields = { + Field.PRIMARY: 'id', + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("client, obj", [ + (RESTClientMock(json={"username": "test"}, method=Method.POST), UserCreate(username="test")), + (RESTClientMock(json={"username": "321"}, method=Method.POST), UserCreate(username="321")), + (RESTClientMock(json={"username": "test"}, method=Method.POST), {"username": "test"}), +]) +async def test_create(client, obj): + manager = UserManager() + + await manager.create(client=client, obj=obj) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("data", [ + ({"username": "test", "id": 1}, {"username": "test123", "id": 2}, {"username": "test321", "id": 3}), +]) +async def test_read(data): + client = RESTClientMock( + response=Response( + request=Request(url="", method=Method.GET), status=200, json=data, ), + method=Method.GET + ) + manager = UserManager() + + objs = await manager.read(client=client, response_type=UserRead) + + assert tuple(obj.model_dump() for obj in objs) == data + + +@pytest.mark.asyncio +@pytest.mark.parametrize("client, obj", [ + ( + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123} + ), method=Method.GET), + UserRead(username="test", id=123) + ), + +]) +async def test_read_one(client, obj): + manager = UserManager() + + assert await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) == obj + + +@pytest.mark.asyncio +@pytest.mark.parametrize("client, obj", [ + ( + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123} + ), method=Method.PATCH, url="users/123"), + UserUpdate(username="test", id=123) + ), + +]) +async def test_update(client, obj): + manager = UserManager() + + await manager.update(client=client, obj=obj) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("client, pk", [ + ( + RESTClientMock(url="users/123", method=Method.DELETE), + 123 + ), + ( + RESTClientMock(url="users/321", method=Method.DELETE), + UserRead(username="test", id=321) + ) + +]) +async def test_delete(client, pk): + manager = UserManager() + + await manager.delete(client, obj_or_pk=pk) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("client, pk, abc", [ + (RESTClientMock(url="users/123/hello"), 123, "hello"), + (RESTClientMock(url="users/321/world"), 321, "world"), +]) +async def test_url_building(client, pk, abc): + manager = UserManagerForURLBuilding() + + await manager.read_one(client=client, obj_or_pk=pk, abc=abc) + + +@pytest.mark.parametrize("manager", [ + ManagerWithoutSerializer, + ManagerWithInvalidSerializer, +]) +def test_invalid_or_unspec_serializer(manager): + manager = manager() + + with pytest.raises(RuntimeError): + manager.get_serializer() + + +def test_get_unspec_method(): + manager = ManagerWithUnspecifiedMethods() + + with pytest.raises(RuntimeError): + manager.get_method(Endpoint.CREATE) + + +def test_get_unspec_field(): + manager = ManagerWithUnspecifiedFields() + + with pytest.raises(RuntimeError): + manager.get_field(Field.PRIMARY) + + +def test_get_field(): + manager = ManagerWithPkField() + + assert manager.get_field(Field.PRIMARY) == 'id' + + +@pytest.mark.parametrize("obj, pk", [ + ({'id': "test"}, "test"), + (UserRead(id=321, username='123'), 321), +]) +def test_get_pk(obj, pk): + manager = ManagerWithPkField() + + assert manager.get_pk(obj) == pk + + +@pytest.mark.asyncio +@pytest.mark.parametrize("url", [ + "test", +]) +async def test_passing_url(url): + client = RESTClientMock(url=url) + manager = UserManagerForURLBuilding() + + await manager.delete(client, obj_or_pk=1, url=url) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("data, response_type, result", [ + ({"username": "test"}, dict, {"username": "test"}), + (("username", "test"), list, ["username", "test"]), + ({"username": "test", "id": 123}, lambda r: dict.keys(r.json), dict.keys({"username": "test", "id": 123})), + ({"username": "test", "id": 123}, lambda r, t: dict.keys(r), {"username": "test", "id": 123}) +]) +async def test_response_type(data, response_type, result): + client = RESTClientMock(response=Response( + request=Request( + url="", + method=Method.GET, + ), + status=200, + json=data + )) + + manager = UserManager() + + response = await manager.read(client=client, response_type=response_type, ) + + assert response == result