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") diff --git a/tests/unit/test_async_mcp_client.py b/tests/unit/test_async_mcp_client.py index 3c76a2d..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, 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 +66,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 +252,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 +284,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 +311,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 +332,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..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, 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 +64,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 +233,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 +264,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 +310,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"}) 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" },