11import contextlib
2- import json
32import logging
43import subprocess
54import tempfile
65import time
76from enum import Enum
87from itertools import cycle
98from pathlib import Path
10- from typing import Any , Dict , Generator , List , Optional , Union
9+ from typing import Any , Dict , List , Optional , Union
1110
1211import fastar
1312import rignore
1413import typer
1514from httpx import Client
16- from pydantic import BaseModel , EmailStr , TypeAdapter , ValidationError
15+ from pydantic import BaseModel , EmailStr , ValidationError
1716from rich .text import Text
1817from rich_toolkit import RichToolkit
1918from rich_toolkit .menu import Option
2019from typing_extensions import Annotated
2120
2221from fastapi_cloud_cli .commands .login import login
23- from fastapi_cloud_cli .utils .api import APIClient
22+ from fastapi_cloud_cli .utils .api import APIClient , BuildLogError , TooManyRetriesError
2423from fastapi_cloud_cli .utils .apps import AppConfig , get_app_config , write_app_config
2524from fastapi_cloud_cli .utils .auth import is_logged_in
2625from fastapi_cloud_cli .utils .cli import get_rich_toolkit , handle_http_errors
26+ from fastapi_cloud_cli .utils .pydantic_compat import (
27+ TypeAdapter ,
28+ model_dump ,
29+ model_validate ,
30+ )
2731
2832logger = logging .getLogger (__name__ )
2933
@@ -91,7 +95,7 @@ def _get_teams() -> List[Team]:
9195
9296 data = response .json ()["data" ]
9397
94- return [Team . model_validate (team ) for team in data ]
98+ return [model_validate (Team , team ) for team in data ]
9599
96100
97101class AppResponse (BaseModel ):
@@ -108,7 +112,7 @@ def _create_app(team_id: str, app_name: str) -> AppResponse:
108112
109113 response .raise_for_status ()
110114
111- return AppResponse . model_validate (response .json ())
115+ return model_validate (AppResponse , response .json ())
112116
113117
114118class DeploymentStatus (str , Enum ):
@@ -161,7 +165,7 @@ def _create_deployment(app_id: str) -> CreateDeploymentResponse:
161165 response = client .post (f"/apps/{ app_id } /deployments/" )
162166 response .raise_for_status ()
163167
164- return CreateDeploymentResponse . model_validate (response .json ())
168+ return model_validate (CreateDeploymentResponse , response .json ())
165169
166170
167171class RequestUploadResponse (BaseModel ):
@@ -186,7 +190,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
186190 response = fastapi_client .post (f"/deployments/{ deployment_id } /upload" )
187191 response .raise_for_status ()
188192
189- upload_data = RequestUploadResponse . model_validate (response .json ())
193+ upload_data = model_validate (RequestUploadResponse , response .json ())
190194 logger .debug ("Received upload URL: %s" , upload_data .url )
191195
192196 # Upload the archive
@@ -221,7 +225,7 @@ def _get_app(app_slug: str) -> Optional[AppResponse]:
221225
222226 data = response .json ()
223227
224- return AppResponse . model_validate (data )
228+ return model_validate (AppResponse , data )
225229
226230
227231def _get_apps (team_id : str ) -> List [AppResponse ]:
@@ -231,24 +235,14 @@ def _get_apps(team_id: str) -> List[AppResponse]:
231235
232236 data = response .json ()["data" ]
233237
234- return [AppResponse .model_validate (app ) for app in data ]
235-
236-
237- def _stream_build_logs (deployment_id : str ) -> Generator [str , None , None ]:
238- with APIClient () as client :
239- with client .stream (
240- "GET" , f"/deployments/{ deployment_id } /build-logs" , timeout = 60
241- ) as response :
242- response .raise_for_status ()
243-
244- yield from response .iter_lines ()
238+ return [model_validate (AppResponse , app ) for app in data ]
245239
246240
247241WAITING_MESSAGES = [
248242 "🚀 Preparing for liftoff! Almost there..." ,
249243 "👹 Sneaking past the dependency gremlins... Don't wake them up!" ,
250244 "🤏 Squishing code into a tiny digital sandwich. Nom nom nom." ,
251- "📉 Server space running low. Time to delete those cat videos? " ,
245+ "🐱 Removing cat videos from our servers to free up space. " ,
252246 "🐢 Uploading at blazing speeds of 1 byte per hour. Patience, young padawan." ,
253247 "🔌 Connecting to server... Please stand by while we argue with the firewall." ,
254248 "💥 Oops! We've angered the Python God. Sacrificing a rubber duck to appease it." ,
@@ -358,17 +352,15 @@ def _wait_for_deployment(
358352
359353 with toolkit .progress (
360354 next (messages ), inline_logs = True , lines_to_show = 20
361- ) as progress :
362- with handle_http_errors ( progress = progress ) :
363- for line in _stream_build_logs (deployment .id ):
355+ ) as progress , APIClient () as client :
356+ try :
357+ for log in client . stream_build_logs (deployment .id ):
364358 time_elapsed = time .monotonic () - started_at
365359
366- data = json .loads (line )
367-
368- if "message" in data :
369- progress .log (Text .from_ansi (data ["message" ].rstrip ()))
360+ if log .type == "message" :
361+ progress .log (Text .from_ansi (log .message .rstrip ()))
370362
371- if data . get ( " type" ) == "complete" :
363+ if log . type == "complete" :
372364 progress .log ("" )
373365 progress .log (
374366 f"🐔 Ready the chicken! Your app is ready at [link={ deployment .url } ]{ deployment .url } [/link]"
@@ -382,20 +374,28 @@ def _wait_for_deployment(
382374
383375 break
384376
385- if data . get ( " type" ) == "failed" :
377+ if log . type == "failed" :
386378 progress .log ("" )
387379 progress .log (
388380 f"😔 Oh no! Something went wrong. Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
389381 )
390382 raise typer .Exit (1 )
391383
392384 if time_elapsed > 30 :
393- messages = cycle (LONG_WAIT_MESSAGES ) # pragma: no cover
385+ messages = cycle (LONG_WAIT_MESSAGES )
394386
395387 if (time .monotonic () - last_message_changed_at ) > 2 :
396- progress .title = next (messages ) # pragma: no cover
388+ progress .title = next (messages )
397389
398- last_message_changed_at = time .monotonic () # pragma: no cover
390+ last_message_changed_at = time .monotonic ()
391+
392+ except (BuildLogError , TooManyRetriesError ) as e :
393+ logger .error ("Build log streaming failed: %s" , e )
394+ toolkit .print_line ()
395+ toolkit .print (
396+ f"⚠️ Unable to stream build logs. Check the dashboard for status: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
397+ )
398+ raise typer .Exit (1 ) from e
399399
400400
401401class SignupToWaitingList (BaseModel ):
@@ -416,9 +416,7 @@ def _send_waitlist_form(
416416 with toolkit .progress ("Sending your request..." ) as progress :
417417 with APIClient () as client :
418418 with handle_http_errors (progress ):
419- response = client .post (
420- "/users/waiting-list" , json = result .model_dump (mode = "json" )
421- )
419+ response = client .post ("/users/waiting-list" , json = model_dump (result ))
422420
423421 response .raise_for_status ()
424422
@@ -443,7 +441,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
443441
444442 toolkit .print_line ()
445443
446- result = SignupToWaitingList ( email = email )
444+ result = model_validate ( SignupToWaitingList , { " email" : email } )
447445
448446 if toolkit .confirm (
449447 "Do you want to get access faster by giving us more information?" ,
@@ -467,11 +465,12 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
467465 result = form .run () # type: ignore
468466
469467 try :
470- result = SignupToWaitingList .model_validate (
468+ result = model_validate (
469+ SignupToWaitingList ,
471470 {
472471 "email" : email ,
473472 ** result , # type: ignore
474- }
473+ },
475474 )
476475 except ValidationError :
477476 toolkit .print (
@@ -499,7 +498,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
499498
500499 with contextlib .suppress (Exception ):
501500 subprocess .run (
502- ["open" , "raycast://confetti?emojis=🐔⚡" ],
501+ ["open" , "-g" , " raycast://confetti?emojis=🐔⚡" ],
503502 stdout = subprocess .DEVNULL ,
504503 stderr = subprocess .DEVNULL ,
505504 check = False ,
0 commit comments