Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 23 additions & 3 deletions src/fastapi_cloud_cli/commands/deploy/cloud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from pydantic import BaseModel

from fastapi_cloud_cli.utils.api import APIClient, DeploymentStatus
from fastapi_cloud_cli.utils.api import (
APIClient,
DeploymentStatus,
_get_response_error_message,
)


class ArchiveTooLargeError(Exception):
pass


class Team(BaseModel):
Expand Down Expand Up @@ -57,8 +65,20 @@ def _create_app(
return AppResponse.model_validate(response.json())


def _create_deployment(client: APIClient, app_id: str) -> CreateDeploymentResponse:
response = client.post(f"/apps/{app_id}/deployments/")
def _create_deployment(
client: APIClient, app_id: str, archive_size_bytes: int
) -> CreateDeploymentResponse:
response = client.post(
f"/apps/{app_id}/deployments/",
json={"archive_size_bytes": archive_size_bytes},
)

if response.status_code == 413:
raise ArchiveTooLargeError(
_get_response_error_message(response)
or "The app source code exceeds the maximum allowed size."
)

response.raise_for_status()

return CreateDeploymentResponse.model_validate(response.json())
Expand Down
36 changes: 33 additions & 3 deletions src/fastapi_cloud_cli/commands/deploy/command.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import logging
import tempfile
from pathlib import Path
from typing import Annotated, Any, cast
from typing import Annotated, Any, NoReturn, cast

import typer
from pydantic import BaseModel
from rich_toolkit.progress import Progress

from fastapi_cloud_cli.commands.deploy.archive import _get_large_files, archive
from fastapi_cloud_cli.commands.deploy.cloud import (
AppResponse,
ArchiveTooLargeError,
CreateDeploymentResponse,
_create_deployment,
_get_app,
)
from fastapi_cloud_cli.commands.deploy.configure import _configure_app
from fastapi_cloud_cli.commands.deploy.upload import _cancel_upload, _upload_deployment
from fastapi_cloud_cli.commands.deploy.upload import (
_cancel_upload,
_upload_deployment,
)
Comment thread
patrick91 marked this conversation as resolved.
Outdated
from fastapi_cloud_cli.commands.deploy.wait import _wait_for_deployment
from fastapi_cloud_cli.commands.login import _interactive_login
from fastapi_cloud_cli.utils.api import APIClient, DeploymentStatus
Expand Down Expand Up @@ -47,6 +52,18 @@ def _get_deploy_output(deployment: CreateDeploymentResponse) -> DeployOutput:
)


def _fail_archive_too_large(
toolkit: FastAPIRichToolkit, progress: Progress, message: str
) -> NoReturn:
hint = "You can exclude files from the deployment with a .fastapicloudignore file."

if toolkit.mode == "json":
toolkit.fail("invalid_input", message, hint=hint)

progress.set_error(f"{message}\n\n[dim]hint: {hint}[/]")
raise typer.Exit(1) from None
Comment thread
patrick91 marked this conversation as resolved.
Outdated


def _get_large_file_warnings(
large_files: list[tuple[Path, int]],
*,
Expand Down Expand Up @@ -313,6 +330,7 @@ def deploy(
logger.debug("Creating archive for deployment")
archive_path = Path(temp_dir) / "archive.tar"
archive(path_to_deploy, archive_path)
archive_size = archive_path.stat().st_size

with (
toolkit.progress(
Expand All @@ -324,7 +342,15 @@ def deploy(
client.handle_http_errors(progress, toolkit=toolkit),
):
logger.debug("Creating deployment for app: %s", app.id)
deployment = _create_deployment(client=client, app_id=app.id)

try:
deployment = _create_deployment(
client=client,
app_id=app.id,
archive_size_bytes=archive_size,
)
except ArchiveTooLargeError as e:
_fail_archive_too_large(toolkit, progress, str(e))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the comment above, with toolkit.fail we can probably remove this floating function


try:
progress.log(
Expand All @@ -335,13 +361,17 @@ def deploy(
fastapi_client=client,
deployment_id=deployment.id,
archive_path=archive_path,
archive_size=archive_size,
progress=progress,
)

progress.log("Deployment uploaded successfully!")
except KeyboardInterrupt:
_cancel_upload(client=client, deployment_id=deployment.id)
raise
except ArchiveTooLargeError as e:
_cancel_upload(client=client, deployment_id=deployment.id)
_fail_archive_too_large(toolkit, progress, str(e))

if will_wait:
logger.debug("Waiting for deployment to complete")
Expand Down
29 changes: 26 additions & 3 deletions src/fastapi_cloud_cli/commands/deploy/upload.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import logging
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import BinaryIO, cast

from httpx import Client
from httpx import Client, Response
from pydantic import BaseModel
from rich_toolkit.progress import Progress

from fastapi_cloud_cli.commands.deploy.cloud import CreateDeploymentResponse
from fastapi_cloud_cli.commands.deploy.cloud import (
ArchiveTooLargeError,
CreateDeploymentResponse,
)
from fastapi_cloud_cli.utils.api import APIClient
from fastapi_cloud_cli.utils.progress_file import ProgressFile

Expand Down Expand Up @@ -39,13 +43,23 @@ def _format_size(size_in_bytes: int) -> str:
return f"{size_in_bytes} bytes"


def _get_s3_error_code(response: Response) -> str | None:
"""Extract the error code from an S3 XML error response."""
try:
root = ET.fromstring(response.text)
except ET.ParseError:
return None

return root.findtext("Code")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big fan of having S3 specific things here, could we avoid it?

(not sure if it is even worth handling this error directly, since it shouldn't ever happen)



def _upload_deployment(
fastapi_client: APIClient,
deployment_id: str,
archive_path: Path,
archive_size: int,
progress: Progress,
) -> CreateDeploymentResponse:
archive_size = archive_path.stat().st_size
archive_size_str = _format_size(archive_size)

progress.log(f"Uploading deployment ({archive_size_str})...")
Expand Down Expand Up @@ -79,6 +93,15 @@ def progress_callback(bytes_read: int) -> None:
files={"file": cast(BinaryIO, archive_file_with_progress)},
)

if upload_response.is_error:
logger.debug("File upload failed with response: %s", upload_response.text)

if _get_s3_error_code(upload_response) == "EntityTooLarge":
raise ArchiveTooLargeError(
f"The deployment archive is {archive_size_str}, "
"which exceeds the maximum allowed size."
)

upload_response.raise_for_status()
logger.debug("File upload completed successfully")

Expand Down
Loading
Loading