hyperping 1.2.1__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {hyperping-1.2.1 → hyperping-1.4.0}/PKG-INFO +1 -1
  2. {hyperping-1.2.1 → hyperping-1.4.0}/pyproject.toml +1 -1
  3. hyperping-1.4.0/scripts/verify_endpoints.py +166 -0
  4. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/__init__.py +33 -5
  5. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_client.py +2 -3
  6. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_incidents_mixin.py +3 -9
  7. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_maintenance_mixin.py +3 -9
  8. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_monitors_mixin.py +2 -6
  9. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_outages_mixin.py +6 -13
  10. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_statuspages_mixin.py +15 -13
  11. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_incidents_mixin.py +3 -1
  12. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_internals.py +1 -4
  13. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_maintenance_mixin.py +3 -1
  14. hyperping-1.4.0/src/hyperping/_mcp_transport.py +145 -0
  15. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_monitors_mixin.py +1 -3
  16. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_outages_mixin.py +4 -7
  17. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_statuspages_mixin.py +12 -4
  18. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_utils.py +2 -5
  19. hyperping-1.4.0/src/hyperping/_version.py +1 -0
  20. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/client.py +6 -4
  21. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/endpoints.py +3 -0
  22. hyperping-1.4.0/src/hyperping/mcp_client.py +221 -0
  23. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/__init__.py +27 -1
  24. hyperping-1.4.0/src/hyperping/models/_integration_models.py +14 -0
  25. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_monitor_models.py +3 -8
  26. hyperping-1.4.0/src/hyperping/models/_observability_models.py +46 -0
  27. hyperping-1.4.0/src/hyperping/models/_oncall_models.py +27 -0
  28. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_outage_models.py +25 -0
  29. hyperping-1.4.0/src/hyperping/models/_reporting_models.py +17 -0
  30. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_async_client.py +14 -42
  31. hyperping-1.4.0/tests/unit/test_async_preexisting.py +325 -0
  32. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_healthchecks.py +8 -21
  33. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_incidents.py +1 -3
  34. hyperping-1.4.0/tests/unit/test_mcp_client.py +161 -0
  35. hyperping-1.4.0/tests/unit/test_mcp_transport.py +161 -0
  36. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_monitors.py +5 -15
  37. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_outages.py +3 -3
  38. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_pagination.py +1 -3
  39. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_statuspages.py +11 -15
  40. hyperping-1.2.1/src/hyperping/_version.py +0 -1
  41. {hyperping-1.2.1 → hyperping-1.4.0}/.gitignore +0 -0
  42. {hyperping-1.2.1 → hyperping-1.4.0}/CHANGELOG.md +0 -0
  43. {hyperping-1.2.1 → hyperping-1.4.0}/CONTRIBUTING.md +0 -0
  44. {hyperping-1.2.1 → hyperping-1.4.0}/LICENSE +0 -0
  45. {hyperping-1.2.1 → hyperping-1.4.0}/README.md +0 -0
  46. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_async_healthchecks_mixin.py +0 -0
  47. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_circuit_breaker.py +0 -0
  48. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_healthchecks_mixin.py +0 -0
  49. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_monitor_constants.py +0 -0
  50. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/_protocols.py +0 -0
  51. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/exceptions.py +0 -0
  52. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_healthcheck_models.py +0 -0
  53. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_incident_models.py +0 -0
  54. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_maintenance_models.py +0 -0
  55. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/models/_statuspage_models.py +0 -0
  56. {hyperping-1.2.1 → hyperping-1.4.0}/src/hyperping/py.typed +0 -0
  57. {hyperping-1.2.1 → hyperping-1.4.0}/tests/__init__.py +0 -0
  58. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/__init__.py +0 -0
  59. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/conftest.py +0 -0
  60. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_maintenance.py +0 -0
  61. {hyperping-1.2.1 → hyperping-1.4.0}/tests/unit/test_sdk_surface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperping
3
- Version: 1.2.1
3
+ Version: 1.4.0
4
4
  Summary: Python SDK for the Hyperping uptime monitoring and incident management API
5
5
  Project-URL: Homepage, https://github.com/develeap/hyperping-python
6
6
  Project-URL: Documentation, https://github.com/develeap/hyperping-python#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hyperping"
