Skip to content

Commit c4cdda3

Browse files
committed
✨ Allow to specify application directory
Shortcake-Parent: main
1 parent 823a5df commit c4cdda3

7 files changed

Lines changed: 318 additions & 25 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dependencies = [
3333
"uvicorn[standard] >= 0.15.0",
3434
"rignore >= 0.5.1",
3535
"httpx >= 0.27.0",
36-
"rich-toolkit >= 0.14.5",
36+
"rich-toolkit >= 0.19.4",
3737
"pydantic[email] >= 2.0",
3838
"sentry-sdk >= 2.20.0",
3939
"fastar >= 0.8.0",

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import contextlib
22
import logging
3+
import re
34
import subprocess
45
import tempfile
56
import time
67
from enum import Enum
78
from itertools import cycle
8-
from pathlib import Path
9+
from pathlib import Path, PurePosixPath
910
from textwrap import dedent
1011
from typing import Annotated, Any, Optional, Union
1112

1213
import fastar
1314
import rignore
1415
import typer
1516
from httpx import Client
16-
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
17+
from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError
1718
from rich.text import Text
1819
from rich_toolkit import RichToolkit
1920
from rich_toolkit.menu import Option
@@ -27,6 +28,39 @@
2728
logger = logging.getLogger(__name__)
2829

2930

31+
def validate_app_directory(v: Optional[str]) -> Optional[str]:
32+
if v is None:
33+
return None
34+
35+
v = v.strip()
36+
37+
if not v:
38+
return None
39+
40+
if v.startswith("~"):
41+
raise ValueError("cannot start with '~'")
42+
43+
path = PurePosixPath(v)
44+
45+
if path.is_absolute():
46+
raise ValueError("must be a relative path, not absolute")
47+
48+
if ".." in path.parts:
49+
raise ValueError("cannot contain '..' path segments")
50+
51+
normalized = path.as_posix()
52+
53+
if not re.fullmatch(r"[A-Za-z0-9._/ -]+", normalized):
54+
raise ValueError(
55+
"contains invalid characters (allowed: letters, numbers, space, / . _ -)"
56+
)
57+
58+
return normalized
59+
60+
61+
AppDirectory = Annotated[Optional[str], AfterValidator(validate_app_directory)]
62+
63+
3064
def _cancel_upload(deployment_id: str) -> None:
3165
logger.debug("Cancelling upload for deployment: %s", deployment_id)
3266

@@ -113,13 +147,14 @@ def _get_teams() -> list[Team]:
113147
class AppResponse(BaseModel):
114148
id: str
115149
slug: str
150+
directory: Optional[str]
116151

117152

118-
def _create_app(team_id: str, app_name: str) -> AppResponse:
153+
def _create_app(team_id: str, app_name: str, directory: Optional[str]) -> AppResponse:
119154
with APIClient() as client:
120155
response = client.post(
121156
"/apps/",
122-
json={"name": app_name, "team_id": team_id},
157+
json={"name": app_name, "team_id": team_id, "directory": directory},
123158
)
124159

125160
response.raise_for_status()
@@ -332,10 +367,26 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
332367

333368
toolkit.print_line()
334369

370+
initial_directory = selected_app.directory if selected_app else ""
371+
372+
directory_input = toolkit.input(
373+
title="Path to the directory containing your app (e.g. src, backend):",
374+
tag="dir",
375+
value=initial_directory or "",
376+
placeholder="[italic]Leave empty if it's the current directory[/italic]",
377+
validator=TypeAdapter(AppDirectory),
378+
)
379+
380+
directory: Optional[str] = directory_input if directory_input else None
381+
382+
toolkit.print_line()
383+
335384
toolkit.print("Deployment configuration:", tag="summary")
336385
toolkit.print_line()
337386
toolkit.print(f"Team: [bold]{team.name}[/bold]")
338387
toolkit.print(f"App name: [bold]{app_name}[/bold]")
388+
toolkit.print(f"Directory: [bold]{directory or '.'}[/bold]")
389+
339390
toolkit.print_line()
340391

341392
choice = toolkit.ask(
@@ -357,7 +408,7 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
357408
else:
358409
with toolkit.progress(title="Creating app...") as progress:
359410
with handle_http_errors(progress):
360-
app = _create_app(team.id, app_name)
411+
app = _create_app(team.id, app_name, directory=directory)
361412

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

src/fastapi_cloud_cli/utils/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ def get_rich_toolkit(minimal: bool = False) -> RichToolkit:
5656
theme={
5757
"tag.title": "white on #009485",
5858
"tag": "white on #007166",
59-
"placeholder": "grey85",
59+
"placeholder": "grey62",
6060
"text": "white",
6161
"selected": "#007166",
6262
"result": "grey85",
6363
"progress": "on #007166",
6464
"error": "red",
65+
"cancelled": "indian_red italic",
6566
},
6667
)
6768

0 commit comments

Comments
 (0)