nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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 (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,414 @@
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
+ [code]--run-groups[/code] flag. Each run group specifies how child runs
34
+ are executed.
35
+
36
+ You can provide run groups in three ways:
37
+ - A single run group as a [magenta]json[/magenta] object.
38
+ - Multiple run groups by repeating the [code]--run-groups[/code] flag.
39
+ - Multiple run groups as a [magenta]json[/magenta] array in a single [code]--run-groups[/code] flag.
40
+
41
+ Each run group must have the following fields:
42
+ - [magenta]id[/magenta]: Unique identifier for the run group (required).
43
+ - [magenta]instance_id[/magenta]: The instance to execute runs on (required).
44
+ - [magenta]options[/magenta]: Runtime options/parameters (optional). Options should be provided as a
45
+ [magenta]json[/magenta] object with [magenta]string[/magenta] key-value pairs.
46
+ - [magenta]repetitions[/magenta]: Number of times to repeat the run (optional).
47
+
48
+ Object format:
49
+ [green]{{
50
+ "id": "rg1",
51
+ "instance_id": "inst-123",
52
+ "options": {{"param": "value"}},
53
+ "repetitions": 5
54
+ }}[/green]
55
+
56
+ [bold][underline]Evaluation Rules[/underline][/bold]
57
+
58
+ Evaluation rules are provided as [magenta]json[/magenta] objects using the
59
+ [code]--rules[/code] flag. Each rule determines how to evaluate and select
60
+ the best result from the child runs.
61
+
62
+ You can provide rules in three ways:
63
+ - A single rule as a [magenta]json[/magenta] object.
64
+ - Multiple rules by repeating the [code]--rules[/code] flag.
65
+ - Multiple rules as a [magenta]json[/magenta] array in a single [code]--rules[/code] flag.
66
+
67
+ Each rule must have the following fields:
68
+ - [magenta]id[/magenta]: Unique identifier for the rule (required).
69
+ - [magenta]statistics_path[/magenta]: JSONPath to the metric (e.g., [magenta]$.result.value[/magenta]) (required).
70
+ - [magenta]objective[/magenta]: Objective for the evaluation (required).
71
+ Allowed values: {enum_values(RuleObjective)}.
72
+ - [magenta]tolerance[/magenta]: Object with the following fields (required):
73
+ - [magenta]value[/magenta]: Tolerance value (float).
74
+ - [magenta]type[/magenta]: Tolerance type. Allowed values: {enum_values(RuleToleranceType)}.
75
+ - [magenta]index[/magenta]: Evaluation order - lower indices evaluated first (required).
76
+
77
+ Object format:
78
+ [green]{{
79
+ "id": "rule1",
80
+ "statistics_path": "$.result.value",
81
+ "objective": "minimize",
82
+ "tolerance": {{"value": 0.1, "type": "relative"}},
83
+ "index": 0
84
+ }}[/green]
85
+
86
+ [bold][underline]Examples[/underline][/bold]
87
+
88
+ - Create an ensemble definition with a single run group and rule.
89
+ $ [green]RUN_GROUP='{{
90
+ "id": "rg1",
91
+ "instance_id": "inst-123"
92
+ }}'
93
+ RULE='{{
94
+ "id": "rule1",
95
+ "statistics_path": "$.result.value",
96
+ "objective": "minimize",
97
+ "tolerance": {{"value": 0.1, "type": "relative"}},
98
+ "index": 0
99
+ }}'
100
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP" --rules "$RULE"[/green]
101
+
102
+ - Create with multiple run groups by repeating the flag.
103
+ $ [green]RUN_GROUP_1='{{
104
+ "id": "rg1",
105
+ "instance_id": "inst-123"
106
+ }}'
107
+ RUN_GROUP_2='{{
108
+ "id": "rg2",
109
+ "instance_id": "inst-456",
110
+ "options": {{"param": "value"}}
111
+ }}'
112
+ RULE='{{
113
+ "id": "rule1",
114
+ "statistics_path": "$.result.value",
115
+ "objective": "minimize",
116
+ "tolerance": {{"value": 0.1, "type": "relative"}},
117
+ "index": 0
118
+ }}'
119
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP_1" --run-groups "$RUN_GROUP_2" \\
120
+ --rules "$RULE"[/green]
121
+
122
+ - Create with multiple items in a single JSON array.
123
+ $ [green]RUN_GROUPS='[
124
+ {{"id": "rg1", "instance_id": "inst-123"}},
125
+ {{"id": "rg2", "instance_id": "inst-456"}}
126
+ ]'
127
+ RULES='[{{
128
+ "id": "rule1",
129
+ "statistics_path": "$.result.value",
130
+ "objective": "minimize",
131
+ "tolerance": {{"value": 0.1, "type": "relative"}},
132
+ "index": 0
133
+ }}]'
134
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUPS" --rules "$RULES"[/green]
135
+
136
+ - Create with custom ID, name, and description.
137
+ $ [green]RUN_GROUP='{{
138
+ "id": "rg1",
139
+ "instance_id": "inst-123"
140
+ }}'
141
+ RULE='{{
142
+ "id": "rule1",
143
+ "statistics_path": "$.result.value",
144
+ "objective": "minimize",
145
+ "tolerance": {{"value": 0.1, "type": "relative"}},
146
+ "index": 0
147
+ }}'
148
+ nextmv cloud ensemble create --app-id hare-app \\
149
+ --ensemble-definition-id prod-ensemble --name "Production Ensemble" \\
150
+ --description "Production ensemble with multiple solvers" \\
151
+ --run-groups "$RUN_GROUP" --rules "$RULE"[/green]
152
+
153
+ - Create with run group repetitions.
154
+ $ [green]RUN_GROUP='{{
155
+ "id": "rg1",
156
+ "instance_id": "inst-123",
157
+ "repetitions": 5
158
+ }}'
159
+ RULE='{{
160
+ "id": "rule1",
161
+ "statistics_path": "$.result.value",
162
+ "objective": "minimize",
163
+ "tolerance": {{"value": 0.1, "type": "relative"}},
164
+ "index": 0
165
+ }}'
166
+ nextmv cloud ensemble create --app-id hare-app --run-groups "$RUN_GROUP" --rules "$RULE"[/green]
167
+ """
168
+ )
169
+ def create(
170
+ app_id: AppIDOption,
171
+ run_groups: Annotated[
172
+ list[str],
173
+ typer.Option(
174
+ "--run-groups",
175
+ "-r",
176
+ help="Run groups to configure for the ensemble. Data should be valid [magenta]json[/magenta]. "
177
+ "Pass multiple run groups by repeating the flag, or providing a list of objects. "
178
+ "See command help for details on run group formatting.",
179
+ metavar="RUN_GROUPS",
180
+ ),
181
+ ],
182
+ rules: Annotated[
183
+ list[str],
184
+ typer.Option(
185
+ "--rules",
186
+ "-u",
187
+ help="Evaluation rules to configure for the ensemble. Data should be valid [magenta]json[/magenta]. "
188
+ "Pass multiple rules by repeating the flag, or providing a list of objects. "
189
+ "See command help for details on rule formatting.",
190
+ metavar="RULES",
191
+ ),
192
+ ],
193
+ description: Annotated[
194
+ str | None,
195
+ typer.Option(
196
+ "--description",
197
+ "-d",
198
+ help="An optional description for the ensemble definition.",
199
+ metavar="DESCRIPTION",
200
+ ),
201
+ ] = None,
202
+ name: Annotated[
203
+ str | None,
204
+ typer.Option(
205
+ "--name",
206
+ "-n",
207
+ help="A name for the ensemble definition.",
208
+ metavar="NAME",
209
+ ),
210
+ ] = None,
211
+ ensemble_definition_id: Annotated[
212
+ str | None,
213
+ typer.Option(
214
+ "--ensemble-definition-id",
215
+ "-e",
216
+ help="The ID to assign to the new ensemble definition. If not provided, a random ID will be generated.",
217
+ envvar="NEXTMV_ENSEMBLE_DEFINITION_ID",
218
+ metavar="ENSEMBLE_DEFINITION_ID",
219
+ ),
220
+ ] = None,
221
+ profile: ProfileOption = None,
222
+ ) -> None:
223
+ cloud_app = build_app(app_id=app_id, profile=profile)
224
+ in_progress(msg="Creating ensemble definition...")
225
+
226
+ # Build the run groups and rules lists from the CLI options
227
+ run_groups_list = build_run_groups(run_groups)
228
+ rules_list = build_rules(rules)
229
+
230
+ ensemble_definition = cloud_app.new_ensemble_definition(
231
+ run_groups=run_groups_list,
232
+ rules=rules_list,
233
+ id=ensemble_definition_id,
234
+ name=name,
235
+ description=description,
236
+ )
237
+ print_json(ensemble_definition.to_dict())
238
+
239
+
240
+ def build_run_groups(run_groups: list[str]) -> list[RunGroup]:
241
+ """
242
+ Builds the run groups list from the CLI option(s).
243
+
244
+ Parameters
245
+ ----------
246
+ run_groups : list[str]
247
+ List of run groups provided via the CLI.
248
+
249
+ Returns
250
+ -------
251
+ list[RunGroup]
252
+ The built run groups list.
253
+ """
254
+ import json
255
+
256
+ run_groups_list = []
257
+
258
+ for run_group_str in run_groups:
259
+ try:
260
+ run_group_data = json.loads(run_group_str)
261
+
262
+ # Handle the case where the value is a list of run groups.
263
+ if isinstance(run_group_data, list):
264
+ for ix, item in enumerate(run_group_data):
265
+ if item.get("id") is None or item.get("instance_id") is None:
266
+ error(
267
+ f"Invalid run group format at index [magenta]{ix}[/magenta] in "
268
+ f"[magenta]{run_group_str}[/magenta]. Each run group must have "
269
+ "[magenta]id[/magenta] and [magenta]instance_id[/magenta] fields."
270
+ )
271
+
272
+ run_group = RunGroup(
273
+ id=item["id"],
274
+ instance_id=item["instance_id"],
275
+ options=item.get("options"),
276
+ repetitions=item.get("repetitions"),
277
+ )
278
+ run_groups_list.append(run_group)
279
+
280
+ # Handle the case where the value is a single run group.
281
+ elif isinstance(run_group_data, dict):
282
+ if run_group_data.get("id") is None or run_group_data.get("instance_id") is None:
283
+ error(
284
+ f"Invalid run group format in [magenta]{run_group_str}[/magenta]. "
285
+ "Each run group must have [magenta]id[/magenta] and [magenta]instance_id[/magenta] fields."
286
+ )
287
+
288
+ run_group = RunGroup(
289
+ id=run_group_data["id"],
290
+ instance_id=run_group_data["instance_id"],
291
+ options=run_group_data.get("options"),
292
+ repetitions=run_group_data.get("repetitions"),
293
+ )
294
+ run_groups_list.append(run_group)
295
+
296
+ else:
297
+ error(
298
+ f"Invalid run group format: [magenta]{run_group_str}[/magenta]. "
299
+ "Expected [magenta]json[/magenta] object or array."
300
+ )
301
+
302
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
303
+ error(f"Invalid run group format: [magenta]{run_group_str}[/magenta]. Error: {e}")
304
+
305
+ return run_groups_list
306
+
307
+
308
+ def build_rules(rules: list[str]) -> list[EvaluationRule]:
309
+ """
310
+ Builds the rules list from the CLI option(s).
311
+
312
+ Parameters
313
+ ----------
314
+ rules : list[str]
315
+ List of rules provided via the CLI.
316
+
317
+ Returns
318
+ -------
319
+ list[EvaluationRule]
320
+ The built rules list.
321
+ """
322
+ import json
323
+
324
+ rules_list = []
325
+
326
+ for rule_str in rules:
327
+ try:
328
+ rule_data = json.loads(rule_str)
329
+
330
+ # Handle the case where the value is a list of rules.
331
+ if isinstance(rule_data, list):
332
+ for ix, item in enumerate(rule_data):
333
+ validate_rule_data(item, rule_str, ix)
334
+ rule = create_evaluation_rule(item)
335
+ rules_list.append(rule)
336
+
337
+ # Handle the case where the value is a single rule.
338
+ elif isinstance(rule_data, dict):
339
+ validate_rule_data(rule_data, rule_str)
340
+ rule = create_evaluation_rule(rule_data)
341
+ rules_list.append(rule)
342
+
343
+ else:
344
+ error(
345
+ f"Invalid rule format: [magenta]{rule_str}[/magenta]. "
346
+ "Expected [magenta]json[/magenta] object or array."
347
+ )
348
+
349
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
350
+ error(f"Invalid rule format: [magenta]{rule_str}[/magenta]. Error: {e}")
351
+
352
+ return rules_list
353
+
354
+
355
+ def validate_rule_data(data: dict, rule_str: str, index: int | None = None) -> None:
356
+ """
357
+ Validates that rule data contains all required fields.
358
+
359
+ Parameters
360
+ ----------
361
+ data : dict
362
+ The rule data to validate.
363
+ rule_str : str
364
+ The original rule string for error messages.
365
+ index : int | None
366
+ The index if this is part of a list, for error messages.
367
+ """
368
+ required_fields = ["id", "statistics_path", "objective", "tolerance", "index"]
369
+ missing_fields = [field for field in required_fields if data.get(field) is None]
370
+
371
+ if missing_fields:
372
+ index_msg = f" at index [magenta]{index}[/magenta]" if index is not None else ""
373
+ error(
374
+ f"Invalid rule format{index_msg} in [magenta]{rule_str}[/magenta]. "
375
+ f"Missing required fields: [magenta]{', '.join(missing_fields)}[/magenta]."
376
+ )
377
+
378
+ # Validate tolerance structure
379
+ tolerance = data.get("tolerance")
380
+ if not isinstance(tolerance, dict) or tolerance.get("value") is None or tolerance.get("type") is None:
381
+ index_msg = f" at index [magenta]{index}[/magenta]" if index is not None else ""
382
+ error(
383
+ f"Invalid tolerance format{index_msg} in [magenta]{rule_str}[/magenta]. "
384
+ "Tolerance must have [magenta]value[/magenta] and [magenta]type[/magenta] fields."
385
+ )
386
+
387
+
388
+ def create_evaluation_rule(data: dict) -> EvaluationRule:
389
+ """
390
+ Creates an EvaluationRule from validated data.
391
+
392
+ Parameters
393
+ ----------
394
+ data : dict
395
+ The validated rule data.
396
+
397
+ Returns
398
+ -------
399
+ EvaluationRule
400
+ The created evaluation rule.
401
+ """
402
+ tolerance_data = data["tolerance"]
403
+ tolerance = RuleTolerance(
404
+ value=float(tolerance_data["value"]),
405
+ type=RuleToleranceType(tolerance_data["type"]),
406
+ )
407
+
408
+ return EvaluationRule(
409
+ id=data["id"],
410
+ statistics_path=data["statistics_path"],
411
+ objective=RuleObjective(data["objective"]),
412
+ tolerance=tolerance,
413
+ index=int(data["index"]),
414
+ )
@@ -0,0 +1,67 @@
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
+ 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, 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 [code]--yes[/code]
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
+ $ [green]nextmv cloud ensemble delete --app-id hare-app --ensemble-definition-id prod-ensemble[/green]
43
+
44
+ - Delete the ensemble definition without confirmation prompt.
45
+ $ [green]nextmv cloud ensemble delete --app-id hare-app --ensemble-definition-id prod-ensemble --yes[/green]
46
+ """
47
+
48
+ if not yes:
49
+ confirm = Confirm.ask(
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
+ default=False,
53
+ )
54
+
55
+ if not confirm:
56
+ info(
57
+ msg=f"Ensemble definition [magenta]{ensemble_definition_id}[/magenta] will not be deleted.",
58
+ emoji=":bulb:",
59
+ )
60
+ return
61
+
62
+ cloud_app = build_app(app_id=app_id, profile=profile)
63
+ cloud_app.delete_ensemble_definition(ensemble_definition_id=ensemble_definition_id)
64
+ success(
65
+ f"Ensemble definition [magenta]{ensemble_definition_id}[/magenta] deleted successfully "
66
+ f"from application [magenta]{app_id}[/magenta]."
67
+ )
@@ -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
+ $ [green]nextmv cloud ensemble get --app-id hare-app \\
44
+ --ensemble-definition-id prod-ensemble[/green]
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
+ $ [green]nextmv cloud ensemble get --app-id hare-app \\
49
+ --ensemble-definition-id prod-ensemble --output ensemble.json[/green]
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,103 @@
1
+ """
2
+ This module defines the cloud ensemble update 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, 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 update(
20
+ app_id: AppIDOption,
21
+ ensemble_definition_id: EnsembleDefinitionIDOption,
22
+ description: Annotated[
23
+ str | None,
24
+ typer.Option(
25
+ "--description",
26
+ "-d",
27
+ help="A new description for the ensemble definition.",
28
+ metavar="DESCRIPTION",
29
+ ),
30
+ ] = None,
31
+ name: Annotated[
32
+ str | None,
33
+ typer.Option(
34
+ "--name",
35
+ "-n",
36
+ help="A new name for the ensemble definition.",
37
+ metavar="NAME",
38
+ ),
39
+ ] = None,
40
+ output: Annotated[
41
+ str | None,
42
+ typer.Option(
43
+ "--output",
44
+ "-o",
45
+ help="Saves the updated ensemble definition information to this location.",
46
+ metavar="OUTPUT_PATH",
47
+ ),
48
+ ] = None,
49
+ profile: ProfileOption = None,
50
+ ) -> None:
51
+ """
52
+ Update a Nextmv Cloud ensemble definition.
53
+
54
+ You can update the name and/or description of an existing ensemble
55
+ definition. To modify run groups or evaluation rules, you need to delete
56
+ and recreate the ensemble definition.
57
+
58
+ [bold][underline]Examples[/underline][/bold]
59
+
60
+ - Update the name of an ensemble definition.
61
+ $ [green]nextmv cloud ensemble update --app-id hare-app \\
62
+ --ensemble-definition-id prod-ensemble --name "Updated Production Ensemble"[/green]
63
+
64
+ - Update the description of an ensemble definition.
65
+ $ [green]nextmv cloud ensemble update --app-id hare-app \\
66
+ --ensemble-definition-id prod-ensemble \\
67
+ --description "Updated ensemble for production workloads"[/green]
68
+
69
+ - Update both name and description.
70
+ $ [green]nextmv cloud ensemble update --app-id hare-app \\
71
+ --ensemble-definition-id prod-ensemble --name "Production Ensemble v2" \\
72
+ --description "Enhanced ensemble configuration for production"[/green]
73
+
74
+ - Update and save the result to a file.
75
+ $ [green]nextmv cloud ensemble update --app-id hare-app \\
76
+ --ensemble-definition-id prod-ensemble --name "New Name" \\
77
+ --description "New Description" --output updated.json[/green]
78
+ """
79
+
80
+ if name is None and description is None:
81
+ error("Provide at least one option to update: [code]--name[/code] or [code]--description[/code].")
82
+
83
+ cloud_app = build_app(app_id=app_id, profile=profile)
84
+
85
+ ensemble_definition = cloud_app.update_ensemble_definition(
86
+ id=ensemble_definition_id,
87
+ name=name,
88
+ description=description,
89
+ )
90
+ success(
91
+ f"Ensemble definition [magenta]{ensemble_definition_id}[/magenta] updated successfully "
92
+ f"in application [magenta]{app_id}[/magenta]."
93
+ )
94
+
95
+ if output is not None and output != "":
96
+ with open(output, "w") as f:
97
+ json.dump(ensemble_definition.to_dict(), f, indent=2)
98
+
99
+ success(msg=f"Updated ensemble definition information saved to [magenta]{output}[/magenta].")
100
+
101
+ return
102
+
103
+ print_json(ensemble_definition.to_dict())
@@ -0,0 +1,30 @@
1
+ """
2
+ This module defines the cloud input-set command tree for the Nextmv CLI.
3
+ """
4
+
5
+ import typer
6
+
7
+ from nextmv.cli.cloud.input_set.create import app as create_app
8
+ from nextmv.cli.cloud.input_set.get import app as get_app
9
+ from nextmv.cli.cloud.input_set.list import app as list_app
10
+ from nextmv.cli.cloud.input_set.update import app as update_app
11
+
12
+ # Set up subcommand application.
13
+ app = typer.Typer()
14
+ app.add_typer(create_app)
15
+ app.add_typer(get_app)
16
+ app.add_typer(list_app)
17
+ app.add_typer(update_app)
18
+
19
+
20
+ @app.callback()
21
+ def callback() -> None:
22
+ """
23
+ Create and manage Nextmv Cloud input sets.
24
+
25
+ An input set is a collection of inputs from associated runs that can be
26
+ reused across multiple experiments. Input sets allow you to test different
27
+ configurations of your decision model using the same set of inputs for
28
+ consistent comparison.
29
+ """
30
+ pass