1313import fastar
1414import rignore
1515import typer
16- from httpx import Client
16+ from httpx import Client , HTTPError
1717from pydantic import AfterValidator , BaseModel , EmailStr , TypeAdapter , ValidationError
1818from rich .text import Text
1919from rich_toolkit import RichToolkit
2020from rich_toolkit .menu import Option
21+ from rich_toolkit .progress import Progress
2122
2223from fastapi_cloud_cli .commands .login import login
2324from fastapi_cloud_cli .utils .api import APIClient , StreamLogError , TooManyRetriesError
@@ -210,6 +211,17 @@ def to_human_readable(cls, status: "DeploymentStatus") -> str:
210211 }[status ]
211212
212213
214+ SUCCESSFUL_STATUSES = {DeploymentStatus .success , DeploymentStatus .verifying_skipped }
215+ FAILED_STATUSES = {
216+ DeploymentStatus .failed ,
217+ DeploymentStatus .verifying_failed ,
218+ DeploymentStatus .deploying_failed ,
219+ DeploymentStatus .building_image_failed ,
220+ DeploymentStatus .extracting_failed ,
221+ }
222+ TERMINAL_STATUSES = SUCCESSFUL_STATUSES | FAILED_STATUSES
223+
224+
213225class CreateDeploymentResponse (BaseModel ):
214226 id : str
215227 app_id : str
@@ -440,6 +452,82 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
440452 return app_config
441453
442454
455+ POLL_INTERVAL = 2.0
456+ POLL_TIMEOUT = 120.0
457+
458+
459+ def _poll_deployment_status (
460+ client : APIClient ,
461+ app_id : str ,
462+ deployment_id : str ,
463+ timeout : float = POLL_TIMEOUT ,
464+ interval : float = POLL_INTERVAL ,
465+ ) -> DeploymentStatus :
466+ start = time .monotonic ()
467+ consecutive_errors = 0
468+ max_errors = 5
469+
470+ while time .monotonic () - start < timeout :
471+ try :
472+ response = client .get (f"/apps/{ app_id } /deployments/{ deployment_id } " )
473+ response .raise_for_status ()
474+ status = DeploymentStatus (response .json ()["status" ])
475+ consecutive_errors = 0
476+ except HTTPError as e :
477+ consecutive_errors += 1
478+ logger .debug ("Error polling deployment status: %s" , e )
479+ if consecutive_errors >= max_errors :
480+ raise
481+ time .sleep (interval )
482+ continue
483+
484+ if status in TERMINAL_STATUSES :
485+ return status
486+
487+ time .sleep (interval )
488+
489+ raise TimeoutError ("Deployment verification timed out" )
490+
491+
492+ def _verify_deployment (
493+ toolkit : RichToolkit ,
494+ client : APIClient ,
495+ app_id : str ,
496+ deployment : CreateDeploymentResponse ,
497+ ) -> None :
498+ with Progress (
499+ title = "Verifying deployment..." ,
500+ console = toolkit .console ,
501+ style = toolkit .style ,
502+ inline_logs = True ,
503+ ) as progress :
504+ progress .metadata ["done_emoji" ] = "✅"
505+ try :
506+ final_status = _poll_deployment_status (client , app_id , deployment .id )
507+ except TimeoutError :
508+ progress .metadata ["done_emoji" ] = "⚠️"
509+ progress .log (
510+ f"Could not confirm deployment status. "
511+ f"Check the dashboard: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
512+ )
513+ return
514+
515+ if final_status in SUCCESSFUL_STATUSES :
516+ progress .title = "Deployment verified!"
517+ progress .log (
518+ f"🐔 Ready the chicken! Your app is ready at [link={ deployment .url } ]{ deployment .url } [/link]"
519+ )
520+ else :
521+ progress .metadata ["done_emoji" ] = "❌"
522+ progress .title = "Deployment failed"
523+ human_status = DeploymentStatus .to_human_readable (final_status )
524+ progress .log (
525+ f"😔 Oh no! Deployment failed: { human_status } . "
526+ f"Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
527+ )
528+ raise typer .Exit (1 )
529+
530+
443531def _wait_for_deployment (
444532 toolkit : RichToolkit , app_id : str , deployment : CreateDeploymentResponse
445533) -> None :
@@ -451,73 +539,63 @@ def _wait_for_deployment(
451539 )
452540 toolkit .print_line ()
453541
454- toolkit .print (
455- f"You can also check the status at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]" ,
456- )
457- toolkit .print_line ()
458-
459542 time_elapsed = 0.0
460543
461544 started_at = time .monotonic ()
462545
463546 last_message_changed_at = time .monotonic ()
464547
465- with (
466- toolkit .progress (
548+ build_complete = False
549+
550+ with APIClient () as client :
551+ with toolkit .progress (
467552 next (messages ),
468553 inline_logs = True ,
469554 lines_to_show = 20 ,
470555 done_emoji = "🚀" ,
471- ) as progress ,
472- APIClient () as client ,
473- ):
474- try :
475- for log in client .stream_build_logs (deployment .id ):
476- time_elapsed = time .monotonic () - started_at
477-
478- if log .type == "message" :
479- progress .log (Text .from_ansi (log .message .rstrip ()))
556+ ) as progress :
557+ try :
558+ for log in client .stream_build_logs (deployment .id ):
559+ time_elapsed = time .monotonic () - started_at
480560
481- if log .type == "complete" :
482- progress .title = "Build complete!"
483- progress .log ("" )
484- progress .log (
485- f"You can also check the app logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
486- )
561+ if log .type == "message" :
562+ progress .log (Text .from_ansi (log .message .rstrip ()))
487563
488- progress .log ("" )
564+ if log .type == "complete" :
565+ build_complete = True
566+ progress .title = "Build complete!"
567+ break
489568
490- progress .log (
491- f"🐔 Ready the chicken! Your app is ready at [link={ deployment .url } ]{ deployment .url } [/link]"
492- )
569+ if log .type == "failed" :
570+ progress .log ("" )
571+ progress .log (
572+ f"😔 Oh no! Something went wrong. Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
573+ )
574+ raise typer .Exit (1 )
493575
494- break
576+ if time_elapsed > 30 :
577+ messages = cycle (LONG_WAIT_MESSAGES )
495578
496- if log .type == "failed" :
497- progress .log ("" )
498- progress .log (
499- f"😔 Oh no! Something went wrong. Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
500- )
501- raise typer .Exit (1 )
579+ if (time .monotonic () - last_message_changed_at ) > 2 :
580+ progress .title = next (messages )
502581
503- if time_elapsed > 30 :
504- messages = cycle (LONG_WAIT_MESSAGES )
582+ last_message_changed_at = time .monotonic ()
505583
506- if (time .monotonic () - last_message_changed_at ) > 2 :
507- progress .title = next (messages )
584+ except (StreamLogError , TooManyRetriesError , TimeoutError ) as e :
585+ progress .set_error (
586+ dedent (f"""
587+ [error]Build log streaming failed: { e } [/]
508588
509- last_message_changed_at = time .monotonic ()
589+ Unable to stream build logs. Check the dashboard for status: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]
590+ """ ).strip ()
591+ )
510592
511- except (StreamLogError , TooManyRetriesError , TimeoutError ) as e :
512- progress .set_error (
513- dedent (f"""
514- [error]Build log streaming failed: { e } [/]
593+ raise typer .Exit (1 ) from None
515594
516- Unable to stream build logs. Check the dashboard for status: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]
517- """ ).strip ()
518- )
595+ if build_complete :
596+ toolkit .print_line ()
519597
520- raise typer . Exit ( 1 ) from None
598+ _verify_deployment ( toolkit , client , app_id , deployment )
521599
522600
523601class SignupToWaitingList (BaseModel ):
0 commit comments