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/config.py ADDED
@@ -0,0 +1,161 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Configuration settings for Vantage CLI."""
4
+
5
+ import inspect
6
+ import json
7
+ import shutil
8
+ from asyncio.log import logger
9
+ from functools import wraps
10
+ from typing import Any, Callable
11
+
12
+ import typer
13
+ from pydantic import BaseModel, ValidationError, computed_field
14
+
15
+ from .constants import (
16
+ USER_CONFIG_FILE,
17
+ USER_TOKEN_CACHE_DIR,
18
+ VANTAGE_CLI_ACTIVE_PROFILE,
19
+ VANTAGE_CLI_LOCAL_USER_BASE_DIR,
20
+ )
21
+
22
+
23
+ class Settings(BaseModel):
24
+ """Configuration settings for the Vantage CLI."""
25
+
26
+ supported_clouds: list[str] = ["localhost", "aws", "gcp", "azure", "on-premises", "k8s"]
27
+ api_base_url: str = "https://apis.vantagecompute.ai"
28
+ oidc_base_url: str = "https://auth.vantagecompute.ai"
29
+ tunnel_api_url: str = "https://tunnel.vantagecompute.ai"
30
+ oidc_client_id: str = "default"
31
+ oidc_max_poll_time: int = 5 * 60 # 5 minutes
32
+
33
+ @computed_field
34
+ @property
35
+ def oidc_domain(self) -> str:
36
+ """Extract the domain from the OIDC base URL."""
37
+ return self.oidc_base_url.split("//")[-1]
38
+
39
+ @computed_field
40
+ @property
41
+ def oidc_token_url(self) -> str:
42
+ """Construct the OIDC token URL from the base URL."""
43
+ return f"{self.oidc_base_url}/realms/vantage/protocol/openid-connect/token"
44
+
45
+
46
+ def init_user_filesystem(profile: str) -> None:
47
+ """Initialize the user filesystem directories for a profile."""
48
+ (USER_TOKEN_CACHE_DIR / profile).mkdir(parents=True, exist_ok=True)
49
+
50
+
51
+ def init_settings(**settings_values) -> Settings:
52
+ """Initialize settings with validation."""
53
+ try:
54
+ logger.debug("Validating settings")
55
+ return Settings(**settings_values)
56
+ except ValidationError as e:
57
+ logger.error(f"Settings validation error: {e}")
58
+ raise
59
+
60
+
61
+ def attach_settings(func: Callable[..., Any]) -> Callable[..., Any]:
62
+ """Attach settings to the CLI context."""
63
+ if inspect.iscoroutinefunction(func):
64
+
65
+ @wraps(func)
66
+ async def async_wrapper(ctx: typer.Context, *args, **kwargs):
67
+ try:
68
+ logger.debug(f"Loading settings from {USER_CONFIG_FILE}")
69
+ settings_all_profiles = json.loads(USER_CONFIG_FILE.read_text())
70
+ settings_values = settings_all_profiles.get(ctx.obj.profile)
71
+ except FileNotFoundError:
72
+ logger.error("Settings file missing!")
73
+ typer.echo(
74
+ f"""
75
+ No settings file found at {USER_CONFIG_FILE}!
76
+
77
+ Run the set-config sub-command first to establish your OIDC settings.
78
+ """
79
+ )
80
+ raise typer.Exit(1)
81
+ logger.debug("Binding settings to CLI context")
82
+ ctx.obj.settings = init_settings(**settings_values)
83
+ return await func(ctx, *args, **kwargs)
84
+
85
+ return async_wrapper
86
+ else:
87
+
88
+ @wraps(func)
89
+ def wrapper(ctx: typer.Context, *args, **kwargs):
90
+ try:
91
+ logger.debug(f"Loading settings from {USER_CONFIG_FILE}")
92
+ settings_all_profiles = json.loads(USER_CONFIG_FILE.read_text())
93
+ settings_values = settings_all_profiles.get(ctx.obj.profile)
94
+ except FileNotFoundError:
95
+ logger.error("Settings file missing!")
96
+ typer.echo(
97
+ f"""
98
+ No settings file found at {USER_CONFIG_FILE}!
99
+
100
+ Run the set-config sub-command first to establish your OIDC settings.
101
+ """
102
+ )
103
+ raise typer.Exit(1)
104
+ logger.debug("Binding settings to CLI context")
105
+ ctx.obj.settings = init_settings(**settings_values)
106
+ return func(ctx, *args, **kwargs)
107
+
108
+ return wrapper
109
+
110
+
111
+ def dump_settings(profile: str, settings: Settings) -> None:
112
+ """Save settings to the user configuration file."""
113
+ logger.debug(f"Saving settings to {USER_CONFIG_FILE}")
114
+ if USER_CONFIG_FILE.exists():
115
+ settings_all_profiles = json.loads(USER_CONFIG_FILE.read_text())
116
+ else:
117
+ settings_all_profiles = {}
118
+
119
+ settings_all_profiles[f"{profile}"] = settings.model_dump()
120
+ USER_CONFIG_FILE.write_text(json.dumps(settings_all_profiles))
121
+
122
+
123
+ def clear_settings() -> None:
124
+ """Remove saved settings configuration file."""
125
+ logger.debug(f"Removing saved settings at {USER_CONFIG_FILE}")
126
+ USER_CONFIG_FILE.unlink(missing_ok=True)
127
+ VANTAGE_CLI_ACTIVE_PROFILE.unlink(missing_ok=True)
128
+ try:
129
+ shutil.rmtree(f"{USER_TOKEN_CACHE_DIR}")
130
+ logger.debug(f"Removed user token cache directory at {USER_TOKEN_CACHE_DIR}")
131
+ except FileNotFoundError:
132
+ logger.debug("Token cache directory already absent; nothing to remove")
133
+
134
+
135
+ def ensure_default_profile_exists() -> None:
136
+ """Ensure the default profile exists and is properly configured."""
137
+ if VANTAGE_CLI_LOCAL_USER_BASE_DIR.exists() is False:
138
+ VANTAGE_CLI_LOCAL_USER_BASE_DIR.mkdir(parents=True, exist_ok=True)
139
+ logger.debug(f"Created local user base directory at {VANTAGE_CLI_LOCAL_USER_BASE_DIR}")
140
+
141
+ if not USER_CONFIG_FILE.exists():
142
+ # Create the default profile with default settings
143
+ logger.debug("Creating default profile on first run")
144
+ default_settings = Settings()
145
+ dump_settings("default", default_settings)
146
+
147
+
148
+ def get_active_profile() -> str:
149
+ """Get the currently active profile name."""
150
+ if VANTAGE_CLI_ACTIVE_PROFILE.exists():
151
+ try:
152
+ return VANTAGE_CLI_ACTIVE_PROFILE.read_text().strip()
153
+ except (FileNotFoundError, PermissionError):
154
+ pass
155
+ return "default"
156
+
157
+
158
+ def set_active_profile(profile_name: str) -> None:
159
+ """Set the active profile."""
160
+ VANTAGE_CLI_ACTIVE_PROFILE.parent.mkdir(parents=True, exist_ok=True)
161
+ VANTAGE_CLI_ACTIVE_PROFILE.write_text(profile_name)
@@ -0,0 +1,40 @@
1
+ # © 2025 Vantage Compute, Inc. All rights reserved.
2
+ # Confidential and proprietary. Unauthorized use prohibited.
3
+ """Constants for Vantage CLI."""
4
+
5
+ from pathlib import Path
6
+
7
+ VANTAGE_CLI_LOCAL_USER_BASE_DIR: Path = Path.home() / ".vantage-cli"
8
+ VANTAGE_CLI_ACTIVE_PROFILE: Path = VANTAGE_CLI_LOCAL_USER_BASE_DIR / "active_profile"
9
+
10
+ USER_CONFIG_FILE: Path = VANTAGE_CLI_LOCAL_USER_BASE_DIR / "config.json"
11
+
12
+ USER_TOKEN_CACHE_DIR: Path = VANTAGE_CLI_LOCAL_USER_BASE_DIR / "token_cache"
13
+
14
+ # Common deployment constants
15
+ DEFAULT_CLUSTER_NAME = "vantage-cluster"
16
+ DEFAULT_MODEL_PREFIX = "vantage"
17
+
18
+ # Multipass-specific constants
19
+ MULTIPASS_ARCH = "arm64"
20
+ MULTIPASS_CLOUD_IMAGE_URL = "https://vantage-public-assets.s3.us-west-2.amazonaws.com/multipass-singlenode/multipass-singlenode.img"
21
+ MULTIPASS_CLOUD_IMAGE_DEST = Path("/tmp/multipass-singlenode.img")
22
+ MULTIPASS_CLOUD_IMAGE_LOCAL = (
23
+ Path.home() / "multipass-singlenode" / "build" / "multipass-singlenode.img"
24
+ )
25
+
26
+ # Juju-specific constants
27
+ JUJU_SECRET_NAME = "vantage-jupyterhub-config"
28
+ JUJU_APPLICATION_NAME = "vantage-jupyterhub"
29
+
30
+ # Environment variable names
31
+ ENV_CLIENT_SECRET = "VANTAGE_CLIENT_SECRET"
32
+ ENV_OIDC_DOMAIN = "VANTAGE_OIDC_DOMAIN"
33
+ ENV_BASE_API_URL = "VANTAGE_BASE_API_URL"
34
+ ENV_TUNNEL_API_URL = "VANTAGE_TUNNEL_API_URL"
35
+
36
+ # Error messages
37
+ ERROR_NO_CLUSTER_DATA = "[red]Error: No cluster data provided.[/red]"
38
+ ERROR_NO_CLIENT_ID = "[red]Error: No client ID found in cluster data.[/red]"
39
+ ERROR_NO_CLIENT_SECRET = "[red]Error: No client secret found in cluster data.[/red]"
40
+ ERROR_MULTIPASS_NOT_FOUND = "[red]Error: 'multipass' is not installed or not found in PATH.[/red]"
@@ -0,0 +1,127 @@
1
+ """Exception handling and error management for the Vantage CLI."""
2
+
3
+ import inspect
4
+ from functools import wraps
5
+ from sys import exc_info
6
+
7
+ import buzz
8
+ import snick
9
+ import typer
10
+ from loguru import logger
11
+ from rich import traceback
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+
15
+ # Enables prettified traceback printing via rich
16
+ traceback.install()
17
+
18
+
19
+ class VantageCliError(buzz.Buzz):
20
+ """Base exception class for Vantage CLI errors."""
21
+
22
+ pass
23
+
24
+
25
+ class DeploymentError(VantageCliError):
26
+ """Exception for deployment-related failures."""
27
+
28
+ pass
29
+
30
+
31
+ class ValidationError(VantageCliError):
32
+ """Exception for data validation failures."""
33
+
34
+ pass
35
+
36
+
37
+ class ConfigurationError(VantageCliError):
38
+ """Exception for configuration-related issues."""
39
+
40
+ pass
41
+
42
+
43
+ class AuthenticationError(VantageCliError):
44
+ """Exception for authentication and authorization failures."""
45
+
46
+ pass
47
+
48
+
49
+ class ApiError(VantageCliError):
50
+ """Exception for API communication failures."""
51
+
52
+ pass
53
+
54
+
55
+ class Abort(buzz.Buzz):
56
+ """Exception class for aborting operations with user-friendly messages."""
57
+
58
+ def __init__(
59
+ self,
60
+ message,
61
+ *args,
62
+ subject=None,
63
+ log_message=None,
64
+ warn_only=False,
65
+ **kwargs,
66
+ ):
67
+ self.subject = subject
68
+ self.log_message = log_message
69
+ self.warn_only = warn_only
70
+ (_, self.original_error, __) = exc_info()
71
+ super().__init__(message, *args, **kwargs)
72
+
73
+
74
+ def handle_abort(func):
75
+ """Handle abort exceptions in decorated functions."""
76
+ if inspect.iscoroutinefunction(func):
77
+
78
+ @wraps(func)
79
+ async def async_wrapper(*args, **kwargs):
80
+ try:
81
+ return await func(*args, **kwargs)
82
+ except Abort as err:
83
+ if not err.warn_only:
84
+ if err.log_message is not None:
85
+ logger.error(err.log_message)
86
+
87
+ if err.original_error is not None:
88
+ logger.error(f"Original exception: {err.original_error}")
89
+
90
+ panel_kwargs = {}
91
+ if err.subject is not None:
92
+ panel_kwargs["title"] = f"[red]{err.subject}"
93
+ message = snick.dedent(err.message)
94
+
95
+ console = Console()
96
+ console.print()
97
+ console.print(Panel(message, **panel_kwargs))
98
+ console.print()
99
+ raise typer.Exit(code=1)
100
+
101
+ return async_wrapper
102
+ else:
103
+
104
+ @wraps(func)
105
+ def wrapper(*args, **kwargs):
106
+ try:
107
+ return func(*args, **kwargs)
108
+ except Abort as err:
109
+ if not err.warn_only:
110
+ if err.log_message is not None:
111
+ logger.error(err.log_message)
112
+
113
+ if err.original_error is not None:
114
+ logger.error(f"Original exception: {err.original_error}")
115
+
116
+ panel_kwargs = {}
117
+ if err.subject is not None:
118
+ panel_kwargs["title"] = f"[red]{err.subject}"
119
+ message = snick.dedent(err.message)
120
+
121
+ console = Console()
122
+ console.print()
123
+ console.print(Panel(message, **panel_kwargs))
124
+ console.print()
125
+ raise typer.Exit(code=1)
126
+
127
+ return wrapper
vantage_cli/format.py ADDED
@@ -0,0 +1,39 @@
1
+ """Output formatting utilities for the Vantage CLI."""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+
6
+ import snick
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+
11
+ def terminal_message(
12
+ message: str,
13
+ subject: Optional[str],
14
+ color: str = "green",
15
+ footer: Optional[str] = None,
16
+ indent: bool = True,
17
+ ):
18
+ """Display a formatted message in the terminal."""
19
+ text = snick.dedent(message)
20
+ if indent:
21
+ text = snick.indent(text, prefix=" ")
22
+ console = Console()
23
+ console.print()
24
+
25
+ # Build panel with explicit parameters
26
+ panel_title = f"[{color}]{subject}" if subject is not None else None
27
+ panel_subtitle = f"[dim italic]{footer}[/dim italic]" if footer is not None else None
28
+ panel = Panel(text, title=panel_title, subtitle=panel_subtitle, padding=(1, 1))
29
+
30
+ console.print(panel)
31
+ console.print()
32
+
33
+
34
+ def render_json(data: Any) -> None:
35
+ """Render data as formatted JSON output."""
36
+ console = Console()
37
+ console.print()
38
+ console.print_json(json.dumps(data))
39
+ console.print()