Coverage for src/fastapi_cloud_cli/commands/integrations/resources/disconnect.py: 100%

71 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-11-09 00:25 +0000

1from typing import Annotated, Any 1qrstuvw

2 

3import typer 1qrstuvw

4from pydantic import BaseModel, Field 1qrstuvw

5from rich_toolkit import RichToolkit 1qrstuvw

6from rich_toolkit.menu import Option 1qrstuvw

7 

8from fastapi_cloud_cli.api import APIClient 1qrstuvw

9from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1qrstuvw

10from fastapi_cloud_cli.commands.integrations.resources._app import resources_app 1qrstuvw

11from fastapi_cloud_cli.commands.integrations.resources.get import _get_resource 1qrstuvw

12from fastapi_cloud_cli.commands.integrations.resources.list import _get_resources 1qrstuvw

13from fastapi_cloud_cli.commands.integrations.resources.providers import PROVIDER_NAMES 1qrstuvw

14from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail 1qrstuvw

15from fastapi_cloud_cli.utils.execution import JsonOutputOption 1qrstuvw

16 

17 

18class ResourceDisconnectOutput(BaseModel): 1qrstuvw

19 app_id: str 1qrstuvw

20 resource_id: str 1qrstuvw

21 disconnected: bool = True 1qrstuvw

22 resource_name: Annotated[str, Field(exclude=True)] 1qrstuvw

23 environment_variables: Annotated[list[str], Field(exclude=True)] 1qrstuvw

24 

25 

26def _disconnect_resource( 1qrstuvw

27 client: APIClient, 

28 *, 

29 app_id: str, 

30 resource_id: str, 

31) -> None: 

32 response = client.delete(f"/apps/{app_id}/connected-resources/{resource_id}") 1ajhcxkfyldzmbnigAoeBp

33 response.raise_for_status() 1ajhcxkfyldzmbnigAoeBp

34 

35 

36def _print_disconnect_warning( 1qrstuvw

37 toolkit: RichToolkit, 

38 *, 

39 provider_name: str, 

40 environment_variables: list[str], 

41) -> None: 

42 if environment_variables: 1ajChcxEkfyFldzGmbnDigAHoeBIp

43 variable_label = "variable" if len(environment_variables) == 1 else "variables" 1ajCcxEfyFdzGbnDgAHeBI

44 environment_variable_warning = ( 1ajCcxEfyFdzGbnDgAHeBI

45 f"The managed environment {variable_label} " 

46 f"[bold]{', '.join(environment_variables)}[/bold] will be removed " 

47 "from the app." 

48 ) 

49 else: 

50 environment_variable_warning = ( 1hklmiop

51 "Managed environment variables will be removed from the app." 

52 ) 

53 

54 toolkit.print( 1ajChcxEkfyFldzGmbnDigAHoeBIp

55 f"{environment_variable_warning} " 

56 f"The {provider_name} resource itself will not be deleted.", 

57 emoji="💡", 

58 ) 

59 

60 

61def _render_resource_disconnect_output( 1qrcstduvwe

62 data: ResourceDisconnectOutput, 

63 toolkit: RichToolkit, 

64) -> None: 

65 toolkit.print( 1ahckfldmbigoep

66 f"Disconnected [bold]{data.resource_name}[/bold] from the app.", 

67 emoji="🔌", 

68 ) 

69 

70 if data.environment_variables: 1ahckfldmbigoep

71 toolkit.print_line() 1acfdbge

72 toolkit.print( 1acfdbge

73 "Removed managed environment variables: " 

74 f"[bold]{', '.join(data.environment_variables)}[/bold]." 

75 ) 

76 

77 

78@resources_app.command("disconnect", cls=UserCommand) 1qrstuvw

79def disconnect_resource( 1qr#st$uvw%

80 ctx: typer.Context, 

81 resource_id: Annotated[ 

82 str | None, 

83 typer.Argument( 

84 help="ID of the connected resource to disconnect.", 

85 ), 

86 ] = None, 

87 app_id: Annotated[ 

88 str | None, 

89 typer.Option( 

90 "--app-id", 

91 help="ID of the app that owns the connected resource.", 

92 ), 

93 ] = None, 

94 yes: Annotated[ 

95 bool, 

96 typer.Option( 

97 "--yes", 

98 "-y", 

99 help="Confirm disconnection without prompting.", 

100 ), 

101 ] = False, 

102 json_output: JsonOutputOption = False, 

103) -> Any: 

104 """ 

105 Disconnect a provider resource from an app. 

106 

107 The provider resource is not deleted, but its managed environment variables 

108 are removed from the app. 

109 """ 

110 

111 toolkit = get_user_command_context(ctx).toolkit 1ajCX4QhJcxEY5SkLfyFZ6TlMdzG07UmNbnD18RiKgAH29VoOeBI3!WpP

112 

113 app_id = resolve_app_id_or_fail(toolkit, app_id=app_id) 1ajCX4QhJcxEY5SkLfyFZ6TlMdzG07UmNbnD18RiKgAH29VoOeBI3!WpP

114 resource_id_was_provided = resource_id is not None 1ajCX4QhJcxEY5SkLfyFZ6TlMdzG07UmNbnD18RiKgAH29VoOeBI3!WpP

