From 27289e3e04a2edaa2389b76e76c2ddc3465b7b07 Mon Sep 17 00:00:00 2001 From: Khaled Salhab Date: Sat, 13 Jun 2026 04:45:38 +0300 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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,