Coverage for src/fastapi_cloud_cli/commands/deployments.py: 100%

189 statements  

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

1import json 1abcdefg

2import logging 1abcdefg

3from typing import Annotated, Any 1abcdefg

4 

5import typer 1abcdefg

6from httpx import HTTPError 1abcdefg

7from pydantic import BaseModel 1abcdefg

8from rich.markup import escape 1abcdefg

9from rich.table import Table 1abcdefg

10from rich.text import Text 1abcdefg

11from rich_toolkit import RichToolkit 1abcdefg

12 

13from fastapi_cloud_cli.api import ( 1abcdefg

14 BUILD_FAILED_STATUSES, 

15 TERMINAL_STATUSES, 

16 APIClient, 

17 BuildFailure, 

18 BuildLogLineGeneric, 

19 BuildLogLineMessage, 

20 Deployment, 

21 StreamLogError, 

22 TooManyRetriesError, 

23 get_http_error_code, 

24 get_http_error_hint, 

25 handle_http_error, 

26) 

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

28from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail 1abcdefg

29from fastapi_cloud_cli.utils.cli import ( 1abcdefg

30 FastAPIRichToolkit, 

31 get_details_table, 

32) 

33from fastapi_cloud_cli.utils.dates import format_last_updated 1abcdefg

34from fastapi_cloud_cli.utils.errors import ErrorCode 1abcdefg

35from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg

36 

37logger = logging.getLogger(__name__) 1abcdefg

38 

39DEFAULT_LIMIT = 100 1abcdefg

40DEFAULT_OFFSET = 0 1abcdefg

41 

42 

43class DeploymentsListAPIResponse(BaseModel): 1abcdefg

44 data: list[Deployment] 1abcdefg

45 count: int 1abcdefg

46 

47 

48class DeploymentsListOutput(BaseModel): 1abcdefg

49 deployments: list[Deployment] 1abcdefg

50 total_count: int 1abcdefg

51 limit: int 1abcdefg

52 offset: int 1abcdefg

53 

54 

55class DeploymentGetOutput(BaseModel): 1abcdefg

56 deployment: Deployment 1abcdefg

57 

58 

59class BuildLogOutput(BaseModel): 1abcdefg

60 id: str | None = None 1abcdefg

61 message: str 1abcdefg

62 

63 

64class BuildLogsOutput(BaseModel): 1abcdefg

65 deployment_id: str 1abcdefg

66 failed: bool 1abcdefg

67 logs: list[BuildLogOutput] 1abcdefg

68 failure: BuildFailure | None = None 1abcdefg

69 

70 

71def _get_deployments( 1abcdefg

72 client: APIClient, *, app_id: str, limit: int, offset: int 

73) -> DeploymentsListOutput: 

74 response = client.get( 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

75 f"/apps/{app_id}/deployments/", 

76 params={ 

77 "limit": limit, 

78 "skip": offset, 

79 }, 

80 ) 

81 response.raise_for_status() 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

82 

83 data = DeploymentsListAPIResponse.model_validate(response.json()) 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

84 

85 return DeploymentsListOutput( 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

86 deployments=data.data, 

87 total_count=data.count, 

88 limit=limit, 

89 offset=offset, 

90 ) 

91 

92 

93def _render_deployments_list_output( 2a b jbc d kbe f g lb

94 data: DeploymentsListOutput, toolkit: RichToolkit 

95) -> None: 

96 toolkit.print_title("deployments") 2mb-bjb.bnb/bkb:bob;bpb=blb?b

97 toolkit.print_line() 2mb-bjb.bnb/bkb:bob;bpb=blb?b

98 

99 if not data.deployments: 2mb-bjb.bnb/bkb:bob;bpb=blb?b

100 toolkit.print("No deployments found.", bullet=False) 2-b.b/b:b;b=b?b

101 return 2-b.b/b:b;b=b?b

102 

103 table = Table.grid(padding=(0, 2), pad_edge=False) 2mbjbnbkbobpblb

104 table.add_column("ID", no_wrap=True) 2mbjbnbkbobpblb

105 table.add_column("Status", no_wrap=True) 2mbjbnbkbobpblb

106 table.add_column("Created", style="dim", no_wrap=True) 2mbjbnbkbobpblb

