diff --git a/src/fastapi_cloud_cli/api/_models.py b/src/fastapi_cloud_cli/api/_models.py index 0fc87e66..28899868 100644 --- a/src/fastapi_cloud_cli/api/_models.py +++ b/src/fastapi_cloud_cli/api/_models.py @@ -100,6 +100,8 @@ class DeploymentStatus(str, Enum): waiting_upload = "waiting_upload" upload_cancelled = "upload_cancelled" ready_for_build = "ready_for_build" + build_skipped_filtered = "build_skipped_filtered" + build_skipped_superseded = "build_skipped_superseded" building = "building" extracting = "extracting" extracting_failed = "extracting_failed" @@ -120,12 +122,20 @@ class DeploymentStatus(str, Enum): expired = "expired" failed = "failed" + unknown = "unknown" + + @classmethod + def _missing_(cls, value: object) -> "DeploymentStatus": + return cls.unknown + @classmethod def to_human_readable(cls, status: "DeploymentStatus") -> str: return { cls.waiting_upload: "Awaiting Upload", cls.upload_cancelled: "Upload Cancelled", cls.ready_for_build: "Build Queued", + cls.build_skipped_filtered: "Build Skipped (Filtered)", + cls.build_skipped_superseded: "Build Skipped (Superseded)", cls.building: "Building", cls.extracting: "Extracting Upload", cls.extracting_failed: "Extraction Failed", @@ -145,6 +155,7 @@ def to_human_readable(cls, status: "DeploymentStatus") -> str: cls.degraded: "Degraded", cls.expired: "Expired", cls.failed: "Failed", + cls.unknown: "Unknown", }[status] @@ -167,6 +178,8 @@ class Deployment(BaseModel): DeploymentStatus.extracting_failed_archive_too_large, } FAILED_STATUSES = BUILD_FAILED_STATUSES | { + DeploymentStatus.build_skipped_filtered, + DeploymentStatus.build_skipped_superseded, DeploymentStatus.failed, DeploymentStatus.verifying_failed, DeploymentStatus.verification_failed_oom, diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 2f1c053f..deac1d5e 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -440,6 +440,23 @@ def handler(request: httpx.Request, route: respx.Route) -> Response: assert call_count == 3 +def test_poll_deployment_status_keeps_polling_on_unknown_status( + poll_route: respx.Route, client: APIClient, deployment_id: str +) -> None: + poll_route.mock( + side_effect=[ + Response(200, json={"status": "some_new_status"}), + Response(200, json={"status": "success"}), + ] + ) + + with patch("time.sleep"): + status = client.poll_deployment_status(deployment_id) + + assert status == DeploymentStatus.success + assert poll_route.call_count == 2 + + def test_poll_deployment_status_raises_after_max_consecutive_errors( poll_route: respx.Route, client: APIClient, deployment_id: str ) -> None: diff --git a/tests/test_deploy_utils.py b/tests/test_deploy_utils.py index b51c6f68..a7806cf3 100644 --- a/tests/test_deploy_utils.py +++ b/tests/test_deploy_utils.py @@ -2,7 +2,7 @@ import pytest -from fastapi_cloud_cli.api import DeploymentStatus +from fastapi_cloud_cli.api import Deployment, DeploymentStatus from fastapi_cloud_cli.commands.deploy.archive import ( _get_large_files, _should_exclude_entry, @@ -69,6 +69,8 @@ def test_includes_paths(path: Path) -> None: [ (DeploymentStatus.waiting_upload, "Awaiting Upload"), (DeploymentStatus.ready_for_build, "Build Queued"), + (DeploymentStatus.build_skipped_filtered, "Build Skipped (Filtered)"), + (DeploymentStatus.build_skipped_superseded, "Build Skipped (Superseded)"), (DeploymentStatus.building, "Building"), (DeploymentStatus.extracting, "Extracting Upload"), (DeploymentStatus.extracting_failed, "Extraction Failed"), @@ -87,6 +89,7 @@ def test_includes_paths(path: Path) -> None: (DeploymentStatus.degraded, "Degraded"), (DeploymentStatus.expired, "Expired"), (DeploymentStatus.failed, "Failed"), + (DeploymentStatus.unknown, "Unknown"), ], ) def test_deployment_status_to_human_readable( @@ -103,6 +106,20 @@ def test_every_deployment_status_has_human_readable( assert DeploymentStatus.to_human_readable(status) +def test_deployment_model_accepts_unknown_status() -> None: + deployment = Deployment.model_validate( + { + "id": "dep-1", + "app_id": "app-1", + "slug": "my-app", + "status": "some_new_status", + "created_at": "2026-01-01T00:00:00Z", + } + ) + + assert deployment.status is DeploymentStatus.unknown + + @pytest.mark.parametrize( "value,expected", [