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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies = [
"uvicorn[standard] >= 0.15.0",
"rignore >= 0.5.1",
"httpx >= 0.27.0",
"rich-toolkit >= 0.14.5",
"rich-toolkit >= 0.19.4",
"pydantic[email] >= 2.0",
"sentry-sdk >= 2.20.0",
"fastar >= 0.8.0",
Expand Down
86 changes: 79 additions & 7 deletions src/fastapi_cloud_cli/commands/deploy.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import contextlib
import logging
import re
import subprocess
import tempfile
import time
from enum import Enum
from itertools import cycle
from pathlib import Path
from pathlib import Path, PurePosixPath
from textwrap import dedent
from typing import Annotated, Any, Optional, Union

import fastar
import rignore
import typer
from httpx import Client
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError
from rich.text import Text
from rich_toolkit import RichToolkit
from rich_toolkit.menu import Option
Expand All @@ -27,6 +28,39 @@
logger = logging.getLogger(__name__)


def validate_app_directory(v: Optional[str]) -> Optional[str]:
if v is None:
return None

v = v.strip()

if not v:
return None

if v.startswith("~"):
raise ValueError("cannot start with '~'")

path = PurePosixPath(v)

if path.is_absolute():
raise ValueError("must be a relative path, not absolute")

if ".." in path.parts:
raise ValueError("cannot contain '..' path segments")

normalized = path.as_posix()

if not re.fullmatch(r"[A-Za-z0-9._/ -]+", normalized):
raise ValueError(
"contains invalid characters (allowed: letters, numbers, space, / . _ -)"
)

return normalized


AppDirectory = Annotated[Optional[str], AfterValidator(validate_app_directory)]


def _cancel_upload(deployment_id: str) -> None:
logger.debug("Cancelling upload for deployment: %s", deployment_id)

Expand Down Expand Up @@ -113,13 +147,26 @@ def _get_teams() -> list[Team]:
class AppResponse(BaseModel):
id: str
slug: str
directory: Optional[str]


def _update_app(app_id: str, directory: Optional[str]) -> AppResponse:
with APIClient() as client:
response = client.patch(
f"/apps/{app_id}",
json={"directory": directory},
)

response.raise_for_status()

return AppResponse.model_validate(response.json())


def _create_app(team_id: str, app_name: str) -> AppResponse:
def _create_app(team_id: str, app_name: str, directory: Optional[str]) -> AppResponse:
with APIClient() as client:
response = client.post(
"/apps/",
json={"name": app_name, "team_id": team_id},
json={"name": app_name, "team_id": team_id, "directory": directory},
)

response.raise_for_status()
Expand Down Expand Up @@ -332,10 +379,26 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:

toolkit.print_line()

initial_directory = selected_app.directory if selected_app else ""

directory_input = toolkit.input(
title="Path to the directory containing your app (e.g. src, backend):",
tag="dir",
value=initial_directory or "",
placeholder="[italic]Leave empty if it's the current directory[/italic]",
validator=TypeAdapter(AppDirectory),
)

directory: Optional[str] = directory_input if directory_input else None

toolkit.print_line()

toolkit.print("Deployment configuration:", tag="summary")
toolkit.print_line()
toolkit.print(f"Team: [bold]{team.name}[/bold]")
toolkit.print(f"App name: [bold]{app_name}[/bold]")
toolkit.print(f"Directory: [bold]{directory or '.'}[/bold]")

toolkit.print_line()

choice = toolkit.ask(
Expand All @@ -352,12 +415,21 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
toolkit.print("Deployment cancelled.")
raise typer.Exit(0)

if selected_app: # pragma: no cover
app = selected_app
if selected_app:
if directory != selected_app.directory:
with (
toolkit.progress(title="Updating app directory...") as progress,
handle_http_errors(progress),
):
app = _update_app(selected_app.id, directory=directory)

progress.log(f"App directory updated to '{directory or '.'}'")
else:
app = selected_app
else:
with toolkit.progress(title="Creating app...") as progress:
with handle_http_errors(progress):
app = _create_app(team.id, app_name)
app = _create_app(team.id, app_name, directory=directory)

progress.log(f"App created successfully! App slug: {app.slug}")

Expand Down
3 changes: 2 additions & 1 deletion src/fastapi_cloud_cli/utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ def get_rich_toolkit(minimal: bool = False) -> RichToolkit:
theme={
"tag.title": "white on #009485",
"tag": "white on #007166",
"placeholder": "grey85",
"placeholder": "grey62",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this fixes the placeholder that had wrong colors :D

"text": "white",
"selected": "#007166",
"result": "grey85",
"progress": "on #007166",
"error": "red",
"cancelled": "indian_red italic",
},
)

Expand Down
Loading