Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "hyperping"
version = "1.3.0"
version = "1.4.0"
description = "Python SDK for the Hyperping uptime monitoring and incident management API"
readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "MIT"}
Expand Down
166 changes: 166 additions & 0 deletions scripts/verify_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Verify speculative MCP-discovered endpoint paths against the live Hyperping API.

Usage:
export HYPERPING_API_KEY=sk_...
python scripts/verify_endpoints.py

All calls are read-only (GET only). Safe to run repeatedly.
"""

import os
import sys
from dataclasses import dataclass

import httpx

API_BASE = "https://api.hyperping.io"


@dataclass
class Result:
method: str
path: str
status: int
ok: bool
snippet: str


def probe(
client: httpx.Client,
method_name: str,
path: str,
params: dict | None = None,
) -> Result:
"""Send a GET request and return a Result."""
url = f"{API_BASE}{path}"
try:
resp = client.get(url, params=params or {})
body = resp.text[:120].replace("\n", " ")
return Result(
method=method_name,
path=path,
status=resp.status_code,
ok=resp.status_code < 400,
snippet=body,
)
except httpx.HTTPError as exc:
return Result(
method=method_name,
path=path,
status=0,
ok=False,
snippet=f"Connection error: {exc}",
)


def main() -> int:
api_key = os.environ.get("HYPERPING_API_KEY", "")
if not api_key:
print("ERROR: Set HYPERPING_API_KEY environment variable.")
return 1

headers = {"Authorization": f"Bearer {api_key}"}
client = httpx.Client(headers=headers, timeout=15.0)

# -- Fetch real UUIDs for sub-resource endpoints --
print("Fetching monitor and outage UUIDs for sub-resource tests...")
monitor_uuid: str | None = None
outage_uuid: str | None = None

resp = client.get(f"{API_BASE}/v1/monitors")
if resp.status_code == 200:
data = resp.json()
monitors = data if isinstance(data, list) else data.get("monitors", [])
if monitors:
monitor_uuid = monitors[0].get("uuid") or monitors[0].get("id")
print(f" Monitor UUID: {monitor_uuid}")

resp = client.get(f"{API_BASE}/v2/outages")
if resp.status_code == 200:
data = resp.json()
outages = data if isinstance(data, list) else data.get("outages", [])
if outages:
outage_uuid = outages[0].get("uuid") or outages[0].get("id")
print(f" Outage UUID: {outage_uuid}")

print()

# -- Define all speculative endpoints to verify --
checks: list[tuple[str, str, dict | None]] = [
# Endpoint enum speculative paths
("get_status_summary", "/v2/status-summary", None),
("list_recent_alerts", "/v2/alerts", None),
("list_on_call_schedules", "/v2/on-call-schedules", None),
("list_escalation_policies", "/v2/escalation-policies", None),
("list_team_members", "/v2/team-members", None),
("list_integrations", "/v2/integrations", None),
# Monitor search
("search_monitors", "/v1/monitors/search", {"query": "test"}),
]

# Sub-resource paths needing a real UUID
if monitor_uuid:
checks.extend([
(
"get_monitor_response_time",
f"/v2/reporting/response-time/{monitor_uuid}",
{"period": "24h"},
),
(
"get_monitor_mtta",
f"/v2/reporting/mtta/{monitor_uuid}",
{"period": "30d"},
),
(
"get_monitor_anomalies",
f"/v1/monitors/{monitor_uuid}/anomalies",
None,
),
(
"get_monitor_http_logs",
f"/v1/monitors/{monitor_uuid}/http-logs",
{"page": "0", "limit": "5"},
),
])
else:
print("WARNING: No monitors found; skipping monitor sub-resource checks.\n")

if outage_uuid:
checks.append((
"get_outage_timeline",
f"/v2/outages/{outage_uuid}/timeline",
None,
))
else:
print("WARNING: No outages found; skipping outage sub-resource checks.\n")

# -- Run probes --
results: list[Result] = []
for method_name, path, params in checks:
r = probe(client, method_name, path, params)
results.append(r)

client.close()

# -- Print results table --
print(f"{'Method':<30} {'Path':<50} {'Status':<8} {'Result'}")
print("-" * 120)
for r in results:
tag = "OK" if r.ok else ("404" if r.status == 404 else "ERROR")
print(f"{r.method:<30} {r.path:<50} {r.status:<8} {tag}")
if not r.ok:
print(f" {r.snippet}")

# -- Summary --
ok_count = sum(1 for r in results if r.ok)
fail_count = sum(1 for r in results if not r.ok)
not_found = sum(1 for r in results if r.status == 404)
print()
print(f"Total: {len(results)} | OK: {ok_count} | 404: {not_found} | Other errors: {fail_count - not_found}")

return 1 if fail_count > 0 else 0


if __name__ == "__main__":
sys.exit(main())
5 changes: 5 additions & 0 deletions src/hyperping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

from hyperping._async_client import AsyncHyperpingClient
from hyperping._mcp_transport import MCP_URL
from hyperping._version import __version__
from hyperping.client import (
CircuitBreaker,
Expand All @@ -35,6 +36,7 @@
HyperpingRateLimitError,
HyperpingValidationError,
)
from hyperping.mcp_client import HyperpingMcpClient
from hyperping.models import (
DEFAULT_REGIONS,
AddIncidentUpdateRequest,
Expand Down Expand Up @@ -91,6 +93,9 @@
# Clients
"AsyncHyperpingClient",
"HyperpingClient",
"HyperpingMcpClient",
# MCP
"MCP_URL",
# Configuration
"RetryConfig",
"CircuitBreakerConfig",
Expand Down
8 changes: 0 additions & 8 deletions src/hyperping/_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,9 @@

from hyperping._async_healthchecks_mixin import AsyncHealthchecksMixin
from hyperping._async_incidents_mixin import AsyncIncidentsMixin
from hyperping._async_integrations_mixin import AsyncIntegrationsMixin
from hyperping._async_maintenance_mixin import AsyncMaintenanceMixin
from hyperping._async_monitors_mixin import AsyncMonitorsMixin
from hyperping._async_observability_mixin import AsyncObservabilityMixin
from hyperping._async_oncall_mixin import AsyncOnCallMixin
from hyperping._async_outages_mixin import AsyncOutagesMixin
from hyperping._async_reporting_mixin import AsyncReportingMixin
from hyperping._async_statuspages_mixin import AsyncStatusPagesMixin
from hyperping._circuit_breaker import (
CircuitBreaker,
Expand All @@ -52,10 +48,6 @@ class AsyncHyperpingClient(
AsyncOutagesMixin,
AsyncStatusPagesMixin,
AsyncHealthchecksMixin,
AsyncReportingMixin,
AsyncObservabilityMixin,
AsyncOnCallMixin,
AsyncIntegrationsMixin,
):
"""Async client for interacting with the Hyperping API.

