devhelm 1.3.0__tar.gz → 1.4.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.4.0}/PKG-INFO +1 -1
  2. {devhelm-1.3.0 → devhelm-1.4.0}/docs/openapi/monitoring-api.json +43 -0
  3. {devhelm-1.3.0 → devhelm-1.4.0}/pyproject.toml +1 -1
  4. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_errors.py +37 -1
  5. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_http.py +1 -0
  6. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/api_keys.py +9 -1
  7. {devhelm-1.3.0 → devhelm-1.4.0}/tests/run_sdk.py +32 -0
  8. devhelm-1.4.0/tests/test_api_keys.py +76 -0
  9. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_errors.py +27 -0
  10. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_http.py +46 -2
  11. {devhelm-1.3.0 → devhelm-1.4.0}/uv.lock +1 -1
  12. {devhelm-1.3.0 → devhelm-1.4.0}/.github/workflows/ci.yml +0 -0
  13. {devhelm-1.3.0 → devhelm-1.4.0}/.github/workflows/release.yml +0 -0
  14. {devhelm-1.3.0 → devhelm-1.4.0}/.github/workflows/spec-check.yml +0 -0
  15. {devhelm-1.3.0 → devhelm-1.4.0}/.gitignore +0 -0
  16. {devhelm-1.3.0 → devhelm-1.4.0}/LICENSE +0 -0
  17. {devhelm-1.3.0 → devhelm-1.4.0}/Makefile +0 -0
  18. {devhelm-1.3.0 → devhelm-1.4.0}/README.md +0 -0
  19. {devhelm-1.3.0 → devhelm-1.4.0}/scripts/emit_response_enums.py +0 -0
  20. {devhelm-1.3.0 → devhelm-1.4.0}/scripts/inject_strict_config.py +0 -0
  21. {devhelm-1.3.0 → devhelm-1.4.0}/scripts/regen-from.sh +0 -0
  22. {devhelm-1.3.0 → devhelm-1.4.0}/scripts/release.sh +0 -0
  23. {devhelm-1.3.0 → devhelm-1.4.0}/scripts/typegen.sh +0 -0
  24. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/__init__.py +0 -0
  25. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_enums.py +0 -0
  26. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_generated.py +0 -0
  27. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_pagination.py +0 -0
  28. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/_validation.py +0 -0
  29. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/client.py +0 -0
  30. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/py.typed +0 -0
  31. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/__init__.py +0 -0
  32. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/alert_channels.py +0 -0
  33. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/dependencies.py +0 -0
  34. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/deploy_lock.py +0 -0
  35. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/environments.py +0 -0
  36. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/forensics.py +0 -0
  37. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/incidents.py +0 -0
  38. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/maintenance_windows.py +0 -0
  39. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/monitors.py +0 -0
  40. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/notification_policies.py +0 -0
  41. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/resource_groups.py +0 -0
  42. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/secrets.py +0 -0
  43. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/services.py +0 -0
  44. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/status.py +0 -0
  45. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/status_pages.py +0 -0
  46. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/tags.py +0 -0
  47. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/resources/webhooks.py +0 -0
  48. {devhelm-1.3.0 → devhelm-1.4.0}/src/devhelm/types.py +0 -0
  49. {devhelm-1.3.0 → devhelm-1.4.0}/tests/__init__.py +0 -0
  50. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_client.py +0 -0
  51. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_dependencies.py +0 -0
  52. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_maintenance_windows.py +0 -0
  53. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_negative_validation.py +0 -0
  54. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_schemas.py +0 -0
  55. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_services.py +0 -0
  56. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_spec_parity.py +0 -0
  57. {devhelm-1.3.0 → devhelm-1.4.0}/tests/test_typing.py +0 -0
  58. {devhelm-1.3.0 → devhelm-1.4.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.4.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.4.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"
@@ -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):
@@ -180,6 +180,7 @@ def checked_fetch(response: httpx.Response) -> _JsonResponse:
180
180
  response.status_code,
181
181
  response.text,
182
182
  request_id=response.headers.get("x-request-id"),
183
+ retry_after=response.headers.get("retry-after"),
183
184
  )
184
185
 
185
186
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import httpx
4
4
 
5
5
  from devhelm._generated import ApiKeyCreateResponse, ApiKeyDto, CreateApiKeyRequest
6
- from devhelm._http import api_delete, api_post, path_param
6
+ from devhelm._http import api_delete, api_get, api_post, path_param
7
7
  from devhelm._pagination import Page, fetch_all_pages, fetch_page
8
8
  from devhelm._validation import RequestBody, parse_single, validate_request
9
9
 
@@ -22,6 +22,14 @@ class ApiKeys:
22
22
  """List API keys with manual page control."""
23
23
  return fetch_page(self._client, "/api/v1/api-keys", ApiKeyDto, page, size)
24
24
 
25
+ def get(self, id: int | str) -> ApiKeyDto:
26
+ """Get a single API key by ID."""
27
+ return parse_single(
28
+ ApiKeyDto,
29
+ api_get(self._client, f"/api/v1/api-keys/{path_param(id)}"),
30
+ f"GET /api/v1/api-keys/{id}",
31
+ )
32
+
25
33
  def create(self, body: RequestBody[CreateApiKeyRequest]) -> ApiKeyCreateResponse:
