hyperping 1.1.0__tar.gz → 1.3.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 (69) hide show
  1. {hyperping-1.1.0 → hyperping-1.3.0}/PKG-INFO +2 -2
  2. {hyperping-1.1.0 → hyperping-1.3.0}/pyproject.toml +2 -2
  3. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/__init__.py +28 -5
  4. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_client.py +10 -3
  5. hyperping-1.3.0/src/hyperping/_async_integrations_mixin.py +26 -0
  6. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_monitors_mixin.py +15 -6
  7. hyperping-1.3.0/src/hyperping/_async_observability_mixin.py +65 -0
  8. hyperping-1.3.0/src/hyperping/_async_oncall_mixin.py +53 -0
  9. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_outages_mixin.py +37 -13
  10. hyperping-1.3.0/src/hyperping/_async_reporting_mixin.py +39 -0
  11. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_incidents_mixin.py +11 -1
  12. hyperping-1.3.0/src/hyperping/_integrations_mixin.py +41 -0
  13. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_maintenance_mixin.py +5 -6
  14. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_monitors_mixin.py +21 -3
  15. hyperping-1.3.0/src/hyperping/_observability_mixin.py +102 -0
  16. hyperping-1.3.0/src/hyperping/_oncall_mixin.py +88 -0
  17. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_outages_mixin.py +119 -4
  18. hyperping-1.3.0/src/hyperping/_reporting_mixin.py +71 -0
  19. hyperping-1.3.0/src/hyperping/_version.py +1 -0
  20. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/client.py +14 -4
  21. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/endpoints.py +75 -0
  22. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/__init__.py +27 -1
  23. hyperping-1.3.0/src/hyperping/models/_integration_models.py +14 -0
  24. hyperping-1.3.0/src/hyperping/models/_observability_models.py +46 -0
  25. hyperping-1.3.0/src/hyperping/models/_oncall_models.py +27 -0
  26. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_outage_models.py +25 -0
  27. hyperping-1.3.0/src/hyperping/models/_reporting_models.py +17 -0
  28. hyperping-1.3.0/tests/unit/test_async_new_mixins.py +262 -0
  29. hyperping-1.3.0/tests/unit/test_async_preexisting.py +331 -0
  30. hyperping-1.3.0/tests/unit/test_integrations.py +98 -0
  31. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_monitors.py +43 -0
  32. hyperping-1.3.0/tests/unit/test_observability.py +203 -0
  33. hyperping-1.3.0/tests/unit/test_oncall.py +217 -0
  34. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_outages.py +98 -1
  35. hyperping-1.3.0/tests/unit/test_reporting.py +140 -0
  36. hyperping-1.1.0/src/hyperping/_version.py +0 -1
  37. {hyperping-1.1.0 → hyperping-1.3.0}/.gitignore +0 -0
  38. {hyperping-1.1.0 → hyperping-1.3.0}/CHANGELOG.md +0 -0
  39. {hyperping-1.1.0 → hyperping-1.3.0}/CONTRIBUTING.md +0 -0
  40. {hyperping-1.1.0 → hyperping-1.3.0}/LICENSE +0 -0
  41. {hyperping-1.1.0 → hyperping-1.3.0}/README.md +0 -0
  42. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_healthchecks_mixin.py +0 -0
  43. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_incidents_mixin.py +0 -0
  44. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_maintenance_mixin.py +0 -0
  45. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_statuspages_mixin.py +0 -0
  46. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_circuit_breaker.py +0 -0
  47. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_healthchecks_mixin.py +0 -0
  48. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_internals.py +0 -0
  49. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_monitor_constants.py +0 -0
  50. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_protocols.py +0 -0
  51. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_statuspages_mixin.py +0 -0
  52. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_utils.py +0 -0
  53. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/exceptions.py +0 -0
  54. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_healthcheck_models.py +0 -0
  55. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_incident_models.py +0 -0
  56. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_maintenance_models.py +0 -0
  57. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_monitor_models.py +0 -0
  58. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_statuspage_models.py +0 -0
  59. {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/py.typed +0 -0
  60. {hyperping-1.1.0 → hyperping-1.3.0}/tests/__init__.py +0 -0
  61. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/__init__.py +0 -0
  62. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/conftest.py +0 -0
  63. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_async_client.py +0 -0
  64. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_healthchecks.py +0 -0
  65. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_incidents.py +0 -0
  66. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_maintenance.py +0 -0
  67. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_pagination.py +0 -0
  68. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_sdk_surface.py +0 -0
  69. {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_statuspages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperping
3
- Version: 1.1.0
3
+ Version: 1.3.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
@@ -30,7 +30,7 @@ Requires-Dist: mypy>=1.10; extra == 'dev'
30
30
  Requires-Dist: pip-audit>=2.7; extra == 'dev'
31
31
  Requires-Dist: pydantic; extra == 'dev'
32
32
  Requires-Dist: pytest-cov; extra == 'dev'
33
- Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
34
34
  Requires-Dist: respx>=0.21; extra == 'dev'
35
35
  Requires-Dist: ruff>=0.4; extra == 'dev'
36
36
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hyperping"
7
- version = "1.1.0"
7
+ version = "1.3.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"}
@@ -31,7 +31,7 @@ dependencies = [
31
31
 
32
32
  [project.optional-dependencies]
33
33
  dev = [
34
- "pytest>=8.0",
34
+ "pytest>=9.0.3",
35
35
  "pytest-cov",
36
36
  "respx>=0.21",
37
37
  "ruff>=0.4",
@@ -38,7 +38,9 @@ from hyperping.exceptions import (
38
38
  from hyperping.models import (
39
39
  DEFAULT_REGIONS,
40
40
  AddIncidentUpdateRequest,
41
+ AlertNotification,
41
42
  DnsRecordType,
43
+ EscalationPolicy,
42
44
  Healthcheck,
43
45
  HealthcheckCreate,
44
46
  HealthcheckUpdate,
@@ -49,11 +51,13 @@ from hyperping.models import (
49
51
  IncidentUpdate,
50
52
  IncidentUpdateRequest,
51
53
  IncidentUpdateType,
54
+ Integration,
52
55
  LocalizedText,
53
56
  Maintenance,
54
57
  MaintenanceCreate,
55
58
  MaintenanceUpdate,
56
59
  Monitor,
60
+ MonitorAnomaly,
57
61
  MonitorBase,
58
62
  MonitorCreate,
59
63
  MonitorFrequency,
@@ -63,10 +67,14 @@ from hyperping.models import (
63
67
  MonitorTimeout,
64
68
  MonitorUpdate,
65
69
  NotificationOption,
70
+ OnCallSchedule,
66
71
  Outage,
67
72
  OutageAction,
68
73
  OutageDetail,
69
74
  OutageStats,
75
+ OutageTimeline,
76
+ OutageTimelineEvent,
77
+ ProbeLog,
70
78
  Region,
71
79
  ReportPeriod,
72
80
  RequestHeader,
@@ -74,6 +82,7 @@ from hyperping.models import (
74
82
  StatusPageCreate,
75
83
  StatusPageSubscriber,
76
84
  StatusPageUpdate,
85
+ StatusSummary,
77
86
  )
78
87
 
79
88
  __all__ = [
@@ -137,6 +146,19 @@ __all__ = [
137
146
  # Outages
138
147
  "Outage",
139
148
  "OutageAction",
149
+ "OutageTimeline",
150
+ "OutageTimelineEvent",
151
+ # Observability
152
+ "MonitorAnomaly",
153
+ "ProbeLog",
154
+ "AlertNotification",
155
+ # On-call
156
+ "OnCallSchedule",
157
+ "EscalationPolicy",
158
+ # Integrations
159
+ "Integration",
160
+ # Reporting
161
+ "StatusSummary",
140
162
  # Healthchecks
141
163
  "Healthcheck",
142
164
  "HealthcheckCreate",
@@ -161,8 +183,7 @@ def __getattr__(name: str) -> object:
161
183
 
162
184
  if name == "HYPERPING_API_BASE":
163
185
  warnings.warn(
164
- "HYPERPING_API_BASE is deprecated and will be removed in v0.3.0. "
165
- "Use API_BASE instead.",
186
+ "HYPERPING_API_BASE is deprecated and will be removed in v0.3.0. Use API_BASE instead.",
166
187
  DeprecationWarning,
167
188
  stacklevel=2,
168
189
  )
@@ -170,8 +191,7 @@ def __getattr__(name: str) -> object:
170
191
 
171
192
  if name == "API_PATHS":
172
193
  warnings.warn(
173
- "API_PATHS is deprecated and will be removed in v0.3.0. "
174
- "Use the Endpoint enum instead.",
194
+ "API_PATHS is deprecated and will be removed in v0.3.0. Use the Endpoint enum instead.",
175
195
  DeprecationWarning,
176
196
  stacklevel=2,
177
197
  )
@@ -199,7 +219,10 @@ def __getattr__(name: str) -> object:
199
219
 
200
220
  # Symbols removed from __all__ (H5) but still accessible for backward compat
201
221
  _endpoint_helpers = {
202
- "EndpointConfig", "ENDPOINTS", "get_endpoint_url", "get_version_for_endpoint",
222
+ "EndpointConfig",
223
+ "ENDPOINTS",
224
+ "get_endpoint_url",
225
+ "get_version_for_endpoint",
203
226
  }
204
227
  if name in _endpoint_helpers:
205
228
  from hyperping import endpoints as _ep
@@ -21,9 +21,13 @@ from pydantic import SecretStr
21
21
 
22
22
  from hyperping._async_healthchecks_mixin import AsyncHealthchecksMixin
23
23
  from hyperping._async_incidents_mixin import AsyncIncidentsMixin
24
+ from hyperping._async_integrations_mixin import AsyncIntegrationsMixin
24
25
  from hyperping._async_maintenance_mixin import AsyncMaintenanceMixin
25
26
  from hyperping._async_monitors_mixin import AsyncMonitorsMixin
27
+ from hyperping._async_observability_mixin import AsyncObservabilityMixin
28
+ from hyperping._async_oncall_mixin import AsyncOnCallMixin
26
29
  from hyperping._async_outages_mixin import AsyncOutagesMixin
30
+ from hyperping._async_reporting_mixin import AsyncReportingMixin
27
31
  from hyperping._async_statuspages_mixin import AsyncStatusPagesMixin
28
32
  from hyperping._circuit_breaker import (
29
33
  CircuitBreaker,
@@ -48,6 +52,10 @@ class AsyncHyperpingClient(
48
52
  AsyncOutagesMixin,
49
53
  AsyncStatusPagesMixin,
50
54
  AsyncHealthchecksMixin,
55
+ AsyncReportingMixin,
56
+ AsyncObservabilityMixin,
57
+ AsyncOnCallMixin,
58
+ AsyncIntegrationsMixin,
51
59
  ):
52
60
  """Async client for interacting with the Hyperping API.
53
61
 
@@ -178,6 +186,7 @@ class AsyncHyperpingClient(
178
186
  )
179
187
  if status in (400, 422):
180
188
  from hyperping.exceptions import HyperpingValidationError
189
+
181
190
  raise HyperpingValidationError(
182
191
  message=f"Validation error: {error_msg}",
183
192
  status_code=status,
@@ -321,9 +330,7 @@ class AsyncHyperpingClient(
321
330
  continue
322
331
  self._circuit_breaker.record_failure()
323
332
  if isinstance(e, httpx.TimeoutException):
324
- raise HyperpingAPIError(
325
- f"Request timeout after {max_attempts} attempts"
326
- ) from e
333
+ raise HyperpingAPIError(f"Request timeout after {max_attempts} attempts") from e
327
334
  raise HyperpingAPIError(f"Request failed: {e}") from e
328
335
 
329
336
  raise HyperpingAPIError( # pragma: no cover
@@ -0,0 +1,26 @@
1
+ """Async integrations mixin: notification channel management."""
2
+
3
+ from hyperping._protocols import _AsyncClientProtocol
4
+ from hyperping._utils import expect_dict, parse_list, validate_id
5
+ from hyperping.endpoints import Endpoint
6
+ from hyperping.exceptions import HyperpingAPIError, HyperpingNotFoundError
7
+ from hyperping.models._integration_models import Integration
8
+
9
+
10
+ class AsyncIntegrationsMixin(_AsyncClientProtocol):
11
+ """Async integration API operations."""
12
+
13
+ async def list_integrations(self) -> list[Integration]:
14
+ """Get all configured notification integrations."""
15
+ try:
16
+ result = await self._request("GET", Endpoint.INTEGRATIONS)
17
+ except (HyperpingNotFoundError, HyperpingAPIError):
18
+ return []
19
+ items = result if isinstance(result, list) else []
20
+ return parse_list(items, Integration, "integration")
21
+
22
+ async def get_integration(self, integration_id: str) -> Integration:
23
+ """Get a single integration."""
24
+ validate_id(integration_id, "integration_id")
25
+ result = await self._request("GET", f"{Endpoint.INTEGRATIONS}/{integration_id}")
26
+ return Integration.model_validate(expect_dict(result, "get_integration"))
@@ -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",
@@ -191,3 +187,16 @@ class AsyncMonitorsMixin(_AsyncClientProtocol):
191
187
  if r.uuid == monitor_id:
192
188
  return r
193
189
  raise HyperpingNotFoundError(f"No report found for monitor: {monitor_id}")
190
+
191
+ async def search_monitors_by_name(self, query: str) -> list[Monitor]:
192
+ """Search monitors by name (case-insensitive substring match)."""
193
+ if not query:
194
+ return []
195
+ try:
196
+ result = await self._request(
197
+ "GET", f"{Endpoint.MONITORS}/search", params={"query": query}
198
+ )
199
+ except HyperpingNotFoundError:
200
+ return []
201
+ items = result if isinstance(result, list) else []
202
+ return parse_list(items, Monitor, "monitor")
@@ -0,0 +1,65 @@
1
+ """Async observability mixin: anomalies, probe logs, alert history."""
2
+
3
+ from typing import Any
4
+
5
+ from hyperping._protocols import _AsyncClientProtocol
6
+ from hyperping._utils import parse_list, validate_id
7
+ from hyperping.endpoints import Endpoint
8
+ from hyperping.exceptions import HyperpingAPIError, HyperpingNotFoundError
9
+ from hyperping.models._observability_models import (
10
+ AlertNotification,
11
+ MonitorAnomaly,
12
+ ProbeLog,
13
+ )
14
+
15
+
16
+ class AsyncObservabilityMixin(_AsyncClientProtocol):
17
+ """Async observability API operations."""
18
+
19
+ async def get_monitor_anomalies(self, monitor_uuid: str) -> list[MonitorAnomaly]:
20
+ """Get detected anomalies for a monitor."""
21
+ validate_id(monitor_uuid, "monitor_uuid")
22
+ try:
23
+ result = await self._request("GET", f"{Endpoint.MONITORS}/{monitor_uuid}/anomalies")
24
+ except (HyperpingNotFoundError, HyperpingAPIError):
25
+ return []
26
+ items = result if isinstance(result, list) else []
27
+ return parse_list(items, MonitorAnomaly, "anomaly")
28
+
29
+ async def get_monitor_http_logs(
30
+ self, monitor_uuid: str, page: int = 0, limit: int = 50, level: str | None = None
31
+ ) -> list[ProbeLog]:
32
+ """Get recent HTTP probe logs for a monitor."""
33
+ validate_id(monitor_uuid, "monitor_uuid")
34
+ params: dict[str, Any] = {"page": page, "limit": limit}
35
+ if level is not None:
36
+ params["level"] = level
37
+ try:
38
+ result = await self._request(
39
+ "GET", f"{Endpoint.MONITORS}/{monitor_uuid}/http-logs", params=params
40
+ )
41
+ except (HyperpingNotFoundError, HyperpingAPIError):
42
+ return []
43
+ items = result if isinstance(result, list) else []
44
+ return parse_list(items, ProbeLog, "probe_log")
45
+
46
+ async def list_recent_alerts(
47
+ self,
48
+ from_dt: str | None = None,
49
+ to_dt: str | None = None,
50
+ monitor_uuids: list[str] | None = None,
51
+ ) -> list[AlertNotification]:
52
+ """Get recent alert notification history."""
53
+ params: dict[str, Any] = {}
54
+ if from_dt is not None:
55
+ params["from"] = from_dt
56
+ if to_dt is not None:
57
+ params["to"] = to_dt
58
+ if monitor_uuids:
59
+ params["monitor_uuids"] = ",".join(monitor_uuids)
60
+ try:
61
+ result = await self._request("GET", Endpoint.ALERTS, params=params)
62
+ except (HyperpingNotFoundError, HyperpingAPIError):
63
+ return []
64
+ items = result if isinstance(result, list) else []
65
+ return parse_list(items, AlertNotification, "alert")
@@ -0,0 +1,53 @@
1
+ """Async on-call mixin: schedules, escalation policies, team members."""
2
+
3
+ from typing import Any
4
+
5
+ from hyperping._protocols import _AsyncClientProtocol
6
+ from hyperping._utils import expect_dict, parse_list, validate_id
7
+ from hyperping.endpoints import Endpoint
8
+ from hyperping.exceptions import HyperpingAPIError, HyperpingNotFoundError
9
+ from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule
10
+
11
+
12
+ class AsyncOnCallMixin(_AsyncClientProtocol):
13
+ """Async on-call context API operations."""
14
+
15
+ async def list_on_call_schedules(self) -> list[OnCallSchedule]:
16
+ """Get all on-call rotation schedules."""
17
+ try:
18
+ result = await self._request("GET", Endpoint.ON_CALL_SCHEDULES)
19
+ except (HyperpingNotFoundError, HyperpingAPIError):
20
+ return []
21
+ items = result if isinstance(result, list) else []
22
+ return parse_list(items, OnCallSchedule, "on_call_schedule")
23
+
24
+ async def get_on_call_schedule(self, schedule_id: str) -> OnCallSchedule:
25
+ """Get a single on-call schedule."""
26
+ validate_id(schedule_id, "schedule_id")
27
+ result = await self._request("GET", f"{Endpoint.ON_CALL_SCHEDULES}/{schedule_id}")
28
+ return OnCallSchedule.model_validate(expect_dict(result, "get_on_call_schedule"))
29
+
30
+ async def list_escalation_policies(self) -> list[EscalationPolicy]:
31
+ """Get all escalation policies."""
32
+ try:
33
+ result = await self._request("GET", Endpoint.ESCALATION_POLICIES)
34
+ except (HyperpingNotFoundError, HyperpingAPIError):
35
+ return []
36
+ items = result if isinstance(result, list) else []
37
+ return parse_list(items, EscalationPolicy, "escalation_policy")
38
+
39
+ async def get_escalation_policy(self, policy_id: str) -> EscalationPolicy:
40
+ """Get a single escalation policy."""
41
+ validate_id(policy_id, "policy_id")
42
+ result = await self._request("GET", f"{Endpoint.ESCALATION_POLICIES}/{policy_id}")
43
+ return EscalationPolicy.model_validate(expect_dict(result, "get_escalation_policy"))
44
+
45
+ async def list_team_members(self) -> list[dict[str, Any]]:
46
+ """Get all team members."""
47
+ try:
48
+ result = await self._request("GET", Endpoint.TEAM_MEMBERS)
49
+ except (HyperpingNotFoundError, HyperpingAPIError):
50
+ return []
51
+ if isinstance(result, list):
52
+ return result
53
+ return []
@@ -19,6 +19,7 @@ from hyperping._utils import (
19
19
  from hyperping.endpoints import Endpoint
20
20
  from hyperping.exceptions import HyperpingNotFoundError
21
21
  from hyperping.models import Outage, OutageAction
22
+ from hyperping.models._outage_models import OutageTimeline, OutageTimelineEvent
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
@@ -56,9 +57,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
56
57
  ValueError: If *status* or *outage_type* is not a recognised value.
57
58
  """
58
59
  if status not in _VALID_STATUSES:
59
- raise ValueError(
60
- f"Invalid status {status!r}. Valid values: {sorted(_VALID_STATUSES)}"
61
- )
60
+ raise ValueError(f"Invalid status {status!r}. Valid values: {sorted(_VALID_STATUSES)}")
62
61
  if outage_type not in _VALID_TYPES:
63
62
  raise ValueError(
64
63
  f"Invalid outage_type {outage_type!r}. Valid values: {sorted(_VALID_TYPES)}"
@@ -75,7 +74,8 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
75
74
  params["page"] = page
76
75
  data = await self._request("GET", Endpoint.OUTAGES, params=params)
77
76
  raw: list[Any] = (
78
- data.get("outages", []) if isinstance(data, dict)
77
+ data.get("outages", [])
78
+ if isinstance(data, dict)
79
79
  else (data if isinstance(data, list) else [])
80
80
  )
81
81
  return parse_list(raw, Outage, "outage")
@@ -86,9 +86,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
86
86
  logger.debug("Outage endpoint not available (404)")
87
87
  return []
88
88
 
89
- async def acknowledge_outage(
90
- self, outage_id: str, message: str | None = None
91
- ) -> OutageAction:
89
+ async def acknowledge_outage(self, outage_id: str, message: str | None = None) -> OutageAction:
92
90
  """Acknowledge an outage.
93
91
 
94
92
  Args:
@@ -110,9 +108,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
110
108
  )
111
109
  return OutageAction.from_raw(expect_dict(result, "outage operation"))
112
110
 
113
- async def resolve_outage(
114
- self, outage_id: str, message: str | None = None
115
- ) -> OutageAction:
111
+ async def resolve_outage(self, outage_id: str, message: str | None = None) -> OutageAction:
116
112
  """Resolve an outage.
117
113
 
118
114
  Args:
@@ -163,9 +159,7 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
163
159
  HyperpingNotFoundError: If outage not found.
164
160
  """
165
161
  validate_id(outage_id, "outage_id")
166
- result = await self._request(
167
- "POST", f"{Endpoint.OUTAGES}/{outage_id}/unacknowledge"
168
- )
162
+ result = await self._request("POST", f"{Endpoint.OUTAGES}/{outage_id}/unacknowledge")
169
163
  return OutageAction.from_raw(expect_dict(result, "outage operation"))
170
164
 
171
165
  async def delete_outage(self, outage_id: str) -> None:
@@ -212,3 +206,33 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
212
206
  validate_id(outage_id, "outage_id")
213
207
  result = await self._request("GET", f"{Endpoint.OUTAGES}/{outage_id}")
214
208
  return Outage.model_validate(expect_dict(result, "get_outage"))
209
+
210
+ async def get_outage_timeline(self, outage_id: str) -> OutageTimeline:
211
+ """Get the lifecycle timeline for an outage."""
212
+ validate_id(outage_id, "outage_id")
213
+ result = await self._request("GET", f"{Endpoint.OUTAGES}/{outage_id}/timeline")
214
+ data = expect_dict(result, "get_outage_timeline")
215
+ raw_events = data.get("events", [])
216
+ events = parse_list(raw_events, OutageTimelineEvent, "timeline_event")
217
+ return OutageTimeline.model_validate({"outageUuid": outage_id, "events": events})
218
+
219
+ async def get_monitor_outages(
220
+ self,
221
+ monitor_uuid: str,
222
+ page: int | None = None,
223
+ status: str = "all",
224
+ ) -> list[Outage]:
225
+ """Get outages scoped to a single monitor."""
226
+ validate_id(monitor_uuid, "monitor_uuid")
227
+ params: dict[str, Any] = {
228
+ "monitor_uuid": monitor_uuid,
229
+ "status": status,
230
+ }
231
+ if page is not None:
232
+ params["page"] = page
233
+ try:
234
+ result = await self._request("GET", Endpoint.OUTAGES, params=params)
235
+ except HyperpingNotFoundError:
236
+ return []
237
+ items = result if isinstance(result, list) else []
238
+ return parse_list(items, Outage, "outage")
@@ -0,0 +1,39 @@
1
+ """Async reporting mixin: status summary, response time, MTTA."""
2
+
3
+ from typing import Any
4
+
5
+ from hyperping._protocols import _AsyncClientProtocol
6
+ from hyperping._utils import expect_dict, validate_id
7
+ from hyperping.endpoints import Endpoint
8
+ from hyperping.models._reporting_models import StatusSummary
9
+
10
+
11
+ class AsyncReportingMixin(_AsyncClientProtocol):
12
+ """Async status and reporting API operations."""
13
+
14
+ async def get_status_summary(self) -> StatusSummary:
15
+ """Get aggregate status counts for the project."""
16
+ result = await self._request("GET", Endpoint.STATUS_SUMMARY)
17
+ return StatusSummary.model_validate(expect_dict(result, "get_status_summary"))
18
+
19
+ async def get_monitor_response_time(
20
+ self, monitor_uuid: str, period: str = "24h"
21
+ ) -> dict[str, Any]:
22
+ """Get latency percentiles for a monitor."""
23
+ validate_id(monitor_uuid, "monitor_uuid")
24
+ result = await self._request(
25
+ "GET",
26
+ f"{Endpoint.MONITOR_RESPONSE_TIME}/{monitor_uuid}",
27
+ params={"period": period},
28
+ )
29
+ return expect_dict(result, "get_monitor_response_time")
30
+
31
+ async def get_monitor_mtta(self, monitor_uuid: str, period: str = "30d") -> dict[str, Any]:
32
+ """Get Mean Time To Acknowledge for a monitor."""
33
+ validate_id(monitor_uuid, "monitor_uuid")
34
+ result = await self._request(
35
+ "GET",
36
+ f"{Endpoint.MONITOR_MTTA}/{monitor_uuid}",
37
+ params={"period": period},
38
+ )
39
+ return expect_dict(result, "get_monitor_mtta")
@@ -12,6 +12,7 @@ from datetime import UTC, datetime
12
12
  from hyperping._protocols import _ClientProtocol
13
13
  from hyperping._utils import expect_dict, parse_list, unwrap_list, validate_id
14
14
  from hyperping.endpoints import Endpoint
15
+ from hyperping.exceptions import HyperpingAPIError
15
16
  from hyperping.models import (
16
17
  AddIncidentUpdateRequest, # canonical name (M18)
17
18
  Incident,
@@ -144,7 +145,16 @@ class IncidentsMixin(_ClientProtocol):
144
145
  payload = update.model_dump(exclude_none=True, by_alias=True)
145
146
  url = f"{Endpoint.INCIDENTS}/{incident_id}/updates"
146
147
  self._request("POST", url, json=payload) # Returns {"message": "..."} — not a full Incident
147
- return self.get_incident(incident_id)
148
+ try:
149
+ return self.get_incident(incident_id)
150
+ except HyperpingAPIError as exc:
151
+ raise HyperpingAPIError(
152
+ f"Incident update was posted successfully but refreshing "
153
+ f"incident {incident_id!r} failed: {exc}",
154
+ status_code=exc.status_code,
155
+ response_body=exc.response_body,
156
+ request_id=exc.request_id,
157
+ ) from exc
148
158
 
149
159
  def resolve_incident(self, incident_id: str, message: str | None = None) -> Incident:
150
160
  """Resolve an incident.
@@ -0,0 +1,41 @@
1
+ """Integrations mixin: notification channel management."""
2
+
3
+ from hyperping._protocols import _ClientProtocol
4
+ from hyperping._utils import expect_dict, parse_list, validate_id
5
+ from hyperping.endpoints import Endpoint
6
+ from hyperping.exceptions import HyperpingAPIError, HyperpingNotFoundError
7
+ from hyperping.models._integration_models import Integration
8
+
9
+
10
+ class IntegrationsMixin(_ClientProtocol):
11
+ """Integration API operations."""
12
+
13
+ def list_integrations(self) -> list[Integration]:
14
+ """Get all configured notification integrations.
15
+
16
+ Returns:
17
+ List of :class:`~hyperping.models.Integration` objects.
18
+ Returns empty list on 404.
19
+ """
20
+ try:
21
+ result = self._request("GET", Endpoint.INTEGRATIONS)
22
+ except (HyperpingNotFoundError, HyperpingAPIError):
23
+ return []
24
+ items = result if isinstance(result, list) else []
25
+ return parse_list(items, Integration, "integration")
26
+
27
+ def get_integration(self, integration_id: str) -> Integration:
28
+ """Get a single integration.
29
+
30
+ Args:
31
+ integration_id: Integration UUID.
32
+
33
+ Returns:
34
+ :class:`~hyperping.models.Integration` object.
35
+
36
+ Raises:
37
+ HyperpingNotFoundError: If integration not found.
38
+ """
39
+ validate_id(integration_id, "integration_id")
40
+ result = self._request("GET", f"{Endpoint.INTEGRATIONS}/{integration_id}")
41
+ return Integration.model_validate(expect_dict(result, "get_integration"))
@@ -125,12 +125,11 @@ class MaintenanceMixin(_ClientProtocol):
125
125
  current = self.get_maintenance(maintenance_id)
126
126
  partial = update.model_dump(exclude_none=True, by_alias=True, mode="json")
127
127
 
128
- payload: dict[str, object] = {
129
- "name": current.name,
130
- "start_date": current.start_date,
131
- "end_date": current.end_date,
132
- "monitors": current.monitors,
133
- }
128
+ payload: dict[str, object] = current.model_dump(
129
+ mode="json",
130
+ exclude_none=True,
131
+ include={"name", "start_date", "end_date", "monitors"},
132
+ )
134
133
  payload.update(partial)
135
134
 
136
135
  response = expect_dict(
@@ -171,9 +171,7 @@ class MonitorsMixin(_ClientProtocol):
171
171
  HyperpingAPIError: On unexpected API errors.
172
172
  """
173
173
  if period not in VALID_PERIODS:
174
- raise ValueError(
175
- f"Invalid period {period!r}. Valid values: {sorted(VALID_PERIODS)}"
176
- )
174
+ raise ValueError(f"Invalid period {period!r}. Valid values: {sorted(VALID_PERIODS)}")
177
175
  response = expect_dict(
178
176
  self._request("GET", Endpoint.REPORTS, params={"period": period}),
179
177
  "get_all_reports",
@@ -215,3 +213,23 @@ class MonitorsMixin(_ClientProtocol):
215
213
  if r.uuid == monitor_id:
216
214
  return r
217
215
  raise HyperpingNotFoundError(f"No report found for monitor: {monitor_id}")
216
+
217
+ def search_monitors_by_name(self, query: str) -> list[Monitor]:
218
+ """Search monitors by name (case-insensitive substring match).
219
+
220
+ Args:
221
+ query: Search string to match against monitor names and URLs.
222
+
223
+ Returns:
224
+ List of matching :class:`~hyperping.models.Monitor` objects.
225
+ Returns empty list on 404 or no matches.
226
+ """
227
+ if not query:
228
+ return []
229
+ try:
230
+ # Path is speculative; derived from MCP tool name.
231
+ result = self._request("GET", f"{Endpoint.MONITORS}/search", params={"query": query})
232
+ except HyperpingNotFoundError:
233
+ return []
234
+ items = result if isinstance(result, list) else []
235
+ return parse_list(items, Monitor, "monitor")