vantage-cli 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. vantage_cli/__init__.py +131 -0
  2. vantage_cli/apps/__init__.py +22 -0
  3. vantage_cli/apps/common.py +78 -0
  4. vantage_cli/apps/juju_localhost/__init__.py +17 -0
  5. vantage_cli/apps/juju_localhost/app.py +255 -0
  6. vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
  7. vantage_cli/apps/microk8s/README.md +47 -0
  8. vantage_cli/apps/microk8s/__init__.py +3 -0
  9. vantage_cli/apps/microk8s/app.py +301 -0
  10. vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
  11. vantage_cli/apps/multipass_singlenode/app.py +173 -0
  12. vantage_cli/apps/templates.py +178 -0
  13. vantage_cli/auth.py +429 -0
  14. vantage_cli/cache.py +143 -0
  15. vantage_cli/client.py +84 -0
  16. vantage_cli/command_base.py +63 -0
  17. vantage_cli/commands/__init__.py +1 -0
  18. vantage_cli/commands/clouds/__init__.py +20 -0
  19. vantage_cli/commands/clouds/add.py +81 -0
  20. vantage_cli/commands/clouds/delete.py +61 -0
  21. vantage_cli/commands/clouds/render.py +146 -0
  22. vantage_cli/commands/clouds/update.py +97 -0
  23. vantage_cli/commands/clusters/__init__.py +27 -0
  24. vantage_cli/commands/clusters/create.py +270 -0
  25. vantage_cli/commands/clusters/delete.py +101 -0
  26. vantage_cli/commands/clusters/get.py +30 -0
  27. vantage_cli/commands/clusters/list.py +84 -0
  28. vantage_cli/commands/clusters/render.py +233 -0
  29. vantage_cli/commands/clusters/schema.py +31 -0
  30. vantage_cli/commands/clusters/utils.py +248 -0
  31. vantage_cli/commands/profile/__init__.py +30 -0
  32. vantage_cli/commands/profile/crud.py +529 -0
  33. vantage_cli/commands/profile/render.py +55 -0
  34. vantage_cli/config.py +161 -0
  35. vantage_cli/constants.py +40 -0
  36. vantage_cli/exceptions.py +127 -0
  37. vantage_cli/format.py +39 -0
  38. vantage_cli/gql_client.py +655 -0
  39. vantage_cli/main.py +303 -0
  40. vantage_cli/render.py +56 -0
  41. vantage_cli/schemas.py +48 -0
  42. vantage_cli/time_loop.py +124 -0
  43. vantage_cli-0.1.1.dist-info/METADATA +30 -0
  44. vantage_cli-0.1.1.dist-info/RECORD +46 -0
  45. vantage_cli-0.1.1.dist-info/WHEEL +4 -0
  46. vantage_cli-0.1.1.dist-info/entry_points.txt +2 -0
