Coverage for src/fastapi_cloud_cli/commands/apps/list.py: 100%

88 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-11-09 00:25 +0000

1import logging 1abcdefg

2from typing import Annotated, Any 1abcdefg

3 

4import typer 1abcdefg

5from pydantic import BaseModel, Field 1abcdefg

6from rich.table import Table 1abcdefg

7from rich.text import Text 1abcdefg

8from rich_toolkit import RichToolkit 1abcdefg

9 

10from fastapi_cloud_cli.api import APIClient 1abcdefg

11from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1abcdefg

12from fastapi_cloud_cli.commands.apps._app import apps_app 1abcdefg

13from fastapi_cloud_cli.config import Settings 1abcdefg

14from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg

15from fastapi_cloud_cli.utils.teams import Team, select_team 1abcdefg

16 

17logger = logging.getLogger(__name__) 1abcdefg

18 

19DEFAULT_LIMIT = 100 1abcdefg

20DEFAULT_OFFSET = 0 1abcdefg

21 

22 

23class App(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 url: str | None = None 1abcdefg

30 region: str | None = None 1abcdefg

31 updated_at: str | None = None 1abcdefg

32 

33 

34class AppsListAPIResponse(BaseModel): 1abcdefg

35 data: list[App] 1abcdefg

36 count: int 1abcdefg

37 

38 

39class AppsListOutput(BaseModel): 1abcdefg

40 apps: list[App] 1abcdefg

41 total_count: int 1abcdefg

42 limit: int 1abcdefg

43 offset: int 1abcdefg

44 team_slug: Annotated[str, Field(exclude=True)] 1abcdefg

45 

46 

47def _get_app_dashboard_url(app: App, *, team_slug: str, settings: Settings) -> str: 1abcdefg

48 return f"{settings.dashboard_base_url}/{team_slug}/apps/{app.slug}" 2J K L ZbM h r N O P 0bQ i o R S T 1bU j s V W X 2bY k p Z 0 1 3b2 l t 3 4 5 4b6 m u 7 8 9 5b! n q

49 

50 

51def _format_app_name(app: App, *, team_slug: str, settings: Settings) -> str: 1abcdefg

52 dashboard_url = _get_app_dashboard_url( 1hriojskpltmunq

53 app, 

54 team_slug=team_slug, 

55 settings=settings, 

56 ) 

57 return f"[link={dashboard_url}]{app.name}[/link]" 1hriojskpltmunq

58 

59 

60def _get_apps_list_table( 1abcdefg

61 apps: list[App], *, team_slug: str, settings: Settings 

62) -> Table: 

63 table = Table.grid(padding=(0, 2), pad_edge=False) 1hriojskpltmunq

64 table.add_column("Name", no_wrap=True) 1hriojskpltmunq

65 table.add_column("ID", no_wrap=True, overflow="ignore") 1hriojskpltmunq

66 table.add_row( 1hriojskpltmunq

67 "[bold]Name[/bold]", 

68 "[bold]ID[/bold]", 

69 ) 

70 table.add_row("", "") 1hriojskpltmunq

71 

72 for app in apps: 1hriojskpltmunq

73 table.add_row( 1hriojskpltmunq

74 _format_app_name(app, team_slug=team_slug, settings=settings), 

75 Text(app.id), 

76 ) 

77 

78 return table 1hriojskpltmunq

79 

80 

81def _get_team(client: APIClient, team_id: str) -> Team: 1abcdefg

82 response = client.get(f"/teams/{team_id}") 1JKLMChvNOPQDiwRSTUEjxVWXYFkyZ012Glz3456HmA789!InB

83 response.raise_for_status() 1JKLMChvNOPQDiwRSTUEjxVWXYFkyZ012Glz3456HmA789!InB

84 

85 return Team.model_validate(response.json()) 1JKLMChvNOPQDiwRSTUEjxVWXYFkyZ012Glz3456HmA789!InB

86 

87 

88def _get_app(client: APIClient, app_id: str) -> App: 1abcdefg

89 response = client.get(f"/apps/{app_id}") 2LbJ K L = ? M Mb@ [ ] ^ _ NbN O P ` { Q Ob| } ~ abbbPbR S T cbdbU QbebfbgbhbibRbV W X jbkbY SblbmbnbobpbTbZ 0 1 qbrb2 UbsbtbubvbwbVb3 4 5 xbyb6 WbzbAbBbCbDbXb7 8 9 EbFb! YbGbHbIbJbKb

90 response.raise_for_status() 2LbJ K L = ? M Mb@ [ ] ^ _ NbN O P ` { Q Ob| } ~ abbbPbR S T cbdbU QbebfbgbhbibRbV W X jbkbY SblbmbnbobpbTbZ 0 1 qbrb2 UbsbtbubvbwbVb3 4 5 xbyb6 WbzbAbBbCbDbXb7 8 9 EbFb! YbGbHbIbJbKb

91 

92 return App.model_validate(response.json()) 2J K L = ? M @ [ ] ^ _ N O P ` { Q | } ~ abbbR S T cbdbU ebfbgbhbibV W X jbkbY lbmbnbobpbZ 0 1 qbrb2 sbtbubvbwb3 4 5 xbyb6 zbAbBbCbDb7 8 9 EbFb! GbHbIbJbKb

93 

94 

95def _get_apps( 1abcdefg

96 client: APIClient, *, team_id: str, limit: int, offset: int, team_slug: str 

97) -> AppsListOutput: 

98 response = client.get( 1ChvrDiwoEjxsFkypGlztHmAuInBq

99 "/apps/", 

100 params={ 

101 "team_id": team_id, 

102 "limit": limit, 

103 "skip": offset, 

104 }, 

105 ) 

106 response.raise_for_status() 1ChvrDiwoEjxsFkypGlztHmAuInBq

107 

108 data = AppsListAPIResponse.model_validate(response.json()) 1ChvrDiwoEjxsFkypGlztHmAuInBq

109 

110 return AppsListOutput( 1ChvrDiwoEjxsFkypGlztHmAuInBq

111 apps=data.data, 

112 total_count=data.count, 

113 limit=limit, 

114 offset=offset, 

115 team_slug=team_slug, 

116 ) 

117 

118 

119def _render_apps_list_output(data: AppsListOutput, toolkit: RichToolkit) -> None: 1abocdpefgq

120 toolkit.print_title("apps") 1hvriwojxskyplztmAunBq

121 toolkit.print_line() 1hvriwojxskyplztmAunBq

122 

123 if not data.apps: 1hvriwojxskyplztmAunBq

124 toolkit.print("No apps found.", bullet=False) 1vwxyzAB

125 return 1vwxyzAB

126 

127 toolkit.print( 1hriojskpltmunq

128 _get_apps_list_table( 

129 data.apps, 

130 team_slug=data.team_slug, 

131 settings=Settings.get(), 

132 ), 

133 bullet=False, 

134 ) 

135 

136 

137@apps_app.command("list", cls=UserCommand) 1abcdefg

138def list_apps( 2a b 6bc d 7be f g 8b

139 ctx: typer.Context, 

140 team_id: Annotated[ 

141 str | None, 

142 typer.Option( 

143 "--team-id", 

144 help="ID of the team whose apps should be listed.", 

145 ), 

146 ] = None, 

147 limit: Annotated[ 

148 int, 

149 typer.Option( 

150 "--limit", 

151 help="Maximum number of apps to return.", 

152 min=1, 

153 ), 

154 ] = DEFAULT_LIMIT, 

155 offset: Annotated[ 

156 int, 

157 typer.Option( 

158 "--offset", 

159 help="Offset into the app result set.", 

160 min=0, 

161 ), 

162 ] = DEFAULT_OFFSET, 

163 json_output: JsonOutputOption = False, 

164) -> Any: 

165 """ 

166 List FastAPI Cloud apps. 

167 """ 

168 

169 toolkit = get_user_command_context(ctx).toolkit 1Chv#r$Diw%o'Ejx(s)Fky*p+Glz,t-HmA.u/InB:q;

170 

171 with APIClient() as client: 1Chv#r$Diw%o'Ejx(s)Fky*p+Glz,t-HmA.u/InB:q;

172 team_slug: str | None = None 1Chv#r$Diw%o'Ejx(s)Fky*p+Glz,t-HmA.u/InB:q;

173 

174 if team_id is None: 1Chv#r$Diw%o'Ejx(s)Fky*p+Glz,t-HmA.u/InB:q;

175 if json_output: 1#r$%o'(s)*p+,t-.u/:q;

176 toolkit.fail( 1#%(*,.:

177 "missing_required_input", 

178 "Team ID is required.", 

179 hint="Pass --team-id to choose a team.", 

180 ) 

181 

182 team = select_team( 1r$o's)p+t-u/q;

183 toolkit, 

184 client, 

185 empty_hint="Create a team before listing apps.", 

186 ) 

187 team_id = team.id 1rosptuq

188 team_slug = team.slug 1rosptuq

189 

190 toolkit.print_line() 1rosptuq

191 else: 

192 with toolkit.progress( 1ChvDiwEjxFkyGlzHmAInB

193 title="Fetching team", 

194 transient=True, 

195 ) as progress: 

196 with client.handle_http_errors( 1ChvDiwEjxFkyGlzHmAInB

197 progress, 

198 default_message="Error fetching team. Please try again later.", 

199 not_found_message="Team not found.", 

200 toolkit=toolkit, 

201 ): 

202 team = _get_team(client, team_id) 1ChvDiwEjxFkyGlzHmAInB

203 team_slug = team.slug 1ChvDiwEjxFkyGlzHmAInB

204 

205 with toolkit.progress( 1ChvrDiwoEjxsFkypGlztHmAuInBq

206 title="Fetching apps", 

207 transient=True, 

208 ) as progress: 

209 with client.handle_http_errors( 1ChvrDiwoEjxsFkypGlztHmAuInBq

210 progress, 

211 default_message="Error fetching apps. Please try again later.", 

212 toolkit=toolkit, 

213 ): 

214 result = _get_apps( 1ChvrDiwoEjxsFkypGlztHmAuInBq

215 client, 

216 team_id=team_id, 

217 limit=limit, 

218 offset=offset, 

219 team_slug=team_slug, 

220 ) 

221 

222 toolkit.success(result, render_output=_render_apps_list_output) 1ChvrDiwoEjxsFkypGlztHmAuInBq