hyperping 0.1.0__py3-none-any.whl
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/__init__.py +151 -0
- hyperping/_incidents_mixin.py +198 -0
- hyperping/_maintenance_mixin.py +195 -0
- hyperping/_monitors_mixin.py +250 -0
- hyperping/_outages_mixin.py +102 -0
- hyperping/_statuspages_mixin.py +226 -0
- hyperping/_version.py +1 -0
- hyperping/client.py +452 -0
- hyperping/endpoints.py +231 -0
- hyperping/exceptions.py +89 -0
- hyperping/models.py +769 -0
- hyperping/py.typed +0 -0
- hyperping-0.1.0.dist-info/METADATA +223 -0
- hyperping-0.1.0.dist-info/RECORD +16 -0
- hyperping-0.1.0.dist-info/WHEEL +4 -0
- hyperping-0.1.0.dist-info/licenses/LICENSE +21 -0
hyperping/__init__.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Hyperping API client.
|
|
2
|
+
|
|
3
|
+
A Python client for the `Hyperping <https://hyperping.io>`_ monitoring API,
|
|
4
|
+
providing typed models, automatic retries with exponential backoff, and a
|
|
5
|
+
circuit breaker for fault tolerance.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from hyperping import HyperpingClient
|
|
10
|
+
|
|
11
|
+
with HyperpingClient(api_key="sk_...") as client:
|
|
12
|
+
monitors = client.list_monitors()
|
|
13
|
+
for m in monitors:
|
|
14
|
+
print(f"{m.name}: {'down' if m.down else 'up'}")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from hyperping._version import __version__
|
|
18
|
+
from hyperping.client import (
|
|
19
|
+
CircuitBreaker,
|
|
20
|
+
CircuitBreakerConfig,
|
|
21
|
+
CircuitState,
|
|
22
|
+
HyperpingClient,
|
|
23
|
+
RetryConfig,
|
|
24
|
+
)
|
|
25
|
+
from hyperping.endpoints import (
|
|
26
|
+
API_BASE,
|
|
27
|
+
API_PATHS,
|
|
28
|
+
ENDPOINTS,
|
|
29
|
+
HYPERPING_API_BASE,
|
|
30
|
+
APIVersion,
|
|
31
|
+
Endpoint,
|
|
32
|
+
EndpointConfig,
|
|
33
|
+
get_endpoint_url,
|
|
34
|
+
get_version_for_endpoint,
|
|
35
|
+
)
|
|
36
|
+
from hyperping.exceptions import (
|
|
37
|
+
HyperpingAPIError,
|
|
38
|
+
HyperpingAuthError,
|
|
39
|
+
HyperpingNotFoundError,
|
|
40
|
+
HyperpingRateLimitError,
|
|
41
|
+
HyperpingValidationError,
|
|
42
|
+
)
|
|
43
|
+
from hyperping.models import (
|
|
44
|
+
DEFAULT_REGIONS,
|
|
45
|
+
AddIncidentUpdateRequest,
|
|
46
|
+
APIErrorResponse,
|
|
47
|
+
DnsRecordType,
|
|
48
|
+
HttpMethod,
|
|
49
|
+
Incident,
|
|
50
|
+
IncidentCreate,
|
|
51
|
+
IncidentStatus,
|
|
52
|
+
IncidentType,
|
|
53
|
+
IncidentUpdate,
|
|
54
|
+
IncidentUpdateCreate,
|
|
55
|
+
IncidentUpdateRequest,
|
|
56
|
+
IncidentUpdateType,
|
|
57
|
+
LocalizedText,
|
|
58
|
+
Maintenance,
|
|
59
|
+
MaintenanceCreate,
|
|
60
|
+
MaintenanceUpdate,
|
|
61
|
+
Monitor,
|
|
62
|
+
MonitorBase,
|
|
63
|
+
MonitorCreate,
|
|
64
|
+
MonitorFrequency,
|
|
65
|
+
MonitorListResponse,
|
|
66
|
+
MonitorProtocol,
|
|
67
|
+
MonitorReport,
|
|
68
|
+
MonitorTimeout,
|
|
69
|
+
MonitorUpdate,
|
|
70
|
+
NotificationOption,
|
|
71
|
+
OutageDetail,
|
|
72
|
+
OutageStats,
|
|
73
|
+
Region,
|
|
74
|
+
ReportPeriod,
|
|
75
|
+
RequestHeader,
|
|
76
|
+
StatusPage,
|
|
77
|
+
StatusPageCreate,
|
|
78
|
+
StatusPageSubscriber,
|
|
79
|
+
StatusPageUpdate,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
# Version
|
|
84
|
+
"__version__",
|
|
85
|
+
# Client
|
|
86
|
+
"HyperpingClient",
|
|
87
|
+
# Configuration
|
|
88
|
+
"RetryConfig",
|
|
89
|
+
"CircuitBreakerConfig",
|
|
90
|
+
"CircuitBreaker",
|
|
91
|
+
"CircuitState",
|
|
92
|
+
# Endpoints
|
|
93
|
+
"API_BASE",
|
|
94
|
+
"Endpoint",
|
|
95
|
+
"APIVersion",
|
|
96
|
+
"EndpointConfig",
|
|
97
|
+
"ENDPOINTS",
|
|
98
|
+
"get_endpoint_url",
|
|
99
|
+
"get_version_for_endpoint",
|
|
100
|
+
# Convenience aliases (also used in tests)
|
|
101
|
+
"HYPERPING_API_BASE",
|
|
102
|
+
"API_PATHS",
|
|
103
|
+
# Exceptions
|
|
104
|
+
"HyperpingAPIError",
|
|
105
|
+
"HyperpingAuthError",
|
|
106
|
+
"HyperpingNotFoundError",
|
|
107
|
+
"HyperpingRateLimitError",
|
|
108
|
+
"HyperpingValidationError",
|
|
109
|
+
# Monitor enums
|
|
110
|
+
"HttpMethod",
|
|
111
|
+
"MonitorFrequency",
|
|
112
|
+
"MonitorTimeout",
|
|
113
|
+
"Region",
|
|
114
|
+
"MonitorProtocol",
|
|
115
|
+
"DnsRecordType",
|
|
116
|
+
"DEFAULT_REGIONS",
|
|
117
|
+
# Monitors
|
|
118
|
+
"MonitorBase",
|
|
119
|
+
"Monitor",
|
|
120
|
+
"MonitorCreate",
|
|
121
|
+
"MonitorUpdate",
|
|
122
|
+
"MonitorReport",
|
|
123
|
+
"MonitorListResponse",
|
|
124
|
+
"RequestHeader",
|
|
125
|
+
# Incidents
|
|
126
|
+
"AddIncidentUpdateRequest",
|
|
127
|
+
"Incident",
|
|
128
|
+
"IncidentCreate",
|
|
129
|
+
"IncidentStatus",
|
|
130
|
+
"IncidentType",
|
|
131
|
+
"IncidentUpdate",
|
|
132
|
+
"IncidentUpdateCreate",
|
|
133
|
+
"IncidentUpdateRequest",
|
|
134
|
+
"IncidentUpdateType",
|
|
135
|
+
"LocalizedText",
|
|
136
|
+
# Maintenance
|
|
137
|
+
"Maintenance",
|
|
138
|
+
"MaintenanceCreate",
|
|
139
|
+
"MaintenanceUpdate",
|
|
140
|
+
"NotificationOption",
|
|
141
|
+
# Reports
|
|
142
|
+
"ReportPeriod",
|
|
143
|
+
"OutageDetail",
|
|
144
|
+
"OutageStats",
|
|
145
|
+
"APIErrorResponse",
|
|
146
|
+
# Status Pages
|
|
147
|
+
"StatusPage",
|
|
148
|
+
"StatusPageCreate",
|
|
149
|
+
"StatusPageUpdate",
|
|
150
|
+
"StatusPageSubscriber",
|
|
151
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Incident operations mixin for HyperpingClient.
|
|
2
|
+
|
|
3
|
+
Provides CRUD methods for Hyperping incidents (v3 API). Mixed into
|
|
4
|
+
:class:`~hyperping.client.HyperpingClient` at class definition time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
from hyperping.endpoints import Endpoint
|
|
16
|
+
from hyperping.models import (
|
|
17
|
+
AddIncidentUpdateRequest,
|
|
18
|
+
Incident,
|
|
19
|
+
IncidentCreate,
|
|
20
|
+
IncidentStatus,
|
|
21
|
+
IncidentUpdateCreate,
|
|
22
|
+
IncidentUpdateRequest,
|
|
23
|
+
LocalizedText,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class IncidentsMixin:
|
|
30
|
+
"""Incident-related API operations."""
|
|
31
|
+
|
|
32
|
+
def _request( # type: ignore[empty-body]
|
|
33
|
+
self,
|
|
34
|
+
method: str,
|
|
35
|
+
path: str,
|
|
36
|
+
json: dict[str, Any] | None = None,
|
|
37
|
+
params: dict[str, Any] | None = None,
|
|
38
|
+
) -> dict[str, Any]: ... # provided by HyperpingClient
|
|
39
|
+
|
|
40
|
+
def list_incidents(self, status: str | None = None) -> list[Incident]:
|
|
41
|
+
"""List all incidents.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
status: Filter by status (``investigating``, ``identified``,
|
|
45
|
+
``monitoring``, ``resolved``).
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of :class:`Incident` objects. Incidents that fail to parse
|
|
49
|
+
are silently skipped with a warning log.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
HyperpingAuthError: If the API key is invalid.
|
|
53
|
+
HyperpingAPIError: On unexpected API errors.
|
|
54
|
+
"""
|
|
55
|
+
params = {}
|
|
56
|
+
if status:
|
|
57
|
+
params["status"] = status
|
|
58
|
+
|
|
59
|
+
response = self._request("GET", Endpoint.INCIDENTS, params=params if params else None)
|
|
60
|
+
|
|
61
|
+
# Handle different response formats
|
|
62
|
+
if isinstance(response, list):
|
|
63
|
+
incidents_data = response
|
|
64
|
+
elif "incidents" in response:
|
|
65
|
+
incidents_data = response["incidents"]
|
|
66
|
+
else:
|
|
67
|
+
incidents_data = response.get("data", [])
|
|
68
|
+
|
|
69
|
+
incidents = []
|
|
70
|
+
skipped = 0
|
|
71
|
+
for data in incidents_data:
|
|
72
|
+
try:
|
|
73
|
+
incidents.append(Incident.model_validate(data))
|
|
74
|
+
except (ValueError, ValidationError) as e:
|
|
75
|
+
skipped += 1
|
|
76
|
+
logger.warning(f"Failed to parse incident data: {e}", extra={"data": data})
|
|
77
|
+
|
|
78
|
+
if skipped:
|
|
79
|
+
logger.warning(
|
|
80
|
+
f"{skipped} of {len(incidents_data)} incidents could not be parsed and were skipped"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return incidents
|
|
84
|
+
|
|
85
|
+
def get_incident(self, incident_id: str) -> Incident:
|
|
86
|
+
"""Get a single incident by ID.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
incident_id: Incident ID
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Incident object
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
HyperpingNotFoundError: If incident not found
|
|
96
|
+
"""
|
|
97
|
+
response = self._request("GET", f"{Endpoint.INCIDENTS}/{incident_id}")
|
|
98
|
+
return Incident.model_validate(response)
|
|
99
|
+
|
|
100
|
+
def create_incident(self, incident: IncidentCreate) -> Incident:
|
|
101
|
+
"""Create a new incident.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
incident: Incident creation data.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Created :class:`Incident` object.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
111
|
+
HyperpingAPIError: On unexpected API errors.
|
|
112
|
+
|
|
113
|
+
Note:
|
|
114
|
+
v3 API returns {"message": "...", "uuid": "..."} on create,
|
|
115
|
+
not the full incident object. The full incident is fetched after creation.
|
|
116
|
+
"""
|
|
117
|
+
payload = incident.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
118
|
+
response = self._request("POST", Endpoint.INCIDENTS, json=payload)
|
|
119
|
+
# v3 API returns minimal response with just uuid
|
|
120
|
+
if "uuid" in response and "title" not in response:
|
|
121
|
+
# Fetch the full incident after creation
|
|
122
|
+
return self.get_incident(response["uuid"])
|
|
123
|
+
return Incident.model_validate(response)
|
|
124
|
+
|
|
125
|
+
def update_incident(self, incident_id: str, update: IncidentUpdateRequest) -> Incident:
|
|
126
|
+
"""Update an existing incident.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
incident_id: Incident UUID.
|
|
130
|
+
update: Fields to update.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Updated :class:`Incident` object.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
HyperpingNotFoundError: If the incident does not exist.
|
|
137
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
138
|
+
HyperpingAPIError: On unexpected API errors.
|
|
139
|
+
"""
|
|
140
|
+
payload = update.model_dump(exclude_none=True, by_alias=True)
|
|
141
|
+
response = self._request("PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload)
|
|
142
|
+
return Incident.model_validate(response)
|
|
143
|
+
|
|
144
|
+
def add_incident_update(
|
|
145
|
+
self,
|
|
146
|
+
incident_id: str,
|
|
147
|
+
update: IncidentUpdateCreate,
|
|
148
|
+
) -> Incident:
|
|
149
|
+
"""Add an update to an incident.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
incident_id: Incident UUID.
|
|
153
|
+
update: Update data with message and new status.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Updated :class:`Incident` object (re-fetched after posting the update).
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
HyperpingNotFoundError: If the incident does not exist.
|
|
160
|
+
HyperpingAPIError: On unexpected API errors.
|
|
161
|
+
"""
|
|
162
|
+
payload = update.model_dump(exclude_none=True, by_alias=True)
|
|
163
|
+
url = f"{Endpoint.INCIDENTS}/{incident_id}/updates"
|
|
164
|
+
self._request("POST", url, json=payload) # Returns {"message": "..."} — not a full Incident
|
|
165
|
+
return self.get_incident(incident_id)
|
|
166
|
+
|
|
167
|
+
def resolve_incident(self, incident_id: str, message: str | None = None) -> Incident:
|
|
168
|
+
"""Resolve an incident.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
incident_id: Incident UUID.
|
|
172
|
+
message: Optional resolution message. Defaults to
|
|
173
|
+
``"This incident has been resolved."``.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Resolved :class:`Incident` object.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
HyperpingNotFoundError: If the incident does not exist.
|
|
180
|
+
HyperpingAPIError: On unexpected API errors.
|
|
181
|
+
"""
|
|
182
|
+
update = AddIncidentUpdateRequest(
|
|
183
|
+
text=LocalizedText(en=message or "This incident has been resolved."),
|
|
184
|
+
type=IncidentStatus.RESOLVED,
|
|
185
|
+
date=datetime.now(UTC).isoformat(),
|
|
186
|
+
)
|
|
187
|
+
return self.add_incident_update(incident_id, update)
|
|
188
|
+
|
|
189
|
+
def delete_incident(self, incident_id: str) -> None:
|
|
190
|
+
"""Delete an incident.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
incident_id: Incident ID
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
HyperpingNotFoundError: If incident not found
|
|
197
|
+
"""
|
|
198
|
+
self._request("DELETE", f"{Endpoint.INCIDENTS}/{incident_id}")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Maintenance operations mixin for HyperpingClient.
|
|
2
|
+
|
|
3
|
+
Provides CRUD methods for Hyperping maintenance windows (v1 API). Mixed into
|
|
4
|
+
:class:`~hyperping.client.HyperpingClient` at class definition time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
from hyperping.endpoints import Endpoint
|
|
16
|
+
from hyperping.models import (
|
|
17
|
+
Maintenance,
|
|
18
|
+
MaintenanceCreate,
|
|
19
|
+
MaintenanceUpdate,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MaintenanceMixin:
|
|
26
|
+
"""Maintenance-related API operations."""
|
|
27
|
+
|
|
28
|
+
def _request( # type: ignore[empty-body]
|
|
29
|
+
self,
|
|
30
|
+
method: str,
|
|
31
|
+
path: str,
|
|
32
|
+
json: dict[str, Any] | None = None,
|
|
33
|
+
params: dict[str, Any] | None = None,
|
|
34
|
+
) -> dict[str, Any]: ... # provided by HyperpingClient
|
|
35
|
+
|
|
36
|
+
def list_maintenance(self, status: str | None = None) -> list[Maintenance]:
|
|
37
|
+
"""List all maintenance windows.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
status: Filter by status (``scheduled``, ``in_progress``, ``completed``).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of :class:`Maintenance` objects. Windows that fail to parse
|
|
44
|
+
are silently skipped with a warning log.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
HyperpingAuthError: If the API key is invalid.
|
|
48
|
+
HyperpingAPIError: On unexpected API errors.
|
|
49
|
+
"""
|
|
50
|
+
params = {}
|
|
51
|
+
if status:
|
|
52
|
+
params["status"] = status
|
|
53
|
+
|
|
54
|
+
response = self._request("GET", Endpoint.MAINTENANCE, params=params if params else None)
|
|
55
|
+
|
|
56
|
+
# Handle different response formats
|
|
57
|
+
# API returns {"maintenanceWindows": [...]} as of current version
|
|
58
|
+
if isinstance(response, list):
|
|
59
|
+
maintenance_data = response
|
|
60
|
+
elif "maintenanceWindows" in response:
|
|
61
|
+
maintenance_data = response["maintenanceWindows"]
|
|
62
|
+
elif "maintenance" in response:
|
|
63
|
+
maintenance_data = response["maintenance"]
|
|
64
|
+
else:
|
|
65
|
+
maintenance_data = response.get("data", [])
|
|
66
|
+
|
|
67
|
+
windows = []
|
|
68
|
+
skipped = 0
|
|
69
|
+
for data in maintenance_data:
|
|
70
|
+
try:
|
|
71
|
+
windows.append(Maintenance.model_validate(data))
|
|
72
|
+
except (ValueError, ValidationError) as e:
|
|
73
|
+
skipped += 1
|
|
74
|
+
logger.warning(f"Failed to parse maintenance data: {e}", extra={"data": data})
|
|
75
|
+
|
|
76
|
+
if skipped:
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"{skipped} of {len(maintenance_data)} maintenance windows "
|
|
79
|
+
"could not be parsed and were skipped"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return windows
|
|
83
|
+
|
|
84
|
+
def get_maintenance(self, maintenance_id: str) -> Maintenance:
|
|
85
|
+
"""Get a single maintenance window by ID.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
maintenance_id: Maintenance ID
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Maintenance object
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
HyperpingNotFoundError: If maintenance not found
|
|
95
|
+
"""
|
|
96
|
+
response = self._request("GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}")
|
|
97
|
+
return Maintenance.model_validate(response)
|
|
98
|
+
|
|
99
|
+
def create_maintenance(self, maintenance: MaintenanceCreate) -> Maintenance:
|
|
100
|
+
"""Create a new maintenance window.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
maintenance: Maintenance creation data.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Created :class:`Maintenance` object.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
110
|
+
HyperpingAPIError: On unexpected API errors.
|
|
111
|
+
|
|
112
|
+
Note:
|
|
113
|
+
v1 API returns {"uuid": "..."} on create, not the full maintenance object.
|
|
114
|
+
The full maintenance window is fetched after creation.
|
|
115
|
+
"""
|
|
116
|
+
payload = maintenance.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
117
|
+
response = self._request("POST", Endpoint.MAINTENANCE, json=payload)
|
|
118
|
+
# v1 API returns minimal response with just uuid
|
|
119
|
+
if "uuid" in response and "name" not in response:
|
|
120
|
+
# Fetch the full maintenance after creation
|
|
121
|
+
return self.get_maintenance(response["uuid"])
|
|
122
|
+
return Maintenance.model_validate(response)
|
|
123
|
+
|
|
124
|
+
def update_maintenance(
|
|
125
|
+
self,
|
|
126
|
+
maintenance_id: str,
|
|
127
|
+
update: MaintenanceUpdate,
|
|
128
|
+
) -> Maintenance:
|
|
129
|
+
"""Update an existing maintenance window.
|
|
130
|
+
|
|
131
|
+
The v1 API PUT requires a full payload (partial updates return 401).
|
|
132
|
+
We fetch the current state and merge the supplied fields before sending.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
maintenance_id: Maintenance ID
|
|
136
|
+
update: Fields to update (only non-None fields are applied)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Updated Maintenance object
|
|
140
|
+
"""
|
|
141
|
+
current = self.get_maintenance(maintenance_id)
|
|
142
|
+
partial = update.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
143
|
+
|
|
144
|
+
payload: dict[str, object] = {
|
|
145
|
+
"name": current.name,
|
|
146
|
+
"start_date": current.start_date,
|
|
147
|
+
"end_date": current.end_date,
|
|
148
|
+
"monitors": current.monitors,
|
|
149
|
+
}
|
|
150
|
+
payload.update(partial)
|
|
151
|
+
|
|
152
|
+
response = self._request("PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload)
|
|
153
|
+
return Maintenance.model_validate(response)
|
|
154
|
+
|
|
155
|
+
def delete_maintenance(self, maintenance_id: str) -> None:
|
|
156
|
+
"""Delete a maintenance window.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
maintenance_id: Maintenance ID
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
HyperpingNotFoundError: If maintenance not found
|
|
163
|
+
"""
|
|
164
|
+
self._request("DELETE", f"{Endpoint.MAINTENANCE}/{maintenance_id}")
|
|
165
|
+
|
|
166
|
+
def get_active_maintenance(self) -> list[Maintenance]:
|
|
167
|
+
"""Get currently active maintenance windows.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of active :class:`Maintenance` objects.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
HyperpingAuthError: If the API key is invalid.
|
|
174
|
+
HyperpingAPIError: On unexpected API errors.
|
|
175
|
+
"""
|
|
176
|
+
all_maintenance = self.list_maintenance()
|
|
177
|
+
now = datetime.now(UTC)
|
|
178
|
+
|
|
179
|
+
return [m for m in all_maintenance if m.is_active(now)]
|
|
180
|
+
|
|
181
|
+
def is_monitor_in_maintenance(self, monitor_uuid: str) -> bool:
|
|
182
|
+
"""Check if a monitor is currently in a maintenance window.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
monitor_uuid: Monitor UUID to check.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
``True`` if the monitor is in an active maintenance window.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
HyperpingAuthError: If the API key is invalid.
|
|
192
|
+
HyperpingAPIError: On unexpected API errors.
|
|
193
|
+
"""
|
|
194
|
+
active = self.get_active_maintenance()
|
|
195
|
+
return any(m.affects_monitor(monitor_uuid) for m in active)
|