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
8 changes: 8 additions & 0 deletions src/devhelm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from devhelm.resources.environments import Environments
from devhelm.resources.forensics import Forensics
from devhelm.resources.incidents import Incidents
from devhelm.resources.maintenance_windows import MaintenanceWindows
from devhelm.resources.monitors import Monitors
from devhelm.resources.notification_policies import NotificationPolicies
from devhelm.resources.resource_groups import ResourceGroups
Expand Down Expand Up @@ -51,6 +52,7 @@
CreateAlertChannelRequest,
CreateApiKeyRequest,
CreateEnvironmentRequest,
CreateMaintenanceWindowRequest,
CreateManualIncidentRequest,
CreateMonitorRequest,
CreateNotificationPolicyRequest,
Expand All @@ -77,6 +79,7 @@
IncidentTimelineDto,
IncidentUpdateCreatedBy,
LinkedIncidentStatus,
MaintenanceWindowDto,
MembershipStatus,
MemberStatus,
MonitorAssertionSeverity,
Expand Down Expand Up @@ -122,6 +125,7 @@
UpdateAlertChannelRequest,
UpdateAssertionSeverity,
UpdateEnvironmentRequest,
UpdateMaintenanceWindowRequest,
UpdateMonitorRequest,
UpdateNotificationPolicyRequest,
UpdateResourceGroupRequest,
Expand Down Expand Up @@ -178,6 +182,7 @@
"ApiKeys",
"Dependencies",
"DeployLock",
"MaintenanceWindows",
"Status",
"StatusPages",
# Response DTOs
Expand All @@ -190,6 +195,7 @@
"StatusPageSubscriberDto",
"StatusPageCustomDomainDto",
"StatusPageBranding",
"MaintenanceWindowDto",
"MonitorDto",
"IncidentDto",
"IncidentDetailDto",
Expand Down Expand Up @@ -231,6 +237,8 @@
"AdminAddSubscriberRequest",
"ReorderComponentsRequest",
"ReorderPageLayoutRequest",
"CreateMaintenanceWindowRequest",
"UpdateMaintenanceWindowRequest",
"CreateMonitorRequest",
"UpdateMonitorRequest",
"CreateManualIncidentRequest",
Expand Down
3 changes: 3 additions & 0 deletions src/devhelm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from devhelm.resources.environments import Environments
from devhelm.resources.forensics import Forensics
from devhelm.resources.incidents import Incidents
from devhelm.resources.maintenance_windows import MaintenanceWindows
from devhelm.resources.monitors import Monitors
from devhelm.resources.notification_policies import NotificationPolicies
from devhelm.resources.resource_groups import ResourceGroups
Expand Down Expand Up @@ -51,6 +52,7 @@ class Devhelm:
api_keys: ApiKeys
dependencies: Dependencies
deploy_lock: DeployLock
maintenance_windows: MaintenanceWindows
status: Status
status_pages: StatusPages