107 table.add_row("[bold]ID[/bold]", "[bold]Status[/bold]", "[bold]Created[/bold]") 2mbjbnbkbobpblb

108 table.add_row("", "", "") 2mbjbnbkbobpblb

109 

110 for deployment in data.deployments: 2mbjbnbkbobpblb

111 table.add_row( 2mbjbnbkbobpblb

112 deployment.id, 

113 deployment.status.value, 

114 Text(format_last_updated(deployment.created_at)), 

115 ) 

116 

117 toolkit.print(table, bullet=False) 2mbjbnbkbobpblb

118 

119 

120def _render_deployment_get_output( 2a b ecc d fce f g gc

121 data: DeploymentGetOutput, toolkit: FastAPIRichToolkit 

122) -> None: 

123 deployment = data.deployment 2qbhcsbectbjcubfcrbicvbkcwbgc

124 

125 toolkit.print_title("deployment") 2qbhcsbectbjcubfcrbicvbkcwbgc

126 toolkit.print_line() 2qbhcsbectbjcubfcrbicvbkcwbgc

127 

128 toolkit.print(f"[bold]{deployment.id}[/bold]", emoji="🚀") 2qbhcsbectbjcubfcrbicvbkcwbgc

129 toolkit.print_line() 2qbhcsbectbjcubfcrbicvbkcwbgc

130 toolkit.print( 2qbhcsbectbjcubfcrbicvbkcwbgc

131 get_details_table( 

132 [ 

133 ("app id", deployment.app_id), 

134 ("slug", deployment.slug), 

135 ("status", deployment.status.value), 

136 ("created", format_last_updated(deployment.created_at)), 

137 ( 

138 "url", 

139 deployment.url 

140 if deployment.url is not None 

141 else Text("-", style="dim"), 

142 ), 

143 ( 

144 "dashboard", 

145 Text( 

146 deployment.dashboard_url, 

147 style=f"link {deployment.dashboard_url}", 

148 ) 

149 if deployment.dashboard_url is not None 

150 else Text("-", style="dim"), 

151 ), 

152 ] 

153 ) 

154 ) 

155 

156 if deployment.failure is not None: 2qbhcsbectbjcubfcrbicvbkcwbgc

157 toolkit.print_line() 2qbsbtbubrbvbwb

158 _print_deployment_failure(toolkit, deployment.failure) 2qbsbtbubrbvbwb

159 

160 

161def _print_deployment_failure( 1abcdefg

162 toolkit: FastAPIRichToolkit, failure: BuildFailure 

163) -> None: 

164 toolkit.print("This deployment failed with the following error:", emoji="🚨") 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

165 toolkit.print_line() 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

166 toolkit.print(Text(failure.error_title, style="bold")) 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

167 toolkit.print_line() 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

168 toolkit.print(Text(failure.error_message)) 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

169 if failure.error_hint: 2v qb0 h w sb1 i x tb2 j y ub3 k z rb4 l A vb5 m B wb6 n

170 toolkit.print_line() 2v qbh w sbi x tbj y ubk z rbl A vbm B wbn

171 toolkit.print_hint(escape(failure.error_hint)) 2v qbh w sbi x tbj y ubk z rbl A vbm B wbn

172 

173 

174def _print_build_log_json( 1abcdefg

175 deployment_id: str, 

176 record_type: str, 

177 *, 

178 log_id: str | None, 

179 message: str | None = None, 

180 failure: BuildFailure | None = None, 

181) -> None: 

182 record = { 2= ? # @ [ $ ] ^ % _ ` ' { | ( } ~ ) abbb*

183 "type": record_type, 

184 "deployment_id": deployment_id, 

185 "id": log_id, 

186 "message": message, 

187 "failure": failure.model_dump(mode="json") if failure is not None else None, 

188 } 

189 

190 typer.echo( 2= ? # @ [ $ ] ^ % _ ` ' { | ( } ~ ) abbb*

191 json.dumps( 

192 {key: value for key, value in record.items() if value is not None}, 

193 separators=(",", ":"), 

194 ) 

195 ) 

196 

197 

198BUILD_LOG_BULLET = "[dim]▕[/dim]" 1abcdefg

199 

200 

201def _print_build_log_line(toolkit: RichToolkit, message: str) -> None: 1abcdefg

