Coverage for src/fastapi_cloud_cli/commands/apps/create.py: 100%
62 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1import logging 1abcdefg
2from pathlib import Path 1abcdefg
3from typing import Annotated, Any 1abcdefg
5import typer 1abcdefg
6from pydantic import BaseModel, Field 1abcdefg
7from rich_toolkit import RichToolkit 1abcdefg
9from fastapi_cloud_cli.api import APIClient 1abcdefg
10from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1abcdefg
11from fastapi_cloud_cli.commands.apps._app import apps_app 1abcdefg
12from fastapi_cloud_cli.commands.deploy.archive import ( 1abcdefg
13 _get_app_name,
14 validate_app_directory,
15)
16from fastapi_cloud_cli.utils.apps import AppConfig, write_app_config 1abcdefg
17from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg
18from fastapi_cloud_cli.utils.teams import select_team 1abcdefg
20logger = logging.getLogger(__name__) 1abcdefg
23class CreatedApp(BaseModel): 1abcdefg
24 id: str 1abcdefg
25 team_id: str 1abcdefg
26 slug: str 1abcdefg
27 name: str 1abcdefg
28 directory: str | None 1abcdefg
31class AppsCreateOutput(BaseModel): 1abcdefg
32 app: CreatedApp 1abcdefg
33 linked: bool 1abcdefg
34 path_to_link: Annotated[Path | None, Field(exclude=True)] = None 1abcdefg
37def _create_app( 1abcdefg
38 client: APIClient, *, team_id: str, name: str, directory: str | None
39) -> CreatedApp:
40 response = client.post( 1vChrwDijxEksyFlmzGntAHouBIpq
41 "/apps/",
42 json={"team_id": team_id, "name": name, "directory": directory},
43 )
44 response.raise_for_status() 1vChrwDijxEksyFlmzGntAHouBIpq
46 return CreatedApp.model_validate(response.json()) 1vChrwDijxEksyFlmzGntAHouBIpq
49def _render_apps_create_output(data: AppsCreateOutput, toolkit: RichToolkit) -> None: 1abjcdmefgq
50 toolkit.print(f"Created app [bold]{data.app.name}[/bold]", bullet=False) 1hrijkslmntoupq
52 if data.linked and data.path_to_link is not None: 1hrijkslmntoupq
53 toolkit.print( 1hiklnop
54 f"Linked [bold]{data.path_to_link}[/bold] to [bold]{data.app.name}[/bold]",
55 bullet=False,
56 )
59@apps_app.command("create", cls=UserCommand) 1abcdefg
60def create_app( 1ab4cd5efg6
61 ctx: typer.Context,
62 team_id: Annotated[
63 str | None,
64 typer.Option(
65 "--team-id",
66 help="ID of the team where the app should be created.",
67 ),
68 ] = None,
69 name: Annotated[
70 str | None,
71 typer.Option(
72 "--name",
73 help="Name of the app to create.",
74 ),
75 ] = None,
76 directory: Annotated[
77 str | None,
78 typer.Option(
79 "--directory",
80 help=(
81 "Relative app directory containing the pyproject.toml "
82 "(for example: backend or webserver)."
83 ),
84 ),
85 ] = None,
86 link: Annotated[
87 bool | None,
88 typer.Option(
89 "--link/--no-link",
90 help="Link the local directory to the created app.",
91 ),
92 ] = None,
93 path: Annotated[
94 Path | None,
95 typer.Option(
96 "--path",
97 help="Directory to link when --link is enabled.",
98 ),
99 ] = None,
100 json_output: JsonOutputOption = False,
101) -> Any:
102 """
103 Create a FastAPI Cloud app.
104 """
105 path_to_link = path or Path.cwd() 1vCJXKhrwDLYMijxENZOksyFP0QlmzGR1SntAHT2UouBIV3Wpq
107 # JSON output is non-interactive, so it defaults to create-only unless --link is explicit.
108 link_app = link if link is not None else not json_output 1vCJXKhrwDLYMijxENZOksyFP0QlmzGR1SntAHT2UouBIV3Wpq
110 toolkit = get_user_command_context(ctx).toolkit 1vCJXKhrwDLYMijxENZOksyFP0QlmzGR1SntAHT2UouBIV3Wpq
112 if not link_app and path is not None: 1vCJXKhrwDLYMijxENZOksyFP0QlmzGR1SntAHT2UouBIV3Wpq
113 toolkit.fail( 1XYZ0123
114 "invalid_input",
115 "Path can only be used when linking.",
116 hint="Pass --link or omit --path.",
117 )
119 with APIClient() as client: 1vCJKhrwDLMijxENOksyFPQlmzGRSntAHTUouBIVWpq
120 if team_id is None: 1vCJKhrwDLMijxENOksyFPQlmzGRSntAHTUouBIVWpq
121 if json_output: 1KrMjOsQmStUuWq
122 toolkit.fail( 1KMOQSUW
123 "missing_required_input",
124 "Team ID is required.",
125 hint="Pass --team-id to choose a team.",
126 )
128 team = select_team( 1rjsmtuq
129 toolkit,
130 client,
131 empty_hint="Create a team before listing apps.",
132 )
133 team_id = team.id 1rjsmtuq
134 toolkit.print_line() 1rjsmtuq
136 if name is None: 1vCJKhrwDLMijxENOksyFPQlmzGRSntAHTUouBIVWpq
137 if json_output: 1KhMiOkQlSnUoWp
138 toolkit.fail( 1KMOQSUW
139 "missing_required_input",
140 "App name is required.",
141 hint="Pass --name to choose an app name.",
142 )
144 name = toolkit.input( 1hiklnop
145 title="What's your app name?",
146 default=_get_app_name(path_to_link),
147 bullet=False,
148 )
149 toolkit.print_line() 1hiklnop
151 try: 1vCJhrwDLijxENksyFPlmzGRntAHTouBIVpq
152 directory = validate_app_directory(directory) 1vCJhrwDLijxENksyFPlmzGRntAHTouBIVpq
153 except ValueError as e: 1JLNPRTV
154 toolkit.fail( 1JLNPRTV
155 "invalid_input",
156 f"Invalid app directory: {e}",
157 hint=(
158 "Pass a relative app directory such as `backend` or `webserver`; "
159 "use --path with --link to choose a local filesystem path."
160 ),
161 )
163 with toolkit.progress( 1vChrwDijxEksyFlmzGntAHouBIpq
164 title="Creating app",
165 transient=True,
166 ) as progress:
167 with client.handle_http_errors( 1vChrwDijxEksyFlmzGntAHouBIpq
168 progress,
169 default_message="Error creating app. Please try again later.",
170 toolkit=toolkit,
171 ):
172 app = _create_app( 1vChrwDijxEksyFlmzGntAHouBIpq
173 client,
174 team_id=team_id,
175 name=name,
176 directory=directory,
177 )
179 if link_app: 1vChrwDijxEksyFlmzGntAHouBIpq
180 write_app_config( 1vhwixkylznAoBp
181 path_to_link,
182 AppConfig(app_id=app.id, team_id=app.team_id),
183 )
185 result = AppsCreateOutput( 1vChrwDijxEksyFlmzGntAHouBIpq
186 app=app,
187 linked=link_app,
188 path_to_link=path_to_link if link_app else None,
189 )
191 toolkit.success(result, render_output=_render_apps_create_output) 1vChrwDijxEksyFlmzGntAHouBIpq