Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/hyperping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
AlertHistory,
DnsRecordType,
EscalationPolicy,
EscalationStep,
Healthcheck,
HealthcheckCreate,
HealthcheckUpdate,
Expand Down Expand Up @@ -168,6 +169,7 @@
"ProbeLogResponse",
# On-call
"OnCallSchedule",
"EscalationStep",
"EscalationPolicy",
"TeamMember",
# Integrations
Expand Down
8 changes: 7 additions & 1 deletion src/hyperping/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -133,6 +138,7 @@
"ProbeLogResponse",
# On-call models
"OnCallSchedule",
"EscalationStep",
"EscalationPolicy",
"TeamMember",
# Integration models
Expand Down
17 changes: 15 additions & 2 deletions src/hyperping/models/_integration_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Integration models: notification channel configuration."""

from typing import Any

from pydantic import BaseModel, ConfigDict, Field


Expand All @@ -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")
37 changes: 34 additions & 3 deletions src/hyperping/models/_oncall_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""On-call models: schedules and escalation policies."""

from typing import Any

from pydantic import BaseModel, ConfigDict, Field


Expand All @@ -17,14 +15,44 @@ 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."""

model_config = ConfigDict(extra="allow", populate_by_name=True, frozen=True)

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):
Expand All @@ -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")
78 changes: 69 additions & 9 deletions tests/unit/test_async_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", {})


Expand Down Expand Up @@ -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", {})


Expand All @@ -254,23 +284,46 @@ 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"})


@pytest.mark.asyncio
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", {})


Expand All @@ -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"})


Expand Down
Loading