vantage_cli/cache.py ADDED
@@ -0,0 +1,143 @@
1
+ """Token caching functionality for the Vantage CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from functools import wraps
7
+ from pathlib import Path
8
+
9
+ from loguru import logger
10
+
11
+ from vantage_cli.constants import USER_TOKEN_CACHE_DIR
12
+ from vantage_cli.exceptions import Abort
13
+ from vantage_cli.schemas import TokenSet
14
+
15
+
16
+ def init_cache(cache_dir: Path) -> None:
17
+ """Initialize cache directory."""
18
+ try:
19
+ USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
20
+ token_dir = USER_TOKEN_CACHE_DIR / "token"
21
+ token_dir.mkdir(exist_ok=True)
22
+ info_file = USER_TOKEN_CACHE_DIR / "info.txt"
23
+ info_file.write_text("This directory is used by Vantage CLI for its cache.")
24
+ except (PermissionError, OSError, FileNotFoundError) as e:
25
+ raise Abort(
26
+ f"""
27
+ Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
28
+ Error: {e}
29
+
30
+ Please check your home directory permissions and try again.
31
+ """,
32
+ subject="Non-writable cache dir",
33
+ log_message="Non-writable cache dir",
34
+ )
35
+
36
+
37
+ def with_cache(func):
38
+ """Initialize cache before function execution."""
39
+ if inspect.iscoroutinefunction(func):
40
+
41
+ @wraps(func)
42
+ async def async_wrapper(*args, **kwargs):
43
+ try:
44
+ USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
45
+ token_dir = USER_TOKEN_CACHE_DIR / "token"
46
+ token_dir.mkdir(exist_ok=True)
47
+ info_file = USER_TOKEN_CACHE_DIR / "info.txt"
48
+ info_file.write_text("This directory is used by Vantage CLI for its cache.")
49
+ except (PermissionError, OSError, FileNotFoundError) as e:
50
+ raise Abort(
51
+ f"""
52
+ Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
53
+ Error: {e}
54
+
55
+ Please check your home directory permissions and try again.
56
+ """,
57
+ subject="Non-writable cache dir",
58
+ log_message=f"Non-writable cache dir: {e}",
59
+ )
60
+ return await func(*args, **kwargs)
61
+
62
+ return async_wrapper
63
+ else:
64
+
65
+ @wraps(func)
66
+ def wrapper(*args, **kwargs):
67
+ try:
68
+ USER_TOKEN_CACHE_DIR.mkdir(exist_ok=True, parents=True)
69
+ token_dir = USER_TOKEN_CACHE_DIR / "token"
70
+ token_dir.mkdir(exist_ok=True)
71
+ info_file = USER_TOKEN_CACHE_DIR / "info.txt"
72
+ info_file.write_text("This directory is used by Vantage CLI for its cache.")
73
+ except (PermissionError, OSError, FileNotFoundError) as e:
74
+ raise Abort(
75
+ f"""
76
+ Cache directory {USER_TOKEN_CACHE_DIR} doesn't exist, is not writable, or could not be created.
77
+ Error: {e}
78
+
79
+ Please check your home directory permissions and try again.
80
+ """,
81
+ subject="Non-writable cache dir",
82
+ log_message="Non-writable cache dir",
83
+ )
84
+ return func(*args, **kwargs)
85
+
86
+ return wrapper
87
+
88
+
89
+ def _get_token_paths(profile: str) -> tuple[Path, Path]:
90
+ token_dir = USER_TOKEN_CACHE_DIR / profile
91
+ access_token_path: Path = token_dir / "access.token"
92
+ refresh_token_path: Path = token_dir / "refresh.token"
93
+ return (access_token_path, refresh_token_path)
94
+
95
+
96
+ def load_tokens_from_cache(profile: str) -> TokenSet:
97
+ """Load access token and refresh token from the cache."""
98
+ (access_token_path, refresh_token_path) = _get_token_paths(profile)
99
+
100
+ Abort.require_condition(
101
+ access_token_path.exists(),
102
+ "Please login with your auth token first using the `vantage login` command",
103
+ raise_kwargs={"subject": "You need to login"},
104
+ )
105
+
106
+ logger.debug("Retrieving access token from cache")
107
+ token_set: TokenSet = TokenSet(access_token=access_token_path.read_text())
108
+
109
+ if refresh_token_path.exists():
110
+ logger.debug("Retrieving refresh token from cache")
111
+ token_set.refresh_token = refresh_token_path.read_text()
112
+
113
+ return token_set
114
+
115
+
116
+ def save_tokens_to_cache(profile: str, token_set: TokenSet):
117
+ """Save tokens from a token_set to the cache."""
118
+ (access_token_path, refresh_token_path) = _get_token_paths(profile)
119
+ # make sure the parent directory exists
120
+ access_token_path.parent.mkdir(parents=True, exist_ok=True)
121
+
122
+ logger.debug(f"Caching access token at {access_token_path}")
123
+ access_token_path.write_text(token_set.access_token)
124
+ access_token_path.chmod(0o600)
125
+
126
+ if token_set.refresh_token is not None:
127
+ logger.debug(f"Caching refresh token at {refresh_token_path}")
128
+ refresh_token_path.write_text(token_set.refresh_token)
129
+ refresh_token_path.chmod(0o600)
130
+
131
+
132
+ def clear_token_cache(profile: str):
133
+ """Clear the token cache."""
134
+ logger.debug("Clearing cached tokens")
135
+ (access_token_path, refresh_token_path) = _get_token_paths(profile)
136
+
137
+ logger.debug(f"Removing access token at {access_token_path}")
138
+ if access_token_path.exists():
139
+ access_token_path.unlink()
140
+
141
+ logger.debug(f"Removing refresh token at {refresh_token_path}")
142
+ if refresh_token_path.exists():
143
+ refresh_token_path.unlink()
vantage_cli/client.py ADDED
@@ -0,0 +1,84 @@
1
+ """Simple async HTTP client for OAuth token operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import wraps
6
+ from typing import Any, Callable, TypeVar
7
+
8
+ import httpx
9
+ import pydantic
10
+ import typer
11
+ from loguru import logger
12
+
13
+ from vantage_cli.exceptions import Abort
14
+
15
+
16
+ def attach_client(func: Callable) -> Callable:
17
+ """Create async HTTP client for OAuth operations."""
18
+
19
+ @wraps(func)
20
+ async def wrapper(ctx: typer.Context, *args: Any, **kwargs: Any) -> Any:
21
+ if ctx.obj.settings is None:
22
+ raise Abort(
23
+ "Cannot attach client before settings!",
24
+ subject="Configuration Error",
25
+ log_message="Settings not configured before client attachment",
26
+ )
27
+
28
+ logger.debug("Creating async HTTP client for OAuth operations")
29
+ async with httpx.AsyncClient(
30
+ base_url=ctx.obj.settings.oidc_base_url,
31
+ headers={"content-type": "application/x-www-form-urlencoded"},
32
+ ) as client:
33
+ ctx.obj.client = client
34
+ return await func(ctx, *args, **kwargs)
35
+
36
+ return wrapper
37
+
38
+
39
+ ResponseModel = TypeVar("ResponseModel", bound=pydantic.BaseModel)
40
+
41
+
42
+ async def make_oauth_request(
43
+ client: httpx.AsyncClient,
44
+ url_path: str,
45
+ data: dict[str, Any],
46
+ response_model_cls: type[ResponseModel],
47
+ abort_message: str = "OAuth request failed",
48
+ abort_subject: str = "AUTHENTICATION ERROR",
49
+ ) -> ResponseModel:
50
+ """Make an async OAuth token request.
51
+
52
+ Simplified version focused only on OAuth POST requests with form data.
53
+ """
54
+ logger.debug(f"Making OAuth request to {url_path}")
55
+
56
+ try:
57
+ response = await client.post(url_path, data=data)
58
+ response.raise_for_status()
59
+
60
+ response_data = response.json()
61
+ logger.debug(f"OAuth response received: {response_data}")
62
+
63
+ return response_model_cls(**response_data)
64
+
65
+ except httpx.HTTPStatusError as e:
66
+ logger.error(
67
+ f"OAuth request failed with status {e.response.status_code}: {e.response.text}"
68
+ )
69
+ if abort_message == "IGNORE":
70
+ raise e
71
+ raise Abort(
72
+ f"{abort_message}: Received error response",
73
+ subject=abort_subject,
74
+ log_message=f"OAuth request failed: {e.response.status_code} - {e.response.text}",
75
+ )
76
+ except (httpx.RequestError, httpx.ConnectError, httpx.TimeoutException) as e:
77
+ logger.error(f"OAuth request failed: {e}")
78
+ if abort_message == "IGNORE":
79
+ raise e
80
+ raise Abort(
81
+ f"{abort_message}: Request failed",
82
+ subject=abort_subject,
83
+ log_message=f"OAuth request error: {e}",
84
+ )
@@ -0,0 +1,63 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Base command utilities for consistent global option handling."""
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from typing_extensions import Annotated
9
+
10
+ # Reusable type annotations for global options
11
+ JsonOption = Annotated[bool, typer.Option("--json", "-j", help="Output in JSON format")]
12
+ VerboseOption = Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")]
13
+
14
+
15
+ class GlobalOptions:
16
+ """Helper class to manage global options consistently across commands."""
17
+
18
+ def __init__(self, ctx: typer.Context, local_json: bool = False, local_verbose: bool = False):
19
+ self.ctx = ctx
20
+ self.local_json = local_json
21
+ self.local_verbose = local_verbose
22
+
23
+ @property
24
+ def json_output(self) -> bool:
25
+ """Get effective JSON output setting."""
26
+ global_json = getattr(self.ctx.obj, "json_output", False) if self.ctx.obj else False
27
+ return self.local_json or global_json
28
+
29
+ @property
30
+ def verbose(self) -> bool:
31
+ """Get effective verbose setting."""
32
+ global_verbose = getattr(self.ctx.obj, "verbose", False) if self.ctx.obj else False
33
+ return self.local_verbose or global_verbose
34
+
35
+ @property
36
+ def profile(self) -> Optional[str]:
37
+ """Get the active profile."""
38
+ return getattr(self.ctx.obj, "profile", None) if self.ctx.obj else None
39
+
40
+
41
+ def get_global_options(
42
+ ctx: typer.Context, json_output: bool = False, verbose: bool = False
43
+ ) -> GlobalOptions:
44
+ """Create GlobalOptions instance."""
45
+ return GlobalOptions(ctx, json_output, verbose)
46
+
47
+
48
+ # Utility functions for individual options
49
+ def get_effective_json_output(ctx: typer.Context, local_json: bool = False) -> bool:
50
+ """Get the effective JSON output setting with precedence: local > global > default."""
51
+ global_json = getattr(ctx.obj, "json_output", False) if ctx.obj else False
52
+ return local_json or global_json
53
+
54
+
55
+ def get_effective_verbose(ctx: typer.Context, local_verbose: bool = False) -> bool:
56
+ """Get the effective verbose setting with precedence: local > global > default."""
57
+ global_verbose = getattr(ctx.obj, "verbose", False) if ctx.obj else False
58
+ return local_verbose or global_verbose
59
+
60
+
61
+ def get_active_profile(ctx: typer.Context) -> Optional[str]:
62
+ """Get the active profile from context."""
63
+ return getattr(ctx.obj, "profile", None) if ctx.obj else None
@@ -0,0 +1 @@
1
+ """Vantage CLI command modules."""
@@ -0,0 +1,20 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Clouds management commands package."""
4
+
5
+ import typer
6
+
7
+ from .add import add_command
8
+ from .delete import delete_command
9
+ from .update import update_command
10
+
11
+ clouds_app = typer.Typer(
12
+ name="clouds",
13
+ help="Manage cloud provider configurations and integrations for your Vantage infrastructure.",
14
+ no_args_is_help=True,
15
+ )
16
+
17
+ # Register all commands
18
+ clouds_app.command("add")(add_command)
19
+ clouds_app.command("delete")(delete_command)
20
+ clouds_app.command("update")(update_command)
@@ -0,0 +1,81 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Add cloud command."""
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from typing_extensions import Annotated
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def add_command(
16
+ ctx: typer.Context,
17
+ cloud_name: Annotated[str, typer.Argument(help="Name of the cloud to add")],
18
+ provider: Annotated[
19
+ str, typer.Option("--provider", "-p", help="Cloud provider (aws, gcp, azure, etc.)")
20
+ ],
21
+ region: Annotated[
22
+ Optional[str], typer.Option("--region", "-r", help="Default region for the cloud")
23
+ ] = None,
24
+ config_file: Annotated[
25
+ Optional[Path],
26
+ typer.Option(
27
+ "--config-file",
28
+ help="Path to cloud configuration file",
29
+ exists=True,
30
+ file_okay=True,
31
+ dir_okay=False,
32
+ readable=True,
33
+ ),
34
+ ] = None,
35
+ credentials_file: Annotated[
36
+ Optional[Path],
37
+ typer.Option(
38
+ "--credentials-file",
39
+ help="Path to credentials file",
40
+ exists=True,
41
+ file_okay=True,
42
+ dir_okay=False,
43
+ readable=True,
44
+ ),
45
+ ] = None,
46
+ ):
47
+ """Add a new cloud configuration."""
48
+ verbose = ctx.obj.get("verbose", False)
49
+ settings = ctx.obj.get("settings")
50
+
51
+ logger.info(f"Adding cloud configuration: {cloud_name}")
52
+
53
+ if verbose:
54
+ logger.debug(f"Provider: {provider}")
55
+ logger.debug(f"Region: {region}")
56
+ logger.debug(f"Config file: {config_file}")
57
+ logger.debug(f"Credentials file: {credentials_file}")
58
+ logger.debug(f"Settings: {settings}")
59
+
60
+ # TODO: Validate that cloud doesn't already exist
61
+
62
+ if config_file:
63
+ logger.info(f"Using config file: {config_file}")
64
+ typer.echo(f"Adding cloud '{cloud_name}' with config file: {config_file}")
65
+ else:
66
+ typer.echo(f"Adding cloud '{cloud_name}' with provider: {provider}")
67
+ if region:
68
+ typer.echo(f"Default region: {region}")
69
+
70
+ if credentials_file:
71
+ logger.info(f"Using credentials file: {credentials_file}")
72
+ typer.echo(f"Credentials file: {credentials_file}")
73
+
74
+ # TODO: Implement actual cloud configuration addition logic
75
+ # This would typically:
76
+ # 1. Validate the cloud configuration
77
+ # 2. Store the cloud config in the appropriate location
78
+ # 3. Update the supported clouds list
79
+
80
+ logger.info(f"Cloud {cloud_name} added successfully")
81
+ typer.echo(f"✅ Cloud '{cloud_name}' added successfully")
@@ -0,0 +1,61 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Delete cloud command."""
4
+
5
+ import logging
6
+
7
+ import typer
8
+ from typing_extensions import Annotated
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def delete_command(
14
+ ctx: typer.Context,
15
+ cloud_name: Annotated[str, typer.Argument(help="Name of the cloud to delete")],
16
+ force: Annotated[
17
+ bool, typer.Option("--force", help="Force deletion without confirmation")
18
+ ] = False,
19
+ remove_credentials: Annotated[
20
+ bool, typer.Option("--remove-credentials", help="Also remove stored credentials")
21
+ ] = False,
22
+ ):
23
+ """Delete a cloud configuration."""
24
+ verbose = ctx.obj.get("verbose", False)
25
+ settings = ctx.obj.get("settings")
26
+
27
+ logger.info(f"Deleting cloud configuration: {cloud_name}")
28
+
29
+ if verbose:
30
+ logger.debug(f"Force: {force}")
31
+ logger.debug(f"Remove credentials: {remove_credentials}")
32
+ logger.debug(f"Settings: {settings}")
33
+
34
+ # TODO: Check if cloud exists
35
+ # TODO: Check if cloud is in use by any clusters
36
+
37
+ if not force:
38
+ warning_msg = f"Are you sure you want to delete cloud configuration '{cloud_name}'?"
39
+ if remove_credentials:
40
+ warning_msg += " This will also remove stored credentials."
41
+
42
+ confirm = typer.confirm(warning_msg)
43
+ if not confirm:
44
+ typer.echo("Operation cancelled.")
45
+ raise typer.Abort()
46
+
47
+ typer.echo(f"Deleting cloud configuration: {cloud_name}")
48
+
49
+ if remove_credentials:
50
+ typer.echo("Removing stored credentials...")
51
+ logger.info("Removing stored credentials")
52
+
53
+ # TODO: Implement actual cloud configuration deletion logic
54
+ # This would typically:
55
+ # 1. Remove cloud config from storage
56
+ # 2. Update the supported clouds list
57
+ # 3. Optionally remove credentials
58
+ # 4. Clean up any related files
59
+
60
+ logger.info(f"Cloud {cloud_name} deleted successfully")
61
+ typer.echo(f"✅ Cloud '{cloud_name}' deleted successfully")
@@ -0,0 +1,146 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Render helpers for cloud command output formatting."""
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from rich import print_json
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from vantage_cli.render import StyleMapper
13
+
14
+
15
+ def render_clouds_table(
16
+ clouds: List[Dict[str, Any]],
17
+ title: str = "Cloud Accounts",
18
+ total_count: Optional[int] = None,
19
+ json_output: bool = False,
20
+ ) -> None:
21
+ """Render a list of cloud accounts in a Rich table format.
22
+
23
+ Args:
24
+ clouds: List of cloud account dictionaries
25
+ title: Title for the table
26
+ total_count: Total number of cloud accounts available
27
+ json_output: If True, output as JSON instead of a table
28
+ """
29
+ if json_output:
30
+ output = {"clouds": clouds, "total": total_count or len(clouds)}
31
+ print_json(data=output)
32
+ return
33
+
34
+ if not clouds:
35
+ console = Console()
36
+ console.print()
37
+ console.print(Panel("No cloud accounts found.", title="[yellow]No Results"))
38
+ console.print()
39
+ return
40
+
41
+ # Define cloud-specific styling
42
+ style_mapper = StyleMapper(
43
+ name="bold cyan", provider="blue", status="green", accountId="white", region="yellow"
44
+ )
45
+
46
+ # Create the table
47
+ table = Table(
48
+ title=title,
49
+ caption=f"Items: {len(clouds)}{f' of {total_count}' if total_count else ''}",
50
+ show_header=True,
51
+ header_style="bold white",
52
+ )
53
+
54
+ # Add columns based on cloud data structure
55
+ column_mapping = {
56
+ "name": "Name",
57
+ "provider": "Provider",
58
+ "status": "Status",
59
+ "accountId": "Account ID",
60
+ "region": "Region",
61
+ }
62
+
63
+ # Add columns that exist in the first cloud entry
64
+ if clouds:
65
+ first_cloud = clouds[0]
66
+ for key, display_name in column_mapping.items():
67
+ if key in first_cloud:
68
+ table.add_column(display_name, **style_mapper.map_style(key))
69
+
70
+ # Add rows
71
+ for cloud in clouds:
72
+ row_values = []
73
+ for key in column_mapping.keys():
74
+ if key in cloud:
75
+ value = cloud.get(key, "")
76
+ if value is None:
77
+ value = ""
78
+ row_values.append(str(value))
79
+ if row_values: # Only add row if we have values
80
+ table.add_row(*row_values)
81
+
82
+ # Print the table
83
+ console = Console()
84
+ console.print()
85
+ console.print(table)
86
+ console.print()
87
+
88
+
89
+ def render_cloud_operation_result(
90
+ operation: str,
91
+ cloud_name: str,
92
+ success: bool = True,
93
+ details: Optional[Dict[str, Any]] = None,
94
+ json_output: bool = False,
95
+ ) -> None:
96
+ """Render the result of a cloud operation (add, update, delete).
97
+
98
+ Args:
99
+ operation: The operation performed (add, update, delete)
100
+ cloud_name: Name of the cloud account
101
+ success: Whether the operation was successful
102
+ details: Additional details about the operation
103
+ json_output: If True, output as JSON instead of a formatted view
104
+ """
105
+ if json_output:
106
+ result = {
107
+ "operation": operation,
108
+ "cloud_name": cloud_name,
109
+ "success": success,
110
+ "details": details or {},
111
+ }
112
+ print_json(data=result)
113
+ return
114
+
115
+ console = Console()
116
+ console.print()
117
+
118
+ status_icon = "✅" if success else "❌"
119
+ status_color = "green" if success else "red"
120
+ action_text = f"{operation.title()}d" if success else f"{operation.title()} Failed"
121
+
122
+ console.print(
123
+ Panel(
124
+ f"{status_icon} Cloud account '[bold cyan]{cloud_name}[/bold cyan]' {operation} {'successful' if success else 'failed'}!",
125
+ title=f"[{status_color}]{action_text}[/{status_color}]",
126
+ border_style=status_color,
127
+ )
128
+ )
129
+
130
+ if details and not json_output:
131
+ # Show additional details if provided
132
+ details_table = Table(
133
+ title="Operation Details", show_header=False, box=None, padding=(0, 1)
134
+ )
135
+ details_table.add_column("Field", style="bold blue", width=20)
136
+ details_table.add_column("Value", style="white")
137
+
138
+ for key, value in details.items():
139
+ if value is not None:
140
+ display_key = key.replace("_", " ").title()
141
+ details_table.add_row(display_key, str(value))
142
+
143
+ console.print()
144
+ console.print(details_table)
145
+
146
+ console.print()