From bcb3da4e615f87ce62310c74e78fbc0be601d8a1 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Fri, 17 May 2024 13:00:49 +0200 Subject: [PATCH 1/5] fix test --- requirements.txt | 4 +++- tests/test_init.py | 32 ++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index a387c1e..70d165d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ typing_extensions aiohttp requests nose2 -mypy \ No newline at end of file +mypy +pytest +pytest-asyncio \ No newline at end of file diff --git a/tests/test_init.py b/tests/test_init.py index ba52acf..42eedeb 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,11 +1,13 @@ from dataclasses import dataclass -from dataclass_factory import Factory, NameStyle, Schema +import pytest +from adaptix import Retort, NameStyle, name_mapping from requests import Session from dataclass_rest import get -from dataclass_rest.async_base import AsyncClient -from dataclass_rest.sync_base import Client +from dataclass_rest.http.aiohttp import AiohttpClient +from dataclass_rest.http.requests import RequestsClient + @dataclass class Todo: @@ -13,12 +15,14 @@ class Todo: def test_sync(): - class RealClient(Client): + class RealClient(RequestsClient): def __init__(self): super().__init__("https://jsonplaceholder.typicode.com/", Session()) - def _init_factory(self): - return Factory(default_schema=Schema(name_style=NameStyle.camel_lower)) + def _init_request_body_factory(self) -> Retort: + return Retort(recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ]) @get("todos/{id}") def get_todo(self, id: str) -> Todo: @@ -27,16 +31,20 @@ def get_todo(self, id: str) -> Todo: assert RealClient() -def test_async(): - class RealClient(AsyncClient): +@pytest.mark.asyncio +async def test_async(): + class RealClient(AiohttpClient): def __init__(self): - super().__init__("https://jsonplaceholder.typicode.com/", Session()) + super().__init__("https://jsonplaceholder.typicode.com/") - def _init_factory(self): - return Factory(default_schema=Schema(name_style=NameStyle.camel_lower)) + def _init_request_body_factory(self) -> Retort: + return Retort(recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ]) @get("todos/{id}") async def get_todo(self, id: str) -> Todo: pass - assert RealClient() + client = RealClient() + await client.session.close() From f951c3ede94021d9a34d7cd11da5aa1934b0f168 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Fri, 17 May 2024 13:02:03 +0200 Subject: [PATCH 2/5] github actions --- .github/workflows/setup.yml | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/setup.yml diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml new file mode 100644 index 0000000..21b3e9b --- /dev/null +++ b/.github/workflows/setup.yml @@ -0,0 +1,38 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: CI + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + cpython: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + python-version: + - "3.10" + - "3.11" + - "3.12" + + steps: + - uses: actions/checkout@v4 + - name: Set up ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install '.' -r requirements_dev.txt + + - name: Run tests + run: | + pytest From 7441163d7992a60ebc69ded9e1feafae18c87e18 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Fri, 17 May 2024 16:22:55 +0200 Subject: [PATCH 3/5] test params and factory --- requirements.txt | 1 + tests/__init__.py | 0 tests/requests/__init__.py | 0 tests/requests/conftest.py | 14 +++++++ tests/requests/test_factory.py | 59 +++++++++++++++++++++++++++++ tests/requests/test_params.py | 69 ++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/requests/__init__.py create mode 100644 tests/requests/conftest.py create mode 100644 tests/requests/test_factory.py create mode 100644 tests/requests/test_params.py diff --git a/requirements.txt b/requirements.txt index 70d165d..48476ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ adaptix typing_extensions aiohttp requests +requests-mock nose2 mypy pytest diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requests/__init__.py b/tests/requests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requests/conftest.py b/tests/requests/conftest.py new file mode 100644 index 0000000..9ee4254 --- /dev/null +++ b/tests/requests/conftest.py @@ -0,0 +1,14 @@ +import pytest +import requests_mock + +from dataclass_rest.http import requests + +@pytest.fixture +def session(): + return requests.Session() + + +@pytest.fixture +def mocker(session): + with requests_mock.Mocker(session=session, case_sensitive=True) as session_mock: + yield session_mock diff --git a/tests/requests/test_factory.py b/tests/requests/test_factory.py new file mode 100644 index 0000000..05bf77a --- /dev/null +++ b/tests/requests/test_factory.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from enum import Enum + +from adaptix import Retort, NameStyle, name_mapping + +from dataclass_rest import patch +from dataclass_rest.http.requests import RequestsClient + + +class Selection(Enum): + ONE = "ONE" + TWO = "TWO" + + +@dataclass +class RequestBody: + int_param: int + selection: Selection + + +@dataclass +class ResponseBody: + int_param: int + selection: Selection + + +def test_body(session, mocker): + class Api(RequestsClient): + def _init_request_body_factory(self) -> Retort: + return Retort(recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ]) + + def _init_request_args_factory(self) -> Retort: + return Retort(recipe=[ + name_mapping(name_style=NameStyle.UPPER_DOT), + ]) + + def _init_response_body_factory(self) -> Retort: + return Retort(recipe=[ + name_mapping(name_style=NameStyle.LOWER_KEBAB), + ]) + + @patch("/post/") + def post_x(self, long_param: str, body: RequestBody) -> ResponseBody: + raise NotImplementedError() + + mocker.patch( + url="http://example.com/post/", + text="""{"int-param": 1, "selection": "TWO"}""", + ) + client = Api(base_url="http://example.com", session=session) + result = client.post_x( + long_param="hello", body=RequestBody(int_param=42, selection=Selection.ONE), + ) + assert result == ResponseBody(int_param=1, selection=Selection.TWO) + assert mocker.called_once + assert mocker.request_history[0].json() == {"intParam": 42, "selection": "ONE"} + assert mocker.request_history[0].query == "LONG.PARAM=hello" diff --git a/tests/requests/test_params.py b/tests/requests/test_params.py new file mode 100644 index 0000000..6b0b958 --- /dev/null +++ b/tests/requests/test_params.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass +from typing import Optional + +from dataclass_rest import get, post +from dataclass_rest.http.requests import RequestsClient + + +def test_methods(session, mocker): + class Api(RequestsClient): + @get("/get") + def get_x(self) -> list[int]: + raise NotImplementedError() + + @post("/post") + def post_x(self) -> list[int]: + raise NotImplementedError() + + mocker.get("http://example.com/get", text="[1,2]") + mocker.post("http://example.com/post", text="[1,2,3]") + client = Api(base_url="http://example.com", session=session) + assert client.get_x() == [1, 2] + assert client.post_x() == [1, 2, 3] + + +def test_path_params(session, mocker): + class Api(RequestsClient): + @post("/post/{id}") + def post_x(self, id) -> list[int]: + raise NotImplementedError() + + mocker.post("http://example.com/post/1", text="[1]") + mocker.post("http://example.com/post/2", text="[1,2]") + client = Api(base_url="http://example.com", session=session) + assert client.post_x(1) == [1] + assert client.post_x(2) == [1, 2] + + +def test_query_params(session, mocker): + class Api(RequestsClient): + @post("/post/{id}") + def post_x(self, id: str, param: Optional[int]) -> list[int]: + raise NotImplementedError() + + mocker.post("http://example.com/post/x?", text="[0]") + mocker.post("http://example.com/post/x?param=1", text="[1]") + mocker.post("http://example.com/post/x?param=2", text="[1,2]") + client = Api(base_url="http://example.com", session=session) + assert client.post_x("x", None) == [0] + assert client.post_x("x", 1) == [1] + assert client.post_x("x", 2) == [1, 2] + + +@dataclass +class RequestBody: + x: int + y: str + + +def test_body(session, mocker): + class Api(RequestsClient): + @post("/post/") + def post_x(self, body: RequestBody) -> None: + raise NotImplementedError() + + mocker.post("http://example.com/post/", text="null") + client = Api(base_url="http://example.com", session=session) + assert client.post_x(RequestBody(x=1, y="test")) is None + assert mocker.called_once + assert mocker.request_history[0].json() == {"x": 1, "y": "test"} From baf55c96e3319b26d22650f73126f0a4e65fd680 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Fri, 17 May 2024 16:52:52 +0200 Subject: [PATCH 4/5] fix reqs file name, remove travis --- .travis.yml | 10 ---------- requirements.txt => requirements_dev.txt | 0 2 files changed, 10 deletions(-) delete mode 100644 .travis.yml rename requirements.txt => requirements_dev.txt (100%) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 07741a6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -dist: bionic -language: python -python: - - "3.8" - - "3.7" - - "3.6" -script: - nose2 -v && mypy tests dataclass_rest -install: - - pip3 install -r requirements.txt diff --git a/requirements.txt b/requirements_dev.txt similarity index 100% rename from requirements.txt rename to requirements_dev.txt From 39b4851deff351d2d606891eaaa883ac9100a9eb Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Fri, 17 May 2024 17:11:30 +0200 Subject: [PATCH 5/5] more strict matching --- requirements_dev.txt | 1 - tests/requests/test_factory.py | 4 ++-- tests/requests/test_params.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 48476ff..23604f7 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,7 +3,6 @@ typing_extensions aiohttp requests requests-mock -nose2 mypy pytest pytest-asyncio \ No newline at end of file diff --git a/tests/requests/test_factory.py b/tests/requests/test_factory.py index 05bf77a..4a5d762 100644 --- a/tests/requests/test_factory.py +++ b/tests/requests/test_factory.py @@ -46,8 +46,9 @@ def post_x(self, long_param: str, body: RequestBody) -> ResponseBody: raise NotImplementedError() mocker.patch( - url="http://example.com/post/", + url="http://example.com/post/?LONG.PARAM=hello", text="""{"int-param": 1, "selection": "TWO"}""", + complete_qs=True, ) client = Api(base_url="http://example.com", session=session) result = client.post_x( @@ -56,4 +57,3 @@ def post_x(self, long_param: str, body: RequestBody) -> ResponseBody: assert result == ResponseBody(int_param=1, selection=Selection.TWO) assert mocker.called_once assert mocker.request_history[0].json() == {"intParam": 42, "selection": "ONE"} - assert mocker.request_history[0].query == "LONG.PARAM=hello" diff --git a/tests/requests/test_params.py b/tests/requests/test_params.py index 6b0b958..50a8573 100644 --- a/tests/requests/test_params.py +++ b/tests/requests/test_params.py @@ -15,8 +15,8 @@ def get_x(self) -> list[int]: def post_x(self) -> list[int]: raise NotImplementedError() - mocker.get("http://example.com/get", text="[1,2]") - mocker.post("http://example.com/post", text="[1,2,3]") + mocker.get("http://example.com/get", text="[1,2]", complete_qs=True) + mocker.post("http://example.com/post", text="[1,2,3]", complete_qs=True) client = Api(base_url="http://example.com", session=session) assert client.get_x() == [1, 2] assert client.post_x() == [1, 2, 3] @@ -28,8 +28,8 @@ class Api(RequestsClient): def post_x(self, id) -> list[int]: raise NotImplementedError() - mocker.post("http://example.com/post/1", text="[1]") - mocker.post("http://example.com/post/2", text="[1,2]") + mocker.post("http://example.com/post/1", text="[1]", complete_qs=True) + mocker.post("http://example.com/post/2", text="[1,2]", complete_qs=True) client = Api(base_url="http://example.com", session=session) assert client.post_x(1) == [1] assert client.post_x(2) == [1, 2] @@ -41,9 +41,9 @@ class Api(RequestsClient): def post_x(self, id: str, param: Optional[int]) -> list[int]: raise NotImplementedError() - mocker.post("http://example.com/post/x?", text="[0]") - mocker.post("http://example.com/post/x?param=1", text="[1]") - mocker.post("http://example.com/post/x?param=2", text="[1,2]") + mocker.post("http://example.com/post/x?", text="[0]", complete_qs=True) + mocker.post("http://example.com/post/x?param=1", text="[1]", complete_qs=True) + mocker.post("http://example.com/post/x?param=2", text="[1,2]", complete_qs=True) client = Api(base_url="http://example.com", session=session) assert client.post_x("x", None) == [0] assert client.post_x("x", 1) == [1] @@ -62,7 +62,7 @@ class Api(RequestsClient): def post_x(self, body: RequestBody) -> None: raise NotImplementedError() - mocker.post("http://example.com/post/", text="null") + mocker.post("http://example.com/post/", text="null", complete_qs=True) client = Api(base_url="http://example.com", session=session) assert client.post_x(RequestBody(x=1, y="test")) is None assert mocker.called_once