From 75a2072b53563625c546c99a59382213cefe0e98 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 23 Jun 2026 10:40:47 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20new=20deployment=20r?= =?UTF-8?q?oute=20to=20fetch=20deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shortcake-Parent: main --- src/fastapi_cloud_cli/commands/deploy/wait.py | 7 +--- src/fastapi_cloud_cli/commands/deployments.py | 9 ++--- src/fastapi_cloud_cli/utils/api.py | 3 +- tests/test_api_client.py | 25 ++++-------- tests/test_cli_deploy.py | 38 +++++++++---------- tests/test_cli_deployments.py | 10 ++--- 6 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/fastapi_cloud_cli/commands/deploy/wait.py b/src/fastapi_cloud_cli/commands/deploy/wait.py index 3f7ec40a..ad7f8c27 100644 --- a/src/fastapi_cloud_cli/commands/deploy/wait.py +++ b/src/fastapi_cloud_cli/commands/deploy/wait.py @@ -46,7 +46,6 @@ def _verify_deployment( toolkit: RichToolkit, client: APIClient, - app_id: str, deployment: CreateDeploymentResponse, ) -> None: failed_status: str | None = None @@ -57,7 +56,7 @@ def _verify_deployment( done_emoji="✅", ) as progress: try: - final_status = client.poll_deployment_status(app_id, deployment.id) + final_status = client.poll_deployment_status(deployment.id) except (TimeoutError, TooManyRetriesError, StreamLogError): progress.metadata["done_emoji"] = "⚠️" progress.current_message = ( @@ -168,6 +167,4 @@ def _wait_for_deployment( if build_complete: toolkit.print_line() - _verify_deployment( - toolkit=toolkit, client=client, app_id=app_id, deployment=deployment - ) + _verify_deployment(toolkit=toolkit, client=client, deployment=deployment) diff --git a/src/fastapi_cloud_cli/commands/deployments.py b/src/fastapi_cloud_cli/commands/deployments.py index 489621ed..30e76e25 100644 --- a/src/fastapi_cloud_cli/commands/deployments.py +++ b/src/fastapi_cloud_cli/commands/deployments.py @@ -95,10 +95,8 @@ def _get_deployments( ) -def _get_deployment( - client: APIClient, *, app_id: str, deployment_id: str -) -> DeploymentGetOutput: - response = client.get(f"/apps/{app_id}/deployments/{deployment_id}") +def _get_deployment(client: APIClient, *, deployment_id: str) -> DeploymentGetOutput: + response = client.get(f"/deployments/{deployment_id}") response.raise_for_status() return DeploymentGetOutput(deployment=Deployment.model_validate(response.json())) @@ -344,7 +342,7 @@ def get_deployment( hint="Run `fastapi cloud login` or set FASTAPI_CLOUD_TOKEN.", ) - target_app_id = resolve_app_id_or_fail(toolkit, app_id=app_id) + resolve_app_id_or_fail(toolkit, app_id=app_id) with APIClient() as client: with toolkit.progress( @@ -359,7 +357,6 @@ def get_deployment( ): result = _get_deployment( client, - app_id=target_app_id, deployment_id=deployment_id, ) diff --git a/src/fastapi_cloud_cli/utils/api.py b/src/fastapi_cloud_cli/utils/api.py index 3a4a5e24..35bf364e 100644 --- a/src/fastapi_cloud_cli/utils/api.py +++ b/src/fastapi_cloud_cli/utils/api.py @@ -487,7 +487,6 @@ def stream_app_logs( def poll_deployment_status( self, - app_id: str, deployment_id: str, ) -> DeploymentStatus: start = time.monotonic() @@ -498,7 +497,7 @@ def poll_deployment_status( raise TimeoutError("Deployment verification timed out") with attempt(error_count): - response = self.get(f"/apps/{app_id}/deployments/{deployment_id}") + response = self.get(f"/deployments/{deployment_id}") response.raise_for_status() status = DeploymentStatus(response.json()["status"]) error_count = 0 diff --git a/tests/test_api_client.py b/tests/test_api_client.py index 66fc43f1..bb3cf675 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -389,19 +389,12 @@ def responses(request: httpx.Request, route: respx.Route) -> Response: @pytest.fixture -def app_id() -> str: - return "test-app-456" - - -@pytest.fixture -def poll_route( - respx_mock: respx.MockRouter, app_id: str, deployment_id: str -) -> respx.Route: - return respx_mock.get(f"/apps/{app_id}/deployments/{deployment_id}") +def poll_route(respx_mock: respx.MockRouter, deployment_id: str) -> respx.Route: + return respx_mock.get(f"/deployments/{deployment_id}") def test_poll_deployment_status_recovers_from_transient_errors( - poll_route: respx.Route, client: APIClient, app_id: str, deployment_id: str + poll_route: respx.Route, client: APIClient, deployment_id: str ) -> None: call_count = 0 @@ -415,26 +408,24 @@ def handler(request: httpx.Request, route: respx.Route) -> Response: poll_route.mock(side_effect=handler) with patch("time.sleep"): - status = client.poll_deployment_status(app_id, deployment_id) + status = client.poll_deployment_status(deployment_id) assert status == DeploymentStatus.success assert call_count == 3 def test_poll_deployment_status_raises_after_max_consecutive_errors( - poll_route: respx.Route, client: APIClient, app_id: str, deployment_id: str + poll_route: respx.Route, client: APIClient, deployment_id: str ) -> None: poll_route.mock(return_value=Response(500)) with patch("time.sleep"), pytest.raises(TooManyRetriesError): - client.poll_deployment_status(app_id, deployment_id) + client.poll_deployment_status(deployment_id) -def test_poll_deployment_status_timeout( - client: APIClient, app_id: str, deployment_id: str -) -> None: +def test_poll_deployment_status_timeout(client: APIClient, deployment_id: str) -> None: with ( patch("fastapi_cloud_cli.utils.api.POLL_TIMEOUT", timedelta(seconds=-1)), pytest.raises(TimeoutError, match="timed out"), ): - client.poll_deployment_status(app_id, deployment_id) + client.poll_deployment_status(deployment_id) diff --git a/tests/test_cli_deploy.py b/tests/test_cli_deploy.py index 7c4917c8..80390703 100644 --- a/tests/test_cli_deploy.py +++ b/tests/test_cli_deploy.py @@ -844,7 +844,7 @@ def test_updates_app_directory_via_api_when_changed( ) ) - respx_mock.get(f"/apps/{app_data['id']}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -917,7 +917,7 @@ def test_does_not_update_app_directory_when_unchanged( ) ) - respx_mock.get(f"/apps/{app_data['id']}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -999,7 +999,7 @@ def test_exits_successfully_when_deployment_is_done( ) ) - respx_mock.get(f"/apps/{app_data['id']}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1065,7 +1065,7 @@ def test_exits_successfully_when_deployment_is_done_when_app_is_configured( ) ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1497,7 +1497,7 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: side_effect=build_logs_handler ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1581,7 +1581,7 @@ def build_logs_handler(request: httpx.Request, route: respx.Route) -> Response: side_effect=build_logs_handler ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1733,7 +1733,7 @@ def test_deploy_successfully_with_token( ) respx_mock.get( - f"/apps/{app_id}/deployments/{deployment_data['id']}", + f"/deployments/{deployment_data['id']}", headers={"Authorization": "Bearer hello"}, ).mock(return_value=Response(200, json={**deployment_data, "status": "success"})) @@ -1856,7 +1856,7 @@ def test_upload_deployment_progress( ), ) ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_id}").mock( + respx_mock.get(f"/deployments/{deployment_id}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1919,7 +1919,7 @@ def test_deploy_with_app_id_arg( ) ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -1971,7 +1971,7 @@ def test_deploy_with_app_id_from_env_var( ) ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2028,7 +2028,7 @@ def test_deploy_with_app_id_matching_local_config( ) ) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2186,7 +2186,7 @@ def test_verification_failure_after_build_complete( _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response( 200, json={**deployment_data, "status": "verifying_failed"} ) @@ -2221,7 +2221,7 @@ def poll_handler(request: httpx.Request, route: respx.Route) -> Response: return Response(200, json={**deployment_data, "status": "verifying"}) return Response(200, json={**deployment_data, "status": "success"}) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( side_effect=poll_handler ) @@ -2268,7 +2268,7 @@ def test_verifying_skipped_treated_as_success( _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response( 200, json={**deployment_data, "status": "verifying_skipped"} ) @@ -2369,7 +2369,7 @@ def test_large_file_threshold_warning( deployment_data = _get_random_deployment(app_id=app_id) _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2397,7 +2397,7 @@ def test_large_file_threshold_only_top_three_files_with_more_indicator( deployment_data = _get_random_deployment(app_id=app_id) _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2429,7 +2429,7 @@ def test_large_file_threshold_does_not_warn_when_no_large_files( deployment_data = _get_random_deployment(app_id=app_id) _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2455,7 +2455,7 @@ def test_large_file_threshold_custom_threshold( deployment_data = _get_random_deployment(app_id=app_id) _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) @@ -2480,7 +2480,7 @@ def test_large_file_threshold_custom_threshold_envvar( deployment_data = _get_random_deployment(app_id=app_id) _setup_deployment_mocks(respx_mock, app_id, team_id, deployment_data, tmp_path) - respx_mock.get(f"/apps/{app_id}/deployments/{deployment_data['id']}").mock( + respx_mock.get(f"/deployments/{deployment_data['id']}").mock( return_value=Response(200, json={**deployment_data, "status": "success"}) ) diff --git a/tests/test_cli_deployments.py b/tests/test_cli_deployments.py index 145cbb90..82deaf5f 100644 --- a/tests/test_cli_deployments.py +++ b/tests/test_cli_deployments.py @@ -232,7 +232,7 @@ def test_gets_deployment_as_json_with_app_id( "url": "https://api.fastapicloud.app", "dashboard_url": "https://dashboard.fastapicloud.com/acme/apps/api/deployments/api-20260522", } - respx_mock.get(f"/apps/{app_id}/deployments/{deployment['id']}").mock( + respx_mock.get(f"/deployments/{deployment['id']}").mock( return_value=Response(200, json=deployment) ) @@ -268,9 +268,9 @@ def test_gets_deployment_as_json_uses_linked_app( "url": "https://api.fastapicloud.app", "dashboard_url": "https://dashboard.fastapicloud.com/acme/apps/api/deployments/api-20260522", } - respx_mock.get( - f"/apps/{configured_app.app_id}/deployments/{deployment['id']}" - ).mock(return_value=Response(200, json=deployment)) + respx_mock.get(f"/deployments/{deployment['id']}").mock( + return_value=Response(200, json=deployment) + ) with changing_dir(configured_app.path): result = runner.invoke(app, ["deployments", "get", deployment["id"], "--json"]) @@ -315,7 +315,7 @@ def test_gets_deployment_in_human_output( "url": "https://api.fastapicloud.app", "dashboard_url": "https://dashboard.example.com/d/api-20260522", } - respx_mock.get(f"/apps/{app_id}/deployments/{deployment['id']}").mock( + respx_mock.get(f"/deployments/{deployment['id']}").mock( return_value=Response(200, json=deployment) )