Skip to content

Commit 86661fa

Browse files
fix(models): reconcile Integration/EscalationPolicy/TeamMember against production API (#34)
* 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 * 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 * chore: sync uv.lock version to v1.8.0 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * 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. --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent fe70f46 commit 86661fa

6 files changed

Lines changed: 196 additions & 24 deletions

File tree

src/hyperping/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
AlertHistory,
4545
DnsRecordType,
4646
EscalationPolicy,
47+
EscalationStep,
4748
Healthcheck,
4849
HealthcheckCreate,
4950
HealthcheckUpdate,
@@ -168,6 +169,7 @@
168169
"ProbeLogResponse",
169170
# On-call
170171
"OnCallSchedule",
172+
"EscalationStep",
171173
"EscalationPolicy",
172174
"TeamMember",
173175
# Integrations

src/hyperping/models/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@
5656
ProbeLog,
5757
ProbeLogResponse,
5858
)
59-
from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember
59+
from hyperping.models._oncall_models import (
60+
EscalationPolicy,
61+
EscalationStep,
62+
OnCallSchedule,
63+
TeamMember,
64+
)
6065
from hyperping.models._outage_models import (
6166
Outage,
6267
OutageAction,
@@ -133,6 +138,7 @@
133138
"ProbeLogResponse",
134139
# On-call models
135140
"OnCallSchedule",
141+
"EscalationStep",
136142
"EscalationPolicy",
137143
"TeamMember",
138144
# Integration models

src/hyperping/models/_integration_models.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Integration models: notification channel configuration."""
22

3+
from typing import Any
4+
35
from pydantic import BaseModel, ConfigDict, Field
46

57

@@ -10,5 +12,16 @@ class Integration(BaseModel):
1012

1113
uuid: str = Field(..., description="Integration UUID")
1214
name: str = Field(..., description="Integration display name")
13-
integration_type: str = Field(..., alias="type", description="Channel type")
14-
active: bool = Field(default=True, description="Whether the integration is active")
15+
integration_type: str = Field(
16+
..., alias="channel", description="Channel type (e.g. 'teams', 'slack')"
17+
)
18+
created_by: str | None = Field(
19+
default=None,
20+
alias="createdBy",
21+
description="Creator UUID (list endpoint) or email address (get endpoint)",
22+
)
23+
created_at: str | None = Field(
24+
default=None, alias="createdAt", description="ISO-8601 creation timestamp"
25+
)
26+
region: str | None = Field(default=None, description="Deployment region, if set")
27+
metadata: Any | None = Field(default=None, description="Arbitrary integration metadata")

src/hyperping/models/_oncall_models.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""On-call models: schedules and escalation policies."""
22

3-
from typing import Any
4-
53
from pydantic import BaseModel, ConfigDict, Field
64

75

@@ -17,14 +15,44 @@ class OnCallSchedule(BaseModel):
1715
)
1816

1917

18+
class EscalationStep(BaseModel):
19+
"""Single step in an escalation policy."""
20+
21+
model_config = ConfigDict(extra="allow", populate_by_name=True, frozen=True)
22+
23+
uuid: str = Field(..., description="Step UUID")
24+
wait_before: int = Field(
25+
default=0, description="Minutes to wait before escalating to this step"
26+
)
27+
channels: list[str] = Field(default_factory=list, description="Integration UUIDs to notify")
28+
temp_id: str | None = Field(
29+
default=None, alias="tempId", description="Temporary client-side ID"
30+
)
31+
32+
2033
class EscalationPolicy(BaseModel):
2134
"""Escalation policy with step chain."""
2235

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

2538
uuid: str = Field(..., description="Policy UUID")
2639
name: str = Field(..., description="Policy name")
27-
steps: list[dict[str, Any]] = Field(default_factory=list, description="Escalation steps")
40+
steps: list[EscalationStep] = Field(default_factory=list, description="Escalation steps")
41+
created_by: str | None = Field(
42+
default=None, alias="createdBy", description="Creator UUID or email"
43+
)
44+
created_at: str | None = Field(
45+
default=None, alias="createdAt", description="ISO-8601 creation timestamp"
46+
)
47+
grouped_alerts_window: int | None = Field(
48+
default=None, description="Alert grouping window in seconds"
49+
)
50+
grouped_alerts_enabled: int | None = Field(
51+
default=None, description="Whether alert grouping is enabled (0/1)"
52+
)
53+
monitor_count: int | None = Field(
54+
default=None, alias="monitorCount", description="Number of monitors using this policy"
55+
)
2856

2957

3058
class TeamMember(BaseModel):
@@ -39,4 +67,7 @@ class TeamMember(BaseModel):
3967
profile_picture_url: str | None = Field(
4068
default=None, alias="profilePictureUrl", description="Profile picture URL"
4169
)
70+
sso_picture_url: str | None = Field(
71+
default=None, alias="ssoPictureUrl", description="SSO provider profile picture URL"
72+
)
4273
account_role: str = Field(default="", alias="accountRole", description="Role in project")

