nextmv 1.0.0__py3-none-any.whl → 1.0.0.dev0__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 +2 -1
- nextmv/__init__.py +4 -0
- nextmv/cli/CONTRIBUTING.md +40 -112
- nextmv/cli/cloud/__init__.py +0 -4
- nextmv/cli/cloud/acceptance/create.py +22 -20
- nextmv/cli/cloud/acceptance/delete.py +12 -8
- nextmv/cli/cloud/acceptance/get.py +10 -9
- nextmv/cli/cloud/acceptance/list.py +3 -3
- nextmv/cli/cloud/acceptance/update.py +6 -6
- nextmv/cli/cloud/account/__init__.py +3 -3
- nextmv/cli/cloud/account/create.py +11 -11
- nextmv/cli/cloud/account/delete.py +8 -7
- nextmv/cli/cloud/account/get.py +3 -3
- nextmv/cli/cloud/account/update.py +5 -5
- nextmv/cli/cloud/app/create.py +26 -25
- nextmv/cli/cloud/app/delete.py +7 -6
- nextmv/cli/cloud/app/exists.py +2 -2
- nextmv/cli/cloud/app/get.py +2 -2
- nextmv/cli/cloud/app/list.py +3 -3
- nextmv/cli/cloud/app/push.py +54 -349
- nextmv/cli/cloud/app/update.py +12 -12
- nextmv/cli/cloud/batch/create.py +28 -26
- nextmv/cli/cloud/batch/delete.py +10 -6
- nextmv/cli/cloud/batch/get.py +9 -9
- nextmv/cli/cloud/batch/list.py +3 -3
- nextmv/cli/cloud/batch/metadata.py +4 -4
- nextmv/cli/cloud/batch/update.py +6 -6
- nextmv/cli/cloud/data/__init__.py +1 -1
- nextmv/cli/cloud/data/upload.py +15 -15
- nextmv/cli/cloud/ensemble/__init__.py +0 -2
- nextmv/cli/cloud/ensemble/create.py +22 -21
- nextmv/cli/cloud/ensemble/delete.py +10 -6
- nextmv/cli/cloud/ensemble/get.py +4 -4
- nextmv/cli/cloud/ensemble/update.py +9 -9
- nextmv/cli/cloud/input_set/__init__.py +0 -2
- nextmv/cli/cloud/input_set/create.py +22 -22
- nextmv/cli/cloud/input_set/get.py +3 -3
- nextmv/cli/cloud/input_set/list.py +3 -3
- nextmv/cli/cloud/input_set/update.py +24 -24
- nextmv/cli/cloud/instance/create.py +15 -14
- nextmv/cli/cloud/instance/delete.py +7 -6
- nextmv/cli/cloud/instance/exists.py +2 -2
- nextmv/cli/cloud/instance/get.py +2 -2
- nextmv/cli/cloud/instance/list.py +3 -3
- nextmv/cli/cloud/instance/update.py +14 -14
- nextmv/cli/cloud/managed_input/create.py +16 -14
- nextmv/cli/cloud/managed_input/delete.py +8 -7
- nextmv/cli/cloud/managed_input/get.py +3 -3
- nextmv/cli/cloud/managed_input/list.py +3 -3
- nextmv/cli/cloud/managed_input/update.py +9 -9
- nextmv/cli/cloud/run/cancel.py +2 -2
- nextmv/cli/cloud/run/create.py +40 -34
- nextmv/cli/cloud/run/get.py +8 -8
- nextmv/cli/cloud/run/input.py +4 -4
- nextmv/cli/cloud/run/list.py +6 -6
- nextmv/cli/cloud/run/logs.py +10 -9
- nextmv/cli/cloud/run/metadata.py +4 -4
- nextmv/cli/cloud/run/track.py +33 -32
- nextmv/cli/cloud/scenario/create.py +21 -21
- nextmv/cli/cloud/scenario/delete.py +10 -6
- nextmv/cli/cloud/scenario/get.py +9 -9
- nextmv/cli/cloud/scenario/list.py +3 -3
- nextmv/cli/cloud/scenario/metadata.py +4 -4
- nextmv/cli/cloud/scenario/update.py +6 -6
- nextmv/cli/cloud/secrets/create.py +17 -17
- nextmv/cli/cloud/secrets/delete.py +10 -6
- nextmv/cli/cloud/secrets/get.py +4 -4
- nextmv/cli/cloud/secrets/list.py +3 -3
- nextmv/cli/cloud/secrets/update.py +20 -17
- nextmv/cli/cloud/upload/create.py +2 -2
- nextmv/cli/cloud/version/create.py +10 -9
- nextmv/cli/cloud/version/delete.py +7 -6
- nextmv/cli/cloud/version/exists.py +2 -2
- nextmv/cli/cloud/version/get.py +2 -2
- nextmv/cli/cloud/version/list.py +3 -3
- nextmv/cli/cloud/version/update.py +8 -8
- nextmv/cli/community/__init__.py +1 -1
- nextmv/cli/community/clone.py +204 -20
- nextmv/cli/community/list.py +125 -60
- nextmv/cli/configuration/config.py +10 -43
- nextmv/cli/configuration/create.py +7 -7
- nextmv/cli/configuration/delete.py +8 -8
- nextmv/cli/configuration/list.py +3 -3
- nextmv/cli/main.py +36 -26
- nextmv/cli/message.py +54 -71
- nextmv/cli/options.py +0 -28
- nextmv/cli/version.py +1 -1
- nextmv/cloud/__init__.py +38 -14
- nextmv/cloud/acceptance_test.py +65 -1
- nextmv/cloud/account.py +6 -1
- nextmv/cloud/application/__init__.py +75 -18
- nextmv/cloud/application/_acceptance.py +8 -13
- nextmv/cloud/application/_batch_scenario.py +19 -4
- nextmv/cloud/application/_input_set.py +6 -42
- nextmv/cloud/application/_instance.py +3 -3
- nextmv/cloud/application/_managed_input.py +2 -2
- nextmv/cloud/application/_version.py +3 -4
- nextmv/cloud/batch_experiment.py +1 -3
- nextmv/cloud/integration.py +4 -7
- nextmv/deprecated.py +3 -5
- nextmv/input.py +52 -0
- nextmv/local/runner.py +1 -1
- nextmv/model.py +11 -50
- nextmv/options.py +256 -11
- nextmv/output.py +62 -0
- nextmv/run.py +10 -1
- nextmv/status.py +51 -1
- {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/METADATA +4 -5
- nextmv-1.0.0.dev0.dist-info/RECORD +158 -0
- nextmv/cli/cloud/ensemble/list.py +0 -63
- nextmv/cli/cloud/input_set/delete.py +0 -64
- nextmv/cli/cloud/shadow/__init__.py +0 -33
- nextmv/cli/cloud/shadow/create.py +0 -184
- nextmv/cli/cloud/shadow/delete.py +0 -64
- nextmv/cli/cloud/shadow/get.py +0 -61
- nextmv/cli/cloud/shadow/list.py +0 -63
- nextmv/cli/cloud/shadow/metadata.py +0 -66
- nextmv/cli/cloud/shadow/start.py +0 -43
- nextmv/cli/cloud/shadow/stop.py +0 -53
- nextmv/cli/cloud/shadow/update.py +0 -96
- nextmv/cli/cloud/switchback/__init__.py +0 -33
- nextmv/cli/cloud/switchback/create.py +0 -151
- nextmv/cli/cloud/switchback/delete.py +0 -64
- nextmv/cli/cloud/switchback/get.py +0 -62
- nextmv/cli/cloud/switchback/list.py +0 -63
- nextmv/cli/cloud/switchback/metadata.py +0 -68
- nextmv/cli/cloud/switchback/start.py +0 -43
- nextmv/cli/cloud/switchback/stop.py +0 -53
- nextmv/cli/cloud/switchback/update.py +0 -96
- nextmv/cli/confirm.py +0 -34
- nextmv/cloud/application/_shadow.py +0 -320
- nextmv/cloud/application/_switchback.py +0 -332
- nextmv/cloud/community.py +0 -446
- nextmv/cloud/shadow.py +0 -254
- nextmv/cloud/switchback.py +0 -228
- nextmv-1.0.0.dist-info/RECORD +0 -185
- nextmv-1.0.0.dist-info/entry_points.txt +0 -2
- {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/WHEEL +0 -0
- {nextmv-1.0.0.dist-info → nextmv-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
nextmv/cli/community/clone.py
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
This module defines the community clone command for the Nextmv CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Callable
|
|
5
10
|
from typing import Annotated
|
|
6
11
|
|
|
12
|
+
import rich
|
|
7
13
|
import typer
|
|
8
14
|
|
|
9
|
-
from nextmv.cli.
|
|
10
|
-
from nextmv.cli.message import error
|
|
15
|
+
from nextmv.cli.community.list import download_file, download_manifest, find_app, versions_table
|
|
16
|
+
from nextmv.cli.message import error, success
|
|
11
17
|
from nextmv.cli.options import ProfileOption
|
|
12
|
-
from nextmv.cloud.community import clone_community_app
|
|
13
18
|
|
|
14
19
|
# Set up subcommand application.
|
|
15
20
|
app = typer.Typer()
|
|
@@ -48,39 +53,218 @@ def clone(
|
|
|
48
53
|
Clone a community app locally.
|
|
49
54
|
|
|
50
55
|
By default, the [magenta]latest[/magenta] version will be used. You can
|
|
51
|
-
specify a version with the --version flag, and customize the
|
|
52
|
-
directory with the --directory flag. If you want to
|
|
53
|
-
apps, use the [code]nextmv community list[/code] command.
|
|
56
|
+
specify a version with the [code]--version[/code] flag, and customize the
|
|
57
|
+
output directory with the [code]--directory[/code] flag. If you want to
|
|
58
|
+
list the available apps, use the [code]nextmv community list[/code] command.
|
|
54
59
|
|
|
55
60
|
[bold][underline]Examples[/underline][/bold]
|
|
56
61
|
|
|
57
62
|
- Clone the [magenta]go-nextroute[/magenta] community app (under the
|
|
58
63
|
[magenta]"go-nextroute"[/magenta] directory), using the [magenta]latest[/magenta] version.
|
|
59
|
-
$ [
|
|
64
|
+
$ [green]nextmv community clone --app go-nextroute[/green]
|
|
60
65
|
|
|
61
66
|
- Clone the [magenta]go-nextroute[/magenta] community app under the
|
|
62
67
|
[magenta]"~/sample/my_app"[/magenta] directory, using the [magenta]latest[/magenta] version.
|
|
63
|
-
$ [
|
|
68
|
+
$ [green]nextmv community clone --app go-nextroute --directory ~/sample/my_app[/green]
|
|
64
69
|
|
|
65
70
|
- Clone the [magenta]go-nextroute[/magenta] community app (under the
|
|
66
71
|
[magenta]"go-nextroute"[/magenta] directory), using version [magenta]v1.2.0[/magenta].
|
|
67
|
-
$ [
|
|
72
|
+
$ [green]nextmv community clone --app go-nextroute --version v1.2.0[/green]
|
|
68
73
|
|
|
69
74
|
- Clone the [magenta]go-nextroute[/magenta] community app (under the
|
|
70
75
|
[magenta]"go-nextroute"[/magenta] directory), using the [magenta]latest[/magenta] version
|
|
71
76
|
and a profile named [magenta]hare[/magenta].
|
|
72
|
-
$ [
|
|
77
|
+
$ [green]nextmv community clone --app go-nextroute --profile hare[/green]
|
|
73
78
|
"""
|
|
74
79
|
|
|
80
|
+
manifest = download_manifest(profile=profile)
|
|
81
|
+
app_obj = find_app(manifest, app)
|
|
82
|
+
|
|
75
83
|
if version is not None and version == "":
|
|
76
|
-
error("The --version flag cannot be an empty string.")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
error("The [code]--version[/code] flag cannot be an empty string.")
|
|
85
|
+
|
|
86
|
+
if not app_has_version(app_obj, version):
|
|
87
|
+
# We don't use error() here to allow printing something before exiting.
|
|
88
|
+
rich.print(
|
|
89
|
+
f"[red]Error:[/red] Version [magenta]{version}[/magenta] not found "
|
|
90
|
+
f"for community app [magenta]{app}[/magenta]. Available versions are:"
|
|
91
|
+
)
|
|
92
|
+
versions_table(manifest, app)
|
|
93
|
+
|
|
94
|
+
raise typer.Exit(code=1)
|
|
95
|
+
|
|
96
|
+
original_version = version
|
|
97
|
+
if version == LATEST_VERSION:
|
|
98
|
+
version = app_obj.get("latest_app_version")
|
|
99
|
+
|
|
100
|
+
destination = directory
|
|
101
|
+
if directory is None or directory == "":
|
|
102
|
+
destination = app
|
|
103
|
+
|
|
104
|
+
full_destination = get_valid_path(destination, os.stat)
|
|
105
|
+
os.makedirs(full_destination, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
tarball = f"{app}_{version}.tar.gz"
|
|
108
|
+
s3_file_path = f"{app}/{version}/{tarball}"
|
|
109
|
+
downloaded_object = download_object(
|
|
110
|
+
file=s3_file_path,
|
|
111
|
+
path="community-apps",
|
|
112
|
+
output_dir=full_destination,
|
|
113
|
+
output_file=tarball,
|
|
114
|
+
profile=profile,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Extract the tarball to a temporary directory to handle nested structure
|
|
118
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
119
|
+
with tarfile.open(downloaded_object, "r:gz") as tar:
|
|
120
|
+
tar.extractall(path=temp_dir)
|
|
121
|
+
|
|
122
|
+
# Find the extracted directory (typically the app name)
|
|
123
|
+
extracted_items = os.listdir(temp_dir)
|
|
124
|
+
if len(extracted_items) == 1 and os.path.isdir(os.path.join(temp_dir, extracted_items[0])):
|
|
125
|
+
# Move contents from the extracted directory to full_destination
|
|
126
|
+
extracted_dir = os.path.join(temp_dir, extracted_items[0])
|
|
127
|
+
for item in os.listdir(extracted_dir):
|
|
128
|
+
shutil.move(os.path.join(extracted_dir, item), full_destination)
|
|
129
|
+
else:
|
|
130
|
+
# If structure is unexpected, move everything directly
|
|
131
|
+
for item in extracted_items:
|
|
132
|
+
shutil.move(os.path.join(temp_dir, item), full_destination)
|
|
133
|
+
|
|
134
|
+
# Remove the tarball after extraction
|
|
135
|
+
os.remove(downloaded_object)
|
|
136
|
+
|
|
137
|
+
success(
|
|
138
|
+
f"Successfully cloned the [magenta]{app}[/magenta] community app, "
|
|
139
|
+
f"using version [magenta]{original_version}[/magenta] in path: [magenta]{full_destination}[/magenta]."
|
|
86
140
|
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def app_has_version(app_obj: dict, version: str) -> bool:
|
|
144
|
+
"""
|
|
145
|
+
Check if the given app object has the specified version.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
app_obj : dict
|
|
150
|
+
The community app object.
|
|
151
|
+
version : str
|
|
152
|
+
The version to check.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
bool
|
|
157
|
+
True if the app has the specified version, False otherwise.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
if version == LATEST_VERSION:
|
|
161
|
+
version = app_obj.get("latest_app_version")
|
|
162
|
+
|
|
163
|
+
if version in app_obj.get("app_versions", []):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_valid_path(path: str, stat_fn: Callable[[str], os.stat_result], ending: str = "") -> str:
|
|
170
|
+
"""
|
|
171
|
+
Validates and returns a non-existing path. If the path exists,
|
|
172
|
+
it will append a number to the path and return it. If the path does not
|
|
173
|
+
exist, it will return the path as is.
|
|
174
|
+
|
|
175
|
+
The ending parameter is used to check if the path ends with a specific
|
|
176
|
+
string. This is useful to specify if it is a file (like foo.json, in which
|
|
177
|
+
case the next iteration is foo-1.json) or a directory (like foo, in which
|
|
178
|
+
case the next iteration is foo-1).
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
path : str
|
|
183
|
+
The initial path to validate.
|
|
184
|
+
stat_fn : Callable[[str], os.stat_result]
|
|
185
|
+
A function that takes a path and returns its stat result.
|
|
186
|
+
ending : str, optional
|
|
187
|
+
The expected ending of the path (e.g., file extension), by default "".
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
str
|
|
192
|
+
A valid, non-existing path.
|
|
193
|
+
|
|
194
|
+
Raises
|
|
195
|
+
------
|
|
196
|
+
Exception
|
|
197
|
+
If an unexpected error occurs during path validation
|
|
198
|
+
"""
|
|
199
|
+
base_name = os.path.basename(path)
|
|
200
|
+
name_without_ending = base_name.removesuffix(ending) if ending else base_name
|
|
201
|
+
|
|
202
|
+
while True:
|
|
203
|
+
try:
|
|
204
|
+
stat_fn(path)
|
|
205
|
+
# If we get here, the path exists
|
|
206
|
+
# Get folder/file name number, increase it and create new path
|
|
207
|
+
name = os.path.basename(path)
|
|
208
|
+
|
|
209
|
+
# Get folder/file name number
|
|
210
|
+
parts = name.split("-")
|
|
211
|
+
last = parts[-1].removesuffix(ending) if ending else parts[-1]
|
|
212
|
+
|
|
213
|
+
# Save last folder name index to be changed
|
|
214
|
+
i = path.rfind(name)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
num = int(last)
|
|
218
|
+
# Increase number and create new path
|
|
219
|
+
if ending:
|
|
220
|
+
temp_path = path[:i] + f"{name_without_ending}-{num + 1}{ending}"
|
|
221
|
+
else:
|
|
222
|
+
temp_path = path[:i] + f"{base_name}-{num + 1}"
|
|
223
|
+
path = temp_path
|
|
224
|
+
except ValueError:
|
|
225
|
+
# If there is no number, add it
|
|
226
|
+
if ending:
|
|
227
|
+
temp_path = path[:i] + f"{name_without_ending}-1{ending}"
|
|
228
|
+
else:
|
|
229
|
+
temp_path = path[:i] + f"{name}-1"
|
|
230
|
+
path = temp_path
|
|
231
|
+
|
|
232
|
+
except FileNotFoundError:
|
|
233
|
+
# Path doesn't exist, we can use it
|
|
234
|
+
return path
|
|
235
|
+
except Exception:
|
|
236
|
+
# Re-raise unexpected errors
|
|
237
|
+
error(f"An unexpected error occurred while validating the path: {path}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def download_object(file: str, path: str, output_dir: str, output_file: str, profile: str | None = None) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Downloads an object from the internal bucket and saves it to the specified
|
|
243
|
+
output directory.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
file : str
|
|
248
|
+
The name of the file to download.
|
|
249
|
+
path : str
|
|
250
|
+
The directory in the bucket where the file is located.
|
|
251
|
+
output_dir : str
|
|
252
|
+
The local directory where the file will be saved.
|
|
253
|
+
output_file : str
|
|
254
|
+
The name of the output file.
|
|
255
|
+
profile : str | None
|
|
256
|
+
The profile name to use. If None, the default profile is used.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
str
|
|
261
|
+
The path to the downloaded file.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
response = download_file(directory=path, file=file, profile=profile)
|
|
265
|
+
file_name = os.path.join(output_dir, output_file)
|
|
266
|
+
|
|
267
|
+
with open(file_name, "wb") as f:
|
|
268
|
+
f.write(response.content)
|
|
269
|
+
|
|
270
|
+
return file_name
|
nextmv/cli/community/list.py
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
This module defines the community list command for the Nextmv CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated, Any
|
|
6
6
|
|
|
7
|
+
import requests
|
|
7
8
|
import rich
|
|
8
9
|
import typer
|
|
10
|
+
import yaml
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.table import Table
|
|
11
13
|
|
|
12
14
|
from nextmv.cli.configuration.config import build_client
|
|
13
15
|
from nextmv.cli.message import error
|
|
14
16
|
from nextmv.cli.options import ProfileOption
|
|
15
|
-
from nextmv.cloud.client import Client
|
|
16
|
-
from nextmv.cloud.community import CommunityApp, list_community_apps
|
|
17
17
|
|
|
18
18
|
# Set up subcommand application.
|
|
19
19
|
app = typer.Typer()
|
|
@@ -37,98 +37,121 @@ def list(
|
|
|
37
37
|
"""
|
|
38
38
|
List the available community apps
|
|
39
39
|
|
|
40
|
-
Use the --app flag to list that app's versions. Use the
|
|
41
|
-
flatten the list of names/versions. If you
|
|
42
|
-
locally, use the [code]nextmv community clone[/code] command.
|
|
40
|
+
Use the [code]--app[/code] flag to list that app's versions. Use the
|
|
41
|
+
[code]--flat[/code] flag to flatten the list of names/versions. If you
|
|
42
|
+
want to clone a community app locally, use the [code]nextmv community clone[/code] command.
|
|
43
43
|
|
|
44
44
|
[bold][underline]Examples[/underline][/bold]
|
|
45
45
|
|
|
46
46
|
- List the available community apps.
|
|
47
|
-
$ [
|
|
47
|
+
$ [green]nextmv community list[/green]
|
|
48
48
|
|
|
49
49
|
- List the available versions of the [magenta]go-nextroute[/magenta] community app.
|
|
50
|
-
$ [
|
|
50
|
+
$ [green]nextmv community list --app go-nextroute[/green]
|
|
51
51
|
|
|
52
52
|
- List the names of the available community apps as a flat list.
|
|
53
|
-
$ [
|
|
53
|
+
$ [green]nextmv community list --flat[/green]
|
|
54
54
|
|
|
55
55
|
- List the available versions of the [magenta]go-nextroute[/magenta] community app as a flat list.
|
|
56
|
-
$ [
|
|
56
|
+
$ [green]nextmv community list --app go-nextroute --flat[/green]
|
|
57
57
|
|
|
58
58
|
- List the available community apps using a profile named [magenta]hare[/magenta].
|
|
59
|
-
$ [
|
|
59
|
+
$ [green]nextmv community list --profile hare[/green]
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
62
|
if app is not None and app == "":
|
|
63
|
-
error("The --app flag cannot be an empty string.")
|
|
63
|
+
error("The [code]--app[/code] flag cannot be an empty string.")
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
manifest = download_manifest(profile=profile)
|
|
66
66
|
if flat and app is None:
|
|
67
|
-
|
|
67
|
+
apps_list(manifest)
|
|
68
68
|
raise typer.Exit()
|
|
69
69
|
elif not flat and app is None:
|
|
70
|
-
|
|
70
|
+
apps_table(manifest)
|
|
71
71
|
raise typer.Exit()
|
|
72
72
|
elif flat and app is not None and app != "":
|
|
73
|
-
|
|
73
|
+
versions_list(manifest, app)
|
|
74
74
|
raise typer.Exit()
|
|
75
75
|
elif not flat and app is not None and app != "":
|
|
76
|
-
|
|
76
|
+
versions_table(manifest, app)
|
|
77
77
|
raise typer.Exit()
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def
|
|
80
|
+
def download_manifest(profile: str | None = None) -> dict:
|
|
81
81
|
"""
|
|
82
|
-
|
|
82
|
+
Downloads and returns the community apps manifest.
|
|
83
83
|
|
|
84
84
|
Parameters
|
|
85
85
|
----------
|
|
86
|
-
|
|
87
|
-
The
|
|
86
|
+
profile : str | None
|
|
87
|
+
The profile name to use. If None, the default profile is used.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
dict
|
|
92
|
+
The community apps manifest as a dictionary.
|
|
93
|
+
|
|
94
|
+
Raises
|
|
95
|
+
requests.HTTPError
|
|
96
|
+
If the response status code is not 2xx.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
response = download_file(directory="community-apps", file="manifest.yml", profile=profile)
|
|
100
|
+
manifest = yaml.safe_load(response.text)
|
|
101
|
+
|
|
102
|
+
return manifest
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def apps_table(manifest: dict[str, Any]) -> None:
|
|
106
|
+
"""
|
|
107
|
+
This function prints a table of community apps from the manifest.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
manifest : dict[str, Any]
|
|
112
|
+
The community apps manifest.
|
|
88
113
|
"""
|
|
89
114
|
|
|
90
|
-
apps = list_community_apps(client)
|
|
91
115
|
table = Table("Name", "Type", "Latest", "Description", border_style="cyan", header_style="cyan")
|
|
92
|
-
for app in apps:
|
|
116
|
+
for app in manifest.get("apps", []):
|
|
93
117
|
table.add_row(
|
|
94
|
-
app.name,
|
|
95
|
-
app.
|
|
96
|
-
app.latest_app_version
|
|
97
|
-
app.description,
|
|
118
|
+
app.get("name", ""),
|
|
119
|
+
app.get("type", ""),
|
|
120
|
+
app.get("latest_app_version", ""),
|
|
121
|
+
app.get("description", ""),
|
|
98
122
|
)
|
|
99
123
|
|
|
100
124
|
console.print(table)
|
|
101
125
|
|
|
102
126
|
|
|
103
|
-
def
|
|
127
|
+
def apps_list(manifest: dict[str, Any]) -> None:
|
|
104
128
|
"""
|
|
105
|
-
This function prints a flat list of community app names.
|
|
129
|
+
This function prints a flat list of community app names from the manifest.
|
|
106
130
|
|
|
107
131
|
Parameters
|
|
108
132
|
----------
|
|
109
|
-
|
|
110
|
-
The
|
|
133
|
+
manifest : dict[str, Any]
|
|
134
|
+
The community apps manifest.
|
|
111
135
|
"""
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
names = [app.name for app in apps]
|
|
137
|
+
names = [app.get("name", "") for app in manifest.get("apps", [])]
|
|
115
138
|
print("\n".join(names))
|
|
116
139
|
|
|
117
140
|
|
|
118
|
-
def
|
|
141
|
+
def versions_table(manifest: dict[str, Any], app: str) -> None:
|
|
119
142
|
"""
|
|
120
143
|
This function prints a table of versions for a specific community app.
|
|
121
144
|
|
|
122
145
|
Parameters
|
|
123
146
|
----------
|
|
124
|
-
|
|
125
|
-
The
|
|
147
|
+
manifest : dict[str, Any]
|
|
148
|
+
The community apps manifest.
|
|
126
149
|
app : str
|
|
127
150
|
The name of the community app.
|
|
128
151
|
"""
|
|
129
152
|
|
|
130
|
-
|
|
131
|
-
latest_version =
|
|
153
|
+
app_obj = find_app(manifest, app)
|
|
154
|
+
latest_version = app_obj.get("latest_app_version", "")
|
|
132
155
|
|
|
133
156
|
# Add the latest version with indicator
|
|
134
157
|
table = Table("Version", "Latest?", border_style="cyan", header_style="cyan")
|
|
@@ -136,7 +159,7 @@ def _versions_table(client: Client, app: str) -> None:
|
|
|
136
159
|
table.add_row("", "") # Empty row to separate latest from others.
|
|
137
160
|
|
|
138
161
|
# Add all other versions (excluding the latest)
|
|
139
|
-
versions =
|
|
162
|
+
versions = app_obj.get("app_versions", [])
|
|
140
163
|
for version in versions:
|
|
141
164
|
if version != latest_version:
|
|
142
165
|
table.add_row(version, "")
|
|
@@ -144,57 +167,99 @@ def _versions_table(client: Client, app: str) -> None:
|
|
|
144
167
|
console.print(table)
|
|
145
168
|
|
|
146
169
|
|
|
147
|
-
def
|
|
170
|
+
def versions_list(manifest: dict[str, Any], app: str) -> None:
|
|
148
171
|
"""
|
|
149
172
|
This function prints a flat list of versions for a specific community app.
|
|
150
173
|
|
|
151
174
|
Parameters
|
|
152
175
|
----------
|
|
153
|
-
|
|
154
|
-
The
|
|
176
|
+
manifest : dict[str, Any]
|
|
177
|
+
The community apps manifest.
|
|
155
178
|
app : str
|
|
156
179
|
The name of the community app.
|
|
157
180
|
"""
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
versions =
|
|
182
|
+
app_obj = find_app(manifest, app)
|
|
183
|
+
versions = app_obj.get("app_versions", [])
|
|
161
184
|
|
|
162
185
|
versions_output = ""
|
|
163
186
|
for version in versions:
|
|
164
187
|
versions_output += f"{version}\n"
|
|
165
188
|
|
|
166
|
-
print(
|
|
189
|
+
print("\n".join(app_obj.get("app_versions", [])))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def download_file(
|
|
193
|
+
directory: str,
|
|
194
|
+
file: str,
|
|
195
|
+
profile: str | None = None,
|
|
196
|
+
) -> requests.Response:
|
|
197
|
+
"""
|
|
198
|
+
Gets a file from an internal bucket and return it.
|
|
167
199
|
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
directory : str
|
|
203
|
+
The directory in the bucket where the file is located.
|
|
204
|
+
file : str
|
|
205
|
+
The name of the file to download.
|
|
206
|
+
profile : str | None
|
|
207
|
+
The profile name to use. If None, the default profile is used.
|
|
168
208
|
|
|
169
|
-
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
requests.Response
|
|
212
|
+
The response object containing the file data.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
requests.HTTPError
|
|
216
|
+
If the response status code is not 2xx.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
client = build_client(profile)
|
|
220
|
+
|
|
221
|
+
# Request the download URL for the file.
|
|
222
|
+
response = client.request(
|
|
223
|
+
method="GET",
|
|
224
|
+
endpoint="v0/internal/tools",
|
|
225
|
+
headers=client.headers | {"request-source": "cli"}, # Pass `client.headers` to preserve auth.
|
|
226
|
+
query_params={"file": f"{directory}/{file}"},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Use the URL obtained to download the file.
|
|
230
|
+
body = response.json()
|
|
231
|
+
download_response = client.request(
|
|
232
|
+
method="GET",
|
|
233
|
+
endpoint=body.get("url"),
|
|
234
|
+
headers={"Content-Type": "application/json"},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return download_response
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def find_app(manifest: dict[str, Any], app: str) -> dict[str, Any] | None:
|
|
170
241
|
"""
|
|
171
242
|
Finds and returns a community app from the manifest by its name.
|
|
172
243
|
|
|
173
244
|
Parameters
|
|
174
245
|
----------
|
|
175
|
-
|
|
176
|
-
The
|
|
246
|
+
manifest : dict[str, Any]
|
|
247
|
+
The community apps manifest.
|
|
177
248
|
app : str
|
|
178
249
|
The name of the community app to find.
|
|
179
250
|
|
|
180
251
|
Returns
|
|
181
252
|
-------
|
|
182
|
-
|
|
183
|
-
The community app if found.
|
|
184
|
-
|
|
185
|
-
Raises
|
|
186
|
-
------
|
|
187
|
-
ValueError
|
|
188
|
-
If the community app is not found.
|
|
253
|
+
dict[str, Any] | None
|
|
254
|
+
The community app dictionary if found, otherwise None.
|
|
189
255
|
"""
|
|
190
256
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return comm_app
|
|
257
|
+
for manifest_app in manifest.get("apps", []):
|
|
258
|
+
if manifest_app.get("name", "") == app:
|
|
259
|
+
return manifest_app
|
|
195
260
|
|
|
196
261
|
# We don't use error() here to allow printing something before exiting.
|
|
197
262
|
rich.print(f"[red]Error:[/red] Community app [magenta]{app}[/magenta] was not found. Here are the available apps:")
|
|
198
|
-
|
|
263
|
+
apps_table(manifest)
|
|
199
264
|
|
|
200
265
|
raise typer.Exit(code=1)
|
|
@@ -7,8 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
|
-
from nextmv.cli.
|
|
11
|
-
from nextmv.cli.message import error, success, warning
|
|
10
|
+
from nextmv.cli.message import error
|
|
12
11
|
from nextmv.cloud.account import Account
|
|
13
12
|
from nextmv.cloud.application import Application
|
|
14
13
|
from nextmv.cloud.client import Client
|
|
@@ -60,18 +59,6 @@ def save_config(config: dict[str, Any]) -> None:
|
|
|
60
59
|
yaml.safe_dump(config, file)
|
|
61
60
|
|
|
62
61
|
|
|
63
|
-
def non_profile_keys() -> set[str]:
|
|
64
|
-
"""
|
|
65
|
-
Returns the set of keys that are not profile names in the configuration.
|
|
66
|
-
|
|
67
|
-
Returns
|
|
68
|
-
-------
|
|
69
|
-
set[str]
|
|
70
|
-
The set of non-profile keys.
|
|
71
|
-
"""
|
|
72
|
-
return {API_KEY_KEY, ENDPOINT_KEY}
|
|
73
|
-
|
|
74
|
-
|
|
75
62
|
def build_client(profile: str | None = None) -> Client:
|
|
76
63
|
"""
|
|
77
64
|
Builds a `cloud.Client` using the API key and endpoint for the given
|
|
@@ -104,35 +91,23 @@ def build_client(profile: str | None = None) -> Client:
|
|
|
104
91
|
|
|
105
92
|
if profile is not None:
|
|
106
93
|
if profile not in config:
|
|
107
|
-
error(
|
|
108
|
-
f"Profile [magenta]{profile}[/magenta] does not exist. "
|
|
109
|
-
"Create it using [code]nextmv configuration create[/code] with the --profile option."
|
|
110
|
-
)
|
|
94
|
+
error(f"Profile [magenta]{profile}[/magenta] does not exist.")
|
|
111
95
|
|
|
112
96
|
api_key = config[profile].get(API_KEY_KEY)
|
|
113
97
|
if api_key is None or api_key == "":
|
|
114
|
-
error(
|
|
115
|
-
f"API key for profile [magenta]{profile}[/magenta] is not set or is empty. "
|
|
116
|
-
"Set it using [code]nextmv configuration create[/code] with the --profile and --api-key options."
|
|
117
|
-
)
|
|
98
|
+
error(f"API key for profile [magenta]{profile}[/magenta] is not set or is empty.")
|
|
118
99
|
|
|
119
100
|
endpoint = config[profile].get(ENDPOINT_KEY)
|
|
120
101
|
if endpoint is None or endpoint == "":
|
|
121
|
-
error(
|
|
122
|
-
f"Endpoint for profile [magenta]{profile}[/magenta] is not set or is empty. "
|
|
123
|
-
"Please run [code]nextmv configuration create[/code]."
|
|
124
|
-
)
|
|
102
|
+
error(f"Endpoint for profile [magenta]{profile}[/magenta] is not set or is empty.")
|
|
125
103
|
else:
|
|
126
104
|
api_key = config.get(API_KEY_KEY)
|
|
127
105
|
if api_key is None or api_key == "":
|
|
128
|
-
error(
|
|
129
|
-
"Default API key is not set or is empty. "
|
|
130
|
-
"Please run [code]nextmv configuration create[/code] with the --api-key option."
|
|
131
|
-
)
|
|
106
|
+
error("Default API key is not set or is empty.")
|
|
132
107
|
|
|
133
108
|
endpoint = config.get(ENDPOINT_KEY)
|
|
134
109
|
if endpoint is None or endpoint == "":
|
|
135
|
-
error("Default endpoint is not set or is empty.
|
|
110
|
+
error("Default endpoint is not set or is empty.")
|
|
136
111
|
|
|
137
112
|
return Client(api_key=api_key, url=f"https://{endpoint}")
|
|
138
113
|
|
|
@@ -162,21 +137,13 @@ def build_app(app_id: str, profile: str | None = None) -> Application:
|
|
|
162
137
|
"""
|
|
163
138
|
client = build_client(profile)
|
|
164
139
|
exists = Application.exists(client=client, id=app_id)
|
|
165
|
-
if exists:
|
|
166
|
-
return Application(client=client, id=app_id)
|
|
167
|
-
|
|
168
|
-
warning(f"Application with ID [magenta]{app_id}[/magenta] does not exist.")
|
|
169
|
-
should_create = get_confirmation(f"Do you want to create a new application with ID [magenta]{app_id}[/magenta]?")
|
|
170
|
-
if not should_create:
|
|
140
|
+
if not exists:
|
|
171
141
|
error(
|
|
172
|
-
f"Application with ID [magenta]{app_id}[/magenta]
|
|
173
|
-
"Use [code]nextmv cloud app create[/code] to create a new
|
|
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."
|
|
174
144
|
)
|
|
175
145
|
|
|
176
|
-
|
|
177
|
-
success(f"Application with ID and name [magenta]{app_id}[/magenta] created successfully.")
|
|
178
|
-
|
|
179
|
-
return app
|
|
146
|
+
return Application(client=client, id=app_id)
|
|
180
147
|
|
|
181
148
|
|
|
182
149
|
def build_account(account_id: str | None = None, profile: str | None = None) -> Account:
|