202 text = Text.from_ansi(message.rstrip()) 1o7Jh+QpXKi,Rq8Lj-SrYMk.Ts9Nl/Ut!Om:VuZPn;W

203 

204 if not text.plain.strip(): 1o7Jh+QpXKi,Rq8Lj-SrYMk.Ts9Nl/Ut!Om:VuZPn;W

205 # Keep the log marker when the style would otherwise omit an empty line. 

206 text = Text("\u200b") 1hijklmn

207 

208 toolkit.print(text, emoji=BUILD_LOG_BULLET) 1o7Jh+QpXKi,Rq8Lj-SrYMk.Ts9Nl/Ut!Om:VuZPn;W

209 

210 

211def _render_build_logs_output( 1abXcdYefgZ

212 data: BuildLogsOutput, toolkit: FastAPIRichToolkit 

213) -> None: 

214 for log in data.logs: 2C v o 0 7 EbJ h D w p 1 X FbK i E x q 2 8 GbL j F y r 3 Y HbM k G z s 4 9 IbN l H A t 5 ! JbO m I B u 6 Z KbP n

215 _print_build_log_line(toolkit, log.message) 1o7JhpXKiq8LjrYMks9Nlt!OmuZPn

216 

217 if data.failure: 2C v o 0 7 EbJ h D w p 1 X FbK i E x q 2 8 GbL j F y r 3 Y HbM k G z s 4 9 IbN l H A t 5 ! JbO m I B u 6 Z KbP n

218 if data.logs: 1v0hw1ix2jy3kz4lA5mB6n

219 toolkit.print_line() 1hijklmn

220 

221 _print_deployment_failure(toolkit, data.failure) 1v0hw1ix2jy3kz4lA5mB6n

222 elif data.failed: 2C o 7 EbJ D p X FbK E q 8 GbL F r Y HbM G s 9 IbN H t ! JbO I u Z KbP

223 toolkit.print_line() 1oJpKqLrMsNtOuP

224 toolkit.print_error("Build failed.") 1oJpKqLrMsNtOuP

225 elif not data.logs: 2C 7 EbD X FbE 8 GbF Y HbG 9 IbH ! JbI Z Kb

226 toolkit.print("No build logs found.") 2C EbD FbE GbF HbG IbH JbI Kb

227 

228 

229def _stream_build_logs( 1abcdefg

230 toolkit: FastAPIRichToolkit, 

231 client: APIClient, 

232 deployment_id: str, 

233) -> bool: 