Expand Down
12 changes: 3 additions & 9 deletions src/hyperping/_async_incidents_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ async def list_incidents(self, status: str | None = None) -> list[Incident]:
if status:
params["status"] = status

response = await self._request(
"GET", Endpoint.INCIDENTS, params=params or None
)
response = await self._request("GET", Endpoint.INCIDENTS, params=params or None)
return parse_list(unwrap_list(response, "incidents"), Incident, "incident")

async def get_incident(self, incident_id: str) -> Incident:
Expand Down Expand Up @@ -114,9 +112,7 @@ async def update_incident(
validate_id(incident_id, "incident_id")
payload = update.model_dump(exclude_none=True, by_alias=True)
response = expect_dict(
await self._request(
"PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload
),
await self._request("PUT", f"{Endpoint.INCIDENTS}/{incident_id}", json=payload),
"update_incident",
)
return Incident.model_validate(response)
Expand Down Expand Up @@ -145,9 +141,7 @@ async def add_incident_update(
await self._request("POST", url, json=payload)
return await self.get_incident(incident_id)

async def resolve_incident(
self, incident_id: str, message: str | None = None
) -> Incident:
async def resolve_incident(self, incident_id: str, message: str | None = None) -> Incident:
"""Resolve an incident.

Args:
Expand Down
26 changes: 0 additions & 26 deletions src/hyperping/_async_integrations_mixin.py

This file was deleted.

12 changes: 3 additions & 9 deletions src/hyperping/_async_maintenance_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ async def list_maintenance(self, status: str | None = None) -> list[Maintenance]
if status:
params["status"] = status

response = await self._request(
"GET", Endpoint.MAINTENANCE, params=params or None
)
response = await self._request("GET", Endpoint.MAINTENANCE, params=params or None)

raw = unwrap_list(response, "maintenanceWindows")
if not raw and isinstance(response, dict) and "maintenance" in response:
Expand All @@ -64,9 +62,7 @@ async def get_maintenance(self, maintenance_id: str) -> Maintenance:
HyperpingNotFoundError: If maintenance not found
"""
validate_id(maintenance_id, "maintenance_id")
response = await self._request(
"GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}"
)
response = await self._request("GET", f"{Endpoint.MAINTENANCE}/{maintenance_id}")
return Maintenance.model_validate(expect_dict(response, "get_maintenance"))

async def create_maintenance(self, maintenance: MaintenanceCreate) -> Maintenance:
Expand Down Expand Up @@ -127,9 +123,7 @@ async def update_maintenance(
payload.update(partial)

response = expect_dict(
await self._request(
"PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload
),
await self._request("PUT", f"{Endpoint.MAINTENANCE}/{maintenance_id}", json=payload),
"update_maintenance",
)
return Maintenance.model_validate(response)
Expand Down
13 changes: 0 additions & 13 deletions src/hyperping/_async_monitors_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,3 @@ async def get_monitor_report(
if r.uuid == monitor_id:
return r
raise HyperpingNotFoundError(f"No report found for monitor: {monitor_id}")

async def search_monitors_by_name(self, query: str) -> list[Monitor]:
"""Search monitors by name (case-insensitive substring match)."""
if not query:
return []
try:
result = await self._request(
"GET", f"{Endpoint.MONITORS}/search", params={"query": query}
)
except HyperpingNotFoundError:
return []
items = result if isinstance(result, list) else []
return parse_list(items, Monitor, "monitor")
Loading
Loading