diff --git a/app/processing.py b/app/processing.py index dbeb6fb..affe673 100644 --- a/app/processing.py +++ b/app/processing.py @@ -4,7 +4,7 @@ import random from collections.abc import Awaitable, Callable from datetime import UTC, datetime -import httpx +import httpx2 from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker @@ -37,7 +37,7 @@ class WebhookClient: self._timeout = timeout async def send(self, url: str, payload: PaymentWebhook) -> None: - async with httpx.AsyncClient(timeout=self._timeout) as client: + async with httpx2.AsyncClient(timeout=self._timeout) as client: response = await client.post(url, json=payload.model_dump(mode="json")) response.raise_for_status() diff --git a/pyproject.toml b/pyproject.toml index 66d5275..ffc15d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "asyncpg>=0.30,<1", "fastapi>=0.115,<1", "faststream[cli,rabbit]>=0.5.48,<1", - "httpx>=0.28,<1", + "httpx2>=2.13,<3", "pydantic>=2.10,<3", "pydantic-settings>=2.7,<3", "sqlalchemy[asyncio]>=2.0.36,<3", diff --git a/tests/test_api_contract.py b/tests/test_api_contract.py index 4b70a1b..b76b7f8 100644 --- a/tests/test_api_contract.py +++ b/tests/test_api_contract.py @@ -1,58 +1,65 @@ import uuid +from collections.abc import AsyncIterator +import httpx2 +import pytest from fastapi import FastAPI -from fastapi.testclient import TestClient from app.api import router -def make_client() -> TestClient: +@pytest.fixture +async def client() -> AsyncIterator[httpx2.AsyncClient]: app = FastAPI() app.include_router(router) - return TestClient(app) + transport = httpx2.ASGITransport(app=app) + async with httpx2.AsyncClient( + transport=transport, + base_url="http://testserver", + ) as test_client: + yield test_client -def test_get_payment_requires_api_key() -> None: - with make_client() as client: - response = client.get(f"/api/v1/payments/{uuid.uuid4()}") +async def test_get_payment_requires_api_key(client: httpx2.AsyncClient) -> None: + response = await client.get(f"/api/v1/payments/{uuid.uuid4()}") assert response.status_code == 401 assert response.json() == {"detail": "Invalid or missing API key"} -def test_create_payment_requires_idempotency_key() -> None: - with make_client() as client: - response = client.post( - "/api/v1/payments", - headers={"X-API-Key": "change-me"}, - json={ - "amount": "100.00", - "currency": "RUB", - "description": "Test payment", - "metadata": {}, - "webhook_url": "https://example.com/webhook", - }, - ) +async def test_create_payment_requires_idempotency_key(client: httpx2.AsyncClient) -> None: + response = await client.post( + "/api/v1/payments", + headers={"X-API-Key": "change-me"}, + json={ + "amount": "100.00", + "currency": "RUB", + "description": "Test payment", + "metadata": {}, + "webhook_url": "https://example.com/webhook", + }, + ) assert response.status_code == 422 assert response.json()["detail"][0]["loc"] == ["header", "Idempotency-Key"] -def test_create_payment_rejects_unsupported_currency() -> None: - with make_client() as client: - response = client.post( - "/api/v1/payments", - headers={ - "X-API-Key": "change-me", - "Idempotency-Key": "test-key", - }, - json={ - "amount": "100.00", - "currency": "GBP", - "description": "Test payment", - "metadata": {}, - "webhook_url": "https://example.com/webhook", - }, - ) +async def test_create_payment_rejects_unsupported_currency( + client: httpx2.AsyncClient, +) -> None: + response = await client.post( + "/api/v1/payments", + headers={ + "X-API-Key": "change-me", + "Idempotency-Key": "test-key", + }, + json={ + "amount": "100.00", + "currency": "GBP", + "description": "Test payment", + "metadata": {}, + "webhook_url": "https://example.com/webhook", + }, + ) assert response.status_code == 422 diff --git a/uv.lock b/uv.lock index dd57b15..81851bb 100644 --- a/uv.lock +++ b/uv.lock @@ -117,15 +117,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, -] - [[package]] name = "click" version = "8.5.0" @@ -278,16 +269,16 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, ] [[package]] @@ -327,18 +318,29 @@ wheels = [ ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] @@ -620,7 +622,7 @@ dependencies = [ { name = "asyncpg" }, { name = "fastapi" }, { name = "faststream", extra = ["cli", "rabbit"] }, - { name = "httpx" }, + { name = "httpx2" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "sqlalchemy", extra = ["asyncio"] }, @@ -641,7 +643,7 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.30,<1" }, { name = "fastapi", specifier = ">=0.115,<1" }, { name = "faststream", extras = ["cli", "rabbit"], specifier = ">=0.5.48,<1" }, - { name = "httpx", specifier = ">=0.28,<1" }, + { name = "httpx2", specifier = ">=2.13,<3" }, { name = "pydantic", specifier = ">=2.10,<3" }, { name = "pydantic-settings", specifier = ">=2.7,<3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3,<9" }, @@ -1075,6 +1077,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.81"