nextmv 0.40.0__py3-none-any.whl → 1.0.0__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 (163) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +1 -2
  3. nextmv/__init__.py +2 -4
  4. nextmv/cli/CONTRIBUTING.md +583 -0
  5. nextmv/cli/cloud/__init__.py +49 -0
  6. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  7. nextmv/cli/cloud/acceptance/create.py +391 -0
  8. nextmv/cli/cloud/acceptance/delete.py +64 -0
  9. nextmv/cli/cloud/acceptance/get.py +103 -0
  10. nextmv/cli/cloud/acceptance/list.py +62 -0
  11. nextmv/cli/cloud/acceptance/update.py +95 -0
  12. nextmv/cli/cloud/account/__init__.py +28 -0
  13. nextmv/cli/cloud/account/create.py +83 -0
  14. nextmv/cli/cloud/account/delete.py +59 -0
  15. nextmv/cli/cloud/account/get.py +66 -0
  16. nextmv/cli/cloud/account/update.py +70 -0
  17. nextmv/cli/cloud/app/__init__.py +35 -0
  18. nextmv/cli/cloud/app/create.py +140 -0
  19. nextmv/cli/cloud/app/delete.py +57 -0
  20. nextmv/cli/cloud/app/exists.py +44 -0
  21. nextmv/cli/cloud/app/get.py +66 -0
  22. nextmv/cli/cloud/app/list.py +61 -0
  23. nextmv/cli/cloud/app/push.py +432 -0
  24. nextmv/cli/cloud/app/update.py +124 -0
  25. nextmv/cli/cloud/batch/__init__.py +29 -0
  26. nextmv/cli/cloud/batch/create.py +452 -0
  27. nextmv/cli/cloud/batch/delete.py +64 -0
  28. nextmv/cli/cloud/batch/get.py +104 -0
  29. nextmv/cli/cloud/batch/list.py +63 -0
  30. nextmv/cli/cloud/batch/metadata.py +66 -0
  31. nextmv/cli/cloud/batch/update.py +95 -0
  32. nextmv/cli/cloud/data/__init__.py +26 -0
  33. nextmv/cli/cloud/data/upload.py +162 -0
  34. nextmv/cli/cloud/ensemble/__init__.py +33 -0
  35. nextmv/cli/cloud/ensemble/create.py +413 -0
  36. nextmv/cli/cloud/ensemble/delete.py +63 -0
  37. nextmv/cli/cloud/ensemble/get.py +65 -0
  38. nextmv/cli/cloud/ensemble/list.py +63 -0
  39. nextmv/cli/cloud/ensemble/update.py +103 -0
  40. nextmv/cli/cloud/input_set/__init__.py +32 -0
  41. nextmv/cli/cloud/input_set/create.py +168 -0
  42. nextmv/cli/cloud/input_set/delete.py +64 -0
  43. nextmv/cli/cloud/input_set/get.py +63 -0
  44. nextmv/cli/cloud/input_set/list.py +63 -0
  45. nextmv/cli/cloud/input_set/update.py +123 -0
  46. nextmv/cli/cloud/instance/__init__.py +35 -0
  47. nextmv/cli/cloud/instance/create.py +289 -0
  48. nextmv/cli/cloud/instance/delete.py +61 -0
  49. nextmv/cli/cloud/instance/exists.py +39 -0
  50. nextmv/cli/cloud/instance/get.py +62 -0
  51. nextmv/cli/cloud/instance/list.py +60 -0
  52. nextmv/cli/cloud/instance/update.py +216 -0
  53. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  54. nextmv/cli/cloud/managed_input/create.py +144 -0
  55. nextmv/cli/cloud/managed_input/delete.py +64 -0
  56. nextmv/cli/cloud/managed_input/get.py +63 -0
  57. nextmv/cli/cloud/managed_input/list.py +60 -0
  58. nextmv/cli/cloud/managed_input/update.py +97 -0
  59. nextmv/cli/cloud/run/__init__.py +37 -0
  60. nextmv/cli/cloud/run/cancel.py +37 -0
  61. nextmv/cli/cloud/run/create.py +524 -0
  62. nextmv/cli/cloud/run/get.py +199 -0
  63. nextmv/cli/cloud/run/input.py +86 -0
  64. nextmv/cli/cloud/run/list.py +80 -0
  65. nextmv/cli/cloud/run/logs.py +166 -0
  66. nextmv/cli/cloud/run/metadata.py +67 -0
  67. nextmv/cli/cloud/run/track.py +500 -0
  68. nextmv/cli/cloud/scenario/__init__.py +29 -0
  69. nextmv/cli/cloud/scenario/create.py +451 -0
  70. nextmv/cli/cloud/scenario/delete.py +61 -0
  71. nextmv/cli/cloud/scenario/get.py +102 -0
  72. nextmv/cli/cloud/scenario/list.py +63 -0
  73. nextmv/cli/cloud/scenario/metadata.py +67 -0
  74. nextmv/cli/cloud/scenario/update.py +93 -0
  75. nextmv/cli/cloud/secrets/__init__.py +33 -0
  76. nextmv/cli/cloud/secrets/create.py +206 -0
  77. nextmv/cli/cloud/secrets/delete.py +63 -0
  78. nextmv/cli/cloud/secrets/get.py +66 -0
  79. nextmv/cli/cloud/secrets/list.py +60 -0
  80. nextmv/cli/cloud/secrets/update.py +144 -0
  81. nextmv/cli/cloud/shadow/__init__.py +33 -0
  82. nextmv/cli/cloud/shadow/create.py +184 -0
  83. nextmv/cli/cloud/shadow/delete.py +64 -0
  84. nextmv/cli/cloud/shadow/get.py +61 -0
  85. nextmv/cli/cloud/shadow/list.py +63 -0
  86. nextmv/cli/cloud/shadow/metadata.py +66 -0
  87. nextmv/cli/cloud/shadow/start.py +43 -0
  88. nextmv/cli/cloud/shadow/stop.py +53 -0
  89. nextmv/cli/cloud/shadow/update.py +96 -0
  90. nextmv/cli/cloud/switchback/__init__.py +33 -0
  91. nextmv/cli/cloud/switchback/create.py +151 -0
  92. nextmv/cli/cloud/switchback/delete.py +64 -0
  93. nextmv/cli/cloud/switchback/get.py +62 -0
  94. nextmv/cli/cloud/switchback/list.py +63 -0
  95. nextmv/cli/cloud/switchback/metadata.py +68 -0
  96. nextmv/cli/cloud/switchback/start.py +43 -0
  97. nextmv/cli/cloud/switchback/stop.py +53 -0
  98. nextmv/cli/cloud/switchback/update.py +96 -0
  99. nextmv/cli/cloud/upload/__init__.py +22 -0
  100. nextmv/cli/cloud/upload/create.py +39 -0
  101. nextmv/cli/cloud/version/__init__.py +33 -0
  102. nextmv/cli/cloud/version/create.py +96 -0
  103. nextmv/cli/cloud/version/delete.py +61 -0
  104. nextmv/cli/cloud/version/exists.py +39 -0
  105. nextmv/cli/cloud/version/get.py +62 -0
  106. nextmv/cli/cloud/version/list.py +60 -0
  107. nextmv/cli/cloud/version/update.py +92 -0
  108. nextmv/cli/community/__init__.py +24 -0
  109. nextmv/cli/community/clone.py +20 -204
  110. nextmv/cli/community/list.py +61 -126
  111. nextmv/cli/configuration/__init__.py +23 -0
  112. nextmv/cli/configuration/config.py +103 -6
  113. nextmv/cli/configuration/create.py +17 -18
  114. nextmv/cli/configuration/delete.py +25 -13
  115. nextmv/cli/configuration/list.py +4 -4
  116. nextmv/cli/confirm.py +34 -0
  117. nextmv/cli/main.py +68 -36
  118. nextmv/cli/message.py +170 -0
  119. nextmv/cli/options.py +196 -0
  120. nextmv/cli/version.py +20 -1
  121. nextmv/cloud/__init__.py +17 -38
  122. nextmv/cloud/acceptance_test.py +20 -83
  123. nextmv/cloud/account.py +269 -30
  124. nextmv/cloud/application/__init__.py +898 -0
  125. nextmv/cloud/application/_acceptance.py +424 -0
  126. nextmv/cloud/application/_batch_scenario.py +845 -0
  127. nextmv/cloud/application/_ensemble.py +251 -0
  128. nextmv/cloud/application/_input_set.py +263 -0
  129. nextmv/cloud/application/_instance.py +289 -0
  130. nextmv/cloud/application/_managed_input.py +227 -0
  131. nextmv/cloud/application/_run.py +1393 -0
  132. nextmv/cloud/application/_secrets.py +294 -0
  133. nextmv/cloud/application/_shadow.py +320 -0
  134. nextmv/cloud/application/_switchback.py +332 -0
  135. nextmv/cloud/application/_utils.py +54 -0
  136. nextmv/cloud/application/_version.py +304 -0
  137. nextmv/cloud/batch_experiment.py +6 -2
  138. nextmv/cloud/community.py +446 -0
  139. nextmv/cloud/instance.py +11 -1
  140. nextmv/cloud/integration.py +8 -5
  141. nextmv/cloud/package.py +50 -9
  142. nextmv/cloud/shadow.py +254 -0
  143. nextmv/cloud/switchback.py +228 -0
  144. nextmv/deprecated.py +5 -3
  145. nextmv/input.py +20 -88
  146. nextmv/local/application.py +3 -15
  147. nextmv/local/runner.py +1 -1
  148. nextmv/model.py +50 -11
  149. nextmv/options.py +11 -256
  150. nextmv/output.py +0 -62
  151. nextmv/polling.py +54 -16
  152. nextmv/run.py +84 -37
  153. nextmv/status.py +1 -51
  154. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
  155. nextmv-1.0.0.dist-info/RECORD +185 -0
  156. nextmv-1.0.0.dist-info/entry_points.txt +2 -0
  157. nextmv/cli/community/community.py +0 -24
  158. nextmv/cli/configuration/configuration.py +0 -23
  159. nextmv/cli/error.py +0 -22
  160. nextmv/cloud/application.py +0 -4204
  161. nextmv-0.40.0.dist-info/RECORD +0 -66
  162. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
  163. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,29 @@
