From 2066c281bccb41edc7fe051dbccbea3ad91c6fcc Mon Sep 17 00:00:00 2001 From: caballeto Date: Wed, 6 May 2026 15:31:08 +0200 Subject: [PATCH 1/2] fix(types): handle CurrentStatus enum suffix shift after MonitorDto adds currentStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mono v0.13.0 adds `MonitorDto.currentStatus`. datamodel-code-generator names inline enums by iteration order, so the existing inline StatusPageComponentDto.currentStatus enum shifts from CurrentStatus1 to CurrentStatus2 (MonitorDto + ResultSummaryDto share CurrentStatus — deduped because their value sets are identical). Update the type alias in `types.py` to match the new generator output and re-pin _generated.py + spec to mono v0.13.0. Adds a comment explaining the suffix-stability hazard so future authors don't repeat this lesson the hard way. Caught by mono#369's Spec Evolution Harness — 16 sdk-python tests + 11 mcp-server tests (which depends on sdk-python) failed to import. Co-authored-by: Cursor --- docs/openapi/monitoring-api.json | 210 +++++++++++++++++++++++++++++-- src/devhelm/_generated.py | 135 ++++++++++++++++---- src/devhelm/types.py | 14 ++- uv.lock | 2 +- 4 files changed, 319 insertions(+), 42 deletions(-) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index 02f8524..b60c933 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -166,8 +166,52 @@ "Alert Channels" ], "summary": "List active alert channels for the authenticated org", + "description": "Supports filtering by `type` (channel integration), `managedBy` (creating surface), and `search` (case-insensitive contains on name). Unrecognised query parameters are silently ignored — pin to the documented set above.", "operationId": "list_14", "parameters": [ + { + "name": "type", + "in": "query", + "description": "Filter by channel integration type (e.g. SLACK, WEBHOOK, EMAIL)", + "required": false, + "schema": { + "type": "string", + "enum": [ + "email", + "webhook", + "slack", + "pagerduty", + "opsgenie", + "teams", + "discord" + ] + } + }, + { + "name": "managedBy", + "in": "query", + "description": "Filter by managed-by source (DASHBOARD, CLI, TERRAFORM, MCP, API)", + "required": false, + "schema": { + "type": "string", + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] + } + }, + { + "name": "search", + "in": "query", + "description": "Case-insensitive contains-match on the channel name", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "pageable", "in": "query", @@ -6374,17 +6418,27 @@ "Monitors" ], "summary": "List monitors for the authenticated org", + "description": "Supports filtering by `enabled`, `status` (alias active|paused for enabled), `type`, `managedBy`, `tag` / `tags`, `search`, and `environmentId`. Unrecognised query parameters are silently ignored (Spring's default binding behaviour) — pin to the documented set above.", "operationId": "list_8", "parameters": [ { "name": "enabled", "in": "query", - "description": "Filter by enabled state", + "description": "Filter by enabled state (true/false)", "required": false, "schema": { "type": "boolean" } }, + { + "name": "status", + "in": "query", + "description": "Lifecycle status alias: 'active' (enabled=true) or 'paused' (enabled=false). Ignored when ?enabled is also supplied.", + "required": false, + "schema": { + "type": "string" + } + }, { "name": "type", "in": "query", @@ -6421,7 +6475,16 @@ { "name": "tags", "in": "query", - "description": "Filter by tag names, comma-separated (e.g. prod,critical)", + "description": "Filter by tag names, comma-separated (e.g. prod,critical); OR semantics", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "tag", + "in": "query", + "description": "Filter by a single tag name (alias for ?tags=); merged with ?tags using OR semantics", "required": false, "schema": { "type": "string" @@ -21729,6 +21792,18 @@ "description": "SHA-256 hash of the channel config; use for change detection", "nullable": true }, + "managedBy": { + "type": "string", + "description": "Source that created/owns this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on channels created before this attribution column existed.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] + }, "lastDeliveryAt": { "type": "string", "description": "Timestamp of the most recent delivery attempt", @@ -22929,6 +23004,18 @@ "$ref": "#/components/schemas/WebhookChannelConfig" } ] + }, + "managedBy": { + "type": "string", + "description": "Source creating this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } } }, @@ -23189,12 +23276,14 @@ "maxLength": 100, "minLength": 0, "type": "string", - "description": "iCal RRULE for recurring windows (max 100 chars); null for one-time", + "description": "Reserved: iCal RRULE for recurring windows (stored but not yet honored)", "nullable": true }, "reason": { + "maxLength": 500, + "minLength": 0, "type": "string", - "description": "Human-readable reason for the maintenance", + "description": "Human-readable reason for the maintenance (max 500 chars)", "nullable": true }, "suppressAlerts": { @@ -23241,7 +23330,6 @@ "CreateMonitorRequest": { "required": [ "config", - "managedBy", "name", "type" ], @@ -23309,7 +23397,8 @@ }, "managedBy": { "type": "string", - "description": "Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API. Use the value matching your surface so audit logs, drift detection, and analytics attribute correctly.", + "description": "Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted; set to your surface so audit logs, drift detection, and analytics attribute correctly.", + "nullable": true, "enum": [ "DASHBOARD", "CLI", @@ -23510,6 +23599,18 @@ "description": "Recovery cooldown in minutes after group incident resolves (0–60)", "format": "int32", "nullable": true + }, + "managedBy": { + "type": "string", + "description": "Source creating this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } }, "description": "Request body for creating a resource group" @@ -23811,6 +23912,18 @@ "REVIEW", "AUTOMATIC" ] + }, + "managedBy": { + "type": "string", + "description": "Source creating this page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } } }, @@ -26436,7 +26549,7 @@ }, "repeatRule": { "type": "string", - "description": "iCal RRULE for recurring windows; null for one-time", + "description": "Reserved: iCal RRULE for recurring windows (stored but not yet honored)", "nullable": true }, "reason": { @@ -27271,6 +27384,18 @@ "description": "Alert channel IDs linked to this monitor; populated on single-monitor responses", "format": "uuid" } + }, + "currentStatus": { + "type": "string", + "description": "Current operational state — UP, DOWN, DEGRADED, PAUSED, or UNKNOWN if no probe data yet", + "nullable": true, + "enum": [ + "up", + "degraded", + "down", + "paused", + "unknown" + ] } }, "description": "Full monitor representation" @@ -28461,6 +28586,18 @@ "$ref": "#/components/schemas/ResourceGroupMemberDto" } }, + "managedBy": { + "type": "string", + "description": "Source that created/owns this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on groups created before this attribution column existed.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] + }, "createdAt": { "type": "string", "description": "Timestamp when the group was created", @@ -28721,6 +28858,7 @@ "up", "degraded", "down", + "paused", "unknown" ] }, @@ -31056,6 +31194,18 @@ "UNDER_MAINTENANCE" ] }, + "managedBy": { + "type": "string", + "description": "Source that created/owns this status page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Null on pages created before this attribution column existed.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] + }, "createdAt": { "type": "string", "format": "date-time" @@ -32999,6 +33149,18 @@ "$ref": "#/components/schemas/WebhookChannelConfig" } ] + }, + "managedBy": { + "type": "string", + "description": "New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } } }, @@ -33239,7 +33401,7 @@ "properties": { "monitorId": { "type": "string", - "description": "Monitor to attach this maintenance window to; null preserves current", + "description": "Monitor this window applies to; null switches the window to org-wide", "format": "uuid", "nullable": true }, @@ -33257,17 +33419,19 @@ "maxLength": 100, "minLength": 0, "type": "string", - "description": "Updated iCal RRULE; null clears the repeat rule", + "description": "Reserved: iCal RRULE for recurring windows (stored but not yet honored); null clears it", "nullable": true }, "reason": { + "maxLength": 500, + "minLength": 0, "type": "string", - "description": "Updated reason; null clears the existing reason", + "description": "Updated reason (max 500 chars); null clears the existing reason", "nullable": true }, "suppressAlerts": { "type": "boolean", - "description": "Whether to suppress alerts; null preserves current", + "description": "Whether to suppress alerts during this window; null defaults to true", "nullable": true } } @@ -33605,6 +33769,18 @@ "description": "Recovery cooldown in minutes; null clears", "format": "int32", "nullable": true + }, + "managedBy": { + "type": "string", + "description": "New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } }, "description": "Request body for updating a resource group" @@ -33809,6 +33985,18 @@ "REVIEW", "AUTOMATIC" ] + }, + "managedBy": { + "type": "string", + "description": "New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI", + "TERRAFORM", + "MCP", + "API" + ] } } }, diff --git a/src/devhelm/_generated.py b/src/devhelm/_generated.py index aedb376..b3d4e2f 100644 --- a/src/devhelm/_generated.py +++ b/src/devhelm/_generated.py @@ -155,6 +155,14 @@ class ChannelType(StrEnum): 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")] @@ -186,6 +194,13 @@ class AlertChannelDto(BaseModel): description="SHA-256 hash of the channel config; use for change detection", ), ] = None + managed_by: Annotated[ + ManagedBy | 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.", + ), + ] = None last_delivery_at: Annotated[ AwareDatetime | None, Field( @@ -814,13 +829,18 @@ class CreateMaintenanceWindowRequest(BaseModel): str | None, Field( alias="repeatRule", - description="iCal RRULE for recurring windows (max 100 chars); null for one-time", + description="Reserved: iCal RRULE for recurring windows (stored but not yet honored)", max_length=100, min_length=0, ), ] = None reason: Annotated[ - str | None, Field(description="Human-readable reason for the maintenance") + str | None, + Field( + description="Human-readable reason for the maintenance (max 500 chars)", + max_length=500, + min_length=0, + ), ] = None suppress_alerts: Annotated[ bool | None, @@ -865,14 +885,6 @@ class Type(StrEnum): heartbeat = "HEARTBEAT" -class ManagedBy(StrEnum): - dashboard = "DASHBOARD" - cli = "CLI" - terraform = "TERRAFORM" - mcp = "MCP" - api = "API" - - class HealthThresholdType(StrEnum): count = "COUNT" # type: ignore[assignment] percentage = "PERCENTAGE" @@ -2528,7 +2540,7 @@ class MaintenanceWindowDto(BaseModel): str | None, Field( alias="repeatRule", - description="iCal RRULE for recurring windows; null for one-time", + description="Reserved: iCal RRULE for recurring windows (stored but not yet honored)", ), ] = None reason: Annotated[ @@ -2813,6 +2825,14 @@ class Type3(StrEnum): 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")] @@ -3600,13 +3620,6 @@ class ResponseTimeWarnAssertion(BaseModel): ] -class CurrentStatus(StrEnum): - up = "up" - degraded = "degraded" - down = "down" - unknown = "unknown" - - class ResultSummaryDto(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) current_status: Annotated[ @@ -4597,7 +4610,7 @@ class Type5(StrEnum): static = "STATIC" -class CurrentStatus1(StrEnum): +class CurrentStatus2(StrEnum): operational = "OPERATIONAL" degraded_performance = "DEGRADED_PERFORMANCE" partial_outage = "PARTIAL_OUTAGE" @@ -4615,7 +4628,7 @@ class StatusPageComponentDto(BaseModel): 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")] + current_status: Annotated[CurrentStatus2, Field(alias="currentStatus")] show_uptime: Annotated[bool, Field(alias="showUptime")] display_order: Annotated[int, Field(alias="displayOrder")] page_order: Annotated[int, Field(alias="pageOrder")] @@ -4699,6 +4712,13 @@ class StatusPageDto(BaseModel): 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 + managed_by: Annotated[ + ManagedBy | 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.", + ), + ] = None created_at: Annotated[AwareDatetime, Field(alias="createdAt")] updated_at: Annotated[AwareDatetime, Field(alias="updatedAt")] @@ -5419,7 +5439,7 @@ class UpdateMaintenanceWindowRequest(BaseModel): UUID | None, Field( alias="monitorId", - description="Monitor to attach this maintenance window to; null preserves current", + description="Monitor this window applies to; null switches the window to org-wide", ), ] = None starts_at: Annotated[ @@ -5433,19 +5453,24 @@ class UpdateMaintenanceWindowRequest(BaseModel): str | None, Field( alias="repeatRule", - description="Updated iCal RRULE; null clears the repeat rule", + description="Reserved: iCal RRULE for recurring windows (stored but not yet honored); null clears it", max_length=100, min_length=0, ), ] = None reason: Annotated[ - str | None, Field(description="Updated reason; null clears the existing reason") + str | None, + Field( + description="Updated reason (max 500 chars); null clears the existing reason", + max_length=500, + min_length=0, + ), ] = None suppress_alerts: Annotated[ bool | None, Field( alias="suppressAlerts", - description="Whether to suppress alerts; null preserves current", + description="Whether to suppress alerts during this window; null defaults to true", ), ] = None @@ -5591,6 +5616,13 @@ class UpdateResourceGroupRequest(BaseModel): le=60, ), ] = None + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + ), + ] = None class UpdateSecretRequest(BaseModel): @@ -5774,6 +5806,13 @@ class UpdateStatusPageRequest(BaseModel): description="Incident mode: MANUAL, REVIEW, or AUTOMATIC; null preserves current", ), ] = None + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + ), + ] = None class UpdateTagRequest(BaseModel): @@ -6195,6 +6234,13 @@ class CreateAlertChannelRequest(BaseModel): | WebhookChannelConfig, Field(discriminator="channel_type"), ] + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="Source creating this channel: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + ), + ] = None class CreateAssertionRequest(BaseModel): @@ -6286,12 +6332,12 @@ class CreateMonitorRequest(BaseModel): Field(description="Probe regions to run checks from, e.g. us-east, eu-west"), ] = None managed_by: Annotated[ - ManagedBy, + ManagedBy | None, Field( alias="managedBy", - description="Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API. Use the value matching your surface so audit logs, drift detection, and analytics attribute correctly.", + description="Source that created/owns this monitor: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted; set to your surface so audit logs, drift detection, and analytics attribute correctly.", ), - ] + ] = None environment_id: Annotated[ UUID | None, Field( @@ -6409,6 +6455,13 @@ class CreateResourceGroupRequest(BaseModel): le=60, ), ] = None + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="Source creating this group: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + ), + ] = None class CreateStatusPageRequest(BaseModel): @@ -6455,6 +6508,13 @@ class CreateStatusPageRequest(BaseModel): description="Incident mode: MANUAL, REVIEW, or AUTOMATIC (default: AUTOMATIC)", ), ] = None + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="Source creating this page: DASHBOARD, CLI, TERRAFORM, MCP, or API. Defaults to API when omitted.", + ), + ] = None class CursorPageServiceCatalogDto(BaseModel): @@ -6896,6 +6956,13 @@ class MonitorDto(BaseModel): description="Alert channel IDs linked to this monitor; populated on single-monitor responses", ), ] = None + current_status: Annotated[ + CurrentStatus | None, + Field( + alias="currentStatus", + description="Current operational state — UP, DOWN, DEGRADED, PAUSED, or UNKNOWN if no probe data yet", + ), + ] = None class MonitorTestRequest(BaseModel): @@ -7075,6 +7142,13 @@ class ResourceGroupDto(BaseModel): description="Member list with individual statuses; populated on detail GET only" ), ] = None + managed_by: Annotated[ + ManagedBy | 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.", + ), + ] = None created_at: Annotated[ AwareDatetime, Field(alias="createdAt", description="Timestamp when the group was created"), @@ -7475,6 +7549,13 @@ class UpdateAlertChannelRequest(BaseModel): | WebhookChannelConfig, Field(discriminator="channel_type"), ] + managed_by: Annotated[ + ManagedBy | None, + Field( + alias="managedBy", + description="New attribution source: DASHBOARD, CLI, TERRAFORM, MCP, or API; null preserves current value.", + ), + ] = None class UpdateMonitorRequest(BaseModel): diff --git a/src/devhelm/types.py b/src/devhelm/types.py index 2996623..eed19e6 100644 --- a/src/devhelm/types.py +++ b/src/devhelm/types.py @@ -143,11 +143,19 @@ ) from devhelm._generated import ( # - # CurrentStatus enums - CurrentStatus as MonitorCurrentStatus, # ResultSummaryDto.current_status + # 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 ( - CurrentStatus1 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status + CurrentStatus2 as StatusPageComponentCurrentStatus, # StatusPageComponentDto.current_status ) from devhelm._generated import ( # diff --git a/uv.lock b/uv.lock index 1eb3963..890e24c 100644 --- a/uv.lock +++ b/uv.lock @@ -331,7 +331,7 @@ wheels = [ [[package]] name = "devhelm" -version = "0.5.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From fd0a852e4542ea77e44d012393f998d861434d15 Mon Sep 17 00:00:00 2001 From: caballeto Date: Wed, 6 May 2026 15:33:25 +0200 Subject: [PATCH 2/2] test: managedBy is optional on Create as of mono v0.13 The previous test asserted managedBy was required. mono v0.13 made it optional with a server-side default of API, so SDK consumers can omit it. Replace the negative test with a positive assertion that omitting it parses cleanly with managed_by == None. Co-authored-by: Cursor --- tests/test_negative_validation.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/test_negative_validation.py b/tests/test_negative_validation.py index 0770e16..75f24b5 100644 --- a/tests/test_negative_validation.py +++ b/tests/test_negative_validation.py @@ -546,15 +546,18 @@ def test_missing_config(self) -> None: {"name": "X", "type": "HTTP", "managedBy": "DASHBOARD"} ) - def test_missing_managed_by(self) -> None: - with pytest.raises(ValidationError, match="managedBy"): - CreateMonitorRequest.model_validate( - { - "name": "X", - "type": "HTTP", - "config": {"url": "https://x.com", "method": "GET"}, - } - ) + def test_managed_by_optional_defaults_server_side(self) -> None: + # As of mono v0.13, managedBy is optional on Create requests and + # defaults to API server-side. The SDK should accept payloads + # without it. + req = CreateMonitorRequest.model_validate( + { + "name": "X", + "type": "HTTP", + "config": {"url": "https://x.com", "method": "GET"}, + } + ) + assert req.managed_by is None def test_invalid_type_enum(self) -> None: with pytest.raises(ValidationError):