26
34
  """Create an API key. Returns the key value (shown only once)."""
27
35
  body = validate_request(CreateApiKeyRequest, body, "apiKeys.create")
@@ -259,6 +259,8 @@ def run(client: Devhelm, resource: str, action: str, rest: list[str]) -> Any: #
259
259
  # -- API Keys --
260
260
  if op == "api-keys.list":
261
261
  return client.api_keys.list()
262
+ if op == "api-keys.get":
263
+ return client.api_keys.get(rest[0])
262
264
  if op == "api-keys.create":
263
265
  return client.api_keys.create(_parse(CreateApiKeyRequest, rest[0]))
264
266
  if op == "api-keys.revoke":
@@ -275,10 +277,40 @@ def run(client: Devhelm, resource: str, action: str, rest: list[str]) -> Any: #
275
277
  return client.dependencies.get(rest[0])
276
278
  if op == "dependencies.track":
277
279
  return client.dependencies.track(rest[0])
280
+ if op == "dependencies.update-alert-sensitivity":
281
+ return client.dependencies.update_alert_sensitivity(rest[0], rest[1])
278
282
  if op == "dependencies.delete":
279
283
  client.dependencies.delete(rest[0])
280
284
  return None
281
285
 
286
+ # -- Services (Status Data catalog, read-only) --
287
+ if op == "services.list":
288
+ return client.services.list()
289
+ if op == "services.get":
290
+ return client.services.get(rest[0])
291
+ if op == "services.live-status":
292
+ return client.services.live_status(rest[0])
293
+ if op == "services.categories":
294
+ return client.services.categories()
295
+ if op == "services.summary":
296
+ return client.services.summary()
297
+ if op == "services.components":
298
+ return client.services.components(rest[0])
299
+ if op == "services.component-uptime":
300
+ return client.services.component_uptime(rest[0], rest[1])
301
+ if op == "services.batch-component-uptime":
302
+ return client.services.batch_component_uptime(rest[0])
303
+ if op == "services.day":
304
+ return client.services.day(rest[0], rest[1])
305
+ if op == "services.incidents":
306
+ return client.services.incidents(rest[0] if rest else None)
307
+ if op == "services.incident":
308
+ return client.services.incident(rest[0], rest[1])
309
+ if op == "services.uptime":
310
+ return client.services.uptime(rest[0])
311
+ if op == "services.maintenances":
312
+ return client.services.maintenances(rest[0])
313
+
282
314
  # -- Deploy Lock --
283
315
  if op == "deploy-lock.acquire":
284
316
  return client.deploy_lock.acquire(_parse(AcquireDeployLockRequest, rest[0]))
