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

1from typing import Annotated, Any 1opqrstu

2 

3import typer 1opqrstu

4from pydantic import BaseModel, Field 1opqrstu

5from rich_toolkit import RichToolkit 1opqrstu

6from rich_toolkit.menu import Option 1opqrstu

7 

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

16 

17 

18class CustomDomainAddOutput(BaseModel): 1opqrstu

19 app_id: str 1opqrstu

20 domain: CustomDomain 1opqrstu

21 show_title: Annotated[bool, Field(exclude=True)] = True 1opqrstu

22 

23 

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 ) 

36 

37 domain = toolkit.input( 1abefghijcdklmn

38 "What domain do you want to add?", 

39 emoji="🌐", 

40 bullet=False, 

41 ) 

42 toolkit.print_line() 1abefghijcdklmn

43 

44 return _normalize_domain_name(domain) 14CaJKLvb5EePQRwf6FgSTUAh7GiVWXxj8DcMNOyd9HkYZ0Bl!Im123zn

45 

46 

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 ) 

64 

65 toolkit.print(f"Is {domain} already serving traffic?") 1abefghijcdklmn

66 toolkit.print_line() 1abefghijcdklmn

67 

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 ) 

85 

86 

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

94 

95 toolkit.print(f"Added [bold]{data.domain.name}[/bold]", emoji="🐔") 1avbewfgAhixjcydkBlmzn

96 render_custom_domain_setup(data.domain, toolkit) 1avbewfgAhixjcydkBlmzn

97 

98 

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

136 

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 ) 

143 

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

148 

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 ) 

156 

157 if prompts_user: 1CaJKLvbEePQRwfFgSTUAhGiVWXxjDcMNOydHkYZ0BlIm123zn

158 toolkit.print_line() 1abefghijcdklmn

159 

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 ) 

178 

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 )