diff --git a/pyproject.toml b/pyproject.toml index 9e9540c..af584e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Environment :: Console", ] dependencies = [ - "httpx>=0.27,<1.0", + "httpx2>=2.4,<3.0", "pydantic>=2.0,<3.0", ] diff --git a/src/hyperping/_async_client.py b/src/hyperping/_async_client.py index 23e1dea..63dca10 100644 --- a/src/hyperping/_async_client.py +++ b/src/hyperping/_async_client.py @@ -20,7 +20,7 @@ from typing import Any from urllib.parse import urlsplit -import httpx +import httpx2 as httpx from pydantic import SecretStr from hyperping._async_healthchecks_mixin import AsyncHealthchecksMixin diff --git a/src/hyperping/_async_mcp_transport.py b/src/hyperping/_async_mcp_transport.py index 6be0272..ca60bd1 100644 --- a/src/hyperping/_async_mcp_transport.py +++ b/src/hyperping/_async_mcp_transport.py @@ -9,7 +9,7 @@ import time from typing import Any -import httpx +import httpx2 as httpx from pydantic import SecretStr from hyperping._internals import validate_base_url diff --git a/src/hyperping/_mcp_transport.py b/src/hyperping/_mcp_transport.py index 9353d3e..a9cb5ad 100644 --- a/src/hyperping/_mcp_transport.py +++ b/src/hyperping/_mcp_transport.py @@ -9,7 +9,7 @@ import time from typing import Any -import httpx +import httpx2 as httpx from pydantic import SecretStr from hyperping._internals import validate_base_url diff --git a/src/hyperping/client.py b/src/hyperping/client.py index 87953f2..97096e7 100644 --- a/src/hyperping/client.py +++ b/src/hyperping/client.py @@ -18,7 +18,7 @@ from typing import Any from urllib.parse import urlsplit -import httpx +import httpx2 as httpx from pydantic import SecretStr from hyperping._circuit_breaker import ( diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 86c2219..9869263 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,10 +3,22 @@ from collections.abc import Generator import pytest +from respx.mocks import HTTPCoreMocker from hyperping.client import HyperpingClient, RetryConfig from hyperping.endpoints import API_BASE +# httpx2 uses httpcore2 instead of httpcore; extend respx's default mocker so +# that @respx.mock intercepts requests made through httpx2 clients. +HTTPCoreMocker.add_targets( + "httpcore2._sync.connection.HTTPConnection", + "httpcore2._sync.connection_pool.ConnectionPool", + "httpcore2._sync.http_proxy.HTTPProxy", + "httpcore2._async.connection.AsyncHTTPConnection", + "httpcore2._async.connection_pool.AsyncConnectionPool", + "httpcore2._async.http_proxy.AsyncHTTPProxy", +) + @pytest.fixture def client() -> Generator[HyperpingClient, None, None]: diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index c6dee53..a0bfc9a 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch -import httpx +import httpx2 as httpx import pytest import pytest_asyncio diff --git a/tests/unit/test_client_coverage.py b/tests/unit/test_client_coverage.py index 3dd75e7..e40f299 100644 --- a/tests/unit/test_client_coverage.py +++ b/tests/unit/test_client_coverage.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import httpcore2 import httpx import pytest import respx @@ -69,7 +70,7 @@ def test_ping_api_error_wraps(self) -> None: def test_ping_timeout_wraps(self) -> None: """ping() wraps httpx.TimeoutException.""" respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - side_effect=httpx.TimeoutException("timed out") + side_effect=httpcore2.ConnectTimeout("timed out") ) c = HyperpingClient( api_key="sk_test", @@ -83,7 +84,7 @@ def test_ping_timeout_wraps(self) -> None: def test_ping_request_error_wraps(self) -> None: """ping() wraps httpx.RequestError.""" respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - side_effect=httpx.ConnectError("connection refused") + side_effect=httpcore2.ConnectError("connection refused") ) c = HyperpingClient( api_key="sk_test", @@ -250,7 +251,7 @@ class TestTimeoutRetry: def test_timeout_retries_then_raises(self) -> None: """Timeout after all retries raises HyperpingAPIError.""" respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - side_effect=httpx.TimeoutException("timed out") + side_effect=httpcore2.ConnectTimeout("timed out") ) with patch("hyperping.client.time.sleep"): c = HyperpingClient( @@ -265,7 +266,7 @@ def test_timeout_retries_then_raises(self) -> None: def test_request_error_retries_then_raises(self) -> None: """Connection error after all retries raises HyperpingAPIError.""" respx.get(f"{API_BASE}{Endpoint.MONITORS}").mock( - side_effect=httpx.ConnectError("connection refused") + side_effect=httpcore2.ConnectError("connection refused") ) with patch("hyperping.client.time.sleep"): c = HyperpingClient( diff --git a/uv.lock b/uv.lock index a9774e8..f4435d2 100644 --- a/uv.lock +++ b/uv.lock @@ -327,6 +327,19 @@ 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 = "httpcore2" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/9b/2b1d1833a58236d1f6ee755e027a3917da0db59cc9708554cefc440ee8b6/httpcore2-2.4.0.tar.gz", hash = "sha256:3093a8ab8980d9f910b9cb4351df9186a0ad2350a6284a9107ac9a362a584422", size = 64618, upload-time = "2026-06-11T06:35:53.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/72/4fdf2306143a92a471fad9f3655aa542d43aa9188a7c9534e82c9aecf837/httpcore2-2.4.0-py3-none-any.whl", hash = "sha256:5218779da5d6e3c2013ac706121abfb3815d450e0613495c0de50264dce58242", size = 80151, upload-time = "2026-06-11T06:35:50.89Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -342,12 +355,28 @@ 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 = "httpx2" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/60/b43ced4ccf26e95b396dbf67051d3e5042b645917d4da0469dd82a3bdd4f/httpx2-2.4.0.tar.gz", hash = "sha256:32e0734b61eb0824b3f56a9e98d6d92d381a3ef12c0045aa917ee63df6c411ef", size = 81691, upload-time = "2026-06-11T06:35:54.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/45/82bc57c3d9c3314f663b67cc057f1c017a6450685dde513f4f8db5cf431f/httpx2-2.4.0-py3-none-any.whl", hash = "sha256:425acd99297829599decf6701386dd84db3542597d36d3e2e4def930ecd57fd9", size = 74941, upload-time = "2026-06-11T06:35:52.235Z" }, +] + [[package]] name = "hyperping" version = "1.8.0" source = { editable = "." } dependencies = [ - { name = "httpx" }, + { name = "httpx2" }, { name = "pydantic" }, ] @@ -373,7 +402,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "httpx", specifier = ">=0.27,<1.0" }, + { name = "httpx2", specifier = ">=2.4,<3.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7" }, { name = "pydantic", specifier = ">=2.0,<3.0" }, @@ -392,11 +421,11 @@ dev = [{ name = "pytest-asyncio", specifier = ">=0.23.0" }] [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1046,6 +1075,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[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 = "typer" version = "0.26.7"