234 deployment = client.get_deployment(deployment_id) 2C Lb@bxbcbMbv o h [b= ? + # Q D Nb]bybdbObw p i ^b@ [ , $ R E Pb_bzbebQbx q j `b] ^ - % S F Rb{bAbfbSby r k |b_ ` . ' T G Tb}bBbgbUbz s l ~b{ | / ( U H VbacCbhbWbA t m bc} ~ : ) V I XbccDbibYbB u n dcabbb; * W

235 terminal_log: BuildLogLineGeneric | None = None 2C LbxbcbMbv o h [b= ? + # Q D NbybdbObw p i ^b@ [ , $ R E PbzbebQbx q j `b] ^ - % S F RbAbfbSby r k |b_ ` . ' T G TbBbgbUbz s l ~b{ | / ( U H VbCbhbWbA t m bc} ~ : ) V I XbDbibYbB u n dcabbb; * W

236 

237 for log in client.stream_build_logs( 2C LbxbcbMbv o h [b= ? + # Q D NbybdbObw p i ^b@ [ , $ R E PbzbebQbx q j `b] ^ - % S F RbAbfbSby r k |b_ ` . ' T G TbBbgbUbz s l ~b{ | / ( U H VbCbhbWbA t m bc} ~ : ) V I XbDbibYbB u n dcabbb; * W

238 deployment_id, follow=deployment.status not in TERMINAL_STATUSES 

239 ): 

240 if isinstance(log, BuildLogLineMessage): 2C o h = ? + # Q D p i @ [ , $ R E q j ] ^ - % S F r k _ ` . ' T G s l { | / ( U H t m } ~ : ) V I u n abbb; * W

241 if toolkit.mode == "json": 2o h = ? + # Q p i @ [ , $ R q j ] ^ - % S r k _ ` . ' T s l { | / ( U t m } ~ : ) V u n abbb; * W

242 _print_build_log_json( 2= ? # @ [ $ ] ^ % _ ` ' { | ( } ~ ) abbb*

243 deployment_id, 

244 "log", 

245 log_id=log.id, 

246 message=log.message, 

247 ) 

248 else: 

249 _print_build_log_line(toolkit, log.message) 1oh+Qpi,Rqj-Srk.Tsl/Utm:Vun;W

250 else: 

251 terminal_log = log 2C o h = ? + # Q D p i @ [ , $ R E q j ] ^ - % S F r k _ ` . ' T G s l { | / ( U H t m } ~ : ) V I u n abbb; * W

252 

253 if deployment.status not in TERMINAL_STATUSES: 2C v o h = ? + # Q D w p i @ [ , $ R E x q j ] ^ - % S F y r k _ ` . ' T G z s l { | / ( U H A t m } ~ : ) V I B u n abbb; * W

254 deployment = client.get_deployment(deployment_id) 2o h ? + # Q p i [ , $ R q j ^ - % S r k ` . ' T s l | / ( U t m ~ : ) V u n bb; * W

255 

256 failed = ( 2C v o h = ? + # Q D w p i @ [ , $ R E x q j ] ^ - % S F y r k _ ` . ' T G z s l { | / ( U H A t m } ~ : ) V I B u n abbb; * W

257 terminal_log is not None and terminal_log.type == "failed" 

258 ) or deployment.status in BUILD_FAILED_STATUSES 

259 

260 if toolkit.mode == "json": 2C v o h = ? + # Q D w p i @ [ , $ R E x q j ] ^ - % S F y r k _ ` . ' T G z s l { | / ( U H A t m } ~ : ) V I B u n abbb; * W

261 if failed or terminal_log is not None or deployment.status in TERMINAL_STATUSES: 2= ? # @ [ $ ] ^ % _ ` ' { | ( } ~ ) abbb*

262 _print_build_log_json( 2= ? # @ [ $ ] ^ % _ ` ' { | ( } ~ ) abbb*

263 deployment_id, 

264 "failed" if failed else "complete", 

265 log_id=terminal_log.id if terminal_log is not None else None, 

266 failure=deployment.failure, 

267 ) 

268 elif deployment.failure is not None: 1Cvoh+QDwpi,RExqj-SFyrk.TGzsl/UHAtm:VIBun;W

269 toolkit.print_line() 1vhwixjykzlAmBn

270 _print_deployment_failure(toolkit, deployment.failure) 1vhwixjykzlAmBn

271 elif failed: 1Co+QDp,REq-SFr.TGs/UHt:VIu;W

272 toolkit.print_line() 1oQpRqSrTsUtVuW

273 toolkit.print_error("Build failed.") 1oQpRqSrTsUtVuW

274 

275 return failed 2C v o h = ? + # Q D w p i @ [ , $ R E x q j ] ^ - % S F y r k _ ` . ' T G z s l { | / ( U H A t m } ~ : ) V I B u n abbb; * W

276 

277 

278def _fetch_build_logs(client: APIClient, deployment_id: str) -> BuildLogsOutput: 1abcdefg

279 logs: list[BuildLogOutput] = [] 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

280 failed = False 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

281 

282 for log in client.stream_build_logs(deployment_id, follow=False): 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

283 if isinstance(log, BuildLogLineMessage): 2C o Zb%b7 0bJ h D p 1b'bX 2bK i E q 3b(b8 4bL j F r 5b)bY 6bM k G s 7b*b9 8bN l H t 9b+b! !bO m I u #b,bZ $bP n

284 logs.append(BuildLogOutput(id=log.id, message=log.message)) 2o Zb%b7 0bJ h p 1b'bX 2bK i q 3b(b8 4bL j r 5b)bY 6bM k s 7b*b9 8bN l t 9b+b! !bO m u #b,bZ $bP n

285 

286 elif log.type == "failed": 2C o Zb7 0bJ h D p 1bX 2bK i E q 3b8 4bL j F r 5bY 6bM k G s 7b9 8bN l H t 9b! !bO m I u #bZ $bP n

287 failed = True 2o Zb0bJ h p 1b2bK i q 3b4bL j r 5b6bM k s 7b8bN l t 9b!bO m u #b$bP n

288 

289 deployment = client.get_deployment(deployment_id) 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

290 return BuildLogsOutput( 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

291 deployment_id=deployment_id, 

292 failed=failed or deployment.status in BUILD_FAILED_STATUSES, 

293 logs=logs, 

294 failure=deployment.failure, 

295 ) 

296 

297 

298def _handle_build_log_error( 1abcdefg

299 toolkit: FastAPIRichToolkit, 

300 error: StreamLogError, 

301) -> None: 

302 hint: str | None = None 2xbcbMbybdbObzbebQbAbfbSbBbgbUbCbhbWbDbibYb

303 

304 if error.status_code == 404: 2xbcbMbybdbObzbebQbAbfbSbBbgbUbCbhbWbDbibYb

305 code: ErrorCode = "not_found" 2MbObQbSbUbWbYb

306 message = "Deployment not found." 2MbObQbSbUbWbYb

307 

308 elif isinstance(error.__cause__, HTTPError): 2xbcbybdbzbebAbfbBbgbCbhbDbib

309 code = get_http_error_code(error.__cause__) 2cbdbebfbgbhbib

310 message = handle_http_error(error.__cause__) 2cbdbebfbgbhbib

311 hint = get_http_error_hint(code) 2cbdbebfbgbhbib

312 

313 else: 

314 code = "api_error" 2xbybzbAbBbCbDb

315 message = f"Error streaming build logs: {error}" 2xbybzbAbBbCbDb

316 

317 toolkit.fail( 2xbcbMbybdbObzbebQbAbfbSbBbgbUbCbhbWbDbibYb

318 code, 

319 message, 

320 hint=hint, 

321 render_output=_render_build_log_error, 

322 ) 

323 

324 

325def _render_build_log_error( 1abcdefg

326 toolkit: FastAPIRichToolkit, 

327 *, 

328 code: ErrorCode, 

329 message: str, 

330 hint: str, 

331) -> None: 

332 toolkit.print_error(message) 2LbxbcbMbNbybdbObPbzbebQbRbAbfbSbTbBbgbUbVbCbhbWbXbDbibYb

333 if hint: 2LbxbcbMbNbybdbObPbzbebQbRbAbfbSbTbBbgbUbVbCbhbWbXbDbibYb

334 toolkit.print_line() 2LbcbNbdbPbebRbfbTbgbVbhbXbib

335 toolkit.print_hint(hint) 2LbcbNbdbPbebRbfbTbgbVbhbXbib

336 

337 

338deployments_app = typer.Typer( 1abcdefg

339 no_args_is_help=True, 

340 help="Manage the deployments of your app.", 

341) 

342 

343 

344@deployments_app.command("get", cls=UserCommand) 1abcdefg

345def get_deployment( 2a b Ucc d Vce f g Wc

346 ctx: typer.Context, 

347 deployment_id: Annotated[ 

348 str, 

349 typer.Argument( 

350 help="ID of the deployment to return.", 

351 ), 

352 ], 

353 app_id: Annotated[ 

354 str | None, 

355 typer.Option( 

356 "--app-id", 

357 help="ID of the app that owns the deployment.", 

358 ), 

359 ] = None, 

360 json_output: JsonOutputOption = False, 

361) -> Any: 

362 """ 

363 Get a FastAPI Cloud deployment by ID. 

364 """ 

365 

366 toolkit = get_user_command_context(ctx).toolkit 2zcqbAcBchcXcFcsbGcHcecYcIctbJcKcjcZcLcubMcNcfc0cCcrbDcEcic1cOcvbPcQckc2cRcwbScTcgc3c

367 

368 resolve_app_id_or_fail(toolkit, app_id=app_id) 2zcqbAcBchcXcFcsbGcHcecYcIctbJcKcjcZcLcubMcNcfc0cCcrbDcEcic1cOcvbPcQckc2cRcwbScTcgc3c

369 

370 with APIClient() as client: 2zcqbAcBchcFcsbGcHcecIctbJcKcjcLcubMcNcfcCcrbDcEcicOcvbPcQckcRcwbScTcgc

371 with ( 2zcqbAcBchcCcrbDcEcic

372 toolkit.progress( 

373 title="Fetching deployment", 

374 transient=True, 

375 ) as progress, 

376 client.handle_http_errors( 

377 progress, 

378 default_message="Error fetching deployment. Please try again later.", 

379 not_found_message="Deployment not found.", 

380 toolkit=toolkit, 

381 ), 

382 ): 

383 deployment = client.get_deployment(deployment_id) 2zcqbAcBchcFcsbGcHcecIctbJcKcjcLcubMcNcfcCcrbDcEcicOcvbPcQckcRcwbScTcgc

384 

385 toolkit.success( 2zcqbAcBchcFcsbGcHcecIctbJcKcjcLcubMcNcfcCcrbDcEcicOcvbPcQckcRcwbScTcgc

386 DeploymentGetOutput(deployment=deployment), 

387 render_output=_render_deployment_get_output, 

388 ) 

389 

390 

391@deployments_app.command("build-logs", cls=UserCommand) 1abcdefg

392def build_logs( 2a b Ucc d Vce f g Wc

393 ctx: typer.Context, 

394 deployment_id: Annotated[ 

395 str, 

396 typer.Argument( 

397 help="ID of the deployment whose build logs should be returned.", 

398 ), 

399 ], 

400 follow: Annotated[ 

401 bool, 

402 typer.Option( 

403 "--follow/--no-follow", 

404 "-f", 

405 help="Stream build logs until the build reaches a terminal state.", 

406 ), 

407 ] = True, 

408 json_output: JsonOutputOption = False, 

409) -> None: 

410 """ 

411 Stream or fetch build logs for a FastAPI Cloud deployment. 

412 """ 

413 

414 toolkit = get_user_command_context(ctx).toolkit 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

415 

416 if follow: 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

417 toolkit.print( 2C Lb@bxbcbMbv o h [b= ? + # Q D Nb]bybdbObw p i ^b@ [ , $ R E Pb_bzbebQbx q j `b] ^ - % S F Rb{bAbfbSby r k |b_ ` . ' T G Tb}bBbgbUbz s l ~b{ | / ( U H VbacCbhbWbA t m bc} ~ : ) V I XbccDbibYbB u n dcabbb; * W

418 f"Streaming build logs for [bold]{deployment_id}[/bold]...", 

419 emoji="📡", 

420 ) 

421 else: 

422 toolkit.print( 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

423 f"Fetching build logs for [bold]{deployment_id}[/bold]...", 

424 emoji="📜", 

425 ) 

426 toolkit.print_line() 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

427 

428 try: 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

429 with APIClient() as client: 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

430 if follow: 2C Lb@bxbcbMbv o Zb0 %b7 Eb0bJ h [b= ? + # Q D Nb]bybdbObw p 1b1 'bX Fb2bK i ^b@ [ , $ R E Pb_bzbebQbx q 3b2 (b8 Gb4bL j `b] ^ - % S F Rb{bAbfbSby r 5b3 )bY Hb6bM k |b_ ` . ' T G Tb}bBbgbUbz s 7b4 *b9 Ib8bN l ~b{ | / ( U H VbacCbhbWbA t 9b5 +b! Jb!bO m bc} ~ : ) V I XbccDbibYbB u #b6 ,bZ Kb$bP n dcabbb; * W

431 failed = _stream_build_logs(toolkit, client, deployment_id) 2C Lb@bxbcbMbv o h [b= ? + # Q D Nb]bybdbObw p i ^b@ [ , $ R E Pb_bzbebQbx q j `b] ^ - % S F Rb{bAbfbSby r k |b_ ` . ' T G Tb}bBbgbUbz s l ~b{ | / ( U H VbacCbhbWbA t m bc} ~ : ) V I XbccDbibYbB u n dcabbb; * W

432 else: 

433 result = _fetch_build_logs(client, deployment_id) 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

434 toolkit.success(result, render_output=_render_build_logs_output) 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

435 failed = result.failed 2C v o Zb0 %b7 Eb0bJ h D w p 1b1 'bX Fb2bK i E x q 3b2 (b8 Gb4bL j F y r 5b3 )bY Hb6bM k G z s 7b4 *b9 Ib8bN l H A t 9b5 +b! Jb!bO m I B u #b6 ,bZ Kb$bP n

436 

437 except KeyboardInterrupt: # pragma: no cover 2Lb@bxbcbMb[bNb]bybdbOb^bPb_bzbebQb`bRb{bAbfbSb|bTb}bBbgbUb~bVbacCbhbWbbcXbccDbibYbdc

438 toolkit.print_line() 2[b^b`b|b~bbcdc

439 return 2[b^b`b|b~bbcdc

440 except StreamLogError as e: 2Lb@bxbcbMbNb]bybdbObPb_bzbebQbRb{bAbfbSbTb}bBbgbUbVbacCbhbWbXbccDbibYb

441 _handle_build_log_error(toolkit, e) 2xbcbMbybdbObzbebQbAbfbSbBbgbUbCbhbWbDbibYb

442 except HTTPError as e: 2Lb@bNb]bPb_bRb{bTb}bVbacXbcc

443 code = get_http_error_code(e) 2@b]b_b{b}baccc

444 toolkit.fail( 2@b]b_b{b}baccc

445 code, 

446 handle_http_error(e, not_found_message="Deployment not found."), 

447 hint=get_http_error_hint(code), 

448 render_output=_render_build_log_error, 

449 ) 

450 

451 except (TooManyRetriesError, TimeoutError): 2LbNbPbRbTbVbXb

452 message = "Lost connection to build log stream. Please try again later." 2LbNbPbRbTbVbXb

453 toolkit.fail( 2LbNbPbRbTbVbXb

454 "network_error", 

455 message, 

456 hint="Please try again later.", 

457 render_output=_render_build_log_error, 

458 ) 

459 

460 if failed: 2C v o Zb0 %b7 Eb0bJ h = ? + # Q D w p 1b1 'bX Fb2bK i @ [ , $ R E x q 3b2 (b8 Gb4bL j ] ^ - % S F y r 5b3 )bY Hb6bM k _ ` . ' T G z s 7b4 *b9 Ib8bN l { | / ( U H A t 9b5 +b! Jb!bO m } ~ : ) V I B u #b6 ,bZ Kb$bP n abbb; * W

461 raise typer.Exit(1) 2v o Zb0 0bJ h = # Q w p 1b1 2bK i @ $ R x q 3b2 4bL j ] % S y r 5b3 6bM k _ ' T z s 7b4 8bN l { ( U A t 9b5 !bO m } ) V B u #b6 $bP n ab* W

462 

463 

464@deployments_app.command("list", cls=UserCommand) 1abcdefg

465def list_deployments( 2a b Ucc d Vce f g Wc

466 ctx: typer.Context, 

467 app_id: Annotated[ 

468 str | None, 

469 typer.Option( 

470 "--app-id", 

471 help="ID of the app whose deployments should be listed.", 

472 ), 

473 ] = None, 

474 limit: Annotated[ 

475 int, 

476 typer.Option( 

477 "--limit", 

478 help="Maximum number of deployments to return.", 

479 min=1, 

480 ), 

481 ] = DEFAULT_LIMIT, 

482 offset: Annotated[ 

483 int, 

484 typer.Option( 

485 "--offset", 

486 help="Offset into the deployment result set.", 

487 min=0, 

488 ), 

489 ] = DEFAULT_OFFSET, 

490 json_output: JsonOutputOption = False, 

491) -> Any: 

492 """ 

493 List FastAPI Cloud deployments for an app. 

494 """ 

495 

496 toolkit = get_user_command_context(ctx).toolkit 2lcmcmb-b4cncocjb.b5cpcqcnb/b6crcsckb:b7ctcucob;b8cvcwcpb=b9cxcyclb?b!c

497 

498 target_app_id = resolve_app_id_or_fail(toolkit, app_id=app_id) 2lcmcmb-b4cncocjb.b5cpcqcnb/b6crcsckb:b7ctcucob;b8cvcwcpb=b9cxcyclb?b!c

499 

500 with APIClient() as client: 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

501 with toolkit.progress( 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

502 title="Fetching deployments", 

503 transient=True, 

504 ) as progress: 

505 with client.handle_http_errors( 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

506 progress, 

507 default_message="Error fetching deployments. Please try again later.", 

508 not_found_message="App not found.", 

509 toolkit=toolkit, 

510 ): 

511 result = _get_deployments( 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b

512 client, 

513 app_id=target_app_id, 

514 limit=limit, 

515 offset=offset, 

516 ) 

517 

518 toolkit.success(result, render_output=_render_deployments_list_output) 2lcmcmb-bncocjb.bpcqcnb/brcsckb:btcucob;bvcwcpb=bxcyclb?b