devhelm 1.5.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.5.0 → devhelm-1.6.0}/PKG-INFO +1 -1
- {devhelm-1.5.0 → devhelm-1.6.0}/pyproject.toml +5 -2
- {devhelm-1.5.0 → devhelm-1.6.0}/scripts/inject_strict_config.py +38 -21
- {devhelm-1.5.0 → devhelm-1.6.0}/scripts/typegen.sh +4 -5
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_generated.py +112 -111
- devhelm-1.6.0/tests/test_inject_strict_config.py +37 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_schemas.py +55 -16
- {devhelm-1.5.0 → devhelm-1.6.0}/uv.lock +2 -2
- {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/ci.yml +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/release.yml +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/spec-check.yml +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/.gitignore +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/LICENSE +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/Makefile +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/README.md +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/docs/openapi/monitoring-api.json +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/scripts/emit_response_enums.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/scripts/regen-from.sh +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/scripts/release.sh +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/__init__.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_enums.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_errors.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_http.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_pagination.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_validation.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/client.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/py.typed +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/__init__.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/alert_channels.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/api_keys.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/dependencies.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/deploy_lock.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/environments.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/forensics.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/incidents.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/maintenance_windows.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/monitors.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/notification_policies.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/resource_groups.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/secrets.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/services.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/status.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/status_pages.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/tags.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/webhooks.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/types.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/__init__.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/run_sdk.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_api_keys.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_client.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_dependencies.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_errors.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_http.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_maintenance_windows.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_negative_validation.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_services.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_spec_parity.py +0 -0
- {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_typing.py +0 -0
- {devhelm-1.5.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,20 +57,28 @@ 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
|
-
CONFIG_LINE_STRICT =
|
|
54
|
-
|
|
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
|
+
)
|
|
55
66
|
|
|
56
67
|
|
|
57
68
|
def _is_response_shape(class_name: str) -> bool:
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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():
|
|
62
80
|
return False
|
|
63
|
-
return
|
|
64
|
-
class_name.endswith(("Dto", "Response"))
|
|
65
|
-
or class_name.startswith(("SingleValueResponse", "TableValueResult", "CursorPage"))
|
|
66
|
-
)
|
|
81
|
+
return not class_name.endswith(("Request", "Params"))
|
|
67
82
|
|
|
68
83
|
|
|
69
84
|
# Keep the old name for backward compat in case anything imports it
|
|
@@ -78,7 +93,7 @@ CLASS_BANNERS: dict[str, str] = {
|
|
|
78
93
|
"Note: ``currentStatus`` was removed from this DTO. "
|
|
79
94
|
"Inspect ``enabled`` and the incident-policy API to derive a "
|
|
80
95
|
"live status for a monitor instead."
|
|
81
|
-
)
|
|
96
|
+
)
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
|
|
@@ -104,9 +119,7 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
104
119
|
"""Return (new_source, count_of_classes_modified)."""
|
|
105
120
|
if "from pydantic import" in source and "ConfigDict" not in source:
|
|
106
121
|
source = source.replace(
|
|
107
|
-
"from pydantic import",
|
|
108
|
-
"from pydantic import ConfigDict, ",
|
|
109
|
-
1,
|
|
122
|
+
"from pydantic import", "from pydantic import ConfigDict, ", 1
|
|
110
123
|
)
|
|
111
124
|
source = source.replace("ConfigDict, ConfigDict, ", "ConfigDict, ", 1)
|
|
112
125
|
|
|
@@ -147,7 +160,11 @@ def inject(source: str) -> tuple[str, int]:
|
|
|
147
160
|
i += 1
|
|
148
161
|
continue
|
|
149
162
|
class_name = m.group(1)
|
|
150
|
-
config_line =
|
|
163
|
+
config_line = (
|
|
164
|
+
CONFIG_LINE_TOLERANT
|
|
165
|
+
if _is_response_shape(class_name)
|
|
166
|
+
else CONFIG_LINE_STRICT
|
|
167
|
+
)
|
|
151
168
|
# Look at the very next line. If it's already model_config or pass,
|
|
152
169
|
# leave the class alone (idempotency / empty class).
|
|
153
170
|
next_idx = i + 1
|
|
@@ -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
|