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

1import logging 1abcdefg

2from pathlib import Path 1abcdefg

3from typing import Annotated, Any 1abcdefg

4 

5import typer 1abcdefg

6from pydantic import BaseModel, Field 1abcdefg

7from rich_toolkit import RichToolkit 1abcdefg

8 

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

19 

20logger = logging.getLogger(__name__) 1abcdefg

21 

22 

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

29 

30 

31class AppsCreateOutput(BaseModel): 1abcdefg

32 app: CreatedApp 1abcdefg

33 linked: bool 1abcdefg

34 path_to_link: Annotated[Path | None, Field(exclude=True)] = None 1abcdefg

35 

36 

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

45 

46 return CreatedApp.model_validate(response.json()) 1vChrwDijxEksyFlmzGntAHouBIpq

47 

48 

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

51 

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 ) 

57 

58 

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

106 

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

109 

110 toolkit = get_user_command_context(ctx).toolkit 1vCJXKhrwDLYMijxENZOksyFP0QlmzGR1SntAHT2UouBIV3Wpq

111 

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 ) 

118 

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 ) 

127 

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

135 

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 ) 

143 

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

150 

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 ) 

162 

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 ) 

178 

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 ) 

184 

185 result = AppsCreateOutput( 1vChrwDijxEksyFlmzGntAHouBIpq

186 app=app, 

187 linked=link_app, 

188 path_to_link=path_to_link if link_app else None, 

189 ) 

190 

191 toolkit.success(result, render_output=_render_apps_create_output) 1vChrwDijxEksyFlmzGntAHouBIpq