1
+ """
2
+ This module defines the cloud batch command tree for the Nextmv CLI.
3
+ """
4
+
5
+ import typer
6
+
7
+ from nextmv.cli.cloud.batch.create import app as create_app
8
+ from nextmv.cli.cloud.batch.delete import app as delete_app
9
+ from nextmv.cli.cloud.batch.get import app as get_app
10
+ from nextmv.cli.cloud.batch.list import app as list_app
11
+ from nextmv.cli.cloud.batch.metadata import app as metadata_app
12
+ from nextmv.cli.cloud.batch.update import app as update_app
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+ app.add_typer(create_app)
17
+ app.add_typer(delete_app)
18
+ app.add_typer(get_app)
19
+ app.add_typer(list_app)
20
+ app.add_typer(metadata_app)
21
+ app.add_typer(update_app)
22
+
23
+
24
+ @app.callback()
25
+ def callback() -> None:
26
+ """
27
+ Create and manage Nextmv Cloud batch experiments.
28
+ """
29
+ pass
@@ -0,0 +1,452 @@
1
+ """
2
+ This module defines the cloud batch create command for the Nextmv CLI.
3
+ """
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from nextmv.cli.configuration.config import build_app
11
+ from nextmv.cli.message import error, in_progress, print_json, success
12
+ from nextmv.cli.options import AppIDOption, ProfileOption
13
+ from nextmv.cloud.batch_experiment import BatchExperimentRun
14
+ from nextmv.polling import default_polling_options
15
+
16
+ # Set up subcommand application.
17
+ app = typer.Typer()
18
+
19
+
20
+ @app.command()
21
+ def create(
22
+ app_id: AppIDOption,
23
+ # Options for batch experiment configuration.
24
+ batch_experiment_id: Annotated[
25
+ str | None,
26
+ typer.Option(
27
+ "--batch-experiment-id",
28
+ "-b",
29
+ help="ID for the batch experiment. Will be generated if not provided.",
30
+ envvar="NEXTMV_BATCH_EXPERIMENT_ID",
31
+ metavar="BATCH_EXPERIMENT_ID",
32
+ rich_help_panel="Batch experiment configuration",
33
+ ),
34
+ ] = None,
35
+ description: Annotated[
36
+ str | None,
37
+ typer.Option(
38
+ "--description",
39
+ "-d",
40
+ help="Description of the batch experiment.",
41
+ metavar="DESCRIPTION",
42
+ rich_help_panel="Batch experiment configuration",
43
+ ),
44
+ ] = None,
45
+ input_set_id: Annotated[
46
+ str | None,
47
+ typer.Option(
48
+ "--input-set-id",
49
+ "-i",
50
+ help="ID of the input set to use for the batch experiment.",
51
+ metavar="INPUT_SET_ID",
52
+ rich_help_panel="Batch experiment configuration",
53
+ ),
54
+ ] = None,
55
+ name: Annotated[
56
+ str | None,
57
+ typer.Option(
58
+ "--name",
59
+ "-n",
60
+ help="Name of the batch experiment. If not provided, the ID will be used as the name.",
61
+ metavar="NAME",
62
+ rich_help_panel="Batch experiment configuration",
63
+ ),
64
+ ] = None,
65
+ option_sets: Annotated[
66
+ str | None,
67
+ typer.Option(
68
+ "--option-sets",
69
+ help="Option sets to use for the batch experiment. Data should be valid [magenta]json[/magenta]. "
70
+ "See command help for details on option sets formatting.",
71
+ metavar="OPTION_SETS",
72
+ rich_help_panel="Batch experiment configuration",
73
+ ),
74
+ ] = None,
75
+ runs: Annotated[
76
+ list[str] | None,
77
+ typer.Option(
78
+ "--runs",
79
+ "-r",
80
+ help="Runs to execute for the batch experiment. Data should be valid [magenta]json[/magenta]. "
81
+ "Pass multiple runs by repeating the flag, or providing a list of objects. "
82
+ "See command help for details on run formatting.",
83
+ metavar="RUNS",
84
+ rich_help_panel="Batch experiment configuration",
85
+ ),
86
+ ] = None,
87
+ # Options for controlling output.
88
+ output: Annotated[
89
+ str | None,
90
+ typer.Option(
91
+ "--output",
92
+ "-o",
93
+ help="Waits for the experiment to complete and saves the results to this location.",
94
+ metavar="OUTPUT_PATH",
95
+ rich_help_panel="Output control",
96
+ ),
97
+ ] = None,
98
+ timeout: Annotated[
99
+ int,
100
+ typer.Option(
101
+ help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
102
+ metavar="TIMEOUT_SECONDS",
103
+ rich_help_panel="Output control",
104
+ ),
105
+ ] = -1,
106
+ wait: Annotated[
107
+ bool,
108
+ typer.Option(
109
+ "--wait",
110
+ "-w",
111
+ help="Wait for the batch experiment to complete. Results are printed to [magenta]stdout[/magenta]. "
112
+ "Specify output location with --output.",
113
+ rich_help_panel="Output control",
114
+ ),
115
+ ] = False,
116
+ profile: ProfileOption = None,
117
+ ) -> None:
118
+ """
119
+ Create a new Nextmv Cloud batch experiment.
120
+
121
+ A batch experiment executes multiple runs across different inputs and/or
122
+ configurations. Each run is defined by a combination of input, instance or
123
+ version, and optional configuration options.
124
+
125
+ Use the --wait flag to wait for the batch experiment to complete, polling
126
+ for results. Using the --output flag will also activate waiting, and allows
127
+ you to specify a destination file for the results.
128
+
129
+ [bold][underline]Runs[/underline][/bold]
130
+
131
+ Runs are provided as [magenta]json[/magenta] objects using the --runs flag.
132
+ Each run defines what input, instance/version, and configuration to use.
133
+
134
+ You can provide runs in three ways:
135
+ - A single run as a [magenta]json[/magenta] object.
136
+ - Multiple runs by repeating the --runs flag.
137
+ - Multiple runs as a [magenta]json[/magenta] array in a single --runs flag.
138
+
139
+ Each run must have the following fields:
140
+ - [magenta]input_id[/magenta]: ID of the input to use for this run
141
+ (required). If a managed input is used, this should be the ID of the
142
+ managed input. If [magenta]input_set_id[/magenta] is provided for the run,
143
+ this should be the ID of an input within that input set.
144
+ - [magenta]instance_id[/magenta] OR [magenta]version_id[/magenta]: Either an instance ID or
145
+ version ID must be provided (at least one required).
146
+ - [magenta]option_set[/magenta]: ID of the option set to use (optional).
147
+ Make sure to define the option sets using the --option-sets flag.
148
+ - [magenta]input_set_id[/magenta]: ID of the input set (optional).
149
+ - [magenta]scenario_id[/magenta]: Scenario ID if part of a scenario test (optional).
150
+ - [magenta]repetition[/magenta]: Repetition number (optional).
151
+
152
+ Object format:
153
+ [dim]{
154
+ "input_id": "meadow-input-a1",
155
+ "instance_id": "bunny-hopper-v2",
156
+ "option_set": "speed-optimized",
157
+ "input_set_id": "spring-gardens"
158
+ }[/dim]
159
+
160
+ [bold][underline]Option Sets[/underline][/bold]
161
+
162
+ Option sets are provided as a [magenta]json[/magenta] object using the
163
+ --option-sets flag. Option sets define named collections of
164
+ runtime options that can be referenced by runs.
165
+
166
+ The option sets object is a dictionary where keys are option set IDs and
167
+ values are dictionaries of string key-value pairs representing the options.
168
+
169
+ Object format:
170
+ [dim]{
171
+ "speed-optimized": {"timeout": "30", "algorithm": "fast"},
172
+ "quality-focused": {"timeout": "300", "algorithm": "thorough"}
173
+ }[/dim]
174
+
175
+ [bold][underline]Examples[/underline][/bold]
176
+
177
+ - Create a batch experiment with a single run.
178
+ $ [dim]RUN='{
179
+ "input_id": "carrot-patch-a",
180
+ "instance_id": "warren-planner-v1"
181
+ }'
182
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id bunny-hop-test \\
183
+ --name "Spring Meadow Routes" --input-set-id spring-gardens --runs "$RUN"[/dim]
184
+
185
+ - Create with multiple runs by repeating the flag.
186
+ $ [dim]RUN1='{
187
+ "input_id": "lettuce-field-1",
188
+ "instance_id": "hop-optimizer"
189
+ }'
190
+ RUN2='{
191
+ "input_id": "lettuce-field-2",
192
+ "instance_id": "hop-optimizer"
193
+ }'
194
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id lettuce-routes \\
195
+ --name "Lettuce Delivery Optimization" --input-set-id veggie-gardens \\
196
+ --runs "$RUN1" --runs "$RUN2"[/dim]
197
+
198
+ - Create with multiple runs in a single [magenta]json[/magenta] array.
199
+ $ [dim]RUNS='[
200
+ {
201
+ "input_id": "warren-zone-a",
202
+ "instance_id": "burrow-builder"
203
+ },
204
+ {
205
+ "input_id": "warren-zone-b",
206
+ "version_id": "tunnel-planner-v3"
207
+ }
208
+ ]'
209
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id warren-expansion \\
210
+ --name "Warren Construction Plans" --input-set-id burrow-sites --runs "$RUNS"[/dim]
211
+
212
+ - Create a batch experiment and wait for it to complete.
213
+ $ [dim]RUN='{
214
+ "input_id": "carrot-harvest",
215
+ "instance_id": "foraging-route"
216
+ }'
217
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id harvest-time \\
218
+ --name "Autumn Carrot Collection" --input-set-id harvest-season \\
219
+ --runs "$RUN" --wait[/dim]
220
+
221
+ - Create a batch experiment and save the results to a file, waiting for completion.
222
+ $ [dim]RUN='{
223
+ "input_id": "predator-zones",
224
+ "instance_id": "safe-hopper"
225
+ }'
226
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id safety-analysis \\
227
+ --name "Fox Avoidance Routes" --input-set-id danger-zones \\
228
+ --runs "$RUN" --output bunny-safety-results.json[/dim]
229
+
230
+ - Create a batch experiment with option sets.
231
+ $ [dim]RUN1='{
232
+ "input_id": "garden-route-1",
233
+ "instance_id": "hop-optimizer",
234
+ "option_set": "fast-hops"
235
+ }'
236
+ RUN2='{
237
+ "input_id": "garden-route-1",
238
+ "instance_id": "hop-optimizer",
239
+ "option_set": "careful-hops"
240
+ }'
241
+ OPTION_SETS='{
242
+ "fast-hops": {"max_speed": "10", "caution_level": "low"},
243
+ "careful-hops": {"max_speed": "5", "caution_level": "high"}
244
+ }'
245
+ nextmv cloud batch create --app-id hare-app --batch-experiment-id hop-comparison \\
246
+ --name "Speed vs Safety Analysis" --input-set-id garden-paths \\
247
+ --runs "$RUN1" --runs "$RUN2" --option-sets "$OPTION_SETS"[/dim]
248
+ """
249
+
250
+ cloud_app = build_app(app_id=app_id, profile=profile)
251
+
252
+ # Build the runs list from the CLI options
253
+ runs_list = build_runs(runs)
254
+
255
+ # Build the option sets from the CLI options
256
+ option_sets_dict = build_option_sets(option_sets)
257
+
258
+ batch_id = cloud_app.new_batch_experiment(
259
+ id=batch_experiment_id,
260
+ name=name,
261
+ runs=runs_list,
262
+ description=description,
263
+ input_set_id=input_set_id,
264
+ option_sets=option_sets_dict,
265
+ )
266
+
267
+ # If we don't need to poll at all we are done.
268
+ if not wait and (output is None or output == ""):
269
+ print_json({"batch_experiment_id": batch_id})
270
+
271
+ return
272
+
273
+ success(f"Batch experiment [magenta]{batch_id}[/magenta] created.")
274
+
275
+ # Build the polling options.
276
+ polling_options = default_polling_options()
277
+ polling_options.max_duration = timeout
278
+
279
+ in_progress(msg="Getting batch experiment results...")
280
+ batch_experiment = cloud_app.batch_experiment_with_polling(
281
+ batch_id=batch_id,
282
+ polling_options=polling_options,
283
+ )
284
+ batch_experiment_dict = batch_experiment.to_dict()
285
+
286
+ # Handle output
287
+ if output is not None and output != "":
288
+ with open(output, "w") as f:
289
+ json.dump(batch_experiment_dict, f, indent=2)
290
+
291
+ success(msg=f"Batch experiment results saved to [magenta]{output}[/magenta].")
292
+ return
293
+
294
+ print_json(batch_experiment_dict)
295
+
296
+
297
+ def build_option_sets(option_sets: str | None) -> dict[str, dict[str, str]] | None:
298
+ """
299
+ Builds the option sets dictionary from the CLI option.
300
+
301
+ Parameters
302
+ ----------
303
+ option_sets : str | None
304
+ Option sets JSON string provided via the CLI.
305
+
306
+ Returns
307
+ -------
308
+ dict[str, dict[str, str]] | None
309
+ The built option sets dictionary, or None if not provided.
310
+ """
311
+ if option_sets is None:
312
+ return None
313
+
314
+ try:
315
+ option_sets_data = json.loads(option_sets)
316
+
317
+ if not isinstance(option_sets_data, dict):
318
+ error(
319
+ f"Invalid option sets format: [magenta]{option_sets}[/magenta]. "
320
+ "Expected [magenta]json[/magenta] object."
321
+ )
322
+
323
+ # Validate structure
324
+ for key, value in option_sets_data.items():
325
+ if not isinstance(value, dict):
326
+ error(
327
+ f"Invalid option sets format: [magenta]{option_sets}[/magenta]. "
328
+ f"Each option set must be a [magenta]json[/magenta] object. "
329
+ f"Key [magenta]{key}[/magenta] has invalid value."
330
+ )
331
+
332
+ return option_sets_data
333
+
334
+ except json.JSONDecodeError as e:
335
+ error(f"Invalid option sets format: [magenta]{option_sets}[/magenta]. Error: {e}")
336
+
337
+
338
+ def build_runs(runs: list[str] | None) -> list[BatchExperimentRun] | None:
339
+ """
340
+ Builds the runs list from the CLI option(s).
341
+
342
+ Parameters
343
+ ----------
344
+ runs : list[str] | None
345
+ List of runs provided via the CLI.
346
+
347
+ Returns
348
+ -------
349
+ list[BatchExperimentRun] | None
350
+ The built runs list, or None if no runs provided.
351
+ """
352
+ if runs is None:
353
+ return None
354
+
355
+ runs_list = []
356
+
357
+ for run_str in runs:
358
+ try:
359
+ run_data = json.loads(run_str)
360
+
361
+ # Handle the case where the value is a list of runs.
362
+ if isinstance(run_data, list):
363
+ runs_list.extend(_process_run_list(run_data, run_str))
364
+
365
+ # Handle the case where the value is a single run.
366
+ elif isinstance(run_data, dict):
367
+ runs_list.append(_process_single_run(run_data, run_str))
368
+
369
+ else:
370
+ error(
371
+ f"Invalid run format: [magenta]{run_str}[/magenta]. "
372
+ "Expected [magenta]json[/magenta] object or array."
373
+ )
374
+
375
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
376
+ error(f"Invalid run format: [magenta]{run_str}[/magenta]. Error: {e}")
377
+
378
+ return runs_list if runs_list else None
379
+
380
+
381
+ def _process_run_list(run_data: list, run_str: str) -> list[BatchExperimentRun]:
382
+ """
383
+ Process a list of run objects.
384
+
385
+ Parameters
386
+ ----------
387
+ run_data : list
388
+ List of run dictionaries.
389
+ run_str : str
390
+ Original string for error messages.
391
+
392
+ Returns
393
+ -------
394
+ list[BatchExperimentRun]
395
+ List of processed runs.
396
+ """
397
+ processed_runs = []
398
+ for ix, item in enumerate(run_data):
399
+ _validate_run_fields(item, run_str, ix)
400
+ run = BatchExperimentRun(**item)
401
+ processed_runs.append(run)
402
+ return processed_runs
403
+
404
+
405
+ def _process_single_run(run_data: dict, run_str: str) -> BatchExperimentRun:
406
+ """
407
+ Process a single run object.
408
+
409
+ Parameters
410
+ ----------
411
+ run_data : dict
412
+ Run dictionary.
413
+ run_str : str
414
+ Original string for error messages.
415
+
416
+ Returns
417
+ -------
418
+ BatchExperimentRun
419
+ Processed run.
420
+ """
421
+ _validate_run_fields(run_data, run_str)
422
+ return BatchExperimentRun(**run_data)
423
+
424
+
425
+ def _validate_run_fields(run_data: dict, run_str: str, index: int | None = None) -> None:
426
+ """
427
+ Validate that required run fields are present.
428
+
429
+ Parameters
430
+ ----------
431
+ run_data : dict
432
+ Run dictionary to validate.
433
+ run_str : str
434
+ Original string for error messages.
435
+ index : int | None
436
+ Index in array if validating array element.
437
+ """
438
+ location = f"at index [magenta]{index}[/magenta] in " if index is not None else "in "
439
+
440
+ if run_data.get("input_id") is None:
441
+ error(
442
+ f"Invalid run format {location}"
443
+ f"[magenta]{run_str}[/magenta]. Each run must have an "
444
+ "[magenta]input_id[/magenta] field."
445
+ )
446
+
447
+ if run_data.get("instance_id") is None and run_data.get("version_id") is None:
448
+ error(
449
+ f"Invalid run format {location}"
450
+ f"[magenta]{run_str}[/magenta]. Each run must have either an "
451
+ "[magenta]instance_id[/magenta] or [magenta]version_id[/magenta] field."
452
+ )
@@ -0,0 +1,64 @@
1
+ """
2
+ This module defines the cloud batch delete command for the Nextmv CLI.
3
+ """
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from nextmv.cli.configuration.config import build_app
10
+ from nextmv.cli.confirm import get_confirmation
11
+ from nextmv.cli.message import info, success
12
+ from nextmv.cli.options import AppIDOption, BatchExperimentIDOption, ProfileOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def delete(
20
+ app_id: AppIDOption,
21
+ batch_experiment_id: BatchExperimentIDOption,
22
+ yes: Annotated[
23
+ bool,
24
+ typer.Option(
25
+ "--yes",
26
+ "-y",
27
+ help="Agree to deletion confirmation prompt. Useful for non-interactive sessions.",
28
+ ),
29
+ ] = False,
30
+ profile: ProfileOption = None,
31
+ ) -> None:
32
+ """
33
+ Deletes a Nextmv Cloud batch experiment.
34
+
35
+ This action is permanent and cannot be undone. The batch experiment and all
36
+ associated data, including runs, will be deleted. Use the --yes
37
+ flag to skip the confirmation prompt.
38
+
39
+ [bold][underline]Examples[/underline][/bold]
40
+
41
+ - Delete the batch experiment with the ID [magenta]hop-analysis[/magenta] from application
42
+ [magenta]hare-app[/magenta].
43
+ $ [dim]nextmv cloud batch delete --app-id hare-app --batch-experiment-id hop-analysis[/dim]
44
+
45
+ - Delete the batch experiment without confirmation prompt.
46
+ $ [dim]nextmv cloud batch delete --app-id hare-app --batch-experiment-id carrot-routes --yes[/dim]
47
+ """
48
+
49
+ if not yes:
50
+ confirm = get_confirmation(
51
+ f"Are you sure you want to delete batch experiment [magenta]{batch_experiment_id}[/magenta] "
52
+ f"from application [magenta]{app_id}[/magenta]? This action cannot be undone.",
53
+ )
54
+
55
+ if not confirm:
56
+ info(f"Batch experiment [magenta]{batch_experiment_id}[/magenta] will not be deleted.")
57
+ return
58
+
59
+ cloud_app = build_app(app_id=app_id, profile=profile)
60
+ cloud_app.delete_batch_experiment(batch_id=batch_experiment_id)
61
+ success(
62
+ f"Batch experiment [magenta]{batch_experiment_id}[/magenta] deleted successfully "
63
+ f"from application [magenta]{app_id}[/magenta]."
64
+ )
@@ -0,0 +1,104 @@
1
+ """
2
+ This module defines the cloud batch get command for the Nextmv CLI.
3
+ """
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from nextmv.cli.configuration.config import build_app
11
+ from nextmv.cli.message import in_progress, print_json, success
12
+ from nextmv.cli.options import AppIDOption, BatchExperimentIDOption, ProfileOption
13
+ from nextmv.polling import default_polling_options
14
+
15
+ # Set up subcommand application.
16
+ app = typer.Typer()
17
+
18
+
19
+ @app.command()
20
+ def get(
21
+ app_id: AppIDOption,
22
+ batch_experiment_id: BatchExperimentIDOption,
23
+ output: Annotated[
24
+ str | None,
25
+ typer.Option(
26
+ "--output",
27
+ "-o",
28
+ help="Waits for the batch experiment to complete and saves the results to this location.",
29
+ metavar="OUTPUT_PATH",
30
+ ),
31
+ ] = None,
32
+ timeout: Annotated[
33
+ int,
34
+ typer.Option(
35
+ help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
36
+ metavar="TIMEOUT_SECONDS",
37
+ ),
38
+ ] = -1,
39
+ wait: Annotated[
40
+ bool,
41
+ typer.Option(
42
+ "--wait",
43
+ "-w",
44
+ help="Wait for the batch experiment to complete. Results are printed to [magenta]stdout[/magenta]. "
45
+ "Specify output location with --output.",
46
+ ),
47
+ ] = False,
48
+ profile: ProfileOption = None,
49
+ ) -> None:
50
+ """
51
+ Get a Nextmv Cloud batch experiment, including its runs.
52
+
53
+ Use the --wait flag to wait for the batch experiment to
54
+ complete, polling for results. Using the --output flag will
55
+ also activate waiting, and allows you to specify a destination file for the
56
+ results.
57
+
58
+ [bold][underline]Examples[/underline][/bold]
59
+
60
+ - Get the batch experiment with ID [magenta]carrot-optimization[/magenta] from application
61
+ [magenta]hare-app[/magenta].
62
+ $ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id carrot-optimization[/dim]
63
+
64
+ - Get the batch experiment and wait for it to complete if necessary.
65
+ $ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id bunny-hop-test --wait[/dim]
66
+
67
+ - Get the batch experiment and save the results to a file.
68
+ $ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id warren-planning \\
69
+ --output results.json[/dim]
70
+
71
+ - Get the batch experiment using a specific profile.
72
+ $ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id lettuce-routes --profile prod[/dim]
73
+ """
74
+
75
+ cloud_app = build_app(app_id=app_id, profile=profile)
76
+
77
+ # Build the polling options.
78
+ polling_options = default_polling_options()
79
+ polling_options.max_duration = timeout
80
+
81
+ # Determine if we should wait
82
+ should_wait = wait or (output is not None and output != "")
83
+
84
+ in_progress(msg="Getting batch experiment...")
85
+ if should_wait:
86
+ batch_experiment = cloud_app.batch_experiment_with_polling(
87
+ batch_id=batch_experiment_id,
88
+ polling_options=polling_options,
89
+ )
90
+ else:
91
+ batch_experiment = cloud_app.batch_experiment(batch_id=batch_experiment_id)
92
+
93
+ batch_experiment_dict = batch_experiment.to_dict()
94
+
95
+ # Handle output
96
+ if output is not None and output != "":
97
+ with open(output, "w") as f:
98
+ json.dump(batch_experiment_dict, f, indent=2)
99
+
100
+ success(msg=f"Batch experiment results saved to [magenta]{output}[/magenta].")
101
+
102
+ return
103
+
104
+ print_json(batch_experiment_dict)