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
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1from typing import Annotated, Any 1qrstuvw
3import typer 1qrstuvw
4from pydantic import BaseModel, Field 1qrstuvw
5from rich_toolkit import RichToolkit 1qrstuvw
6from rich_toolkit.menu import Option 1qrstuvw
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
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
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
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 )
54 toolkit.print( 1ajChcxEkfyFldzGmbnDigAHoeBIp
55 f"{environment_variable_warning} "
56 f"The {provider_name} resource itself will not be deleted.",
57 emoji="💡",
58 )
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 )
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 )
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.
107 The provider resource is not deleted, but its managed environment variables
108 are removed from the app.
109 """
111 toolkit = get_user_command_context(ctx).toolkit 1ajCX4QhJcxEY5SkLfyFZ6TlMdzG07UmNbnD18RiKgAH29VoOeBI3!WpP
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
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 )
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 )
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
149 toolkit.print_title("disconnect resource") 1aJcLfMdNbKgOeP
150 toolkit.print_line() 1aJcLfMdNbKgOeP
152 if not resources: 1aJcLfMdNbKgOeP
153 toolkit.print("No connected resources found.", bullet=False) 1JLMNKOP
154 return 1JLMNKOP
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
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 )
193 if resource_id_was_provided: 1ajChcxEkfyFldzGmbnDigAHoeBIp
194 toolkit.print_title("disconnect resource") 1jChxEkyFlzGmnDiAHoBIp
195 toolkit.print_line() 1jChxEkyFlzGmnDiAHoBIp
197 _print_disconnect_warning( 1ajChcxEkfyFldzGmbnDigAHoeBIp
198 toolkit,
199 provider_name=PROVIDER_NAMES[resource.provider],
200 environment_variables=resource.environment_variables,
201 )
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
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 )
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 )