From 7edecabf3a0fcfa7f5bad7cd24c7bc9c6046d735 Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 11 May 2026 18:33:25 +0200 Subject: [PATCH] feat: spec-level Postel's-Law tolerant readers + codegen-stable enum aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline rewrite. `scripts/typegen.sh` now runs through the shared `@devhelm/openapi-tools` preprocessor (which calls `relaxResponseEnumsInSpec`) before `datamodel-codegen` sees the spec. Response-DTO multi-value enum fields decode as plain `str` — so `MonitorDto.type`, `IncidentDto.status`, etc. accept future API values added after this SDK version was built. Request DTOs keep their StrEnum classes and `Field(discriminator=…)` wiring for strict authoring-time validation. `scripts/emit_response_enums.py` (new) emits `_enums.py` from the *un-relaxed* spec, with one `Literal[...]` alias per `(SchemaName, propertyName)` pair (164 aliases). Names are stable across spec evolution because they don't depend on `datamodel-codegen`'s suffixed names (`Status1`…`Status15`, `Type1`…`Type6`) which shifted on every spec change. `types.py` now imports every public enum alias (`IncidentStatus`, `MonitorType`, `CustomDomainStatus`, `ConfirmationPolicyType`, …) from `_enums.py` so the public API stays stable while the runtime stays Postel-tolerant. Hand-coded `ConfirmationPolicyType` literal removed in favour of the auto-generated alias (caught a wrong value in the process — the literal is `"multi_region"`, not `"confirmation"`). Negative tests around response-DTO enum rejection are flipped to assert acceptance (tolerance). Request-DTO negative tests stay strict. See `mini/runbooks/api-contract.md` § 3.2 for the cross-surface design and `_enums.py` rationale. Coverage: 742 / 742 tests pass; ruff + mypy clean (26 source files). Co-authored-by: Cursor --- docs/openapi/monitoring-api.json | 2 +- scripts/emit_response_enums.py | 166 +++++++++ scripts/typegen.sh | 10 + src/devhelm/__init__.py | 2 + src/devhelm/_enums.py | 569 ++++++++++++++++++++++++++++++ src/devhelm/_generated.py | 450 ++++++----------------- src/devhelm/types.py | 310 +++++++--------- tests/test_negative_validation.py | 78 ++-- tests/test_schemas.py | 4 +- uv.lock | 2 +- 10 files changed, 1020 insertions(+), 573 deletions(-) create mode 100644 scripts/emit_response_enums.py create mode 100644 src/devhelm/_enums.py diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index ce1a576..e5771a6 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -5187,7 +5187,7 @@ "Invites" ], "summary": "Resend invite", - "operationId": "resend", + "operationId": "resend_1", "parameters": [ { "name": "inviteId", diff --git a/scripts/emit_response_enums.py b/scripts/emit_response_enums.py new file mode 100644 index 0000000..577859a --- /dev/null +++ b/scripts/emit_response_enums.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Generate ``src/devhelm/_enums.py`` from the *un-relaxed* OpenAPI spec. + +Why this exists +=============== + +Under the spec-level Postel's-Law relaxation +(see ``mini/runbooks/api-contract.md`` § 3 and the design notes at +the top of ``../mini/packages/openapi-tools/src/preprocess.ts``) +multi-value enums on response-shape DTOs are dropped from the +preprocessed spec before code generation. That makes the runtime +behaviour tolerant — ``MonitorDto.type`` decodes any string, including +new wire values added by the API after the SDK was built — but it also +removes the StrEnum classes that ``types.py`` historically re-exported +under public-facing names like ``IncidentStatus``, +``CustomDomainStatus``, and ``MonitorDtoType``. + +We *also* don't want to depend on ``datamodel-codegen``'s numbered +suffixes (``Status1``…``Status15``, ``Type1``…``Type6``) for the +remaining request-side enums — those numbers shift whenever the spec +gains or loses an enum, which would force a churn of hand-written +imports in ``types.py`` on every schema evolution. + +This script eliminates both problems by emitting one ``Literal[...]`` +alias per ``(schemaName, propertyName)`` pair for every named +multi-value enum in the **un-relaxed** spec. The alias name is +```` so it is stable across spec evolution +and independent of codegen numbering. ``types.py`` imports everything +from the resulting ``_enums.py`` and re-exports under the SDK's +public-facing aliases (``IncidentStatus``, ``MonitorType``, …). + +Sequencing matters: the script is invoked from ``scripts/typegen.sh`` +*after* datamodel-codegen has consumed the preprocessed spec, but it +reads the original (un-relaxed) spec directly so multi-value enums +survive even on response DTOs. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +SPEC_PATH = ROOT / "docs" / "openapi" / "monitoring-api.json" +OUTPUT = ROOT / "src" / "devhelm" / "_enums.py" + + +def pascal_property(name: str) -> str: + parts = re.split(r"[_\-]", name) + return "".join(p[:1].upper() + p[1:] for p in parts if p) + + +def collect_named_enums( + spec: dict[str, Any], +) -> dict[str, list[str]]: + """Return ``{: [values]}`` for every + multi-value enum on any named schema's property — request, response, + or anywhere in between. Naming is uniform across request and + response shapes so consumers can reference a stable alias regardless + of which side a value travels on. + """ + schemas = (spec.get("components") or {}).get("schemas") or {} + out: dict[str, list[str]] = {} + + def visit(schema_name: str, properties: dict[str, Any] | None) -> None: + if not properties: + return + for prop_name, prop in properties.items(): + if not isinstance(prop, dict): + continue + enum = prop.get("enum") + # Emit length-1 enums too — those are discriminator tags + # installed by `inlineDiscriminatorSubtypesWithInfo` and the + # only way to source the canonical value from the spec + # rather than hand-coding it. Keeps types.py free of magic + # strings (e.g. ConfirmationPolicy.type = "multi_region"). + if ( + isinstance(enum, list) + and len(enum) >= 1 + and all(isinstance(v, str) for v in enum) + ): + alias = schema_name + pascal_property(prop_name) + out[alias] = list(enum) + items = prop.get("items") + if isinstance(items, dict): + items_enum = items.get("enum") + if ( + isinstance(items_enum, list) + and len(items_enum) >= 1 + and all(isinstance(v, str) for v in items_enum) + ): + alias = schema_name + pascal_property(prop_name) + "Item" + out[alias] = list(items_enum) + + for name, schema in schemas.items(): + # Skip anonymous / lowercase schemas — those are inline types + # that don't have a stable name and we don't surface them. + if not name or not name[0].isupper(): + continue + if not isinstance(schema, dict): + continue + visit(name, schema.get("properties")) + for member in schema.get("allOf") or []: + if isinstance(member, dict): + visit(name, member.get("properties")) + + return out + + +def render(aliases: dict[str, list[str]]) -> str: + lines = [ + '"""Auto-generated enum literal aliases (uniform request + response).', + "", + "DO NOT EDIT — regenerated on every ``typegen.sh`` run from the", + "*un-relaxed* OpenAPI spec. See ``scripts/emit_response_enums.py``", + "and ``mini/runbooks/api-contract.md`` § 3 for the design.", + "", + "Each alias is a ``typing.Literal[...]`` of the wire-format values", + "the API currently accepts (request-side) or emits (response-side)", + "for the named ```` field. Naming is", + "stable: it does not depend on ``datamodel-codegen``'s suffixed", + "names (``Status1``, ``Type5``…) which shift on every spec change.", + "", + "Response-DTO fields decode to plain ``str`` in ``_generated.py``", + "(Postel-tolerant on receive). Request-DTO fields keep strict", + "validation through the corresponding ``StrEnum`` in", + "``_generated.py`` (strict on send). These aliases give SDK", + "callers a single canonical name they can annotate against in", + "either direction.", + '"""', + "", + "from __future__ import annotations", + "", + "from typing import Literal", + "", + ] + for alias in sorted(aliases): + values = aliases[alias] + rendered = ", ".join(f'"{v}"' for v in values) + lines.append(f"{alias} = Literal[{rendered}]") + lines.append("") + lines.append("__all__ = [") + for alias in sorted(aliases): + lines.append(f' "{alias}",') + lines.append("]") + lines.append("") + return "\n".join(lines) + + +def main() -> int: + if not SPEC_PATH.exists(): + print(f"error: spec not found at {SPEC_PATH}", file=sys.stderr) + return 1 + spec = json.loads(SPEC_PATH.read_text()) + aliases = collect_named_enums(spec) + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(render(aliases)) + print(f"emit_enums: wrote {len(aliases)} aliases → {OUTPUT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/typegen.sh b/scripts/typegen.sh index 1e28164..9c2fdcb 100755 --- a/scripts/typegen.sh +++ b/scripts/typegen.sh @@ -81,5 +81,15 @@ uv run python "$SCRIPT_DIR/inject_strict_config.py" "$OUTPUT" # child env (e.g. inherited VIRTUAL_ENV from a pytest parent). uv run ruff format --quiet "$OUTPUT" || echo "warning: ruff format skipped" >&2 +# Emit Literal aliases for every named multi-value enum from the +# *un-relaxed* spec. Provides ``types.py`` with codegen-stable public +# enum names (``IncidentStatus``, ``MonitorType`` …) that don't depend +# on ``datamodel-codegen``'s numbered suffixes. See +# ``mini/runbooks/api-contract.md`` § 3 and the script docstring. +echo "=> Emitting enum literal aliases..." +uv run python "$SCRIPT_DIR/emit_response_enums.py" +uv run ruff format --quiet "$ROOT_DIR/src/devhelm/_enums.py" \ + || echo "warning: ruff format on _enums.py skipped" >&2 + rm -f "$PREPROCESSED" echo "=> Generated: $OUTPUT" diff --git a/src/devhelm/__init__.py b/src/devhelm/__init__.py index b0bd5ff..515c2eb 100644 --- a/src/devhelm/__init__.py +++ b/src/devhelm/__init__.py @@ -120,6 +120,7 @@ StatusPageUpdateStatus, TagDto, TestChannelResult, + Tier, TriggerRuleSeverity, TriggerRuleType, UpdateAlertChannelRequest, @@ -289,6 +290,7 @@ "StatusPageOverallStatus", "StatusPageUpdateCreatedBy", "StatusPageUpdateStatus", + "Tier", "TriggerRuleSeverity", "TriggerRuleType", "UpdateAssertionSeverity", diff --git a/src/devhelm/_enums.py b/src/devhelm/_enums.py new file mode 100644 index 0000000..e6613fd --- /dev/null +++ b/src/devhelm/_enums.py @@ -0,0 +1,569 @@ +"""Auto-generated enum literal aliases (uniform request + response). + +DO NOT EDIT — regenerated on every ``typegen.sh`` run from the +*un-relaxed* OpenAPI spec. See ``scripts/emit_response_enums.py`` +and ``mini/runbooks/api-contract.md`` § 3 for the design. + +Each alias is a ``typing.Literal[...]`` of the wire-format values +the API currently accepts (request-side) or emits (response-side) +for the named ```` field. Naming is +stable: it does not depend on ``datamodel-codegen``'s suffixed +names (``Status1``, ``Type5``…) which shift on every spec change. + +Response-DTO fields decode to plain ``str`` in ``_generated.py`` +(Postel-tolerant on receive). Request-DTO fields keep strict +validation through the corresponding ``StrEnum`` in +``_generated.py`` (strict on send). These aliases give SDK +callers a single canonical name they can annotate against in +either direction. +""" + +from __future__ import annotations + +from typing import Literal + +AddIncidentUpdateRequestNewStatus = Literal[ + "WATCHING", "TRIGGERED", "CONFIRMED", "RESOLVED" +] +AffectedComponentStatus = Literal[ + "OPERATIONAL", + "DEGRADED_PERFORMANCE", + "PARTIAL_OUTAGE", + "MAJOR_OUTAGE", + "UNDER_MAINTENANCE", +] +AlertChannelDtoChannelType = Literal[ + "email", "webhook", "slack", "pagerduty", "opsgenie", "teams", "discord" +] +AlertChannelDtoManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +AlertDeliveryDtoEventType = Literal[ + "INCIDENT_CREATED", "INCIDENT_RESOLVED", "INCIDENT_REOPENED" +] +AlertDeliveryDtoStatus = Literal[ + "PENDING", "DELIVERED", "RETRY_PENDING", "FAILED", "CANCELLED" +] +ApiKeyAuthConfigType = Literal["api_key"] +AssertionResultDtoSeverity = Literal["fail", "warn"] +AssertionTestResultDtoAssertionType = Literal[ + "status_code", + "response_time", + "body_contains", + "json_path", + "header_value", + "regex_body", + "dns_resolves", + "dns_response_time", + "dns_expected_ips", + "dns_expected_cname", + "dns_record_contains", + "dns_record_equals", + "dns_txt_contains", + "dns_min_answers", + "dns_max_answers", + "dns_response_time_warn", + "dns_ttl_low", + "dns_ttl_high", + "mcp_connects", + "mcp_response_time", + "mcp_has_capability", + "mcp_tool_available", + "mcp_min_tools", + "mcp_protocol_version", + "mcp_response_time_warn", + "mcp_tool_count_changed", + "ssl_expiry", + "response_size", + "redirect_count", + "redirect_target", + "response_time_warn", + "tcp_connects", + "tcp_response_time", + "tcp_response_time_warn", + "icmp_reachable", + "icmp_response_time", + "icmp_response_time_warn", + "icmp_packet_loss", + "heartbeat_received", + "heartbeat_max_interval", + "heartbeat_interval_drift", + "heartbeat_payload_contains", +] +AssertionTestResultDtoSeverity = Literal["fail", "warn"] +BasicAuthConfigType = Literal["basic"] +BearerAuthConfigType = Literal["bearer"] +BodyContainsAssertionType = Literal["body_contains"] +BulkMonitorActionRequestAction = Literal[ + "PAUSE", "RESUME", "DELETE", "ADD_TAG", "REMOVE_TAG" +] +ChangeRoleRequestOrgRole = Literal["OWNER", "ADMIN", "MEMBER"] +ChangeStatusRequestStatus = Literal[ + "INVITED", "ACTIVE", "SUSPENDED", "LEFT", "REMOVED", "DECLINED" +] +ConfirmationPolicyType = Literal["multi_region"] +CreateAlertChannelRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +CreateAssertionRequestSeverity = Literal["fail", "warn"] +CreateInviteRequestRoleOffered = Literal["OWNER", "ADMIN", "MEMBER"] +CreateManualIncidentRequestSeverity = Literal["DOWN", "DEGRADED", "MAINTENANCE"] +CreateMonitorRequestManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +CreateMonitorRequestType = Literal[ + "HTTP", "DNS", "MCP_SERVER", "TCP", "ICMP", "HEARTBEAT" +] +CreateResourceGroupRequestHealthThresholdType = Literal["COUNT", "PERCENTAGE"] +CreateResourceGroupRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +CreateStatusPageComponentRequestType = Literal["MONITOR", "GROUP", "STATIC"] +CreateStatusPageIncidentRequestImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +CreateStatusPageIncidentRequestStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +CreateStatusPageIncidentUpdateRequestStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +CreateStatusPageRequestIncidentMode = Literal["MANUAL", "REVIEW", "AUTOMATIC"] +CreateStatusPageRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +CreateStatusPageRequestVisibility = Literal["PUBLIC", "PASSWORD", "IP_RESTRICTED"] +CreateWebhookEndpointRequestSubscribedEventsItem = Literal[ + "monitor.created", + "monitor.updated", + "monitor.deleted", + "incident.created", + "incident.resolved", + "incident.reopened", + "service.status_changed", + "service.component_changed", + "service.incident_created", + "service.incident_updated", + "service.incident_resolved", +] +DayIncidentImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +DayIncidentStatus = Literal["INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED"] +DiscordChannelConfigChannelType = Literal["discord"] +DnsCheckType = Literal["dns"] +DnsExpectedCnameAssertionType = Literal["dns_expected_cname"] +DnsExpectedIpsAssertionType = Literal["dns_expected_ips"] +DnsMaxAnswersAssertionType = Literal["dns_max_answers"] +DnsMinAnswersAssertionType = Literal["dns_min_answers"] +DnsMonitorConfigRecordTypesItem = Literal[ + "A", "AAAA", "CNAME", "MX", "NS", "TXT", "SRV", "SOA", "CAA", "PTR" +] +DnsRecordContainsAssertionType = Literal["dns_record_contains"] +DnsRecordEqualsAssertionType = Literal["dns_record_equals"] +DnsResolvesAssertionType = Literal["dns_resolves"] +DnsResponseTimeAssertionType = Literal["dns_response_time"] +DnsResponseTimeWarnAssertionType = Literal["dns_response_time_warn"] +DnsTtlHighAssertionType = Literal["dns_ttl_high"] +DnsTtlLowAssertionType = Literal["dns_ttl_low"] +DnsTxtContainsAssertionType = Literal["dns_txt_contains"] +EmailChannelConfigChannelType = Literal["email"] +HeaderAuthConfigType = Literal["header"] +HeaderValueAssertionOperator = Literal[ + "equals", "contains", "less_than", "greater_than", "matches", "range" +] +HeaderValueAssertionType = Literal["header_value"] +HeartbeatIntervalDriftAssertionType = Literal["heartbeat_interval_drift"] +HeartbeatMaxIntervalAssertionType = Literal["heartbeat_max_interval"] +HeartbeatPayloadContainsAssertionType = Literal["heartbeat_payload_contains"] +HeartbeatReceivedAssertionType = Literal["heartbeat_received"] +HttpCheckType = Literal["http"] +HttpMonitorConfigMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] +IcmpCheckType = Literal["icmp"] +IcmpPacketLossAssertionType = Literal["icmp_packet_loss"] +IcmpReachableAssertionType = Literal["icmp_reachable"] +IcmpResponseTimeAssertionType = Literal["icmp_response_time"] +IcmpResponseTimeWarnAssertionType = Literal["icmp_response_time_warn"] +IncidentDtoResolutionReason = Literal["MANUAL", "AUTO_RECOVERED", "AUTO_RESOLVED"] +IncidentDtoSeverity = Literal["DOWN", "DEGRADED", "MAINTENANCE"] +IncidentDtoSource = Literal[ + "AUTOMATIC", "MANUAL", "MONITORS", "STATUS_DATA", "RESOURCE_GROUP" +] +IncidentDtoStatus = Literal["WATCHING", "TRIGGERED", "CONFIRMED", "RESOLVED"] +IncidentFilterParamsSeverity = Literal["DOWN", "DEGRADED", "MAINTENANCE"] +IncidentFilterParamsSource = Literal[ + "AUTOMATIC", "MANUAL", "MONITORS", "STATUS_DATA", "RESOURCE_GROUP" +] +IncidentFilterParamsStatus = Literal["WATCHING", "TRIGGERED", "CONFIRMED", "RESOLVED"] +IncidentUpdateDtoCreatedBy = Literal["SYSTEM", "USER"] +IncidentUpdateDtoNewStatus = Literal["WATCHING", "TRIGGERED", "CONFIRMED", "RESOLVED"] +IncidentUpdateDtoOldStatus = Literal["WATCHING", "TRIGGERED", "CONFIRMED", "RESOLVED"] +IntegrationDtoTierAvailability = Literal[ + "FREE", "STARTER", "PRO", "TEAM", "BUSINESS", "ENTERPRISE" +] +InviteDtoRoleOffered = Literal["OWNER", "ADMIN", "MEMBER"] +JsonPathAssertionOperator = Literal[ + "equals", "contains", "less_than", "greater_than", "matches", "range" +] +JsonPathAssertionType = Literal["json_path"] +LinkedStatusPageIncidentDtoImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +LinkedStatusPageIncidentDtoStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +MatchRuleType = Literal[ + "severity_gte", + "monitor_id_in", + "region_in", + "incident_status", + "monitor_type_in", + "service_id_in", + "resource_group_id_in", + "component_name_in", + "monitor_tag_in", +] +McpConnectsAssertionType = Literal["mcp_connects"] +McpHasCapabilityAssertionType = Literal["mcp_has_capability"] +McpMinToolsAssertionType = Literal["mcp_min_tools"] +McpProtocolVersionAssertionType = Literal["mcp_protocol_version"] +McpResponseTimeAssertionType = Literal["mcp_response_time"] +McpResponseTimeWarnAssertionType = Literal["mcp_response_time_warn"] +McpServerCheckType = Literal["mcp_server"] +McpToolAvailableAssertionType = Literal["mcp_tool_available"] +McpToolCountChangedAssertionType = Literal["mcp_tool_count_changed"] +MemberDtoOrgRole = Literal["OWNER", "ADMIN", "MEMBER"] +MemberDtoStatus = Literal[ + "INVITED", "ACTIVE", "SUSPENDED", "LEFT", "REMOVED", "DECLINED" +] +MemberRoleChangedMetadataKind = Literal["member_role_changed"] +MemberRoleChangedMetadataNewRole = Literal["OWNER", "ADMIN", "MEMBER"] +MemberRoleChangedMetadataOldRole = Literal["OWNER", "ADMIN", "MEMBER"] +MonitorAssertionDtoAssertionType = Literal[ + "status_code", + "response_time", + "body_contains", + "json_path", + "header_value", + "regex_body", + "dns_resolves", + "dns_response_time", + "dns_expected_ips", + "dns_expected_cname", + "dns_record_contains", + "dns_record_equals", + "dns_txt_contains", + "dns_min_answers", + "dns_max_answers", + "dns_response_time_warn", + "dns_ttl_low", + "dns_ttl_high", + "mcp_connects", + "mcp_response_time", + "mcp_has_capability", + "mcp_tool_available", + "mcp_min_tools", + "mcp_protocol_version", + "mcp_response_time_warn", + "mcp_tool_count_changed", + "ssl_expiry", + "response_size", + "redirect_count", + "redirect_target", + "response_time_warn", + "tcp_connects", + "tcp_response_time", + "tcp_response_time_warn", + "icmp_reachable", + "icmp_response_time", + "icmp_response_time_warn", + "icmp_packet_loss", + "heartbeat_received", + "heartbeat_max_interval", + "heartbeat_interval_drift", + "heartbeat_payload_contains", +] +MonitorAssertionDtoSeverity = Literal["fail", "warn"] +MonitorAuthDtoAuthType = Literal["bearer", "basic", "header", "api_key"] +MonitorDtoCurrentStatus = Literal["up", "degraded", "down", "paused", "unknown"] +MonitorDtoManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +MonitorDtoType = Literal["HTTP", "DNS", "MCP_SERVER", "TCP", "ICMP", "HEARTBEAT"] +MonitorTestRequestType = Literal[ + "HTTP", "DNS", "MCP_SERVER", "TCP", "ICMP", "HEARTBEAT" +] +MonitorVersionDtoChangedVia = Literal["API", "DASHBOARD", "CLI", "TERRAFORM"] +NotificationDispatchDtoCompletionReason = Literal["EXHAUSTED", "RESOLVED", "NO_STEPS"] +NotificationDispatchDtoStatus = Literal[ + "PENDING", "DISPATCHING", "DELIVERED", "ESCALATING", "ACKNOWLEDGED", "COMPLETED" +] +OpsGenieChannelConfigChannelType = Literal["opsgenie"] +PagerDutyChannelConfigChannelType = Literal["pagerduty"] +PlanInfoTier = Literal["FREE", "STARTER", "PRO", "TEAM", "BUSINESS", "ENTERPRISE"] +PublishStatusPageIncidentRequestImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +PublishStatusPageIncidentRequestStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +RedirectCountAssertionType = Literal["redirect_count"] +RedirectTargetAssertionOperator = Literal[ + "equals", "contains", "less_than", "greater_than", "matches", "range" +] +RedirectTargetAssertionType = Literal["redirect_target"] +RegexBodyAssertionType = Literal["regex_body"] +ResourceGroupDtoHealthThresholdType = Literal["COUNT", "PERCENTAGE"] +ResourceGroupDtoManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +ResourceGroupHealthDtoStatus = Literal["operational", "maintenance", "degraded", "down"] +ResourceGroupHealthDtoThresholdStatus = Literal["healthy", "degraded", "down"] +ResourceGroupMemberDtoStatus = Literal["operational", "maintenance", "degraded", "down"] +ResponseSizeAssertionType = Literal["response_size"] +ResponseTimeAssertionType = Literal["response_time"] +ResponseTimeWarnAssertionType = Literal["response_time_warn"] +ResultSummaryDtoCurrentStatus = Literal["up", "degraded", "down", "paused", "unknown"] +ServiceSubscriptionDtoAlertSensitivity = Literal["ALL", "INCIDENTS_ONLY", "MAJOR_ONLY"] +SlackChannelConfigChannelType = Literal["slack"] +SslExpiryAssertionType = Literal["ssl_expiry"] +StateTransitionDetailsSource = Literal["pipeline", "public-api"] +StatusCodeAssertionOperator = Literal[ + "equals", "contains", "less_than", "greater_than", "matches", "range" +] +StatusCodeAssertionType = Literal["status_code"] +StatusPageComponentDtoCurrentStatus = Literal[ + "OPERATIONAL", + "DEGRADED_PERFORMANCE", + "PARTIAL_OUTAGE", + "MAJOR_OUTAGE", + "UNDER_MAINTENANCE", +] +StatusPageComponentDtoType = Literal["MONITOR", "GROUP", "STATIC"] +StatusPageCustomDomainDtoStatus = Literal[ + "PENDING_VERIFICATION", + "VERIFICATION_FAILED", + "VERIFIED", + "SSL_PENDING", + "ACTIVE", + "FAILED", + "REMOVED", +] +StatusPageCustomDomainDtoVerificationMethod = Literal["CNAME", "TXT"] +StatusPageDtoIncidentMode = Literal["MANUAL", "REVIEW", "AUTOMATIC"] +StatusPageDtoManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +StatusPageDtoOverallStatus = Literal[ + "OPERATIONAL", + "DEGRADED_PERFORMANCE", + "PARTIAL_OUTAGE", + "MAJOR_OUTAGE", + "UNDER_MAINTENANCE", +] +StatusPageDtoVisibility = Literal["PUBLIC", "PASSWORD", "IP_RESTRICTED"] +StatusPageIncidentComponentDtoComponentStatus = Literal[ + "OPERATIONAL", + "DEGRADED_PERFORMANCE", + "PARTIAL_OUTAGE", + "MAJOR_OUTAGE", + "UNDER_MAINTENANCE", +] +StatusPageIncidentDtoImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +StatusPageIncidentDtoStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +StatusPageIncidentUpdateDtoCreatedBy = Literal["USER", "SYSTEM"] +StatusPageIncidentUpdateDtoStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +TcpCheckType = Literal["tcp"] +TcpConnectsAssertionType = Literal["tcp_connects"] +TcpResponseTimeAssertionType = Literal["tcp_response_time"] +TcpResponseTimeWarnAssertionType = Literal["tcp_response_time_warn"] +TeamsChannelConfigChannelType = Literal["teams"] +TriggerRuleAggregationType = Literal["all_exceed", "average", "p95", "max"] +TriggerRuleScope = Literal["per_region", "any_region"] +TriggerRuleSeverity = Literal["down", "degraded"] +TriggerRuleType = Literal["consecutive_failures", "failures_in_window", "response_time"] +UpdateAlertChannelRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +UpdateAssertionRequestSeverity = Literal["fail", "warn"] +UpdateMonitorRequestManagedBy = Literal["DASHBOARD", "CLI", "TERRAFORM", "MCP", "API"] +UpdateResourceGroupRequestHealthThresholdType = Literal["COUNT", "PERCENTAGE"] +UpdateResourceGroupRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +UpdateStatusPageIncidentRequestImpact = Literal["NONE", "MINOR", "MAJOR", "CRITICAL"] +UpdateStatusPageIncidentRequestStatus = Literal[ + "INVESTIGATING", "IDENTIFIED", "MONITORING", "RESOLVED" +] +UpdateStatusPageRequestIncidentMode = Literal["MANUAL", "REVIEW", "AUTOMATIC"] +UpdateStatusPageRequestManagedBy = Literal[ + "DASHBOARD", "CLI", "TERRAFORM", "MCP", "API" +] +UpdateStatusPageRequestVisibility = Literal["PUBLIC", "PASSWORD", "IP_RESTRICTED"] +UpdateWebhookEndpointRequestSubscribedEventsItem = Literal[ + "monitor.created", + "monitor.updated", + "monitor.deleted", + "incident.created", + "incident.resolved", + "incident.reopened", + "service.status_changed", + "service.component_changed", + "service.incident_created", + "service.incident_updated", + "service.incident_resolved", +] +WebhookChannelConfigChannelType = Literal["webhook"] + +__all__ = [ + "AddIncidentUpdateRequestNewStatus", + "AffectedComponentStatus", + "AlertChannelDtoChannelType", + "AlertChannelDtoManagedBy", + "AlertDeliveryDtoEventType", + "AlertDeliveryDtoStatus", + "ApiKeyAuthConfigType", + "AssertionResultDtoSeverity", + "AssertionTestResultDtoAssertionType", + "AssertionTestResultDtoSeverity", + "BasicAuthConfigType", + "BearerAuthConfigType", + "BodyContainsAssertionType", + "BulkMonitorActionRequestAction", + "ChangeRoleRequestOrgRole", + "ChangeStatusRequestStatus", + "ConfirmationPolicyType", + "CreateAlertChannelRequestManagedBy", + "CreateAssertionRequestSeverity", + "CreateInviteRequestRoleOffered", + "CreateManualIncidentRequestSeverity", + "CreateMonitorRequestManagedBy", + "CreateMonitorRequestType", + "CreateResourceGroupRequestHealthThresholdType", + "CreateResourceGroupRequestManagedBy", + "CreateStatusPageComponentRequestType", + "CreateStatusPageIncidentRequestImpact", + "CreateStatusPageIncidentRequestStatus", + "CreateStatusPageIncidentUpdateRequestStatus", + "CreateStatusPageRequestIncidentMode", + "CreateStatusPageRequestManagedBy", + "CreateStatusPageRequestVisibility", + "CreateWebhookEndpointRequestSubscribedEventsItem", + "DayIncidentImpact", + "DayIncidentStatus", + "DiscordChannelConfigChannelType", + "DnsCheckType", + "DnsExpectedCnameAssertionType", + "DnsExpectedIpsAssertionType", + "DnsMaxAnswersAssertionType", + "DnsMinAnswersAssertionType", + "DnsMonitorConfigRecordTypesItem", + "DnsRecordContainsAssertionType", + "DnsRecordEqualsAssertionType", + "DnsResolvesAssertionType", + "DnsResponseTimeAssertionType", + "DnsResponseTimeWarnAssertionType", + "DnsTtlHighAssertionType", + "DnsTtlLowAssertionType", + "DnsTxtContainsAssertionType", + "EmailChannelConfigChannelType", + "HeaderAuthConfigType", + "HeaderValueAssertionOperator", + "HeaderValueAssertionType", + "HeartbeatIntervalDriftAssertionType", + "HeartbeatMaxIntervalAssertionType", + "HeartbeatPayloadContainsAssertionType", + "HeartbeatReceivedAssertionType", + "HttpCheckType", + "HttpMonitorConfigMethod", + "IcmpCheckType", + "IcmpPacketLossAssertionType", + "IcmpReachableAssertionType", + "IcmpResponseTimeAssertionType", + "IcmpResponseTimeWarnAssertionType", + "IncidentDtoResolutionReason", + "IncidentDtoSeverity", + "IncidentDtoSource", + "IncidentDtoStatus", + "IncidentFilterParamsSeverity", + "IncidentFilterParamsSource", + "IncidentFilterParamsStatus", + "IncidentUpdateDtoCreatedBy", + "IncidentUpdateDtoNewStatus", + "IncidentUpdateDtoOldStatus", + "IntegrationDtoTierAvailability", + "InviteDtoRoleOffered", + "JsonPathAssertionOperator", + "JsonPathAssertionType", + "LinkedStatusPageIncidentDtoImpact", + "LinkedStatusPageIncidentDtoStatus", + "MatchRuleType", + "McpConnectsAssertionType", + "McpHasCapabilityAssertionType", + "McpMinToolsAssertionType", + "McpProtocolVersionAssertionType", + "McpResponseTimeAssertionType", + "McpResponseTimeWarnAssertionType", + "McpServerCheckType", + "McpToolAvailableAssertionType", + "McpToolCountChangedAssertionType", + "MemberDtoOrgRole", + "MemberDtoStatus", + "MemberRoleChangedMetadataKind", + "MemberRoleChangedMetadataNewRole", + "MemberRoleChangedMetadataOldRole", + "MonitorAssertionDtoAssertionType", + "MonitorAssertionDtoSeverity", + "MonitorAuthDtoAuthType", + "MonitorDtoCurrentStatus", + "MonitorDtoManagedBy", + "MonitorDtoType", + "MonitorTestRequestType", + "MonitorVersionDtoChangedVia", + "NotificationDispatchDtoCompletionReason", + "NotificationDispatchDtoStatus", + "OpsGenieChannelConfigChannelType", + "PagerDutyChannelConfigChannelType", + "PlanInfoTier", + "PublishStatusPageIncidentRequestImpact", + "PublishStatusPageIncidentRequestStatus", + "RedirectCountAssertionType", + "RedirectTargetAssertionOperator", + "RedirectTargetAssertionType", + "RegexBodyAssertionType", + "ResourceGroupDtoHealthThresholdType", + "ResourceGroupDtoManagedBy", + "ResourceGroupHealthDtoStatus", + "ResourceGroupHealthDtoThresholdStatus", + "ResourceGroupMemberDtoStatus", + "ResponseSizeAssertionType", + "ResponseTimeAssertionType", + "ResponseTimeWarnAssertionType", + "ResultSummaryDtoCurrentStatus", + "ServiceSubscriptionDtoAlertSensitivity", + "SlackChannelConfigChannelType", + "SslExpiryAssertionType", + "StateTransitionDetailsSource", + "StatusCodeAssertionOperator", + "StatusCodeAssertionType", + "StatusPageComponentDtoCurrentStatus", + "StatusPageComponentDtoType", + "StatusPageCustomDomainDtoStatus", + "StatusPageCustomDomainDtoVerificationMethod", + "StatusPageDtoIncidentMode", + "StatusPageDtoManagedBy", + "StatusPageDtoOverallStatus", + "StatusPageDtoVisibility", + "StatusPageIncidentComponentDtoComponentStatus", + "StatusPageIncidentDtoImpact", + "StatusPageIncidentDtoStatus", + "StatusPageIncidentUpdateDtoCreatedBy", + "StatusPageIncidentUpdateDtoStatus", + "TcpCheckType", + "TcpConnectsAssertionType", + "TcpResponseTimeAssertionType", + "TcpResponseTimeWarnAssertionType", + "TeamsChannelConfigChannelType", + "TriggerRuleAggregationType", + "TriggerRuleScope", + "TriggerRuleSeverity", + "TriggerRuleType", + "UpdateAlertChannelRequestManagedBy", + "UpdateAssertionRequestSeverity", + "UpdateMonitorRequestManagedBy", + "UpdateResourceGroupRequestHealthThresholdType", + "UpdateResourceGroupRequestManagedBy", + "UpdateStatusPageIncidentRequestImpact", + "UpdateStatusPageIncidentRequestStatus", + "UpdateStatusPageRequestIncidentMode", + "UpdateStatusPageRequestManagedBy", + "UpdateStatusPageRequestVisibility", + "UpdateWebhookEndpointRequestSubscribedEventsItem", + "WebhookChannelConfigChannelType", +] diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index e8d7720..5a1a027 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -145,30 +145,12 @@ class AlertChannelDisplayConfig(BaseModel): ] = None -class ChannelType(StrEnum): - email = "email" - webhook = "webhook" - slack = "slack" - pagerduty = "pagerduty" - opsgenie = "opsgenie" - teams = "teams" - discord = "discord" - - -class ManagedBy(StrEnum): - dashboard = "DASHBOARD" - cli = "CLI" - terraform = "TERRAFORM" - mcp = "MCP" - api = "API" - - class AlertChannelDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: Annotated[UUID, Field(description="Unique alert channel identifier")] name: Annotated[str, Field(description="Human-readable channel name")] channel_type: Annotated[ - ChannelType, + str, Field( alias="channelType", description="Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL)", @@ -195,7 +177,7 @@ class AlertChannelDto(BaseModel): ), ] = None managed_by: Annotated[ - ManagedBy | None, + str | None, Field( alias="managedBy", description="Source that created/owns this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on channels created before this attribution column existed.", @@ -217,20 +199,6 @@ class AlertChannelDto(BaseModel): ] = None -class Status1(StrEnum): - pending = "PENDING" - delivered = "DELIVERED" - retry_pending = "RETRY_PENDING" - failed = "FAILED" - cancelled = "CANCELLED" - - -class EventType(StrEnum): - incident_created = "INCIDENT_CREATED" - incident_resolved = "INCIDENT_RESOLVED" - incident_reopened = "INCIDENT_REOPENED" - - class AlertDeliveryDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID @@ -256,9 +224,9 @@ class AlertDeliveryDto(BaseModel): description="Alert channel type (e.g. slack, email, webhook)", ), ] - status: Annotated[Status1, Field(description="Current delivery status")] + status: Annotated[str, Field(description="Current delivery status")] event_type: Annotated[ - EventType, + str, Field( alias="eventType", description="Incident lifecycle event that triggered this delivery", @@ -395,16 +363,11 @@ class ApiKeyDto(BaseModel): ] = None -class Severity(StrEnum): - fail = "fail" - warn = "warn" - - class AssertionResultDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) type: Annotated[str, Field(description="Assertion type", examples=["status_code"])] passed: Annotated[bool, Field(description="Whether the assertion passed")] - severity: Annotated[Severity, Field(description="Assertion severity")] + severity: Annotated[str, Field(description="Assertion severity")] message: Annotated[ str | None, Field(description="Human-readable result message") ] = None @@ -416,59 +379,13 @@ class AssertionResultDto(BaseModel): ] = None -class AssertionType(StrEnum): - status_code = "status_code" - response_time = "response_time" - body_contains = "body_contains" - json_path = "json_path" - header_value = "header_value" - regex_body = "regex_body" - dns_resolves = "dns_resolves" - dns_response_time = "dns_response_time" - dns_expected_ips = "dns_expected_ips" - dns_expected_cname = "dns_expected_cname" - dns_record_contains = "dns_record_contains" - dns_record_equals = "dns_record_equals" - dns_txt_contains = "dns_txt_contains" - dns_min_answers = "dns_min_answers" - dns_max_answers = "dns_max_answers" - dns_response_time_warn = "dns_response_time_warn" - dns_ttl_low = "dns_ttl_low" - dns_ttl_high = "dns_ttl_high" - mcp_connects = "mcp_connects" - mcp_response_time = "mcp_response_time" - mcp_has_capability = "mcp_has_capability" - mcp_tool_available = "mcp_tool_available" - mcp_min_tools = "mcp_min_tools" - mcp_protocol_version = "mcp_protocol_version" - mcp_response_time_warn = "mcp_response_time_warn" - mcp_tool_count_changed = "mcp_tool_count_changed" - ssl_expiry = "ssl_expiry" - response_size = "response_size" - redirect_count = "redirect_count" - redirect_target = "redirect_target" - response_time_warn = "response_time_warn" - tcp_connects = "tcp_connects" - tcp_response_time = "tcp_response_time" - tcp_response_time_warn = "tcp_response_time_warn" - icmp_reachable = "icmp_reachable" - icmp_response_time = "icmp_response_time" - icmp_response_time_warn = "icmp_response_time_warn" - icmp_packet_loss = "icmp_packet_loss" - heartbeat_received = "heartbeat_received" - heartbeat_max_interval = "heartbeat_max_interval" - heartbeat_interval_drift = "heartbeat_interval_drift" - heartbeat_payload_contains = "heartbeat_payload_contains" - - class AssertionTestResultDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) assertion_type: Annotated[ - AssertionType, - Field(alias="assertionType", description="Assertion type evaluated"), + str, Field(alias="assertionType", description="Assertion type evaluated") ] passed: Annotated[bool, Field(description="Whether the assertion passed")] - severity: Annotated[Severity, Field(description="Assertion severity: FAIL or WARN")] + severity: Annotated[str, Field(description="Assertion severity: FAIL or WARN")] message: Annotated[str, Field(description="Human-readable result description")] expected: Annotated[str | None, Field(description="Expected value")] = None actual: Annotated[ @@ -543,7 +460,7 @@ class ChangeRoleRequest(BaseModel): ] -class Status2(StrEnum): +class Status1(StrEnum): invited = "INVITED" active = "ACTIVE" suspended = "SUSPENDED" @@ -555,7 +472,7 @@ class Status2(StrEnum): class ChangeStatusRequest(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) status: Annotated[ - Status2, Field(description="New membership status (ACTIVE or SUSPENDED)") + Status1, Field(description="New membership status (ACTIVE or SUSPENDED)") ] @@ -736,6 +653,14 @@ class ConfirmationPolicy(BaseModel): ] +class ManagedBy(StrEnum): + dashboard = "DASHBOARD" + cli = "CLI" + terraform = "TERRAFORM" + mcp = "MCP" + api = "API" + + class CreateApiKeyRequest(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) name: Annotated[ @@ -755,6 +680,11 @@ class CreateApiKeyRequest(BaseModel): ] = None +class Severity(StrEnum): + fail = "fail" + warn = "warn" + + class CreateEnvironmentRequest(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) name: Annotated[ @@ -851,7 +781,7 @@ class CreateMaintenanceWindowRequest(BaseModel): ] = None -class Severity3(StrEnum): +class Severity1(StrEnum): down = "DOWN" degraded = "DEGRADED" maintenance = "MAINTENANCE" @@ -863,7 +793,7 @@ class CreateManualIncidentRequest(BaseModel): str, Field(description="Short summary of the incident", min_length=1) ] severity: Annotated[ - Severity3, + Severity1, Field(description="Incident severity: DOWN, DEGRADED, or MAINTENANCE"), ] monitor_id: Annotated[ @@ -996,7 +926,7 @@ class CreateStatusPageComponentRequest(BaseModel): ] = None -class Status3(StrEnum): +class Status2(StrEnum): investigating = "INVESTIGATING" identified = "IDENTIFIED" monitoring = "MONITORING" @@ -1019,7 +949,7 @@ class CreateStatusPageIncidentRequest(BaseModel): ), ] status: Annotated[ - Status3 | None, Field(description="Initial status (default: INVESTIGATING)") + Status2 | None, Field(description="Initial status (default: INVESTIGATING)") ] = None impact: Annotated[ Impact, Field(description="Impact level: NONE, MINOR, MAJOR, or CRITICAL") @@ -1068,7 +998,7 @@ class CreateStatusPageIncidentRequest(BaseModel): class CreateStatusPageIncidentUpdateRequest(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) status: Annotated[ - Status3, Field(description="Incident status at this point in the timeline") + Status2, Field(description="Incident status at this point in the timeline") ] body: Annotated[str, Field(description="Update body in markdown", min_length=1)] notify_subscribers: Annotated[ @@ -1166,7 +1096,7 @@ class DayIncident(BaseModel): id: Annotated[UUID, Field(description="Status page incident UUID")] title: Annotated[str, Field(description="Incident title")] status: Annotated[ - Status3, + Status2, Field( description="Lifecycle status (investigating, identified, monitoring, resolved, …)" ), @@ -2005,27 +1935,6 @@ class IcmpResponseTimeWarnAssertion(BaseModel): ] -class Source(StrEnum): - automatic = "AUTOMATIC" - manual = "MANUAL" - monitors = "MONITORS" - status_data = "STATUS_DATA" - resource_group = "RESOURCE_GROUP" - - -class Status6(StrEnum): - watching = "WATCHING" - triggered = "TRIGGERED" - confirmed = "CONFIRMED" - resolved = "RESOLVED" - - -class ResolutionReason(StrEnum): - manual = "MANUAL" - auto_recovered = "AUTO_RECOVERED" - auto_resolved = "AUTO_RESOLVED" - - class IncidentDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: Annotated[UUID, Field(description="Unique incident identifier")] @@ -2043,13 +1952,13 @@ class IncidentDto(BaseModel): ), ] source: Annotated[ - Source, Field(description="Incident origin: MONITOR, SERVICE, or MANUAL") + str, Field(description="Incident origin: MONITOR, SERVICE, or MANUAL") ] status: Annotated[ - Status6, Field(description="Current lifecycle status (OPEN, RESOLVED, etc.)") + str, Field(description="Current lifecycle status (OPEN, RESOLVED, etc.)") ] severity: Annotated[ - Severity3, Field(description="Severity level: DOWN, DEGRADED, or MAINTENANCE") + str, Field(description="Severity level: DOWN, DEGRADED, or MAINTENANCE") ] title: Annotated[ str | None, @@ -2124,7 +2033,7 @@ class IncidentDto(BaseModel): str | None, Field(description="Short URL linking to the incident details") ] = None resolution_reason: Annotated[ - ResolutionReason | None, + str | None, Field( alias="resolutionReason", description="How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.)", @@ -2243,16 +2152,31 @@ class IncidentDto(BaseModel): ] = None +class Status5(StrEnum): + watching = "WATCHING" + triggered = "TRIGGERED" + confirmed = "CONFIRMED" + resolved = "RESOLVED" + + +class Source(StrEnum): + automatic = "AUTOMATIC" + manual = "MANUAL" + monitors = "MONITORS" + status_data = "STATUS_DATA" + resource_group = "RESOURCE_GROUP" + + class IncidentFilterParams(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) status: Annotated[ - Status6 | None, + Status5 | None, Field( description="Filter by incident lifecycle status; null returns every status" ), ] = None severity: Annotated[ - Severity3 | None, + Severity1 | None, Field(description="Filter by severity; null returns every severity"), ] = None source: Annotated[ @@ -2350,39 +2274,18 @@ class IncidentsSummaryDto(BaseModel): mttr30d: float | None = None -class OldStatus(StrEnum): - watching = "WATCHING" - triggered = "TRIGGERED" - confirmed = "CONFIRMED" - resolved = "RESOLVED" - - -class CreatedBy(StrEnum): - system = "SYSTEM" - user = "USER" - - class IncidentUpdateDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID incident_id: Annotated[UUID, Field(alias="incidentId")] - old_status: Annotated[OldStatus | None, Field(alias="oldStatus")] = None - new_status: Annotated[NewStatus | None, Field(alias="newStatus")] = None + old_status: Annotated[str | None, Field(alias="oldStatus")] = None + new_status: Annotated[str | None, Field(alias="newStatus")] = None body: str | None = None - created_by: Annotated[CreatedBy | None, Field(alias="createdBy")] = None + created_by: Annotated[str | None, Field(alias="createdBy")] = None notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] created_at: Annotated[AwareDatetime, Field(alias="createdAt")] -class TierAvailability(StrEnum): - free = "FREE" - starter = "STARTER" - pro = "PRO" - team = "TEAM" - business = "BUSINESS" - enterprise = "ENTERPRISE" - - class IntegrationFieldDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) key: str @@ -2403,7 +2306,7 @@ class InviteDto(BaseModel): ] email: Annotated[str, Field(description="Email address the invite was sent to")] role_offered: Annotated[ - RoleOffered, + str, Field( alias="roleOffered", description="Role that will be assigned to the invitee on acceptance", @@ -2467,13 +2370,6 @@ class KeyInfo(BaseModel): ] = None -class Status8(StrEnum): - investigating = "INVESTIGATING" - identified = "IDENTIFIED" - monitoring = "MONITORING" - resolved = "RESOLVED" - - class LinkedStatusPageIncidentDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID @@ -2481,8 +2377,8 @@ class LinkedStatusPageIncidentDto(BaseModel): status_page_name: Annotated[str, Field(alias="statusPageName")] status_page_slug: Annotated[str, Field(alias="statusPageSlug")] title: str - status: Status8 - impact: Impact + status: str + impact: str scheduled: bool published_at: Annotated[AwareDatetime | None, Field(alias="publishedAt")] = None @@ -2722,15 +2618,6 @@ class McpToolCountChangedAssertion(BaseModel): ] -class Status9(StrEnum): - invited = "INVITED" - active = "ACTIVE" - suspended = "SUSPENDED" - left = "LEFT" - removed = "REMOVED" - declined = "DECLINED" - - class MemberDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) user_id: Annotated[ @@ -2741,14 +2628,14 @@ class MemberDto(BaseModel): str | None, Field(description="Member display name; null if not set") ] = None org_role: Annotated[ - OrgRole, + str, Field( alias="orgRole", description="Member role within this organization (OWNER, ADMIN, MEMBER)", ), ] status: Annotated[ - Status9, Field(description="Membership status (ACTIVE, PENDING, SUSPENDED)") + str, Field(description="Membership status (ACTIVE, PENDING, SUSPENDED)") ] created_at: Annotated[ AwareDatetime, @@ -2784,11 +2671,6 @@ class MemberRoleChangedMetadata(BaseModel): ] -class Severity6(StrEnum): - fail = "fail" - warn = "warn" - - class MonitorAuthConfig( RootModel[BearerAuthConfig | BasicAuthConfig | HeaderAuthConfig | ApiKeyAuthConfig] ): @@ -2801,38 +2683,14 @@ class MonitorAuthConfig( ] -class AuthType(StrEnum): - bearer = "bearer" - basic = "basic" - header = "header" - api_key = "api_key" - - class MonitorAuthDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID monitor_id: Annotated[UUID, Field(alias="monitorId")] - auth_type: Annotated[AuthType, Field(alias="authType")] + auth_type: Annotated[str, Field(alias="authType")] config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig -class Type3(StrEnum): - http = "HTTP" - dns = "DNS" - mcp_server = "MCP_SERVER" - tcp = "TCP" - icmp = "ICMP" - heartbeat = "HEARTBEAT" - - -class CurrentStatus(StrEnum): - up = "up" - degraded = "degraded" - down = "down" - paused = "paused" - unknown = "unknown" - - class MonitorReference(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: Annotated[UUID, Field(description="Monitor identifier")] @@ -2868,6 +2726,15 @@ class MonitorsSummaryDto(BaseModel): ] = None +class Type3(StrEnum): + http = "HTTP" + dns = "DNS" + mcp_server = "MCP_SERVER" + tcp = "TCP" + icmp = "ICMP" + heartbeat = "HEARTBEAT" + + class MonitorTestResultDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) passed: bool @@ -2887,13 +2754,6 @@ class MonitorTestResultDto(BaseModel): warnings: list[str] | None = None -class ChangedVia(StrEnum): - api = "API" - dashboard = "DASHBOARD" - cli = "CLI" - terraform = "TERRAFORM" - - class NewTagRequest(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) name: Annotated[str, Field(description="Tag name", max_length=100, min_length=0)] @@ -2906,21 +2766,6 @@ class NewTagRequest(BaseModel): ] = None -class Status10(StrEnum): - pending = "PENDING" - dispatching = "DISPATCHING" - delivered = "DELIVERED" - escalating = "ESCALATING" - acknowledged = "ACKNOWLEDGED" - completed = "COMPLETED" - - -class CompletionReason(StrEnum): - exhausted = "EXHAUSTED" - resolved = "RESOLVED" - no_steps = "NO_STEPS" - - class NotificationDispatchDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: Annotated[UUID, Field(description="Unique dispatch record identifier")] @@ -2941,9 +2786,9 @@ class NotificationDispatchDto(BaseModel): description="Human-readable name of the matched policy (null if policy has been deleted)", ), ] = None - status: Annotated[Status10, Field(description="Current dispatch state")] + status: Annotated[str, Field(description="Current dispatch state")] completion_reason: Annotated[ - CompletionReason | None, + str | None, Field( alias="completionReason", description="Why the dispatch reached COMPLETED: EXHAUSTED (all steps ran, no ack), RESOLVED (incident resolved), NO_STEPS (policy had no steps). Null for non-terminal states.", @@ -3228,7 +3073,7 @@ class PollChartBucketDto(BaseModel): ] -class Status11(StrEnum): +class Status6(StrEnum): investigating = "INVESTIGATING" identified = "IDENTIFIED" monitoring = "MONITORING" @@ -3249,7 +3094,7 @@ class PublishStatusPageIncidentRequest(BaseModel): Impact | None, Field(description="Impact level; null keeps draft value") ] = None status: Annotated[ - Status11 | None, + Status6 | None, Field( description="Incident status; null keeps draft value (must be an active status)" ), @@ -3428,23 +3273,10 @@ class ResolveIncidentRequest(BaseModel): ] = None -class Status12(StrEnum): - operational = "operational" - maintenance = "maintenance" - degraded = "degraded" - down = "down" - - -class ThresholdStatus(StrEnum): - healthy = "healthy" - degraded = "degraded" - down = "down" - - class ResourceGroupHealthDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) status: Annotated[ - Status12, Field(description="Worst-of health status across all members") + str, Field(description="Worst-of health status across all members") ] total_members: Annotated[ int, @@ -3465,7 +3297,7 @@ class ResourceGroupHealthDto(BaseModel): ), ] threshold_status: Annotated[ - ThresholdStatus | None, + str | None, Field( alias="thresholdStatus", description="Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured.", @@ -3522,9 +3354,7 @@ class ResourceGroupMemberDto(BaseModel): description="Subscription ID for the service (services only); used to link to the dependency detail page", ), ] = None - status: Annotated[ - Status12, Field(description="Computed health status for this member") - ] + status: Annotated[str, Field(description="Computed health status for this member")] effective_frequency: Annotated[ str | None, Field( @@ -3623,7 +3453,7 @@ class ResponseTimeWarnAssertion(BaseModel): class ResultSummaryDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) current_status: Annotated[ - CurrentStatus, + str, Field( alias="currentStatus", description="Derived current status across all regions", @@ -4217,12 +4047,6 @@ class ServiceSubscribeRequest(BaseModel): ] = None -class AlertSensitivity(StrEnum): - all = "ALL" - incidents_only = "INCIDENTS_ONLY" - major_only = "MAJOR_ONLY" - - class ServiceSubscriptionDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) subscription_id: Annotated[ @@ -4259,10 +4083,11 @@ class ServiceSubscriptionDto(BaseModel): ] = None component: ServiceComponentDto | None = None alert_sensitivity: Annotated[ - AlertSensitivity, + str, Field( alias="alertSensitivity", description="Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity)", + min_length=1, ), ] subscribed_at: Annotated[ @@ -4445,7 +4270,7 @@ class SslExpiryAssertion(BaseModel): ] -class Source2(StrEnum): +class Source1(StrEnum): pipeline = "pipeline" public_api = "public-api" @@ -4453,7 +4278,7 @@ class Source2(StrEnum): class StateTransitionDetails(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) source: Annotated[ - Source2, + Source1, Field( description="Actor that produced this transition (pipeline | public-api)" ), @@ -4604,20 +4429,6 @@ class StatusPageBranding(BaseModel): ] = None -class Type5(StrEnum): - monitor = "MONITOR" - group = "GROUP" - static = "STATIC" - - -class CurrentStatus2(StrEnum): - operational = "OPERATIONAL" - degraded_performance = "DEGRADED_PERFORMANCE" - partial_outage = "PARTIAL_OUTAGE" - major_outage = "MAJOR_OUTAGE" - under_maintenance = "UNDER_MAINTENANCE" - - class StatusPageComponentDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID @@ -4625,10 +4436,10 @@ class StatusPageComponentDto(BaseModel): group_id: Annotated[UUID | None, Field(alias="groupId")] = None name: Annotated[str, Field(min_length=1)] description: str | None = None - type: Type5 + type: str monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None - current_status: Annotated[CurrentStatus2, Field(alias="currentStatus")] + current_status: Annotated[str, Field(alias="currentStatus")] show_uptime: Annotated[bool, Field(alias="showUptime")] display_order: Annotated[int, Field(alias="displayOrder")] page_order: Annotated[int, Field(alias="pageOrder")] @@ -4652,29 +4463,12 @@ class StatusPageComponentGroupDto(BaseModel): updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] -class Status14(StrEnum): - pending_verification = "PENDING_VERIFICATION" - verification_failed = "VERIFICATION_FAILED" - verified = "VERIFIED" - ssl_pending = "SSL_PENDING" - active = "ACTIVE" - failed = "FAILED" - removed = "REMOVED" - - -class VerificationMethod(StrEnum): - cname = "CNAME" - txt = "TXT" - - class StatusPageCustomDomainDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID hostname: str - status: Status14 - verification_method: Annotated[ - VerificationMethod, Field(alias="verificationMethod") - ] + status: str + verification_method: Annotated[str, Field(alias="verificationMethod")] verification_token: Annotated[str, Field(alias="verificationToken")] verification_cname_target: Annotated[str, Field(alias="verificationCnameTarget")] verified_at: Annotated[AwareDatetime | None, Field(alias="verifiedAt")] = None @@ -4689,14 +4483,6 @@ class StatusPageCustomDomainDto(BaseModel): primary: bool -class OverallStatus(StrEnum): - operational = "OPERATIONAL" - degraded_performance = "DEGRADED_PERFORMANCE" - partial_outage = "PARTIAL_OUTAGE" - major_outage = "MAJOR_OUTAGE" - under_maintenance = "UNDER_MAINTENANCE" - - class StatusPageDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID @@ -4706,14 +4492,14 @@ class StatusPageDto(BaseModel): slug: Annotated[str, Field(min_length=1)] description: str | None = None branding: StatusPageBranding - visibility: Visibility + visibility: str enabled: bool - incident_mode: Annotated[IncidentMode, Field(alias="incidentMode")] + incident_mode: Annotated[str, Field(alias="incidentMode")] component_count: Annotated[int | None, Field(alias="componentCount")] = None subscriber_count: Annotated[int | None, Field(alias="subscriberCount")] = None - overall_status: Annotated[OverallStatus | None, Field(alias="overallStatus")] = None + overall_status: Annotated[str | None, Field(alias="overallStatus")] = None managed_by: Annotated[ - ManagedBy | None, + str | None, Field( alias="managedBy", description="Source that created/owns this status page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on pages created before this attribution column existed.", @@ -4723,39 +4509,19 @@ class StatusPageDto(BaseModel): updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] -class ComponentStatus(StrEnum): - operational = "OPERATIONAL" - degraded_performance = "DEGRADED_PERFORMANCE" - partial_outage = "PARTIAL_OUTAGE" - major_outage = "MAJOR_OUTAGE" - under_maintenance = "UNDER_MAINTENANCE" - - class StatusPageIncidentComponentDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) status_page_component_id: Annotated[UUID, Field(alias="statusPageComponentId")] - component_status: Annotated[ComponentStatus, Field(alias="componentStatus")] + component_status: Annotated[str, Field(alias="componentStatus")] component_name: Annotated[str, Field(alias="componentName")] -class Status15(StrEnum): - investigating = "INVESTIGATING" - identified = "IDENTIFIED" - monitoring = "MONITORING" - resolved = "RESOLVED" - - -class CreatedBy1(StrEnum): - user = "USER" - system = "SYSTEM" - - class StatusPageIncidentUpdateDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID - status: Status15 + status: str body: str - created_by: Annotated[CreatedBy1 | None, Field(alias="createdBy")] = None + created_by: Annotated[str | None, Field(alias="createdBy")] = None created_by_user_id: Annotated[int | None, Field(alias="createdByUserId")] = None notify_subscribers: Annotated[bool, Field(alias="notifySubscribers")] created_at: Annotated[AwareDatetime, Field(alias="createdAt")] @@ -5253,7 +5019,7 @@ class TlsInfoDto(BaseModel): ] = None -class Type6(StrEnum): +class Type4(StrEnum): consecutive_failures = "consecutive_failures" failures_in_window = "failures_in_window" response_time = "response_time" @@ -5264,7 +5030,7 @@ class Scope(StrEnum): any_region = "any_region" -class Severity7(StrEnum): +class Severity3(StrEnum): down = "down" degraded = "degraded" @@ -5279,7 +5045,7 @@ class AggregationType(StrEnum): class TriggerRule(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) type: Annotated[ - Type6, + Type4, Field( description="Condition that opens or escalates an incident from check results" ), @@ -5307,7 +5073,7 @@ class TriggerRule(BaseModel): ), ] = None severity: Annotated[ - Severity7, Field(description="Incident severity when this rule fires") + Severity3, Field(description="Incident severity when this rule fires") ] aggregation_type: Annotated[ AggregationType | None, @@ -5339,7 +5105,7 @@ class UpdateApiKeyRequest(BaseModel): ] -class Severity8(StrEnum): +class Severity4(StrEnum): fail = "fail" warn = "warn" @@ -5392,7 +5158,7 @@ class UpdateAssertionRequest(BaseModel): Field(discriminator="type"), ] severity: Annotated[ - Severity8 | None, Field(description="New outcome severity: FAIL or WARN") + Severity4 | None, Field(description="New outcome severity: FAIL or WARN") ] = None @@ -5744,7 +5510,7 @@ class UpdateStatusPageIncidentRequest(BaseModel): ), ] = None status: Annotated[ - Status15 | None, Field(description="New status; null preserves current") + Status6 | None, Field(description="New status; null preserves current") ] = None impact: Annotated[ Impact | None, Field(description="New impact level; null preserves current") @@ -6822,7 +6588,7 @@ class IntegrationDto(BaseModel): description: str logo_url: Annotated[str, Field(alias="logoUrl")] auth_type: Annotated[str, Field(alias="authType")] - tier_availability: Annotated[TierAvailability, Field(alias="tierAvailability")] + tier_availability: Annotated[str, Field(alias="tierAvailability")] lifecycle: str setup_guide_url: Annotated[str, Field(alias="setupGuideUrl")] config_schema: Annotated[IntegrationConfigSchemaDto, Field(alias="configSchema")] @@ -6832,7 +6598,7 @@ class MonitorAssertionDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) id: UUID monitor_id: Annotated[UUID, Field(alias="monitorId")] - assertion_type: Annotated[AssertionType, Field(alias="assertionType")] + assertion_type: Annotated[str, Field(alias="assertionType")] config: Annotated[ BodyContainsAssertion | DnsExpectedCnameAssertion @@ -6878,7 +6644,7 @@ class MonitorAssertionDto(BaseModel): | TcpResponseTimeWarnAssertion, Field(discriminator="type"), ] - severity: Severity6 + severity: str class MonitorDto(BaseModel): @@ -6895,7 +6661,7 @@ class MonitorDto(BaseModel): name: Annotated[ str, Field(description="Human-readable name for this monitor", min_length=1) ] - type: Type3 + type: str config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6916,7 +6682,7 @@ class MonitorDto(BaseModel): list[str], Field(description="Probe regions where checks are executed") ] managed_by: Annotated[ - ManagedBy, + str, Field( alias="managedBy", description="Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API", @@ -6961,7 +6727,7 @@ class MonitorDto(BaseModel): ), ] = None current_status: Annotated[ - CurrentStatus | None, + str | None, Field( alias="currentStatus", description="Current operational state — UP, DOWN, DEGRADED, PAUSED, or UNKNOWN if no probe data yet", @@ -7004,7 +6770,7 @@ class MonitorVersionDto(BaseModel): ), ] = None changed_via: Annotated[ - ChangedVia, + str, Field(alias="changedVia", description="Change source (DASHBOARD, CLI, API)"), ] change_summary: Annotated[ @@ -7108,7 +6874,7 @@ class ResourceGroupDto(BaseModel): ), ] = None health_threshold_type: Annotated[ - HealthThresholdType | None, + str | None, Field( alias="healthThresholdType", description="Health threshold type: COUNT or PERCENTAGE", @@ -7147,7 +6913,7 @@ class ResourceGroupDto(BaseModel): ), ] = None managed_by: Annotated[ - ManagedBy | None, + str | None, Field( alias="managedBy", description="Source that created/owns this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on groups created before this attribution column existed.", @@ -7388,8 +7154,8 @@ class StatusPageIncidentDto(BaseModel): id: UUID status_page_id: Annotated[UUID, Field(alias="statusPageId")] title: Annotated[str, Field(min_length=1)] - status: Status15 - impact: Impact + status: str + impact: str scheduled: bool scheduled_for: Annotated[AwareDatetime | None, Field(alias="scheduledFor")] = None scheduled_until: Annotated[AwareDatetime | None, Field(alias="scheduledUntil")] = ( diff --git a/src/devhelm/types.py b/src/devhelm/types.py index eed19e6..46f1fd6 100644 --- a/src/devhelm/types.py +++ b/src/devhelm/types.py @@ -1,53 +1,111 @@ -"""Public type re-exports from generated OpenAPI models. +"""Public type re-exports. -These are the stable public names consumers should import from `devhelm`. -The underlying `_generated` module is auto-generated by `datamodel-codegen` -from the vendored OpenAPI spec; do not import from it directly. +Two underlying modules feed this file: -Enum Alias Mapping -================== -``datamodel-codegen`` produces numbered suffixes (Status1 … Status15, Type1 … Type6, -etc.) when multiple OpenAPI schemas share the same enum name. The aliases below -give each generated enum a descriptive, context-aware name based on the DTO that -references it. + * ``devhelm._generated`` — Pydantic v2 DTO classes produced by + ``datamodel-code-generator`` from the **preprocessed** OpenAPI spec. + Response-DTO multi-value enum fields decode as plain ``str`` (the + spec-level Postel's-Law relaxation; see + ``mini/runbooks/api-contract.md`` § 3); request-DTO enums keep + strict validation through ``StrEnum`` field types so callers cannot + accidentally send unknown wire values. + * ``devhelm._enums`` — auto-generated ``typing.Literal[...]`` aliases, + one per ``(SchemaName, propertyName)`` pair. Names are stable across + spec evolution because they don't depend on + ``datamodel-codegen``'s suffixed names (``Status1``…``Status15``, + ``Type1``…``Type6``) which shift on every schema change. -When ``_generated.py`` is regenerated the numbering may shift. Verify aliases by -checking which DTO class references each generated enum name, then update the -imports here to match. - -Duplicate-value groups (same members, different Python classes): - StatusPageIncidentStatus ≡ LinkedIncidentStatus ≡ PublishIncidentStatus ≡ StatusPageUpdateStatus - MembershipStatus ≡ MemberStatus - AssertionSeverity ≡ MonitorAssertionSeverity ≡ UpdateAssertionSeverity - MonitorType ≡ MonitorDtoType - StatusPageComponentType ≡ StatusPageComponentDtoType - IncidentUpdateCreatedBy ≡ StatusPageUpdateCreatedBy +Public-facing aliases below pick a canonical name per concept (e.g. +``IncidentStatus`` over ``IncidentDtoStatus``) so SDK callers don't +have to think about whether a value lives on the request or response +side — most enum value-sets are identical between sides anyway, and +the API treats unknown values tolerantly on receive (Postel's Law) and +strictly on send (Pydantic ``StrEnum`` validation in ``_generated``). """ from __future__ import annotations +from devhelm._enums import AffectedComponentStatus as AffectedComponentStatus +from devhelm._enums import AlertChannelDtoChannelType as ChannelType +from devhelm._enums import AlertChannelDtoManagedBy as AlertChannelManagedBy +from devhelm._enums import AlertDeliveryDtoEventType as EventType +from devhelm._enums import AlertDeliveryDtoStatus as AlertDeliveryStatus +from devhelm._enums import AssertionResultDtoSeverity as AssertionSeverity +from devhelm._enums import BulkMonitorActionRequestAction as Action +from devhelm._enums import ChangeStatusRequestStatus as MembershipStatus +from devhelm._enums import ConfirmationPolicyType as ConfirmationPolicyType +from devhelm._enums import CreateMonitorRequestType as MonitorType +from devhelm._enums import ( + CreateStatusPageComponentRequestType as StatusPageComponentType, +) +from devhelm._enums import DnsMonitorConfigRecordTypesItem as RecordType +from devhelm._enums import HttpMonitorConfigMethod as Method +from devhelm._enums import IncidentDtoResolutionReason as ResolutionReason +from devhelm._enums import IncidentDtoSeverity as IncidentSeverity +from devhelm._enums import IncidentDtoSource as Source +from devhelm._enums import IncidentDtoStatus as IncidentStatus +from devhelm._enums import IncidentUpdateDtoCreatedBy as IncidentUpdateCreatedBy +from devhelm._enums import IncidentUpdateDtoNewStatus as IncidentNewStatus +from devhelm._enums import IncidentUpdateDtoOldStatus as IncidentOldStatus +from devhelm._enums import IntegrationDtoTierAvailability as TierAvailability +from devhelm._enums import InviteDtoRoleOffered as RoleOffered +from devhelm._enums import LinkedStatusPageIncidentDtoStatus as LinkedIncidentStatus +from devhelm._enums import MemberDtoOrgRole as OrgRole +from devhelm._enums import MemberDtoStatus as MemberStatus +from devhelm._enums import MonitorAssertionDtoAssertionType as AssertionType +from devhelm._enums import MonitorAssertionDtoSeverity as MonitorAssertionSeverity +from devhelm._enums import MonitorAuthDtoAuthType as AuthType +from devhelm._enums import MonitorDtoCurrentStatus as MonitorCurrentStatus +from devhelm._enums import MonitorDtoManagedBy as ManagedBy +from devhelm._enums import MonitorDtoType as MonitorDtoType +from devhelm._enums import MonitorVersionDtoChangedVia as ChangedVia +from devhelm._enums import NotificationDispatchDtoCompletionReason as CompletionReason +from devhelm._enums import NotificationDispatchDtoStatus as NotificationDispatchStatus +from devhelm._enums import PlanInfoTier as Tier # noqa: F401 (re-export) +from devhelm._enums import ( + PublishStatusPageIncidentRequestStatus as PublishIncidentStatus, +) +from devhelm._enums import ResourceGroupDtoHealthThresholdType as HealthThresholdType +from devhelm._enums import ResourceGroupHealthDtoStatus as ResourceGroupHealthStatus +from devhelm._enums import ResourceGroupHealthDtoThresholdStatus as ThresholdStatus +from devhelm._enums import ServiceSubscriptionDtoAlertSensitivity as AlertSensitivity +from devhelm._enums import ( + StatusPageComponentDtoCurrentStatus as StatusPageComponentCurrentStatus, +) +from devhelm._enums import StatusPageComponentDtoType as StatusPageComponentDtoType +from devhelm._enums import StatusPageCustomDomainDtoStatus as CustomDomainStatus +from devhelm._enums import ( + StatusPageCustomDomainDtoVerificationMethod as VerificationMethod, +) +from devhelm._enums import StatusPageDtoIncidentMode as IncidentMode +from devhelm._enums import StatusPageDtoOverallStatus as StatusPageOverallStatus +from devhelm._enums import StatusPageDtoVisibility as Visibility +from devhelm._enums import ( + StatusPageIncidentComponentDtoComponentStatus as StatusPageIncidentComponentStatus, +) +from devhelm._enums import StatusPageIncidentDtoImpact as Impact +from devhelm._enums import StatusPageIncidentDtoStatus as StatusPageIncidentStatus +from devhelm._enums import ( + StatusPageIncidentUpdateDtoCreatedBy as StatusPageUpdateCreatedBy, +) +from devhelm._enums import StatusPageIncidentUpdateDtoStatus as StatusPageUpdateStatus +from devhelm._enums import TriggerRuleAggregationType as AggregationType +from devhelm._enums import TriggerRuleScope as Scope +from devhelm._enums import TriggerRuleSeverity as TriggerRuleSeverity +from devhelm._enums import TriggerRuleType as TriggerRuleType +from devhelm._enums import UpdateAssertionRequestSeverity as UpdateAssertionSeverity from devhelm._generated import ( - # ── DTOs ────────────────────────────────────────────────────────────── AcquireDeployLockRequest, - # ── Enums: unique names (no suffix collisions) ──────────────────────── - Action, AddCustomDomainRequest, AddIncidentUpdateRequest, AddResourceGroupMemberRequest, AdminAddSubscriberRequest, - AggregationType, AlertChannelDto, - AlertSensitivity, ApiKeyCreateResponse, ApiKeyDto, AssertionTestResultDto, - AssertionType, - AuthType, - ChangedVia, - ChannelType, CheckResultDto, # noqa: F401 CheckTraceDto, - CompletionReason, CreateAlertChannelRequest, CreateApiKeyRequest, CreateEnvironmentRequest, @@ -67,37 +125,25 @@ DashboardOverviewDto, DeployLockDto, EnvironmentDto, - EventType, - HealthThresholdType, - Impact, IncidentDetailDto, IncidentDto, - IncidentMode, IncidentStateTransitionDto, IncidentTimelineDto, MaintenanceWindowDto, - ManagedBy, - Method, MonitorDto, MonitorVersionDto, NotificationPolicyDto, - Operator, - OrgRole, + Operator, # request-side enum that survived as a StrEnum class PolicySnapshotDto, PublishStatusPageIncidentRequest, - RecordType, ReorderComponentsRequest, ReorderPageLayoutRequest, - ResolutionReason, ResolveIncidentRequest, ResourceGroupDto, ResourceGroupMemberDto, # noqa: F401 - RoleOffered, RuleEvaluationDto, - Scope, SecretDto, ServiceSubscriptionDto, - Source, StatusPageBranding, StatusPageComponentDto, StatusPageComponentGroupDto, @@ -109,9 +155,6 @@ StatusPageSubscriberDto, TagDto, TestChannelResult, - ThresholdStatus, - Tier, - TierAvailability, UpdateAlertChannelRequest, UpdateEnvironmentRequest, UpdateMaintenanceWindowRequest, @@ -125,110 +168,9 @@ UpdateStatusPageRequest, UpdateTagRequest, UpdateWebhookEndpointRequest, - VerificationMethod, - Visibility, WebhookEndpointDto, WebhookTestResult, ) -from devhelm._generated import ( - ComponentStatus as StatusPageIncidentComponentStatus, # StatusPageIncidentComponentDto.component_status -) -from devhelm._generated import ( - # - # CreatedBy enums - CreatedBy as IncidentUpdateCreatedBy, # IncidentUpdateDto.created_by -) -from devhelm._generated import ( - CreatedBy1 as StatusPageUpdateCreatedBy, # StatusPageIncidentUpdateDto.created_by -) -from devhelm._generated import ( - # - # CurrentStatus enums. - # - # NOTE on suffix stability: datamodel-code-generator names inline enums by - # iteration order (CurrentStatus, CurrentStatus1, …). Adding another DTO - # with a `currentStatus` field can shift the suffixes. As of mono v0.13+ - # `CurrentStatus` is shared by `MonitorDto.currentStatus` and - # `ResultSummaryDto.currentStatus` (deduped — identical value sets), and - # `CurrentStatus2` belongs to `StatusPageComponentDto.currentStatus` - # (different value set: OPERATIONAL/DEGRADED_PERFORMANCE/...). - CurrentStatus as MonitorCurrentStatus, # MonitorDto.current_status + ResultSummaryDto.current_status -) -from devhelm._generated import ( - CurrentStatus2 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status -) -from devhelm._generated import ( - # - # NewStatus / OldStatus / OverallStatus / ComponentStatus — already - # semi-descriptive but aliased for consistency with the rest of the SDK. - NewStatus as IncidentNewStatus, # AddIncidentUpdateRequest.new_status -) -from devhelm._generated import ( - OldStatus as IncidentOldStatus, # IncidentUpdateDto.old_status -) -from devhelm._generated import ( - OverallStatus as StatusPageOverallStatus, # StatusPageDto.overall_status -) -from devhelm._generated import ( - # - # Severity enums - Severity as AssertionSeverity, # AssertionResultDto.severity -) -from devhelm._generated import ( - Severity3 as IncidentSeverity, # CreateManualIncidentRequest.severity -) -from devhelm._generated import ( - Severity6 as MonitorAssertionSeverity, # MonitorAssertionDto.severity -) -from devhelm._generated import Severity7 as TriggerRuleSeverity # TriggerRule.severity -from devhelm._generated import ( - Severity8 as UpdateAssertionSeverity, # UpdateAssertionRequest.severity -) -from devhelm._generated import ( - # ── Enums: ambiguous generated names → descriptive aliases ──────────── - # - # Status enums - Status as AffectedComponentStatus, # AffectedComponent.status -) -from devhelm._generated import Status1 as AlertDeliveryStatus # AlertDeliveryDto.status -from devhelm._generated import Status2 as MembershipStatus # ChangeStatusRequest.status -from devhelm._generated import ( - Status3 as StatusPageIncidentStatus, # CreateStatusPageIncidentRequest.status -) -from devhelm._generated import Status6 as IncidentStatus # IncidentDto.status -from devhelm._generated import ( - Status8 as LinkedIncidentStatus, # LinkedStatusPageIncidentDto.status -) -from devhelm._generated import Status9 as MemberStatus # MemberDto.status -from devhelm._generated import ( - Status10 as NotificationDispatchStatus, # NotificationDispatchDto.status -) -from devhelm._generated import ( - Status11 as PublishIncidentStatus, # PublishStatusPageIncidentRequest.status -) -from devhelm._generated import ( - Status12 as ResourceGroupHealthStatus, # ResourceGroupHealthDto.status -) -from devhelm._generated import ( - Status14 as CustomDomainStatus, # StatusPageCustomDomainDto.status -) -from devhelm._generated import ( - Status15 as StatusPageUpdateStatus, # StatusPageIncidentUpdateDto.status -) -from devhelm._generated import ( - # - # Type enums - Type as ConfirmationPolicyType, # ConfirmationPolicy.type -) -from devhelm._generated import Type1 as MonitorType # CreateMonitorRequest.type -from devhelm._generated import ( - Type2 as StatusPageComponentType, # CreateStatusPageComponentRequest.type -) -from devhelm._generated import Type3 as MonitorDtoType # MonitorDto.type -from devhelm._generated import ( - Type5 as StatusPageComponentDtoType, # StatusPageComponentDto.type -) -from devhelm._generated import Type6 as TriggerRuleType # TriggerRule.type __all__ = [ # ── DTOs ────────────────────────────────────────────────────────────── @@ -306,69 +248,63 @@ "UpdateWebhookEndpointRequest", "WebhookEndpointDto", "WebhookTestResult", - # ── Enums: unique names ─────────────────────────────────────────────── + # ── Enum aliases (canonical public names) ──────────────────────────── "Action", + "AffectedComponentStatus", "AggregationType", + "AlertChannelManagedBy", + "AlertDeliveryStatus", "AlertSensitivity", + "AssertionSeverity", "AssertionType", "AuthType", "ChangedVia", "ChannelType", "CompletionReason", + "ConfirmationPolicyType", + "CustomDomainStatus", "EventType", "HealthThresholdType", "Impact", "IncidentMode", + "IncidentNewStatus", + "IncidentOldStatus", + "IncidentSeverity", + "IncidentStatus", + "IncidentUpdateCreatedBy", + "LinkedIncidentStatus", "ManagedBy", + "MemberStatus", + "MembershipStatus", "Method", + "MonitorAssertionSeverity", + "MonitorCurrentStatus", + "MonitorDtoType", + "MonitorType", + "NotificationDispatchStatus", "Operator", "OrgRole", + "PublishIncidentStatus", "RecordType", "ResolutionReason", + "ResourceGroupHealthStatus", "RoleOffered", "Scope", "Source", + "StatusPageComponentCurrentStatus", + "StatusPageComponentDtoType", + "StatusPageComponentType", + "StatusPageIncidentComponentStatus", + "StatusPageIncidentStatus", + "StatusPageOverallStatus", + "StatusPageUpdateCreatedBy", + "StatusPageUpdateStatus", "ThresholdStatus", "Tier", "TierAvailability", - "VerificationMethod", - "Visibility", - # ── Enums: descriptive aliases for ambiguous generated names ────────── - # Status - "AffectedComponentStatus", - "AlertDeliveryStatus", - "CustomDomainStatus", - "IncidentStatus", - "LinkedIncidentStatus", - "MemberStatus", - "MembershipStatus", - "NotificationDispatchStatus", - "PublishIncidentStatus", - "ResourceGroupHealthStatus", - "StatusPageIncidentStatus", - "StatusPageUpdateStatus", - # Severity - "AssertionSeverity", - "IncidentSeverity", - "MonitorAssertionSeverity", "TriggerRuleSeverity", - "UpdateAssertionSeverity", - # Type - "ConfirmationPolicyType", - "MonitorDtoType", - "MonitorType", - "StatusPageComponentDtoType", - "StatusPageComponentType", "TriggerRuleType", - # CurrentStatus - "MonitorCurrentStatus", - "StatusPageComponentCurrentStatus", - # CreatedBy - "IncidentUpdateCreatedBy", - "StatusPageUpdateCreatedBy", - # Already semi-descriptive, aliased for consistency - "IncidentNewStatus", - "IncidentOldStatus", - "StatusPageIncidentComponentStatus", - "StatusPageOverallStatus", + "UpdateAssertionSeverity", + "VerificationMethod", + "Visibility", ] diff --git a/tests/test_negative_validation.py b/tests/test_negative_validation.py index 75f24b5..e74905c 100644 --- a/tests/test_negative_validation.py +++ b/tests/test_negative_validation.py @@ -458,8 +458,8 @@ def test_missing_type(self) -> None: MonitorDto.model_validate(_del(_monitor(), "type")) def test_invalid_type_enum(self) -> None: - with pytest.raises(ValidationError): - MonitorDto.model_validate(_monitor(type="BANANA")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + MonitorDto.model_validate(_monitor(type="BANANA")) def test_wrong_frequency_type(self) -> None: with pytest.raises(ValidationError): @@ -486,8 +486,8 @@ def test_missing_managed_by(self) -> None: MonitorDto.model_validate(_del(_monitor(), "managedBy")) def test_invalid_managed_by(self) -> None: - with pytest.raises(ValidationError): - MonitorDto.model_validate(_monitor(managedBy="MAGIC")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + MonitorDto.model_validate(_monitor(managedBy="MAGIC")) def test_missing_created_at(self) -> None: with pytest.raises(ValidationError, match="createdAt"): @@ -651,24 +651,24 @@ def test_missing_source(self) -> None: IncidentDto.model_validate(_del(_incident(), "source")) def test_invalid_source_enum(self) -> None: - with pytest.raises(ValidationError): - IncidentDto.model_validate(_incident(source="ALIEN")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + IncidentDto.model_validate(_incident(source="ALIEN")) def test_missing_status(self) -> None: with pytest.raises(ValidationError, match="status"): IncidentDto.model_validate(_del(_incident(), "status")) def test_invalid_status_enum(self) -> None: - with pytest.raises(ValidationError): - IncidentDto.model_validate(_incident(status="OPEN")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + IncidentDto.model_validate(_incident(status="OPEN")) def test_missing_severity(self) -> None: with pytest.raises(ValidationError, match="severity"): IncidentDto.model_validate(_del(_incident(), "severity")) def test_invalid_severity_enum(self) -> None: - with pytest.raises(ValidationError): - IncidentDto.model_validate(_incident(severity="SEV1")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + IncidentDto.model_validate(_incident(severity="SEV1")) def test_wrong_reopen_count_type(self) -> None: with pytest.raises(ValidationError): @@ -763,8 +763,8 @@ def test_missing_channel_type(self) -> None: AlertChannelDto.model_validate(_del(_alert_channel(), "channelType")) def test_invalid_channel_type(self) -> None: - with pytest.raises(ValidationError): - AlertChannelDto.model_validate(_alert_channel(channelType="telegram")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + AlertChannelDto.model_validate(_alert_channel(channelType="telegram")) def test_missing_created_at(self) -> None: with pytest.raises(ValidationError, match="createdAt"): @@ -1631,12 +1631,12 @@ def test_empty_slug(self) -> None: StatusPageDto.model_validate(_status_page(slug="")) def test_invalid_visibility(self) -> None: - with pytest.raises(ValidationError): - StatusPageDto.model_validate(_status_page(visibility="PRIVATE")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageDto.model_validate(_status_page(visibility="PRIVATE")) def test_invalid_incident_mode(self) -> None: - with pytest.raises(ValidationError): - StatusPageDto.model_validate(_status_page(incidentMode="AUTO_PILOT")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageDto.model_validate(_status_page(incidentMode="AUTO_PILOT")) def test_missing_branding(self) -> None: with pytest.raises(ValidationError, match="branding"): @@ -1779,12 +1779,12 @@ def test_missing_type(self) -> None: StatusPageComponentDto.model_validate(_del(_sp_component(), "type")) def test_invalid_type(self) -> None: - with pytest.raises(ValidationError): - StatusPageComponentDto.model_validate(_sp_component(type="CUSTOM")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageComponentDto.model_validate(_sp_component(type="CUSTOM")) def test_invalid_current_status(self) -> None: - with pytest.raises(ValidationError): - StatusPageComponentDto.model_validate(_sp_component(currentStatus="BROKEN")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageComponentDto.model_validate(_sp_component(currentStatus="BROKEN")) def test_missing_show_uptime(self) -> None: with pytest.raises(ValidationError, match="showUptime"): @@ -1951,12 +1951,12 @@ def test_empty_title(self) -> None: StatusPageIncidentDto.model_validate(_sp_incident(title="")) def test_invalid_status(self) -> None: - with pytest.raises(ValidationError): - StatusPageIncidentDto.model_validate(_sp_incident(status="PENDING")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageIncidentDto.model_validate(_sp_incident(status="PENDING")) def test_invalid_impact(self) -> None: - with pytest.raises(ValidationError): - StatusPageIncidentDto.model_validate(_sp_incident(impact="CATASTROPHIC")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageIncidentDto.model_validate(_sp_incident(impact="CATASTROPHIC")) def test_missing_scheduled(self) -> None: with pytest.raises(ValidationError, match="scheduled"): @@ -2083,10 +2083,8 @@ def test_missing_status(self) -> None: ) def test_invalid_status(self) -> None: - with pytest.raises(ValidationError): - StatusPageIncidentUpdateDto.model_validate( - _sp_incident_update(status="OOPS") - ) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageIncidentUpdateDto.model_validate(_sp_incident_update(status="OOPS")) def test_missing_body(self) -> None: with pytest.raises(ValidationError, match="body"): @@ -2149,14 +2147,14 @@ def test_missing_hostname(self) -> None: ) def test_invalid_status(self) -> None: - with pytest.raises(ValidationError): - StatusPageCustomDomainDto.model_validate(_sp_custom_domain(status="MAGIC")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageCustomDomainDto.model_validate(_sp_custom_domain(status="MAGIC")) def test_invalid_verification_method(self) -> None: - with pytest.raises(ValidationError): - StatusPageCustomDomainDto.model_validate( - _sp_custom_domain(verificationMethod="HTTP") - ) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageCustomDomainDto.model_validate( + _sp_custom_domain(verificationMethod="HTTP") + ) def test_missing_verification_token(self) -> None: with pytest.raises(ValidationError, match="verificationToken"): @@ -2194,10 +2192,10 @@ def test_missing_component_status(self) -> None: ) def test_invalid_component_status(self) -> None: - with pytest.raises(ValidationError): - StatusPageIncidentComponentDto.model_validate( - _sp_incident_component(componentStatus="BROKEN") - ) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageIncidentComponentDto.model_validate( + _sp_incident_component(componentStatus="BROKEN") + ) def test_missing_component_name(self) -> None: with pytest.raises(ValidationError, match="componentName"): @@ -2650,8 +2648,8 @@ def test_missing_changed_via(self) -> None: MonitorVersionDto.model_validate(_del(_monitor_version(), "changedVia")) def test_invalid_changed_via(self) -> None: - with pytest.raises(ValidationError): - MonitorVersionDto.model_validate(_monitor_version(changedVia="GITHUB")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + MonitorVersionDto.model_validate(_monitor_version(changedVia="GITHUB")) def test_empty_dict(self) -> None: with pytest.raises(ValidationError): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index ba19b26..1396e22 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -528,8 +528,8 @@ def test_valid(self) -> None: assert dto.title == "Incident" def test_invalid_status_raises(self) -> None: - with pytest.raises(ValidationError): - StatusPageIncidentDto.model_validate(_sp_incident_fixture(status="BANANA")) + # Postel's-Law tolerance: response-DTO unknown enum values must be accepted. + StatusPageIncidentDto.model_validate(_sp_incident_fixture(status="BANANA")) class TestStatusPageComponentDto: diff --git a/uv.lock b/uv.lock index 890e24c..f039a58 100644 --- a/uv.lock +++ b/uv.lock @@ -331,7 +331,7 @@ wheels = [ [[package]] name = "devhelm" -version = "0.7.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "httpx" },