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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Monitor operations mixin for HyperpingClient.
|
|
2
|
+
|
|
3
|
+
Provides CRUD and reporting methods for Hyperping monitors. Mixed into
|
|
4
|
+
:class:`~hyperping.client.HyperpingClient` at class definition time.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from hyperping.endpoints import Endpoint
|
|
15
|
+
from hyperping.exceptions import HyperpingNotFoundError
|
|
16
|
+
from hyperping.models import (
|
|
17
|
+
Monitor,
|
|
18
|
+
MonitorCreate,
|
|
19
|
+
MonitorReport,
|
|
20
|
+
MonitorUpdate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MonitorsMixin:
|
|
27
|
+
"""Monitor-related API operations."""
|
|
28
|
+
|
|
29
|
+
def _request( # type: ignore[empty-body]
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
path: str,
|
|
33
|
+
json: dict[str, Any] | None = None,
|
|
34
|
+
params: dict[str, Any] | None = None,
|
|
35
|
+
) -> dict[str, Any]: ... # provided by HyperpingClient
|
|
36
|
+
|
|
37
|
+
def list_monitors(self) -> list[Monitor]:
|
|
38
|
+
"""List all monitors in the account.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of :class:`Monitor` objects. Monitors that fail to parse
|
|
42
|
+
are silently skipped with a warning log.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
HyperpingAuthError: If the API key is invalid.
|
|
46
|
+
HyperpingAPIError: On unexpected API errors.
|
|
47
|
+
"""
|
|
48
|
+
response = self._request("GET", Endpoint.MONITORS)
|
|
49
|
+
|
|
50
|
+
# Handle different response formats
|
|
51
|
+
if isinstance(response, list):
|
|
52
|
+
monitors_data = response
|
|
53
|
+
elif "monitors" in response:
|
|
54
|
+
monitors_data = response["monitors"]
|
|
55
|
+
else:
|
|
56
|
+
monitors_data = response.get("data", [])
|
|
57
|
+
|
|
58
|
+
monitors = []
|
|
59
|
+
skipped = 0
|
|
60
|
+
for data in monitors_data:
|
|
61
|
+
try:
|
|
62
|
+
monitors.append(Monitor.model_validate(data))
|
|
63
|
+
except (ValueError, ValidationError) as e:
|
|
64
|
+
skipped += 1
|
|
65
|
+
logger.warning(f"Failed to parse monitor data: {e}", extra={"data": data})
|
|
66
|
+
|
|
67
|
+
if skipped:
|
|
68
|
+
logger.warning(
|
|
69
|
+
f"{skipped} of {len(monitors_data)} monitors could not be parsed and were skipped"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return monitors
|
|
73
|
+
|
|
74
|
+
def get_monitor(self, monitor_id: str) -> Monitor:
|
|
75
|
+
"""Get a single monitor by ID.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
monitor_id: Monitor UUID
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Monitor object
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
HyperpingNotFoundError: If monitor not found
|
|
85
|
+
"""
|
|
86
|
+
response = self._request("GET", f"{Endpoint.MONITORS}/{monitor_id}")
|
|
87
|
+
return Monitor.model_validate(response)
|
|
88
|
+
|
|
89
|
+
def create_monitor(self, monitor: MonitorCreate) -> Monitor:
|
|
90
|
+
"""Create a new monitor.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
monitor: Monitor creation data.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Created :class:`Monitor` object.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
100
|
+
HyperpingAPIError: On unexpected API errors.
|
|
101
|
+
"""
|
|
102
|
+
payload = monitor.model_dump(exclude_none=True)
|
|
103
|
+
response = self._request("POST", Endpoint.MONITORS, json=payload)
|
|
104
|
+
return Monitor.model_validate(response)
|
|
105
|
+
|
|
106
|
+
# Writable fields for the Hyperping monitor PUT endpoint
|
|
107
|
+
_MONITOR_WRITABLE_FIELDS: frozenset[str] = frozenset(
|
|
108
|
+
{
|
|
109
|
+
"name",
|
|
110
|
+
"url",
|
|
111
|
+
"protocol",
|
|
112
|
+
"http_method",
|
|
113
|
+
"check_frequency",
|
|
114
|
+
"regions",
|
|
115
|
+
"request_headers",
|
|
116
|
+
"request_body",
|
|
117
|
+
"follow_redirects",
|
|
118
|
+
"expected_status_code",
|
|
119
|
+
"required_keyword",
|
|
120
|
+
"paused",
|
|
121
|
+
"port",
|
|
122
|
+
"alerts_wait",
|
|
123
|
+
"escalation_policy",
|
|
124
|
+
"dns_record_type",
|
|
125
|
+
"dns_nameserver",
|
|
126
|
+
"dns_expected_answer",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def update_monitor(self, monitor_id: str, update: MonitorUpdate) -> Monitor:
|
|
131
|
+
"""Update an existing monitor using read-modify-write.
|
|
132
|
+
|
|
133
|
+
The Hyperping v1 PUT endpoint requires a full payload. We fetch the
|
|
134
|
+
current state first and apply the update on top to avoid clobbering
|
|
135
|
+
fields not included in the partial update.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
monitor_id: Monitor UUID
|
|
139
|
+
update: Fields to update
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Updated Monitor object
|
|
143
|
+
"""
|
|
144
|
+
current = self.get_monitor(monitor_id)
|
|
145
|
+
|
|
146
|
+
# Build full payload from current writable state
|
|
147
|
+
payload: dict[str, Any] = current.model_dump(
|
|
148
|
+
mode="json",
|
|
149
|
+
exclude_none=True,
|
|
150
|
+
include=set(self._MONITOR_WRITABLE_FIELDS),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Apply the requested changes on top of current state
|
|
154
|
+
payload.update(update.model_dump(exclude_none=True))
|
|
155
|
+
|
|
156
|
+
response = self._request("PUT", f"{Endpoint.MONITORS}/{monitor_id}", json=payload)
|
|
157
|
+
return Monitor.model_validate(response)
|
|
158
|
+
|
|
159
|
+
def delete_monitor(self, monitor_id: str) -> None:
|
|
160
|
+
"""Delete a monitor.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
monitor_id: Monitor UUID
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
HyperpingNotFoundError: If monitor not found
|
|
167
|
+
"""
|
|
168
|
+
self._request("DELETE", f"{Endpoint.MONITORS}/{monitor_id}")
|
|
169
|
+
|
|
170
|
+
def pause_monitor(self, monitor_id: str) -> Monitor:
|
|
171
|
+
"""Pause a monitor.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
monitor_id: Monitor UUID
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Updated Monitor object
|
|
178
|
+
"""
|
|
179
|
+
return self.update_monitor(monitor_id, MonitorUpdate(paused=True))
|
|
180
|
+
|
|
181
|
+
def resume_monitor(self, monitor_id: str) -> Monitor:
|
|
182
|
+
"""Resume a paused monitor.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
monitor_id: Monitor UUID
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Updated Monitor object
|
|
189
|
+
"""
|
|
190
|
+
return self.update_monitor(monitor_id, MonitorUpdate(paused=False))
|
|
191
|
+
|
|
192
|
+
def get_all_reports(self, period: str = "30d") -> list[MonitorReport]:
|
|
193
|
+
"""Get uptime reports for all monitors in a single batch call.
|
|
194
|
+
|
|
195
|
+
Uses the v2 batch endpoint -- one API call for all monitors.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
period: Report period (``1h``, ``24h``, ``7d``, ``30d``, ``90d``).
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of :class:`MonitorReport` objects. Reports that fail to parse
|
|
202
|
+
are silently skipped with a warning log.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
HyperpingAuthError: If the API key is invalid.
|
|
206
|
+
HyperpingAPIError: On unexpected API errors.
|
|
207
|
+
"""
|
|
208
|
+
response = self._request("GET", Endpoint.REPORTS, params={"period": period})
|
|
209
|
+
period_info = response.get("period", {})
|
|
210
|
+
monitors_data = response.get("monitors", [])
|
|
211
|
+
|
|
212
|
+
reports = []
|
|
213
|
+
skipped = 0
|
|
214
|
+
for m in monitors_data:
|
|
215
|
+
try:
|
|
216
|
+
reports.append(MonitorReport.model_validate({**m, "period": period_info}))
|
|
217
|
+
except (ValueError, ValidationError) as e:
|
|
218
|
+
skipped += 1
|
|
219
|
+
logger.warning(f"Failed to parse monitor report: {e}", extra={"data": m})
|
|
220
|
+
|
|
221
|
+
if skipped:
|
|
222
|
+
logger.warning(
|
|
223
|
+
f"{skipped} of {len(monitors_data)} reports could not be parsed and were skipped"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return reports
|
|
227
|
+
|
|
228
|
+
def get_monitor_report(
|
|
229
|
+
self,
|
|
230
|
+
monitor_id: str,
|
|
231
|
+
period: str = "30d",
|
|
232
|
+
) -> MonitorReport:
|
|
233
|
+
"""Get uptime report for a single monitor.
|
|
234
|
+
|
|
235
|
+
Fetches the batch report and filters by monitor UUID.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
monitor_id: Monitor UUID
|
|
239
|
+
period: Report period (1h, 24h, 7d, 30d, 90d)
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
MonitorReport object
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
HyperpingNotFoundError: If no report found for the monitor
|
|
246
|
+
"""
|
|
247
|
+
for r in self.get_all_reports(period):
|
|
248
|
+
if r.uuid == monitor_id:
|
|
249
|
+
return r
|
|
250
|
+
raise HyperpingNotFoundError(f"No report found for monitor: {monitor_id}")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Outage operations mixin for HyperpingClient.
|
|
2
|
+
|
|
3
|
+
Provides methods for managing auto-detected outages (v2 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 typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from hyperping.endpoints import Endpoint
|
|
13
|
+
from hyperping.exceptions import HyperpingNotFoundError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OutagesMixin:
|
|
19
|
+
"""Outage-related API operations."""
|
|
20
|
+
|
|
21
|
+
def _request( # type: ignore[empty-body]
|
|
22
|
+
self,
|
|
23
|
+
method: str,
|
|
24
|
+
path: str,
|
|
25
|
+
json: dict[str, Any] | None = None,
|
|
26
|
+
params: dict[str, Any] | None = None,
|
|
27
|
+
) -> dict[str, Any]: ... # provided by HyperpingClient
|
|
28
|
+
|
|
29
|
+
def list_outages(self) -> list[dict[str, Any]]:
|
|
30
|
+
"""List auto-detected outages.
|
|
31
|
+
|
|
32
|
+
Returns raw dicts since the exact API response shape is not fully
|
|
33
|
+
documented. The command layer normalizes the response defensively.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of outage dicts from the API.
|
|
37
|
+
Empty list if the endpoint is not available (404).
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
data = self._request("GET", Endpoint.OUTAGES)
|
|
41
|
+
if isinstance(data, list):
|
|
42
|
+
return data
|
|
43
|
+
if isinstance(data, dict) and "outages" in data:
|
|
44
|
+
return cast(list[dict[str, Any]], data["outages"])
|
|
45
|
+
return []
|
|
46
|
+
except HyperpingNotFoundError:
|
|
47
|
+
logger.debug("Outage endpoint not available (404)")
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
def acknowledge_outage(self, outage_id: str, message: str | None = None) -> dict[str, Any]:
|
|
51
|
+
"""Acknowledge an outage.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
outage_id: Outage UUID.
|
|
55
|
+
message: Optional acknowledgement message.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
API response dict.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
HyperpingNotFoundError: If outage not found.
|
|
62
|
+
"""
|
|
63
|
+
json_body = {"message": message} if message else None
|
|
64
|
+
return self._request(
|
|
65
|
+
"POST",
|
|
66
|
+
f"{Endpoint.OUTAGES}/{outage_id}/acknowledge",
|
|
67
|
+
json=json_body,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def resolve_outage(self, outage_id: str, message: str | None = None) -> dict[str, Any]:
|
|
71
|
+
"""Resolve an outage.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
outage_id: Outage UUID.
|
|
75
|
+
message: Optional resolution message.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
API response dict.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
HyperpingNotFoundError: If outage not found.
|
|
82
|
+
"""
|
|
83
|
+
json_body = {"message": message} if message else None
|
|
84
|
+
return self._request(
|
|
85
|
+
"POST",
|
|
86
|
+
f"{Endpoint.OUTAGES}/{outage_id}/resolve",
|
|
87
|
+
json=json_body,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def escalate_outage(self, outage_id: str) -> dict[str, Any]:
|
|
91
|
+
"""Escalate an outage.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
outage_id: Outage UUID.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
API response dict.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
HyperpingNotFoundError: If outage not found.
|
|
101
|
+
"""
|
|
102
|
+
return self._request("POST", f"{Endpoint.OUTAGES}/{outage_id}/escalate")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Status page operations mixin for HyperpingClient.
|
|
2
|
+
|
|
3
|
+
Provides CRUD and subscriber methods for Hyperping status pages (v2 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 typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
14
|
+
from hyperping.endpoints import Endpoint
|
|
15
|
+
from hyperping.models import (
|
|
16
|
+
StatusPage,
|
|
17
|
+
StatusPageCreate,
|
|
18
|
+
StatusPageSubscriber,
|
|
19
|
+
StatusPageUpdate,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StatusPagesMixin:
|
|
26
|
+
"""Status page-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_status_pages(self, search: str | None = None) -> list[StatusPage]:
|
|
37
|
+
"""List all status pages.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
search: Optional search query to filter by name.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of :class:`StatusPage` objects. Pages 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
|
+
query_params: dict[str, Any] = {}
|
|
51
|
+
if search:
|
|
52
|
+
query_params["search"] = search
|
|
53
|
+
|
|
54
|
+
response = self._request(
|
|
55
|
+
"GET",
|
|
56
|
+
Endpoint.STATUSPAGES,
|
|
57
|
+
params=query_params if query_params else None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if isinstance(response, list):
|
|
61
|
+
pages_data = response
|
|
62
|
+
elif "statuspages" in response:
|
|
63
|
+
pages_data = response["statuspages"]
|
|
64
|
+
else:
|
|
65
|
+
pages_data = response.get("data", [])
|
|
66
|
+
|
|
67
|
+
pages = []
|
|
68
|
+
skipped = 0
|
|
69
|
+
for data in pages_data:
|
|
70
|
+
try:
|
|
71
|
+
pages.append(StatusPage.model_validate(data))
|
|
72
|
+
except (ValueError, ValidationError) as e:
|
|
73
|
+
skipped += 1
|
|
74
|
+
logger.warning(f"Failed to parse status page data: {e}", extra={"data": data})
|
|
75
|
+
|
|
76
|
+
if skipped:
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"{skipped} of {len(pages_data)} status pages could not be parsed and were skipped"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return pages
|
|
82
|
+
|
|
83
|
+
def get_status_page(self, status_page_id: str) -> StatusPage:
|
|
84
|
+
"""Get a single status page by ID.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
status_page_id: Status page UUID.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
:class:`StatusPage` object.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
HyperpingNotFoundError: If status page not found.
|
|
94
|
+
"""
|
|
95
|
+
response = self._request("GET", f"{Endpoint.STATUSPAGES}/{status_page_id}")
|
|
96
|
+
return StatusPage.model_validate(response)
|
|
97
|
+
|
|
98
|
+
def create_status_page(self, status_page: StatusPageCreate) -> StatusPage:
|
|
99
|
+
"""Create a new status page.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
status_page: Status page creation data.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Created :class:`StatusPage` object.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
109
|
+
HyperpingAPIError: On unexpected API errors.
|
|
110
|
+
"""
|
|
111
|
+
payload = status_page.model_dump(exclude_none=True, by_alias=True)
|
|
112
|
+
response = self._request("POST", Endpoint.STATUSPAGES, json=payload)
|
|
113
|
+
return StatusPage.model_validate(response)
|
|
114
|
+
|
|
115
|
+
def update_status_page(
|
|
116
|
+
self,
|
|
117
|
+
status_page_id: str,
|
|
118
|
+
update: StatusPageUpdate,
|
|
119
|
+
) -> StatusPage:
|
|
120
|
+
"""Update an existing status page.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
status_page_id: Status page UUID.
|
|
124
|
+
update: Fields to update.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Updated :class:`StatusPage` object.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
HyperpingNotFoundError: If status page not found.
|
|
131
|
+
HyperpingValidationError: If the payload fails server-side validation.
|
|
132
|
+
HyperpingAPIError: On unexpected API errors.
|
|
133
|
+
"""
|
|
134
|
+
payload = update.model_dump(exclude_none=True, by_alias=True)
|
|
135
|
+
response = self._request("PUT", f"{Endpoint.STATUSPAGES}/{status_page_id}", json=payload)
|
|
136
|
+
return StatusPage.model_validate(response)
|
|
137
|
+
|
|
138
|
+
def delete_status_page(self, status_page_id: str) -> None:
|
|
139
|
+
"""Delete a status page.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
status_page_id: Status page UUID.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
HyperpingNotFoundError: If status page not found.
|
|
146
|
+
"""
|
|
147
|
+
self._request("DELETE", f"{Endpoint.STATUSPAGES}/{status_page_id}")
|
|
148
|
+
|
|
149
|
+
def list_subscribers(self, status_page_id: str) -> list[StatusPageSubscriber]:
|
|
150
|
+
"""List subscribers for a status page.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
status_page_id: Status page UUID.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of :class:`StatusPageSubscriber` objects.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
HyperpingNotFoundError: If status page not found.
|
|
160
|
+
HyperpingAPIError: On unexpected API errors.
|
|
161
|
+
"""
|
|
162
|
+
response = self._request(
|
|
163
|
+
"GET", f"{Endpoint.STATUSPAGES}/{status_page_id}/subscribers"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if isinstance(response, list):
|
|
167
|
+
subscribers_data = response
|
|
168
|
+
elif "subscribers" in response:
|
|
169
|
+
subscribers_data = response["subscribers"]
|
|
170
|
+
else:
|
|
171
|
+
subscribers_data = response.get("data", [])
|
|
172
|
+
|
|
173
|
+
subscribers = []
|
|
174
|
+
skipped = 0
|
|
175
|
+
for data in subscribers_data:
|
|
176
|
+
try:
|
|
177
|
+
subscribers.append(StatusPageSubscriber.model_validate(data))
|
|
178
|
+
except (ValueError, ValidationError) as e:
|
|
179
|
+
skipped += 1
|
|
180
|
+
logger.warning(f"Failed to parse subscriber data: {e}", extra={"data": data})
|
|
181
|
+
|
|
182
|
+
if skipped:
|
|
183
|
+
logger.warning(
|
|
184
|
+
f"{skipped} of {len(subscribers_data)} subscribers could not be parsed "
|
|
185
|
+
"and were skipped"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return subscribers
|
|
189
|
+
|
|
190
|
+
def add_subscriber(self, status_page_id: str, email: str) -> StatusPageSubscriber:
|
|
191
|
+
"""Add a subscriber to a status page.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
status_page_id: Status page UUID.
|
|
195
|
+
email: Subscriber email address.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Created :class:`StatusPageSubscriber` object.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
HyperpingNotFoundError: If status page not found.
|
|
202
|
+
HyperpingValidationError: If the email is invalid.
|
|
203
|
+
HyperpingAPIError: On unexpected API errors.
|
|
204
|
+
"""
|
|
205
|
+
payload = {"email": email}
|
|
206
|
+
response = self._request(
|
|
207
|
+
"POST",
|
|
208
|
+
f"{Endpoint.STATUSPAGES}/{status_page_id}/subscribers",
|
|
209
|
+
json=payload,
|
|
210
|
+
)
|
|
211
|
+
return StatusPageSubscriber.model_validate(response)
|
|
212
|
+
|
|
213
|
+
def remove_subscriber(self, status_page_id: str, subscriber_id: str) -> None:
|
|
214
|
+
"""Remove a subscriber from a status page.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
status_page_id: Status page UUID.
|
|
218
|
+
subscriber_id: Subscriber ID.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
HyperpingNotFoundError: If status page or subscriber not found.
|
|
222
|
+
"""
|
|
223
|
+
self._request(
|
|
224
|
+
"DELETE",
|
|
225
|
+
f"{Endpoint.STATUSPAGES}/{status_page_id}/subscribers/{subscriber_id}",
|
|
226
|
+
)
|
hyperping/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|