From 27289e3e04a2edaa2389b76e76c2ddc3465b7b07 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 04:45:38 +0300 Subject: [PATCH 1/5] fix(models): reconcile Integration/EscalationPolicy/TeamMember against production API (#9606db) - Fix Integration crash: alias was 'type' but API returns 'channel'; rename alias to 'channel', keep Python attribute as integration_type for compat - Remove phantom active field from Integration (API never returns it) - Promote Integration fields: created_by, created_at, region, metadata - Define EscalationStep sub-model with uuid, wait_before, channels, temp_id - Promote EscalationPolicy fields: created_by, created_at, grouped_alerts_window, grouped_alerts_enabled, monitor_count; type steps as list[EscalationStep] - Add sso_picture_url to TeamMember - Export EscalationStep from hyperping.models and hyperping top-level --- src/hyperping/__init__.py | 2 ++ src/hyperping/models/__init__.py | 8 ++++- src/hyperping/models/_integration_models.py | 17 ++++++++-- src/hyperping/models/_oncall_models.py | 37 +++++++++++++++++++-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/hyperping/__init__.py b/src/hyperping/__init__.py index 25dab8b..f8665ce 100644 --- a/src/hyperping/__init__.py +++ b/src/hyperping/__init__.py @@ -44,6 +44,7 @@ AlertHistory, DnsRecordType, EscalationPolicy, + EscalationStep, Healthcheck, HealthcheckCreate, HealthcheckUpdate, @@ -168,6 +169,7 @@ "ProbeLogResponse", # On-call "OnCallSchedule", + "EscalationStep", "EscalationPolicy", "TeamMember", # Integrations diff --git a/src/hyperping/models/__init__.py b/src/hyperping/models/__init__.py index d693bf3..40b287c 100644 --- a/src/hyperping/models/__init__.py +++ b/src/hyperping/models/__init__.py @@ -56,7 +56,12 @@ ProbeLog, ProbeLogResponse, ) -from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember +from hyperping.models._oncall_models import ( + EscalationPolicy, + EscalationStep, + OnCallSchedule, + TeamMember, +) from hyperping.models._outage_models import ( Outage, OutageAction, @@ -133,6 +138,7 @@ "ProbeLogResponse", # On-call models "OnCallSchedule", + "EscalationStep", "EscalationPolicy", "TeamMember", # Integration models diff --git a/src/hyperping/models/_integration_models.py b/src/hyperping/models/_integration_models.py index e38f712..3287724 100644 --- a/src/hyperping/models/_integration_models.py +++ b/src/hyperping/models/_integration_models.py @@ -1,5 +1,7 @@ """Integration models: notification channel configuration.""" +from typing import Any + from pydantic import BaseModel, ConfigDict, Field @@ -10,5 +12,16 @@ class Integration(BaseModel): uuid: str = Field(..., description="Integration UUID") name: str = Field(..., description="Integration display name") - integration_type: str = Field(..., alias="type", description="Channel type") - active: bool = Field(default=True, description="Whether the integration is active") + integration_type: str = Field( + ..., alias="channel", description="Channel type (e.g. 'teams', 'slack')" + ) + created_by: str | None = Field( + default=None, + alias="createdBy", + description="Creator UUID (list endpoint) or email address (get endpoint)", + ) + created_at: str | None = Field( + default=None, alias="createdAt", description="ISO-8601 creation timestamp" + ) + region: str | None = Field(default=None, description="Deployment region, if set") + metadata: Any | None = Field(default=None, description="Arbitrary integration metadata") diff --git a/src/hyperping/models/_oncall_models.py b/src/hyperping/models/_oncall_models.py index d497058..911e1aa 100644 --- a/src/hyperping/models/_oncall_models.py +++ b/src/hyperping/models/_oncall_models.py @@ -1,7 +1,5 @@ """On-call models: schedules and escalation policies.""" -from typing import Any - from pydantic import BaseModel, ConfigDict, Field @@ -17,6 +15,21 @@ class OnCallSchedule(BaseModel): ) +class EscalationStep(BaseModel): + """Single step in an escalation policy.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True, frozen=True) + + uuid: str = Field(..., description="Step UUID") + wait_before: int = Field( + default=0, description="Minutes to wait before escalating to this step" + ) + channels: list[str] = Field(default_factory=list, description="Integration UUIDs to notify") + temp_id: str | None = Field( + default=None, alias="tempId", description="Temporary client-side ID" + ) + + class EscalationPolicy(BaseModel): """Escalation policy with step chain.""" @@ -24,7 +37,22 @@ class EscalationPolicy(BaseModel): uuid: str = Field(..., description="Policy UUID") name: str = Field(..., description="Policy name") - steps: list[dict[str, Any]] = Field(default_factory=list, description="Escalation steps") + steps: list[EscalationStep] = Field(default_factory=list, description="Escalation steps") + created_by: str | None = Field( + default=None, alias="createdBy", description="Creator UUID or email" + ) + created_at: str | None = Field( + default=None, alias="createdAt", description="ISO-8601 creation timestamp" + ) + grouped_alerts_window: int | None = Field( + default=None, description="Alert grouping window in seconds" + ) + grouped_alerts_enabled: int | None = Field( + default=None, description="Whether alert grouping is enabled (0/1)" + ) + monitor_count: int | None = Field( + default=None, alias="monitorCount", description="Number of monitors using this policy" + ) class TeamMember(BaseModel): @@ -39,4 +67,7 @@ class TeamMember(BaseModel): profile_picture_url: str | None = Field( default=None, alias="profilePictureUrl", description="Profile picture URL" ) + sso_picture_url: str | None = Field( + default=None, alias="ssoPictureUrl", description="SSO provider profile picture URL" + ) account_role: str = Field(default="", alias="accountRole", description="Role in project") From 2e329ad4f2aab3f17f931058a5139ae35451d604 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 04:45:43 +0300 Subject: [PATCH 2/5] test(models): update mocks to production shapes for Integration/EscalationPolicy/TeamMember (#9606db) - Sync test: use channel instead of type in Integration mocks, assert new fields - Sync test: enrich EscalationPolicy mocks with steps/createdAt/monitorCount, add EscalationStep assertions - Sync test: add ssoPictureUrl to TeamMember mock, assert sso_picture_url - Async test: mirror all sync mock updates and assertions --- tests/unit/test_async_mcp_client.py | 73 +++++++++++++++++++++++++---- tests/unit/test_mcp_client.py | 73 +++++++++++++++++++++++++---- 2 files changed, 128 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_async_mcp_client.py b/tests/unit/test_async_mcp_client.py index 3c76a2d..61c6f9f 100644 --- a/tests/unit/test_async_mcp_client.py +++ b/tests/unit/test_async_mcp_client.py @@ -12,7 +12,7 @@ from hyperping.models._integration_models import Integration from hyperping.models._monitor_models import Monitor from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse -from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember +from hyperping.models._oncall_models import EscalationPolicy, EscalationStep, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline from hyperping.models._reporting_models import ( AlertHistory, @@ -61,12 +61,18 @@ async def test_list_on_call_schedules(): async def test_list_team_members_bare_array(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "u1", "email": "a@b.com", "name": "A"}, + { + "uuid": "u1", + "email": "a@b.com", + "name": "A", + "ssoPictureUrl": "https://sso.example.com/pic.png", + }, ] result = await client.list_team_members() assert len(result) == 1 assert isinstance(result[0], TeamMember) assert result[0].email == "a@b.com" + assert result[0].sso_picture_url == "https://sso.example.com/pic.png" client._transport.call_tool.assert_called_once_with("list_team_members", {}) @@ -241,11 +247,30 @@ async def test_get_on_call_schedule(): async def test_list_escalation_policies(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "ep1", "name": "Default", "steps": []}, + { + "uuid": "ep1", + "name": "Core-Escalation", + "steps": [ + { + "uuid": "step_1", + "wait_before": 0, + "channels": ["int_abc"], + "tempId": "temp_123", + } + ], + "createdBy": None, + "createdAt": "2026-03-02T09:04:49.000Z", + "grouped_alerts_window": 300, + "grouped_alerts_enabled": 1, + "monitorCount": 69, + }, ] result = await client.list_escalation_policies() assert len(result) == 1 assert isinstance(result[0], EscalationPolicy) + assert result[0].monitor_count == 69 + assert isinstance(result[0].steps[0], EscalationStep) + assert result[0].steps[0].channels == ["int_abc"] client._transport.call_tool.assert_called_once_with("list_escalation_policies", {}) @@ -254,11 +279,26 @@ async def test_get_escalation_policy(): client = make_client() client._transport.call_tool.return_value = { "uuid": "ep1", - "name": "Default", - "steps": [], + "name": "Core-Escalation", + "steps": [ + { + "uuid": "step_1", + "wait_before": 5, + "channels": ["int_xyz"], + "tempId": "temp_456", + } + ], + "createdBy": None, + "createdAt": "2026-03-02T09:04:49.000Z", + "grouped_alerts_window": 300, + "grouped_alerts_enabled": 1, + "monitorCount": 42, } result = await client.get_escalation_policy("ep1") assert isinstance(result, EscalationPolicy) + assert result.monitor_count == 42 + assert isinstance(result.steps[0], EscalationStep) + assert result.steps[0].wait_before == 5 client._transport.call_tool.assert_called_once_with("get_escalation_policy", {"uuid": "ep1"}) @@ -266,11 +306,19 @@ async def test_get_escalation_policy(): async def test_list_integrations(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "int1", "name": "Slack", "type": "slack", "active": True}, + { + "uuid": "int1", + "name": "Teams", + "channel": "teams", + "createdBy": "usr_x", + "createdAt": "2026-03-03T15:00:59.000Z", + }, ] result = await client.list_integrations() assert len(result) == 1 assert isinstance(result[0], Integration) + assert result[0].integration_type == "teams" + assert result[0].created_by == "usr_x" client._transport.call_tool.assert_called_once_with("list_integrations", {}) @@ -279,12 +327,19 @@ async def test_get_integration(): client = make_client() client._transport.call_tool.return_value = { "uuid": "int1", - "name": "Slack", - "type": "slack", - "active": True, + "name": "Teams", + "channel": "teams", + "createdBy": "admin@example.com", + "createdAt": "2026-03-03T15:00:59.000Z", + "region": None, + "metadata": None, } result = await client.get_integration("int1") assert isinstance(result, Integration) + assert result.integration_type == "teams" + assert result.created_by == "admin@example.com" + assert result.created_at == "2026-03-03T15:00:59.000Z" + assert result.region is None client._transport.call_tool.assert_called_once_with("get_integration", {"uuid": "int1"}) diff --git a/tests/unit/test_mcp_client.py b/tests/unit/test_mcp_client.py index b55d608..c024b8f 100644 --- a/tests/unit/test_mcp_client.py +++ b/tests/unit/test_mcp_client.py @@ -13,7 +13,7 @@ from hyperping.models._integration_models import Integration from hyperping.models._monitor_models import Monitor from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse -from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember +from hyperping.models._oncall_models import EscalationPolicy, EscalationStep, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline from hyperping.models._reporting_models import ( AlertHistory, @@ -59,12 +59,18 @@ def test_list_on_call_schedules(): def test_list_team_members_bare_array(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "u1", "email": "a@b.com", "name": "A"}, + { + "uuid": "u1", + "email": "a@b.com", + "name": "A", + "ssoPictureUrl": "https://sso.example.com/pic.png", + }, ] result = client.list_team_members() assert len(result) == 1 assert isinstance(result[0], TeamMember) assert result[0].email == "a@b.com" + assert result[0].sso_picture_url == "https://sso.example.com/pic.png" client._transport.call_tool.assert_called_once_with("list_team_members", {}) @@ -222,11 +228,30 @@ def test_get_on_call_schedule(): def test_list_escalation_policies(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "ep1", "name": "Default", "steps": []}, + { + "uuid": "ep1", + "name": "Core-Escalation", + "steps": [ + { + "uuid": "step_1", + "wait_before": 0, + "channels": ["int_abc"], + "tempId": "temp_123", + } + ], + "createdBy": None, + "createdAt": "2026-03-02T09:04:49.000Z", + "grouped_alerts_window": 300, + "grouped_alerts_enabled": 1, + "monitorCount": 69, + }, ] result = client.list_escalation_policies() assert len(result) == 1 assert isinstance(result[0], EscalationPolicy) + assert result[0].monitor_count == 69 + assert isinstance(result[0].steps[0], EscalationStep) + assert result[0].steps[0].channels == ["int_abc"] client._transport.call_tool.assert_called_once_with("list_escalation_policies", {}) @@ -234,22 +259,45 @@ def test_get_escalation_policy(): client = make_client() client._transport.call_tool.return_value = { "uuid": "ep1", - "name": "Default", - "steps": [], + "name": "Core-Escalation", + "steps": [ + { + "uuid": "step_1", + "wait_before": 5, + "channels": ["int_xyz"], + "tempId": "temp_456", + } + ], + "createdBy": None, + "createdAt": "2026-03-02T09:04:49.000Z", + "grouped_alerts_window": 300, + "grouped_alerts_enabled": 1, + "monitorCount": 42, } result = client.get_escalation_policy("ep1") assert isinstance(result, EscalationPolicy) + assert result.monitor_count == 42 + assert isinstance(result.steps[0], EscalationStep) + assert result.steps[0].wait_before == 5 client._transport.call_tool.assert_called_once_with("get_escalation_policy", {"uuid": "ep1"}) def test_list_integrations(): client = make_client() client._transport.call_tool.return_value = [ - {"uuid": "int1", "name": "Slack", "type": "slack", "active": True}, + { + "uuid": "int1", + "name": "Teams", + "channel": "teams", + "createdBy": "usr_x", + "createdAt": "2026-03-03T15:00:59.000Z", + }, ] result = client.list_integrations() assert len(result) == 1 assert isinstance(result[0], Integration) + assert result[0].integration_type == "teams" + assert result[0].created_by == "usr_x" client._transport.call_tool.assert_called_once_with("list_integrations", {}) @@ -257,12 +305,19 @@ def test_get_integration(): client = make_client() client._transport.call_tool.return_value = { "uuid": "int1", - "name": "Slack", - "type": "slack", - "active": True, + "name": "Teams", + "channel": "teams", + "createdBy": "admin@example.com", + "createdAt": "2026-03-03T15:00:59.000Z", + "region": None, + "metadata": None, } result = client.get_integration("int1") assert isinstance(result, Integration) + assert result.integration_type == "teams" + assert result.created_by == "admin@example.com" + assert result.created_at == "2026-03-03T15:00:59.000Z" + assert result.region is None client._transport.call_tool.assert_called_once_with("get_integration", {"uuid": "int1"}) From e442477e431f1beac3b60ed82adda30d0b777d4d Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 10:05:00 +0300 Subject: [PATCH 3/5] chore: sync uv.lock version to v1.8.0 Co-Authored-By: Claude Haiku 4.5 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index f7589d6..827b823 100644 --- a/uv.lock +++ b/uv.lock @@ -335,7 +335,7 @@ wheels = [ [[package]] name = "hyperping" -version = "1.7.0" +version = "1.8.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From 0cfda0a4d09676189443be39629d08b9aec9c0c5 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 10:25:10 +0300 Subject: [PATCH 4/5] test(mcp): wrap oncall_models import to satisfy ruff E501/I001 Adding EscalationStep to the oncall_models import pushed the line past the 100-character limit. Split it across lines using parentheses so both E501 and I001 stay clean. --- tests/unit/test_async_mcp_client.py | 7 ++++++- tests/unit/test_mcp_client.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_async_mcp_client.py b/tests/unit/test_async_mcp_client.py index 61c6f9f..e281264 100644 --- a/tests/unit/test_async_mcp_client.py +++ b/tests/unit/test_async_mcp_client.py @@ -12,7 +12,12 @@ from hyperping.models._integration_models import Integration from hyperping.models._monitor_models import Monitor from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse -from hyperping.models._oncall_models import EscalationPolicy, EscalationStep, OnCallSchedule, TeamMember +from hyperping.models._oncall_models import ( + EscalationPolicy, + EscalationStep, + OnCallSchedule, + TeamMember, +) from hyperping.models._outage_models import OutageTimeline from hyperping.models._reporting_models import ( AlertHistory, diff --git a/tests/unit/test_mcp_client.py b/tests/unit/test_mcp_client.py index c024b8f..e2424fa 100644 --- a/tests/unit/test_mcp_client.py +++ b/tests/unit/test_mcp_client.py @@ -13,7 +13,12 @@ from hyperping.models._integration_models import Integration from hyperping.models._monitor_models import Monitor from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse -from hyperping.models._oncall_models import EscalationPolicy, EscalationStep, OnCallSchedule, TeamMember +from hyperping.models._oncall_models import ( + EscalationPolicy, + EscalationStep, + OnCallSchedule, + TeamMember, +) from hyperping.models._outage_models import OutageTimeline from hyperping.models._reporting_models import ( AlertHistory, From bc700ad7f4d2314133d38b17ae3ff6036e236907 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 23:25:40 +0300 Subject: [PATCH 5/5] chore: migrate httpx to httpx2 (PY-09) Replace httpx>=0.27,<1.0 with httpx2>=2.4,<3.0 in project dependencies. Update all four client construction sites (client.py, _async_client.py, _mcp_transport.py, _async_mcp_transport.py) to import httpx2 with an httpx alias so the rest of the code is unchanged. Since httpx2 uses httpcore2 instead of httpcore, extend respx's default HTTPCoreMocker with httpcore2 targets in conftest so @respx.mock continues to intercept test requests. Two test adjustments for type compatibility: - test_async_client.py: switch to httpx2 (no respx mock usage, direct _client.request patching); MagicMock(spec=httpx2.Response) and httpx2.ConnectError side effects now match the client exception handlers. - test_client_coverage.py: keep httpx for respx return values; replace httpx.ConnectError/TimeoutException side effects with httpcore2 equivalents so httpx2 exception mapper converts them to types the client catches. --- pyproject.toml | 2 +- src/hyperping/_async_client.py | 2 +- src/hyperping/_async_mcp_transport.py | 2 +- src/hyperping/_mcp_transport.py | 2 +- src/hyperping/client.py | 2 +- tests/unit/conftest.py | 12 +++++++ tests/unit/test_async_client.py | 2 +- tests/unit/test_client_coverage.py | 9 ++--- uv.lock | 48 ++++++++++++++++++++++++--- 9 files changed, 66 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 47d1ff4..5a22c82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Typing :: Typed", ] 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 827b823..b06c849 100644 --- a/uv.lock +++ b/uv.lock @@ -318,6 +318,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" @@ -333,12 +346,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" }, ] @@ -360,7 +389,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" }, @@ -377,11 +406,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]] @@ -1022,6 +1051,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 = "typing-extensions" version = "4.15.0"