7
- version = "1.2.1"
7
+ version = "1.4.0"
8
8
  description = "Python SDK for the Hyperping uptime monitoring and incident management API"
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  license = {text = "MIT"}
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """Verify speculative MCP-discovered endpoint paths against the live Hyperping API.
3
+
4
+ Usage:
5
+ export HYPERPING_API_KEY=sk_...
6
+ python scripts/verify_endpoints.py
7
+
8
+ All calls are read-only (GET only). Safe to run repeatedly.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ from dataclasses import dataclass
14
+
15
+ import httpx
16
+
17
+ API_BASE = "https://api.hyperping.io"
18
+
19
+
20
+ @dataclass
21
+ class Result:
22
+ method: str
23
+ path: str
24
+ status: int
25
+ ok: bool
26
+ snippet: str
27
+
28
+
29
+ def probe(
30
+ client: httpx.Client,
31
+ method_name: str,
32
+ path: str,
33
+ params: dict | None = None,
34
+ ) -> Result:
35
+ """Send a GET request and return a Result."""
36
+ url = f"{API_BASE}{path}"
37
+ try:
38
+ resp = client.get(url, params=params or {})
39
+ body = resp.text[:120].replace("\n", " ")
40
+ return Result(
41
+ method=method_name,
42
+ path=path,
43
+ status=resp.status_code,
44
+ ok=resp.status_code < 400,
45
+ snippet=body,
46
+ )
47
+ except httpx.HTTPError as exc:
48
+ return Result(
49
+ method=method_name,
50
+ path=path,
51
+ status=0,
52
+ ok=False,
53
+ snippet=f"Connection error: {exc}",
54
+ )
55
+
56
+
57
+ def main() -> int:
58
+ api_key = os.environ.get("HYPERPING_API_KEY", "")
59
+ if not api_key:
60
+ print("ERROR: Set HYPERPING_API_KEY environment variable.")
61
+ return 1
62
+
63
+ headers = {"Authorization": f"Bearer {api_key}"}
64
+ client = httpx.Client(headers=headers, timeout=15.0)
65
+
66
+ # -- Fetch real UUIDs for sub-resource endpoints --
67
+ print("Fetching monitor and outage UUIDs for sub-resource tests...")
68
+ monitor_uuid: str | None = None
69
+ outage_uuid: str | None = None
70
+
71
+ resp = client.get(f"{API_BASE}/v1/monitors")
72
+ if resp.status_code == 200:
73
+ data = resp.json()
74
+ monitors = data if isinstance(data, list) else data.get("monitors", [])
75
+ if monitors:
76
+ monitor_uuid = monitors[0].get("uuid") or monitors[0].get("id")
77
+ print(f" Monitor UUID: {monitor_uuid}")
78
+
79
+ resp = client.get(f"{API_BASE}/v2/outages")
80
+ if resp.status_code == 200:
81
+ data = resp.json()
82
+ outages = data if isinstance(data, list) else data.get("outages", [])
83
+ if outages:
84
+ outage_uuid = outages[0].get("uuid") or outages[0].get("id")
85
+ print(f" Outage UUID: {outage_uuid}")
86
+
87
+ print()
88
+
89
+ # -- Define all speculative endpoints to verify --
90
+ checks: list[tuple[str, str, dict | None]] = [
91
+ # Endpoint enum speculative paths
92
+ ("get_status_summary", "/v2/status-summary", None),
93
+ ("list_recent_alerts", "/v2/alerts", None),
94
+ ("list_on_call_schedules", "/v2/on-call-schedules", None),
95
+ ("list_escalation_policies", "/v2/escalation-policies", None),
96
+ ("list_team_members", "/v2/team-members", None),
97
+ ("list_integrations", "/v2/integrations", None),
98
+ # Monitor search
99
+ ("search_monitors", "/v1/monitors/search", {"query": "test"}),
100
+ ]
101
+
102
+ # Sub-resource paths needing a real UUID
103
+ if monitor_uuid:
104
+ checks.extend([
105
+ (
106
+ "get_monitor_response_time",
107
+ f"/v2/reporting/response-time/{monitor_uuid}",
108
+ {"period": "24h"},
109
+ ),
110
+ (
111
+ "get_monitor_mtta",
112
+ f"/v2/reporting/mtta/{monitor_uuid}",
113
+ {"period": "30d"},
114
+ ),
115
+ (
116
+ "get_monitor_anomalies",
117
+ f"/v1/monitors/{monitor_uuid}/anomalies",
118
+ None,
119
+ ),
120
+ (
121
+ "get_monitor_http_logs",
122
+ f"/v1/monitors/{monitor_uuid}/http-logs",
123
+ {"page": "0", "limit": "5"},
124
+ ),
125
+ ])
126
+ else:
127
+ print("WARNING: No monitors found; skipping monitor sub-resource checks.\n")
128
+
129
+ if outage_uuid:
130
+ checks.append((
131
+ "get_outage_timeline",
132
+ f"/v2/outages/{outage_uuid}/timeline",
133
+ None,
134
+ ))
135
+ else:
136
+ print("WARNING: No outages found; skipping outage sub-resource checks.\n")
137
+
138
+ # -- Run probes --
139
+ results: list[Result] = []
140
+ for method_name, path, params in checks:
141
+ r = probe(client, method_name, path, params)
142
+ results.append(r)
143
+
144
+ client.close()
145
+
146
+ # -- Print results table --
147
+ print(f"{'Method':<30} {'Path':<50} {'Status':<8} {'Result'}")
148
+ print("-" * 120)
149
+ for r in results:
150
+ tag = "OK" if r.ok else ("404" if r.status == 404 else "ERROR")
151
+ print(f"{r.method:<30} {r.path:<50} {r.status:<8} {tag}")
152
+ if not r.ok:
153
+ print(f" {r.snippet}")
154
+
155
+ # -- Summary --
156
+ ok_count = sum(1 for r in results if r.ok)
157
+ fail_count = sum(1 for r in results if not r.ok)
158
+ not_found = sum(1 for r in results if r.status == 404)
159
+ print()
160
+ print(f"Total: {len(results)} | OK: {ok_count} | 404: {not_found} | Other errors: {fail_count - not_found}")
161
+
162
+ return 1 if fail_count > 0 else 0
163
+
164
+
165
+ if __name__ == "__main__":
166
+ sys.exit(main())
@@ -15,6 +15,7 @@ Quick start::
15
15
  """
