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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +1 -2
- nextmv/__init__.py +2 -4
- nextmv/cli/CONTRIBUTING.md +583 -0
- nextmv/cli/cloud/__init__.py +49 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +391 -0
- nextmv/cli/cloud/acceptance/delete.py +64 -0
- nextmv/cli/cloud/acceptance/get.py +103 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +59 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +140 -0
- nextmv/cli/cloud/app/delete.py +57 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +432 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +452 -0
- nextmv/cli/cloud/batch/delete.py +64 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +33 -0
- nextmv/cli/cloud/ensemble/create.py +413 -0
- nextmv/cli/cloud/ensemble/delete.py +63 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/list.py +63 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +32 -0
- nextmv/cli/cloud/input_set/create.py +168 -0
- nextmv/cli/cloud/input_set/delete.py +64 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +289 -0
- nextmv/cli/cloud/instance/delete.py +61 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +144 -0
- nextmv/cli/cloud/managed_input/delete.py +64 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +524 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +166 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +500 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +61 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +63 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +144 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +64 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +53 -0
- nextmv/cli/cloud/shadow/update.py +96 -0
- nextmv/cli/cloud/switchback/__init__.py +33 -0
- nextmv/cli/cloud/switchback/create.py +151 -0
- nextmv/cli/cloud/switchback/delete.py +64 -0
- nextmv/cli/cloud/switchback/get.py +62 -0
- nextmv/cli/cloud/switchback/list.py +63 -0
- nextmv/cli/cloud/switchback/metadata.py +68 -0
- nextmv/cli/cloud/switchback/start.py +43 -0
- nextmv/cli/cloud/switchback/stop.py +53 -0
- nextmv/cli/cloud/switchback/update.py +96 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +96 -0
- nextmv/cli/cloud/version/delete.py +61 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +86 -0
- nextmv/cli/community/list.py +200 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +228 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +161 -3
- nextmv/cli/message.py +170 -0
- nextmv/cli/options.py +220 -0
- nextmv/cli/version.py +22 -2
- nextmv/cloud/__init__.py +17 -38
- nextmv/cloud/acceptance_test.py +20 -83
- nextmv/cloud/account.py +269 -30
- nextmv/cloud/application/__init__.py +898 -0
- nextmv/cloud/application/_acceptance.py +424 -0
- nextmv/cloud/application/_batch_scenario.py +845 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +263 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +320 -0
- nextmv/cloud/application/_switchback.py +332 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +304 -0
- nextmv/cloud/batch_experiment.py +6 -2
- nextmv/cloud/community.py +446 -0
- nextmv/cloud/instance.py +11 -1
- nextmv/cloud/integration.py +8 -5
- nextmv/cloud/package.py +50 -9
- nextmv/cloud/shadow.py +254 -0
- nextmv/cloud/switchback.py +228 -0
- nextmv/deprecated.py +5 -3
- nextmv/input.py +20 -88
- nextmv/local/application.py +3 -15
- nextmv/local/runner.py +1 -1
- nextmv/model.py +50 -11
- nextmv/options.py +11 -256
- nextmv/output.py +0 -62
- nextmv/polling.py +54 -16
- nextmv/run.py +84 -37
- nextmv/status.py +1 -51
- {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
- nextmv-1.0.0.dist-info/RECORD +185 -0
- nextmv-1.0.0.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -4204
- nextmv-0.39.0.dev1.dist-info/RECORD +0 -55
- nextmv-0.39.0.dev1.dist-info/entry_points.txt +0 -2
- {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
- {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)
|