From 277138238a80396f101ebed7e345fa37b8e50148 Mon Sep 17 00:00:00 2001 From: Albert Sola Date: Wed, 30 Jul 2025 18:34:54 +0100 Subject: [PATCH] #MPT-12324 HTTP layer with Httpx --- mpt_api_client/client.py | 3 -- mpt_api_client/http/client.py | 25 +++++++++++ pyproject.toml | 6 ++- setup.cfg | 3 ++ tests/http/test_client.py | 57 +++++++++++++++++++++++++ tests/test_client.py | 9 ---- uv.lock | 78 +++++++++++++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 13 deletions(-) delete mode 100644 mpt_api_client/client.py create mode 100644 mpt_api_client/http/client.py create mode 100644 tests/http/test_client.py delete mode 100644 tests/test_client.py diff --git a/mpt_api_client/client.py b/mpt_api_client/client.py deleted file mode 100644 index 9a0a03d..0000000 --- a/mpt_api_client/client.py +++ /dev/null @@ -1,3 +0,0 @@ -def some_function_to_test(test_str: str) -> bool: - """Function to run tests.""" - return test_str == "test" diff --git a/mpt_api_client/http/client.py b/mpt_api_client/http/client.py new file mode 100644 index 0000000..a938f53 --- /dev/null +++ b/mpt_api_client/http/client.py @@ -0,0 +1,25 @@ +import httpx + + +class MPTClient(httpx.Client): + """A client for interacting with SoftwareOne Marketplace Platform API.""" + + def __init__( + self, + *, + base_url: str, + api_token: str, + timeout: float = 5.0, + retries: int = 0, + ): + self.api_token = api_token + base_headers = { + "User-Agent": "swo-marketplace-client/1.0", + "Authorization": f"Bearer {api_token}", + } + super().__init__( + base_url=base_url, + headers=base_headers, + timeout=timeout, + transport=httpx.HTTPTransport(retries=retries), + ) diff --git a/pyproject.toml b/pyproject.toml index 4da0f63..61948b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Topic :: Utilities", ] -dependencies = [] +dependencies = [ + "httpx==0.28.*" +] [dependency-groups] dev = [ @@ -35,6 +37,7 @@ dev = [ "pytest-randomly==3.16.*", "pytest-xdist==3.6.*", "responses==0.25.*", + "respx==0.22.*", "ruff==0.12.*", "typing-extensions==4.13.*", "wemake-python-styleguide==1.3.*", @@ -164,6 +167,7 @@ pydocstyle.convention = "google" [tool.ruff.lint.per-file-ignores] "tests/*.py" = [ "D103", # missing docstring in public function + "PLR2004", # allow magic numbers in tests "S101", # asserts "S105", # hardcoded passwords "S404", # subprocess calls are for tests diff --git a/setup.cfg b/setup.cfg index f1c7417..19c1015 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,3 +24,6 @@ extend-exclude = # We only run `wemake-python-styleguide` with `flake8`: select = WPS, E999 + +per-file-ignores = + tests/*: WPS432 diff --git a/tests/http/test_client.py b/tests/http/test_client.py new file mode 100644 index 0000000..0a09cc1 --- /dev/null +++ b/tests/http/test_client.py @@ -0,0 +1,57 @@ +import pytest +import respx +from httpx import ConnectTimeout, Response, codes + +from mpt_api_client.http.client import MPTClient + +API_TOKEN = "test-token" +API_URL = "https://api.example.com" + + +@pytest.fixture +def mpt_client(): + return MPTClient(base_url=API_URL, api_token=API_TOKEN) + + +def test_mpt_client_initialization(): + client = MPTClient(base_url=API_URL, api_token=API_TOKEN) + + assert client.api_token == API_TOKEN + assert client.base_url == API_URL + assert client.headers["Authorization"] == "Bearer test-token" + assert client.headers["User-Agent"] == "swo-marketplace-client/1.0" + + +@respx.mock +def test_mock_call_success(mpt_client): + success_route = respx.get(f"{API_URL}/").mock( + return_value=Response(200, json={"message": "Hello, World!"}) + ) + + success_response = mpt_client.get("/") + + assert success_response.status_code == codes.OK + assert success_response.json() == {"message": "Hello, World!"} + assert success_route.called + + +@respx.mock +def test_mock_call_failure(mpt_client): + timeout_route = respx.get(f"{API_URL}/timeout").mock(side_effect=ConnectTimeout("Mock Timeout")) + + with pytest.raises(ConnectTimeout): + mpt_client.get("/timeout") + + assert timeout_route.called + + +@respx.mock +def test_mock_call_failure_with_retries(mpt_client): + not_found_route = respx.get(f"{API_URL}/not-found").mock( + side_effect=Response(codes.NOT_FOUND, json={"message": "Not Found"}) + ) + + not_found_response = mpt_client.get("/not-found") + + assert not_found_response.status_code == codes.NOT_FOUND + assert not_found_route.called diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 6445eab..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,9 +0,0 @@ -from mpt_api_client.client import some_function_to_test - - -def test_some_function_to_test_false(): - assert not some_function_to_test("some") - - -def test_some_function_to_test_true(): - assert some_function_to_test("test") diff --git a/uv.lock b/uv.lock index 6034c28..ca004e3 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,20 @@ version = 1 revision = 2 requires-python = ">=3.12, <4" +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + [[package]] name = "asttokens" version = "3.0.0" @@ -217,6 +231,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/fe/fda2d2dccda9b5eac3a7d00dea11efcbb776b00c4f1b471e0c3a26c9c751/freezegun-1.5.3-py3-none-any.whl", hash = "sha256:1ce20ee4be61349ba52c3af64f5eaba8d08ff51acfcf1b3ea671f03e54c818f1", size = 19062, upload-time = "2025-07-12T18:39:39.333Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.12" @@ -327,6 +378,9 @@ wheels = [ name = "mpt-api-client" version = "1.0.0" source = { editable = "." } +dependencies = [ + { name = "httpx" }, +] [package.dev-dependencies] dev = [ @@ -342,12 +396,14 @@ dev = [ { name = "pytest-randomly" }, { name = "pytest-xdist" }, { name = "responses" }, + { name = "respx" }, { name = "ruff" }, { name = "typing-extensions" }, { name = "wemake-python-styleguide" }, ] [package.metadata] +requires-dist = [{ name = "httpx", specifier = "==0.28.*" }] [package.metadata.requires-dev] dev = [ @@ -363,6 +419,7 @@ dev = [ { name = "pytest-randomly", specifier = "==3.16.*" }, { name = "pytest-xdist", specifier = "==3.6.*" }, { name = "responses", specifier = "==0.25.*" }, + { name = "respx", specifier = "==0.22.*" }, { name = "ruff", specifier = "==0.12.*" }, { name = "typing-extensions", specifier = "==4.13.*" }, { name = "wemake-python-styleguide", specifier = "==1.3.*" }, @@ -676,6 +733,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/fc/1d20b64fa90e81e4fa0a34c9b0240a6cfb1326b7e06d18a5432a9917c316/responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c", size = 34732, upload-time = "2025-03-11T15:36:14.589Z" }, ] +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + [[package]] name = "ruff" version = "0.12.5" @@ -710,6 +779,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "stack-data" version = "0.6.3"