hyperping 1.3.0__tar.gz → 1.4.1__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.3.0 → hyperping-1.4.1}/CHANGELOG.md +88 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/PKG-INFO +38 -1
- {hyperping-1.3.0 → hyperping-1.4.1}/README.md +37 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/pyproject.toml +1 -1
- hyperping-1.4.1/scripts/verify_endpoints.py +166 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/__init__.py +5 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_client.py +0 -8
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_incidents_mixin.py +3 -9
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_maintenance_mixin.py +3 -9
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_monitors_mixin.py +0 -13
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_outages_mixin.py +0 -31
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_statuspages_mixin.py +15 -13
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_incidents_mixin.py +3 -1
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_internals.py +1 -4
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_maintenance_mixin.py +3 -1
- hyperping-1.4.1/src/hyperping/_mcp_transport.py +146 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_monitors_mixin.py +0 -20
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_outages_mixin.py +0 -55
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_statuspages_mixin.py +12 -4
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_utils.py +2 -5
- hyperping-1.4.1/src/hyperping/_version.py +1 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/client.py +0 -8
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/endpoints.py +3 -75
- hyperping-1.4.1/src/hyperping/mcp_client.py +222 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_monitor_models.py +3 -8
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_async_client.py +14 -42
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_async_preexisting.py +3 -9
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_healthchecks.py +8 -21
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_incidents.py +1 -3
- hyperping-1.4.1/tests/unit/test_mcp_client.py +161 -0
- hyperping-1.4.1/tests/unit/test_mcp_transport.py +172 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_monitors.py +5 -58
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_outages.py +4 -101
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_pagination.py +1 -3
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_statuspages.py +11 -15
- hyperping-1.3.0/src/hyperping/_async_integrations_mixin.py +0 -26
- hyperping-1.3.0/src/hyperping/_async_observability_mixin.py +0 -65
- hyperping-1.3.0/src/hyperping/_async_oncall_mixin.py +0 -53
- hyperping-1.3.0/src/hyperping/_async_reporting_mixin.py +0 -39
- hyperping-1.3.0/src/hyperping/_integrations_mixin.py +0 -41
- hyperping-1.3.0/src/hyperping/_observability_mixin.py +0 -102
- hyperping-1.3.0/src/hyperping/_oncall_mixin.py +0 -88
- hyperping-1.3.0/src/hyperping/_reporting_mixin.py +0 -71
- hyperping-1.3.0/src/hyperping/_version.py +0 -1
- hyperping-1.3.0/tests/unit/test_async_new_mixins.py +0 -262
- hyperping-1.3.0/tests/unit/test_integrations.py +0 -98
- hyperping-1.3.0/tests/unit/test_observability.py +0 -203
- hyperping-1.3.0/tests/unit/test_oncall.py +0 -217
- hyperping-1.3.0/tests/unit/test_reporting.py +0 -140
- {hyperping-1.3.0 → hyperping-1.4.1}/.gitignore +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/CONTRIBUTING.md +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/LICENSE +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_async_healthchecks_mixin.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_circuit_breaker.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_healthchecks_mixin.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_monitor_constants.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/_protocols.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/exceptions.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/__init__.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_healthcheck_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_incident_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_integration_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_maintenance_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_observability_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_oncall_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_outage_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_reporting_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/models/_statuspage_models.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/src/hyperping/py.typed +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/__init__.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/__init__.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/conftest.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_maintenance.py +0 -0
- {hyperping-1.3.0 → hyperping-1.4.1}/tests/unit/test_sdk_surface.py +0 -0
|
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.1] - 2026-04-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- HTTP 403 from MCP server now correctly raises `HyperpingAuthError` (was
|
|
13
|
+
`HyperpingAPIError`). The Hyperping MCP server returns 403 for invalid API keys;
|
|
14
|
+
the transport only handled 401. Now matches REST client behavior.
|
|
15
|
+
- `MCP_URL` defined in single location (`endpoints.py`); removed duplicate in
|
|
16
|
+
`_mcp_transport.py`.
|
|
17
|
+
- MCP handshake version string uses `__version__` instead of hardcoded `"1.4.0"`.
|
|
18
|
+
|
|
19
|
+
## [1.4.0] - 2026-04-19
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`HyperpingMcpClient`** -- new client for Hyperping MCP server features not available
|
|
24
|
+
via the REST API. Uses JSON-RPC 2.0 over HTTP at `/v1/mcp` with the same Bearer token
|
|
25
|
+
API key. Provides 16 typed methods: `get_status_summary`, `get_monitor_response_time`,
|
|
26
|
+
`get_monitor_mtta`, `get_monitor_mttr`, `get_monitor_anomalies`, `get_monitor_http_logs`,
|
|
27
|
+
`list_recent_alerts`, `list_on_call_schedules`, `get_on_call_schedule`,
|
|
28
|
+
`list_escalation_policies`, `get_escalation_policy`, `list_team_members`,
|
|
29
|
+
`list_integrations`, `get_integration`, `get_outage_timeline`, `search_monitors_by_name`.
|
|
30
|
+
- **`McpTransport`** -- low-level JSON-RPC 2.0 transport with auto-initialization handshake,
|
|
31
|
+
double-parse response extraction, and error mapping to existing SDK exception types.
|
|
32
|
+
- **`MCP_URL`** constant exported from `hyperping` top-level.
|
|
33
|
+
- **Sync outage methods** -- `create_outage`, `delete_outage`, `get_outage`,
|
|
34
|
+
`unacknowledge_outage` added to `OutagesMixin` (were only in async client).
|
|
35
|
+
- **Verification script** -- `scripts/verify_endpoints.py` for testing endpoints against
|
|
36
|
+
the live API.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **Maintenance update** uses `model_dump(include=...)` instead of hard-coded field list.
|
|
41
|
+
- **Incident update error handling** -- `add_incident_update` now provides context when
|
|
42
|
+
the POST succeeds but the follow-up GET fails.
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- **Speculative REST methods** -- 12 methods that called nonexistent REST endpoints
|
|
47
|
+
(on-call, alerts, anomalies, integrations, probe logs, response time, MTTA, status
|
|
48
|
+
summary, outage timeline, monitor search) removed from `HyperpingClient` and
|
|
49
|
+
`AsyncHyperpingClient`. These features are MCP-only; use `HyperpingMcpClient` instead.
|
|
50
|
+
- **8 speculative mixin files** (sync + async) deleted.
|
|
51
|
+
- **8 speculative Endpoint enum entries** removed from `endpoints.py`.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- HTTP 403 from MCP server now correctly raises `HyperpingAuthError` (was
|
|
56
|
+
`HyperpingAPIError`). Matches REST client behavior.
|
|
57
|
+
- `MCP_URL` defined in single location (`endpoints.py`), not duplicated.
|
|
58
|
+
- MCP handshake version uses `__version__` instead of hardcoded string.
|
|
59
|
+
- `pytest` bumped to 9.0.3 (CVE-2025-71176).
|
|
60
|
+
|
|
61
|
+
## [1.3.0] - 2026-04-18 [YANKED]
|
|
62
|
+
|
|
63
|
+
v1.3.0 added 18 speculative REST methods for MCP-discovered features (reporting,
|
|
64
|
+
observability, on-call, integrations). All 12 endpoint paths were guessed from MCP
|
|
65
|
+
tool names and **none of them work via the REST API** (verified: 10x 404, 2x 401).
|
|
66
|
+
These features are only accessible through the MCP server (JSON-RPC 2.0). Superseded
|
|
67
|
+
by v1.4.0 which replaces the broken REST methods with a proper `HyperpingMcpClient`.
|
|
68
|
+
|
|
69
|
+
## [1.2.1] - 2026-04-17
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Bump `pytest` to 9.0.3 (CVE-2025-71176).
|
|
74
|
+
|
|
75
|
+
## [1.2.0] - 2026-04-17
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **Sync outage methods** -- `create_outage`, `delete_outage`, `get_outage`,
|
|
80
|
+
`unacknowledge_outage` added to `OutagesMixin` for feature parity with async client.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- **Maintenance update** uses `model_dump(include=...)` instead of hard-coded field
|
|
85
|
+
enumeration for robustness.
|
|
86
|
+
- **Incident update error handling** -- `add_incident_update` now provides context when
|
|
87
|
+
the POST succeeds but the follow-up GET fails.
|
|
88
|
+
|
|
8
89
|
## [1.1.0] - 2026-04-09
|
|
9
90
|
|
|
10
91
|
### Added
|
|
@@ -30,6 +111,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
30
111
|
- **`collect_all_pages` / `collect_all_pages_async`** helpers in `_utils.py` for
|
|
31
112
|
transparent multi-page result aggregation.
|
|
32
113
|
|
|
114
|
+
## [1.0.1] - 2026-04-05
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- Fix version string in `pyproject.toml` (was out of sync with `_version.py`).
|
|
119
|
+
- Fix package metadata: incorrect email address in project config.
|
|
120
|
+
|
|
33
121
|
## [1.0.0] - 2026-04-05
|
|
34
122
|
|
|
35
123
|
First stable release. The public API is production-ready and covered by semver
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperping
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
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
|
|
@@ -181,6 +181,43 @@ sub = client.add_subscriber("sp_uuid", "user@example.com")
|
|
|
181
181
|
client.remove_subscriber("sp_uuid", sub.id)
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### MCP Client (on-call, alerts, anomalies, integrations)
|
|
185
|
+
|
|
186
|
+
Some Hyperping features are only available via the MCP server (JSON-RPC 2.0),
|
|
187
|
+
not the REST API. Use `HyperpingMcpClient` for these:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from hyperping import HyperpingMcpClient
|
|
191
|
+
|
|
192
|
+
with HyperpingMcpClient(api_key="sk_...") as mcp:
|
|
193
|
+
# Status & reporting
|
|
194
|
+
summary = mcp.get_status_summary()
|
|
195
|
+
mtta = mcp.get_monitor_mtta("mon_uuid")
|
|
196
|
+
mttr = mcp.get_monitor_mttr("mon_uuid")
|
|
197
|
+
response_time = mcp.get_monitor_response_time("mon_uuid")
|
|
198
|
+
|
|
199
|
+
# On-call & escalation
|
|
200
|
+
schedules = mcp.list_on_call_schedules()
|
|
201
|
+
policies = mcp.list_escalation_policies()
|
|
202
|
+
members = mcp.list_team_members()
|
|
203
|
+
|
|
204
|
+
# Observability
|
|
205
|
+
anomalies = mcp.get_monitor_anomalies("mon_uuid")
|
|
206
|
+
logs = mcp.get_monitor_http_logs("mon_uuid")
|
|
207
|
+
alerts = mcp.list_recent_alerts()
|
|
208
|
+
|
|
209
|
+
# Integrations
|
|
210
|
+
integrations = mcp.list_integrations()
|
|
211
|
+
|
|
212
|
+
# Outage timeline & monitor search
|
|
213
|
+
timeline = mcp.get_outage_timeline("out_uuid")
|
|
214
|
+
results = mcp.search_monitors_by_name("api")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The MCP client uses the same API key as `HyperpingClient`. All methods return
|
|
218
|
+
plain dicts/lists; use the exported Pydantic models (e.g., `OnCallSchedule`,
|
|
219
|
+
`EscalationPolicy`) for validation if needed.
|
|
220
|
+
|
|
184
221
|
### Healthchecks
|
|
185
222
|
|
|
186
223
|
```python
|
|
@@ -144,6 +144,43 @@ sub = client.add_subscriber("sp_uuid", "user@example.com")
|
|
|
144
144
|
client.remove_subscriber("sp_uuid", sub.id)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### MCP Client (on-call, alerts, anomalies, integrations)
|
|
148
|
+
|
|
149
|
+
Some Hyperping features are only available via the MCP server (JSON-RPC 2.0),
|
|
150
|
+
not the REST API. Use `HyperpingMcpClient` for these:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from hyperping import HyperpingMcpClient
|
|
154
|
+
|
|
155
|
+
with HyperpingMcpClient(api_key="sk_...") as mcp:
|
|
156
|
+
# Status & reporting
|
|
157
|
+
summary = mcp.get_status_summary()
|
|
158
|
+
mtta = mcp.get_monitor_mtta("mon_uuid")
|
|
159
|
+
mttr = mcp.get_monitor_mttr("mon_uuid")
|
|
160
|
+
response_time = mcp.get_monitor_response_time("mon_uuid")
|
|
161
|
+
|
|
162
|
+
# On-call & escalation
|
|
163
|
+
schedules = mcp.list_on_call_schedules()
|
|
164
|
+
policies = mcp.list_escalation_policies()
|
|
165
|
+
members = mcp.list_team_members()
|
|
166
|
+
|
|
167
|
+
# Observability
|
|
168
|
+
anomalies = mcp.get_monitor_anomalies("mon_uuid")
|
|
169
|
+
logs = mcp.get_monitor_http_logs("mon_uuid")
|
|
170
|
+
alerts = mcp.list_recent_alerts()
|
|
171
|
+
|
|
172
|
+
# Integrations
|
|
173
|
+
integrations = mcp.list_integrations()
|
|
174
|
+
|
|
175
|
+
# Outage timeline & monitor search
|
|
176
|
+
timeline = mcp.get_outage_timeline("out_uuid")
|
|
177
|
+
results = mcp.search_monitors_by_name("api")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The MCP client uses the same API key as `HyperpingClient`. All methods return
|
|
181
|
+
plain dicts/lists; use the exported Pydantic models (e.g., `OnCallSchedule`,
|
|
182
|
+
`EscalationPolicy`) for validation if needed.
|
|
183
|
+
|
|
147
184
|
### Healthchecks
|
|
148
185
|
|
|
149
186
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hyperping"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.1"
|
|
8
8
|
description = "Python SDK for the Hyperping uptime monitoring and incident management API"
|
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify speculative MCP-discovered endpoint paths against the live Hyperping API.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
export HYPERPING_API_KEY=sk_...
|
|
6
|
+
python scripts/verify_endpoints.py
|
|
7
|
+
|
|
8
|
+
All calls are read-only (GET only). Safe to run repeatedly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
API_BASE = "https://api.hyperping.io"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Result:
|
|
22
|
+
method: str
|
|
23
|
+
path: str
|
|
24
|
+
status: int
|
|
25
|
+
ok: bool
|
|
26
|
+
snippet: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def probe(
|
|
30
|
+
client: httpx.Client,
|
|
31
|
+
method_name: str,
|
|
32
|
+
path: str,
|
|
33
|
+
params: dict | None = None,
|
|
34
|
+
) -> Result:
|
|
35
|
+
"""Send a GET request and return a Result."""
|
|
36
|
+
url = f"{API_BASE}{path}"
|
|
37
|
+
try:
|
|
38
|
+
resp = client.get(url, params=params or {})
|
|
39
|
+
body = resp.text[:120].replace("\n", " ")
|
|
40
|
+
return Result(
|
|
41
|
+
method=method_name,
|
|
42
|
+
path=path,
|
|
43
|
+
status=resp.status_code,
|
|
44
|
+
ok=resp.status_code < 400,
|
|
45
|
+
snippet=body,
|
|
46
|
+
)
|
|
47
|
+
except httpx.HTTPError as exc:
|
|
48
|
+
return Result(
|
|
49
|
+
method=method_name,
|
|
50
|
+
path=path,
|
|
51
|
+
status=0,
|
|
52
|
+
ok=False,
|
|
53
|
+
snippet=f"Connection error: {exc}",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> int:
|
|
58
|
+
api_key = os.environ.get("HYPERPING_API_KEY", "")
|
|
59
|
+
if not api_key:
|
|
60
|
+
print("ERROR: Set HYPERPING_API_KEY environment variable.")
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
64
|
+
client = httpx.Client(headers=headers, timeout=15.0)
|
|
65
|
+
|
|
66
|
+
# -- Fetch real UUIDs for sub-resource endpoints --
|
|
67
|
+
print("Fetching monitor and outage UUIDs for sub-resource tests...")
|
|
68
|
+
monitor_uuid: str | None = None
|
|
69
|
+
outage_uuid: str | None = None
|
|
70
|
+
|
|
71
|
+
resp = client.get(f"{API_BASE}/v1/monitors")
|
|
72
|
+
if resp.status_code == 200:
|
|
73
|
+
data = resp.json()
|
|
74
|
+
monitors = data if isinstance(data, list) else data.get("monitors", [])
|
|
75
|
+
if monitors:
|
|
76
|
+
monitor_uuid = monitors[0].get("uuid") or monitors[0].get("id")
|
|
77
|
+
print(f" Monitor UUID: {monitor_uuid}")
|
|
78
|
+
|
|
79
|
+
resp = client.get(f"{API_BASE}/v2/outages")
|
|
80
|
+
if resp.status_code == 200:
|
|
81
|
+
data = resp.json()
|
|
82
|
+
outages = data if isinstance(data, list) else data.get("outages", [])
|
|
83
|
+
if outages:
|
|
84
|
+
outage_uuid = outages[0].get("uuid") or outages[0].get("id")
|
|
85
|
+
print(f" Outage UUID: {outage_uuid}")
|
|
86
|
+
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
# -- Define all speculative endpoints to verify --
|
|
90
|
+
checks: list[tuple[str, str, dict | None]] = [
|
|
91
|
+
# Endpoint enum speculative paths
|
|
92
|
+
("get_status_summary", "/v2/status-summary", None),
|
|
93
|
+
("list_recent_alerts", "/v2/alerts", None),
|
|
94
|
+
("list_on_call_schedules", "/v2/on-call-schedules", None),
|
|
95
|
+
("list_escalation_policies", "/v2/escalation-policies", None),
|
|
96
|
+
("list_team_members", "/v2/team-members", None),
|
|
97
|
+
("list_integrations", "/v2/integrations", None),
|
|
98
|
+
# Monitor search
|
|
99
|
+
("search_monitors", "/v1/monitors/search", {"query": "test"}),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
# Sub-resource paths needing a real UUID
|
|
103
|
+
if monitor_uuid:
|
|
104
|
+
checks.extend([
|
|
105
|
+
(
|
|
106
|
+
"get_monitor_response_time",
|
|
107
|
+
f"/v2/reporting/response-time/{monitor_uuid}",
|
|
108
|
+
{"period": "24h"},
|
|
109
|
+
),
|
|
110
|
+
(
|
|
111
|
+
"get_monitor_mtta",
|
|
112
|
+
f"/v2/reporting/mtta/{monitor_uuid}",
|
|
113
|
+
{"period": "30d"},
|
|
114
|
+
),
|
|
115
|
+
(
|
|
116
|
+
"get_monitor_anomalies",
|
|
117
|
+
f"/v1/monitors/{monitor_uuid}/anomalies",
|
|
118
|
+
None,
|
|
119
|
+
),
|
|
120
|
+
(
|
|
121
|
+
"get_monitor_http_logs",
|
|
122
|
+
f"/v1/monitors/{monitor_uuid}/http-logs",
|
|
123
|
+
{"page": "0", "limit": "5"},
|
|
124
|
+
),
|
|
125
|
+
])
|
|
126
|
+
else:
|
|
127
|
+
print("WARNING: No monitors found; skipping monitor sub-resource checks.\n")
|
|
128
|
+
|
|
129
|
+
if outage_uuid:
|
|
130
|
+
checks.append((
|
|
131
|
+
"get_outage_timeline",
|
|
132
|
+
f"/v2/outages/{outage_uuid}/timeline",
|
|
133
|
+
None,
|
|
134
|
+
))
|
|
135
|
+
else:
|
|
136
|
+
print("WARNING: No outages found; skipping outage sub-resource checks.\n")
|
|
137
|
+
|
|
138
|
+
# -- Run probes --
|
|
139
|
+
results: list[Result] = []
|
|
140
|
+
for method_name, path, params in checks:
|
|
141
|
+
r = probe(client, method_name, path, params)
|
|
142
|
+
results.append(r)
|
|
143
|
+
|
|
144
|
+
client.close()
|
|
145
|
+
|
|
146
|
+
# -- Print results table --
|
|
147
|
+
print(f"{'Method':<30} {'Path':<50} {'Status':<8} {'Result'}")
|
|
148
|
+
print("-" * 120)
|
|
149
|
+
for r in results:
|
|
150
|
+
tag = "OK" if r.ok else ("404" if r.status == 404 else "ERROR")
|
|
151
|
+
print(f"{r.method:<30} {r.path:<50} {r.status:<8} {tag}")
|
|
152
|
+
if not r.ok:
|
|
153
|
+
print(f" {r.snippet}")
|
|
154
|
+
|
|
155
|
+
# -- Summary --
|
|
156
|
+
ok_count = sum(1 for r in results if r.ok)
|
|
157
|
+
fail_count = sum(1 for r in results if not r.ok)
|
|
158
|
+
not_found = sum(1 for r in results if r.status == 404)
|
|
159
|
+
print()
|
|
160
|
+
print(f"Total: {len(results)} | OK: {ok_count} | 404: {not_found} | Other errors: {fail_count - not_found}")
|
|
161
|
+
|
|
162
|
+
return 1 if fail_count > 0 else 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
sys.exit(main())
|
|
@@ -25,6 +25,7 @@ from hyperping.client import (
|
|
|
25
25
|
)
|
|
26
26
|
from hyperping.endpoints import (
|
|
27
27
|
API_BASE,
|
|
28
|
+
MCP_URL,
|
|
28
29
|
APIVersion,
|
|
29
30
|
Endpoint,
|
|
30
31
|
)
|
|
@@ -35,6 +36,7 @@ from hyperping.exceptions import (
|
|
|
35
36
|
HyperpingRateLimitError,
|
|
36
37
|
HyperpingValidationError,
|
|
37
38
|
)
|
|
39
|
+
from hyperping.mcp_client import HyperpingMcpClient
|
|
38
40
|
from hyperping.models import (
|
|
39
41
|
DEFAULT_REGIONS,
|
|
40
42
|
AddIncidentUpdateRequest,
|
|
@@ -91,6 +93,9 @@ __all__ = [
|
|
|
91
93
|
# Clients
|
|
92
94
|
"AsyncHyperpingClient",
|
|
93
95
|
"HyperpingClient",
|
|
96
|
+
"HyperpingMcpClient",
|
|
97
|
+
# MCP
|
|
98
|
+
"MCP_URL",
|
|
94
99
|
# Configuration
|
|
95
100
|
"RetryConfig",
|
|
96
101
|
"CircuitBreakerConfig",
|
|
@@ -21,13 +21,9 @@ 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
|
|
25
24
|
from hyperping._async_maintenance_mixin import AsyncMaintenanceMixin
|
|
26
25
|
from hyperping._async_monitors_mixin import AsyncMonitorsMixin
|
|
27
|
-
from hyperping._async_observability_mixin import AsyncObservabilityMixin
|
|
28
|
-
from hyperping._async_oncall_mixin import AsyncOnCallMixin
|
|
29
26
|
from hyperping._async_outages_mixin import AsyncOutagesMixin
|
|
30
|
-
from hyperping._async_reporting_mixin import AsyncReportingMixin
|
|
31
27
|
from hyperping._async_statuspages_mixin import AsyncStatusPagesMixin
|
|
32
28
|
from hyperping._circuit_breaker import (
|
|
33
29
|
CircuitBreaker,
|
|
@@ -52,10 +48,6 @@ class AsyncHyperpingClient(
|
|
|
52
48
|
AsyncOutagesMixin,
|
|
53
49
|
AsyncStatusPagesMixin,
|
|
54
50
|
AsyncHealthchecksMixin,
|
|
55
|
-
AsyncReportingMixin,
|
|
56
|
-
AsyncObservabilityMixin,
|
|
57
|
-
AsyncOnCallMixin,
|
|
58
|
-
AsyncIntegrationsMixin,
|
|
59
51
|
):
|
|
60
52
|
"""Async client for interacting with the Hyperping API.
|
|
61
53
|
|
|
@@ -45,9 +45,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
|
|
|
45
45
|
if status:
|
|
46
46
|
params["status"] = status
|
|
47
47
|
|
|
48
|
-
response = await self._request(
|
|
49
|
-
"GET", Endpoint.INCIDENTS, params=params or None
|
|
50
|
-
)
|
|
48
|
+
response = await self._request("GET", Endpoint.INCIDENTS, params=params or None)
|
|
51
49
|
return parse_list(unwrap_list(response, "incidents"), Incident, "incident")
|
|
52
50
|
|
|
53
51
|
async def get_incident(self, incident_id: str) -> Incident:
|
|
@@ -114,9 +112,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
|
|
|
114
112
|
validate_id(incident_id, "incident_id")
|
|
115
113
|
payload = update.model_dump(exclude_none=True, by_alias=True)
|
|
116
114
|
response = expect_dict(
|
|
117
|
-
await self._request(
|
|
118
|
-
"PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload
|
|
119
|
-
),
|
|
115
|
+
await self._request("PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload),
|
|
120
116
|
"update_incident",
|
|
121
117
|
)
|
|
122
118
|
return Incident.model_validate(response)
|
|
@@ -145,9 +141,7 @@ class AsyncIncidentsMixin(_AsyncClientProtocol):
|
|
|
145
141
|
await self._request("POST", url, json=payload)
|
|
146
142
|
return await self.get_incident(incident_id)
|
|
147
143
|
|
|
148
|
-
async def resolve_incident(
|
|
149
|
-
self, incident_id: str, message: str | None = None
|
|
150
|
-
) -> Incident:
|
|
144
|
+
async def resolve_incident(self, incident_id: str, message: str | None = None) -> Incident:
|
|
151
145
|
"""Resolve an incident.
|
|
152
146
|
|
|
153
147
|
Args:
|
|
@@ -41,9 +41,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
|
|
|
41
41
|
if status:
|
|
42
42
|
params["status"] = status
|
|
43
43
|
|
|
44
|
-
response = await self._request(
|
|
45
|
-
"GET", Endpoint.MAINTENANCE, params=params or None
|
|
46
|
-
)
|
|
44
|
+
response = await self._request("GET", Endpoint.MAINTENANCE, params=params or None)
|
|
47
45
|
|
|
48
46
|
raw = unwrap_list(response, "maintenanceWindows")
|
|
49
47
|
if not raw and isinstance(response, dict) and "maintenance" in response:
|
|
@@ -64,9 +62,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
|
|
|
64
62
|
HyperpingNotFoundError: If maintenance not found
|
|
65
63
|
"""
|
|
66
64
|
validate_id(maintenance_id, "maintenance_id")
|
|
67
|
-
response = await self._request(
|
|
68
|
-
"GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}"
|
|
69
|
-
)
|
|
65
|
+
response = await self._request("GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}")
|
|
70
66
|
return Maintenance.model_validate(expect_dict(response, "get_maintenance"))
|
|
71
67
|
|
|
72
68
|
async def create_maintenance(self, maintenance: MaintenanceCreate) -> Maintenance:
|
|
@@ -127,9 +123,7 @@ class AsyncMaintenanceMixin(_AsyncClientProtocol):
|
|
|
127
123
|
payload.update(partial)
|
|
128
124
|
|
|
129
125
|
response = expect_dict(
|
|
130
|
-
await self._request(
|
|
131
|
-
"PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload
|
|
132
|
-
),
|
|
126
|
+
await self._request("PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload),
|
|
133
127
|
"update_maintenance",
|
|
134
128
|
)
|
|
135
129
|
return Maintenance.model_validate(response)
|
|
@@ -187,16 +187,3 @@ class AsyncMonitorsMixin(_AsyncClientProtocol):
|
|
|
187
187
|
if r.uuid == monitor_id:
|
|
188
188
|
return r
|
|
189
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")
|
|
@@ -19,7 +19,6 @@ 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
|
|
23
22
|
|
|
24
23
|
logger = logging.getLogger(__name__)
|
|
25
24
|
|
|
@@ -206,33 +205,3 @@ class AsyncOutagesMixin(_AsyncClientProtocol):
|
|
|
206
205
|
validate_id(outage_id, "outage_id")
|
|
207
206
|
result = await self._request("GET", f"{Endpoint.OUTAGES}/{outage_id}")
|
|
208
207
|
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")
|
|
@@ -69,8 +69,12 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
|
|
|
69
69
|
|
|
70
70
|
try:
|
|
71
71
|
return await collect_all_pages_async(
|
|
72
|
-
self._request,
|
|
73
|
-
|
|
72
|
+
self._request,
|
|
73
|
+
Endpoint.STATUSPAGES,
|
|
74
|
+
"statuspages",
|
|
75
|
+
params or None,
|
|
76
|
+
StatusPage,
|
|
77
|
+
"status page",
|
|
74
78
|
)
|
|
75
79
|
except HyperpingNotFoundError:
|
|
76
80
|
logger.debug("Status pages endpoint not available (404)")
|
|
@@ -131,9 +135,7 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
|
|
|
131
135
|
validate_id(status_page_id, "status_page_id")
|
|
132
136
|
payload = update.model_dump(exclude_none=True, by_alias=True)
|
|
133
137
|
response = expect_dict(
|
|
134
|
-
await self._request(
|
|
135
|
-
"PUT", f"{Endpoint.STATUSPAGES}/{status_page_id}", json=payload
|
|
136
|
-
),
|
|
138
|
+
await self._request("PUT", f"{Endpoint.STATUSPAGES}/{status_page_id}", json=payload),
|
|
137
139
|
"update_status_page",
|
|
138
140
|
)
|
|
139
141
|
return StatusPage.model_validate(response)
|
|
@@ -189,13 +191,15 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
|
|
|
189
191
|
)
|
|
190
192
|
|
|
191
193
|
return await collect_all_pages_async(
|
|
192
|
-
self._request,
|
|
193
|
-
|
|
194
|
+
self._request,
|
|
195
|
+
endpoint,
|
|
196
|
+
"subscribers",
|
|
197
|
+
params or None,
|
|
198
|
+
StatusPageSubscriber,
|
|
199
|
+
"subscriber",
|
|
194
200
|
)
|
|
195
201
|
|
|
196
|
-
async def add_subscriber(
|
|
197
|
-
self, status_page_id: str, email: str
|
|
198
|
-
) -> StatusPageSubscriber:
|
|
202
|
+
async def add_subscriber(self, status_page_id: str, email: str) -> StatusPageSubscriber:
|
|
199
203
|
"""Add a subscriber to a status page.
|
|
200
204
|
|
|
201
205
|
Args:
|
|
@@ -225,9 +229,7 @@ class AsyncStatusPagesMixin(_AsyncClientProtocol):
|
|
|
225
229
|
)
|
|
226
230
|
return StatusPageSubscriber.model_validate(response)
|
|
227
231
|
|
|
228
|
-
async def remove_subscriber(
|
|
229
|
-
self, status_page_id: str, subscriber_id: str
|
|
230
|
-
) -> None:
|
|
232
|
+
async def remove_subscriber(self, status_page_id: str, subscriber_id: str) -> None:
|
|
231
233
|
"""Remove a subscriber from a status page.
|
|
232
234
|
|
|
233
235
|
Args:
|
|
@@ -48,7 +48,9 @@ class IncidentsMixin(_ClientProtocol):
|
|
|
48
48
|
params["status"] = status
|
|
49
49
|
|
|
50
50
|
response = self._request(
|
|
51
|
-
"GET",
|
|
51
|
+
"GET",
|
|
52
|
+
Endpoint.INCIDENTS,
|
|
53
|
+
params=params or None, # M20
|
|
52
54
|
)
|
|
53
55
|
return parse_list(unwrap_list(response, "incidents"), Incident, "incident")
|
|
54
56
|
|
|
@@ -28,7 +28,4 @@ def sanitize_for_log(data: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
|
28
28
|
"""
|
|
29
29
|
if data is None:
|
|
30
30
|
return None
|
|
31
|
-
return {
|
|
32
|
-
k: "[REDACTED]" if k.lower() in _SENSITIVE_LOG_KEYS else v
|
|
33
|
-
for k, v in data.items()
|
|
34
|
-
}
|
|
31
|
+
return {k: "[REDACTED]" if k.lower() in _SENSITIVE_LOG_KEYS else v for k, v in data.items()}
|
|
@@ -43,7 +43,9 @@ class MaintenanceMixin(_ClientProtocol):
|
|
|
43
43
|
params["status"] = status
|
|
44
44
|
|
|
45
45
|
response = self._request(
|
|
46
|
-
"GET",
|
|
46
|
+
"GET",
|
|
47
|
+
Endpoint.MAINTENANCE,
|
|
48
|
+
params=params or None, # M20
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
# API returns {"maintenanceWindows": [...]} as of current version
|