Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/fastapi_cloud_cli/commands/deploy/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
def _verify_deployment(
toolkit: RichToolkit,
client: APIClient,
app_id: str,
deployment: CreateDeploymentResponse,
) -> None:
failed_status: str | None = None
Expand All @@ -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 = (
Expand Down Expand Up @@ -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)
9 changes: 3 additions & 6 deletions src/fastapi_cloud_cli/commands/deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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(
Expand All @@ -359,7 +357,6 @@ def get_deployment(
):
result = _get_deployment(
client,
app_id=target_app_id,
deployment_id=deployment_id,
)

Expand Down
3 changes: 1 addition & 2 deletions src/fastapi_cloud_cli/utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,6 @@ def stream_app_logs(

def poll_deployment_status(
self,
app_id: str,
deployment_id: str,
) -> DeploymentStatus:
start = time.monotonic()
Expand All @@ -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
Expand Down
25 changes: 8 additions & 17 deletions tests/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
38 changes: 19 additions & 19 deletions tests/test_cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"}))

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"}
)
Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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"}
)
Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand Down Expand Up @@ -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"})
)

Expand All @@ -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"})
)

Expand All @@ -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"})
)

Expand Down
10 changes: 5 additions & 5 deletions tests/test_cli_deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)

Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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)
)

Expand Down
Loading