Coverage for src/fastapi_cloud_cli/commands/domains/add.py: 100%
58 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
1from typing import Annotated, Any 1opqrstu
3import typer 1opqrstu
4from pydantic import BaseModel, Field 1opqrstu
5from rich_toolkit import RichToolkit 1opqrstu
6from rich_toolkit.menu import Option 1opqrstu
8from fastapi_cloud_cli.api import APIClient, CustomDomain 1opqrstu
9from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1opqrstu
10from fastapi_cloud_cli.commands.domains._app import domains_app 1opqrstu
11from fastapi_cloud_cli.commands.domains._shared import _normalize_domain_name 1opqrstu
12from fastapi_cloud_cli.commands.domains.rendering import render_custom_domain_setup 1opqrstu
13from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail 1opqrstu
14from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit 1opqrstu
15from fastapi_cloud_cli.utils.execution import JsonOutputOption 1opqrstu
18class CustomDomainAddOutput(BaseModel): 1opqrstu
19 app_id: str 1opqrstu
20 domain: CustomDomain 1opqrstu
21 show_title: Annotated[bool, Field(exclude=True)] = True 1opqrstu
24def _resolve_domain_name( 1opqrstu
25 toolkit: FastAPIRichToolkit,
26 *,
27 domain: str | None,
28) -> str:
29 if domain is None: 1#4CaJKLvb$5EePQRwf%6FgSTUAh'7GiVWXxj(8DcMNOyd)9HkYZ0Bl*!Im123zn
30 if toolkit.mode == "json": 1#ab$ef%gh'ij(cd)kl*mn
31 toolkit.fail( 1#$%'()*
32 "missing_required_input",
33 "Custom domain is required.",
34 hint="Pass DOMAIN to choose the hostname to add.",
35 )
37 domain = toolkit.input( 1abefghijcdklmn
38 "What domain do you want to add?",
39 emoji="🌐",
40 bullet=False,
41 )
42 toolkit.print_line() 1abefghijcdklmn
44 return _normalize_domain_name(domain) 14CaJKLvb5EePQRwf6FgSTUAh7GiVWXxj8DcMNOyd9HkYZ0Bl!Im123zn
47def _resolve_pre_validation( 1opqrstu
48 toolkit: FastAPIRichToolkit,
49 *,
50 domain: str,
51 standard: bool,
52 zero_downtime: bool,
53) -> bool:
54 if standard: 14CaJKLvb5EePQRwf6FgSTUAh7GiVWXxj8DcMNOyd9HkYZ0Bl!Im123zn
55 return False 1CJKLvEPQRwFSTUAGVWXxDMNOyHYZ0BI123z
56 if zero_downtime: 14Cab5Eef6Fgh7Gij8Dcd9Hkl!Imn
57 return True 1CEFGDHI
58 if toolkit.mode == "json": 14ab5ef6gh7ij8cd9kl!mn
59 toolkit.fail( 1456789!
60 "missing_required_input",
61 "Custom domain setup mode is required.",
62 hint="Pass either --standard or --zero-downtime.",
63 )
65 toolkit.print(f"Is {domain} already serving traffic?") 1abefghijcdklmn
66 toolkit.print_line() 1abefghijcdklmn
68 return toolkit.ask( 1abefghijcdklmn
69 "",
70 options=[
71 Option(
72 {
73 "name": "No — set up a new or unused domain",
74 "value": False,
75 }
76 ),
77 Option(
78 {
79 "name": "Yes — migrate it without downtime",
80 "value": True,
81 }
82 ),
83 ],
84 )
87def _render_custom_domain_add_output( 1opwqrxstuz
88 data: CustomDomainAddOutput,
89 toolkit: RichToolkit,
90) -> None:
91 if data.show_title: 1avbewfgAhixjcydkBlmzn
92 toolkit.print_title("custom domains") 1vwAxyBz
93 toolkit.print_line() 1vwAxyBz
95 toolkit.print(f"Added [bold]{data.domain.name}[/bold]", emoji="🐔") 1avbewfgAhixjcydkBlmzn
96 render_custom_domain_setup(data.domain, toolkit) 1avbewfgAhixjcydkBlmzn
99@domains_app.command("add", cls=UserCommand) 1opqrstu
100def add_domain( 1op=qr?stu@
101 ctx: typer.Context,
102 domain: Annotated[
103 str | None,
104 typer.Argument(
105 help="Hostname of the custom domain to add.",
106 ),
107 ] = None,
108 app_id: Annotated[
109 str | None,
110 typer.Option(
111 "--app-id",
112 help="ID of the app to which the custom domain should be added.",
113 ),
114 ] = None,
115 standard: Annotated[
116 bool,
117 typer.Option(
118 "--standard",
119 help="Set up a new or unused domain.",
120 ),
121 ] = False,
122 zero_downtime: Annotated[
123 bool,
124 typer.Option(
125 "--zero-downtime",
126 help="Migrate an already-live domain without downtime.",
127 ),
128 ] = False,
129 json_output: JsonOutputOption = False,
130) -> Any:
131 """
132 Add a custom domain to an app.
133 """
134 toolkit = get_user_command_context(ctx).toolkit 1#4C+aJKLvb$5E,ePQRwf%6F-gSTUAh'7G.iVWXxj(8D/cMNOyd)9H:kYZ0Bl*!I;m123zn
135 app_id = resolve_app_id_or_fail(toolkit, app_id=app_id) 1#4C+aJKLvb$5E,ePQRwf%6F-gSTUAh'7G.iVWXxj(8D/cMNOyd)9H:kYZ0Bl*!I;m123zn
137 if standard and zero_downtime: 1#4C+aJKLvb$5E,ePQRwf%6F-gSTUAh'7G.iVWXxj(8D/cMNOyd)9H:kYZ0Bl*!I;m123zn
138 toolkit.fail( 1+,-./:;
139 "invalid_input",
140 "Setup modes are mutually exclusive.",
141 hint="Pass either --standard or --zero-downtime.",
142 )
144 prompts_user = domain is None or not (standard or zero_downtime) 1#4CaJKLvb$5EePQRwf%6FgSTUAh'7GiVWXxj(8DcMNOyd)9HkYZ0Bl*!Im123zn
145 if prompts_user and toolkit.mode != "json": 1#4CaJKLvb$5EePQRwf%6FgSTUAh'7GiVWXxj(8DcMNOyd)9HkYZ0Bl*!Im123zn
146 toolkit.print_title("custom domains") 1abefghijcdklmn
147 toolkit.print_line() 1abefghijcdklmn
149 domain = _resolve_domain_name(toolkit, domain=domain) 1#4CaJKLvb$5EePQRwf%6FgSTUAh'7GiVWXxj(8DcMNOyd)9HkYZ0Bl*!Im123zn
150 is_using_pre_validation = _resolve_pre_validation( 14CaJKLvb5EePQRwf6FgSTUAh7GiVWXxj8DcMNOyd9HkYZ0Bl!Im123zn
151 toolkit,
152 domain=domain,
153 standard=standard,
154 zero_downtime=zero_downtime,
155 )
157 if prompts_user: 1CaJKLvbEePQRwfFgSTUAhGiVWXxjDcMNOydHkYZ0BlIm123zn
158 toolkit.print_line() 1abefghijcdklmn
160 with APIClient() as client: 1CaJKLvbEePQRwfFgSTUAhGiVWXxjDcMNOydHkYZ0BlIm123zn
161 with ( 1CaJKLvbDcMNOyd
162 toolkit.progress(
163 title="Adding custom domain",
164 transient=True,
165 ) as progress,
166 client.handle_http_errors(
167 progress,
168 default_message=("Error adding custom domain. Please try again later."),
169 not_found_message="App not found.",
170 toolkit=toolkit,
171 ),
172 ):
173 created_domain = client.create_custom_domain( 1CaJKLvbEePQRwfFgSTUAhGiVWXxjDcMNOydHkYZ0BlIm123zn
174 app_id=app_id,
175 name=domain,
176 is_using_pre_validation=is_using_pre_validation,
177 )
179 toolkit.success( 1CavbEewfFgAhGixjDcydHkBlImzn
180 CustomDomainAddOutput(
181 app_id=app_id,
182 domain=created_domain,
183 show_title=not prompts_user,
184 ),
185 render_output=_render_custom_domain_add_output,
186 )