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,71 +539,61 @@ 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 (
467- next (messages ), inline_logs = True , lines_to_show = 20
468- ) as progress ,
469- APIClient () as client ,
470- ):
471- progress .metadata ["done_emoji" ] = "🚀"
472- try :
473- for log in client .stream_build_logs (deployment .id ):
474- time_elapsed = time .monotonic () - started_at
548+ build_complete = False
475549
476- if log .type == "message" :
477- progress .log (Text .from_ansi (log .message .rstrip ()))
550+ with APIClient () as client :
551+ with toolkit .progress (
552+ next (messages ), inline_logs = True , lines_to_show = 20
553+ ) as progress :
554+ progress .metadata ["done_emoji" ] = "🚀"
555+ try :
556+ for log in client .stream_build_logs (deployment .id ):
557+ time_elapsed = time .monotonic () - started_at
478558
479- if log .type == "complete" :
480- progress .title = "Build complete!"
481- progress .log ("" )
482- progress .log (
483- f"You can also check the app logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
484- )
559+ if log .type == "message" :
560+ progress .log (Text .from_ansi (log .message .rstrip ()))
485561
486- progress .log ("" )
562+ if log .type == "complete" :
563+ build_complete = True
564+ progress .title = "Build complete!"
565+ break
487566
488- progress .log (
489- f"🐔 Ready the chicken! Your app is ready at [link={ deployment .url } ]{ deployment .url } [/link]"
490- )
567+ if log .type == "failed" :
568+ progress .log ("" )
569+ progress .log (
570+ f"😔 Oh no! Something went wrong. Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
571+ )
572+ raise typer .Exit (1 )
491573
492- break
574+ if time_elapsed > 30 :
575+ messages = cycle (LONG_WAIT_MESSAGES )
493576
494- if log .type == "failed" :
495- progress .log ("" )
496- progress .log (
497- f"😔 Oh no! Something went wrong. Check out the logs at [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]"
498- )
499- raise typer .Exit (1 )
577+ if (time .monotonic () - last_message_changed_at ) > 2 :
578+ progress .title = next (messages )
500579
501- if time_elapsed > 30 :
502- messages = cycle (LONG_WAIT_MESSAGES )
580+ last_message_changed_at = time .monotonic ()
503581
504- if (time .monotonic () - last_message_changed_at ) > 2 :
505- progress .title = next (messages )
582+ except (StreamLogError , TooManyRetriesError , TimeoutError ) as e :
583+ progress .set_error (
584+ dedent (f"""
585+ [error]Build log streaming failed: { e } [/]
506586
507- last_message_changed_at = time .monotonic ()
587+ Unable to stream build logs. Check the dashboard for status: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]
588+ """ ).strip ()
589+ )
508590
509- except (StreamLogError , TooManyRetriesError , TimeoutError ) as e :
510- progress .set_error (
511- dedent (f"""
512- [error]Build log streaming failed: { e } [/]
591+ raise typer .Exit (1 ) from None
513592
514- Unable to stream build logs. Check the dashboard for status: [link={ deployment .dashboard_url } ]{ deployment .dashboard_url } [/link]
515- """ ).strip ()
516- )
593+ if build_complete :
594+ toolkit .print_line ()
517595
518- raise typer . Exit ( 1 ) from None
596+ _verify_deployment ( toolkit , client , app_id , deployment )
519597
520598
521599class SignupToWaitingList (BaseModel ):
0 commit comments