nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +8 -13
- nextmv/__init__.py +53 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +54 -9
- nextmv/cli/CONTRIBUTING.md +511 -0
- nextmv/cli/__init__.py +0 -0
- nextmv/cli/cloud/__init__.py +47 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +393 -0
- nextmv/cli/cloud/acceptance/delete.py +68 -0
- nextmv/cli/cloud/acceptance/get.py +104 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +60 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +141 -0
- nextmv/cli/cloud/app/delete.py +58 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +137 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +454 -0
- nextmv/cli/cloud/batch/delete.py +68 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +31 -0
- nextmv/cli/cloud/ensemble/create.py +414 -0
- nextmv/cli/cloud/ensemble/delete.py +67 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +30 -0
- nextmv/cli/cloud/input_set/create.py +170 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +290 -0
- nextmv/cli/cloud/instance/delete.py +62 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +146 -0
- nextmv/cli/cloud/managed_input/delete.py +65 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +530 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +167 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +501 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +65 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +67 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +147 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +68 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +43 -0
- nextmv/cli/cloud/shadow/update.py +95 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +97 -0
- nextmv/cli/cloud/version/delete.py +62 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +270 -0
- nextmv/cli/community/list.py +265 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +195 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/main.py +188 -0
- nextmv/cli/message.py +153 -0
- nextmv/cli/options.py +206 -0
- nextmv/cli/version.py +38 -0
- nextmv/cloud/__init__.py +71 -17
- nextmv/cloud/acceptance_test.py +757 -51
- nextmv/cloud/account.py +406 -17
- nextmv/cloud/application/__init__.py +957 -0
- nextmv/cloud/application/_acceptance.py +419 -0
- nextmv/cloud/application/_batch_scenario.py +860 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +227 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +314 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +303 -0
- nextmv/cloud/assets.py +48 -0
- nextmv/cloud/batch_experiment.py +294 -33
- nextmv/cloud/client.py +307 -66
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +120 -2
- nextmv/cloud/instance.py +133 -8
- nextmv/cloud/integration.py +533 -0
- nextmv/cloud/package.py +168 -53
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/shadow.py +190 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +132 -4
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/README.md +32 -0
- nextmv/default_app/app.yaml +12 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/requirements.txt +2 -0
- nextmv/default_app/src/__init__.py +0 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +861 -90
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1251 -0
- nextmv/local/executor.py +1042 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/logger.py +80 -9
- nextmv/manifest.py +1466 -0
- nextmv/model.py +241 -66
- nextmv/options.py +708 -115
- nextmv/output.py +1301 -274
- nextmv/polling.py +325 -0
- nextmv/run.py +1702 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
- nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
- nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -1405
- nextmv/cloud/manifest.py +0 -234
- nextmv/cloud/status.py +0 -29
- nextmv-0.18.0.dist-info/METADATA +0 -770
- nextmv-0.18.0.dist-info/RECORD +0 -25
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains configuration utilities for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from nextmv.cli.message import error
|
|
11
|
+
from nextmv.cloud.account import Account
|
|
12
|
+
from nextmv.cloud.application import Application
|
|
13
|
+
from nextmv.cloud.client import Client
|
|
14
|
+
|
|
15
|
+
# Some useful constants.
|
|
16
|
+
CONFIG_DIR = Path.home() / ".nextmv"
|
|
17
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
18
|
+
API_KEY_KEY = "apikey"
|
|
19
|
+
ENDPOINT_KEY = "endpoint"
|
|
20
|
+
DEFAULT_ENDPOINT = "api.cloud.nextmv.io"
|
|
21
|
+
GO_CLI_PATH = CONFIG_DIR / "nextmv"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_config() -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Load the current configuration from the config file. Returns an empty
|
|
27
|
+
dictionary if no configuration file exists.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
dict[str, Any]
|
|
32
|
+
The current configuration as a dictionary.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
if not CONFIG_FILE.exists():
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
with CONFIG_FILE.open() as file:
|
|
39
|
+
config = yaml.safe_load(file)
|
|
40
|
+
|
|
41
|
+
if config is None:
|
|
42
|
+
return {}
|
|
43
|
+
return config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Save the given configuration to the config file.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
config : dict[str, Any]
|
|
53
|
+
The configuration to save.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
with CONFIG_FILE.open("w") as file:
|
|
59
|
+
yaml.safe_dump(config, file)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_client(profile: str | None = None) -> Client:
|
|
63
|
+
"""
|
|
64
|
+
Builds a `cloud.Client` using the API key and endpoint for the given
|
|
65
|
+
profile. If no profile is given, the default profile is used. If either the
|
|
66
|
+
API key or endpoint is missing, an exception is raised. If the config is
|
|
67
|
+
not available, an exception is raised.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
profile : str | None
|
|
72
|
+
The profile name to use. If None, the default profile is used.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
Client
|
|
77
|
+
A client configured with the API key and endpoint for the selected
|
|
78
|
+
profile or the default configuration.
|
|
79
|
+
|
|
80
|
+
Raises
|
|
81
|
+
------
|
|
82
|
+
typer.Exit
|
|
83
|
+
If no configuration is found, if the requested profile does not exist,
|
|
84
|
+
or if the API key or endpoint (for either the selected profile or the
|
|
85
|
+
default configuration) is not set or is empty.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
config = load_config()
|
|
89
|
+
if config == {}:
|
|
90
|
+
error("No configuration found. Please run [code]nextmv configuration create[/code].")
|
|
91
|
+
|
|
92
|
+
if profile is not None:
|
|
93
|
+
if profile not in config:
|
|
94
|
+
error(f"Profile [magenta]{profile}[/magenta] does not exist.")
|
|
95
|
+
|
|
96
|
+
api_key = config[profile].get(API_KEY_KEY)
|
|
97
|
+
if api_key is None or api_key == "":
|
|
98
|
+
error(f"API key for profile [magenta]{profile}[/magenta] is not set or is empty.")
|
|
99
|
+
|
|
100
|
+
endpoint = config[profile].get(ENDPOINT_KEY)
|
|
101
|
+
if endpoint is None or endpoint == "":
|
|
102
|
+
error(f"Endpoint for profile [magenta]{profile}[/magenta] is not set or is empty.")
|
|
103
|
+
else:
|
|
104
|
+
api_key = config.get(API_KEY_KEY)
|
|
105
|
+
if api_key is None or api_key == "":
|
|
106
|
+
error("Default API key is not set or is empty.")
|
|
107
|
+
|
|
108
|
+
endpoint = config.get(ENDPOINT_KEY)
|
|
109
|
+
if endpoint is None or endpoint == "":
|
|
110
|
+
error("Default endpoint is not set or is empty.")
|
|
111
|
+
|
|
112
|
+
return Client(api_key=api_key, url=f"https://{endpoint}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_app(app_id: str, profile: str | None = None) -> Application:
|
|
116
|
+
"""
|
|
117
|
+
Builds a `cloud.Application` using the given application ID and the API
|
|
118
|
+
key and endpoint for the given profile. If no profile is given, the default
|
|
119
|
+
profile is used. If the application does not exist, an exception is raised.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
app_id : str
|
|
124
|
+
The application ID.
|
|
125
|
+
profile : str | None
|
|
126
|
+
The profile name to use. If None, the default profile is used.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Application
|
|
131
|
+
An application object for the given application ID.
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
typer.Exit
|
|
136
|
+
If the application does not exist.
|
|
137
|
+
"""
|
|
138
|
+
client = build_client(profile)
|
|
139
|
+
exists = Application.exists(client=client, id=app_id)
|
|
140
|
+
if not exists:
|
|
141
|
+
error(
|
|
142
|
+
f"Application with ID [magenta]{app_id}[/magenta] does not exist. "
|
|
143
|
+
"Use [code]nextmv cloud app create[/code] to create a new application."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return Application(client=client, id=app_id)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_account(account_id: str | None = None, profile: str | None = None) -> Account:
|
|
150
|
+
"""
|
|
151
|
+
Builds a `cloud.Account` using the API key and endpoint for the given
|
|
152
|
+
profile. If no profile is given, the default profile is used.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
account_id : str | None
|
|
157
|
+
The account ID. If None, no account ID is set.
|
|
158
|
+
profile : str | None
|
|
159
|
+
The profile name to use. If None, the default profile is used.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
Account
|
|
164
|
+
An account object for the configured profile.
|
|
165
|
+
|
|
166
|
+
Raises
|
|
167
|
+
------
|
|
168
|
+
typer.Exit
|
|
169
|
+
If the configuration is invalid or missing.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
client = build_client(profile)
|
|
173
|
+
|
|
174
|
+
return Account(account_id=account_id, client=client)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def obscure_api_key(api_key: str) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Obscure an API key for display purposes.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
api_key : str
|
|
184
|
+
The API key to obscure.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
str
|
|
189
|
+
The obscured API key.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
if len(api_key) <= 4:
|
|
193
|
+
return "*" * len(api_key)
|
|
194
|
+
|
|
195
|
+
return api_key[:2] + "*" * 4 + api_key[-2:]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the configuration create command for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from nextmv.cli.configuration.config import (
|
|
10
|
+
API_KEY_KEY,
|
|
11
|
+
DEFAULT_ENDPOINT,
|
|
12
|
+
ENDPOINT_KEY,
|
|
13
|
+
load_config,
|
|
14
|
+
obscure_api_key,
|
|
15
|
+
save_config,
|
|
16
|
+
)
|
|
17
|
+
from nextmv.cli.message import error, info, success
|
|
18
|
+
|
|
19
|
+
# Set up subcommand application.
|
|
20
|
+
app = typer.Typer()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def create(
|
|
25
|
+
api_key: Annotated[
|
|
26
|
+
str,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--api-key",
|
|
29
|
+
"-a",
|
|
30
|
+
help="A valid Nextmv Cloud API key. "
|
|
31
|
+
+ "Get one from [link=https://cloud.nextmv.io][bold]https://cloud.nextmv.io[/bold][/link].",
|
|
32
|
+
envvar="NEXTMV_API_KEY",
|
|
33
|
+
metavar="NEXTMV_API_KEY",
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
endpoint: Annotated[ # Hidden because it is meant for internal use.
|
|
37
|
+
str | None,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--endpoint",
|
|
40
|
+
"-e",
|
|
41
|
+
hidden=True,
|
|
42
|
+
),
|
|
43
|
+
] = DEFAULT_ENDPOINT,
|
|
44
|
+
profile: Annotated[ # Similar to nextmv.cli.options.ProfileOption but with different help text.
|
|
45
|
+
str | None,
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--profile",
|
|
48
|
+
"-p",
|
|
49
|
+
help="Profile name to save the configuration under.",
|
|
50
|
+
envvar="NEXTMV_PROFILE",
|
|
51
|
+
metavar="PROFILE_NAME",
|
|
52
|
+
),
|
|
53
|
+
] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Create a new configuration or update an existing one.
|
|
57
|
+
|
|
58
|
+
[bold][underline]Examples[/underline][/bold]
|
|
59
|
+
|
|
60
|
+
- Default configuration.
|
|
61
|
+
$ [green]nextmv configuration create --api-key NEXTMV_API_KEY[/green]
|
|
62
|
+
|
|
63
|
+
- Configure a profile named [magenta]hare[/magenta].
|
|
64
|
+
$ [green]nextmv configuration create --api-key NEXTMV_API_KEY --profile hare[/green]
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if profile is not None and profile.strip().lower() == "default":
|
|
68
|
+
error("[code]default[/code] is a reserved profile name.")
|
|
69
|
+
|
|
70
|
+
endpoint = str(endpoint)
|
|
71
|
+
if endpoint.startswith("https://"):
|
|
72
|
+
endpoint = endpoint[len("https://") :]
|
|
73
|
+
elif endpoint.startswith("http://"):
|
|
74
|
+
endpoint = endpoint[len("http://") :]
|
|
75
|
+
|
|
76
|
+
config = load_config()
|
|
77
|
+
|
|
78
|
+
if profile is None:
|
|
79
|
+
config[API_KEY_KEY] = api_key
|
|
80
|
+
config[ENDPOINT_KEY] = endpoint
|
|
81
|
+
else:
|
|
82
|
+
if profile not in config:
|
|
83
|
+
config[profile] = {}
|
|
84
|
+
|
|
85
|
+
config[profile][API_KEY_KEY] = api_key
|
|
86
|
+
config[profile][ENDPOINT_KEY] = endpoint
|
|
87
|
+
|
|
88
|
+
save_config(config)
|
|
89
|
+
|
|
90
|
+
success("Configuration saved successfully.")
|
|
91
|
+
info(f"\t[bold]Profile[/bold]: {profile or 'Default'}")
|
|
92
|
+
info(f"\t[bold]API Key[/bold]: {obscure_api_key(api_key)}")
|
|
93
|
+
if endpoint != DEFAULT_ENDPOINT:
|
|
94
|
+
info(f"\t[bold]Endpoint[/bold]: {endpoint}")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the configuration delete command for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.prompt import Confirm
|
|
9
|
+
|
|
10
|
+
from nextmv.cli.configuration.config import load_config, save_config
|
|
11
|
+
from nextmv.cli.message import error, info, success
|
|
12
|
+
|
|
13
|
+
# Set up subcommand application.
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def delete(
|
|
19
|
+
profile: Annotated[ # Similar to nextmv.cli.options.ProfileOption but with different help text.
|
|
20
|
+
str,
|
|
21
|
+
typer.Option(
|
|
22
|
+
"--profile",
|
|
23
|
+
"-p",
|
|
24
|
+
help="Profile name to delete.",
|
|
25
|
+
envvar="NEXTMV_PROFILE",
|
|
26
|
+
metavar="PROFILE_NAME",
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
yes: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--yes",
|
|
33
|
+
"-y",
|
|
34
|
+
help="Agree to deletion confirmation prompt. Useful for non-interactive sessions.",
|
|
35
|
+
),
|
|
36
|
+
] = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Delete a profile from the configuration. Use the [code]--yes[/code]
|
|
40
|
+
flag to skip the confirmation prompt.
|
|
41
|
+
|
|
42
|
+
[bold][underline]Examples[/underline][/bold]
|
|
43
|
+
|
|
44
|
+
- Delete a profile named [magenta]hare[/magenta].
|
|
45
|
+
$ [green]nextmv configuration delete --profile hare[/green]
|
|
46
|
+
|
|
47
|
+
- Delete a profile named [magenta]hare[/magenta] without confirmation prompt.
|
|
48
|
+
$ [green]nextmv configuration delete --profile hare --yes[/green]
|
|
49
|
+
"""
|
|
50
|
+
config = load_config()
|
|
51
|
+
if profile not in config:
|
|
52
|
+
error(f"Profile [magenta]{profile}[/magenta] does not exist.")
|
|
53
|
+
|
|
54
|
+
if not yes:
|
|
55
|
+
confirm = Confirm.ask(
|
|
56
|
+
f"Are you sure you want to delete profile [magenta]{profile}[/magenta]? This action cannot be undone.",
|
|
57
|
+
default=False,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not confirm:
|
|
61
|
+
info(msg=f"Profile [magenta]{profile}[/magenta] will not be deleted.", emoji=":bulb:")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
del config[profile]
|
|
65
|
+
save_config(config)
|
|
66
|
+
|
|
67
|
+
success(f"Profile [magenta]{profile}[/magenta] deleted successfully.")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the configuration list command for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from nextmv.cli.configuration.config import API_KEY_KEY, ENDPOINT_KEY, load_config, obscure_api_key
|
|
10
|
+
from nextmv.cli.message import error
|
|
11
|
+
|
|
12
|
+
# Set up subcommand application.
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def list() -> None:
|
|
19
|
+
"""
|
|
20
|
+
List the current configuration and all profiles.
|
|
21
|
+
|
|
22
|
+
[bold][underline]Examples[/underline][/bold]
|
|
23
|
+
|
|
24
|
+
- Show current configuration and all profiles.
|
|
25
|
+
$ [green]nextmv configuration list[/green]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
config = load_config()
|
|
29
|
+
if config == {}:
|
|
30
|
+
error("No configuration found. Please run [code]nextmv configuration[/code].")
|
|
31
|
+
|
|
32
|
+
default = {
|
|
33
|
+
"api_key": config.get(API_KEY_KEY),
|
|
34
|
+
"endpoint": config.get(ENDPOINT_KEY),
|
|
35
|
+
"name": "Default",
|
|
36
|
+
}
|
|
37
|
+
profiles = [default]
|
|
38
|
+
|
|
39
|
+
for k, v in config.items():
|
|
40
|
+
# Skip default configuration.
|
|
41
|
+
if k in {API_KEY_KEY, ENDPOINT_KEY}:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
profile = {
|
|
45
|
+
"name": k,
|
|
46
|
+
"api_key": v.get(API_KEY_KEY),
|
|
47
|
+
"endpoint": v.get(ENDPOINT_KEY),
|
|
48
|
+
}
|
|
49
|
+
profiles.append(profile)
|
|
50
|
+
|
|
51
|
+
table = Table("Profile name", "API Key", "Endpoint")
|
|
52
|
+
not_set = "[italic]Not set[/italic]"
|
|
53
|
+
for profile in profiles:
|
|
54
|
+
if profile["name"] != "Default":
|
|
55
|
+
table.add_row(
|
|
56
|
+
profile["name"],
|
|
57
|
+
obscure_api_key(profile["api_key"]) if profile.get("api_key") is not None else not_set,
|
|
58
|
+
profile["endpoint"] if profile.get("endpoint") is not None else not_set,
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
api_key = not_set
|
|
63
|
+
if profile.get("api_key") is not None:
|
|
64
|
+
api_key = obscure_api_key(profile["api_key"])
|
|
65
|
+
|
|
66
|
+
endpoint = not_set
|
|
67
|
+
if profile.get("endpoint") is not None:
|
|
68
|
+
endpoint = profile["endpoint"]
|
|
69
|
+
|
|
70
|
+
table.add_row(
|
|
71
|
+
f"[bold yellow]{profile['name']}[/bold yellow]",
|
|
72
|
+
f"[bold yellow]{api_key}[/bold yellow]",
|
|
73
|
+
f"[bold yellow]{endpoint}[/bold yellow]",
|
|
74
|
+
)
|
|
75
|
+
table.add_section()
|
|
76
|
+
|
|
77
|
+
console.print(table)
|
nextmv/cli/main.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Nextmv Command Line Interface (CLI).
|
|
3
|
+
|
|
4
|
+
This module is the main entry point for the Nextmv CLI application. The Nextmv
|
|
5
|
+
CLI is built with [Typer](https://typer.tiangolo.com/) and provides various
|
|
6
|
+
commands to interact with Nextmv services. You should visit the "Learn" section
|
|
7
|
+
of the Typer documentation to learn about the features that are used here.
|
|
8
|
+
|
|
9
|
+
The Nextmv CLI also uses [Rich](https://rich.readthedocs.io/en/stable/) for
|
|
10
|
+
rich text and formatting in the terminal. The command documentation is created
|
|
11
|
+
using Rich markup. You should also visit the Rich documentation to learn more
|
|
12
|
+
about the features used here. An example of Rich markup can be found in the
|
|
13
|
+
epilog of the Typer application defined below.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Annotated
|
|
19
|
+
|
|
20
|
+
import rich
|
|
21
|
+
import typer
|
|
22
|
+
from rich.prompt import Confirm
|
|
23
|
+
|
|
24
|
+
from nextmv.cli.cloud import app as cloud_app
|
|
25
|
+
from nextmv.cli.community import app as community_app
|
|
26
|
+
from nextmv.cli.configuration import app as configuration_app
|
|
27
|
+
from nextmv.cli.configuration.config import CONFIG_DIR, GO_CLI_PATH, load_config
|
|
28
|
+
from nextmv.cli.message import error, info, success, warning
|
|
29
|
+
from nextmv.cli.version import app as version_app
|
|
30
|
+
from nextmv.cli.version import version_callback
|
|
31
|
+
|
|
32
|
+
# Main CLI application.
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
help="The Nextmv Command Line Interface (CLI).",
|
|
35
|
+
epilog="[dim]\n---\n\n[italic]:rabbit: Made by Nextmv with :heart:[/italic][/dim]",
|
|
36
|
+
rich_markup_mode="rich",
|
|
37
|
+
context_settings={"help_option_names": ["--help", "-h"]},
|
|
38
|
+
no_args_is_help=True,
|
|
39
|
+
invoke_without_command=True,
|
|
40
|
+
pretty_exceptions_show_locals=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Register subcommands. The `name` parameter is required when the subcommand
|
|
44
|
+
# module has a callback function defined.
|
|
45
|
+
app.add_typer(cloud_app, name="cloud")
|
|
46
|
+
app.add_typer(community_app, name="community")
|
|
47
|
+
app.add_typer(configuration_app, name="configuration")
|
|
48
|
+
app.add_typer(version_app)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.callback()
|
|
52
|
+
def callback(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
version: Annotated[
|
|
55
|
+
bool | None,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--version",
|
|
58
|
+
"-v",
|
|
59
|
+
help="Show the current version of the Nextmv CLI.",
|
|
60
|
+
callback=version_callback,
|
|
61
|
+
),
|
|
62
|
+
] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Callback function that runs before any command. Useful for checks on the
|
|
66
|
+
environment.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
handle_go_cli()
|
|
70
|
+
handle_config_existence(ctx)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def handle_go_cli() -> None:
|
|
74
|
+
"""
|
|
75
|
+
Handle the presence of the deprecated Go CLI by notifying the user.
|
|
76
|
+
|
|
77
|
+
This function checks if the Go CLI is installed and prompts the user to
|
|
78
|
+
remove it to avoid conflicts with the Python CLI.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
exists = go_cli_exists()
|
|
82
|
+
if exists:
|
|
83
|
+
delete = Confirm.ask(
|
|
84
|
+
"Do you want to delete the [italic red]deprecated[/italic red] Nextmv CLI "
|
|
85
|
+
f"at [magenta]{GO_CLI_PATH}[/magenta] now?",
|
|
86
|
+
default=False,
|
|
87
|
+
)
|
|
88
|
+
if delete:
|
|
89
|
+
remove_go_cli()
|
|
90
|
+
else:
|
|
91
|
+
info(
|
|
92
|
+
msg="You can delete the [italic red]deprecated[/italic red] Nextmv CLI later by removing "
|
|
93
|
+
f"[magenta]{GO_CLI_PATH}[/magenta]. Make sure you also clean up your [code]PATH[/code].",
|
|
94
|
+
emoji=":bulb:",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def handle_config_existence(ctx: typer.Context) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Check if configuration exists and show an error if it does not.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
ctx : typer.Context
|
|
105
|
+
The Typer context object.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
ignored_commands = {"configuration", "version"}
|
|
109
|
+
if ctx.invoked_subcommand in ignored_commands:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
config = load_config()
|
|
113
|
+
if config == {}:
|
|
114
|
+
error("No configuration found. Please run [code]nextmv configuration create[/code].")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def go_cli_exists() -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Check if the Go CLI is installed by looking for the 'nextmv' executable
|
|
120
|
+
under the config dir.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
bool
|
|
125
|
+
True if the Go CLI is installed, False otherwise.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# Check if the Go CLI executable exists
|
|
129
|
+
exists = GO_CLI_PATH.exists()
|
|
130
|
+
if exists:
|
|
131
|
+
warning(
|
|
132
|
+
"A [italic red]deprecated[/italic red] Nextmv CLI is installed at "
|
|
133
|
+
f"[magenta]{GO_CLI_PATH}[/magenta]. You must delete it to avoid conflicts."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
check_config_in_path()
|
|
137
|
+
|
|
138
|
+
return exists
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def remove_go_cli() -> None:
|
|
142
|
+
"""
|
|
143
|
+
Remove the Go CLI executable if it exists and notify about PATH cleanup.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
if GO_CLI_PATH.exists():
|
|
147
|
+
GO_CLI_PATH.unlink()
|
|
148
|
+
success(f"Deleted deprecated [magenta]{GO_CLI_PATH}[/magenta].")
|
|
149
|
+
|
|
150
|
+
check_config_in_path()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_config_in_path() -> None:
|
|
154
|
+
"""
|
|
155
|
+
Check if the configuration directory is in the PATH and notify the user.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
|
|
159
|
+
config_dir_str = str(CONFIG_DIR)
|
|
160
|
+
|
|
161
|
+
if config_dir_str in path_dirs:
|
|
162
|
+
warning(
|
|
163
|
+
f"[magenta]{CONFIG_DIR}[/magenta] was found in your [code]PATH[/code]. "
|
|
164
|
+
f"You should remove any entries related to [magenta]{CONFIG_DIR}[/magenta] from your [code]PATH[/code]."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main() -> None:
|
|
169
|
+
"""
|
|
170
|
+
Entry point for the CLI with global exception handling.
|
|
171
|
+
|
|
172
|
+
Catches all exceptions except Typer/Click exceptions (which handle their
|
|
173
|
+
own exit codes) and displays a clean error message instead of a traceback.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
app()
|
|
178
|
+
except (typer.Exit, typer.Abort, SystemExit):
|
|
179
|
+
raise
|
|
180
|
+
except Exception as e:
|
|
181
|
+
# We do not use the messages.error function here because doing so would
|
|
182
|
+
# raise a Typer exception, which would print a traceback.
|
|
183
|
+
msg = str(e).rstrip("\n")
|
|
184
|
+
if not msg.endswith("."):
|
|
185
|
+
msg += "."
|
|
186
|
+
|
|
187
|
+
rich.print(f"[red]Error:[/red] {msg}", file=sys.stderr)
|
|
188
|
+
sys.exit(1)
|