diff --git a/scripts/typegen.sh b/scripts/typegen.sh index a008d7c..765a7c8 100755 --- a/scripts/typegen.sh +++ b/scripts/typegen.sh @@ -52,9 +52,22 @@ uv run datamodel-codegen \ --use-annotated \ --field-constraints \ --snake-case-field \ + --enum-field-as-literal one \ + --use-one-literal-as-default \ --input-file-type openapi \ --formatters ruff-format +# Why --enum-field-as-literal=one + --use-one-literal-as-default? +# +# Without these, single-value enums (used as discriminators on sealed unions +# like AuditMetadata.kind) generate as `kind: Kind` where `Kind(StrEnum)` has +# one entry. Pydantic 2.12+ rejects this when the parent is a discriminated +# union: "Model 'X' needs field 'kind' to be of type `Literal`". +# +# These flags make the codegen emit `kind: Literal["..."] = "..."` instead, +# satisfying the discriminator requirement and making the field optional at +# construction (callers don't need to repeat the discriminator value). + # Post-process: inject `model_config = ConfigDict(extra='forbid')` into every # generated class so that requests with unknown fields and responses with # unknown fields BOTH fail loudly. Implements P1 + P2 from diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index 6f5bd77..13ceca0 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: .openapi-preprocessed.json -# timestamp: 2026-04-23T17:38:04+00:00 +# timestamp: 2026-04-24T17:23:19+00:00 from __future__ import annotations from typing import Annotated, Any, Literal @@ -295,13 +295,9 @@ class AlertDeliveryDto(BaseModel): created_at: Annotated[AwareDatetime, Field(alias="createdAt")] -class Type(StrEnum): - api_key = "api_key" - - class ApiKeyAuthConfig(BaseModel): model_config = ConfigDict(extra="forbid") - type: Literal["api_key"] + type: Literal["api_key"] = "api_key" header_name: Annotated[ str, Field( @@ -514,13 +510,9 @@ class AuditEventDto(BaseModel): ] -class Type1(StrEnum): - basic = "basic" - - class BasicAuthConfig(BaseModel): model_config = ConfigDict(extra="forbid") - type: Literal["basic"] + type: Literal["basic"] = "basic" vault_secret_id: Annotated[ UUID | None, Field( @@ -530,13 +522,9 @@ class BasicAuthConfig(BaseModel): ] = None -class Type2(StrEnum): - bearer = "bearer" - - class BearerAuthConfig(BaseModel): model_config = ConfigDict(extra="forbid") - type: Literal["bearer"] + type: Literal["bearer"] = "bearer" vault_secret_id: Annotated[ UUID | None, Field( @@ -546,13 +534,9 @@ class BearerAuthConfig(BaseModel): ] = None -class Type3(StrEnum): - body_contains = "body_contains" - - class BodyContainsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type3 + type: Literal["body_contains"] = "body_contains" substring: Annotated[ str, Field( @@ -764,16 +748,12 @@ class ComponentUptimeSummaryDto(BaseModel): ] -class Type4(StrEnum): - multi_region = "multi_region" - - class ConfirmationPolicy(BaseModel): model_config = ConfigDict(extra="forbid") type: Annotated[ - Type4, + Literal["multi_region"], Field(description="How incident confirmation is coordinated across regions"), - ] + ] = "multi_region" min_regions_failing: Annotated[ int, Field( @@ -925,7 +905,7 @@ class CreateManualIncidentRequest(BaseModel): ] = None -class Type5(StrEnum): +class Type(StrEnum): http = "HTTP" dns = "DNS" mcp_server = "MCP_SERVER" @@ -987,7 +967,7 @@ class CreateStatusPageComponentGroupRequest(BaseModel): ] = None -class Type6(StrEnum): +class Type1(StrEnum): monitor = "MONITOR" group = "GROUP" static = "STATIC" @@ -1007,7 +987,7 @@ class CreateStatusPageComponentRequest(BaseModel): ), ] = None type: Annotated[ - Type6, Field(description="Component type: MONITOR, GROUP, or STATIC") + Type1, Field(description="Component type: MONITOR, GROUP, or STATIC") ] monitor_id: Annotated[ UUID | None, @@ -1384,13 +1364,9 @@ class DeployLockDto(BaseModel): ] -class ChannelType1(StrEnum): - discord = "discord" - - class DiscordChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType1, Field(alias="channelType")] + channel_type: Annotated[Literal["discord"], Field(alias="channelType")] = "discord" webhook_url: Annotated[ str, Field(alias="webhookUrl", description="Discord webhook URL", min_length=1) ] @@ -1403,13 +1379,9 @@ class DiscordChannelConfig(BaseModel): ] = None -class CheckType(StrEnum): - dns = "dns" - - class Dns(BaseModel): model_config = ConfigDict(extra="forbid") - check_type: Literal["dns"] + check_type: Literal["dns"] = "dns" hostname: Annotated[str | None, Field(description="Target hostname")] = None requested_types: Annotated[ list[str] | None, @@ -1432,13 +1404,9 @@ class Dns(BaseModel): ] = None -class Type7(StrEnum): - dns_expected_cname = "dns_expected_cname" - - class DnsExpectedCnameAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type7 + type: Literal["dns_expected_cname"] = "dns_expected_cname" value: Annotated[ str, Field( @@ -1448,13 +1416,9 @@ class DnsExpectedCnameAssertion(BaseModel): ] -class Type8(StrEnum): - dns_expected_ips = "dns_expected_ips" - - class DnsExpectedIpsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type8 + type: Literal["dns_expected_ips"] = "dns_expected_ips" ips: Annotated[ list[str], Field( @@ -1464,13 +1428,9 @@ class DnsExpectedIpsAssertion(BaseModel): ] -class Type9(StrEnum): - dns_max_answers = "dns_max_answers" - - class DnsMaxAnswersAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type9 + type: Literal["dns_max_answers"] = "dns_max_answers" record_type: Annotated[ str, Field( @@ -1484,13 +1444,9 @@ class DnsMaxAnswersAssertion(BaseModel): ] -class Type10(StrEnum): - dns_min_answers = "dns_min_answers" - - class DnsMinAnswersAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type10 + type: Literal["dns_min_answers"] = "dns_min_answers" record_type: Annotated[ str, Field( @@ -1547,13 +1503,9 @@ class DnsMonitorConfig(BaseModel): ] = None -class Type11(StrEnum): - dns_record_contains = "dns_record_contains" - - class DnsRecordContainsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type11 + type: Literal["dns_record_contains"] = "dns_record_contains" record_type: Annotated[ str, Field( @@ -1571,13 +1523,9 @@ class DnsRecordContainsAssertion(BaseModel): ] -class Type12(StrEnum): - dns_record_equals = "dns_record_equals" - - class DnsRecordEqualsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type12 + type: Literal["dns_record_equals"] = "dns_record_equals" record_type: Annotated[ str, Field( @@ -1592,22 +1540,14 @@ class DnsRecordEqualsAssertion(BaseModel): ] -class Type13(StrEnum): - dns_resolves = "dns_resolves" - - class DnsResolvesAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type13 - - -class Type14(StrEnum): - dns_response_time = "dns_response_time" + type: Literal["dns_resolves"] = "dns_resolves" class DnsResponseTimeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type14 + type: Literal["dns_response_time"] = "dns_response_time" max_ms: Annotated[ int, Field( @@ -1617,13 +1557,9 @@ class DnsResponseTimeAssertion(BaseModel): ] -class Type15(StrEnum): - dns_response_time_warn = "dns_response_time_warn" - - class DnsResponseTimeWarnAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type15 + type: Literal["dns_response_time_warn"] = "dns_response_time_warn" warn_ms: Annotated[ int, Field( @@ -1633,13 +1569,9 @@ class DnsResponseTimeWarnAssertion(BaseModel): ] -class Type16(StrEnum): - dns_ttl_high = "dns_ttl_high" - - class DnsTtlHighAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type16 + type: Literal["dns_ttl_high"] = "dns_ttl_high" max_ttl: Annotated[ int, Field( @@ -1649,13 +1581,9 @@ class DnsTtlHighAssertion(BaseModel): ] -class Type17(StrEnum): - dns_ttl_low = "dns_ttl_low" - - class DnsTtlLowAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type17 + type: Literal["dns_ttl_low"] = "dns_ttl_low" min_ttl: Annotated[ int, Field( @@ -1665,13 +1593,9 @@ class DnsTtlLowAssertion(BaseModel): ] -class Type18(StrEnum): - dns_txt_contains = "dns_txt_contains" - - class DnsTxtContainsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type18 + type: Literal["dns_txt_contains"] = "dns_txt_contains" substring: Annotated[ str, Field( @@ -1681,13 +1605,9 @@ class DnsTxtContainsAssertion(BaseModel): ] -class ChannelType2(StrEnum): - email = "email" - - class EmailChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType2, Field(alias="channelType")] + channel_type: Annotated[Literal["email"], Field(alias="channelType")] = "email" recipients: Annotated[ list[EmailStr], Field(description="Email addresses to send notifications to", min_length=1), @@ -1852,13 +1772,9 @@ class GroupComponentOrder(BaseModel): ] -class Type19(StrEnum): - header = "header" - - class HeaderAuthConfig(BaseModel): model_config = ConfigDict(extra="forbid") - type: Literal["header"] + type: Literal["header"] = "header" header_name: Annotated[ str, Field( @@ -1876,10 +1792,6 @@ class HeaderAuthConfig(BaseModel): ] = None -class Type20(StrEnum): - header_value = "header_value" - - class Operator(StrEnum): equals = "equals" contains = "contains" @@ -1891,7 +1803,7 @@ class Operator(StrEnum): class HeaderValueAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type20 + type: Literal["header_value"] = "header_value" header_name: Annotated[ str, Field( @@ -1911,13 +1823,9 @@ class HeaderValueAssertion(BaseModel): ] -class Type21(StrEnum): - heartbeat_interval_drift = "heartbeat_interval_drift" - - class HeartbeatIntervalDriftAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type21 + type: Literal["heartbeat_interval_drift"] = "heartbeat_interval_drift" max_deviation_percent: Annotated[ int, Field( @@ -1929,13 +1837,9 @@ class HeartbeatIntervalDriftAssertion(BaseModel): ] -class Type22(StrEnum): - heartbeat_max_interval = "heartbeat_max_interval" - - class HeartbeatMaxIntervalAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type22 + type: Literal["heartbeat_max_interval"] = "heartbeat_max_interval" max_seconds: Annotated[ int, Field( @@ -1967,13 +1871,9 @@ class HeartbeatMonitorConfig(BaseModel): ] -class Type23(StrEnum): - heartbeat_payload_contains = "heartbeat_payload_contains" - - class HeartbeatPayloadContainsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type23 + type: Literal["heartbeat_payload_contains"] = "heartbeat_payload_contains" path: Annotated[ str, Field( @@ -1993,17 +1893,9 @@ class HeartbeatPingResponse(BaseModel): ] -class Type24(StrEnum): - heartbeat_received = "heartbeat_received" - - class HeartbeatReceivedAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type24 - - -class CheckType1(StrEnum): - http = "http" + type: Literal["heartbeat_received"] = "heartbeat_received" class Method(StrEnum): @@ -2053,13 +1945,9 @@ class HttpMonitorConfig(BaseModel): ] = None -class CheckType2(StrEnum): - icmp = "icmp" - - class Icmp(BaseModel): model_config = ConfigDict(extra="forbid") - check_type: Literal["icmp"] + check_type: Literal["icmp"] = "icmp" host: Annotated[str, Field(description="Target host", examples=["1.1.1.1"])] packets_sent: Annotated[ int | None, @@ -2109,13 +1997,9 @@ class IcmpMonitorConfig(BaseModel): ] = None -class Type25(StrEnum): - icmp_packet_loss = "icmp_packet_loss" - - class IcmpPacketLossAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type25 + type: Literal["icmp_packet_loss"] = "icmp_packet_loss" max_percent: Annotated[ float, Field( @@ -2127,22 +2011,14 @@ class IcmpPacketLossAssertion(BaseModel): ] -class Type26(StrEnum): - icmp_reachable = "icmp_reachable" - - class IcmpReachableAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type26 - - -class Type27(StrEnum): - icmp_response_time = "icmp_response_time" + type: Literal["icmp_reachable"] = "icmp_reachable" class IcmpResponseTimeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type27 + type: Literal["icmp_response_time"] = "icmp_response_time" max_ms: Annotated[ int, Field( @@ -2152,13 +2028,9 @@ class IcmpResponseTimeAssertion(BaseModel): ] -class Type28(StrEnum): - icmp_response_time_warn = "icmp_response_time_warn" - - class IcmpResponseTimeWarnAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type28 + type: Literal["icmp_response_time_warn"] = "icmp_response_time_warn" warn_ms: Annotated[ int, Field( @@ -2564,13 +2436,9 @@ class InviteDto(BaseModel): ] = None -class Type29(StrEnum): - json_path = "json_path" - - class JsonPathAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type29 + type: Literal["json_path"] = "json_path" path: Annotated[ str, Field( @@ -2698,7 +2566,7 @@ class MaintenanceWindowDto(BaseModel): ] -class Type30(StrEnum): +class Type2(StrEnum): severity_gte = "severity_gte" monitor_id_in = "monitor_id_in" region_in = "region_in" @@ -2713,7 +2581,7 @@ class Type30(StrEnum): class MatchRule(BaseModel): model_config = ConfigDict(extra="forbid") type: Annotated[ - Type30, + Type2, Field(description="Rule type used to evaluate incidents and status events"), ] value: Annotated[ @@ -2736,22 +2604,14 @@ class MatchRule(BaseModel): ] = None -class Type31(StrEnum): - mcp_connects = "mcp_connects" - - class McpConnectsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type31 - - -class Type32(StrEnum): - mcp_has_capability = "mcp_has_capability" + type: Literal["mcp_connects"] = "mcp_connects" class McpHasCapabilityAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type32 + type: Literal["mcp_has_capability"] = "mcp_has_capability" capability: Annotated[ str, Field( @@ -2761,25 +2621,17 @@ class McpHasCapabilityAssertion(BaseModel): ] -class Type33(StrEnum): - mcp_min_tools = "mcp_min_tools" - - class McpMinToolsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type33 + type: Literal["mcp_min_tools"] = "mcp_min_tools" min: Annotated[ int, Field(description="Minimum number of tools the server must expose") ] -class Type34(StrEnum): - mcp_protocol_version = "mcp_protocol_version" - - class McpProtocolVersionAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type34 + type: Literal["mcp_protocol_version"] = "mcp_protocol_version" version: Annotated[ str, Field( @@ -2789,13 +2641,9 @@ class McpProtocolVersionAssertion(BaseModel): ] -class Type35(StrEnum): - mcp_response_time = "mcp_response_time" - - class McpResponseTimeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type35 + type: Literal["mcp_response_time"] = "mcp_response_time" max_ms: Annotated[ int, Field( @@ -2805,13 +2653,9 @@ class McpResponseTimeAssertion(BaseModel): ] -class Type36(StrEnum): - mcp_response_time_warn = "mcp_response_time_warn" - - class McpResponseTimeWarnAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type36 + type: Literal["mcp_response_time_warn"] = "mcp_response_time_warn" warn_ms: Annotated[ int, Field( @@ -2821,13 +2665,9 @@ class McpResponseTimeWarnAssertion(BaseModel): ] -class CheckType3(StrEnum): - mcp_server = "mcp_server" - - class McpServer(BaseModel): model_config = ConfigDict(extra="forbid") - check_type: Literal["mcp_server"] + check_type: Literal["mcp_server"] = "mcp_server" url: Annotated[str | None, Field(description="MCP server URL")] = None protocol_version: Annotated[ str | None, Field(alias="protocolVersion", description="MCP protocol version") @@ -2864,13 +2704,9 @@ class McpServerMonitorConfig(BaseModel): ] = None -class Type37(StrEnum): - mcp_tool_available = "mcp_tool_available" - - class McpToolAvailableAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type37 + type: Literal["mcp_tool_available"] = "mcp_tool_available" tool_name: Annotated[ str, Field( @@ -2881,13 +2717,9 @@ class McpToolAvailableAssertion(BaseModel): ] -class Type38(StrEnum): - mcp_tool_count_changed = "mcp_tool_count_changed" - - class McpToolCountChangedAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type38 + type: Literal["mcp_tool_count_changed"] = "mcp_tool_count_changed" expected_count: Annotated[ int, Field( @@ -2966,7 +2798,7 @@ class MonitorAuthDto(BaseModel): config: ApiKeyAuthConfig | BasicAuthConfig | BearerAuthConfig | HeaderAuthConfig -class Type39(StrEnum): +class Type3(StrEnum): http = "HTTP" dns = "DNS" mcp_server = "MCP_SERVER" @@ -3179,13 +3011,11 @@ class NotificationDto(BaseModel): ] -class ChannelType3(StrEnum): - opsgenie = "opsgenie" - - class OpsGenieChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType3, Field(alias="channelType")] + channel_type: Annotated[Literal["opsgenie"], Field(alias="channelType")] = ( + "opsgenie" + ) api_key: Annotated[ str, Field( @@ -3228,13 +3058,11 @@ class Pageable(BaseModel): sort: list[str] -class ChannelType4(StrEnum): - pagerduty = "pagerduty" - - class PagerDutyChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType4, Field(alias="channelType")] + channel_type: Annotated[Literal["pagerduty"], Field(alias="channelType")] = ( + "pagerduty" + ) routing_key: Annotated[ str, Field( @@ -3422,13 +3250,9 @@ class RecoveryPolicy(BaseModel): ] -class Type41(StrEnum): - redirect_count = "redirect_count" - - class RedirectCountAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type41 + type: Literal["redirect_count"] = "redirect_count" max_count: Annotated[ int, Field( @@ -3438,13 +3262,9 @@ class RedirectCountAssertion(BaseModel): ] -class Type42(StrEnum): - redirect_target = "redirect_target" - - class RedirectTargetAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type42 + type: Literal["redirect_target"] = "redirect_target" expected: Annotated[ str, Field(description="Expected final URL after following redirects", min_length=1), @@ -3457,13 +3277,9 @@ class RedirectTargetAssertion(BaseModel): ] -class Type43(StrEnum): - regex_body = "regex_body" - - class RegexBodyAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type43 + type: Literal["regex_body"] = "regex_body" pattern: Annotated[ str, Field( @@ -3704,13 +3520,9 @@ class ResourceGroupMemberDto(BaseModel): ] = None -class Type44(StrEnum): - response_size = "response_size" - - class ResponseSizeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type44 + type: Literal["response_size"] = "response_size" max_bytes: Annotated[ int, Field( @@ -3720,13 +3532,9 @@ class ResponseSizeAssertion(BaseModel): ] -class Type45(StrEnum): - response_time = "response_time" - - class ResponseTimeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type45 + type: Literal["response_time"] = "response_time" threshold_ms: Annotated[ int, Field( @@ -3736,13 +3544,9 @@ class ResponseTimeAssertion(BaseModel): ] -class Type46(StrEnum): - response_time_warn = "response_time_warn" - - class ResponseTimeWarnAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type46 + type: Literal["response_time_warn"] = "response_time_warn" warn_ms: Annotated[ int, Field( @@ -4456,13 +4260,9 @@ class SingleValueResponseString(BaseModel): data: str -class ChannelType5(StrEnum): - slack = "slack" - - class SlackChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType5, Field(alias="channelType")] + channel_type: Annotated[Literal["slack"], Field(alias="channelType")] = "slack" webhook_url: Annotated[ str, Field( @@ -4478,13 +4278,9 @@ class SlackChannelConfig(BaseModel): ] = None -class Type47(StrEnum): - ssl_expiry = "ssl_expiry" - - class SslExpiryAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type47 + type: Literal["ssl_expiry"] = "ssl_expiry" min_days_remaining: Annotated[ int, Field( @@ -4494,13 +4290,9 @@ class SslExpiryAssertion(BaseModel): ] -class Type48(StrEnum): - status_code = "status_code" - - class StatusCodeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type48 + type: Literal["status_code"] = "status_code" expected: Annotated[ str, Field( @@ -4642,7 +4434,7 @@ class StatusPageBranding(BaseModel): ] = None -class Type49(StrEnum): +class Type5(StrEnum): monitor = "MONITOR" group = "GROUP" static = "STATIC" @@ -4663,7 +4455,7 @@ class StatusPageComponentDto(BaseModel): group_id: Annotated[UUID | None, Field(alias="groupId")] = None name: Annotated[str, Field(min_length=1)] description: str | None = None - type: Type49 + type: Type5 monitor_id: Annotated[UUID | None, Field(alias="monitorId")] = None resource_group_id: Annotated[UUID | None, Field(alias="resourceGroupId")] = None current_status: Annotated[CurrentStatus1, Field(alias="currentStatus")] @@ -5038,13 +4830,9 @@ class TagDto(BaseModel): ] -class CheckType4(StrEnum): - tcp = "tcp" - - class Tcp(BaseModel): model_config = ConfigDict(extra="forbid") - check_type: Literal["tcp"] + check_type: Literal["tcp"] = "tcp" host: Annotated[str, Field(description="Target host", examples=["db.example.com"])] port: Annotated[int, Field(description="Target port", examples=[5432])] connected: Annotated[ @@ -5052,13 +4840,9 @@ class Tcp(BaseModel): ] -class Type50(StrEnum): - tcp_connects = "tcp_connects" - - class TcpConnectsAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type50 + type: Literal["tcp_connects"] = "tcp_connects" class TcpMonitorConfig(BaseModel): @@ -5073,13 +4857,9 @@ class TcpMonitorConfig(BaseModel): ] = None -class Type51(StrEnum): - tcp_response_time = "tcp_response_time" - - class TcpResponseTimeAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type51 + type: Literal["tcp_response_time"] = "tcp_response_time" max_ms: Annotated[ int, Field( @@ -5089,13 +4869,9 @@ class TcpResponseTimeAssertion(BaseModel): ] -class Type52(StrEnum): - tcp_response_time_warn = "tcp_response_time_warn" - - class TcpResponseTimeWarnAssertion(BaseModel): model_config = ConfigDict(extra="forbid") - type: Type52 + type: Literal["tcp_response_time_warn"] = "tcp_response_time_warn" warn_ms: Annotated[ int, Field( @@ -5105,13 +4881,9 @@ class TcpResponseTimeWarnAssertion(BaseModel): ] -class ChannelType6(StrEnum): - teams = "teams" - - class TeamsChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType6, Field(alias="channelType")] + channel_type: Annotated[Literal["teams"], Field(alias="channelType")] = "teams" webhook_url: Annotated[ str, Field( @@ -5304,7 +5076,7 @@ class TlsInfoDto(BaseModel): ] = None -class Type53(StrEnum): +class Type6(StrEnum): consecutive_failures = "consecutive_failures" failures_in_window = "failures_in_window" response_time = "response_time" @@ -5330,7 +5102,7 @@ class AggregationType(StrEnum): class TriggerRule(BaseModel): model_config = ConfigDict(extra="forbid") type: Annotated[ - Type53, + Type6, Field( description="Condition that opens or escalates an incident from check results" ), @@ -5969,13 +5741,9 @@ class UptimeDto(BaseModel): ] = None -class ChannelType7(StrEnum): - webhook = "webhook" - - class WebhookChannelConfig(BaseModel): model_config = ConfigDict(extra="forbid") - channel_type: Annotated[ChannelType7, Field(alias="channelType")] + channel_type: Annotated[Literal["webhook"], Field(alias="channelType")] = "webhook" url: Annotated[ str, Field( @@ -6324,7 +6092,7 @@ class CreateMonitorRequest(BaseModel): min_length=0, ), ] - type: Annotated[Type5, Field(description="Monitor protocol type")] + type: Annotated[Type, Field(description="Monitor protocol type")] config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6644,7 +6412,7 @@ class GlobalStatusSummaryDto(BaseModel): class Http(BaseModel): model_config = ConfigDict(extra="forbid") - check_type: Literal["http"] + check_type: Literal["http"] = "http" timing: TimingPhasesDto | None = None body_truncated: Annotated[ bool | None, @@ -6790,7 +6558,7 @@ class MonitorDto(BaseModel): name: Annotated[ str, Field(description="Human-readable name for this monitor", min_length=1) ] - type: Type39 + type: Type3 config: ( DnsMonitorConfig | HeartbeatMonitorConfig @@ -6856,7 +6624,7 @@ class MonitorDto(BaseModel): class MonitorTestRequest(BaseModel): model_config = ConfigDict(extra="forbid") - type: Annotated[Type39, Field(description="Monitor protocol type to test")] + type: Annotated[Type3, Field(description="Monitor protocol type to test")] config: ( DnsMonitorConfig | HeartbeatMonitorConfig diff --git a/tests/test_negative_validation.py b/tests/test_negative_validation.py index 8fd6ba1..0770e16 100644 --- a/tests/test_negative_validation.py +++ b/tests/test_negative_validation.py @@ -2319,11 +2319,18 @@ def test_wrong_count_type(self) -> None: class TestConfirmationPolicyNegative: - def test_missing_type(self) -> None: - with pytest.raises(ValidationError, match="type"): - ConfirmationPolicy.model_validate( - {"minRegionsFailing": 1, "maxWaitSeconds": 30} - ) + def test_omitted_type_defaults_to_only_variant(self) -> None: + # ConfirmationPolicy.type is a single-value Literal in the spec + # (currently only "multi_region"). The codegen defaults single-value + # Literals so callers don't repeat the discriminator string. Omitting + # the field on `model_validate` is therefore valid and yields the + # default. If a second variant is added to the spec, the codegen will + # drop the default and this test should flip back to a missing-field + # ValidationError. + policy = ConfirmationPolicy.model_validate( + {"minRegionsFailing": 1, "maxWaitSeconds": 30} + ) + assert policy.type == "multi_region" def test_missing_min_regions_failing(self) -> None: with pytest.raises(ValidationError, match="minRegionsFailing"):