devhelm 1.3.0__tar.gz → 1.5.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.3.0 → devhelm-1.5.0}/PKG-INFO +1 -1
- {devhelm-1.3.0 → devhelm-1.5.0}/docs/openapi/monitoring-api.json +43 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/pyproject.toml +1 -1
- {devhelm-1.3.0 → devhelm-1.5.0}/scripts/inject_strict_config.py +25 -7
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_errors.py +37 -1
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_generated.py +191 -188
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_http.py +1 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/api_keys.py +9 -1
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/run_sdk.py +32 -0
- devhelm-1.5.0/tests/test_api_keys.py +76 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_errors.py +27 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_http.py +46 -2
- {devhelm-1.3.0 → devhelm-1.5.0}/uv.lock +1 -1
- {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/ci.yml +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/release.yml +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/spec-check.yml +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/.gitignore +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/LICENSE +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/Makefile +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/README.md +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/scripts/emit_response_enums.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/scripts/regen-from.sh +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/scripts/release.sh +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/scripts/typegen.sh +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/__init__.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_enums.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_pagination.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_validation.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/client.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/py.typed +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/__init__.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/alert_channels.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/dependencies.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/deploy_lock.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/environments.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/forensics.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/incidents.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/maintenance_windows.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/monitors.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/notification_policies.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/resource_groups.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/secrets.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/services.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/status.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/status_pages.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/tags.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/webhooks.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/types.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/__init__.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_client.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_dependencies.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_maintenance_windows.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_negative_validation.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_schemas.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_services.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_spec_parity.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_typing.py +0 -0
- {devhelm-1.3.0 → devhelm-1.5.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.5.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
|
|
@@ -2274,6 +2274,18 @@
|
|
|
2274
2274
|
],
|
|
2275
2275
|
"summary": "List categories with service counts",
|
|
2276
2276
|
"operationId": "listCategories",
|
|
2277
|
+
"parameters": [
|
|
2278
|
+
{
|
|
2279
|
+
"name": "publishedOnly",
|
|
2280
|
+
"in": "query",
|
|
2281
|
+
"description": "Count only published services (curated public pSEO set); default false",
|
|
2282
|
+
"required": false,
|
|
2283
|
+
"schema": {
|
|
2284
|
+
"type": "boolean",
|
|
2285
|
+
"default": false
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
],
|
|
2277
2289
|
"responses": {
|
|
2278
2290
|
"200": {
|
|
2279
2291
|
"description": "OK",
|
|
@@ -13529,6 +13541,15 @@
|
|
|
13529
13541
|
"type": "string"
|
|
13530
13542
|
}
|
|
13531
13543
|
},
|
|
13544
|
+
{
|
|
13545
|
+
"name": "sort",
|
|
13546
|
+
"in": "query",
|
|
13547
|
+
"description": "Result ordering: 'recent' (default, newest first) or 'curated' (curated/recognizable first)",
|
|
13548
|
+
"required": false,
|
|
13549
|
+
"schema": {
|
|
13550
|
+
"type": "string"
|
|
13551
|
+
}
|
|
13552
|
+
},
|
|
13532
13553
|
{
|
|
13533
13554
|
"name": "cursor",
|
|
13534
13555
|
"in": "query",
|
|
@@ -13670,6 +13691,16 @@
|
|
|
13670
13691
|
"type": "boolean",
|
|
13671
13692
|
"default": false
|
|
13672
13693
|
}
|
|
13694
|
+
},
|
|
13695
|
+
{
|
|
13696
|
+
"name": "publishedOnly",
|
|
13697
|
+
"in": "query",
|
|
13698
|
+
"description": "Resolve only published services (curated public pSEO set); 404 otherwise. Default false",
|
|
13699
|
+
"required": false,
|
|
13700
|
+
"schema": {
|
|
13701
|
+
"type": "boolean",
|
|
13702
|
+
"default": false
|
|
13703
|
+
}
|
|
13673
13704
|
}
|
|
13674
13705
|
],
|
|
13675
13706
|
"responses": {
|
|
@@ -15367,6 +15398,18 @@
|
|
|
15367
15398
|
"summary": "Global status summary across all services",
|
|
15368
15399
|
"description": "Returns aggregate counts of services by status and a list of services currently experiencing issues.",
|
|
15369
15400
|
"operationId": "getGlobalStatusSummary",
|
|
15401
|
+
"parameters": [
|
|
15402
|
+
{
|
|
15403
|
+
"name": "publishedOnly",
|
|
15404
|
+
"in": "query",
|
|
15405
|
+
"description": "Aggregate only published services (curated public pSEO set); default false",
|
|
15406
|
+
"required": false,
|
|
15407
|
+
"schema": {
|
|
15408
|
+
"type": "boolean",
|
|
15409
|
+
"default": false
|
|
15410
|
+
}
|
|
15411
|
+
}
|
|
15412
|
+
],
|
|
15370
15413
|
"responses": {
|
|
15371
15414
|
"200": {
|
|
15372
15415
|
"description": "OK",
|
|
@@ -50,7 +50,24 @@ from pathlib import Path
|
|
|
50
50
|
# `root-model-extra`), so skip them. Their behavior is governed by the
|
|
51
51
|
# inner type, which on its own enforces strict validation.
|
|
52
52
|
CLASS_RE = re.compile(r"^class\s+([A-Za-z_][\w]*)\s*\(\s*(BaseModel)\s*\)\s*:\s*$")
|
|
53
|
-
|
|
53
|
+
CONFIG_LINE_STRICT = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
|
|
54
|
+
CONFIG_LINE_TOLERANT = " model_config = ConfigDict(extra='ignore', populate_by_name=True)"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_response_shape(class_name: str) -> bool:
|
|
58
|
+
"""Response-shape classes tolerate unknown fields (Postel's Law)."""
|
|
59
|
+
if class_name[0].islower():
|
|
60
|
+
return False
|
|
61
|
+
if class_name.endswith(("Request", "Params")):
|
|
62
|
+
return False
|
|
63
|
+
return bool(
|
|
64
|
+
class_name.endswith(("Dto", "Response"))
|
|
65
|
+
or class_name.startswith(("SingleValueResponse", "TableValueResult", "CursorPage"))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Keep the old name for backward compat in case anything imports it
|
|
70
|
+
CONFIG_LINE = CONFIG_LINE_STRICT
|
|
54
71
|
|
|
55
72
|
# Doc-banner injections keyed by class name. Inserted as a leading docstring
|
|
56
73
|
# inside the target class so the note shows up in IDE hovers and stays put
|
|
@@ -130,6 +147,7 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
130
147
|
i += 1
|
|
131
148
|
continue
|
|
132
149
|
class_name = m.group(1)
|
|
150
|
+
config_line = CONFIG_LINE_TOLERANT if _is_response_shape(class_name) else CONFIG_LINE_STRICT
|
|
133
151
|
# Look at the very next line. If it's already model_config or pass,
|
|
134
152
|
# leave the class alone (idempotency / empty class).
|
|
135
153
|
next_idx = i + 1
|
|
@@ -141,10 +159,10 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
141
159
|
out.append(f' """{banner}"""\n')
|
|
142
160
|
modified += 1
|
|
143
161
|
if "model_config" in next_line:
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
if
|
|
147
|
-
out.append(
|
|
162
|
+
# Replace the existing config line if it doesn't match the
|
|
163
|
+
# desired strictness or is missing populate_by_name.
|
|
164
|
+
if next_line.strip() != config_line.strip():
|
|
165
|
+
out.append(config_line + "\n")
|
|
148
166
|
i += 2 # replace the existing model_config line
|
|
149
167
|
modified += 1
|
|
150
168
|
continue
|
|
@@ -154,11 +172,11 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
154
172
|
# exact match (NOT startswith) — fields like `passed: Annotated[...]`
|
|
155
173
|
# also start with "pass" but are not empty class markers.
|
|
156
174
|
if next_line.strip() in ("pass", "pass\n"):
|
|
157
|
-
out.append(
|
|
175
|
+
out.append(config_line + "\n")
|
|
158
176
|
i += 2 # skip the pass
|
|
159
177
|
modified += 1
|
|
160
178
|
continue
|
|
161
|
-
out.append(
|
|
179
|
+
out.append(config_line + "\n")
|
|
162
180
|
modified += 1
|
|
163
181
|
i += 1
|
|
164
182
|
return "".join(out), modified
|
|
@@ -82,6 +82,11 @@ class DevhelmApiError(DevhelmError):
|
|
|
82
82
|
The optional `request_id` field is the per-request id emitted by the
|
|
83
83
|
API as the `X-Request-Id` response header and embedded in the JSON
|
|
84
84
|
error body. Always include it in support tickets.
|
|
85
|
+
|
|
86
|
+
The optional `retry_after` field is the parsed value of the
|
|
87
|
+
`Retry-After` response header in whole seconds. It's populated on
|
|
88
|
+
rate-limit (429) responses that include the header so callers can back
|
|
89
|
+
off for exactly as long as the server asked; ``None`` otherwise.
|
|
85
90
|
"""
|
|
86
91
|
|
|
87
92
|
status: int
|
|
@@ -93,6 +98,7 @@ class DevhelmApiError(DevhelmError):
|
|
|
93
98
|
# narrowing. (Subclasses still inherit the same `str` type.)
|
|
94
99
|
code: str
|
|
95
100
|
request_id: str | None
|
|
101
|
+
retry_after: int | None
|
|
96
102
|
|
|
97
103
|
def __init__(
|
|
98
104
|
self,
|
|
@@ -103,6 +109,7 @@ class DevhelmApiError(DevhelmError):
|
|
|
103
109
|
body: dict[str, Any] | str | None = None,
|
|
104
110
|
code: str | None = None,
|
|
105
111
|
request_id: str | None = None,
|
|
112
|
+
retry_after: int | None = None,
|
|
106
113
|
) -> None:
|
|
107
114
|
super().__init__(message)
|
|
108
115
|
self.status = status
|
|
@@ -113,6 +120,9 @@ class DevhelmApiError(DevhelmError):
|
|
|
113
120
|
# `err.code` is never ``None`` for callers switching on category.
|
|
114
121
|
self.code = code or "API_ERROR"
|
|
115
122
|
self.request_id = request_id
|
|
123
|
+
# Parsed from the `Retry-After` response header (seconds). Populated
|
|
124
|
+
# on 429 / 503 responses that include it; ``None`` otherwise.
|
|
125
|
+
self.retry_after = retry_after
|
|
116
126
|
|
|
117
127
|
|
|
118
128
|
class DevhelmAuthError(DevhelmApiError):
|
|
@@ -152,8 +162,28 @@ class DevhelmTransportError(DevhelmError):
|
|
|
152
162
|
self.__cause__ = cause
|
|
153
163
|
|
|
154
164
|
|
|
165
|
+
def _parse_retry_after(value: str | None) -> int | None:
|
|
166
|
+
"""Parse a ``Retry-After`` header value into whole seconds.
|
|
167
|
+
|
|
168
|
+
The API emits ``Retry-After`` as an integer number of seconds. We parse
|
|
169
|
+
defensively: any non-integer value (an HTTP-date form, or garbage from a
|
|
170
|
+
misbehaving proxy) yields ``None`` rather than raising, so a malformed
|
|
171
|
+
header can never break error construction.
|
|
172
|
+
"""
|
|
173
|
+
if value is None:
|
|
174
|
+
return None
|
|
175
|
+
try:
|
|
176
|
+
return int(value)
|
|
177
|
+
except (TypeError, ValueError):
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
155
181
|
def error_from_response(
|
|
156
|
-
status: int,
|
|
182
|
+
status: int,
|
|
183
|
+
body: str,
|
|
184
|
+
*,
|
|
185
|
+
request_id: str | None = None,
|
|
186
|
+
retry_after: str | None = None,
|
|
157
187
|
) -> DevhelmApiError:
|
|
158
188
|
"""Map an HTTP error response to a typed DevhelmApiError subclass.
|
|
159
189
|
|
|
@@ -161,6 +191,11 @@ def error_from_response(
|
|
|
161
191
|
pulled out at the call site (rather than re-parsed from the body) so the
|
|
162
192
|
SDK still surfaces the id even when the server returns a non-JSON body
|
|
163
193
|
(e.g. an HTML error page from a misconfigured proxy).
|
|
194
|
+
|
|
195
|
+
`retry_after` is the raw value of the `Retry-After` response header,
|
|
196
|
+
pulled out at the call site for the same reason. It's parsed into whole
|
|
197
|
+
seconds and surfaced as ``err.retry_after`` (e.g. on 429 responses) so
|
|
198
|
+
callers can back off for exactly as long as the server asked.
|
|
164
199
|
"""
|
|
165
200
|
message = f"HTTP {status}"
|
|
166
201
|
detail: str | None = None
|
|
@@ -195,6 +230,7 @@ def error_from_response(
|
|
|
195
230
|
"body": parsed_body,
|
|
196
231
|
"code": code,
|
|
197
232
|
"request_id": resolved_request_id,
|
|
233
|
+
"retry_after": _parse_retry_after(retry_after),
|
|
198
234
|
}
|
|
199
235
|
|
|
200
236
|
if status in (401, 403):
|