16
16
 
17
17
  from hyperping._async_client import AsyncHyperpingClient
18
+ from hyperping._mcp_transport import MCP_URL
18
19
  from hyperping._version import __version__
19
20
  from hyperping.client import (
20
21
  CircuitBreaker,
@@ -35,10 +36,13 @@ from hyperping.exceptions import (
35
36
  HyperpingRateLimitError,
36
37
  HyperpingValidationError,
37
38
  )
39
+ from hyperping.mcp_client import HyperpingMcpClient
38
40
  from hyperping.models import (
39
41
  DEFAULT_REGIONS,
40
42
  AddIncidentUpdateRequest,
43
+ AlertNotification,
41
44
  DnsRecordType,
45
+ EscalationPolicy,
42
46
  Healthcheck,
43
47
  HealthcheckCreate,
44
48
  HealthcheckUpdate,
@@ -49,11 +53,13 @@ from hyperping.models import (
49
53
  IncidentUpdate,
50
54
  IncidentUpdateRequest,
51
55
  IncidentUpdateType,
56
+ Integration,
52
57
  LocalizedText,
53
58
  Maintenance,
54
59
  MaintenanceCreate,
55
60
  MaintenanceUpdate,
56
61
  Monitor,
62
+ MonitorAnomaly,
57
63
  MonitorBase,
58
64
  MonitorCreate,
59
65
  MonitorFrequency,
@@ -63,10 +69,14 @@ from hyperping.models import (
63
69
  MonitorTimeout,
64
70
  MonitorUpdate,
65
71
  NotificationOption,
72
+ OnCallSchedule,
66
73
  Outage,
67
74
  OutageAction,
68
75
  OutageDetail,
69
76
  OutageStats,
77
+ OutageTimeline,
78
+ OutageTimelineEvent,
79
+ ProbeLog,
70
80
  Region,
71
81
  ReportPeriod,
72
82
  RequestHeader,
@@ -74,6 +84,7 @@ from hyperping.models import (
74
84
  StatusPageCreate,
75
85
  StatusPageSubscriber,
76
86
  StatusPageUpdate,
87
+ StatusSummary,
77
88
  )
78
89
 
79
90
  __all__ = [
@@ -82,6 +93,9 @@ __all__ = [
82
93
  # Clients
83
94
  "AsyncHyperpingClient",
84
95
  "HyperpingClient",
96
+ "HyperpingMcpClient",
97
+ # MCP
98
+ "MCP_URL",
85
99
  # Configuration
86
100
  "RetryConfig",
87
101
  "CircuitBreakerConfig",
@@ -137,6 +151,19 @@ __all__ = [
137
151
  # Outages
138
152
  "Outage",
139
153
  "OutageAction",
154
+ "OutageTimeline",
155
+ "OutageTimelineEvent",
156
+ # Observability
157
+ "MonitorAnomaly",
158
+ "ProbeLog",
159
+ "AlertNotification",
160
+ # On-call
161
+ "OnCallSchedule",
162
+ "EscalationPolicy",
163
+ # Integrations
164
+ "Integration",
165
+ # Reporting
166
+ "StatusSummary",
140
167
  # Healthchecks
141
168
  "Healthcheck",
142
169
  "HealthcheckCreate",
@@ -161,8 +188,7 @@ def __getattr__(name: str) -> object:
161
188
 
162
189
  if name == "HYPERPING_API_BASE":
163
190
  warnings.warn(
164
- "HYPERPING_API_BASE is deprecated and will be removed in v0.3.0. "
165
- "Use API_BASE instead.",
191
+ "HYPERPING_API_BASE is deprecated and will be removed in v0.3.0. Use API_BASE instead.",
166
192
  DeprecationWarning,
167
193
  stacklevel=2,
168
194
  )
@@ -170,8 +196,7 @@ def __getattr__(name: str) -> object:
170
196
 
171
197
  if name == "API_PATHS":
172
198
  warnings.warn(
173
- "API_PATHS is deprecated and will be removed in v0.3.0. "
174
- "Use the Endpoint enum instead.",
199
+ "API_PATHS is deprecated and will be removed in v0.3.0. Use the Endpoint enum instead.",
175
200
  DeprecationWarning,
176
201
  stacklevel=2,
177
202
  )
@@ -199,7 +224,10 @@ def __getattr__(name: str) -> object:
199
224
 
200
225
  # Symbols removed from __all__ (H5) but still accessible for backward compat
201
226
  _endpoint_helpers = {
202
- "EndpointConfig", "ENDPOINTS", "get_endpoint_url", "get_version_for_endpoint",
227
+ "EndpointConfig",
228
+ "ENDPOINTS",
229
+ "get_endpoint_url",
230
+ "get_version_for_endpoint",
203
231
  }
204
232
  if name in _endpoint_helpers:
205
233
  from hyperping import endpoints as _ep
@@ -178,6 +178,7 @@ class AsyncHyperpingClient(
178
178
  )
179
179
  if status in (400, 422):
180
180
  from hyperping.exceptions import HyperpingValidationError
181
+
181
182
  raise HyperpingValidationError(
182
183
  message=f"Validation error: {error_msg}",
183
184
  status_code=status,
@@ -321,9 +322,7 @@ class AsyncHyperpingClient(
321
322
  continue
322
323
  self._circuit_breaker.record_failure()
323
324
  if isinstance(e, httpx.TimeoutException):
324
- raise HyperpingAPIError(
325
- f"Request timeout after {max_attempts} attempts"
326
- ) from e
325
+ raise HyperpingAPIError(f"Request timeout after {max_attempts} attempts") from e
327
326
  raise HyperpingAPIError(f"Request failed: {e}") from e
328
327
 
329
328
  raise HyperpingAPIError( # pragma: no cover
@@ -45,9 +45,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
45
45
  if status:
46
46
  params["status"] = status
47
47
 
48
- response = await self._request(
49
- "GET", Endpoint.INCIDENTS, params=params or None
50
- )
48
+ response = await self._request("GET", Endpoint.INCIDENTS, params=params or None)
51
49
  return parse_list(unwrap_list(response, "incidents"), Incident, "incident")
52
50
 
53
51
  async def get_incident(self, incident_id: str) -> Incident:
@@ -114,9 +112,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
114
112
  validate_id(incident_id, "incident_id")
115
113
  payload = update.model_dump(exclude_none=True, by_alias=True)
116
114
  response = expect_dict(
117
- await self._request(
118
- "PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload
119
- ),
115
+ await self._request("PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload),
120
116
  "update_incident",
121
117
  )
122
118
  return Incident.model_validate(response)
@@ -145,9 +141,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
145
141
  await self._request("POST", url, json=payload)
146
142
  return await self.get_incident(incident_id)
147
143
 
148
- async def resolve_incident(
149
- self, incident_id: str, message: str | None = None
150
- ) -> Incident:
144
+ async def resolve_incident(self, incident_id: str, message: str | None = None) -> Incident:
151
145
  """Resolve an incident.
152
146
 
153
147
  Args:
@@ -41,9 +41,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
41
41
  if status:
42
42
  params["status"] = status
43
43
 
44
- response = await self._request(
45
- "GET", Endpoint.MAINTENANCE, params=params or None
46
- )
44
+ response = await self._request("GET", Endpoint.MAINTENANCE, params=params or None)
47
45
 
48
46
  raw = unwrap_list(response, "maintenanceWindows")
49
47
  if not raw and isinstance(response, dict) and "maintenance" in response:
@@ -64,9 +62,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
64
62
  HyperpingNotFoundError: If maintenance not found
65
63
  """
66
64
  validate_id(maintenance_id, "maintenance_id")
67
- response = await self._request(
68
- "GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}"
69
- )
65
+ response = await self._request("GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}")
70
66
  return Maintenance.model_validate(expect_dict(response, "get_maintenance"))
71
67
 
72
68
  async def create_maintenance(self, maintenance: MaintenanceCreate) -> Maintenance:
@@ -127,9 +123,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
127
123
  payload.update(partial)
128
124
 
129
125
  response = expect_dict(
130
- await self._request(
131
- "PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload
132
- ),
126
+ await self._request("PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload),
133
127
  "update_maintenance",
134
128
  )
135
129
  return Maintenance.model_validate(response)
@@ -99,9 +99,7 @@ class AsyncMonitorsMixin(_AsyncClientProtocol):
99
99
  )
100
100
  payload.update(update.model_dump(exclude_none=True))
101
101
 
102
- response = await self._request(
103
- "PUT", f"{Endpoint.MONITORS}/{monitor_id}", json=payload
104
- )
102
+ response = await self._request("PUT", f"{Endpoint.MONITORS}/{monitor_id}", json=payload)
105
103
  return Monitor.model_validate(expect_dict(response, "update_monitor"))
106
104
 
107
105
  async def delete_monitor(self, monitor_id: str) -> None:
@@ -156,9 +154,7 @@ class AsyncMonitorsMixin(_AsyncClientProtocol):
156
154
  HyperpingAPIError: On unexpected API errors.
157
155
  """
158
156
  if period not in VALID_PERIODS:
159
- raise ValueError(
160
- f"Invalid period {period!r}. Valid values: {sorted(VALID_PERIODS)}"
161
- )
157
+ raise ValueError(f"Invalid period {period!r}. Valid values: {sorted(VALID_PERIODS)}")
162
158
  response = expect_dict(
163
159
  await self._request("GET", Endpoint.REPORTS, params={"period": period}),
164
160
  "get_all_reports",
@@ -56,9 +56,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
56
56
  ValueError: If *status* or *outage_type* is not a recognised value.
57
57
  """
58
58
  if status not in _VALID_STATUSES:
59
- raise ValueError(
60
- f"Invalid status {status!r}. Valid values: {sorted(_VALID_STATUSES)}"
61
- )
59
+ raise ValueError(f"Invalid status {status!r}. Valid values: {sorted(_VALID_STATUSES)}")
62
60
  if outage_type not in _VALID_TYPES:
63
61
  raise ValueError(
64
62
  f"Invalid outage_type {outage_type!r}. Valid values: {sorted(_VALID_TYPES)}"
@@ -75,7 +73,8 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
75
73
  params["page"] = page
76
74
  data = await self._request("GET", Endpoint.OUTAGES, params=params)
77
75
  raw: list[Any] = (
78
- data.get("outages", []) if isinstance(data, dict)
76
+ data.get("outages", [])
77
+ if isinstance(data, dict)
79
78
  else (data if isinstance(data, list) else [])
80
79
  )
81
80
  return parse_list(raw, Outage, "outage")
@@ -86,9 +85,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
86
85
  logger.debug("Outage endpoint not available (404)")
87
86
  return []
88
87
 
89
- async def acknowledge_outage(
90
- self, outage_id: str, message: str | None = None
91
- ) -> OutageAction:
88
+ async def acknowledge_outage(self, outage_id: str, message: str | None = None) -> OutageAction:
92
89
  """Acknowledge an outage.
93
90
 
94
91
  Args:
@@ -110,9 +107,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
110
107
  )
111
108
  return OutageAction.from_raw(expect_dict(result, "outage operation"))
112
109
 
113
- async def resolve_outage(
114
- self, outage_id: str, message: str | None = None
115
- ) -> OutageAction:
110
+ async def resolve_outage(self, outage_id: str, message: str | None = None) -> OutageAction:
116
111
  """Resolve an outage.
117
112
 
118
113
  Args:
@@ -163,9 +158,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
163
158
  HyperpingNotFoundError: If outage not found.
164
159
  """
165
160
  validate_id(outage_id, "outage_id")
166
- result = await self._request(
167
- "POST", f"{Endpoint.OUTAGES}/{outage_id}/unacknowledge"
168
- )
161
+ result = await self._request("POST", f"{Endpoint.OUTAGES}/{outage_id}/unacknowledge")
169
162
  return OutageAction.from_raw(expect_dict(result, "outage operation"))
170
163
 
171
164
  async def delete_outage(self, outage_id: str) -> None:
@@ -69,8 +69,12 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
69
69
 
70
70
  try:
71
71
  return await collect_all_pages_async(
72
- self._request, Endpoint.STATUSPAGES, "statuspages",
73
- params or None, StatusPage, "status page",
72
+ self._request,
73
+ Endpoint.STATUSPAGES,
74
+ "statuspages",
75
+ params or None,
76
+ StatusPage,
77
+ "status page",
74
78
  )
75
79
  except HyperpingNotFoundError:
76
80
  logger.debug("Status pages endpoint not available (404)")
@@ -131,9 +135,7 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
131
135
  validate_id(status_page_id, "status_page_id")
132
136
  payload = update.model_dump(exclude_none=True, by_alias=True)
133
137
  response = expect_dict(
134
- await self._request(
135
- "PUT", f"{Endpoint.STATUSPAGES}/{status_page_id}", json=payload
136
- ),
138
+ await self._request("PUT", f"{Endpoint.STATUSPAGES}/{status_page_id}", json=payload),
137
139
  "update_status_page",
138
140
  )
139
141
  return StatusPage.model_validate(response)
@@ -189,13 +191,15 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
189
191
  )
190
192
 
191
193
  return await collect_all_pages_async(
192
- self._request, endpoint, "subscribers",
193
- params or None, StatusPageSubscriber, "subscriber",
194
+ self._request,
195
+ endpoint,
196
+ "subscribers",
197
+ params or None,
198
+ StatusPageSubscriber,
199
+ "subscriber",
194
200
  )
195
201
 
196
- async def add_subscriber(
197
- self, status_page_id: str, email: str
198
- ) -> StatusPageSubscriber:
202
+ async def add_subscriber(self, status_page_id: str, email: str) -> StatusPageSubscriber:
199
203
  """Add a subscriber to a status page.
200
204
 
201
205
  Args:
@@ -225,9 +229,7 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
225
229
  )
226
230
  return StatusPageSubscriber.model_validate(response)
227
231
 
228
- async def remove_subscriber(
229
- self, status_page_id: str, subscriber_id: str
230
- ) -> None:
232
+ async def remove_subscriber(self, status_page_id: str, subscriber_id: str) -> None:
231
233
  """Remove a subscriber from a status page.
232
234
 
233
235
  Args:
@@ -48,7 +48,9 @@ class IncidentsMixin(_ClientProtocol):
48
48
  params["status"] = status
49
49
 
50
50
  response = self._request(
51
- "GET", Endpoint.INCIDENTS, params=params or None # M20
51
+ "GET",
52
+ Endpoint.INCIDENTS,
53
+ params=params or None, # M20
52
54
  )
53
55
  return parse_list(unwrap_list(response, "incidents"), Incident, "incident")
54
56
 
@@ -28,7 +28,4 @@ def sanitize_for_log(data: dict[str, Any] | None) -> dict[str, Any] | None:
28
28
  """
29
29
  if data is None:
30
30
  return None
31
- return {
32
- k: "[REDACTED]" if k.lower() in _SENSITIVE_LOG_KEYS else v
33
- for k, v in data.items()
34
- }
31
+ return {k: "[REDACTED]" if k.lower() in _SENSITIVE_LOG_KEYS else v for k, v in data.items()}
@@ -43,7 +43,9 @@ class MaintenanceMixin(_ClientProtocol):
43
43
  params["status"] = status
44
44
 
45
45
  response = self._request(
46
- "GET", Endpoint.MAINTENANCE, params=params or None # M20
46
+ "GET",
47
+ Endpoint.MAINTENANCE,
48
+ params=params or None, # M20
47
49
  )
48
50
 
49
51
  # API returns {"maintenanceWindows": [...]} as of current version