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