tests/unit/test_async_mcp_client.py

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from hyperping.models._integration_models import Integration
1313
from hyperping.models._monitor_models import Monitor, MonitorCreate
1414
from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse
15-
from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember
15+
from hyperping.models._oncall_models import (
16+
EscalationPolicy,
17+
EscalationStep,
18+
OnCallSchedule,
19+
TeamMember,
20+
)
1621
from hyperping.models._outage_models import OutageTimeline
1722
from hyperping.models._reporting_models import (
1823
AlertHistory,
@@ -61,12 +66,18 @@ async def test_list_on_call_schedules():
6166
async def test_list_team_members_bare_array():
6267
client = make_client()
6368
client._transport.call_tool.return_value = [
64-
{"uuid": "u1", "email": "a@b.com", "name": "A"},
69+
{
70+
"uuid": "u1",
71+
"email": "a@b.com",
72+
"name": "A",
73+
"ssoPictureUrl": "https://sso.example.com/pic.png",
74+
},
6575
]
6676
result = await client.list_team_members()
6777
assert len(result) == 1
6878
assert isinstance(result[0], TeamMember)
6979
assert result[0].email == "a@b.com"
80+
assert result[0].sso_picture_url == "https://sso.example.com/pic.png"
7081
client._transport.call_tool.assert_called_once_with("list_team_members", {})
7182

7283

@@ -241,11 +252,30 @@ async def test_get_on_call_schedule():
241252
async def test_list_escalation_policies():
242253
client = make_client()
243254
client._transport.call_tool.return_value = [
244-
{"uuid": "ep1", "name": "Default", "steps": []},
255+
{
256+
"uuid": "ep1",
257+
"name": "Core-Escalation",
258+
"steps": [
259+
{
260+
"uuid": "step_1",
261+
"wait_before": 0,
262+
"channels": ["int_abc"],
263+
"tempId": "temp_123",
264+
}
265+
],
266+
"createdBy": None,
267+
"createdAt": "2026-03-02T09:04:49.000Z",
268+
"grouped_alerts_window": 300,
269+
"grouped_alerts_enabled": 1,
270+
"monitorCount": 69,
271+
},
245272
]
246273
result = await client.list_escalation_policies()
247274
assert len(result) == 1
248275
assert isinstance(result[0], EscalationPolicy)
276+
assert result[0].monitor_count == 69
277+
assert isinstance(result[0].steps[0], EscalationStep)
278+
assert result[0].steps[0].channels == ["int_abc"]
249279
client._transport.call_tool.assert_called_once_with("list_escalation_policies", {})
250280

251281

@@ -254,23 +284,46 @@ async def test_get_escalation_policy():
254284
client = make_client()
255285
client._transport.call_tool.return_value = {
256286
"uuid": "ep1",
257-
"name": "Default",
258-
"steps": [],
287+
"name": "Core-Escalation",
288+
"steps": [
289+
{
290+
"uuid": "step_1",
291+
"wait_before": 5,
292+
"channels": ["int_xyz"],
293+
"tempId": "temp_456",
294+
}
295+
],
296+
"createdBy": None,
297+
"createdAt": "2026-03-02T09:04:49.000Z",
298+
"grouped_alerts_window": 300,
299+
"grouped_alerts_enabled": 1,
300+
"monitorCount": 42,
259301
}
260302
result = await client.get_escalation_policy("ep1")
261303
assert isinstance(result, EscalationPolicy)
304+
assert result.monitor_count == 42
305+
assert isinstance(result.steps[0], EscalationStep)
306+
assert result.steps[0].wait_before == 5
262307
client._transport.call_tool.assert_called_once_with("get_escalation_policy", {"uuid": "ep1"})
263308

264309

265310
@pytest.mark.asyncio
266311
async def test_list_integrations():
267312
client = make_client()
268313
client._transport.call_tool.return_value = [
269-
{"uuid": "int1", "name": "Slack", "type": "slack", "active": True},
314+
{
315+
"uuid": "int1",
316+
"name": "Teams",
317+
"channel": "teams",
318+
"createdBy": "usr_x",
319+
"createdAt": "2026-03-03T15:00:59.000Z",
320+
},
270321
]
271322
result = await client.list_integrations()
272323
assert len(result) == 1
273324
assert isinstance(result[0], Integration)
325+
assert result[0].integration_type == "teams"
326+
assert result[0].created_by == "usr_x"
274327
client._transport.call_tool.assert_called_once_with("list_integrations", {})
275328

276329

@@ -279,12 +332,19 @@ async def test_get_integration():
279332
client = make_client()
280333
client._transport.call_tool.return_value = {
281334
"uuid": "int1",
282-
"name": "Slack",
283-
"type": "slack",
284-
"active": True,
335+
"name": "Teams",
336+
"channel": "teams",
337+
"createdBy": "admin@example.com",
338+
"createdAt": "2026-03-03T15:00:59.000Z",
339+
"region": None,
340+
"metadata": None,
285341
}
286342
result = await client.get_integration("int1")
287343
assert isinstance(result, Integration)
344+
assert result.integration_type == "teams"
345+
assert result.created_by == "admin@example.com"
346+
assert result.created_at == "2026-03-03T15:00:59.000Z"
347+
assert result.region is None
288348
client._transport.call_tool.assert_called_once_with("get_integration", {"uuid": "int1"})
289349

290350

0 commit comments

Comments
 (0)