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