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,27 @@
1
+ """
2
+ This module defines the cloud acceptance command tree for the Nextmv CLI.
3
+ """
4
+
5
+ import typer
6
+
7
+ from nextmv.cli.cloud.acceptance.create import app as create_app
8
+ from nextmv.cli.cloud.acceptance.delete import app as delete_app
9
+ from nextmv.cli.cloud.acceptance.get import app as get_app
10
+ from nextmv.cli.cloud.acceptance.list import app as list_app
11
+ from nextmv.cli.cloud.acceptance.update import app as update_app
12
+
13
+ # Set up subcommand application.
14
+ app = typer.Typer()
15
+ app.add_typer(create_app)
16
+ app.add_typer(delete_app)
17
+ app.add_typer(get_app)
18
+ app.add_typer(list_app)
19
+ app.add_typer(update_app)
20
+
21
+
22
+ @app.callback()
23
+ def callback() -> None:
24
+ """
25
+ Create and manage Nextmv Cloud acceptance tests.
26
+ """
27
+ pass
@@ -0,0 +1,391 @@
1
+ """
2
+ This module defines the cloud acceptance 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 enum_values, error, in_progress, print_json, success
12
+ from nextmv.cli.options import AppIDOption, ProfileOption
13
+ from nextmv.cloud.acceptance_test import Comparison, Metric, MetricToleranceType, MetricType, StatisticType
14
+ from nextmv.polling import default_polling_options
15
+
16
+ # Set up subcommand application.
17
+ app = typer.Typer()
18
+
19
+
20
+ @app.command(
21
+ # AVOID USING THE HELP PARAMETER WITH TYPER COMMAND DECORATOR. For
22
+ # consistency, commands should be documented using docstrings. We were
23
+ # forced to use help here to work around f-string limitations in
24
+ # docstrings.
25
+ help=f"""
26
+ Create a new Nextmv Cloud acceptance test.
27
+
28
+ The acceptance test is based on a batch experiment. If the batch experiment
29
+ with the same ID already exists, it will be reused. Otherwise, you must
30
+ provide the --input-set-id option to create a new batch experiment.
31
+
32
+ Use the --wait flag to wait for the acceptance test to complete, polling
33
+ for results. Using the --output flag will also activate waiting, and allows
34
+ you to specify a destination file for the results.
35
+
36
+ [bold][underline]Metrics[/underline][/bold]
37
+
38
+ Metrics are provided as [magenta]json[/magenta] objects using the
39
+ --metrics flag. Each metric defines how to compare the
40
+ candidate and baseline instances.
41
+
42
+ You can provide metrics in three ways:
43
+ - A single metric as a [magenta]json[/magenta] object.
44
+ - Multiple metrics by repeating the --metrics flag.
45
+ - Multiple metrics as a [magenta]json[/magenta] array in a single --metrics flag.
46
+
47
+ Each metric must have the following fields:
48
+ - [magenta]field[/magenta]: Field of the metric to measure (e.g., "solution.objective").
49
+ - [magenta]metric_type[/magenta]: Type of metric comparison. Allowed values: {enum_values(MetricType)}.
50
+ - [magenta]params[/magenta]: Parameters of the metric comparison.
51
+ - [magenta]operator[/magenta]: Comparison operator. Allowed values: {enum_values(Comparison)}.
52
+ - [magenta]tolerance[/magenta]: Tolerance for the comparison.
53
+ - [magenta]type[/magenta]: Type of tolerance. Allowed values: {enum_values(MetricToleranceType)}.
54
+ - [magenta]value[/magenta]: Tolerance value (numeric).
55
+ - [magenta]statistic[/magenta]: Statistical method. Allowed values: {enum_values(StatisticType)}.
56
+
57
+ Object format:
58
+ [dim]{{
59
+ "field": "field",
60
+ "metric_type": "type",
61
+ "params": {{
62
+ "operator": "op",
63
+ "tolerance": {{
64
+ "type": "tol_type",
65
+ "value": tol_value
66
+ }}
67
+ }},
68
+ "statistic": "statistic"
69
+ }}[/dim]
70
+
71
+ [bold][underline]Examples[/underline][/bold]
72
+
73
+ - Create an acceptance test with a single metric.
74
+ $ [dim]METRIC='{{
75
+ "field": "solution.objective",
76
+ "metric_type": "direct-comparison",
77
+ "params": {{
78
+ "operator": "lt",
79
+ "tolerance": {{"type": "relative", "value": 0.05}}
80
+ }},
81
+ "statistic": "mean"
82
+ }}'
83
+ nextmv cloud acceptance create --app-id hare-app --acceptance-test-id test-123 \\
84
+ --candidate-instance-id candidate-123 --baseline-instance-id baseline-456 \\
85
+ --metrics "$METRIC" --input-set-id input-set-123[/dim]
86
+
87
+ - Create with multiple metrics by repeating the flag.
88
+ $ [dim]METRIC1='{{
89
+ "field": "solution.objective",
90
+ "metric_type": "direct-comparison",
91
+ "params": {{
92
+ "operator": "lt",
93
+ "tolerance": {{"type": "relative", "value": 0.05}}
94
+ }},
95
+ "statistic": "mean"
96
+ }}'
97
+ METRIC2='{{
98
+ "field": "statistics.run.duration",
99
+ "metric_type": "direct-comparison",
100
+ "params": {{
101
+ "operator": "le",
102
+ "tolerance": {{"type": "absolute", "value": 1.0}}
103
+ }},
104
+ "statistic": "p95"
105
+ }}'
106
+ nextmv cloud acceptance create --app-id hare-app --acceptance-test-id test-123 \\
107
+ --candidate-instance-id candidate-123 --baseline-instance-id baseline-456 \\
108
+ --metrics "$METRIC1" --metrics "$METRIC2" --input-set-id input-set-123[/dim]
109
+
110
+ - Create with multiple metrics in a single [magenta]json[/magenta] array.
111
+ $ [dim]METRICS='[
112
+ {{
113
+ "field": "solution.objective",
114
+ "metric_type": "direct-comparison",
115
+ "params": {{
116
+ "operator": "lt",
117
+ "tolerance": {{"type": "relative", "value": 0.05}}
118
+ }},
119
+ "statistic": "mean"
120
+ }},
121
+ {{
122
+ "field": "statistics.run.duration",
123
+ "metric_type": "direct-comparison",
124
+ "params": {{
125
+ "operator": "le",
126
+ "tolerance": {{"type": "absolute", "value": 1.0}}
127
+ }},
128
+ "statistic": "p95"
129
+ }}
130
+ ]'
131
+ nextmv cloud acceptance create --app-id hare-app --acceptance-test-id test-123 \\
132
+ --candidate-instance-id candidate-123 --baseline-instance-id baseline-456 \\
133
+ --metrics "$METRICS" --input-set-id input-set-123[/dim]
134
+
135
+ - Create an acceptance test and wait for it to complete.
136
+ $ [dim]METRIC='{{
137
+ "field": "solution.objective",
138
+ "metric_type": "direct-comparison",
139
+ "params": {{
140
+ "operator": "lt",
141
+ "tolerance": {{"type": "relative", "value": 0.05}}
142
+ }},
143
+ "statistic": "mean"
144
+ }}'
145
+ nextmv cloud acceptance create --app-id hare-app --acceptance-test-id test-123 \\
146
+ --candidate-instance-id candidate-123 --baseline-instance-id baseline-456 \\
147
+ --metrics "$METRIC" --input-set-id input-set-123 --wait[/dim]
148
+
149
+ - Create an acceptance test and save the results to a file, waiting for completion.
150
+ $ [dim]METRIC='{{
151
+ "field": "solution.objective",
152
+ "metric_type": "direct-comparison",
153
+ "params": {{
154
+ "operator": "lt",
155
+ "tolerance": {{"type": "relative", "value": 0.05}}
156
+ }},
157
+ "statistic": "mean"
158
+ }}'
159
+ nextmv cloud acceptance create --app-id hare-app --acceptance-test-id test-123 \\
160
+ --candidate-instance-id candidate-123 --baseline-instance-id baseline-456 \\
161
+ --metrics "$METRIC" --input-set-id input-set-123 --output results.json[/dim]
162
+ """
163
+ )
164
+ def create(
165
+ app_id: AppIDOption,
166
+ # Options for acceptance test configuration.
167
+ acceptance_test_id: Annotated[
168
+ str,
169
+ typer.Option(
170
+ "--acceptance-test-id",
171
+ "-t",
172
+ help="ID for the acceptance test.",
173
+ envvar="NEXTMV_ACCEPTANCE_TEST_ID",
174
+ metavar="ACCEPTANCE_TEST_ID",
175
+ rich_help_panel="Acceptance test configuration",
176
+ ),
177
+ ],
178
+ baseline_instance_id: Annotated[
179
+ str,
180
+ typer.Option(
181
+ "--baseline-instance-id",
182
+ "-b",
183
+ help="ID of the baseline instance to compare against.",
184
+ metavar="BASELINE_INSTANCE_ID",
185
+ rich_help_panel="Acceptance test configuration",
186
+ ),
187
+ ],
188
+ candidate_instance_id: Annotated[
189
+ str,
190
+ typer.Option(
191
+ "--candidate-instance-id",
192
+ "-c",
193
+ help="ID of the candidate instance to test.",
194
+ metavar="CANDIDATE_INSTANCE_ID",
195
+ rich_help_panel="Acceptance test configuration",
196
+ ),
197
+ ],
198
+ metrics: Annotated[
199
+ list[str],
200
+ typer.Option(
201
+ "--metrics",
202
+ "-m",
203
+ help="Metrics to use for the acceptance test. Data should be valid [magenta]json[/magenta]. "
204
+ "Pass multiple metrics by repeating the flag, or providing a list of objects. "
205
+ "See command help for details on metric formatting.",
206
+ metavar="METRICS",
207
+ rich_help_panel="Acceptance test configuration",
208
+ ),
209
+ ],
210
+ description: Annotated[
211
+ str | None,
212
+ typer.Option(
213
+ "--description",
214
+ "-d",
215
+ help="Description of the acceptance test.",
216
+ metavar="DESCRIPTION",
217
+ rich_help_panel="Acceptance test configuration",
218
+ ),
219
+ ] = None,
220
+ input_set_id: Annotated[
221
+ str | None,
222
+ typer.Option(
223
+ "--input-set-id",
224
+ "-i",
225
+ help="ID of the input set to use for the underlying batch experiment. "
226
+ "Required if the batch experiment does not exist yet.",
227
+ metavar="INPUT_SET_ID",
228
+ rich_help_panel="Acceptance test configuration",
229
+ ),
230
+ ] = None,
231
+ name: Annotated[
232
+ str | None,
233
+ typer.Option(
234
+ "--name",
235
+ "-n",
236
+ help="Name of the acceptance test. If not provided, the ID will be used as the name.",
237
+ metavar="NAME",
238
+ rich_help_panel="Acceptance test configuration",
239
+ ),
240
+ ] = None,
241
+ # Options for controlling output.
242
+ output: Annotated[
243
+ str | None,
244
+ typer.Option(
245
+ "--output",
246
+ "-o",
247
+ help="Waits for the test to complete and saves the results to this location.",
248
+ metavar="OUTPUT_PATH",
249
+ rich_help_panel="Output control",
250
+ ),
251
+ ] = None,
252
+ timeout: Annotated[
253
+ int,
254
+ typer.Option(
255
+ help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
256
+ metavar="TIMEOUT_SECONDS",
257
+ rich_help_panel="Output control",
258
+ ),
259
+ ] = -1,
260
+ wait: Annotated[
261
+ bool,
262
+ typer.Option(
263
+ "--wait",
264
+ "-w",
265
+ help="Wait for the acceptance test to complete. Results are printed to [magenta]stdout[/magenta]. "
266
+ "Specify output location with --output.",
267
+ rich_help_panel="Output control",
268
+ ),
269
+ ] = False,
270
+ profile: ProfileOption = None,
271
+ ) -> None:
272
+ cloud_app = build_app(app_id=app_id, profile=profile)
273
+
274
+ # Build the metrics list from the CLI options
275
+ metrics_list = build_metrics(metrics)
276
+
277
+ new_test = cloud_app.new_acceptance_test(
278
+ candidate_instance_id=candidate_instance_id,
279
+ baseline_instance_id=baseline_instance_id,
280
+ id=acceptance_test_id,
281
+ metrics=metrics_list,
282
+ name=name,
283
+ input_set_id=input_set_id,
284
+ description=description,
285
+ )
286
+ acceptance_id = new_test.id
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({"acceptance_test_id": acceptance_id})
291
+
292
+ return
293
+
294
+ success(f"Acceptance test [magenta]{acceptance_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 acceptance test results...")
301
+ acceptance_test = cloud_app.acceptance_test_with_polling(
302
+ acceptance_test_id=acceptance_id,
303
+ polling_options=polling_options,
304
+ )
305
+ acceptance_test_dict = acceptance_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(acceptance_test_dict, f, indent=2)
311
+
312
+ success(msg=f"Acceptance test results saved to [magenta]{output}[/magenta].")
313
+
314
+ return
315
+
316
+ print_json(acceptance_test_dict)
317
+
318
+
319
+ def build_metrics(metrics: list[str]) -> list[Metric]:
320
+ """
321
+ Builds the metrics list from the CLI option(s).
322
+
323
+ Parameters
324
+ ----------
325
+ metrics : list[str]
326
+ List of metrics provided via the CLI.
327
+
328
+ Returns
329
+ -------
330
+ list[Metric]
331
+ The built metrics list.
332
+ """
333
+ metrics_list = []
334
+
335
+ for metric_str in metrics:
336
+ try:
337
+ metric_data = json.loads(metric_str)
338
+
339
+ # Handle the case where the value is a list of metrics.
340
+ if isinstance(metric_data, list):
341
+ for ix, item in enumerate(metric_data):
342
+ if (
343
+ item.get("field") is None
344
+ or item.get("metric_type") is None
345
+ or item.get("params") is None
346
+ or item.get("statistic") is None
347
+ ):
348
+ error(
349
+ f"Invalid metric format at index [magenta]{ix}[/magenta] in "
350
+ f"[magenta]{metric_str}[/magenta]. Each metric must have "
351
+ "[magenta]field[/magenta], [magenta]metric_type[/magenta], "
352
+ "[magenta]params[/magenta], and [magenta]statistic[/magenta] fields."
353
+ )
354
+
355
+ metric = Metric(**item)
356
+ metrics_list.append(metric)
357
+
358
+ # Handle the case where the value is a single metric.
359
+ elif isinstance(metric_data, dict):
360
+ if (
361
+ metric_data.get("field") is None
362
+ or metric_data.get("metric_type") is None
363
+ or metric_data.get("params") is None
364
+ or metric_data.get("statistic") is None
365
+ ):
366
+ error(
367
+ f"Invalid metric format in [magenta]{metric_str}[/magenta]. "
368
+ "Each metric must have [magenta]field[/magenta], [magenta]metric_type[/magenta], "
369
+ "[magenta]params[/magenta], and [magenta]statistic[/magenta] fields."
370
+ )
371
+
372
+ metric = Metric(**metric_data)
373
+ metrics_list.append(metric)
374
+
375
+ else:
376
+ error(
377
+ f"Invalid metric format: [magenta]{metric_str}[/magenta]. "
378
+ "Expected [magenta]json[/magenta] object or array."
379
+ )
380
+
381
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
382
+ error(f"Invalid metric format: [magenta]{metric_str}[/magenta]. Error: {e}")
383
+
384
+ if not metrics_list:
385
+ error(
386
+ "No valid metrics were provided. Please specify at least one metric with "
387
+ "[magenta]field[/magenta], [magenta]metric_type[/magenta], "
388
+ "[magenta]params[/magenta], and [magenta]statistic[/magenta] fields."
389
+ )
390
+
391
+ return metrics_list
@@ -0,0 +1,64 @@
1
+ """
2
+ This module defines the cloud acceptance 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 AcceptanceTestIDOption, AppIDOption, ProfileOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def delete(
20
+ app_id: AppIDOption,
21
+ acceptance_test_id: AcceptanceTestIDOption,
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 acceptance test.
34
+
35
+ This action is permanent and cannot be undone. The underlying batch
36
+ experiment and associated data will also be deleted. Use the --yes flag to
37
+ skip the confirmation prompt.
38
+
39
+ [bold][underline]Examples[/underline][/bold]
40
+
41
+ - Delete the acceptance test with the ID [magenta]test-cotton-tail[/magenta] from application
42
+ [magenta]hare-app[/magenta].
43
+ $ [dim]nextmv cloud acceptance delete --app-id hare-app --acceptance-test-id test-cotton-tail[/dim]
44
+
45
+ - Delete the acceptance test without confirmation prompt.
46
+ $ [dim]nextmv cloud acceptance delete --app-id hare-app --acceptance-test-id test-cotton-tail --yes[/dim]
47
+ """
48
+
49
+ if not yes:
50
+ confirm = get_confirmation(
51
+ f"Are you sure you want to delete acceptance test [magenta]{acceptance_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"Acceptance test [magenta]{acceptance_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_acceptance_test(acceptance_test_id=acceptance_test_id)
61
+ success(
62
+ f"Acceptance test [magenta]{acceptance_test_id}[/magenta] deleted successfully "
63
+ f"from application [magenta]{app_id}[/magenta]."
64
+ )
@@ -0,0 +1,103 @@
1
+ """
2
+ This module defines the cloud acceptance 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 AcceptanceTestIDOption, AppIDOption, 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
+ acceptance_test_id: AcceptanceTestIDOption,
23
+ output: Annotated[
24
+ str | None,
25
+ typer.Option(
26
+ "--output",
27
+ "-o",
28
+ help="Waits for the acceptance 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 acceptance 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 acceptance test.
52
+
53
+ Use the --wait flag to wait for the acceptance test to complete, polling
54
+ for results. Using the --output flag will also activate waiting, and allows
55
+ you to specify a destination file for the results.
56
+
57
+ [bold][underline]Examples[/underline][/bold]
58
+
59
+ - Get the acceptance test with ID [magenta]test-123[/magenta] from application
60
+ [magenta]hare-app[/magenta].
61
+ $ [dim]nextmv cloud acceptance get --app-id hare-app --acceptance-test-id test-123[/dim]
62
+
63
+ - Get the acceptance test and wait for it to complete if necessary.
64
+ $ [dim]nextmv cloud acceptance get --app-id hare-app --acceptance-test-id test-123 --wait[/dim]
65
+
66
+ - Get the acceptance test and save the results to a file.
67
+ $ [dim]nextmv cloud acceptance get --app-id hare-app \\
68
+ --acceptance-test-id test-123 --output results.json[/dim]
69
+
70
+ - Get the acceptance test using a specific profile.
71
+ $ [dim]nextmv cloud acceptance get --app-id hare-app --acceptance-test-id test-123 --profile prod[/dim]
72
+ """
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 acceptance test...")
84
+ if should_wait:
85
+ acceptance_test = cloud_app.acceptance_test_with_polling(
86
+ acceptance_test_id=acceptance_test_id,
87
+ polling_options=polling_options,
88
+ )
89
+ else:
90
+ acceptance_test = cloud_app.acceptance_test(acceptance_test_id=acceptance_test_id)
91
+
92
+ acceptance_test_dict = acceptance_test.to_dict()
93
+
94
+ # Handle output
95
+ if output is not None and output != "":
96
+ with open(output, "w") as f:
97
+ json.dump(acceptance_test_dict, f, indent=2)
98
+
99
+ success(msg=f"Acceptance test results saved to [magenta]{output}[/magenta].")
100
+
101
+ return
102
+
103
+ print_json(acceptance_test_dict)
@@ -0,0 +1,62 @@
1
+ """
2
+ This module defines the cloud acceptance 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 acceptance tests to this location.",
27
+ metavar="OUTPUT_PATH",
28
+ ),
29
+ ] = None,
30
+ profile: ProfileOption = None,
31
+ ) -> None:
32
+ """
33
+ List all Nextmv Cloud acceptance tests for an application.
34
+
35
+ This command retrieves all acceptance tests associated with the specified
36
+ application.
37
+
38
+ [bold][underline]Examples[/underline][/bold]
39
+
40
+ - List all acceptance tests for application [magenta]hare-app[/magenta].
41
+ $ [dim]nextmv cloud acceptance list --app-id hare-app[/dim]
42
+
43
+ - List all acceptance tests and save to a file.
44
+ $ [dim]nextmv cloud acceptance list --app-id hare-app --output tests.json[/dim]
45
+
46
+ - List all acceptance tests using a specific profile.
47
+ $ [dim]nextmv cloud acceptance 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 acceptance tests...")
52
+ acceptance_tests = cloud_app.list_acceptance_tests()
53
+ acceptance_tests_dict = [test.to_dict() for test in acceptance_tests]
54
+
55
+ if output is not None and output != "":
56
+ with open(output, "w") as f:
57
+ json.dump(acceptance_tests_dict, f, indent=2)
58
+
59
+ success(msg=f"Acceptance tests list saved to [magenta]{output}[/magenta].")
60
+ return
61
+
62
+ print_json(acceptance_tests_dict)