Skip to content

Commit e73d234

Browse files
merge main into stream-alerts-empty test branch; resolve conflicts
Same pyproject/test/uv.lock resolution as the parent async-streaming branch. Note: this branch shares the test/0db470 head with PR #42 (based on the async-streaming branch) and carries the same standalone-failing test_otel.py; the OTel client wiring lands in PR #40.
2 parents ed2ff2d + 86661fa commit e73d234

23 files changed

Lines changed: 1348 additions & 34 deletions

‎pyproject.toml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ classifiers = [
2323
"Topic :: Software Development :: Libraries :: Python Modules",
2424
"Topic :: Internet :: WWW/HTTP",
2525
"Typing :: Typed",
26+
"Environment :: Console",
2627
]
2728
dependencies = [
2829
"httpx2>=2.4,<3.0",
2930
"pydantic>=2.0,<3.0",
3031
]
3132

3233
[project.optional-dependencies]
34+
cli = ["typer[all]>=0.12,<1.0"]
3335
otel = [
3436
"opentelemetry-api>=1.20",
3537
]
36-
3738
dev = [
3839
"pytest>=9.0.3",
3940
"pytest-cov",
@@ -42,10 +43,14 @@ dev = [
4243
"mypy>=1.10",
4344
"pydantic",
4445
"pip-audit>=2.7",
46+
"typer[all]>=0.12,<1.0",
4547
"opentelemetry-api>=1.20",
4648
"opentelemetry-sdk>=1.20",
4749
]
4850

51+
[project.scripts]
52+
hyp = "hyperping.cli._app:app"
53+
4954
[project.urls]
5055
Homepage = "https://github.com/develeap/hyperping-python"
5156
Documentation = "https://github.com/develeap/hyperping-python#readme"

‎src/hyperping/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
AlertType,
4747
DnsRecordType,
4848
EscalationPolicy,
49+
EscalationStep,
4950
Healthcheck,
5051
HealthcheckCreate,
5152
HealthcheckUpdate,
@@ -173,6 +174,7 @@
173174
"ProbeLogResponse",
174175
# On-call
175176
"OnCallSchedule",
177+
"EscalationStep",
176178
"EscalationPolicy",
177179
"TeamMember",
178180
# Integrations

‎src/hyperping/_async_mcp_client.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from hyperping._async_mcp_transport import AsyncMcpTransport
2222
from hyperping.endpoints import MCP_URL
2323
from hyperping.models._integration_models import Integration
24-
from hyperping.models._monitor_models import Monitor
24+
from hyperping.models._monitor_models import Monitor, MonitorCreate
2525
from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse
2626
from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember
2727
from hyperping.models._outage_models import OutageTimeline
@@ -270,3 +270,52 @@ async def search_monitors_by_name(self, query: str) -> list[Monitor]:
270270
data = await self._call("search_monitors_by_name", {"query": query})
271271
raw = data if isinstance(data, list) else []
272272
return [Monitor.model_validate(m) for m in raw]
273+
274+
async def create_monitor(self, monitor: MonitorCreate) -> Monitor:
275+
"""Create a new monitor.
276+
277+
Args:
278+
monitor: Monitor configuration.
279+
"""
280+
return Monitor.model_validate(
281+
await self._call("create_monitor", monitor.model_dump(exclude_none=True))
282+
)
283+
284+
async def update_monitor(self, monitor_uuid: str, **kwargs: Any) -> Monitor:
285+
"""Update an existing monitor.
286+
287+
Args:
288+
monitor_uuid: Monitor UUID.
289+
**kwargs: Fields to update.
290+
"""
291+
return Monitor.model_validate(
292+
await self._call("update_monitor", {"uuid": monitor_uuid, **kwargs})
293+
)
294+
295+
async def pause_monitor(self, monitor_uuid: str) -> Monitor:
296+
"""Pause a monitor.
297+
298+
Args:
299+
monitor_uuid: Monitor UUID.
300+
"""
301+
return Monitor.model_validate(
302+
await self._call("pause_monitor", {"uuid": monitor_uuid})
303+
)
304+
305+
async def resume_monitor(self, monitor_uuid: str) -> Monitor:
306+
"""Resume a paused monitor.
307+
308+
Args:
309+
monitor_uuid: Monitor UUID.
310+
"""
311+
return Monitor.model_validate(
312+
await self._call("resume_monitor", {"uuid": monitor_uuid})
313+
)
314+
315+
async def delete_monitor(self, monitor_uuid: str) -> None:
316+
"""Delete a monitor.
317+
318+
Args:
319+
monitor_uuid: Monitor UUID.
320+
"""
321+
await self._call("delete_monitor", {"uuid": monitor_uuid})

‎src/hyperping/cli/__init__.py‎

Whitespace-only changes.

‎src/hyperping/cli/_app.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Main hyp CLI application."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated
6+
7+
import typer
8+
9+
from hyperping._version import __version__
10+
from hyperping.cli._incidents import incident_app
11+
from hyperping.cli._monitors import monitor_app
12+
from hyperping.cli._statuspages import statuspage_app
13+
from hyperping.cli._tenant import tenant_app
14+
15+
app = typer.Typer(
16+
name="hyp",
17+
help="Hyperping CLI: manage monitors, incidents, and status pages.",
18+
no_args_is_help=True,
19+
)
20+
21+
app.add_typer(monitor_app, name="monitor")
22+
app.add_typer(incident_app, name="incident")
23+
app.add_typer(statuspage_app, name="statuspage")
24+
app.add_typer(tenant_app, name="tenant")
25+
26+
27+
def _version_callback(value: bool) -> None:
28+
if value:
29+
typer.echo(f"hyp {__version__}")
30+
raise typer.Exit()
31+
32+
33+
@app.callback()
34+
def main(
35+
ctx: typer.Context,
36+
api_key: Annotated[
37+
str | None,
38+
typer.Option("--api-key", envvar="HYPERPING_API_KEY", help="Hyperping API key."),
39+
] = None,
40+
json_output: Annotated[
41+
bool,
42+
typer.Option("--json", help="Output as JSON."),
43+
] = False,
44+
version: Annotated[
45+
bool | None,
46+
typer.Option("--version", callback=_version_callback, is_eager=True, help="Show version."),
47+
] = None,
48+
) -> None:
49+
"""Hyperping CLI."""
50+
ctx.ensure_object(dict)
51+
ctx.obj["api_key"] = api_key
52+
ctx.obj["json"] = json_output

‎src/hyperping/cli/_config.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""CLI client factory and API key resolution."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
7+
import typer
8+
9+
from hyperping.client import HyperpingClient
10+
11+
12+
def get_client(api_key: str | None) -> HyperpingClient:
13+
"""Resolve API key and return a configured client.
14+
15+
Resolution order: explicit flag value, then HYPERPING_API_KEY env var.
16+
Raises typer.BadParameter if no key is available.
17+
"""
18+
key = api_key or os.environ.get("HYPERPING_API_KEY")
19+
if not key:
20+
raise typer.BadParameter(
21+
"API key required. Pass --api-key or set HYPERPING_API_KEY.",
22+
param_hint="--api-key",
23+
)
24+
return HyperpingClient(api_key=key)

‎src/hyperping/cli/_incidents.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""hyp incident subcommands."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated
6+
7+
import typer
8+
9+
from hyperping.cli._config import get_client
10+
from hyperping.cli._output import print_detail, print_error, print_success, print_table
11+
from hyperping.exceptions import HyperpingAPIError
12+
from hyperping.models import Incident, IncidentCreate, IncidentType, LocalizedText
13+
14+
incident_app = typer.Typer(name="incident", help="Manage incidents.")
15+
16+
17+
def _incident_row(i: Incident) -> list[object]:
18+
return [i.uuid, i.title_en, i.type, str(i.is_resolved), i.date or ""]
19+
20+
21+
@incident_app.command("list")
22+
def incident_list(
23+
ctx: typer.Context,
24+
status: Annotated[str | None, typer.Option("--status", help="Filter by status")] = None,
25+
) -> None:
26+
"""List incidents."""
27+
api_key: str | None = ctx.obj.get("api_key")
28+
json_mode: bool = ctx.obj.get("json", False)
29+
client = get_client(api_key)
30+
incidents = client.list_incidents(status=status)
31+
columns = ["uuid", "title", "type", "resolved", "date"]
32+
rows = [_incident_row(i) for i in incidents]
33+
print_table("Incidents", columns, rows, json_mode)
34+
35+
36+
@incident_app.command("create")
37+
def incident_create(
38+
ctx: typer.Context,
39+
title: Annotated[str, typer.Option("--title", help="Incident title (English)")],
40+
text: Annotated[str, typer.Option("--text", help="Incident message (English)")],
41+
statuspage: Annotated[str, typer.Option("--statuspage", help="Status page UUID")],
42+
incident_type: Annotated[
43+
str | None, typer.Option("--type", help="Incident type: incident or outage")
44+
] = None,
45+
) -> None:
46+
"""Create a new incident."""
47+
api_key: str | None = ctx.obj.get("api_key")
48+
json_mode: bool = ctx.obj.get("json", False)
49+
client = get_client(api_key)
50+
payload = IncidentCreate(
51+
title=LocalizedText.from_string(title),
52+
text=LocalizedText.from_string(text),
53+
type=IncidentType(incident_type) if incident_type else IncidentType.INCIDENT,
54+
statuspages=[statuspage],
55+
)
56+
try:
57+
incident = client.create_incident(payload)
58+
except HyperpingAPIError as exc:
59+
print_error(str(exc))
60+
raise typer.Exit(code=1) from exc
61+
fields = {
62+
"uuid": incident.uuid,
63+
"title": incident.title_en,
64+
"type": incident.type,
65+
"resolved": incident.is_resolved,
66+
"date": incident.date or "",
67+
}
68+
print_detail("Incident", fields, json_mode)
69+
70+
71+
@incident_app.command("resolve")
72+
def incident_resolve(
73+
ctx: typer.Context,
74+
incident_id: Annotated[str, typer.Argument(help="Incident UUID")],
75+
message: Annotated[
76+
str | None, typer.Option("--message", help="Resolution message")
77+
] = None,
78+
) -> None:
79+
"""Resolve an incident."""
80+
api_key: str | None = ctx.obj.get("api_key")
81+
client = get_client(api_key)
82+
try:
83+
incident = client.resolve_incident(incident_id, message=message)
84+
except HyperpingAPIError as exc:
85+
print_error(str(exc))
86+
raise typer.Exit(code=1) from exc
87+
print_success(f"Incident {incident.uuid} resolved.")

‎src/hyperping/cli/_monitors.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""hyp monitor subcommands."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated
6+
7+
import typer
8+
9+
from hyperping.cli._config import get_client
10+
from hyperping.cli._output import print_detail, print_error, print_success, print_table
11+
from hyperping.exceptions import HyperpingAPIError
12+
from hyperping.models import Monitor
13+
14+
monitor_app = typer.Typer(name="monitor", help="Manage monitors.")
15+
16+
17+
def _monitor_status(m: Monitor) -> str:
18+
if m.paused:
19+
return "paused"
20+
return "down" if m.down else "up"
21+
22+
23+
@monitor_app.command("list")
24+
def monitor_list(ctx: typer.Context) -> None:
25+
"""List all monitors."""
26+
api_key: str | None = ctx.obj.get("api_key")
27+
json_mode: bool = ctx.obj.get("json", False)
28+
client = get_client(api_key)
29+
monitors = client.list_monitors()
30+
columns = ["uuid", "name", "url", "protocol", "status"]
31+
rows = [[m.uuid, m.name, m.url, m.protocol, _monitor_status(m)] for m in monitors]
32+
print_table("Monitors", columns, rows, json_mode)
33+
34+
35+
@monitor_app.command("get")
36+
def monitor_get(
37+
ctx: typer.Context,
38+
monitor_id: Annotated[str, typer.Argument(help="Monitor UUID")],
39+
) -> None:
40+
"""Show a single monitor."""
41+
api_key: str | None = ctx.obj.get("api_key")
42+
json_mode: bool = ctx.obj.get("json", False)
43+
client = get_client(api_key)
44+
try:
45+
m = client.get_monitor(monitor_id)
46+
except HyperpingAPIError as exc:
47+
print_error(str(exc))
48+
raise typer.Exit(code=1) from exc
49+
fields = {
50+
"uuid": m.uuid,
51+
"name": m.name,
52+
"url": m.url,
53+
"protocol": m.protocol,
54+
"status": _monitor_status(m),
55+
"check_frequency": m.check_frequency,
56+
"regions": ", ".join(m.regions),
57+
}
58+
print_detail("Monitor", fields, json_mode)
59+
60+
61+
@monitor_app.command("pause")
62+
def monitor_pause(
63+
ctx: typer.Context,
64+
monitor_id: Annotated[str, typer.Argument(help="Monitor UUID")],
65+
) -> None:
66+
"""Pause a monitor."""
67+
api_key: str | None = ctx.obj.get("api_key")
68+
client = get_client(api_key)
69+
try:
70+
m = client.pause_monitor(monitor_id)
71+
except HyperpingAPIError as exc:
72+
print_error(str(exc))
73+
raise typer.Exit(code=1) from exc
74+
print_success(f"Monitor {m.uuid} paused (status: {_monitor_status(m)})")
75+
76+
77+
@monitor_app.command("resume")
78+
def monitor_resume(
79+
ctx: typer.Context,
80+
monitor_id: Annotated[str, typer.Argument(help="Monitor UUID")],
81+
) -> None:
82+
"""Resume a paused monitor."""
83+
api_key: str | None = ctx.obj.get("api_key")
84+
client = get_client(api_key)
85+
try:
86+
m = client.resume_monitor(monitor_id)
87+
except HyperpingAPIError as exc:
88+
print_error(str(exc))
89+
raise typer.Exit(code=1) from exc
90+
print_success(f"Monitor {m.uuid} resumed (status: {_monitor_status(m)})")

0 commit comments

Comments
 (0)