Expand Down Expand Up @@ -96,5 +98,6 @@ def __init__(
self.api_keys = ApiKeys(client)
self.dependencies = Dependencies(client)
self.deploy_lock = DeployLock(client)
self.maintenance_windows = MaintenanceWindows(client)
self.status = Status(client)
self.status_pages = StatusPages(client)
187 changes: 187 additions & 0 deletions src/devhelm/resources/maintenance_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
from __future__ import annotations

import httpx

from devhelm._generated import (
CreateMaintenanceWindowRequest,
MaintenanceWindowDto,
UpdateMaintenanceWindowRequest,
)
from devhelm._http import api_delete, api_get, api_post, api_put, path_param
from devhelm._pagination import Page, fetch_all_pages, fetch_page
from devhelm._validation import RequestBody, parse_single, validate_request

# Query-param values for ``GET /api/v1/maintenance-windows``. Both
# documented filters (``monitorId``, ``filter``) are single-valued strings;
# spelt as a concrete alias rather than ``Any`` so the resource layer stays
# ``Any``-free per ``tests/test_typing.py``.
_ListFilterValue = str


def _build_list_filters(
monitor_id: str | None, status: str | None
) -> dict[str, _ListFilterValue]:
"""Pack the documented ``GET /api/v1/maintenance-windows`` query
params into a single dict, dropping anything left at the default
``None`` so the wire request stays minimal and the API's defaults
apply.

Accepts snake_case at the Python boundary and emits the camelCase
spelling the API expects (``monitor_id`` → ``monitorId``). The
ergonomic ``status`` kwarg is mapped to the API's ``filter`` query
param (which selects ``"active"`` or ``"upcoming"`` windows).
"""
filters: dict[str, _ListFilterValue] = {}
if monitor_id is not None:
filters["monitorId"] = monitor_id
if status is not None:
filters["filter"] = status
return filters


class MaintenanceWindows:
"""Scheduled maintenance windows that suppress alerts during planned downtime."""

def __init__(self, client: httpx.Client) -> None:
self._client = client

def list(
self, *, monitor_id: str | None = None, status: str | None = None
) -> list[MaintenanceWindowDto]:
"""List all maintenance windows for the authenticated org (auto-paginates).

Optional server-side filters mirror the documented
``GET /api/v1/maintenance-windows`` query params:

* ``monitor_id`` — only return windows attached to this monitor.
* ``status`` — ``"active"`` (currently in window) or
``"upcoming"`` (starts in the future).

Examples:
>>> client.maintenance_windows.list()
[MaintenanceWindowDto(...), ...]
>>> client.maintenance_windows.list(status="upcoming")
[...]
"""
# The API's documented query string omits ``page``/``size``, but the
# response envelope is the standard ``TableValueResult`` shape that
# all paginated lists in this codebase use, so the server accepts
# those keys and ignores them where unused. Reusing
# ``fetch_all_pages`` keeps the iterator semantics consistent with
# every other ``MaintenanceWindows.list``-style method on this SDK.
return fetch_all_pages(
self._client,
"/api/v1/maintenance-windows",
MaintenanceWindowDto,
extra_params=_build_list_filters(monitor_id, status),
)

def list_page(
self,
page: int,
size: int,
*,
monitor_id: str | None = None,
status: str | None = None,
) -> Page[MaintenanceWindowDto]:
"""List maintenance windows with manual page control.

Accepts the same filter kwargs as :meth:`list` so callers using
manual pagination get the same server-side filtering.
"""
return fetch_page(
self._client,
"/api/v1/maintenance-windows",
MaintenanceWindowDto,
page,
size,
extra_params=_build_list_filters(monitor_id, status),
)

def get(self, id: str) -> MaintenanceWindowDto:
"""Get a single maintenance window by ID.

Examples:
>>> client.maintenance_windows.get("a8e3...")
MaintenanceWindowDto(...)
"""
return parse_single(
MaintenanceWindowDto,
api_get(self._client, f"/api/v1/maintenance-windows/{path_param(id)}"),
f"GET /api/v1/maintenance-windows/{id}",
)

def create(
self, body: RequestBody[CreateMaintenanceWindowRequest]
) -> MaintenanceWindowDto:
"""Create a new maintenance window.

Pass ``monitorId=None`` to create an org-wide window that
suppresses alerts for every monitor in the organisation; pass a
UUID to scope the window to a single monitor.

Examples:
>>> from datetime import datetime, timedelta, timezone
>>> start = datetime.now(timezone.utc) + timedelta(hours=1)
>>> client.maintenance_windows.create({
... "startsAt": start.isoformat(),
... "endsAt": (start + timedelta(hours=2)).isoformat(),
... "reason": "Quarterly DB upgrade",
... "monitorId": "a8e3...",
... })
MaintenanceWindowDto(...)
"""
body = validate_request(
CreateMaintenanceWindowRequest, body, "maintenanceWindows.create"
)
return parse_single(
MaintenanceWindowDto,
api_post(self._client, "/api/v1/maintenance-windows", body),
"POST /api/v1/maintenance-windows",
)

def update(
self, id: str, body: RequestBody[UpdateMaintenanceWindowRequest]
) -> MaintenanceWindowDto:
"""Update an existing maintenance window.

Examples:
>>> client.maintenance_windows.update("a8e3...", {
... "startsAt": "2026-06-01T00:00:00Z",
... "endsAt": "2026-06-01T02:00:00Z",
... "reason": "Rescheduled DB upgrade",
... })
MaintenanceWindowDto(...)
"""
body = validate_request(
UpdateMaintenanceWindowRequest, body, "maintenanceWindows.update"
)
return parse_single(
MaintenanceWindowDto,
api_put(
self._client, f"/api/v1/maintenance-windows/{path_param(id)}", body
),
f"PUT /api/v1/maintenance-windows/{id}",
)

def delete(self, id: str) -> None:
"""Delete (cancel) a maintenance window.

The API exposes a ``DELETE`` operation for this resource; if you
prefer the cancellation-style verb in your code, see
:meth:`cancel`, which is a thin alias.
"""
api_delete(self._client, f"/api/v1/maintenance-windows/{path_param(id)}")

def cancel(self, id: str) -> None:
"""Cancel a scheduled maintenance window.

Semantic alias for :meth:`delete` — both call the same underlying
``DELETE /api/v1/maintenance-windows/{id}`` endpoint, but
``cancel`` reads better in automation scripts that schedule and
later cancel planned downtime.

Examples:
>>> client.maintenance_windows.cancel("a8e3...")
"""
self.delete(id)
6 changes: 6 additions & 0 deletions src/devhelm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
CreateAlertChannelRequest,
CreateApiKeyRequest,
CreateEnvironmentRequest,
CreateMaintenanceWindowRequest,
CreateManualIncidentRequest,
CreateMonitorRequest,
CreateNotificationPolicyRequest,
Expand All @@ -74,6 +75,7 @@
IncidentMode,
IncidentStateTransitionDto,
IncidentTimelineDto,
MaintenanceWindowDto,
ManagedBy,
Method,
MonitorDto,
Expand Down Expand Up @@ -112,6 +114,7 @@
TierAvailability,
UpdateAlertChannelRequest,
UpdateEnvironmentRequest,
UpdateMaintenanceWindowRequest,
UpdateMonitorRequest,
UpdateNotificationPolicyRequest,
UpdateResourceGroupRequest,
Expand Down Expand Up @@ -244,6 +247,7 @@
"CreateStatusPageComponentRequest",
"CreateStatusPageIncidentRequest",
"CreateStatusPageIncidentUpdateRequest",
"CreateMaintenanceWindowRequest",
"CreateStatusPageRequest",
"CreateTagRequest",
"CreateWebhookEndpointRequest",
Expand All @@ -254,6 +258,7 @@
"IncidentDto",
"IncidentStateTransitionDto",
"IncidentTimelineDto",
"MaintenanceWindowDto",
"MonitorDto",
"MonitorVersionDto",
"NotificationPolicyDto",
Expand All @@ -280,6 +285,7 @@
"TestChannelResult",
"UpdateAlertChannelRequest",
"UpdateEnvironmentRequest",
"UpdateMaintenanceWindowRequest",
"UpdateMonitorRequest",
"UpdateNotificationPolicyRequest",
"UpdateResourceGroupRequest",
Expand Down
11 changes: 11 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from devhelm.resources.environments import Environments
from devhelm.resources.forensics import Forensics
from devhelm.resources.incidents import Incidents
from devhelm.resources.maintenance_windows import MaintenanceWindows
from devhelm.resources.monitors import Monitors
from devhelm.resources.notification_policies import NotificationPolicies
from devhelm.resources.resource_groups import ResourceGroups
Expand Down Expand Up @@ -83,6 +84,16 @@ def test_status(self, client: Devhelm) -> None:
def test_status_pages(self, client: Devhelm) -> None:
assert isinstance(client.status_pages, StatusPages)

def test_maintenance_windows(self, client: Devhelm) -> None:
assert isinstance(client.maintenance_windows, MaintenanceWindows)
assert callable(client.maintenance_windows.list)
assert callable(client.maintenance_windows.list_page)
assert callable(client.maintenance_windows.get)
assert callable(client.maintenance_windows.create)
assert callable(client.maintenance_windows.update)
assert callable(client.maintenance_windows.delete)
assert callable(client.maintenance_windows.cancel)


class TestStatusPagesResource:
"""Verify StatusPages exposes all sub-resources and methods."""
Expand Down
Loading
Loading