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.
Files changed (59) hide show
  1. {devhelm-1.5.0 → devhelm-1.6.0}/PKG-INFO +1 -1
  2. {devhelm-1.5.0 → devhelm-1.6.0}/pyproject.toml +5 -2
  3. {devhelm-1.5.0 → devhelm-1.6.0}/scripts/inject_strict_config.py +38 -21
  4. {devhelm-1.5.0 → devhelm-1.6.0}/scripts/typegen.sh +4 -5
  5. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_generated.py +112 -111
  6. devhelm-1.6.0/tests/test_inject_strict_config.py +37 -0
  7. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_schemas.py +55 -16
  8. {devhelm-1.5.0 → devhelm-1.6.0}/uv.lock +2 -2
  9. {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/ci.yml +0 -0
  10. {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/release.yml +0 -0
  11. {devhelm-1.5.0 → devhelm-1.6.0}/.github/workflows/spec-check.yml +0 -0
  12. {devhelm-1.5.0 → devhelm-1.6.0}/.gitignore +0 -0
  13. {devhelm-1.5.0 → devhelm-1.6.0}/LICENSE +0 -0
  14. {devhelm-1.5.0 → devhelm-1.6.0}/Makefile +0 -0
  15. {devhelm-1.5.0 → devhelm-1.6.0}/README.md +0 -0
  16. {devhelm-1.5.0 → devhelm-1.6.0}/docs/openapi/monitoring-api.json +0 -0
  17. {devhelm-1.5.0 → devhelm-1.6.0}/scripts/emit_response_enums.py +0 -0
  18. {devhelm-1.5.0 → devhelm-1.6.0}/scripts/regen-from.sh +0 -0
  19. {devhelm-1.5.0 → devhelm-1.6.0}/scripts/release.sh +0 -0
  20. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/__init__.py +0 -0
  21. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_enums.py +0 -0
  22. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_errors.py +0 -0
  23. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_http.py +0 -0
  24. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_pagination.py +0 -0
  25. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/_validation.py +0 -0
  26. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/client.py +0 -0
  27. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/py.typed +0 -0
  28. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/__init__.py +0 -0
  29. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/alert_channels.py +0 -0
  30. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/api_keys.py +0 -0
  31. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/dependencies.py +0 -0
  32. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/deploy_lock.py +0 -0
  33. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/environments.py +0 -0
  34. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/forensics.py +0 -0
  35. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/incidents.py +0 -0
  36. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/maintenance_windows.py +0 -0
  37. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/monitors.py +0 -0
  38. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/notification_policies.py +0 -0
  39. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/resource_groups.py +0 -0
  40. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/secrets.py +0 -0
  41. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/services.py +0 -0
  42. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/status.py +0 -0
  43. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/status_pages.py +0 -0
  44. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/tags.py +0 -0
  45. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/resources/webhooks.py +0 -0
  46. {devhelm-1.5.0 → devhelm-1.6.0}/src/devhelm/types.py +0 -0
  47. {devhelm-1.5.0 → devhelm-1.6.0}/tests/__init__.py +0 -0
  48. {devhelm-1.5.0 → devhelm-1.6.0}/tests/run_sdk.py +0 -0
  49. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_api_keys.py +0 -0
  50. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_client.py +0 -0
  51. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_dependencies.py +0 -0
  52. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_errors.py +0 -0
  53. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_http.py +0 -0
  54. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_maintenance_windows.py +0 -0
  55. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_negative_validation.py +0 -0
  56. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_services.py +0 -0
  57. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_spec_parity.py +0 -0
  58. {devhelm-1.5.0 → devhelm-1.6.0}/tests/test_typing.py +0 -0
  59. {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.5.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.5.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,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 = " model_config = ConfigDict(extra='forbid', populate_by_name=True)"
54
- CONFIG_LINE_TOLERANT = " model_config = ConfigDict(extra='ignore', 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
+ )
55
66
 
56
67
 
57
68
  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")):
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 bool(
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 = CONFIG_LINE_TOLERANT if _is_response_shape(class_name) else CONFIG_LINE_STRICT
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: 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