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.
- {hyperping-1.1.0 → hyperping-1.3.0}/PKG-INFO +2 -2
- {hyperping-1.1.0 → hyperping-1.3.0}/pyproject.toml +2 -2
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/__init__.py +28 -5
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_client.py +10 -3
- hyperping-1.3.0/src/hyperping/_async_integrations_mixin.py +26 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_monitors_mixin.py +15 -6
- hyperping-1.3.0/src/hyperping/_async_observability_mixin.py +65 -0
- hyperping-1.3.0/src/hyperping/_async_oncall_mixin.py +53 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_outages_mixin.py +37 -13
- hyperping-1.3.0/src/hyperping/_async_reporting_mixin.py +39 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_incidents_mixin.py +11 -1
- hyperping-1.3.0/src/hyperping/_integrations_mixin.py +41 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_maintenance_mixin.py +5 -6
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_monitors_mixin.py +21 -3
- hyperping-1.3.0/src/hyperping/_observability_mixin.py +102 -0
- hyperping-1.3.0/src/hyperping/_oncall_mixin.py +88 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_outages_mixin.py +119 -4
- hyperping-1.3.0/src/hyperping/_reporting_mixin.py +71 -0
- hyperping-1.3.0/src/hyperping/_version.py +1 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/client.py +14 -4
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/endpoints.py +75 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/__init__.py +27 -1
- hyperping-1.3.0/src/hyperping/models/_integration_models.py +14 -0
- hyperping-1.3.0/src/hyperping/models/_observability_models.py +46 -0
- hyperping-1.3.0/src/hyperping/models/_oncall_models.py +27 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_outage_models.py +25 -0
- hyperping-1.3.0/src/hyperping/models/_reporting_models.py +17 -0
- hyperping-1.3.0/tests/unit/test_async_new_mixins.py +262 -0
- hyperping-1.3.0/tests/unit/test_async_preexisting.py +331 -0
- hyperping-1.3.0/tests/unit/test_integrations.py +98 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_monitors.py +43 -0
- hyperping-1.3.0/tests/unit/test_observability.py +203 -0
- hyperping-1.3.0/tests/unit/test_oncall.py +217 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_outages.py +98 -1
- hyperping-1.3.0/tests/unit/test_reporting.py +140 -0
- hyperping-1.1.0/src/hyperping/_version.py +0 -1
- {hyperping-1.1.0 → hyperping-1.3.0}/.gitignore +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/CHANGELOG.md +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/CONTRIBUTING.md +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/LICENSE +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/README.md +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_healthchecks_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_incidents_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_maintenance_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_async_statuspages_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_circuit_breaker.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_healthchecks_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_internals.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_monitor_constants.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_protocols.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_statuspages_mixin.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/_utils.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/exceptions.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_healthcheck_models.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_incident_models.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_maintenance_models.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_monitor_models.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/models/_statuspage_models.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/src/hyperping/py.typed +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/__init__.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/__init__.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/conftest.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_async_client.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_healthchecks.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_incidents.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_maintenance.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_pagination.py +0 -0
- {hyperping-1.1.0 → hyperping-1.3.0}/tests/unit/test_sdk_surface.py +0 -0
- {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.
|
|
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>=
|
|
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.
|
|
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>=
|
|
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",
|
|
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", [])
|
|
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
|
-
|
|
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
|
-
"
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
|
|
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")
|