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.
Files changed (59) hide show
  1. {devhelm-1.4.0 → devhelm-1.6.0}/PKG-INFO +1 -1
  2. {devhelm-1.4.0 → devhelm-1.6.0}/pyproject.toml +5 -2
  3. {devhelm-1.4.0 → devhelm-1.6.0}/scripts/inject_strict_config.py +52 -17
  4. {devhelm-1.4.0 → devhelm-1.6.0}/scripts/typegen.sh +4 -5
  5. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_generated.py +303 -299
  6. devhelm-1.6.0/tests/test_inject_strict_config.py +37 -0
  7. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_schemas.py +55 -16
  8. {devhelm-1.4.0 → devhelm-1.6.0}/uv.lock +2 -2
  9. {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/ci.yml +0 -0
  10. {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/release.yml +0 -0
  11. {devhelm-1.4.0 → devhelm-1.6.0}/.github/workflows/spec-check.yml +0 -0
  12. {devhelm-1.4.0 → devhelm-1.6.0}/.gitignore +0 -0
  13. {devhelm-1.4.0 → devhelm-1.6.0}/LICENSE +0 -0
  14. {devhelm-1.4.0 → devhelm-1.6.0}/Makefile +0 -0
  15. {devhelm-1.4.0 → devhelm-1.6.0}/README.md +0 -0
  16. {devhelm-1.4.0 → devhelm-1.6.0}/docs/openapi/monitoring-api.json +0 -0
  17. {devhelm-1.4.0 → devhelm-1.6.0}/scripts/emit_response_enums.py +0 -0
  18. {devhelm-1.4.0 → devhelm-1.6.0}/scripts/regen-from.sh +0 -0
  19. {devhelm-1.4.0 → devhelm-1.6.0}/scripts/release.sh +0 -0
  20. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/__init__.py +0 -0
  21. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_enums.py +0 -0
  22. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_errors.py +0 -0
  23. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_http.py +0 -0
  24. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_pagination.py +0 -0
  25. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/_validation.py +0 -0
  26. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/client.py +0 -0
  27. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/py.typed +0 -0
  28. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/__init__.py +0 -0
  29. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/alert_channels.py +0 -0
  30. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/api_keys.py +0 -0
  31. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/dependencies.py +0 -0
  32. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/deploy_lock.py +0 -0
  33. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/environments.py +0 -0
  34. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/forensics.py +0 -0
  35. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/incidents.py +0 -0
  36. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/maintenance_windows.py +0 -0
  37. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/monitors.py +0 -0
  38. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/notification_policies.py +0 -0
  39. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/resource_groups.py +0 -0
  40. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/secrets.py +0 -0
  41. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/services.py +0 -0
  42. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/status.py +0 -0
  43. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/status_pages.py +0 -0
  44. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/tags.py +0 -0
  45. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/resources/webhooks.py +0 -0
  46. {devhelm-1.4.0 → devhelm-1.6.0}/src/devhelm/types.py +0 -0
  47. {devhelm-1.4.0 → devhelm-1.6.0}/tests/__init__.py +0 -0
  48. {devhelm-1.4.0 → devhelm-1.6.0}/tests/run_sdk.py +0 -0
  49. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_api_keys.py +0 -0
  50. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_client.py +0 -0
  51. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_dependencies.py +0 -0
  52. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_errors.py +0 -0
  53. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_http.py +0 -0
  54. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_maintenance_windows.py +0 -0
  55. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_negative_validation.py +0 -0
  56. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_services.py +0 -0
  57. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_spec_parity.py +0 -0
  58. {devhelm-1.4.0 → devhelm-1.6.0}/tests/test_typing.py +0 -0
  59. {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.4.0
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.4.0"
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
- requires = ["hatchling"]
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 = ConfigDict(extra='forbid', populate_by_name=True)``
3
- into every generated Pydantic BaseModel class, and add Pydantic v2
4
- ``Field(discriminator=...)`` annotations on tagged-union fields.
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 policies P1 (response extras forbidden) and P2 (request
34
- extras forbidden) from `mini/cowork/design/040-codegen-policies.md` plus
35
- the two DevEx fixes above.
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
- CONFIG_LINE = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
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
- # 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")
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(CONFIG_LINE + "\n")
192
+ out.append(config_line + "\n")
158
193
  i += 2 # skip the pass
159
194
  modified += 1
160
195
  continue
161
- out.append(CONFIG_LINE + "\n")
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: inject `model_config = ConfigDict(extra='forbid')` into every
73
- # generated class so that requests with unknown fields and responses with
74
- # unknown fields BOTH fail loudly. Implements P1 + P2 from
75
- # `mini/cowork/design/040-codegen-policies.md`.
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