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.
- vantage_cli/__init__.py +131 -0
- vantage_cli/apps/__init__.py +22 -0
- vantage_cli/apps/common.py +78 -0
- vantage_cli/apps/juju_localhost/__init__.py +17 -0
- vantage_cli/apps/juju_localhost/app.py +255 -0
- vantage_cli/apps/juju_localhost/bundle_yaml.py +143 -0
- vantage_cli/apps/microk8s/README.md +47 -0
- vantage_cli/apps/microk8s/__init__.py +3 -0
- vantage_cli/apps/microk8s/app.py +301 -0
- vantage_cli/apps/multipass_singlenode/__init__.py +12 -0
- vantage_cli/apps/multipass_singlenode/app.py +173 -0
- vantage_cli/apps/templates.py +178 -0
- vantage_cli/auth.py +429 -0
- vantage_cli/cache.py +143 -0
- vantage_cli/client.py +84 -0
- vantage_cli/command_base.py +63 -0
- vantage_cli/commands/__init__.py +1 -0
- vantage_cli/commands/clouds/__init__.py +20 -0
- vantage_cli/commands/clouds/add.py +81 -0
- vantage_cli/commands/clouds/delete.py +61 -0
- vantage_cli/commands/clouds/render.py +146 -0
- vantage_cli/commands/clouds/update.py +97 -0
- vantage_cli/commands/clusters/__init__.py +27 -0
- vantage_cli/commands/clusters/create.py +270 -0
- vantage_cli/commands/clusters/delete.py +101 -0
- vantage_cli/commands/clusters/get.py +30 -0
- vantage_cli/commands/clusters/list.py +84 -0
- vantage_cli/commands/clusters/render.py +233 -0
- vantage_cli/commands/clusters/schema.py +31 -0
- vantage_cli/commands/clusters/utils.py +248 -0
- vantage_cli/commands/profile/__init__.py +30 -0
- vantage_cli/commands/profile/crud.py +529 -0
- vantage_cli/commands/profile/render.py +55 -0
- vantage_cli/config.py +161 -0
- vantage_cli/constants.py +40 -0
- vantage_cli/exceptions.py +127 -0
- vantage_cli/format.py +39 -0
- vantage_cli/gql_client.py +655 -0
- vantage_cli/main.py +303 -0
- vantage_cli/render.py +56 -0
- vantage_cli/schemas.py +48 -0
- vantage_cli/time_loop.py +124 -0
- vantage_cli-0.1.1.dist-info/METADATA +30 -0
- vantage_cli-0.1.1.dist-info/RECORD +46 -0
- vantage_cli-0.1.1.dist-info/WHEEL +4 -0
- 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)
|
vantage_cli/constants.py
ADDED
|
@@ -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()
|