devhelm 1.2.0__tar.gz → 1.3.0__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.
- {devhelm-1.2.0 → devhelm-1.3.0}/PKG-INFO +2 -1
- {devhelm-1.2.0 → devhelm-1.3.0}/README.md +1 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/docs/openapi/monitoring-api.json +9 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/pyproject.toml +1 -1
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/__init__.py +34 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_pagination.py +11 -2
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/client.py +3 -0
- devhelm-1.3.0/src/devhelm/resources/dependencies.py +100 -0
- devhelm-1.3.0/src/devhelm/resources/services.py +322 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/types.py +32 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_client.py +19 -0
- devhelm-1.3.0/tests/test_dependencies.py +142 -0
- devhelm-1.3.0/tests/test_services.py +281 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_spec_parity.py +4 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/uv.lock +1 -1
- devhelm-1.2.0/src/devhelm/resources/dependencies.py +0 -51
- {devhelm-1.2.0 → devhelm-1.3.0}/.github/workflows/ci.yml +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/.github/workflows/release.yml +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/.github/workflows/spec-check.yml +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/.gitignore +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/LICENSE +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/Makefile +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/scripts/emit_response_enums.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/scripts/inject_strict_config.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/scripts/regen-from.sh +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/scripts/release.sh +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/scripts/typegen.sh +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_enums.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_errors.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_generated.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_http.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/_validation.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/py.typed +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/__init__.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/alert_channels.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/api_keys.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/deploy_lock.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/environments.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/forensics.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/incidents.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/maintenance_windows.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/monitors.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/notification_policies.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/resource_groups.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/secrets.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/status.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/status_pages.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/tags.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/src/devhelm/resources/webhooks.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/__init__.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/run_sdk.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_errors.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_http.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_maintenance_windows.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_negative_validation.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_schemas.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_typing.py +0 -0
- {devhelm-1.2.0 → devhelm-1.3.0}/tests/test_validation_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devhelm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: DevHelm SDK for Python — typed client for monitors, incidents, alerting, and more
|
|
5
5
|
Project-URL: Homepage, https://github.com/devhelmhq/sdk-python
|
|
6
6
|
Project-URL: Repository, https://github.com/devhelmhq/sdk-python.git
|
|
@@ -108,6 +108,7 @@ The client exposes the following resource modules:
|
|
|
108
108
|
| `client.api_keys` | API key management |
|
|
109
109
|
| `client.dependencies` | Service dependency tracking |
|
|
110
110
|
| `client.deploy_lock` | Deploy lock for safe deployments |
|
|
111
|
+
| `client.services` | Status Data catalog: vendor services, components, incidents, uptime |
|
|
111
112
|
| `client.status` | Dashboard overview |
|
|
112
113
|
|
|
113
114
|
## Pagination
|
|
@@ -84,6 +84,7 @@ The client exposes the following resource modules:
|
|
|
84
84
|
| `client.api_keys` | API key management |
|
|
85
85
|
| `client.dependencies` | Service dependency tracking |
|
|
86
86
|
| `client.deploy_lock` | Deploy lock for safe deployments |
|
|
87
|
+
| `client.services` | Status Data catalog: vendor services, components, incidents, uptime |
|
|
87
88
|
| `client.status` | Dashboard overview |
|
|
88
89
|
|
|
89
90
|
## Pagination
|
|
@@ -13520,6 +13520,15 @@
|
|
|
13520
13520
|
"type": "boolean"
|
|
13521
13521
|
}
|
|
13522
13522
|
},
|
|
13523
|
+
{
|
|
13524
|
+
"name": "search",
|
|
13525
|
+
"in": "query",
|
|
13526
|
+
"description": "Case-insensitive substring match on service name or slug",
|
|
13527
|
+
"required": false,
|
|
13528
|
+
"schema": {
|
|
13529
|
+
"type": "string"
|
|
13530
|
+
}
|
|
13531
|
+
},
|
|
13523
13532
|
{
|
|
13524
13533
|
"name": "cursor",
|
|
13525
13534
|
"in": "query",
|
|
@@ -29,6 +29,7 @@ from devhelm.resources.monitors import Monitors
|
|
|
29
29
|
from devhelm.resources.notification_policies import NotificationPolicies
|
|
30
30
|
from devhelm.resources.resource_groups import ResourceGroups
|
|
31
31
|
from devhelm.resources.secrets import Secrets
|
|
32
|
+
from devhelm.resources.services import Services
|
|
32
33
|
from devhelm.resources.status import Status
|
|
33
34
|
from devhelm.resources.status_pages import StatusPages
|
|
34
35
|
from devhelm.resources.tags import Tags
|
|
@@ -46,8 +47,11 @@ from devhelm.types import (
|
|
|
46
47
|
ApiKeyDto,
|
|
47
48
|
AssertionSeverity,
|
|
48
49
|
AssertionTestResultDto,
|
|
50
|
+
BatchComponentUptimeDto,
|
|
51
|
+
CategoryDto,
|
|
49
52
|
CheckResultDto,
|
|
50
53
|
CheckTraceDto,
|
|
54
|
+
ComponentUptimeDayDto,
|
|
51
55
|
ConfirmationPolicyType,
|
|
52
56
|
CreateAlertChannelRequest,
|
|
53
57
|
CreateApiKeyRequest,
|
|
@@ -69,6 +73,7 @@ from devhelm.types import (
|
|
|
69
73
|
DashboardOverviewDto,
|
|
70
74
|
DeployLockDto,
|
|
71
75
|
EnvironmentDto,
|
|
76
|
+
GlobalStatusSummaryDto,
|
|
72
77
|
IncidentDetailDto,
|
|
73
78
|
IncidentDto,
|
|
74
79
|
IncidentNewStatus,
|
|
@@ -78,6 +83,7 @@ from devhelm.types import (
|
|
|
78
83
|
IncidentStatus,
|
|
79
84
|
IncidentTimelineDto,
|
|
80
85
|
IncidentUpdateCreatedBy,
|
|
86
|
+
LifecycleStatus,
|
|
81
87
|
LinkedIncidentStatus,
|
|
82
88
|
MaintenanceWindowDto,
|
|
83
89
|
MembershipStatus,
|
|
@@ -99,8 +105,18 @@ from devhelm.types import (
|
|
|
99
105
|
ResourceGroupHealthStatus,
|
|
100
106
|
ResourceGroupMemberDto,
|
|
101
107
|
RuleEvaluationDto,
|
|
108
|
+
ScheduledMaintenanceDto,
|
|
102
109
|
SecretDto,
|
|
110
|
+
ServiceCatalogDto,
|
|
111
|
+
ServiceComponentDto,
|
|
112
|
+
ServiceDayDetailDto,
|
|
113
|
+
ServiceDetailDto,
|
|
114
|
+
ServiceIncidentDetailDto,
|
|
115
|
+
ServiceIncidentDto,
|
|
116
|
+
ServiceLiveStatusDto,
|
|
117
|
+
ServiceSubscribeRequest,
|
|
103
118
|
ServiceSubscriptionDto,
|
|
119
|
+
ServiceUptimeResponse,
|
|
104
120
|
StatusPageBranding,
|
|
105
121
|
StatusPageComponentCurrentStatus,
|
|
106
122
|
StatusPageComponentDto,
|
|
@@ -124,6 +140,7 @@ from devhelm.types import (
|
|
|
124
140
|
TriggerRuleSeverity,
|
|
125
141
|
TriggerRuleType,
|
|
126
142
|
UpdateAlertChannelRequest,
|
|
143
|
+
UpdateAlertSensitivityRequest,
|
|
127
144
|
UpdateAssertionSeverity,
|
|
128
145
|
UpdateEnvironmentRequest,
|
|
129
146
|
UpdateMaintenanceWindowRequest,
|
|
@@ -184,6 +201,7 @@ __all__ = [
|
|
|
184
201
|
"Dependencies",
|
|
185
202
|
"DeployLock",
|
|
186
203
|
"MaintenanceWindows",
|
|
204
|
+
"Services",
|
|
187
205
|
"Status",
|
|
188
206
|
"StatusPages",
|
|
189
207
|
# Response DTOs
|
|
@@ -216,6 +234,19 @@ __all__ = [
|
|
|
216
234
|
"ApiKeyDto",
|
|
217
235
|
"ApiKeyCreateResponse",
|
|
218
236
|
"ServiceSubscriptionDto",
|
|
237
|
+
"ServiceCatalogDto",
|
|
238
|
+
"ServiceDetailDto",
|
|
239
|
+
"ServiceLiveStatusDto",
|
|
240
|
+
"ServiceComponentDto",
|
|
241
|
+
"ServiceIncidentDto",
|
|
242
|
+
"ServiceIncidentDetailDto",
|
|
243
|
+
"ServiceUptimeResponse",
|
|
244
|
+
"ServiceDayDetailDto",
|
|
245
|
+
"ScheduledMaintenanceDto",
|
|
246
|
+
"CategoryDto",
|
|
247
|
+
"GlobalStatusSummaryDto",
|
|
248
|
+
"BatchComponentUptimeDto",
|
|
249
|
+
"ComponentUptimeDayDto",
|
|
219
250
|
"MonitorVersionDto",
|
|
220
251
|
"CheckResultDto",
|
|
221
252
|
"DashboardOverviewDto",
|
|
@@ -261,6 +292,8 @@ __all__ = [
|
|
|
261
292
|
"UpdateWebhookEndpointRequest",
|
|
262
293
|
"CreateApiKeyRequest",
|
|
263
294
|
"AcquireDeployLockRequest",
|
|
295
|
+
"ServiceSubscribeRequest",
|
|
296
|
+
"UpdateAlertSensitivityRequest",
|
|
264
297
|
# Enum aliases (descriptive names for codegen-numbered enums)
|
|
265
298
|
"AffectedComponentStatus",
|
|
266
299
|
"AlertDeliveryStatus",
|
|
@@ -272,6 +305,7 @@ __all__ = [
|
|
|
272
305
|
"IncidentSeverity",
|
|
273
306
|
"IncidentStatus",
|
|
274
307
|
"IncidentUpdateCreatedBy",
|
|
308
|
+
"LifecycleStatus",
|
|
275
309
|
"LinkedIncidentStatus",
|
|
276
310
|
"MemberStatus",
|
|
277
311
|
"MembershipStatus",
|
|
@@ -151,9 +151,18 @@ def fetch_cursor_page(
|
|
|
151
151
|
model_class: type[M],
|
|
152
152
|
cursor: str | None = None,
|
|
153
153
|
limit: int | None = None,
|
|
154
|
+
*,
|
|
155
|
+
extra_params: dict[str, Any] | None = None,
|
|
154
156
|
) -> CursorPage[M]:
|
|
155
|
-
"""Fetch a single page from a cursor-paginated endpoint with validation.
|
|
156
|
-
|
|
157
|
+
"""Fetch a single page from a cursor-paginated endpoint with validation.
|
|
158
|
+
|
|
159
|
+
``extra_params`` is merged into the request so callers can forward
|
|
160
|
+
server-side filter kwargs (``category``, ``search``, …) alongside the
|
|
161
|
+
cursor controls. Pagination keys (``cursor``, ``limit``) always win
|
|
162
|
+
over user-supplied ``extra_params`` to keep the iterator's invariants
|
|
163
|
+
intact.
|
|
164
|
+
"""
|
|
165
|
+
params: dict[str, Any] = dict(extra_params) if extra_params else {}
|
|
157
166
|
if cursor:
|
|
158
167
|
params["cursor"] = cursor
|
|
159
168
|
if limit:
|
|
@@ -15,6 +15,7 @@ from devhelm.resources.monitors import Monitors
|
|
|
15
15
|
from devhelm.resources.notification_policies import NotificationPolicies
|
|
16
16
|
from devhelm.resources.resource_groups import ResourceGroups
|
|
17
17
|
from devhelm.resources.secrets import Secrets
|
|
18
|
+
from devhelm.resources.services import Services
|
|
18
19
|
from devhelm.resources.status import Status
|
|
19
20
|
from devhelm.resources.status_pages import StatusPages
|
|
20
21
|
from devhelm.resources.tags import Tags
|
|
@@ -53,6 +54,7 @@ class Devhelm:
|
|
|
53
54
|
dependencies: Dependencies
|
|
54
55
|
deploy_lock: DeployLock
|
|
55
56
|
maintenance_windows: MaintenanceWindows
|
|
57
|
+
services: Services
|
|
56
58
|
status: Status
|
|
57
59
|
status_pages: StatusPages
|
|
58
60
|
|
|
@@ -99,5 +101,6 @@ class Devhelm:
|
|
|
99
101
|
self.dependencies = Dependencies(client)
|
|
100
102
|
self.deploy_lock = DeployLock(client)
|
|
101
103
|
self.maintenance_windows = MaintenanceWindows(client)
|
|
104
|
+
self.services = Services(client)
|
|
102
105
|
self.status = Status(client)
|
|
103
106
|
self.status_pages = StatusPages(client)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from devhelm._generated import (
|
|
6
|
+
ServiceSubscribeRequest,
|
|
7
|
+
ServiceSubscriptionDto,
|
|
8
|
+
UpdateAlertSensitivityRequest,
|
|
9
|
+
)
|
|
10
|
+
from devhelm._http import api_delete, api_get, api_patch, api_post, path_param
|
|
11
|
+
from devhelm._pagination import Page, fetch_all_pages, fetch_page
|
|
12
|
+
from devhelm._validation import parse_single, validate_request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Dependencies:
|
|
16
|
+
"""Service dependency tracking (service subscriptions)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, client: httpx.Client) -> None:
|
|
19
|
+
self._client = client
|
|
20
|
+
|
|
21
|
+
def list(self) -> list[ServiceSubscriptionDto]:
|
|
22
|
+
"""List all tracked service dependencies."""
|
|
23
|
+
return fetch_all_pages(
|
|
24
|
+
self._client, "/api/v1/service-subscriptions", ServiceSubscriptionDto
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def list_page(self, page: int, size: int) -> Page[ServiceSubscriptionDto]:
|
|
28
|
+
"""List tracked dependencies with manual page control."""
|
|
29
|
+
return fetch_page(
|
|
30
|
+
self._client,
|
|
31
|
+
"/api/v1/service-subscriptions",
|
|
32
|
+
ServiceSubscriptionDto,
|
|
33
|
+
page,
|
|
34
|
+
size,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def get(self, id: int | str) -> ServiceSubscriptionDto:
|
|
38
|
+
"""Get a tracked dependency by ID."""
|
|
39
|
+
return parse_single(
|
|
40
|
+
ServiceSubscriptionDto,
|
|
41
|
+
api_get(self._client, f"/api/v1/service-subscriptions/{path_param(id)}"),
|
|
42
|
+
f"GET /api/v1/service-subscriptions/{id}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def track(
|
|
46
|
+
self,
|
|
47
|
+
slug: str,
|
|
48
|
+
*,
|
|
49
|
+
component_id: str | None = None,
|
|
50
|
+
alert_sensitivity: str | None = None,
|
|
51
|
+
) -> ServiceSubscriptionDto:
|
|
52
|
+
"""Track a new service dependency by slug.
|
|
53
|
+
|
|
54
|
+
``component_id`` subscribes to one component instead of the whole
|
|
55
|
+
service. ``alert_sensitivity`` is one of ``ALL``,
|
|
56
|
+
``INCIDENTS_ONLY``, ``MAJOR_ONLY``, or ``AWARENESS`` (the API
|
|
57
|
+
default — silent tracking with no alert fan-out). The request body
|
|
58
|
+
is omitted entirely when neither kwarg is provided.
|
|
59
|
+
"""
|
|
60
|
+
body: ServiceSubscribeRequest | None = None
|
|
61
|
+
if component_id is not None or alert_sensitivity is not None:
|
|
62
|
+
fields: dict[str, str] = {}
|
|
63
|
+
if component_id is not None:
|
|
64
|
+
fields["componentId"] = component_id
|
|
65
|
+
if alert_sensitivity is not None:
|
|
66
|
+
fields["alertSensitivity"] = alert_sensitivity
|
|
67
|
+
body = validate_request(
|
|
68
|
+
ServiceSubscribeRequest, fields, "dependencies.track"
|
|
69
|
+
)
|
|
70
|
+
return parse_single(
|
|
71
|
+
ServiceSubscriptionDto,
|
|
72
|
+
api_post(
|
|
73
|
+
self._client, f"/api/v1/service-subscriptions/{path_param(slug)}", body
|
|
74
|
+
),
|
|
75
|
+
f"POST /api/v1/service-subscriptions/{slug}",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def update_alert_sensitivity(
|
|
79
|
+
self, subscription_id: int | str, alert_sensitivity: str
|
|
80
|
+
) -> ServiceSubscriptionDto:
|
|
81
|
+
"""Update the alert sensitivity on a tracked dependency.
|
|
82
|
+
|
|
83
|
+
``alert_sensitivity`` is one of ``ALL``, ``INCIDENTS_ONLY``,
|
|
84
|
+
``MAJOR_ONLY``, or ``AWARENESS``.
|
|
85
|
+
"""
|
|
86
|
+
body = validate_request(
|
|
87
|
+
UpdateAlertSensitivityRequest,
|
|
88
|
+
{"alertSensitivity": alert_sensitivity},
|
|
89
|
+
"dependencies.update_alert_sensitivity",
|
|
90
|
+
)
|
|
91
|
+
path = f"/api/v1/service-subscriptions/{path_param(subscription_id)}"
|
|
92
|
+
return parse_single(
|
|
93
|
+
ServiceSubscriptionDto,
|
|
94
|
+
api_patch(self._client, f"{path}/alert-sensitivity", body),
|
|
95
|
+
f"PATCH /api/v1/service-subscriptions/{subscription_id}/alert-sensitivity",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def delete(self, id: int | str) -> None:
|
|
99
|
+
"""Remove a tracked dependency."""
|
|
100
|
+
api_delete(self._client, f"/api/v1/service-subscriptions/{path_param(id)}")
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Status Data catalog: third-party service status, components, incidents,
|
|
2
|
+
uptime, and maintenances.
|
|
3
|
+
|
|
4
|
+
These are the read-only ``/api/v1/services`` + ``/api/v1/categories``
|
|
5
|
+
endpoints backing DevHelm's vendor-status catalog (the data the dependency
|
|
6
|
+
tracker subscribes to). Services are addressed by slug (``"github"``) or
|
|
7
|
+
UUID interchangeably — every ``slug_or_id`` parameter accepts either.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import builtins
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from devhelm._generated import (
|
|
20
|
+
BatchComponentUptimeDto,
|
|
21
|
+
CategoryDto,
|
|
22
|
+
ComponentUptimeDayDto,
|
|
23
|
+
GlobalStatusSummaryDto,
|
|
24
|
+
ScheduledMaintenanceDto,
|
|
25
|
+
ServiceCatalogDto,
|
|
26
|
+
ServiceComponentDto,
|
|
27
|
+
ServiceDayDetailDto,
|
|
28
|
+
ServiceDetailDto,
|
|
29
|
+
ServiceIncidentDetailDto,
|
|
30
|
+
ServiceIncidentDto,
|
|
31
|
+
ServiceLiveStatusDto,
|
|
32
|
+
ServiceUptimeResponse,
|
|
33
|
+
)
|
|
34
|
+
from devhelm._http import api_get, path_param
|
|
35
|
+
from devhelm._pagination import CursorPage, Page, _validate_page, fetch_cursor_page
|
|
36
|
+
from devhelm._validation import parse_list, parse_single
|
|
37
|
+
|
|
38
|
+
M = TypeVar("M", bound=BaseModel)
|
|
39
|
+
|
|
40
|
+
# Explicit primitive-only param dict avoids mypy's ``disallow_any_explicit``
|
|
41
|
+
# in strict mode while still accepting the shapes httpx serialises for us.
|
|
42
|
+
# List values serialise as repeated query keys (``status=a&status=b``).
|
|
43
|
+
_ParamValue = str | int | bool | list[str] | None
|
|
44
|
+
_ParamDict = dict[str, _ParamValue]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_date(value: date | datetime | str) -> str:
|
|
48
|
+
"""Normalise ``from``/``to`` calendar params to the ISO ``yyyy-MM-dd``
|
|
49
|
+
the API expects. ``datetime`` is truncated to its calendar day because
|
|
50
|
+
the endpoints reject full timestamps.
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(value, datetime):
|
|
53
|
+
return value.date().isoformat()
|
|
54
|
+
if isinstance(value, date):
|
|
55
|
+
return value.isoformat()
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _window_params(
|
|
60
|
+
period: str, from_: date | datetime | str | None, to: date | datetime | str | None
|
|
61
|
+
) -> _ParamDict:
|
|
62
|
+
"""Pack the shared uptime-window params. ``period`` is always sent (the
|
|
63
|
+
API documents that an explicit ``from``/``to`` window wins over it when
|
|
64
|
+
both are supplied, so forwarding the default is harmless).
|
|
65
|
+
"""
|
|
66
|
+
params: _ParamDict = {"period": period}
|
|
67
|
+
if from_ is not None:
|
|
68
|
+
params["from"] = _format_date(from_)
|
|
69
|
+
if to is not None:
|
|
70
|
+
params["to"] = _format_date(to)
|
|
71
|
+
return params
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_BASE = "/api/v1/services"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _service_path(slug_or_id: str) -> str:
|
|
78
|
+
return f"{_BASE}/{path_param(slug_or_id)}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Services:
|
|
82
|
+
"""Status Data catalog: vendor services, components, incidents, uptime."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, client: httpx.Client) -> None:
|
|
85
|
+
self._client = client
|
|
86
|
+
|
|
87
|
+
def _fetch_table(
|
|
88
|
+
self, path: str, model_class: type[M], params: _ParamDict | None = None
|
|
89
|
+
) -> list[M]:
|
|
90
|
+
"""Catalog list endpoints return the offset-page envelope
|
|
91
|
+
``{data, hasNext, hasPrev, …}`` but are not actually paginated
|
|
92
|
+
server-side (no ``page``/``size`` params) — unwrap the envelope and
|
|
93
|
+
hand back the validated items directly.
|
|
94
|
+
"""
|
|
95
|
+
resp = api_get(self._client, path, params=params or None)
|
|
96
|
+
envelope = _validate_page(resp)
|
|
97
|
+
return parse_list(model_class, envelope.data, f"GET {path}")
|
|
98
|
+
|
|
99
|
+
def list(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
category: str | None = None,
|
|
103
|
+
status: str | None = None,
|
|
104
|
+
search: str | None = None,
|
|
105
|
+
cursor: str | None = None,
|
|
106
|
+
limit: int = 20,
|
|
107
|
+
) -> CursorPage[ServiceCatalogDto]:
|
|
108
|
+
"""List catalog services (cursor-paginated).
|
|
109
|
+
|
|
110
|
+
Optional server-side filters mirror the documented
|
|
111
|
+
``GET /api/v1/services`` query params: ``category`` (exact
|
|
112
|
+
category name), ``status`` (current overall status, e.g.
|
|
113
|
+
``"operational"``), and ``search`` (free-text match on name/slug).
|
|
114
|
+
Pass ``cursor`` from a previous page's ``next_cursor`` to continue.
|
|
115
|
+
"""
|
|
116
|
+
filters: _ParamDict = {}
|
|
117
|
+
if category is not None:
|
|
118
|
+
filters["category"] = category
|
|
119
|
+
if status is not None:
|
|
120
|
+
filters["status"] = status
|
|
121
|
+
if search is not None:
|
|
122
|
+
filters["search"] = search
|
|
123
|
+
return fetch_cursor_page(
|
|
124
|
+
self._client,
|
|
125
|
+
_BASE,
|
|
126
|
+
ServiceCatalogDto,
|
|
127
|
+
cursor=cursor,
|
|
128
|
+
limit=limit,
|
|
129
|
+
extra_params=filters,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def get(self, slug_or_id: str, *, summary: bool = False) -> ServiceDetailDto:
|
|
133
|
+
"""Get a service's detail view by slug or ID.
|
|
134
|
+
|
|
135
|
+
``summary=True`` requests the trimmed payload (component groups
|
|
136
|
+
without leaf children) used by list-style consumers.
|
|
137
|
+
"""
|
|
138
|
+
params: _ParamDict | None = {"summary": True} if summary else None
|
|
139
|
+
return parse_single(
|
|
140
|
+
ServiceDetailDto,
|
|
141
|
+
api_get(self._client, _service_path(slug_or_id), params=params),
|
|
142
|
+
f"GET {_BASE}/{slug_or_id}",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def live_status(self, slug_or_id: str) -> ServiceLiveStatusDto:
|
|
146
|
+
"""Get a service's current live status (overall + per-component)."""
|
|
147
|
+
return parse_single(
|
|
148
|
+
ServiceLiveStatusDto,
|
|
149
|
+
api_get(self._client, f"{_service_path(slug_or_id)}/live-status"),
|
|
150
|
+
f"GET {_BASE}/{slug_or_id}/live-status",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ``builtins.list`` below because the ``list`` *method* shadows the
|
|
154
|
+
# builtin inside the class body for everything defined after it.
|
|
155
|
+
def categories(self) -> builtins.list[CategoryDto]:
|
|
156
|
+
"""List all service categories with their service counts."""
|
|
157
|
+
return self._fetch_table("/api/v1/categories", CategoryDto)
|
|
158
|
+
|
|
159
|
+
def summary(self) -> GlobalStatusSummaryDto:
|
|
160
|
+
"""Get the global status summary across the whole catalog."""
|
|
161
|
+
return parse_single(
|
|
162
|
+
GlobalStatusSummaryDto,
|
|
163
|
+
api_get(self._client, f"{_BASE}/summary"),
|
|
164
|
+
f"GET {_BASE}/summary",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def components(
|
|
168
|
+
self, slug_or_id: str, *, group_id: str | None = None
|
|
169
|
+
) -> builtins.list[ServiceComponentDto]:
|
|
170
|
+
"""List a service's active components.
|
|
171
|
+
|
|
172
|
+
``group_id`` restricts the result to direct children of that group
|
|
173
|
+
component.
|
|
174
|
+
"""
|
|
175
|
+
params: _ParamDict = {}
|
|
176
|
+
if group_id is not None:
|
|
177
|
+
params["groupId"] = group_id
|
|
178
|
+
return self._fetch_table(
|
|
179
|
+
f"{_service_path(slug_or_id)}/components", ServiceComponentDto, params
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def component_uptime(
|
|
183
|
+
self,
|
|
184
|
+
slug_or_id: str,
|
|
185
|
+
component_id: str,
|
|
186
|
+
*,
|
|
187
|
+
period: str = "30d",
|
|
188
|
+
from_: date | datetime | str | None = None,
|
|
189
|
+
to: date | datetime | str | None = None,
|
|
190
|
+
) -> builtins.list[ComponentUptimeDayDto]:
|
|
191
|
+
"""Get daily uptime data for one component.
|
|
192
|
+
|
|
193
|
+
Pass either a preset ``period`` (``7d``, ``30d``, ``90d``, ``1y``)
|
|
194
|
+
or an explicit ``from_``/``to`` calendar window (ISO ``yyyy-MM-dd``,
|
|
195
|
+
max 730 days; ``to`` defaults to today). The explicit window wins
|
|
196
|
+
when both are supplied.
|
|
197
|
+
"""
|
|
198
|
+
return self._fetch_table(
|
|
199
|
+
f"{_service_path(slug_or_id)}/components/{path_param(component_id)}/uptime",
|
|
200
|
+
ComponentUptimeDayDto,
|
|
201
|
+
_window_params(period, from_, to),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def batch_component_uptime(
|
|
205
|
+
self,
|
|
206
|
+
slug_or_id: str,
|
|
207
|
+
*,
|
|
208
|
+
period: str = "30d",
|
|
209
|
+
from_: date | datetime | str | None = None,
|
|
210
|
+
to: date | datetime | str | None = None,
|
|
211
|
+
) -> BatchComponentUptimeDto:
|
|
212
|
+
"""Get daily uptime for every leaf component in a single request,
|
|
213
|
+
keyed by component ID.
|
|
214
|
+
|
|
215
|
+
Accepts the same window kwargs as :meth:`component_uptime`.
|
|
216
|
+
"""
|
|
217
|
+
return parse_single(
|
|
218
|
+
BatchComponentUptimeDto,
|
|
219
|
+
api_get(
|
|
220
|
+
self._client,
|
|
221
|
+
f"{_service_path(slug_or_id)}/components/uptime",
|
|
222
|
+
params=_window_params(period, from_, to),
|
|
223
|
+
),
|
|
224
|
+
f"GET {_BASE}/{slug_or_id}/components/uptime",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def day(self, slug_or_id: str, date: date | str) -> ServiceDayDetailDto:
|
|
228
|
+
"""Get the per-component rollup for one UTC calendar day
|
|
229
|
+
(ISO ``yyyy-MM-dd``).
|
|
230
|
+
"""
|
|
231
|
+
return parse_single(
|
|
232
|
+
ServiceDayDetailDto,
|
|
233
|
+
api_get(
|
|
234
|
+
self._client,
|
|
235
|
+
f"{_service_path(slug_or_id)}/days/{path_param(_format_date(date))}",
|
|
236
|
+
),
|
|
237
|
+
f"GET {_BASE}/{slug_or_id}/days/{date}",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def incidents(
|
|
241
|
+
self,
|
|
242
|
+
slug_or_id: str | None = None,
|
|
243
|
+
*,
|
|
244
|
+
status: str | None = None,
|
|
245
|
+
from_: date | datetime | str | None = None,
|
|
246
|
+
category: str | None = None,
|
|
247
|
+
page: int = 0,
|
|
248
|
+
size: int = 20,
|
|
249
|
+
) -> Page[ServiceIncidentDto]:
|
|
250
|
+
"""List vendor incidents (paginated).
|
|
251
|
+
|
|
252
|
+
With ``slug_or_id``, lists incidents for that one service; without
|
|
253
|
+
it, lists incidents across the whole catalog. ``status`` filters by
|
|
254
|
+
incident status (e.g. ``"resolved"``), ``from_`` bounds the window
|
|
255
|
+
start, and ``category`` (cross-service mode only) restricts to one
|
|
256
|
+
service category.
|
|
257
|
+
"""
|
|
258
|
+
if slug_or_id is None:
|
|
259
|
+
path = f"{_BASE}/incidents"
|
|
260
|
+
else:
|
|
261
|
+
path = f"{_service_path(slug_or_id)}/incidents"
|
|
262
|
+
params: _ParamDict = {"page": page, "size": size}
|
|
263
|
+
if status is not None:
|
|
264
|
+
params["status"] = status
|
|
265
|
+
if from_ is not None:
|
|
266
|
+
params["from"] = _format_date(from_)
|
|
267
|
+
if category is not None:
|
|
268
|
+
params["category"] = category
|
|
269
|
+
|
|
270
|
+
resp = api_get(self._client, path, params=params)
|
|
271
|
+
envelope = _validate_page(resp)
|
|
272
|
+
return Page(
|
|
273
|
+
data=parse_list(ServiceIncidentDto, envelope.data, f"GET {path}"),
|
|
274
|
+
has_next=envelope.hasNext,
|
|
275
|
+
has_prev=envelope.hasPrev,
|
|
276
|
+
total_elements=envelope.totalElements,
|
|
277
|
+
total_pages=envelope.totalPages,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def incident(self, slug_or_id: str, incident_id: str) -> ServiceIncidentDetailDto:
|
|
281
|
+
"""Get one vendor incident with its full update timeline."""
|
|
282
|
+
return parse_single(
|
|
283
|
+
ServiceIncidentDetailDto,
|
|
284
|
+
api_get(
|
|
285
|
+
self._client,
|
|
286
|
+
f"{_service_path(slug_or_id)}/incidents/{path_param(incident_id)}",
|
|
287
|
+
),
|
|
288
|
+
f"GET {_BASE}/{slug_or_id}/incidents/{incident_id}",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def uptime(
|
|
292
|
+
self, slug_or_id: str, *, period: str = "30d", granularity: str = "daily"
|
|
293
|
+
) -> ServiceUptimeResponse:
|
|
294
|
+
"""Get a service's uptime with per-bucket breakdown.
|
|
295
|
+
|
|
296
|
+
``period`` is a preset window (``7d``, ``30d``, ``90d``, ``1y``);
|
|
297
|
+
``granularity`` is ``"hourly"`` or ``"daily"``.
|
|
298
|
+
"""
|
|
299
|
+
return parse_single(
|
|
300
|
+
ServiceUptimeResponse,
|
|
301
|
+
api_get(
|
|
302
|
+
self._client,
|
|
303
|
+
f"{_service_path(slug_or_id)}/uptime",
|
|
304
|
+
params={"period": period, "granularity": granularity},
|
|
305
|
+
),
|
|
306
|
+
f"GET {_BASE}/{slug_or_id}/uptime",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def maintenances(
|
|
310
|
+
self, slug_or_id: str, *, status: str | builtins.list[str] | None = None
|
|
311
|
+
) -> builtins.list[ScheduledMaintenanceDto]:
|
|
312
|
+
"""List a service's scheduled maintenances.
|
|
313
|
+
|
|
314
|
+
``status`` filters by maintenance status (``scheduled``,
|
|
315
|
+
``in_progress``, ``completed``); pass a list to match several.
|
|
316
|
+
"""
|
|
317
|
+
params: _ParamDict = {}
|
|
318
|
+
if status is not None:
|
|
319
|
+
params["status"] = status
|
|
320
|
+
return self._fetch_table(
|
|
321
|
+
f"{_service_path(slug_or_id)}/maintenances", ScheduledMaintenanceDto, params
|
|
322
|
+
)
|