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,413 @@
1
+ """
2
+ This module defines the cloud ensemble create 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.message import enum_values, error, in_progress, print_json
11
+ from nextmv.cli.options import AppIDOption, ProfileOption
12
+ from nextmv.cloud.ensemble import EvaluationRule, RuleObjective, RuleTolerance, RuleToleranceType, RunGroup
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command(
19
+ # AVOID USING THE HELP PARAMETER WITH TYPER COMMAND DECORATOR. For
20
+ # consistency, commands should be documented using docstrings. We were
21
+ # forced to use help here to work around f-string limitations in
22
+ # docstrings.
23
+ help=f"""
24
+ Create a new Nextmv Cloud ensemble definition.
25
+
26
+ An ensemble definition coordinates the execution of multiple child runs for
27
+ an application and determines the optimal result from those runs. Each
28
+ ensemble definition contains run groups and evaluation rules.
29
+
30
+ [bold][underline]Run Groups[/underline][/bold]
31
+
32
+ Run groups are provided as [magenta]json[/magenta] objects using the
33
+ --run-groups flag. Each run group specifies how child runs are executed.
34
+
35
+ You can provide run groups in three ways:
36
+ - A single run group as a [magenta]json[/magenta] object.
37
+ - Multiple run groups by repeating the --run-groups flag.
38
+ - Multiple run groups as a [magenta]json[/magenta] array in a single --run-groups flag.
39
+
40
+ Each run group must have the following fields:
41
+ - [magenta]id[/magenta]: Unique identifier for the run group (required).
42
+ - [magenta]instance_id[/magenta]: The instance to execute runs on (required).
43
+ - [magenta]options[/magenta]: Runtime options/parameters (optional). Options should be provided as a
44
+ [magenta]json[/magenta] object with [magenta]string[/magenta] key-value pairs.
45
+ - [magenta]repetitions[/magenta]: Number of times to repeat the run (optional).
46
+
47
+ Object format:
48
+ [dim]{{
49
+ "id": "rg1",
50
+ "instance_id": "inst-123",
51
+ "options": {{"param": "value"}},
52
+ "repetitions": 5
53
+ }}[/dim]
54
+
55
+ [bold][underline]Evaluation Rules[/underline][/bold]
56
+
57
+ Evaluation rules are provided as [magenta]json[/magenta] objects using the
58
+ --rules flag. Each rule determines how to evaluate and select the best
59
+ result from the child runs.
60
+
61
+ You can provide rules in three ways:
62
+ - A single rule as a [magenta]json[/magenta] object.
63
+ - Multiple rules by repeating the --rules flag.
64
+ - Multiple rules as a [magenta]json[/magenta] array in a single --rules flag.
65
+
66
+ Each rule must have the following fields:
67
+ - [magenta]id[/magenta]: Unique identifier for the rule (required).
68
+ - [magenta]statistics_path[/magenta]: JSONPath to the metric (e.g., [magenta]$.result.value[/magenta]) (required).
69
+ - [magenta]objective[/magenta]: Objective for the evaluation (required).
70
+ Allowed values: {enum_values(RuleObjective)}.
71
+ - [magenta]tolerance[/magenta]: Object with the following fields (required):
72
+ - [magenta]value[/magenta]: Tolerance value (float).
73
+ - [magenta]type[/magenta]: Tolerance type. Allowed values: {enum_values(RuleToleranceType)}.
74
+ - [magenta]index[/magenta]: Evaluation order - lower indices evaluated first (required).
75
+
76
+ Object format:
77
+ [dim]{{
78
+ "id": "rule1",
79
+ "statistics_path": "$.result.value",
80
+ "objective": "minimize",
81
+ "tolerance": {{"value": 0.1, "type": "relative"}},
82
+ "index": 0
83
+ }}[/dim]
84
+
85
+ [bold][underline]Examples[/underline][/bold]
86
+
87
+ - Create an ensemble definition with a single run group and rule.
88
+ $ [dim]RUN_GROUP='{{
89
+ "id": "rg1",
90
+ "instance_id": "inst-123"
91
+ }}'
92
+ RULE='{{
93
+ "id": "rule1",
94
+ "statistics_path": "$.result.value",
95
+ "objective": "minimize",
96
+ "tolerance": {{"value": 0.1, "type": "relative"}},
97
+ "index": 0
98
+ }}'
99
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP" --rules "$RULE"[/dim]
100
+
101
+ - Create with multiple run groups by repeating the flag.
102
+ $ [dim]RUN_GROUP_1='{{
103
+ "id": "rg1",
104
+ "instance_id": "inst-123"
105
+ }}'
106
+ RUN_GROUP_2='{{
107
+ "id": "rg2",
108
+ "instance_id": "inst-456",
109
+ "options": {{"param": "value"}}
110
+ }}'
111
+ RULE='{{
112
+ "id": "rule1",
113
+ "statistics_path": "$.result.value",
114
+ "objective": "minimize",
115
+ "tolerance": {{"value": 0.1, "type": "relative"}},
116
+ "index": 0
117
+ }}'
118
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP_1" --run-groups "$RUN_GROUP_2" \\
119
+ --rules "$RULE"[/dim]
120
+
121
+ - Create with multiple items in a single JSON array.
122
+ $ [dim]RUN_GROUPS='[
123
+ {{"id": "rg1", "instance_id": "inst-123"}},
124
+ {{"id": "rg2", "instance_id": "inst-456"}}
125
+ ]'
126
+ RULES='[{{
127
+ "id": "rule1",
128
+ "statistics_path": "$.result.value",
129
+ "objective": "minimize",
130
+ "tolerance": {{"value": 0.1, "type": "relative"}},
131
+ "index": 0
132
+ }}]'
133
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUPS" --rules "$RULES"[/dim]
134
+
135
+ - Create with custom ID, name, and description.
136
+ $ [dim]RUN_GROUP='{{
137
+ "id": "rg1",
138
+ "instance_id": "inst-123"
139
+ }}'
140
+ RULE='{{
141
+ "id": "rule1",
142
+ "statistics_path": "$.result.value",
143
+ "objective": "minimize",
144
+ "tolerance": {{"value": 0.1, "type": "relative"}},
145
+ "index": 0
146
+ }}'
147
+ nextmv cloud ensemble create --app-id hare-app \\
148
+ --ensemble-definition-id prod-ensemble --name "Production Ensemble" \\
149
+ --description "Production ensemble with multiple solvers" \\
150
+ --run-groups "$RUN_GROUP" --rules "$RULE"[/dim]
151
+
152
+ - Create with run group repetitions.
153
+ $ [dim]RUN_GROUP='{{
154
+ "id": "rg1",
155
+ "instance_id": "inst-123",
156
+ "repetitions": 5
157
+ }}'
158
+ RULE='{{
159
+ "id": "rule1",
160
+ "statistics_path": "$.result.value",
161
+ "objective": "minimize",
162
+ "tolerance": {{"value": 0.1, "type": "relative"}},
163
+ "index": 0
164
+ }}'
165
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP" --rules "$RULE"[/dim]
166
+ """
167
+ )
168
+ def create(
169
+ app_id: AppIDOption,
170
+ run_groups: Annotated[
171
+ list[str],
172
+ typer.Option(
173
+ "--run-groups",
174
+ "-r",
175
+ help="Run groups to configure for the ensemble. Data should be valid [magenta]json[/magenta]. "
176
+ "Pass multiple run groups by repeating the flag, or providing a list of objects. "
177
+ "See command help for details on run group formatting.",
178
+ metavar="RUN_GROUPS",
179
+ ),
180
+ ],
181
+ rules: Annotated[
182
+ list[str],
183
+ typer.Option(
184
+ "--rules",
185
+ "-u",
186
+ help="Evaluation rules to configure for the ensemble. Data should be valid [magenta]json[/magenta]. "
187
+ "Pass multiple rules by repeating the flag, or providing a list of objects. "
188
+ "See command help for details on rule formatting.",
189
+ metavar="RULES",
190
+ ),
191
+ ],
192
+ description: Annotated[
193
+ str | None,
194
+ typer.Option(
195
+ "--description",
196
+ "-d",
197
+ help="An optional description for the ensemble definition.",
198
+ metavar="DESCRIPTION",
199
+ ),
200
+ ] = None,
201
+ name: Annotated[
202
+ str | None,
203
+ typer.Option(
204
+ "--name",
205
+ "-n",
206
+ help="A name for the ensemble definition.",
207
+ metavar="NAME",
208
+ ),
209
+ ] = None,
210
+ ensemble_definition_id: Annotated[
211
+ str | None,
212
+ typer.Option(
213
+ "--ensemble-definition-id",
214
+ "-e",
215
+ help="The ID to assign to the new ensemble definition. If not provided, a random ID will be generated.",
216
+ envvar="NEXTMV_ENSEMBLE_DEFINITION_ID",
217
+ metavar="ENSEMBLE_DEFINITION_ID",
218
+ ),
219
+ ] = None,
220
+ profile: ProfileOption = None,
221
+ ) -> None:
222
+ cloud_app = build_app(app_id=app_id, profile=profile)
223
+ in_progress(msg="Creating ensemble definition...")
224
+
225
+ # Build the run groups and rules lists from the CLI options
226
+ run_groups_list = build_run_groups(run_groups)
227
+ rules_list = build_rules(rules)
228
+
229
+ ensemble_definition = cloud_app.new_ensemble_definition(
230
+ run_groups=run_groups_list,
231
+ rules=rules_list,
232
+ id=ensemble_definition_id,
233
+ name=name,
234
+ description=description,
235
+ )
236
+ print_json(ensemble_definition.to_dict())
237
+
238
+
239
+ def build_run_groups(run_groups: list[str]) -> list[RunGroup]:
240
+ """
241
+ Builds the run groups list from the CLI option(s).
242
+
243
+ Parameters
244
+ ----------
245
+ run_groups : list[str]
246
+ List of run groups provided via the CLI.
247
+
248
+ Returns
249
+ -------
250
+ list[RunGroup]
251
+ The built run groups list.
252
+ """
253
+ import json
254
+
255
+ run_groups_list = []
256
+
257
+ for run_group_str in run_groups:
258
+ try:
259
+ run_group_data = json.loads(run_group_str)
260
+
261
+ # Handle the case where the value is a list of run groups.
262
+ if isinstance(run_group_data, list):
263
+ for ix, item in enumerate(run_group_data):
264
+ if item.get("id") is None or item.get("instance_id") is None:
265
+ error(
266
+ f"Invalid run group format at index [magenta]{ix}[/magenta] in "
267
+ f"[magenta]{run_group_str}[/magenta]. Each run group must have "
268
+ "[magenta]id[/magenta] and [magenta]instance_id[/magenta] fields."
269
+ )
270
+
271
+ run_group = RunGroup(
272
+ id=item["id"],
273
+ instance_id=item["instance_id"],
274
+ options=item.get("options"),
275
+ repetitions=item.get("repetitions"),
276
+ )
277
+ run_groups_list.append(run_group)
278
+
279
+ # Handle the case where the value is a single run group.
280
+ elif isinstance(run_group_data, dict):
281
+ if run_group_data.get("id") is None or run_group_data.get("instance_id") is None:
282
+ error(
283
+ f"Invalid run group format in [magenta]{run_group_str}[/magenta]. "
284
+ "Each run group must have [magenta]id[/magenta] and [magenta]instance_id[/magenta] fields."
285
+ )
286
+
287
+ run_group = RunGroup(
288
+ id=run_group_data["id"],
289
+ instance_id=run_group_data["instance_id"],
290
+ options=run_group_data.get("options"),
291
+ repetitions=run_group_data.get("repetitions"),
292
+ )
293
+ run_groups_list.append(run_group)
294
+
295
+ else:
296
+ error(
297
+ f"Invalid run group format: [magenta]{run_group_str}[/magenta]. "
298
+ "Expected [magenta]json[/magenta] object or array."
299
+ )
300
+
301
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
302
+ error(f"Invalid run group format: [magenta]{run_group_str}[/magenta]. Error: {e}")
303
+
304
+ return run_groups_list
305
+
306
+
307
+ def build_rules(rules: list[str]) -> list[EvaluationRule]:
308
+ """
309
+ Builds the rules list from the CLI option(s).
310
+
311
+ Parameters
312
+ ----------
313
+ rules : list[str]
314
+ List of rules provided via the CLI.
315
+
316
+ Returns
317
+ -------
318
+ list[EvaluationRule]
319
+ The built rules list.
320
+ """
321
+ import json
322
+
323
+ rules_list = []
324
+
325
+ for rule_str in rules:
326
+ try:
327
+ rule_data = json.loads(rule_str)
328
+
329
+ # Handle the case where the value is a list of rules.
330
+ if isinstance(rule_data, list):
331
+ for ix, item in enumerate(rule_data):
332
+ validate_rule_data(item, rule_str, ix)
333
+ rule = create_evaluation_rule(item)
334
+ rules_list.append(rule)
335
+
336
+ # Handle the case where the value is a single rule.
337
+ elif isinstance(rule_data, dict):
338
+ validate_rule_data(rule_data, rule_str)
339
+ rule = create_evaluation_rule(rule_data)
340
+ rules_list.append(rule)
341
+
342
+ else:
343
+ error(
344
+ f"Invalid rule format: [magenta]{rule_str}[/magenta]. "
345
+ "Expected [magenta]json[/magenta] object or array."
346
+ )
347
+
348
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
349
+ error(f"Invalid rule format: [magenta]{rule_str}[/magenta]. Error: {e}")
350
+
351
+ return rules_list
352
+
353
+
354
+ def validate_rule_data(data: dict, rule_str: str, index: int | None = None) -> None:
355
+ """
356
+ Validates that rule data contains all required fields.
357
+
358
+ Parameters
359
+ ----------
360
+ data : dict
361
+ The rule data to validate.
362
+ rule_str : str
363
+ The original rule string for error messages.
364
+ index : int | None
365
+ The index if this is part of a list, for error messages.
366
+ """
367
+ required_fields = ["id", "statistics_path", "objective", "tolerance", "index"]
368
+ missing_fields = [field for field in required_fields if data.get(field) is None]
369
+
370
+ if missing_fields:
371
+ index_msg = f" at index [magenta]{index}[/magenta]" if index is not None else ""
372
+ error(
373
+ f"Invalid rule format{index_msg} in [magenta]{rule_str}[/magenta]. "
374
+ f"Missing required fields: [magenta]{', '.join(missing_fields)}[/magenta]."
375
+ )
376
+
377
+ # Validate tolerance structure
378
+ tolerance = data.get("tolerance")
379
+ if not isinstance(tolerance, dict) or tolerance.get("value") is None or tolerance.get("type") is None:
380
+ index_msg = f" at index [magenta]{index}[/magenta]" if index is not None else ""
381
+ error(
382
+ f"Invalid tolerance format{index_msg} in [magenta]{rule_str}[/magenta]. "
383
+ "Tolerance must have [magenta]value[/magenta] and [magenta]type[/magenta] fields."
384
+ )
385
+
386
+
387
+ def create_evaluation_rule(data: dict) -> EvaluationRule:
388
+ """
389
+ Creates an EvaluationRule from validated data.
390
+
391
+ Parameters
392
+ ----------
393
+ data : dict
394
+ The validated rule data.
395
+
396
+ Returns
397
+ -------
398
+ EvaluationRule
399
+ The created evaluation rule.
400
+ """
401
+ tolerance_data = data["tolerance"]
402
+ tolerance = RuleTolerance(
403
+ value=float(tolerance_data["value"]),
404
+ type=RuleToleranceType(tolerance_data["type"]),
405
+ )
406
+
407
+ return EvaluationRule(
408
+ id=data["id"],
409
+ statistics_path=data["statistics_path"],
410
+ objective=RuleObjective(data["objective"]),
411
+ tolerance=tolerance,
412
+ index=int(data["index"]),
413
+ )
@@ -0,0 +1,63 @@
1
+ """
2
+ This module defines the cloud ensemble 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, EnsembleDefinitionIDOption, ProfileOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def delete(
20
+ app_id: AppIDOption,
21
+ ensemble_definition_id: EnsembleDefinitionIDOption,
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 ensemble definition.
34
+
35
+ This action is permanent and cannot be undone. Use the --yes
36
+ flag to skip the confirmation prompt.
37
+
38
+ [bold][underline]Examples[/underline][/bold]
39
+
40
+ - Delete the ensemble definition with the ID [magenta]prod-ensemble[/magenta] from application
41
+ [magenta]hare-app[/magenta].
42
+ $ [dim]nextmv cloud ensemble delete --app-id hare-app --ensemble-definition-id prod-ensemble[/dim]
43
+
44
+ - Delete the ensemble definition without confirmation prompt.
45
+ $ [dim]nextmv cloud ensemble delete --app-id hare-app --ensemble-definition-id prod-ensemble --yes[/dim]
46
+ """
47
+
48
+ if not yes:
49
+ confirm = get_confirmation(
50
+ f"Are you sure you want to delete ensemble definition [magenta]{ensemble_definition_id}[/magenta] "
51
+ f"from application [magenta]{app_id}[/magenta]? This action cannot be undone.",
52
+ )
53
+
54
+ if not confirm:
55
+ info(f"Ensemble definition [magenta]{ensemble_definition_id}[/magenta] will not be deleted.")
56
+ return
57
+
58
+ cloud_app = build_app(app_id=app_id, profile=profile)
59
+ cloud_app.delete_ensemble_definition(ensemble_definition_id=ensemble_definition_id)
60
+ success(
61
+ f"Ensemble definition [magenta]{ensemble_definition_id}[/magenta] deleted successfully "
62
+ f"from application [magenta]{app_id}[/magenta]."
63
+ )
@@ -0,0 +1,65 @@
1
+ """
2
+ This module defines the cloud ensemble 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, EnsembleDefinitionIDOption, ProfileOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def get(
20
+ app_id: AppIDOption,
21
+ ensemble_definition_id: EnsembleDefinitionIDOption,
22
+ output: Annotated[
23
+ str | None,
24
+ typer.Option(
25
+ "--output",
26
+ "-o",
27
+ help="Saves the ensemble definition information to this location.",
28
+ metavar="OUTPUT_PATH",
29
+ ),
30
+ ] = None,
31
+ profile: ProfileOption = None,
32
+ ) -> None:
33
+ """
34
+ Get a Nextmv Cloud ensemble definition.
35
+
36
+ This command is useful to get the attributes of an existing Nextmv Cloud
37
+ ensemble definition by its ID.
38
+
39
+ [bold][underline]Examples[/underline][/bold]
40
+
41
+ - Get the ensemble definition with the ID [magenta]prod-ensemble[/magenta] from
42
+ application [magenta]hare-app[/magenta].
43
+ $ [dim]nextmv cloud ensemble get --app-id hare-app \\
44
+ --ensemble-definition-id prod-ensemble[/dim]
45
+
46
+ - Get the ensemble definition with the ID [magenta]prod-ensemble[/magenta] and
47
+ save the information to an [magenta]ensemble.json[/magenta] file.
48
+ $ [dim]nextmv cloud ensemble get --app-id hare-app \\
49
+ --ensemble-definition-id prod-ensemble --output ensemble.json[/dim]
50
+ """
51
+
52
+ cloud_app = build_app(app_id=app_id, profile=profile)
53
+ in_progress(msg="Getting ensemble definition...")
54
+ ensemble_definition = cloud_app.ensemble_definition(ensemble_definition_id=ensemble_definition_id)
55
+ ensemble_definition_dict = ensemble_definition.to_dict()
56
+
57
+ if output is not None and output != "":
58
+ with open(output, "w") as f:
59
+ json.dump(ensemble_definition_dict, f, indent=2)
60
+
61
+ success(msg=f"Ensemble definition information saved to [magenta]{output}[/magenta].")
62
+
63
+ return
64
+
65
+ print_json(ensemble_definition_dict)
@@ -0,0 +1,63 @@
1
+ """
2
+ This module defines the cloud ensemble 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 ensemble definitions to this location.",
27
+ metavar="OUTPUT_PATH",
28
+ ),
29
+ ] = None,
30
+ profile: ProfileOption = None,
31
+ ) -> None:
32
+ """
33
+ List all Nextmv Cloud ensemble definitions for an application.
34
+
35
+ This command retrieves all ensemble definitions associated with the specified
36
+ application.
37
+
38
+ [bold][underline]Examples[/underline][/bold]
39
+
40
+ - List all ensemble definitions for application [magenta]hare-app[/magenta].
41
+ $ [dim]nextmv cloud ensemble list --app-id hare-app[/dim]
42
+
43
+ - List all ensemble definitions and save to a file.
44
+ $ [dim]nextmv cloud ensemble list --app-id hare-app --output ensembles.json[/dim]
45
+
46
+ - List all ensemble definitions using a specific profile.
47
+ $ [dim]nextmv cloud ensemble 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 ensemble definitions...")
52
+ ensembles = cloud_app.list_ensemble_definitions()
53
+ ensembles_dict = [ensemble.to_dict() for ensemble in ensembles]
54
+
55
+ if output is not None and output != "":
56
+ with open(output, "w") as f:
57
+ json.dump(ensembles_dict, f, indent=2)
58
+
59
+ success(msg=f"Ensemble definitions list saved to [magenta]{output}[/magenta].")
60
+
61
+ return
62
+
63
+ print_json(ensembles_dict)