From 75a5e412d29a41af0ea0578e7082b72dbf56f27b Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 11:47:55 +0300 Subject: [PATCH 1/6] Reformatted --- README.md | 12 +- examples/crud.py | 17 ++- resty/managers/builders.py | 11 +- resty/managers/managers.py | 127 ++++++++++++------ resty/middlewares/status.py | 6 +- tests/managers/conftest.py | 2 +- tests/managers/test_manager.py | 204 +++++++++++++++++++---------- tests/managers/test_url_builder.py | 66 ++++++++-- 8 files changed, 298 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 8b5416c..fc8e6e4 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,9 @@ from resty.clients.httpx import RESTClient async def main(): client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) - response = await UserManager.create( + manager = UserManager() + + response = await manager.create( client=client, obj=UserCreateSchema( username="admin", @@ -111,7 +113,7 @@ async def main(): ) print(response) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.read( + response = await manager.read( client=client, response_type=UserReadSchema, ) @@ -119,7 +121,7 @@ async def main(): for obj in response: print(obj) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.read_one( + response = await manager.read_one( client=client, obj_or_pk=1, response_type=UserReadSchema, @@ -127,7 +129,7 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.update( + response = await manager.update( client=client, obj=UserUpdateSchema(id=1, username="admin123", ), response_type=UserReadSchema, @@ -135,7 +137,7 @@ async def main(): print(response) # id=1 username='admin123' email='admin@admin.com' age=19 - await UserManager.delete( + await manager.delete( client=client, obj_or_pk=1, expected_status=204, diff --git a/examples/crud.py b/examples/crud.py index b257b78..bb6e927 100644 --- a/examples/crud.py +++ b/examples/crud.py @@ -43,7 +43,9 @@ class UserManager(Manager): async def main(): client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) - response = await UserManager.create( + manager = UserManager() + + response = await manager.create( client=client, obj=UserCreateSchema( username="admin", @@ -55,7 +57,7 @@ async def main(): ) print(response) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.read( + response = await manager.read( client=client, response_type=UserReadSchema, ) @@ -63,7 +65,7 @@ async def main(): for obj in response: print(obj) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.read_one( + response = await manager.read_one( client=client, obj_or_pk=1, response_type=UserReadSchema, @@ -71,15 +73,18 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 - response = await UserManager.update( + response = await manager.update( client=client, - obj=UserUpdateSchema(id=1, username="admin123", ), + obj=UserUpdateSchema( + id=1, + username="admin123", + ), response_type=UserReadSchema, ) print(response) # id=1 username='admin123' email='admin@admin.com' age=19 - await UserManager.delete( + await manager.delete( client=client, obj_or_pk=1, expected_status=204, diff --git a/resty/managers/builders.py b/resty/managers/builders.py index 7f3ff57..2b7abf6 100644 --- a/resty/managers/builders.py +++ b/resty/managers/builders.py @@ -25,22 +25,19 @@ def _normalize_url(cls, url: str | None) -> str: if not url: return "" - if not url.endswith('/'): - return url + '/' + if not url.endswith("/"): + return url + "/" return url @classmethod def build( - cls, endpoints: Endpoints, endpoint: Endpoint, base_url: str = None, **kwargs + cls, endpoints: Endpoints, endpoint: Endpoint, base_url: str = None, **kwargs ) -> str: endpoint_url = cls._get_endpoint_url(endpoints=endpoints, endpoint=endpoint) if endpoint_url: - url = urljoin( - cls._normalize_url(url=base_url), - endpoint_url - ) + url = urljoin(cls._normalize_url(url=base_url), endpoint_url) else: url = base_url or "" diff --git a/resty/managers/managers.py b/resty/managers/managers.py index 70029e0..03108a2 100644 --- a/resty/managers/managers.py +++ b/resty/managers/managers.py @@ -15,17 +15,17 @@ class Manager(BaseManager): @classmethod def get_serializer(cls, **kwargs) -> type[BaseSerializer]: - serializer = kwargs.pop('serializer', cls.serializer_class) + serializer = kwargs.pop("serializer", cls.serializer_class) if not serializer: - raise RuntimeError('Serializer not specified') + raise RuntimeError("Serializer not specified") if not ( - isinstance(serializer, BaseSerializer) - or inspect.isclass(serializer) - and issubclass(serializer, BaseSerializer) + isinstance(serializer, BaseSerializer) + or inspect.isclass(serializer) + and issubclass(serializer, BaseSerializer) ): - raise RuntimeError('The serializer must be a subclass of BaseSerializer') + raise RuntimeError("The serializer must be a subclass of BaseSerializer") return serializer @@ -33,7 +33,7 @@ def get_serializer(cls, **kwargs) -> type[BaseSerializer]: def get_method(cls, endpoint: Endpoint, **kwargs) -> Method: method = cls.methods.get(endpoint) if not method: - raise RuntimeError(f'Method not specified for endpoint: {endpoint}') + raise RuntimeError(f"Method not specified for endpoint: {endpoint}") return method @@ -42,7 +42,7 @@ def get_field(cls, field: Field) -> str: field = cls.fields.get(field) if not field: - raise RuntimeError(f'Field not specified: {field}') + raise RuntimeError(f"Field not specified: {field}") return field @@ -79,8 +79,8 @@ async def _make_request(cls, client: BaseRESTClient, request: Request) -> Respon @classmethod def _prepare_url(cls, endpoint: Endpoint, **kwargs) -> str: - url = kwargs.pop('url', None) - base_url = kwargs.pop('base_url', None) + url = kwargs.pop("url", None) + base_url = kwargs.pop("base_url", None) if isinstance(url, str): return url @@ -89,12 +89,12 @@ def _prepare_url(cls, endpoint: Endpoint, **kwargs) -> str: endpoints=cls.endpoints, endpoint=endpoint, base_url=base_url or cls.url, - **kwargs + **kwargs, ) @classmethod def _prepare_json(cls, **kwargs): - obj = kwargs.pop('obj', None) + obj = kwargs.pop("obj", None) if isinstance(obj, dict | list | set | tuple): return obj @@ -108,18 +108,20 @@ def _prepare_request(cls, endpoint: Endpoint, **kwargs) -> Request: url=cls._prepare_url(endpoint=endpoint, **kwargs), method=cls.get_method(endpoint, **kwargs), endpoint=endpoint, - data=kwargs.pop('data', {}), + data=kwargs.pop("data", {}), json=cls._prepare_json(**kwargs), - timeout=kwargs.pop('timeout', None), - params=kwargs.pop('params', {}), - headers=kwargs.pop('headers', {}), - cookies=kwargs.pop('cookies', {}), - redirects=kwargs.pop('redirects', False), + timeout=kwargs.pop("timeout", None), + params=kwargs.pop("params", {}), + headers=kwargs.pop("headers", {}), + cookies=kwargs.pop("cookies", {}), + redirects=kwargs.pop("redirects", False), middleware_options=kwargs.copy(), ) @classmethod - def _handle_response(cls, response: Response, response_type: ResponseType, **kwargs) -> any: + def _handle_response( + cls, response: Response, response_type: ResponseType, **kwargs + ) -> any: if not response: return @@ -127,7 +129,9 @@ def _handle_response(cls, response: Response, response_type: ResponseType, **kwa if issubclass(response_type, dict | list | tuple | set): return response_type(response.json) elif issubclass(response_type, Schema): - return cls._deserialize(schema=response_type, data=response.json, **kwargs) + return cls._deserialize( + schema=response_type, data=response.json, **kwargs + ) if callable(response_type): try: @@ -138,50 +142,87 @@ def _handle_response(cls, response: Response, response_type: ResponseType, **kwa return response.json @classmethod - async def create[T: Schema](cls, client: BaseRESTClient, obj: Schema | Mapping, response_type: ResponseType = None, - **kwargs) -> T | None: + async def create[ + T: Schema + ]( + cls, + client: BaseRESTClient, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, + ) -> (T | None): request = cls._prepare_request(endpoint=Endpoint.CREATE, obj=obj, **kwargs) response = await cls._make_request(client=client, request=request) - return cls._handle_response(response=response, response_type=response_type, **kwargs) + return cls._handle_response( + response=response, response_type=response_type, **kwargs + ) @classmethod - async def read[T: Schema](cls, client: BaseRESTClient, response_type: ResponseType = None, **kwargs) -> Iterable[T]: + async def read[ + T: Schema + ]( + cls, client: BaseRESTClient, response_type: ResponseType = None, **kwargs + ) -> Iterable[T]: request = cls._prepare_request(endpoint=Endpoint.READ, **kwargs) response = await cls._make_request(client=client, request=request) - return cls._handle_response(response=response, response_type=response_type, **kwargs) + return cls._handle_response( + response=response, response_type=response_type, **kwargs + ) @classmethod - async def read_one[T: Schema](cls, client: BaseRESTClient, obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, **kwargs) -> T: + 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=cls._get_pk(obj_or_pk=obj_or_pk), - **kwargs + endpoint=Endpoint.READ_ONE, pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs ) response = await cls._make_request(client=client, request=request) - return cls._handle_response(response=response, response_type=response_type, **kwargs) + return cls._handle_response( + response=response, response_type=response_type, **kwargs + ) @classmethod - async def update[T: Schema](cls, client: BaseRESTClient, obj: Schema | Mapping, response_type: ResponseType = None, - **kwargs) -> T | None: + async def update[ + T: Schema + ]( + cls, + client: BaseRESTClient, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, + ) -> (T | None): request = cls._prepare_request( endpoint=Endpoint.UPDATE, - pk=kwargs.pop('pk', cls.get_pk(obj)), + pk=kwargs.pop("pk", cls.get_pk(obj)), obj=obj, - **kwargs + **kwargs, ) response = await cls._make_request(client=client, request=request) - return cls._handle_response(response=response, response_type=response_type, **kwargs) + return cls._handle_response( + response=response, response_type=response_type, **kwargs + ) @classmethod - async def delete[T: Schema](cls, client: BaseRESTClient, obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, - **kwargs) -> T | None: + async def delete[ + T: Schema + ]( + cls, + client: BaseRESTClient, + obj_or_pk: Schema | Mapping | any, + response_type: ResponseType = None, + **kwargs, + ) -> (T | None): request = cls._prepare_request( - endpoint=Endpoint.DELETE, - pk=cls._get_pk(obj_or_pk=obj_or_pk), - **kwargs + endpoint=Endpoint.DELETE, pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs ) response = await cls._make_request(client=client, request=request) - return cls._handle_response(response=response, response_type=response_type, **kwargs) + return cls._handle_response( + response=response, response_type=response_type, **kwargs + ) diff --git a/resty/middlewares/status.py b/resty/middlewares/status.py index d4980ae..18078f1 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 diff --git a/tests/managers/conftest.py b/tests/managers/conftest.py index e8c9477..168168a 100644 --- a/tests/managers/conftest.py +++ b/tests/managers/conftest.py @@ -17,7 +17,7 @@ async def request(self, request: Request) -> Response: @pytest.fixture -def client(request): # pragma: nocover +def client(request): # pragma: nocover response, expected = request.param return RESTClientMock( diff --git a/tests/managers/test_manager.py b/tests/managers/test_manager.py index 5a83fdb..b66f60b 100644 --- a/tests/managers/test_manager.py +++ b/tests/managers/test_manager.py @@ -28,10 +28,9 @@ class UserManager(Manager): Endpoint.READ_ONE: "users/{pk}", Endpoint.UPDATE: "users/{pk}", Endpoint.DELETE: "users/{pk}", - } fields = { - Field.PRIMARY: 'id', + Field.PRIMARY: "id", } @@ -40,7 +39,7 @@ class UserManagerForURLBuilding(Manager): Endpoint.READ_ONE: "users/{pk}/{abc}", } fields = { - Field.PRIMARY: 'id', + Field.PRIMARY: "id", } @@ -62,16 +61,28 @@ class ManagerWithUnspecifiedFields(Manager): class ManagerWithPkField(Manager): fields = { - Field.PRIMARY: 'id', + 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"}), -]) +@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() @@ -79,14 +90,24 @@ async def test_create(client, obj): @pytest.mark.asyncio -@pytest.mark.parametrize("data", [ - ({"username": "test", "id": 1}, {"username": "test123", "id": 2}, {"username": "test321", "id": 3}), -]) +@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 + request=Request(url="", method=Method.GET), + status=200, + json=data, + ), + method=Method.GET, ) manager = UserManager() @@ -96,37 +117,49 @@ async def test_read(data): @pytest.mark.asyncio -@pytest.mark.parametrize("client, obj", [ - ( +@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) - ), - -]) + 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 + assert ( + await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) + == obj + ) @pytest.mark.asyncio -@pytest.mark.parametrize("client, obj", [ - ( +@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) - ), - -]) + json={"username": "test", "id": 123}, + ), + method=Method.PATCH, + url="users/123", + ), + UserUpdate(username="test", id=123), + ), + ], +) async def test_update(client, obj): manager = UserManager() @@ -134,17 +167,16 @@ async def test_update(client, obj): @pytest.mark.asyncio -@pytest.mark.parametrize("client, pk", [ - ( - RESTClientMock(url="users/123", method=Method.DELETE), - 123 - ), - ( +@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) - ) - -]) + UserRead(username="test", id=321), + ), + ], +) async def test_delete(client, pk): manager = UserManager() @@ -152,20 +184,26 @@ async def test_delete(client, pk): @pytest.mark.asyncio -@pytest.mark.parametrize("client, pk, abc", [ - (RESTClientMock(url="users/123/hello"), 123, "hello"), - (RESTClientMock(url="users/321/world"), 321, "world"), -]) +@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, -]) +@pytest.mark.parametrize( + "manager", + [ + ManagerWithoutSerializer, + ManagerWithInvalidSerializer, + ], +) def test_invalid_or_unspec_serializer(manager): manager = manager() @@ -190,13 +228,16 @@ def test_get_unspec_field(): def test_get_field(): manager = ManagerWithPkField() - assert manager.get_field(Field.PRIMARY) == 'id' + assert manager.get_field(Field.PRIMARY) == "id" -@pytest.mark.parametrize("obj, pk", [ - ({'id': "test"}, "test"), - (UserRead(id=321, username='123'), 321), -]) +@pytest.mark.parametrize( + "obj, pk", + [ + ({"id": "test"}, "test"), + (UserRead(id=321, username="123"), 321), + ], +) def test_get_pk(obj, pk): manager = ManagerWithPkField() @@ -204,9 +245,12 @@ def test_get_pk(obj, pk): @pytest.mark.asyncio -@pytest.mark.parametrize("url", [ - "test", -]) +@pytest.mark.parametrize( + "url", + [ + "test", + ], +) async def test_passing_url(url): client = RESTClientMock(url=url) manager = UserManagerForURLBuilding() @@ -215,24 +259,40 @@ async def test_passing_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, +@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}, ), - status=200, - json=data - )) + ], +) +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, ) + response = await manager.read( + client=client, + response_type=response_type, + ) assert response == result diff --git a/tests/managers/test_url_builder.py b/tests/managers/test_url_builder.py index 20ad907..346ca78 100644 --- a/tests/managers/test_url_builder.py +++ b/tests/managers/test_url_builder.py @@ -5,19 +5,65 @@ from resty.enums import Endpoint -@pytest.mark.parametrize('endpoints, endpoint, base, kwargs, expected', [ - ({Endpoint.CREATE: "users/", }, Endpoint.CREATE, "base/", {}, "base/users/"), - ({Endpoint.BASE: "users/", }, Endpoint.CREATE, "base/", {}, "base/users/"), - ({Endpoint.BASE: "users/", }, Endpoint.CREATE, "base", {}, "base/users/"), - ({Endpoint.BASE: "users/", }, Endpoint.CREATE, None, {}, "users/"), - ({Endpoint.BASE: "users/{pk}", }, Endpoint.CREATE, None, {"pk": 123}, "users/123"), - ({}, Endpoint.CREATE, "base/{pk}", {"pk": 123}, "base/123"), - ({}, Endpoint.CREATE, None, {"pk": 123}, ""), -]) +@pytest.mark.parametrize( + "endpoints, endpoint, base, kwargs, expected", + [ + ( + { + Endpoint.CREATE: "users/", + }, + Endpoint.CREATE, + "base/", + {}, + "base/users/", + ), + ( + { + Endpoint.BASE: "users/", + }, + Endpoint.CREATE, + "base/", + {}, + "base/users/", + ), + ( + { + Endpoint.BASE: "users/", + }, + Endpoint.CREATE, + "base", + {}, + "base/users/", + ), + ( + { + Endpoint.BASE: "users/", + }, + Endpoint.CREATE, + None, + {}, + "users/", + ), + ( + { + Endpoint.BASE: "users/{pk}", + }, + Endpoint.CREATE, + None, + {"pk": 123}, + "users/123", + ), + ({}, Endpoint.CREATE, "base/{pk}", {"pk": 123}, "base/123"), + ({}, Endpoint.CREATE, None, {"pk": 123}, ""), + ], +) def test_build(endpoints, endpoint, base, kwargs, expected): builder = URLBuilder() - assert builder.build(endpoints=endpoints, endpoint=endpoint, base_url=base, **kwargs) == expected + assert ( + builder.build(endpoints=endpoints, endpoint=endpoint, base_url=base, **kwargs) + == expected + ) def test_build_missing_kwargs(): From 1c3e9a06f419630a2c005033465b697296411304 Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 12:13:29 +0300 Subject: [PATCH 2/6] Update: manager api --- README.md | 13 ++- docs/CHANGELOG.md | 29 +++++ examples/crud.py | 7 +- pyproject.toml | 2 +- resty/__version__.py | 2 +- resty/managers/managers.py | 190 ++++++++++++++++----------------- resty/managers/types.py | 32 ++---- tests/managers/test_manager.py | 95 ++++++++++------- 8 files changed, 197 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index fc8e6e4..9c5db1f 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,9 @@ from resty.clients.httpx import RESTClient async def main(): client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) - manager = UserManager() + manager = UserManager(client=client) response = await manager.create( - client=client, obj=UserCreateSchema( username="admin", email="admin@admin.com", @@ -114,7 +113,6 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.read( - client=client, response_type=UserReadSchema, ) @@ -122,7 +120,6 @@ async def main(): print(obj) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.read_one( - client=client, obj_or_pk=1, response_type=UserReadSchema, ) @@ -130,20 +127,22 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.update( - client=client, - obj=UserUpdateSchema(id=1, username="admin123", ), + obj=UserUpdateSchema( + id=1, + username="admin123", + ), response_type=UserReadSchema, ) print(response) # id=1 username='admin123' email='admin@admin.com' age=19 await manager.delete( - client=client, obj_or_pk=1, expected_status=204, ) + if __name__ == "__main__": asyncio.run(main()) ``` diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e026815..6d3f36e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,3 +23,32 @@ - Improved test coverage to 100% - Improved architecture - Added examples + +## v0.0.6 + +- Change Manager API: + +Now instantiating manager is required. + +You can pass the REST Client into the constructor: + +```python +manager = UserManager(client=client) + +response = await manager.read( + response_type=UserReadSchema, +) +``` + +or specify the client explicitly during calling: + +```python +manager = UserManager() + +response = await manager.read( + response_type=UserReadSchema, + client=client, +) +``` + + diff --git a/examples/crud.py b/examples/crud.py index bb6e927..bb3170d 100644 --- a/examples/crud.py +++ b/examples/crud.py @@ -43,10 +43,9 @@ class UserManager(Manager): async def main(): client = RESTClient(httpx.AsyncClient(base_url="https://localhost:8000")) - manager = UserManager() + manager = UserManager(client=client) response = await manager.create( - client=client, obj=UserCreateSchema( username="admin", email="admin@admin.com", @@ -58,7 +57,6 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.read( - client=client, response_type=UserReadSchema, ) @@ -66,7 +64,6 @@ async def main(): print(obj) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.read_one( - client=client, obj_or_pk=1, response_type=UserReadSchema, ) @@ -74,7 +71,6 @@ async def main(): print(response) # id=1 username='admin' email='admin@admin.com' age=19 response = await manager.update( - client=client, obj=UserUpdateSchema( id=1, username="admin123", @@ -85,7 +81,6 @@ async def main(): print(response) # id=1 username='admin123' email='admin@admin.com' age=19 await manager.delete( - client=client, obj_or_pk=1, expected_status=204, ) diff --git a/pyproject.toml b/pyproject.toml index 4a4f505..188c5a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "resty-client" -version = "0.0.5" +version = "0.0.6" description = "RestyClient is a simple, easy-to-use Python library for interacting with REST APIs using Pydantic's powerful data validation and deserialization tools." authors = ["CrazyProger1 "] license = "MIT" diff --git a/resty/__version__.py b/resty/__version__.py index 90300ba..5937e1a 100644 --- a/resty/__version__.py +++ b/resty/__version__.py @@ -1,4 +1,4 @@ __title__ = "Resty-Client" -__version__ = "0.0.5" +__version__ = "0.0.6" __description__ = """RestyClient is a simple, easy-to-use Python library for interacting with REST APIs using Pydantic's powerful data validation and deserialization tools.""" diff --git a/resty/managers/managers.py b/resty/managers/managers.py index 03108a2..0642d49 100644 --- a/resty/managers/managers.py +++ b/resty/managers/managers.py @@ -1,5 +1,5 @@ import inspect -from typing import Mapping, Iterable, Callable +from typing import Mapping, Iterable from resty.clients import BaseRESTClient from resty.enums import Endpoint, Method, Field @@ -13,103 +13,109 @@ class Manager(BaseManager): serializer_class = Serializer url_builder_class = URLBuilder - @classmethod - def get_serializer(cls, **kwargs) -> type[BaseSerializer]: - serializer = kwargs.pop("serializer", cls.serializer_class) + def __init__(self, client: BaseRESTClient = None): + self._client = client + + def get_serializer(self, **kwargs) -> type[BaseSerializer]: + serializer = kwargs.pop("serializer", self.serializer_class) if not serializer: raise RuntimeError("Serializer not specified") if not ( - isinstance(serializer, BaseSerializer) - or inspect.isclass(serializer) - and issubclass(serializer, BaseSerializer) + isinstance(serializer, BaseSerializer) + or inspect.isclass(serializer) + and issubclass(serializer, BaseSerializer) ): raise RuntimeError("The serializer must be a subclass of BaseSerializer") return serializer - @classmethod - def get_method(cls, endpoint: Endpoint, **kwargs) -> Method: - method = cls.methods.get(endpoint) + def get_method(self, endpoint: Endpoint, **kwargs) -> Method: + method = self.methods.get(endpoint) if not method: raise RuntimeError(f"Method not specified for endpoint: {endpoint}") return method - @classmethod - def get_field(cls, field: Field) -> str: - field = cls.fields.get(field) + def get_field(self, field: Field) -> str: + field = self.fields.get(field) if not field: raise RuntimeError(f"Field not specified: {field}") return field - @classmethod - def get_pk(cls, obj: Schema | Mapping) -> any: - field = cls.get_field(Field.PRIMARY) + def get_pk(self, obj: Schema | Mapping) -> any: + field = self.get_field(Field.PRIMARY) if isinstance(obj, Mapping): return obj.get(field) return getattr(obj, field, None) - @classmethod - def _get_pk(cls, obj_or_pk: any) -> any: + def _get_pk(self, obj_or_pk: any) -> any: if isinstance(obj_or_pk, Mapping | Schema): - return cls.get_pk(obj=obj_or_pk) + return self.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) + def _deserialize(self, schema: type[Schema], data: any, **kwargs): + serializer = self.get_serializer(**kwargs) if isinstance(data, Mapping): return serializer.deserialize(schema=schema, data=data, **kwargs) return serializer.deserialize_many(schema=schema, data=data, **kwargs) - @classmethod - def _serialize(cls, obj: Schema, **kwargs): - serializer = cls.get_serializer(**kwargs) + def _serialize(self, obj: Schema, **kwargs): + serializer = self.get_serializer(**kwargs) return serializer.serialize(obj=obj, **kwargs) - @classmethod - async def _make_request(cls, client: BaseRESTClient, request: Request) -> Response: + def _get_client(self, **kwargs) -> BaseRESTClient: + client = kwargs.pop("client", self._client) + + if not client: + raise TypeError( + "REST Client not specified. Pass it to the constructor or via kwargs" + ) + + if not isinstance(client, BaseRESTClient): + raise TypeError("Client must inherit from BaseRESTClient") + + return client + + async def _make_request(self, request: Request, **kwargs) -> Response: + client = self._get_client(**kwargs) return await client.request(request=request) - @classmethod - def _prepare_url(cls, endpoint: Endpoint, **kwargs) -> str: + def _prepare_url(self, endpoint: Endpoint, **kwargs) -> str: url = kwargs.pop("url", None) base_url = kwargs.pop("base_url", None) if isinstance(url, str): return url - return cls.url_builder_class.build( - endpoints=cls.endpoints, + return self.url_builder_class.build( + endpoints=self.endpoints, endpoint=endpoint, - base_url=base_url or cls.url, + base_url=base_url or self.url, **kwargs, ) - @classmethod - def _prepare_json(cls, **kwargs): + def _prepare_json(self, **kwargs): obj = kwargs.pop("obj", None) if isinstance(obj, dict | list | set | tuple): return obj elif isinstance(obj, Schema): - return cls._serialize(obj, **kwargs) + return self._serialize(obj, **kwargs) return {} - @classmethod - def _prepare_request(cls, endpoint: Endpoint, **kwargs) -> Request: + def _prepare_request(self, endpoint: Endpoint, **kwargs) -> Request: return Request( - url=cls._prepare_url(endpoint=endpoint, **kwargs), - method=cls.get_method(endpoint, **kwargs), + url=self._prepare_url(endpoint=endpoint, **kwargs), + method=self.get_method(endpoint, **kwargs), endpoint=endpoint, data=kwargs.pop("data", {}), - json=cls._prepare_json(**kwargs), + json=self._prepare_json(**kwargs), timeout=kwargs.pop("timeout", None), params=kwargs.pop("params", {}), headers=kwargs.pop("headers", {}), @@ -118,9 +124,8 @@ def _prepare_request(cls, endpoint: Endpoint, **kwargs) -> Request: middleware_options=kwargs.copy(), ) - @classmethod def _handle_response( - cls, response: Response, response_type: ResponseType, **kwargs + self, response: Response, response_type: ResponseType, **kwargs ) -> any: if not response: return @@ -129,7 +134,7 @@ def _handle_response( if issubclass(response_type, dict | list | tuple | set): return response_type(response.json) elif issubclass(response_type, Schema): - return cls._deserialize( + return self._deserialize( schema=response_type, data=response.json, **kwargs ) @@ -141,88 +146,81 @@ def _handle_response( return response.json - @classmethod async def create[ - T: Schema + T: Schema ]( - cls, - client: BaseRESTClient, - obj: Schema | Mapping, - response_type: ResponseType = None, - **kwargs, - ) -> (T | None): - request = cls._prepare_request(endpoint=Endpoint.CREATE, obj=obj, **kwargs) - response = await cls._make_request(client=client, request=request) - return cls._handle_response( + self, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, + ) -> ( + T | None + ): + request = self._prepare_request(endpoint=Endpoint.CREATE, obj=obj, **kwargs) + response = await self._make_request(request=request, **kwargs) + return self._handle_response( response=response, response_type=response_type, **kwargs ) - @classmethod async def read[ - T: Schema - ]( - cls, client: BaseRESTClient, response_type: ResponseType = None, **kwargs - ) -> Iterable[T]: - request = cls._prepare_request(endpoint=Endpoint.READ, **kwargs) - response = await cls._make_request(client=client, request=request) - return cls._handle_response( + T: Schema + ](self, response_type: ResponseType = None, **kwargs) -> Iterable[T]: + request = self._prepare_request(endpoint=Endpoint.READ, **kwargs) + response = await self._make_request(request=request, **kwargs) + return self._handle_response( response=response, response_type=response_type, **kwargs ) - @classmethod async def read_one[ - T: Schema + T: Schema ]( - cls, - client: BaseRESTClient, - obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, - **kwargs, + self, + obj_or_pk: Schema | Mapping | any, + response_type: ResponseType = None, + **kwargs, ) -> T: - request = cls._prepare_request( - endpoint=Endpoint.READ_ONE, pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs + request = self._prepare_request( + endpoint=Endpoint.READ_ONE, pk=self._get_pk(obj_or_pk=obj_or_pk), **kwargs ) - response = await cls._make_request(client=client, request=request) - return cls._handle_response( + response = await self._make_request(request=request, **kwargs) + return self._handle_response( response=response, response_type=response_type, **kwargs ) - @classmethod async def update[ - T: Schema + T: Schema ]( - cls, - client: BaseRESTClient, - obj: Schema | Mapping, - response_type: ResponseType = None, - **kwargs, - ) -> (T | None): - request = cls._prepare_request( + self, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, + ) -> ( + T | None + ): + request = self._prepare_request( endpoint=Endpoint.UPDATE, - pk=kwargs.pop("pk", cls.get_pk(obj)), + pk=kwargs.pop("pk", self.get_pk(obj)), obj=obj, **kwargs, ) - response = await cls._make_request(client=client, request=request) - return cls._handle_response( + response = await self._make_request(request=request, **kwargs) + return self._handle_response( response=response, response_type=response_type, **kwargs ) - @classmethod async def delete[ - T: Schema + T: Schema ]( - cls, - client: BaseRESTClient, - obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, - **kwargs, + self, + obj_or_pk: Schema | Mapping | any, + response_type: ResponseType = None, + **kwargs, ) -> (T | None): - request = cls._prepare_request( - endpoint=Endpoint.DELETE, pk=cls._get_pk(obj_or_pk=obj_or_pk), **kwargs + request = self._prepare_request( + endpoint=Endpoint.DELETE, pk=self._get_pk(obj_or_pk=obj_or_pk), **kwargs ) - response = await cls._make_request(client=client, request=request) - return cls._handle_response( + response = await self._make_request(request=request, **kwargs) + return self._handle_response( response=response, response_type=response_type, **kwargs ) diff --git a/resty/managers/types.py b/resty/managers/types.py index 19acd51..93ca821 100644 --- a/resty/managers/types.py +++ b/resty/managers/types.py @@ -38,66 +38,52 @@ class BaseManager(ABC): serializer_class: BaseSerializer url_builder_class: BaseURLBuilder - @classmethod @abstractmethod - def get_serializer(cls, **kwargs) -> type[BaseSerializer]: ... + def get_serializer(self, **kwargs) -> type[BaseSerializer]: ... - @classmethod @abstractmethod - def get_method(cls, endpoint: Endpoint, **kwargs) -> Method: ... + def get_method(self, endpoint: Endpoint, **kwargs) -> Method: ... - @classmethod @abstractmethod - def get_field(cls, field: Field) -> str: ... + def get_field(self, field: Field) -> str: ... - @classmethod @abstractmethod - def get_pk(cls, obj: Schema | Mapping) -> any: ... + def get_pk(self, obj: Schema | Mapping) -> any: ... - @classmethod @abstractmethod async def create[T: Schema]( - cls, - client: BaseRESTClient, + self, obj: Schema | Mapping, response_type: ResponseType = None, **kwargs, ) -> T | None: ... - @classmethod @abstractmethod async def read[T: Schema]( - cls, - client: BaseRESTClient, + self, response_type: ResponseType = None, **kwargs, ) -> Iterable[T]: ... - @classmethod @abstractmethod async def read_one[T: Schema]( - cls, - client: BaseRESTClient, + self, obj_or_pk: Schema | Mapping | any, response_type: ResponseType = None, **kwargs, ) -> T: ... - @classmethod @abstractmethod async def update[T: Schema]( - cls, - client: BaseRESTClient, + self, obj: Schema | Mapping, response_type: ResponseType = None, **kwargs, ) -> T | None: ... - @classmethod @abstractmethod async def delete[T: Schema]( - cls, - client: BaseRESTClient, + self, obj_or_pk: Schema | Mapping | any, response_type: ResponseType = None, **kwargs, diff --git a/tests/managers/test_manager.py b/tests/managers/test_manager.py index b66f60b..c6157b4 100644 --- a/tests/managers/test_manager.py +++ b/tests/managers/test_manager.py @@ -70,16 +70,16 @@ class ManagerWithPkField(Manager): "client, obj", [ ( - RESTClientMock(json={"username": "test"}, method=Method.POST), - UserCreate(username="test"), + RESTClientMock(json={"username": "test"}, method=Method.POST), + UserCreate(username="test"), ), ( - RESTClientMock(json={"username": "321"}, method=Method.POST), - UserCreate(username="321"), + RESTClientMock(json={"username": "321"}, method=Method.POST), + UserCreate(username="321"), ), ( - RESTClientMock(json={"username": "test"}, method=Method.POST), - {"username": "test"}, + RESTClientMock(json={"username": "test"}, method=Method.POST), + {"username": "test"}, ), ], ) @@ -94,9 +94,9 @@ async def test_create(client, obj): "data", [ ( - {"username": "test", "id": 1}, - {"username": "test123", "id": 2}, - {"username": "test321", "id": 3}, + {"username": "test", "id": 1}, + {"username": "test123", "id": 2}, + {"username": "test321", "id": 3}, ), ], ) @@ -121,15 +121,15 @@ async def test_read(data): "client, obj", [ ( - RESTClientMock( - response=Response( - Request("", Method.GET), - status=200, - json={"username": "test", "id": 123}, + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123}, + ), + method=Method.GET, ), - method=Method.GET, - ), - UserRead(username="test", id=123), + UserRead(username="test", id=123), ), ], ) @@ -137,8 +137,8 @@ async def test_read_one(client, obj): manager = UserManager() assert ( - await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) - == obj + await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) + == obj ) @@ -147,16 +147,16 @@ async def test_read_one(client, obj): "client, obj", [ ( - RESTClientMock( - response=Response( - Request("", Method.GET), - status=200, - json={"username": "test", "id": 123}, + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123}, + ), + method=Method.PATCH, + url="users/123", ), - method=Method.PATCH, - url="users/123", - ), - UserUpdate(username="test", id=123), + UserUpdate(username="test", id=123), ), ], ) @@ -172,15 +172,15 @@ async def test_update(client, obj): [ (RESTClientMock(url="users/123", method=Method.DELETE), 123), ( - RESTClientMock(url="users/321", method=Method.DELETE), - UserRead(username="test", id=321), + 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) + await manager.delete(obj_or_pk=pk, client=client) @pytest.mark.asyncio @@ -253,9 +253,9 @@ def test_get_pk(obj, pk): ) async def test_passing_url(url): client = RESTClientMock(url=url) - manager = UserManagerForURLBuilding() + manager = UserManagerForURLBuilding(client=client) - await manager.delete(client, obj_or_pk=1, url=url) + await manager.delete(obj_or_pk=1, url=url) @pytest.mark.asyncio @@ -265,14 +265,14 @@ async def test_passing_url(url): ({"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: dict.keys(r.json), + dict.keys({"username": "test", "id": 123}), ), ( - {"username": "test", "id": 123}, - lambda r, t: dict.keys(r), - {"username": "test", "id": 123}, + {"username": "test", "id": 123}, + lambda r, t: dict.keys(r), + {"username": "test", "id": 123}, ), ], ) @@ -296,3 +296,20 @@ async def test_response_type(data, response_type, result): ) assert response == result + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "client", + [ + None, + "test", + ], +) +async def test_passing_invalid_client(client): + manager = UserManager() + + with pytest.raises(TypeError): + await manager.read( + client=client + ) From d39caf6924a2ddaa23effe1de8cd92c7cd1a043a Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 13:54:20 +0300 Subject: [PATCH 3/6] Add: tests for django-middlewraes --- docs/CHANGELOG.md | 7 +- resty/ext/__init__.py | 0 resty/ext/django/__init__.py | 0 resty/ext/django/middlewares/__init__.py | 0 .../django/middlewares/pagination/__init__.py | 6 ++ .../middlewares/pagination/constants.py | 1 + .../middlewares/pagination/middlewares.py | 58 +++++++++++ resty/managers/managers.py | 54 +++++----- .../test_django_pagination_middlewares.py | 99 +++++++++++++++++++ tests/managers/test_manager.py | 76 +++++++------- 10 files changed, 234 insertions(+), 67 deletions(-) create mode 100644 resty/ext/__init__.py create mode 100644 resty/ext/django/__init__.py create mode 100644 resty/ext/django/middlewares/__init__.py create mode 100644 resty/ext/django/middlewares/pagination/__init__.py create mode 100644 resty/ext/django/middlewares/pagination/constants.py create mode 100644 resty/ext/django/middlewares/pagination/middlewares.py create mode 100644 tests/ext/django/test_django_pagination_middlewares.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6d3f36e..8cbf998 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,7 +26,7 @@ ## v0.0.6 -- Change Manager API: +- Changed Manager API: Now instantiating manager is required. @@ -51,4 +51,9 @@ response = await manager.read( ) ``` +- Added Django pagination middlewares: + + - `LimitOffsetPaginationMiddleware` + - `PagePaginationMiddleware` + diff --git a/resty/ext/__init__.py b/resty/ext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resty/ext/django/__init__.py b/resty/ext/django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resty/ext/django/middlewares/__init__.py b/resty/ext/django/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resty/ext/django/middlewares/pagination/__init__.py b/resty/ext/django/middlewares/pagination/__init__.py new file mode 100644 index 0000000..9f7a200 --- /dev/null +++ b/resty/ext/django/middlewares/pagination/__init__.py @@ -0,0 +1,6 @@ +from .middlewares import LimitOffsetPaginationMiddleware, PagePaginationMiddleware + +__all__ = [ + "LimitOffsetPaginationMiddleware", + "PagePaginationMiddleware", +] diff --git a/resty/ext/django/middlewares/pagination/constants.py b/resty/ext/django/middlewares/pagination/constants.py new file mode 100644 index 0000000..b7c724a --- /dev/null +++ b/resty/ext/django/middlewares/pagination/constants.py @@ -0,0 +1 @@ +DEFAULT_LIMIT = 100 diff --git a/resty/ext/django/middlewares/pagination/middlewares.py b/resty/ext/django/middlewares/pagination/middlewares.py new file mode 100644 index 0000000..1dfc0ac --- /dev/null +++ b/resty/ext/django/middlewares/pagination/middlewares.py @@ -0,0 +1,58 @@ +from abc import ABC, abstractmethod +from typing import Container + +from resty.enums import Endpoint +from resty.types import Request, Response +from resty.middlewares import BaseRequestMiddleware, BaseResponseMiddleware + +from .constants import DEFAULT_LIMIT + + +class PaginationMiddleware(BaseRequestMiddleware, BaseResponseMiddleware, ABC): + def __init__(self, endpoints: Container[Endpoint] = None): + self._endpoints = endpoints or { + Endpoint.READ, + } + + @abstractmethod + async def paginate(self, request: Request, **kwargs): # pragma: nocover + ... + + async def unpaginate(self, response: Response, **kwargs): + response.json = response.json.get("results", response.json) + + async def _handle_request(self, request: Request, **kwargs): + if request.endpoint in self._endpoints: + await self.paginate(request=request, **kwargs) + + async def _handle_response(self, response: Response, **kwargs): + if response.request.endpoint in self._endpoints: + await self.unpaginate(response=response, **kwargs) + + async def __call__(self, reqresp: Request | Response, **kwargs): + if isinstance(reqresp, Request): + return await self._handle_request(request=reqresp, **kwargs) + return await self._handle_response(response=reqresp, **kwargs) + + +class LimitOffsetPaginationMiddleware(PaginationMiddleware): + def __init__(self, limit: int = DEFAULT_LIMIT, **kwargs): + self._limit = limit + super().__init__(**kwargs) + + async def paginate(self, request: Request, **kwargs): + request.params.update( + { + "limit": kwargs.pop("limit", self._limit), + "offset": kwargs.pop("offset", 0), + } + ) + + +class PagePaginationMiddleware(PaginationMiddleware): + async def paginate(self, request: Request, **kwargs): + request.params.update( + { + "page": kwargs.pop("page", 1), + } + ) diff --git a/resty/managers/managers.py b/resty/managers/managers.py index 0642d49..6288b0f 100644 --- a/resty/managers/managers.py +++ b/resty/managers/managers.py @@ -23,9 +23,9 @@ def get_serializer(self, **kwargs) -> type[BaseSerializer]: raise RuntimeError("Serializer not specified") if not ( - isinstance(serializer, BaseSerializer) - or inspect.isclass(serializer) - and issubclass(serializer, BaseSerializer) + isinstance(serializer, BaseSerializer) + or inspect.isclass(serializer) + and issubclass(serializer, BaseSerializer) ): raise RuntimeError("The serializer must be a subclass of BaseSerializer") @@ -125,7 +125,7 @@ def _prepare_request(self, endpoint: Endpoint, **kwargs) -> Request: ) def _handle_response( - self, response: Response, response_type: ResponseType, **kwargs + self, response: Response, response_type: ResponseType, **kwargs ) -> any: if not response: return @@ -147,14 +147,14 @@ def _handle_response( return response.json async def create[ - T: Schema + T: Schema ]( - self, - obj: Schema | Mapping, - response_type: ResponseType = None, - **kwargs, + self, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, ) -> ( - T | None + T | None ): request = self._prepare_request(endpoint=Endpoint.CREATE, obj=obj, **kwargs) response = await self._make_request(request=request, **kwargs) @@ -163,7 +163,7 @@ async def create[ ) async def read[ - T: Schema + T: Schema ](self, response_type: ResponseType = None, **kwargs) -> Iterable[T]: request = self._prepare_request(endpoint=Endpoint.READ, **kwargs) response = await self._make_request(request=request, **kwargs) @@ -172,12 +172,12 @@ async def read[ ) async def read_one[ - T: Schema + T: Schema ]( - self, - obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, - **kwargs, + self, + obj_or_pk: Schema | Mapping | any, + response_type: ResponseType = None, + **kwargs, ) -> T: request = self._prepare_request( @@ -189,14 +189,14 @@ async def read_one[ ) async def update[ - T: Schema + T: Schema ]( - self, - obj: Schema | Mapping, - response_type: ResponseType = None, - **kwargs, + self, + obj: Schema | Mapping, + response_type: ResponseType = None, + **kwargs, ) -> ( - T | None + T | None ): request = self._prepare_request( endpoint=Endpoint.UPDATE, @@ -210,12 +210,12 @@ async def update[ ) async def delete[ - T: Schema + T: Schema ]( - self, - obj_or_pk: Schema | Mapping | any, - response_type: ResponseType = None, - **kwargs, + self, + obj_or_pk: Schema | Mapping | any, + response_type: ResponseType = None, + **kwargs, ) -> (T | None): request = self._prepare_request( endpoint=Endpoint.DELETE, pk=self._get_pk(obj_or_pk=obj_or_pk), **kwargs diff --git a/tests/ext/django/test_django_pagination_middlewares.py b/tests/ext/django/test_django_pagination_middlewares.py new file mode 100644 index 0000000..297cac5 --- /dev/null +++ b/tests/ext/django/test_django_pagination_middlewares.py @@ -0,0 +1,99 @@ +import pytest + +from resty.enums import Method, Endpoint +from resty.types import Request, Response +from resty.ext.django.middlewares.pagination import ( + LimitOffsetPaginationMiddleware, + PagePaginationMiddleware, +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "req, kwargs, expected", + [ + ( + Request( + url="https://example.com/", method=Method.GET, endpoint=Endpoint.READ + ), + {"limit": 100, "offset": 10}, + {"limit": 100, "offset": 10}, + ) + ], +) +async def test_limit_offset_pagination(req, kwargs, expected): + middleware = LimitOffsetPaginationMiddleware() + + await middleware(reqresp=req, **kwargs) + + assert req.params == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "req, kwargs, expected", + [ + ( + Request( + url="https://example.com/", method=Method.GET, endpoint=Endpoint.READ + ), + {"page": 100}, + {"page": 100}, + ) + ], +) +async def test_page_pagination(req, kwargs, expected): + middleware = PagePaginationMiddleware() + + await middleware(reqresp=req, **kwargs) + + assert req.params == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "middleware, resp, expected", + [ + ( + LimitOffsetPaginationMiddleware(), + Response( + request=Request( + url="https://example.com/", + method=Method.GET, + endpoint=Endpoint.READ, + ), + status=200, + json={ + "results": [ + {"id": 1, "username": "josh"}, + ] + }, + ), + [ + {"id": 1, "username": "josh"}, + ], + ), + ( + PagePaginationMiddleware(), + Response( + request=Request( + url="https://example.com/", + method=Method.GET, + endpoint=Endpoint.READ, + ), + status=200, + json={ + "results": [ + {"id": 1, "username": "josh"}, + ] + }, + ), + [ + {"id": 1, "username": "josh"}, + ], + ), + ], +) +async def test_unpaginate(middleware, resp, expected): + await middleware(reqresp=resp) + assert resp.json == expected diff --git a/tests/managers/test_manager.py b/tests/managers/test_manager.py index c6157b4..6483464 100644 --- a/tests/managers/test_manager.py +++ b/tests/managers/test_manager.py @@ -70,16 +70,16 @@ class ManagerWithPkField(Manager): "client, obj", [ ( - RESTClientMock(json={"username": "test"}, method=Method.POST), - UserCreate(username="test"), + RESTClientMock(json={"username": "test"}, method=Method.POST), + UserCreate(username="test"), ), ( - RESTClientMock(json={"username": "321"}, method=Method.POST), - UserCreate(username="321"), + RESTClientMock(json={"username": "321"}, method=Method.POST), + UserCreate(username="321"), ), ( - RESTClientMock(json={"username": "test"}, method=Method.POST), - {"username": "test"}, + RESTClientMock(json={"username": "test"}, method=Method.POST), + {"username": "test"}, ), ], ) @@ -94,9 +94,9 @@ async def test_create(client, obj): "data", [ ( - {"username": "test", "id": 1}, - {"username": "test123", "id": 2}, - {"username": "test321", "id": 3}, + {"username": "test", "id": 1}, + {"username": "test123", "id": 2}, + {"username": "test321", "id": 3}, ), ], ) @@ -121,15 +121,15 @@ async def test_read(data): "client, obj", [ ( - RESTClientMock( - response=Response( - Request("", Method.GET), - status=200, - json={"username": "test", "id": 123}, - ), - method=Method.GET, + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123}, ), - UserRead(username="test", id=123), + method=Method.GET, + ), + UserRead(username="test", id=123), ), ], ) @@ -137,8 +137,8 @@ async def test_read_one(client, obj): manager = UserManager() assert ( - await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) - == obj + await manager.read_one(client=client, obj_or_pk=123, response_type=UserRead) + == obj ) @@ -147,16 +147,16 @@ async def test_read_one(client, obj): "client, obj", [ ( - RESTClientMock( - response=Response( - Request("", Method.GET), - status=200, - json={"username": "test", "id": 123}, - ), - method=Method.PATCH, - url="users/123", + RESTClientMock( + response=Response( + Request("", Method.GET), + status=200, + json={"username": "test", "id": 123}, ), - UserUpdate(username="test", id=123), + method=Method.PATCH, + url="users/123", + ), + UserUpdate(username="test", id=123), ), ], ) @@ -172,8 +172,8 @@ async def test_update(client, obj): [ (RESTClientMock(url="users/123", method=Method.DELETE), 123), ( - RESTClientMock(url="users/321", method=Method.DELETE), - UserRead(username="test", id=321), + RESTClientMock(url="users/321", method=Method.DELETE), + UserRead(username="test", id=321), ), ], ) @@ -265,14 +265,14 @@ async def test_passing_url(url): ({"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: dict.keys(r.json), + dict.keys({"username": "test", "id": 123}), ), ( - {"username": "test", "id": 123}, - lambda r, t: dict.keys(r), - {"username": "test", "id": 123}, + {"username": "test", "id": 123}, + lambda r, t: dict.keys(r), + {"username": "test", "id": 123}, ), ], ) @@ -310,6 +310,4 @@ async def test_passing_invalid_client(client): manager = UserManager() with pytest.raises(TypeError): - await manager.read( - client=client - ) + await manager.read(client=client) From 2755fcb16dd6bd0c6f9f4fca008f4a23d6fba56d Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 15:22:04 +0300 Subject: [PATCH 4/6] Add: django-middleware examples --- .../{middlewares.py => middlewares/common.py} | 0 examples/middlewares/django.py | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+) rename examples/{middlewares.py => middlewares/common.py} (100%) create mode 100644 examples/middlewares/django.py diff --git a/examples/middlewares.py b/examples/middlewares/common.py similarity index 100% rename from examples/middlewares.py rename to examples/middlewares/common.py diff --git a/examples/middlewares/django.py b/examples/middlewares/django.py new file mode 100644 index 0000000..873032e --- /dev/null +++ b/examples/middlewares/django.py @@ -0,0 +1,70 @@ +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 +from resty.ext.django.middlewares.pagination import ( + LimitOffsetPaginationMiddleware, + PagePaginationMiddleware, +) + + +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")) + + # Using LimitOffset pagination middleware + with client.middlewares.middleware(LimitOffsetPaginationMiddleware(limit=200)): + manager = UserManager(client=client) + + paginated_response = await manager.read( + response_type=UserReadSchema, + offset=100, + ) + + # Using Page pagination middleware + with client.middlewares.middleware(PagePaginationMiddleware()): + manager = UserManager(client=client) + + paginated_response = await manager.read( + response_type=UserReadSchema, + page=3, + ) + + +if __name__ == "__main__": + asyncio.run(main()) From 93f33afb7559e7a6a1d9707ab80f3d42e28d4869 Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 15:23:54 +0300 Subject: [PATCH 5/6] Fix: connect error text --- resty/clients/httpx/clients.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/resty/clients/httpx/clients.py b/resty/clients/httpx/clients.py index 148891c..e13a094 100644 --- a/resty/clients/httpx/clients.py +++ b/resty/clients/httpx/clients.py @@ -1,4 +1,5 @@ import json +from urllib.parse import urljoin import httpx @@ -20,10 +21,10 @@ class RESTClient(BaseRESTClient): def __init__( - self, - httpx_client: httpx.AsyncClient = None, - check_status: bool = True, - middleware_manager: BaseMiddlewareManager = None, + self, + httpx_client: httpx.AsyncClient = None, + check_status: bool = True, + middleware_manager: BaseMiddlewareManager = None, ): self.middlewares = middleware_manager or MiddlewareManager() self._xclient = httpx_client or httpx.AsyncClient() @@ -45,7 +46,7 @@ async def _make_xrequest(self, request: Request) -> httpx.Response: timeout=request.timeout, ) except httpx.ConnectError: - raise ConnectError(url=request.url) + raise ConnectError(url=urljoin(str(self._xclient.base_url), request.url)) @staticmethod def _extract_json_data(xresponse: httpx.Response) -> dict | list: @@ -57,7 +58,7 @@ def _extract_json_data(xresponse: httpx.Response) -> dict | list: return data async def _parse_xresponse( - self, request: Request, xresponse: httpx.Response + self, request: Request, xresponse: httpx.Response ) -> Response: return Response( request=request, From 29635060e99d022421333ed0fa1f736faed48388 Mon Sep 17 00:00:00 2001 From: CrazyProger1 Date: Mon, 29 Apr 2024 15:24:41 +0300 Subject: [PATCH 6/6] Fix: readme status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c5db1f..730ab63 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ if __name__ == "__main__": ## Status -``0.0.5`` - **RELEASED** +``0.0.6`` - **RELEASED** ## Licence