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.
Files changed (58) hide show
  1. {devhelm-1.3.0 → devhelm-1.5.0}/PKG-INFO +1 -1
  2. {devhelm-1.3.0 → devhelm-1.5.0}/docs/openapi/monitoring-api.json +43 -0
  3. {devhelm-1.3.0 → devhelm-1.5.0}/pyproject.toml +1 -1
  4. {devhelm-1.3.0 → devhelm-1.5.0}/scripts/inject_strict_config.py +25 -7
  5. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_errors.py +37 -1
  6. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_generated.py +191 -188
  7. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_http.py +1 -0
  8. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/api_keys.py +9 -1
  9. {devhelm-1.3.0 → devhelm-1.5.0}/tests/run_sdk.py +32 -0
  10. devhelm-1.5.0/tests/test_api_keys.py +76 -0
  11. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_errors.py +27 -0
  12. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_http.py +46 -2
  13. {devhelm-1.3.0 → devhelm-1.5.0}/uv.lock +1 -1
  14. {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/ci.yml +0 -0
  15. {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/release.yml +0 -0
  16. {devhelm-1.3.0 → devhelm-1.5.0}/.github/workflows/spec-check.yml +0 -0
  17. {devhelm-1.3.0 → devhelm-1.5.0}/.gitignore +0 -0
  18. {devhelm-1.3.0 → devhelm-1.5.0}/LICENSE +0 -0
  19. {devhelm-1.3.0 → devhelm-1.5.0}/Makefile +0 -0
  20. {devhelm-1.3.0 → devhelm-1.5.0}/README.md +0 -0
  21. {devhelm-1.3.0 → devhelm-1.5.0}/scripts/emit_response_enums.py +0 -0
  22. {devhelm-1.3.0 → devhelm-1.5.0}/scripts/regen-from.sh +0 -0
  23. {devhelm-1.3.0 → devhelm-1.5.0}/scripts/release.sh +0 -0
  24. {devhelm-1.3.0 → devhelm-1.5.0}/scripts/typegen.sh +0 -0
  25. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/__init__.py +0 -0
  26. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_enums.py +0 -0
  27. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_pagination.py +0 -0
  28. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/_validation.py +0 -0
  29. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/client.py +0 -0
  30. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/py.typed +0 -0
  31. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/__init__.py +0 -0
  32. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/alert_channels.py +0 -0
  33. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/dependencies.py +0 -0
  34. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/deploy_lock.py +0 -0
  35. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/environments.py +0 -0
  36. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/forensics.py +0 -0
  37. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/incidents.py +0 -0
  38. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/maintenance_windows.py +0 -0
  39. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/monitors.py +0 -0
  40. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/notification_policies.py +0 -0
  41. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/resource_groups.py +0 -0
  42. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/secrets.py +0 -0
  43. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/services.py +0 -0
  44. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/status.py +0 -0
  45. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/status_pages.py +0 -0
  46. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/tags.py +0 -0
  47. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/resources/webhooks.py +0 -0
  48. {devhelm-1.3.0 → devhelm-1.5.0}/src/devhelm/types.py +0 -0
  49. {devhelm-1.3.0 → devhelm-1.5.0}/tests/__init__.py +0 -0
  50. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_client.py +0 -0
  51. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_dependencies.py +0 -0
  52. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_maintenance_windows.py +0 -0
  53. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_negative_validation.py +0 -0
  54. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_schemas.py +0 -0
  55. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_services.py +0 -0
  56. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_spec_parity.py +0 -0
  57. {devhelm-1.3.0 → devhelm-1.5.0}/tests/test_typing.py +0 -0
  58. {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.0
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",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devhelm"
3
- version = "1.3.0"
3
+ version = "1.5.0"
4
4
  description = "DevHelm SDK for Python — typed client for monitors, incidents, alerting, and more"
5
5
  authors = [{ name = "DevHelm", email = "hello@devhelm.io" }]
6
6
  license = "MIT"
@@ -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
- CONFIG_LINE = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
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
- # Upgrade the existing config line to include populate_by_name=True
145
- # if it isn't already there. Idempotent across re-runs.
146
- if "populate_by_name" not in next_line:
147
- out.append(CONFIG_LINE + "\n")
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(CONFIG_LINE + "\n")
175
+ out.append(config_line + "\n")
158
176
  i += 2 # skip the pass
159
177
  modified += 1
160
178
  continue
161
- out.append(CONFIG_LINE + "\n")
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, body: str, *, request_id: str | None = None
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):