nextmv 0.39.0.dev1__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 (161) 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 +86 -0
  110. nextmv/cli/community/list.py +200 -0
  111. nextmv/cli/configuration/__init__.py +23 -0
  112. nextmv/cli/configuration/config.py +228 -0
  113. nextmv/cli/configuration/create.py +94 -0
  114. nextmv/cli/configuration/delete.py +67 -0
  115. nextmv/cli/configuration/list.py +77 -0
  116. nextmv/cli/confirm.py +34 -0
  117. nextmv/cli/main.py +161 -3
  118. nextmv/cli/message.py +170 -0
  119. nextmv/cli/options.py +220 -0
  120. nextmv/cli/version.py +22 -2
  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.39.0.dev1.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/cloud/application.py +0 -4204
  158. nextmv-0.39.0.dev1.dist-info/RECORD +0 -55
  159. nextmv-0.39.0.dev1.dist-info/entry_points.txt +0 -2
  160. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
  161. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,451 @@
1
+ """
2
+ This module defines the cloud scenario 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.scenario import Scenario
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 scenario test configuration.
24
+ scenarios: Annotated[
25
+ list[str],
26
+ typer.Option(
27
+ "--scenarios",
28
+ "-s",
29
+ help="Scenarios to use for the test. Data should be valid [magenta]json[/magenta]. "
30
+ "Pass multiple scenarios by repeating the flag, or providing a list of objects. "
31
+ "See command help for details on scenario formatting.",
32
+ metavar="SCENARIOS",
33
+ rich_help_panel="Scenario test configuration",
34
+ ),
35
+ ],
36
+ description: Annotated[
37
+ str | None,
38
+ typer.Option(
39
+ "--description",
40
+ "-d",
41
+ help="Description of the scenario test.",
42
+ metavar="DESCRIPTION",
43
+ rich_help_panel="Scenario test configuration",
44
+ ),
45
+ ] = None,
46
+ name: Annotated[
47
+ str | None,
48
+ typer.Option(
49
+ "--name",
50
+ "-n",
51
+ help="Name of the scenario test. If not provided, the ID will be used as the name.",
52
+ metavar="NAME",
53
+ rich_help_panel="Scenario test configuration",
54
+ ),
55
+ ] = None,
56
+ repetitions: Annotated[
57
+ int | None,
58
+ typer.Option(
59
+ "--repetitions",
60
+ "-r",
61
+ help="Number of times the scenario test is [italic]repeated[/italic]. "
62
+ "0 repetitions = 1 execution, 1 repetition = 2 executions, etc.",
63
+ metavar="REPETITIONS",
64
+ rich_help_panel="Scenario test configuration",
65
+ ),
66
+ ] = 0,
67
+ scenario_test_id: Annotated[
68
+ str | None,
69
+ typer.Option(
70
+ "--scenario-test-id",
71
+ "-i",
72
+ help="ID for the scenario test. Will be generated if not provided.",
73
+ envvar="NEXTMV_SCENARIO_TEST_ID",
74
+ metavar="SCENARIO_TEST_ID",
75
+ rich_help_panel="Scenario test configuration",
76
+ ),
77
+ ] = None,
78
+ # Options for controlling output.
79
+ output: Annotated[
80
+ str | None,
81
+ typer.Option(
82
+ "--output",
83
+ "-o",
84
+ help="Waits for the test to complete and saves the results to this location.",
85
+ metavar="OUTPUT_PATH",
86
+ rich_help_panel="Output control",
87
+ ),
88
+ ] = None,
89
+ timeout: Annotated[
90
+ int,
91
+ typer.Option(
92
+ help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
93
+ metavar="TIMEOUT_SECONDS",
94
+ rich_help_panel="Output control",
95
+ ),
96
+ ] = -1,
97
+ wait: Annotated[
98
+ bool,
99
+ typer.Option(
100
+ "--wait",
101
+ "-w",
102
+ help="Wait for the scenario test to complete. Results are printed to [magenta]stdout[/magenta]. "
103
+ "Specify output location with --output.",
104
+ rich_help_panel="Output control",
105
+ ),
106
+ ] = False,
107
+ profile: ProfileOption = None,
108
+ ) -> None:
109
+ """
110
+ Create a new Nextmv Cloud scenario test.
111
+
112
+ A scenario test allows you to run multiple scenarios with different inputs,
113
+ instances/versions, and configurations in a single test.
114
+
115
+ Use the --wait flag to wait for the scenario test to complete,
116
+ polling for results. Using the --output flag will also
117
+ activate waiting, and allows you to specify a destination file for the
118
+ results.
119
+
120
+ [bold][underline]Scenarios[/underline][/bold]
121
+
122
+ Scenarios are provided as [magenta]json[/magenta] objects using the
123
+ --scenarios flag. Each scenario defines the configuration for a scenario
124
+ test execution.
125
+
126
+ You can provide scenarios in three ways:
127
+ - A single scenario as a [magenta]json[/magenta] object.
128
+ - Multiple scenarios by repeating the --scenarios flag.
129
+ - Multiple scenarios as a [magenta]json[/magenta] array in a single --scenarios flag.
130
+
131
+ Each scenario must have the following fields:
132
+ - [magenta]instance_id[/magenta]: ID of the instance to use for this scenario (required).
133
+ - [magenta]scenario_input[/magenta]: Object containing the scenario input (required), with:
134
+ - [magenta]scenario_input_type[/magenta]: Type of the scenario input (required).
135
+ - [magenta]scenario_input_data[/magenta]: Data for the scenario input (required).
136
+ - [magenta]scenario_id[/magenta]: ID of the scenario (optional). The
137
+ default value will be set as [magenta]scenario-<index>[/magenta] if not set.
138
+ - [magenta]configuration[/magenta]: An array of configuration objects
139
+ (optional). Use this attribute to configure variation of options for the scenario. Each scenario
140
+ configuration object requires:
141
+ - [magenta]name[/magenta]: Name of the configuration option.
142
+ - [magenta]values[/magenta]: List of values for the configuration option.
143
+
144
+ Example object format:
145
+ [dim]{
146
+ "instance_id": "bunny-hopper-v2",
147
+ "scenario_input": {
148
+ "scenario_input_type": "input_set",
149
+ "scenario_input_data": {
150
+ "input_id": "meadow-input-a1",
151
+ "input_set_id": "spring-gardens"
152
+ }
153
+ },
154
+ "configuration": [
155
+ {
156
+ "name": "speed",
157
+ "values": ["optimized", "balanced", "safe"]
158
+ }
159
+ ]
160
+ }[/dim]
161
+
162
+ [bold][underline]Examples[/underline][/bold]
163
+
164
+ - Create a scenario test with a single scenario.
165
+ $ [dim]SCENARIO='{
166
+ "instance_id": "warren-planner-v1",
167
+ "scenario_input": {
168
+ "scenario_input_type": "input_set",
169
+ "scenario_input_data": {
170
+ "input_id": "carrot-patch-a",
171
+ "input_set_id": "spring-gardens"
172
+ }
173
+ }
174
+ }'
175
+ nextmv cloud scenario create --app-id hare-app --name "Spring Meadow Routes" --scenarios "$SCENARIO"[/dim]
176
+
177
+ - Create with multiple scenarios by repeating the flag.
178
+ $ [dim]SCENARIO1='{
179
+ "instance_id": "hop-optimizer",
180
+ "scenario_input": {
181
+ "scenario_input_type": "input_set",
182
+ "scenario_input_data": {
183
+ "input_id": "lettuce-field-1",
184
+ "input_set_id": "veggie-gardens"
185
+ }
186
+ }
187
+ }'
188
+ SCENARIO2='{
189
+ "instance_id": "hop-optimizer",
190
+ "scenario_input": {
191
+ "scenario_input_type": "input_set",
192
+ "scenario_input_data": {
193
+ "input_id": "lettuce-field-2",
194
+ "input_set_id": "veggie-gardens"
195
+ }
196
+ }
197
+ }'
198
+ nextmv cloud scenario create --app-id hare-app --name "Lettuce Delivery Optimization" \\
199
+ --scenarios "$SCENARIO1" --scenarios "$SCENARIO2"[/dim]
200
+
201
+ - Create with multiple scenarios in a single [magenta]json[/magenta] array.
202
+ $ [dim]SCENARIOS='[
203
+ {
204
+ "instance_id": "burrow-builder",
205
+ "scenario_input": {
206
+ "scenario_input_type": "input_set",
207
+ "scenario_input_data": {
208
+ "input_id": "warren-zone-a",
209
+ "input_set_id": "burrow-sites"
210
+ }
211
+ }
212
+ },
213
+ {
214
+ "instance_id": "tunnel-planner-v3",
215
+ "scenario_input": {
216
+ "scenario_input_type": "input_set",
217
+ "scenario_input_data": {
218
+ "input_id": "warren-zone-b",
219
+ "input_set_id": "burrow-sites"
220
+ }
221
+ }
222
+ }
223
+ ]'
224
+ nextmv cloud scenario create --app-id hare-app --name "Warren Construction Plans" \\
225
+ --scenarios "$SCENARIOS"[/dim]
226
+
227
+ - Create a scenario test and wait for it to complete.
228
+ $ [dim]SCENARIO='{
229
+ "instance_id": "foraging-route",
230
+ "scenario_input": {
231
+ "scenario_input_type": "input_set",
232
+ "scenario_input_data": {
233
+ "input_id": "carrot-harvest",
234
+ "input_set_id": "harvest-season"
235
+ }
236
+ }
237
+ }'
238
+ nextmv cloud scenario create --app-id hare-app --name "Autumn Carrot Collection" --scenarios "$SCENARIO" \\
239
+ --wait[/dim]
240
+
241
+ - Create a scenario test and save the results to a file, waiting for completion.
242
+ $ [dim]SCENARIO='{
243
+ "instance_id": "safe-hopper",
244
+ "scenario_input": {
245
+ "scenario_input_type": "input_set",
246
+ "scenario_input_data": {
247
+ "input_id": "predator-zones",
248
+ "input_set_id": "danger-zones"
249
+ }
250
+ }
251
+ }'
252
+ nextmv cloud scenario create --app-id hare-app --name "Fox Avoidance Routes" --scenarios "$SCENARIO" \\
253
+ --output bunny-safety-results.json[/dim]
254
+
255
+ - Create a scenario test with configuration options.
256
+ $ [dim]SCENARIO='{
257
+ "instance_id": "hop-optimizer",
258
+ "scenario_input": {
259
+ "scenario_input_type": "input_set",
260
+ "scenario_input_data": {
261
+ "input_id": "garden-route-1",
262
+ "input_set_id": "garden-paths"
263
+ }
264
+ },
265
+ "configuration": [
266
+ {
267
+ "name": "speed",
268
+ "values": ["fast", "careful"]
269
+ }
270
+ ]
271
+ }'
272
+ nextmv cloud scenario create --app-id hare-app --name "Speed Analysis" --scenarios "$SCENARIO"[/dim]
273
+ """
274
+
275
+ cloud_app = build_app(app_id=app_id, profile=profile)
276
+
277
+ # Build the scenario list from the CLI options
278
+ scenario_list = build_scenarios(scenarios)
279
+
280
+ scenario_id = cloud_app.new_scenario_test(
281
+ scenarios=scenario_list,
282
+ id=scenario_test_id,
283
+ name=name,
284
+ description=description,
285
+ repetitions=repetitions,
286
+ )
287
+
288
+ # If we don't need to poll at all we are done.
289
+ if not wait and (output is None or output == ""):
290
+ print_json({"scenario_test_id": scenario_id})
291
+
292
+ return
293
+
294
+ success(f"Scenario test [magenta]{scenario_id}[/magenta] created.")
295
+
296
+ # Build the polling options.
297
+ polling_options = default_polling_options()
298
+ polling_options.max_duration = timeout
299
+
300
+ in_progress(msg="Getting scenario test results...")
301
+ scenario_test = cloud_app.scenario_test_with_polling(
302
+ scenario_test_id=scenario_id,
303
+ polling_options=polling_options,
304
+ )
305
+ scenario_test_dict = scenario_test.to_dict()
306
+
307
+ # Handle output
308
+ if output is not None and output != "":
309
+ with open(output, "w") as f:
310
+ json.dump(scenario_test_dict, f, indent=2)
311
+
312
+ success(msg=f"Scenario test output saved to [magenta]{output}[/magenta].")
313
+
314
+ return
315
+
316
+ print_json(scenario_test_dict)
317
+
318
+
319
+ def build_scenarios(scenarios: list[str]) -> list[Scenario]:
320
+ """
321
+ Build a list of Scenario objects from CLI JSON input.
322
+
323
+ Parameters
324
+ ----------
325
+ scenarios : list[str]
326
+ List of scenario JSON strings provided via the CLI. Each string can be
327
+ a single scenario object or a list of scenario objects.
328
+
329
+ Returns
330
+ -------
331
+ list[Scenario]
332
+ The built list of Scenario objects.
333
+ """
334
+
335
+ scenario_list = []
336
+
337
+ for scenario_str in scenarios:
338
+ try:
339
+ scenario_data = json.loads(scenario_str)
340
+
341
+ # Handle the case where the value is a list of scenarios.
342
+ if isinstance(scenario_data, list):
343
+ scenario_list.extend(_process_scenario_list(scenario_data, scenario_str))
344
+
345
+ # Handle the case where the value is a single scenario.
346
+ elif isinstance(scenario_data, dict):
347
+ scenario_list.append(_process_single_scenario(scenario_data, scenario_str))
348
+
349
+ else:
350
+ error(
351
+ f"Invalid scenario format: [magenta]{scenario_str}[/magenta]. "
352
+ "Expected [magenta]json[/magenta] object or array."
353
+ )
354
+
355
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
356
+ error(f"Invalid scenario format: [magenta]{scenario_str}[/magenta]. Error: {e}")
357
+
358
+ return scenario_list
359
+
360
+
361
+ def _process_scenario_list(scenario_data: list, scenario_str: str) -> list[Scenario]:
362
+ """
363
+ Process a list of scenario dictionaries into Scenario objects.
364
+
365
+ Parameters
366
+ ----------
367
+ scenario_data : list
368
+ List of scenario dictionaries.
369
+ scenario_str : str
370
+ Original string for error messages.
371
+
372
+ Returns
373
+ -------
374
+ list[Scenario]
375
+ List of processed Scenario objects.
376
+ """
377
+
378
+ processed_scenarios = []
379
+ for ix, item in enumerate(scenario_data):
380
+ _validate_scenario_fields(item, scenario_str, ix)
381
+ scenario = Scenario.from_dict(item)
382
+ processed_scenarios.append(scenario)
383
+
384
+ return processed_scenarios
385
+
386
+
387
+ def _process_single_scenario(scenario_data: dict, scenario_str: str) -> "Scenario":
388
+ """
389
+ Process a single scenario dictionary into a Scenario object.
390
+
391
+ Parameters
392
+ ----------
393
+ scenario_data : dict
394
+ Scenario dictionary.
395
+ scenario_str : str
396
+ Original string for error messages.
397
+
398
+ Returns
399
+ -------
400
+ Scenario
401
+ Processed Scenario object.
402
+ """
403
+
404
+ _validate_scenario_fields(scenario_data, scenario_str)
405
+
406
+ return Scenario.from_dict(scenario_data)
407
+
408
+
409
+ def _validate_scenario_fields(scenario_data: dict, scenario_str: str, index: int | None = None) -> None:
410
+ """
411
+ Validate that required fields are present in a scenario dictionary.
412
+
413
+ Parameters
414
+ ----------
415
+ scenario_data : dict
416
+ Scenario dictionary to validate.
417
+ scenario_str : str
418
+ Original string for error messages.
419
+ index : int | None
420
+ Index in array if validating array element (for error reporting).
421
+ """
422
+ location = f"at index [magenta]{index}[/magenta] in " if index is not None else "in "
423
+
424
+ if scenario_data.get("instance_id") is None:
425
+ error(
426
+ f"Invalid scenario format {location}"
427
+ f"[magenta]{scenario_str}[/magenta]. Each scenario must have an "
428
+ "[magenta]instance_id[/magenta] field."
429
+ )
430
+
431
+ scenario_input = scenario_data.get("scenario_input")
432
+ if scenario_input is None:
433
+ error(
434
+ f"Invalid scenario format {location}"
435
+ f"[magenta]{scenario_str}[/magenta]. Each scenario must have a "
436
+ "[magenta]scenario_input[/magenta] field."
437
+ )
438
+
439
+ if scenario_input.get("scenario_input_type") is None:
440
+ error(
441
+ f"Invalid scenario format {location}"
442
+ f"[magenta]{scenario_str}[/magenta]. Each [magenta]scenario_input[/magenta] must have a "
443
+ "[magenta]scenario_input_type[/magenta] field."
444
+ )
445
+
446
+ if scenario_input.get("scenario_input_data") is None:
447
+ error(
448
+ f"Invalid scenario format {location}"
449
+ f"[magenta]{scenario_str}[/magenta]. Each [magenta]scenario_input[/magenta] must have a "
450
+ "[magenta]scenario_input_data[/magenta] field."
451
+ )
@@ -0,0 +1,61 @@
1
+ """
2
+ This module defines the cloud scenario 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, ProfileOption, ScenarioTestIDOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def delete(
20
+ app_id: AppIDOption,
21
+ scenario_test_id: ScenarioTestIDOption,
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 scenario test.
34
+
35
+ This action is permanent and cannot be undone. The scenario test and all
36
+ associated data will be deleted. Use the --yes flag to skip
37
+ the confirmation prompt.
38
+
39
+ [bold][underline]Examples[/underline][/bold]
40
+
41
+ - Delete the scenario test with the ID [magenta]hop-analysis[/magenta] from application
42
+ [magenta]hare-app[/magenta].
43
+ $ [dim]nextmv cloud scenario delete --app-id hare-app --scenario-test-id hop-analysis[/dim]
44
+
45
+ - Delete the scenario test without confirmation prompt.
46
+ $ [dim]nextmv cloud scenario delete --app-id hare-app --scenario-test-id carrot-routes --yes[/dim]
47
+ """
48
+
49
+ if not yes:
50
+ confirm = get_confirmation(
51
+ f"Are you sure you want to delete scenario test [magenta]{scenario_test_id}[/magenta] "
52
+ f"from application [magenta]{app_id}[/magenta]? This action cannot be undone.",
53
+ )
54
+
55
+ if not confirm:
56
+ info(f"Scenario test [magenta]{scenario_test_id}[/magenta] will not be deleted.")
57
+ return
58
+
59
+ cloud_app = build_app(app_id=app_id, profile=profile)
60
+ cloud_app.delete_scenario_test(scenario_test_id=scenario_test_id)
61
+ success(msg=f"Scenario test [magenta]{scenario_test_id}[/magenta] deleted.")
@@ -0,0 +1,102 @@
1
+ """
2
+ This module defines the cloud scenario 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, ProfileOption, ScenarioTestIDOption
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
+ scenario_test_id: ScenarioTestIDOption,
23
+ output: Annotated[
24
+ str | None,
25
+ typer.Option(
26
+ "--output",
27
+ "-o",
28
+ help="Waits for the scenario test 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 scenario test 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 scenario test, including its runs.
52
+
53
+ Use the --wait flag to wait for the scenario test 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 scenario test with ID [magenta]carrot-optimization[/magenta] from application
61
+ [magenta]hare-app[/magenta].
62
+ $ [dim]nextmv cloud scenario get --app-id hare-app --scenario-test-id carrot-optimization[/dim]
63
+
64
+ - Get the scenario test and wait for it to complete if necessary.
65
+ $ [dim]nextmv cloud scenario get --app-id hare-app --scenario-test-id bunny-hop-test --wait[/dim]
66
+
67
+ - Get the scenario test and save the results to a file.
68
+ $ [dim]nextmv cloud scenario get --app-id hare-app --scenario-test-id warren-planning \\
69
+ --output results.json[/dim]
70
+
71
+ - Get the scenario test using a specific profile.
72
+ $ [dim]nextmv cloud scenario get --app-id hare-app --scenario-test-id lettuce-routes --profile prod[/dim]
73
+ """
74
+ cloud_app = build_app(app_id=app_id, profile=profile)
75
+
76
+ # Build the polling options.
77
+ polling_options = default_polling_options()
78
+ polling_options.max_duration = timeout
79
+
80
+ # Determine if we should wait
81
+ should_wait = wait or (output is not None and output != "")
82
+
83
+ in_progress(msg="Getting scenario test...")
84
+ if should_wait:
85
+ scenario_test = cloud_app.scenario_test_with_polling(
86
+ scenario_test_id=scenario_test_id,
87
+ polling_options=polling_options,
88
+ )
89
+ else:
90
+ scenario_test = cloud_app.scenario_test(scenario_test_id=scenario_test_id)
91
+
92
+ scenario_test_dict = scenario_test.to_dict()
93
+
94
+ if output is not None and output != "":
95
+ with open(output, "w") as f:
96
+ json.dump(scenario_test_dict, f, indent=2)
97
+
98
+ success(msg=f"Scenario test results saved to [magenta]{output}[/magenta].")
99
+
100
+ return
101
+
102
+ print_json(scenario_test_dict)
@@ -0,0 +1,63 @@
1
+ """
2
+ This module defines the cloud scenario 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 scenario tests to this location.",
27
+ metavar="OUTPUT_PATH",
28
+ ),
29
+ ] = None,
30
+ profile: ProfileOption = None,
31
+ ) -> None:
32
+ """
33
+ List all Nextmv Cloud scenario tests for an application.
34
+
35
+ This command retrieves all scenario tests associated with the specified
36
+ application.
37
+
38
+ [bold][underline]Examples[/underline][/bold]
39
+
40
+ - List all scenario tests for application [magenta]hare-app[/magenta].
41
+ $ [dim]nextmv cloud scenario list --app-id hare-app[/dim]
42
+
43
+ - List all scenario tests and save to a file.
44
+ $ [dim]nextmv cloud scenario list --app-id hare-app --output scenario_tests.json[/dim]
45
+
46
+ - List all scenario tests using a specific profile.
47
+ $ [dim]nextmv cloud scenario list --app-id hare-app --profile prod[/dim]
48
+ """
49
+
50
+ cloud_app = build_app(app_id=app_id, profile=profile)
51
+ in_progress(msg="Listing scenario tests...")
52
+ scenario_tests = cloud_app.list_scenario_tests()
53
+ scenario_tests_dict = [exp.to_dict() for exp in scenario_tests]
54
+
55
+ if output is not None and output != "":
56
+ with open(output, "w") as f:
57
+ json.dump(scenario_tests_dict, f, indent=2)
58
+
59
+ success(msg=f"Scenario tests list saved to [magenta]{output}[/magenta].")
60
+
61
+ return
62
+
63
+ print_json(scenario_tests_dict)