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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +8 -13
- nextmv/__init__.py +53 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +54 -9
- nextmv/cli/CONTRIBUTING.md +511 -0
- nextmv/cli/__init__.py +0 -0
- nextmv/cli/cloud/__init__.py +47 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +393 -0
- nextmv/cli/cloud/acceptance/delete.py +68 -0
- nextmv/cli/cloud/acceptance/get.py +104 -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 +60 -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 +141 -0
- nextmv/cli/cloud/app/delete.py +58 -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 +137 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +454 -0
- nextmv/cli/cloud/batch/delete.py +68 -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 +31 -0
- nextmv/cli/cloud/ensemble/create.py +414 -0
- nextmv/cli/cloud/ensemble/delete.py +67 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +30 -0
- nextmv/cli/cloud/input_set/create.py +170 -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 +290 -0
- nextmv/cli/cloud/instance/delete.py +62 -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 +146 -0
- nextmv/cli/cloud/managed_input/delete.py +65 -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 +530 -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 +167 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +501 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +65 -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 +67 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +147 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +68 -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 +43 -0
- nextmv/cli/cloud/shadow/update.py +95 -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 +97 -0
- nextmv/cli/cloud/version/delete.py +62 -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 +270 -0
- nextmv/cli/community/list.py +265 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +195 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/main.py +188 -0
- nextmv/cli/message.py +153 -0
- nextmv/cli/options.py +206 -0
- nextmv/cli/version.py +38 -0
- nextmv/cloud/__init__.py +71 -17
- nextmv/cloud/acceptance_test.py +757 -51
- nextmv/cloud/account.py +406 -17
- nextmv/cloud/application/__init__.py +957 -0
- nextmv/cloud/application/_acceptance.py +419 -0
- nextmv/cloud/application/_batch_scenario.py +860 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +227 -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 +314 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +303 -0
- nextmv/cloud/assets.py +48 -0
- nextmv/cloud/batch_experiment.py +294 -33
- nextmv/cloud/client.py +307 -66
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +120 -2
- nextmv/cloud/instance.py +133 -8
- nextmv/cloud/integration.py +533 -0
- nextmv/cloud/package.py +168 -53
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/shadow.py +190 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +132 -4
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/README.md +32 -0
- nextmv/default_app/app.yaml +12 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/requirements.txt +2 -0
- nextmv/default_app/src/__init__.py +0 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +861 -90
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1251 -0
- nextmv/local/executor.py +1042 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/logger.py +80 -9
- nextmv/manifest.py +1466 -0
- nextmv/model.py +241 -66
- nextmv/options.py +708 -115
- nextmv/output.py +1301 -274
- nextmv/polling.py +325 -0
- nextmv/run.py +1702 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
- nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
- nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -1405
- nextmv/cloud/manifest.py +0 -234
- nextmv/cloud/status.py +0 -29
- nextmv-0.18.0.dist-info/METADATA +0 -770
- nextmv-0.18.0.dist-info/RECORD +0 -25
- {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
|