devhelm 1.4.0__tar.gz → 1.6.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.4.0 → devhelm-1.6.0}/PKG-INFO +1 -1
- {devhelm-1.4.0 → devhelm-1.6.0}/pyproject.toml +5 -2
- {devhelm-1.4.0 → devhelm-1.6.0}/scripts/inject_strict_config.py +52 -17
- {devhelm-1.4.0 → devhelm-1.6.0}/scripts/typegen.sh +4 -5
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_generated.py +303 -299
- devhelm-1.6.0/tests/test_inject_strict_config.py +37 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_schemas.py +55 -16
- {devhelm-1.4.0 → devhelm-1.6.0}/uv.lock +2 -2
- {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/ci.yml +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/release.yml +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/spec-check.yml +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/.gitignore +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/LICENSE +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/Makefile +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/README.md +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/docs/openapi/monitoring-api.json +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/scripts/emit_response_enums.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/scripts/regen-from.sh +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/scripts/release.sh +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/__init__.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_enums.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_errors.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_http.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_pagination.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_validation.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/client.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/py.typed +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/__init__.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/alert_channels.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/api_keys.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/dependencies.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/deploy_lock.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/environments.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/forensics.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/incidents.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/maintenance_windows.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/monitors.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/notification_policies.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/resource_groups.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/secrets.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/services.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/status.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/status_pages.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/tags.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/webhooks.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/types.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/__init__.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/run_sdk.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_api_keys.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_client.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_dependencies.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_errors.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_http.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_maintenance_windows.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_negative_validation.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_services.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_spec_parity.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_typing.py +0 -0
- {devhelm-1.4.0 → devhelm-1.6.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.6.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devhelm"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.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"
|
|
@@ -28,7 +28,10 @@ Repository = "https://github.com/devhelmhq/sdk-python.git"
|
|
|
28
28
|
Issues = "https://github.com/devhelmhq/sdk-python/issues"
|
|
29
29
|
|
|
30
30
|
[build-system]
|
|
31
|
-
|
|
31
|
+
# hatchling 1.30+ emits Metadata-Version 2.5, which released twine and
|
|
32
|
+
# PyPI still reject. Cap until twine ships 2.5 support and warehouse
|
|
33
|
+
# accepts it (pypa/twine#1317, pypi/warehouse#19083).
|
|
34
|
+
requires = ["hatchling>=1.27,<1.30"]
|
|
32
35
|
build-backend = "hatchling.build"
|
|
33
36
|
|
|
34
37
|
[tool.hatch.build.targets.wheel]
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Inject ``model_config
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"""Inject ``model_config`` into every generated Pydantic BaseModel class,
|
|
3
|
+
and add Pydantic v2 ``Field(discriminator=...)`` annotations on tagged-union
|
|
4
|
+
fields.
|
|
5
|
+
|
|
6
|
+
Request / Params models get ``extra='forbid'`` so typos fail before the HTTP
|
|
7
|
+
call. Every other generated model — response DTOs and nested value objects
|
|
8
|
+
used when decoding API responses — gets ``extra='ignore'`` (Postel's Law).
|
|
9
|
+
Additive API fields, including nullable ones the published surface has never
|
|
10
|
+
seen, must not crash the client.
|
|
5
11
|
|
|
6
12
|
datamodel-code-generator does not emit a config block when the source
|
|
7
13
|
OpenAPI spec lacks ``additionalProperties: false``. Springdoc never emits
|
|
@@ -30,9 +36,10 @@ discriminator, Pydantic routes to the correct subtype based on the tag
|
|
|
30
36
|
value and reports only that subtype's errors (typically 1).
|
|
31
37
|
Implements P0.Bug4 from the round-3 DevEx audit.
|
|
32
38
|
|
|
33
|
-
This implements
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
This implements Postel's Law on the wire (`runbooks/api-contract.md` § 2.2:
|
|
40
|
+
tolerant response decoders, strict request authoring) plus the two DevEx
|
|
41
|
+
fixes above. P2 (request extras forbidden) stays; P1 is now "response extras
|
|
42
|
+
ignored", not rejected.
|
|
36
43
|
|
|
37
44
|
The transform is purely syntactic so we can run it on the codegen output
|
|
38
45
|
without parsing Python AST. Idempotent: re-runs upgrade an existing
|
|
@@ -50,7 +57,32 @@ from pathlib import Path
|
|
|
50
57
|
# `root-model-extra`), so skip them. Their behavior is governed by the
|
|
51
58
|
# inner type, which on its own enforces strict validation.
|
|
52
59
|
CLASS_RE = re.compile(r"^class\s+([A-Za-z_][\w]*)\s*\(\s*(BaseModel)\s*\)\s*:\s*$")
|
|
53
|
-
|
|
60
|
+
CONFIG_LINE_STRICT = (
|
|
61
|
+
" model_config = ConfigDict(extra='forbid', populate_by_name=True)"
|
|
62
|
+
)
|
|
63
|
+
CONFIG_LINE_TOLERANT = (
|
|
64
|
+
" model_config = ConfigDict(extra='ignore', populate_by_name=True)"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_response_shape(class_name: str) -> bool:
|
|
69
|
+
"""Tolerate unknown fields on every non-authoring model (Postel's Law).
|
|
70
|
+
|
|
71
|
+
``*Request`` / ``*Params`` stay ``extra='forbid'``. Everything else —
|
|
72
|
+
``*Dto``, nested value objects on those DTOs (``StatusPageBranding``,
|
|
73
|
+
check-detail variants, channel configs), envelopes — ignores unknown
|
|
74
|
+
keys so an additive API field is a non-event. Shared nested types used
|
|
75
|
+
on both request and response follow the response rule: crashing a
|
|
76
|
+
``get`` / ``list`` is worse than dropping an unknown nested request key
|
|
77
|
+
the API would ignore anyway.
|
|
78
|
+
"""
|
|
79
|
+
if not class_name or class_name[0].islower():
|
|
80
|
+
return False
|
|
81
|
+
return not class_name.endswith(("Request", "Params"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Keep the old name for backward compat in case anything imports it
|
|
85
|
+
CONFIG_LINE = CONFIG_LINE_STRICT
|
|
54
86
|
|
|
55
87
|
# Doc-banner injections keyed by class name. Inserted as a leading docstring
|
|
56
88
|
# inside the target class so the note shows up in IDE hovers and stays put
|
|
@@ -61,7 +93,7 @@ CLASS_BANNERS: dict[str, str] = {
|
|
|
61
93
|
"Note: ``currentStatus`` was removed from this DTO. "
|
|
62
94
|
"Inspect ``enabled`` and the incident-policy API to derive a "
|
|
63
95
|
"live status for a monitor instead."
|
|
64
|
-
)
|
|
96
|
+
)
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
|
|
@@ -87,9 +119,7 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
87
119
|
"""Return (new_source, count_of_classes_modified)."""
|
|
88
120
|
if "from pydantic import" in source and "ConfigDict" not in source:
|
|
89
121
|
source = source.replace(
|
|
90
|
-
"from pydantic import",
|
|
91
|
-
"from pydantic import ConfigDict, ",
|
|
92
|
-
1,
|
|
122
|
+
"from pydantic import", "from pydantic import ConfigDict, ", 1
|
|
93
123
|
)
|
|
94
124
|
source = source.replace("ConfigDict, ConfigDict, ", "ConfigDict, ", 1)
|
|
95
125
|
|
|
@@ -130,6 +160,11 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
130
160
|
i += 1
|
|
131
161
|
continue
|
|
132
162
|
class_name = m.group(1)
|
|
163
|
+
config_line = (
|
|
164
|
+
CONFIG_LINE_TOLERANT
|
|
165
|
+
if _is_response_shape(class_name)
|
|
166
|
+
else CONFIG_LINE_STRICT
|
|
167
|
+
)
|
|
133
168
|
# Look at the very next line. If it's already model_config or pass,
|
|
134
169
|
# leave the class alone (idempotency / empty class).
|
|
135
170
|
next_idx = i + 1
|
|
@@ -141,10 +176,10 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
141
176
|
out.append(f' """{banner}"""\n')
|
|
142
177
|
modified += 1
|
|
143
178
|
if "model_config" in next_line:
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
if
|
|
147
|
-
out.append(
|
|
179
|
+
# Replace the existing config line if it doesn't match the
|
|
180
|
+
# desired strictness or is missing populate_by_name.
|
|
181
|
+
if next_line.strip() != config_line.strip():
|
|
182
|
+
out.append(config_line + "\n")
|
|
148
183
|
i += 2 # replace the existing model_config line
|
|
149
184
|
modified += 1
|
|
150
185
|
continue
|
|
@@ -154,11 +189,11 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
154
189
|
# exact match (NOT startswith) — fields like `passed: Annotated[...]`
|
|
155
190
|
# also start with "pass" but are not empty class markers.
|
|
156
191
|
if next_line.strip() in ("pass", "pass\n"):
|
|
157
|
-
out.append(
|
|
192
|
+
out.append(config_line + "\n")
|
|
158
193
|
i += 2 # skip the pass
|
|
159
194
|
modified += 1
|
|
160
195
|
continue
|
|
161
|
-
out.append(
|
|
196
|
+
out.append(config_line + "\n")
|
|
162
197
|
modified += 1
|
|
163
198
|
i += 1
|
|
164
199
|
return "".join(out), modified
|
|
@@ -69,11 +69,10 @@ uv run datamodel-codegen \
|
|
|
69
69
|
# satisfying the discriminator requirement and making the field optional at
|
|
70
70
|
# construction (callers don't need to repeat the discriminator value).
|
|
71
71
|
|
|
72
|
-
# Post-process:
|
|
73
|
-
# generated
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
echo "=> Injecting strict-fail config (extra='forbid') into generated models..."
|
|
72
|
+
# Post-process: extra='forbid' on *Request/*Params, extra='ignore' on every
|
|
73
|
+
# other generated model (Postel's Law — additive API response fields must
|
|
74
|
+
# not crash). See scripts/inject_strict_config.py.
|
|
75
|
+
echo "=> Injecting model_config (forbid on requests, ignore on responses)..."
|
|
77
76
|
uv run python "$SCRIPT_DIR/inject_strict_config.py" "$OUTPUT"
|
|
78
77
|
|
|
79
78
|
# Re-format after injection so the file stays ruff-clean. Non-fatal so the
|