115 

116 if json_output: 1ajCX4QhJcxEY5SkLfyFZ6TlMdzG07UmNbnD18RiKgAH29VoOeBI3!WpP

117 if resource_id is None: 1jX4QxY5SyZ6Tz07Un18RA29VB3!W

118 toolkit.fail( 1456789!

119 "missing_required_input", 

120 "Resource ID is required.", 

121 hint="Pass RESOURCE_ID to choose a connected resource.", 

122 ) 

123 

124 if not yes: 1jXQxYSyZTz0Un1RA2VB3W

125 toolkit.fail( 1XYZ0123

126 "missing_required_input", 

127 "Disconnection confirmation is required.", 

128 hint="Pass --yes to confirm disconnection.", 

129 ) 

130 

131 with APIClient() as client: 1ajCQhJcxESkLfyFTlMdzGUmNbnDRiKgAHVoOeBIWpP

132 if resource_id is None: 1ajCQhJcxESkLfyFTlMdzGUmNbnDRiKgAHVoOeBIWpP

133 with ( 1aJbK

134 toolkit.progress( 

135 title="Fetching connected resources", 

136 transient=True, 

137 ) as progress, 

138 client.handle_http_errors( 

139 progress, 

140 default_message=( 

141 "Error fetching connected resources. Please try again later." 

142 ), 

143 not_found_message="App not found.", 

144 toolkit=toolkit, 

145 ), 

146 ): 

147 resources = _get_resources(client, app_id=app_id) 1aJcLfMdNbKgOeP

148 

149 toolkit.print_title("disconnect resource") 1aJcLfMdNbKgOeP

150 toolkit.print_line() 1aJcLfMdNbKgOeP

151 

152 if not resources: 1aJcLfMdNbKgOeP

153 toolkit.print("No connected resources found.", bullet=False) 1JLMNKOP

154 return 1JLMNKOP

155 

156 resource_id = toolkit.ask( 1acfdbge

157 "Select the resource to disconnect:", 

158 options=[ 

159 Option( 

160 { 

161 "name": ( 

162 f"{resource.name} ({PROVIDER_NAMES[resource.provider]})" 

163 ), 

164 "value": resource.id, 

165 } 

166 ) 

167 for resource in resources 

168 ], 

169 bullet=False, 

170 ) 

171 toolkit.print_line() 1acfdbge

172 

173 with ( 1ajCQhbnDRi

174 toolkit.progress( 

175 title="Fetching connected resource", 

176 transient=True, 

177 ) as progress, 

178 client.handle_http_errors( 

179 progress, 

180 default_message=( 

181 "Error fetching connected resource. Please try again later." 

182 ), 

183 not_found_message="Connected resource not found.", 

184 toolkit=toolkit, 

185 ), 

186 ): 

187 resource = _get_resource( 1ajCQhcxESkfyFTldzGUmbnDRigAHVoeBIWp

188 client, 

189 app_id=app_id, 

190 resource_id=resource_id, 

191 ) 

192 

193 if resource_id_was_provided: 1ajChcxEkfyFldzGmbnDigAHoeBIp

194 toolkit.print_title("disconnect resource") 1jChxEkyFlzGmnDiAHoBIp

195 toolkit.print_line() 1jChxEkyFlzGmnDiAHoBIp

196 

197 _print_disconnect_warning( 1ajChcxEkfyFldzGmbnDigAHoeBIp

198 toolkit, 

199 provider_name=PROVIDER_NAMES[resource.provider], 

200 environment_variables=resource.environment_variables, 

201 ) 

202 

203 if not yes: 1ajChcxEkfyFldzGmbnDigAHoeBIp

204 toolkit.print_line() 1aChcEkfFldGmbDigHoeIp

205 should_disconnect = toolkit.confirm( 1aChcEkfFldGmbDigHoeIp

206 f"Disconnect [bold]{resource.name}[/bold]?", 

207 default=False, 

208 bullet=False, 

209 ) 

210 if not should_disconnect: 1aChcEkfFldGmbDigHoeIp

211 toolkit.print_line() 1CEFGDHI

212 toolkit.print("Disconnection cancelled.", bullet=False) 1CEFGDHI

213 raise typer.Exit(0) 1CEFGDHI

214 

215 toolkit.print_line() 1ajhcxkfyldzmbnigAoeBp

216 with ( 1ajhbni

217 toolkit.progress( 

218 title="Disconnecting connected resource", 

219 transient=True, 

220 ) as progress, 

221 client.handle_http_errors( 

222 progress, 

223 default_message=( 

224 "Error disconnecting connected resource. Please try again later." 

225 ), 

226 not_found_message="Connected resource not found.", 

227 toolkit=toolkit, 

228 ), 

229 ): 

230 _disconnect_resource( 1ajhcxkfyldzmbnigAoeBp

231 client, 

232 app_id=app_id, 

233 resource_id=resource.id, 

234 ) 

235 

236 toolkit.success( 1ajhcxkfyldzmbnigAoeBp

237 ResourceDisconnectOutput( 

238 app_id=app_id, 

239 resource_id=resource.id, 

240 resource_name=resource.name, 

241 environment_variables=resource.environment_variables, 

242 ), 

243 render_output=_render_resource_disconnect_output, 

244 )