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
« 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
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
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
17logger = logging.getLogger(__name__) 1abcdefg
19DEFAULT_LIMIT = 100 1abcdefg
20DEFAULT_OFFSET = 0 1abcdefg
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
34class AppsListAPIResponse(BaseModel): 1abcdefg
35 data: list[App] 1abcdefg
36 count: int 1abcdefg
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
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
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
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
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 )
78 return table 1hriojskpltmunq
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
85 return Team.model_validate(response.json()) 1JKLMChvNOPQDiwRSTUEjxVWXYFkyZ012Glz3456HmA789!InB
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
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
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
108 data = AppsListAPIResponse.model_validate(response.json()) 1ChvrDiwoEjxsFkypGlztHmAuInBq
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 )
119def _render_apps_list_output(data: AppsListOutput, toolkit: RichToolkit) -> None: 1abocdpefgq
120 toolkit.print_title("apps") 1hvriwojxskyplztmAunBq
121 toolkit.print_line() 1hvriwojxskyplztmAunBq
123 if not data.apps: 1hvriwojxskyplztmAunBq
124 toolkit.print("No apps found.", bullet=False) 1vwxyzAB
125 return 1vwxyzAB
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 )
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 """
169 toolkit = get_user_command_context(ctx).toolkit 1Chv#r$Diw%o'Ejx(s)Fky*p+Glz,t-HmA.u/InB:q;
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;
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 )
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
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
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 )
222 toolkit.success(result, render_output=_render_apps_list_output) 1ChvrDiwoEjxsFkypGlztHmAuInBq