@@ -0,0 +1,76 @@
1
+ """Tests for the ``ApiKeys`` resource module.
2
+
3
+ Mirrors ``test_maintenance_windows`` / ``test_services``: spin up an
4
+ ``httpx.MockTransport``, point a real ``ApiKeys`` instance at it, and
5
+ assert the resulting ``httpx.Request`` carries the wire-level URL and
6
+ method the API documents — plus that responses are unwrapped into typed
7
+ models.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import httpx
13
+
14
+ from devhelm.resources.api_keys import ApiKeys
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Fixtures: canned API payload (camelCase wire shape)
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ _API_KEY = {
22
+ "id": 42,
23
+ "name": "CI pipeline",
24
+ "key": "dh_live_abc123",
25
+ "createdAt": "2026-01-01T00:00:00Z",
26
+ "updatedAt": "2026-06-01T00:00:00Z",
27
+ "lastUsedAt": None,
28
+ "revokedAt": None,
29
+ "expiresAt": None,
30
+ }
31
+
32
+
33
+ def _stub_transport(captured: list[httpx.Request]) -> httpx.MockTransport:
34
+ def handler(request: httpx.Request) -> httpx.Response:
35
+ captured.append(request)
36
+ method = request.method
37
+ path = request.url.path
38
+ if method == "GET" and path.startswith("/api/v1/api-keys/"):
39
+ return httpx.Response(200, json={"data": _API_KEY})
40
+ raise AssertionError(f"unexpected {method} {path}")
41
+
42
+ return httpx.MockTransport(handler)
43
+
44
+
45
+ def _resource(transport: httpx.MockTransport) -> ApiKeys:
46
+ http_client = httpx.Client(transport=transport, base_url="http://localhost:8080")
47
+ return ApiKeys(http_client)
48
+
49
+
50
+ class TestGet:
51
+ def test_get_is_callable(self) -> None:
52
+ api_keys = _resource(_stub_transport([]))
53
+ assert callable(api_keys.get)
54
+
55
+ def test_get_hits_resource_url_and_unwraps(self) -> None:
56
+ captured: list[httpx.Request] = []
57
+ api_keys = _resource(_stub_transport(captured))
58
+
59
+ result = api_keys.get(42)
60
+
61
+ assert len(captured) == 1
62
+ assert captured[0].method == "GET"
63
+ assert captured[0].url.path == "/api/v1/api-keys/42"
64
+ assert result.id == 42
65
+ assert result.name == "CI pipeline"
66
+ assert result.key == "dh_live_abc123"
67
+
68
+ def test_get_encodes_path_param(self) -> None:
69
+ captured: list[httpx.Request] = []
70
+ api_keys = _resource(_stub_transport(captured))
71
+
72
+ api_keys.get("a b")
73
+
74
+ # ``url.path`` is percent-decoded by httpx; assert on the raw bytes
75
+ # to confirm ``path_param`` encoded the space before it hit the wire.
76
+ assert b"/api/v1/api-keys/a%20b" == captured[0].url.raw_path
@@ -151,6 +151,33 @@ class TestErrorFromResponse:
151
151
  assert err.request_id is None
152
152
 
153
153
 
154
+ class TestRetryAfter:
155
+ def test_429_parses_retry_after_header_to_int(self) -> None:
156
+ err = error_from_response(
157
+ 429, json.dumps({"message": "Slow down"}), retry_after="30"
158
+ )
159
+ assert isinstance(err, DevhelmRateLimitError)
160
+ assert err.retry_after == 30
161
+ assert isinstance(err.retry_after, int)
162
+
163
+ def test_retry_after_absent_is_none(self) -> None:
164
+ err = error_from_response(429, json.dumps({"message": "Slow down"}))
165
+ assert err.retry_after is None
166
+
167
+ def test_retry_after_non_integer_is_none(self) -> None:
168
+ # HTTP-date form (or any garbage) must not break error construction.
169
+ err = error_from_response(
170
+ 429,
171
+ json.dumps({"message": "Slow down"}),
172
+ retry_after="Wed, 21 Oct 2026 07:28:00 GMT",
173
+ )
174
+ assert err.retry_after is None
175
+
176
+ def test_retry_after_default_none_on_constructor(self) -> None:
177
+ err = DevhelmApiError("boom", status=500)
178
+ assert err.retry_after is None
179
+
180
+
154
181
  class TestDevhelmErrorInheritance:
155
182
  def test_api_error_is_devhelm_error(self) -> None:
156
183
  err = DevhelmApiError("test", status=500)
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import httpx
5
6
  import pytest
6
7
  from pydantic import BaseModel, Field
7
8
 
8
- from devhelm._errors import DevhelmError
9
- from devhelm._http import DevhelmConfig, build_client, path_param
9
+ from devhelm._errors import DevhelmError, DevhelmRateLimitError
10
+ from devhelm._http import DevhelmConfig, api_get, build_client, path_param
10
11
  from devhelm._validation import parse_list, parse_model, parse_single
11
12
 
12
13
  # ---------- path_param ----------
@@ -120,6 +121,49 @@ class TestSurfaceTelemetry:
120
121
  client.close()
121
122
 
122
123
 
124
+ # ---------- Rate-limit Retry-After surfacing ----------
125
+
126
+
127
+ class TestRetryAfterFromResponse:
128
+ """A 429 with a ``Retry-After`` header must surface ``retry_after`` as an
129
+ integer on the raised :class:`DevhelmRateLimitError` so callers can back
130
+ off for exactly as long as the server asked.
131
+ """
132
+
133
+ def test_429_retry_after_header_surfaces_as_int(self) -> None:
134
+ def handler(request: httpx.Request) -> httpx.Response:
135
+ return httpx.Response(
136
+ 429,
137
+ headers={"Retry-After": "30"},
138
+ json={"message": "Slow down", "code": "RATE_LIMITED"},
139
+ )
140
+
141
+ client = httpx.Client(
142
+ transport=httpx.MockTransport(handler), base_url="http://localhost:8080"
143
+ )
144
+ with pytest.raises(DevhelmRateLimitError) as exc_info:
145
+ api_get(client, "/api/v1/monitors")
146
+ client.close()
147
+
148
+ err = exc_info.value
149
+ assert err.status == 429
150
+ assert err.retry_after == 30
151
+ assert isinstance(err.retry_after, int)
152
+
153
+ def test_429_without_header_has_none_retry_after(self) -> None:
154
+ def handler(request: httpx.Request) -> httpx.Response:
155
+ return httpx.Response(429, json={"message": "Slow down"})
156
+
157
+ client = httpx.Client(
158
+ transport=httpx.MockTransport(handler), base_url="http://localhost:8080"
159
+ )
160
+ with pytest.raises(DevhelmRateLimitError) as exc_info:
161
+ api_get(client, "/api/v1/monitors")
162
+ client.close()
163
+
164
+ assert exc_info.value.retry_after is None
165
+
166
+
123
167
  # ---------- Pydantic validation helpers ----------
124
168
 
125
169
 
@@ -315,7 +315,7 @@ wheels = [
315
315
 
316
316
  [[package]]
317
317
  name = "devhelm"
318
- version = "1.3.0"
318
+ version = "1.4.0"
319
319
  source = { editable = "." }
320
320
  dependencies = [
321
321
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes