nextmv 0.40.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +20 -204
- nextmv/cli/community/list.py +61 -126
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +103 -6
- nextmv/cli/configuration/create.py +17 -18
- nextmv/cli/configuration/delete.py +25 -13
- nextmv/cli/configuration/list.py +4 -4
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +68 -36
- nextmv/cli/message.py +170 -0
- nextmv/cli/options.py +196 -0
- nextmv/cli/version.py +20 -1
- 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.40.0.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/cli/community/community.py +0 -24
- nextmv/cli/configuration/configuration.py +0 -23
- nextmv/cli/error.py +0 -22
- nextmv/cloud/application.py +0 -4204
- nextmv-0.40.0.dist-info/RECORD +0 -66
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the cloud batch command tree for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from nextmv.cli.cloud.batch.create import app as create_app
|
|
8
|
+
from nextmv.cli.cloud.batch.delete import app as delete_app
|
|
9
|
+
from nextmv.cli.cloud.batch.get import app as get_app
|
|
10
|
+
from nextmv.cli.cloud.batch.list import app as list_app
|
|
11
|
+
from nextmv.cli.cloud.batch.metadata import app as metadata_app
|
|
12
|
+
from nextmv.cli.cloud.batch.update import app as update_app
|
|
13
|
+
|
|
14
|
+
# Set up subcommand application.
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
app.add_typer(create_app)
|
|
17
|
+
app.add_typer(delete_app)
|
|
18
|
+
app.add_typer(get_app)
|
|
19
|
+
app.add_typer(list_app)
|
|
20
|
+
app.add_typer(metadata_app)
|
|
21
|
+
app.add_typer(update_app)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback()
|
|
25
|
+
def callback() -> None:
|
|
26
|
+
"""
|
|
27
|
+
Create and manage Nextmv Cloud batch experiments.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the cloud batch create command for the Nextmv CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from nextmv.cli.configuration.config import build_app
|
|
11
|
+
from nextmv.cli.message import error, in_progress, print_json, success
|
|
12
|
+
from nextmv.cli.options import AppIDOption, ProfileOption
|
|
13
|
+
from nextmv.cloud.batch_experiment import BatchExperimentRun
|
|
14
|
+
from nextmv.polling import default_polling_options
|
|
15
|
+
|
|
16
|
+
# Set up subcommand application.
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def create(
|
|
22
|
+
app_id: AppIDOption,
|
|
23
|
+
# Options for batch experiment configuration.
|
|
24
|
+
batch_experiment_id: Annotated[
|
|
25
|
+
str | None,
|
|
26
|
+
typer.Option(
|
|
27
|
+
"--batch-experiment-id",
|
|
28
|
+
"-b",
|
|
29
|
+
help="ID for the batch experiment. Will be generated if not provided.",
|
|
30
|
+
envvar="NEXTMV_BATCH_EXPERIMENT_ID",
|
|
31
|
+
metavar="BATCH_EXPERIMENT_ID",
|
|
32
|
+
rich_help_panel="Batch experiment configuration",
|
|
33
|
+
),
|
|
34
|
+
] = None,
|
|
35
|
+
description: Annotated[
|
|
36
|
+
str | None,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--description",
|
|
39
|
+
"-d",
|
|
40
|
+
help="Description of the batch experiment.",
|
|
41
|
+
metavar="DESCRIPTION",
|
|
42
|
+
rich_help_panel="Batch experiment configuration",
|
|
43
|
+
),
|
|
44
|
+
] = None,
|
|
45
|
+
input_set_id: Annotated[
|
|
46
|
+
str | None,
|
|
47
|
+
typer.Option(
|
|
48
|
+
"--input-set-id",
|
|
49
|
+
"-i",
|
|
50
|
+
help="ID of the input set to use for the batch experiment.",
|
|
51
|
+
metavar="INPUT_SET_ID",
|
|
52
|
+
rich_help_panel="Batch experiment configuration",
|
|
53
|
+
),
|
|
54
|
+
] = None,
|
|
55
|
+
name: Annotated[
|
|
56
|
+
str | None,
|
|
57
|
+
typer.Option(
|
|
58
|
+
"--name",
|
|
59
|
+
"-n",
|
|
60
|
+
help="Name of the batch experiment. If not provided, the ID will be used as the name.",
|
|
61
|
+
metavar="NAME",
|
|
62
|
+
rich_help_panel="Batch experiment configuration",
|
|
63
|
+
),
|
|
64
|
+
] = None,
|
|
65
|
+
option_sets: Annotated[
|
|
66
|
+
str | None,
|
|
67
|
+
typer.Option(
|
|
68
|
+
"--option-sets",
|
|
69
|
+
help="Option sets to use for the batch experiment. Data should be valid [magenta]json[/magenta]. "
|
|
70
|
+
"See command help for details on option sets formatting.",
|
|
71
|
+
metavar="OPTION_SETS",
|
|
72
|
+
rich_help_panel="Batch experiment configuration",
|
|
73
|
+
),
|
|
74
|
+
] = None,
|
|
75
|
+
runs: Annotated[
|
|
76
|
+
list[str] | None,
|
|
77
|
+
typer.Option(
|
|
78
|
+
"--runs",
|
|
79
|
+
"-r",
|
|
80
|
+
help="Runs to execute for the batch experiment. Data should be valid [magenta]json[/magenta]. "
|
|
81
|
+
"Pass multiple runs by repeating the flag, or providing a list of objects. "
|
|
82
|
+
"See command help for details on run formatting.",
|
|
83
|
+
metavar="RUNS",
|
|
84
|
+
rich_help_panel="Batch experiment configuration",
|
|
85
|
+
),
|
|
86
|
+
] = None,
|
|
87
|
+
# Options for controlling output.
|
|
88
|
+
output: Annotated[
|
|
89
|
+
str | None,
|
|
90
|
+
typer.Option(
|
|
91
|
+
"--output",
|
|
92
|
+
"-o",
|
|
93
|
+
help="Waits for the experiment to complete and saves the results to this location.",
|
|
94
|
+
metavar="OUTPUT_PATH",
|
|
95
|
+
rich_help_panel="Output control",
|
|
96
|
+
),
|
|
97
|
+
] = None,
|
|
98
|
+
timeout: Annotated[
|
|
99
|
+
int,
|
|
100
|
+
typer.Option(
|
|
101
|
+
help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
|
|
102
|
+
metavar="TIMEOUT_SECONDS",
|
|
103
|
+
rich_help_panel="Output control",
|
|
104
|
+
),
|
|
105
|
+
] = -1,
|
|
106
|
+
wait: Annotated[
|
|
107
|
+
bool,
|
|
108
|
+
typer.Option(
|
|
109
|
+
"--wait",
|
|
110
|
+
"-w",
|
|
111
|
+
help="Wait for the batch experiment to complete. Results are printed to [magenta]stdout[/magenta]. "
|
|
112
|
+
"Specify output location with --output.",
|
|
113
|
+
rich_help_panel="Output control",
|
|
114
|
+
),
|
|
115
|
+
] = False,
|
|
116
|
+
profile: ProfileOption = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Create a new Nextmv Cloud batch experiment.
|
|
120
|
+
|
|
121
|
+
A batch experiment executes multiple runs across different inputs and/or
|
|
122
|
+
configurations. Each run is defined by a combination of input, instance or
|
|
123
|
+
version, and optional configuration options.
|
|
124
|
+
|
|
125
|
+
Use the --wait flag to wait for the batch experiment to complete, polling
|
|
126
|
+
for results. Using the --output flag will also activate waiting, and allows
|
|
127
|
+
you to specify a destination file for the results.
|
|
128
|
+
|
|
129
|
+
[bold][underline]Runs[/underline][/bold]
|
|
130
|
+
|
|
131
|
+
Runs are provided as [magenta]json[/magenta] objects using the --runs flag.
|
|
132
|
+
Each run defines what input, instance/version, and configuration to use.
|
|
133
|
+
|
|
134
|
+
You can provide runs in three ways:
|
|
135
|
+
- A single run as a [magenta]json[/magenta] object.
|
|
136
|
+
- Multiple runs by repeating the --runs flag.
|
|
137
|
+
- Multiple runs as a [magenta]json[/magenta] array in a single --runs flag.
|
|
138
|
+
|
|
139
|
+
Each run must have the following fields:
|
|
140
|
+
- [magenta]input_id[/magenta]: ID of the input to use for this run
|
|
141
|
+
(required). If a managed input is used, this should be the ID of the
|
|
142
|
+
managed input. If [magenta]input_set_id[/magenta] is provided for the run,
|
|
143
|
+
this should be the ID of an input within that input set.
|
|
144
|
+
- [magenta]instance_id[/magenta] OR [magenta]version_id[/magenta]: Either an instance ID or
|
|
145
|
+
version ID must be provided (at least one required).
|
|
146
|
+
- [magenta]option_set[/magenta]: ID of the option set to use (optional).
|
|
147
|
+
Make sure to define the option sets using the --option-sets flag.
|
|
148
|
+
- [magenta]input_set_id[/magenta]: ID of the input set (optional).
|
|
149
|
+
- [magenta]scenario_id[/magenta]: Scenario ID if part of a scenario test (optional).
|
|
150
|
+
- [magenta]repetition[/magenta]: Repetition number (optional).
|
|
151
|
+
|
|
152
|
+
Object format:
|
|
153
|
+
[dim]{
|
|
154
|
+
"input_id": "meadow-input-a1",
|
|
155
|
+
"instance_id": "bunny-hopper-v2",
|
|
156
|
+
"option_set": "speed-optimized",
|
|
157
|
+
"input_set_id": "spring-gardens"
|
|
158
|
+
}[/dim]
|
|
159
|
+
|
|
160
|
+
[bold][underline]Option Sets[/underline][/bold]
|
|
161
|
+
|
|
162
|
+
Option sets are provided as a [magenta]json[/magenta] object using the
|
|
163
|
+
--option-sets flag. Option sets define named collections of
|
|
164
|
+
runtime options that can be referenced by runs.
|
|
165
|
+
|
|
166
|
+
The option sets object is a dictionary where keys are option set IDs and
|
|
167
|
+
values are dictionaries of string key-value pairs representing the options.
|
|
168
|
+
|
|
169
|
+
Object format:
|
|
170
|
+
[dim]{
|
|
171
|
+
"speed-optimized": {"timeout": "30", "algorithm": "fast"},
|
|
172
|
+
"quality-focused": {"timeout": "300", "algorithm": "thorough"}
|
|
173
|
+
}[/dim]
|
|
174
|
+
|
|
175
|
+
[bold][underline]Examples[/underline][/bold]
|
|
176
|
+
|
|
177
|
+
- Create a batch experiment with a single run.
|
|
178
|
+
$ [dim]RUN='{
|
|
179
|
+
"input_id": "carrot-patch-a",
|
|
180
|
+
"instance_id": "warren-planner-v1"
|
|
181
|
+
}'
|
|
182
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id bunny-hop-test \\
|
|
183
|
+
--name "Spring Meadow Routes" --input-set-id spring-gardens --runs "$RUN"[/dim]
|
|
184
|
+
|
|
185
|
+
- Create with multiple runs by repeating the flag.
|
|
186
|
+
$ [dim]RUN1='{
|
|
187
|
+
"input_id": "lettuce-field-1",
|
|
188
|
+
"instance_id": "hop-optimizer"
|
|
189
|
+
}'
|
|
190
|
+
RUN2='{
|
|
191
|
+
"input_id": "lettuce-field-2",
|
|
192
|
+
"instance_id": "hop-optimizer"
|
|
193
|
+
}'
|
|
194
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id lettuce-routes \\
|
|
195
|
+
--name "Lettuce Delivery Optimization" --input-set-id veggie-gardens \\
|
|
196
|
+
--runs "$RUN1" --runs "$RUN2"[/dim]
|
|
197
|
+
|
|
198
|
+
- Create with multiple runs in a single [magenta]json[/magenta] array.
|
|
199
|
+
$ [dim]RUNS='[
|
|
200
|
+
{
|
|
201
|
+
"input_id": "warren-zone-a",
|
|
202
|
+
"instance_id": "burrow-builder"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"input_id": "warren-zone-b",
|
|
206
|
+
"version_id": "tunnel-planner-v3"
|
|
207
|
+
}
|
|
208
|
+
]'
|
|
209
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id warren-expansion \\
|
|
210
|
+
--name "Warren Construction Plans" --input-set-id burrow-sites --runs "$RUNS"[/dim]
|
|
211
|
+
|
|
212
|
+
- Create a batch experiment and wait for it to complete.
|
|
213
|
+
$ [dim]RUN='{
|
|
214
|
+
"input_id": "carrot-harvest",
|
|
215
|
+
"instance_id": "foraging-route"
|
|
216
|
+
}'
|
|
217
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id harvest-time \\
|
|
218
|
+
--name "Autumn Carrot Collection" --input-set-id harvest-season \\
|
|
219
|
+
--runs "$RUN" --wait[/dim]
|
|
220
|
+
|
|
221
|
+
- Create a batch experiment and save the results to a file, waiting for completion.
|
|
222
|
+
$ [dim]RUN='{
|
|
223
|
+
"input_id": "predator-zones",
|
|
224
|
+
"instance_id": "safe-hopper"
|
|
225
|
+
}'
|
|
226
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id safety-analysis \\
|
|
227
|
+
--name "Fox Avoidance Routes" --input-set-id danger-zones \\
|
|
228
|
+
--runs "$RUN" --output bunny-safety-results.json[/dim]
|
|
229
|
+
|
|
230
|
+
- Create a batch experiment with option sets.
|
|
231
|
+
$ [dim]RUN1='{
|
|
232
|
+
"input_id": "garden-route-1",
|
|
233
|
+
"instance_id": "hop-optimizer",
|
|
234
|
+
"option_set": "fast-hops"
|
|
235
|
+
}'
|
|
236
|
+
RUN2='{
|
|
237
|
+
"input_id": "garden-route-1",
|
|
238
|
+
"instance_id": "hop-optimizer",
|
|
239
|
+
"option_set": "careful-hops"
|
|
240
|
+
}'
|
|
241
|
+
OPTION_SETS='{
|
|
242
|
+
"fast-hops": {"max_speed": "10", "caution_level": "low"},
|
|
243
|
+
"careful-hops": {"max_speed": "5", "caution_level": "high"}
|
|
244
|
+
}'
|
|
245
|
+
nextmv cloud batch create --app-id hare-app --batch-experiment-id hop-comparison \\
|
|
246
|
+
--name "Speed vs Safety Analysis" --input-set-id garden-paths \\
|
|
247
|
+
--runs "$RUN1" --runs "$RUN2" --option-sets "$OPTION_SETS"[/dim]
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
cloud_app = build_app(app_id=app_id, profile=profile)
|
|
251
|
+
|
|
252
|
+
# Build the runs list from the CLI options
|
|
253
|
+
runs_list = build_runs(runs)
|
|
254
|
+
|
|
255
|
+
# Build the option sets from the CLI options
|
|
256
|
+
option_sets_dict = build_option_sets(option_sets)
|
|
257
|
+
|
|
258
|
+
batch_id = cloud_app.new_batch_experiment(
|
|
259
|
+
id=batch_experiment_id,
|
|
260
|
+
name=name,
|
|
261
|
+
runs=runs_list,
|
|
262
|
+
description=description,
|
|
263
|
+
input_set_id=input_set_id,
|
|
264
|
+
option_sets=option_sets_dict,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# If we don't need to poll at all we are done.
|
|
268
|
+
if not wait and (output is None or output == ""):
|
|
269
|
+
print_json({"batch_experiment_id": batch_id})
|
|
270
|
+
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
success(f"Batch experiment [magenta]{batch_id}[/magenta] created.")
|
|
274
|
+
|
|
275
|
+
# Build the polling options.
|
|
276
|
+
polling_options = default_polling_options()
|
|
277
|
+
polling_options.max_duration = timeout
|
|
278
|
+
|
|
279
|
+
in_progress(msg="Getting batch experiment results...")
|
|
280
|
+
batch_experiment = cloud_app.batch_experiment_with_polling(
|
|
281
|
+
batch_id=batch_id,
|
|
282
|
+
polling_options=polling_options,
|
|
283
|
+
)
|
|
284
|
+
batch_experiment_dict = batch_experiment.to_dict()
|
|
285
|
+
|
|
286
|
+
# Handle output
|
|
287
|
+
if output is not None and output != "":
|
|
288
|
+
with open(output, "w") as f:
|
|
289
|
+
json.dump(batch_experiment_dict, f, indent=2)
|
|
290
|
+
|
|
291
|
+
success(msg=f"Batch experiment results saved to [magenta]{output}[/magenta].")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
print_json(batch_experiment_dict)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def build_option_sets(option_sets: str | None) -> dict[str, dict[str, str]] | None:
|
|
298
|
+
"""
|
|
299
|
+
Builds the option sets dictionary from the CLI option.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
option_sets : str | None
|
|
304
|
+
Option sets JSON string provided via the CLI.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
dict[str, dict[str, str]] | None
|
|
309
|
+
The built option sets dictionary, or None if not provided.
|
|
310
|
+
"""
|
|
311
|
+
if option_sets is None:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
option_sets_data = json.loads(option_sets)
|
|
316
|
+
|
|
317
|
+
if not isinstance(option_sets_data, dict):
|
|
318
|
+
error(
|
|
319
|
+
f"Invalid option sets format: [magenta]{option_sets}[/magenta]. "
|
|
320
|
+
"Expected [magenta]json[/magenta] object."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Validate structure
|
|
324
|
+
for key, value in option_sets_data.items():
|
|
325
|
+
if not isinstance(value, dict):
|
|
326
|
+
error(
|
|
327
|
+
f"Invalid option sets format: [magenta]{option_sets}[/magenta]. "
|
|
328
|
+
f"Each option set must be a [magenta]json[/magenta] object. "
|
|
329
|
+
f"Key [magenta]{key}[/magenta] has invalid value."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return option_sets_data
|
|
333
|
+
|
|
334
|
+
except json.JSONDecodeError as e:
|
|
335
|
+
error(f"Invalid option sets format: [magenta]{option_sets}[/magenta]. Error: {e}")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def build_runs(runs: list[str] | None) -> list[BatchExperimentRun] | None:
|
|
339
|
+
"""
|
|
340
|
+
Builds the runs list from the CLI option(s).
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
runs : list[str] | None
|
|
345
|
+
List of runs provided via the CLI.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
list[BatchExperimentRun] | None
|
|
350
|
+
The built runs list, or None if no runs provided.
|
|
351
|
+
"""
|
|
352
|
+
if runs is None:
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
runs_list = []
|
|
356
|
+
|
|
357
|
+
for run_str in runs:
|
|
358
|
+
try:
|
|
359
|
+
run_data = json.loads(run_str)
|
|
360
|
+
|
|
361
|
+
# Handle the case where the value is a list of runs.
|
|
362
|
+
if isinstance(run_data, list):
|
|
363
|
+
runs_list.extend(_process_run_list(run_data, run_str))
|
|
364
|
+
|
|
365
|
+
# Handle the case where the value is a single run.
|
|
366
|
+
elif isinstance(run_data, dict):
|
|
367
|
+
runs_list.append(_process_single_run(run_data, run_str))
|
|
368
|
+
|
|
369
|
+
else:
|
|
370
|
+
error(
|
|
371
|
+
f"Invalid run format: [magenta]{run_str}[/magenta]. "
|
|
372
|
+
"Expected [magenta]json[/magenta] object or array."
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
376
|
+
error(f"Invalid run format: [magenta]{run_str}[/magenta]. Error: {e}")
|
|
377
|
+
|
|
378
|
+
return runs_list if runs_list else None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _process_run_list(run_data: list, run_str: str) -> list[BatchExperimentRun]:
|
|
382
|
+
"""
|
|
383
|
+
Process a list of run objects.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
run_data : list
|
|
388
|
+
List of run dictionaries.
|
|
389
|
+
run_str : str
|
|
390
|
+
Original string for error messages.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
list[BatchExperimentRun]
|
|
395
|
+
List of processed runs.
|
|
396
|
+
"""
|
|
397
|
+
processed_runs = []
|
|
398
|
+
for ix, item in enumerate(run_data):
|
|
399
|
+
_validate_run_fields(item, run_str, ix)
|
|
400
|
+
run = BatchExperimentRun(**item)
|
|
401
|
+
processed_runs.append(run)
|
|
402
|
+
return processed_runs
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _process_single_run(run_data: dict, run_str: str) -> BatchExperimentRun:
|
|
406
|
+
"""
|
|
407
|
+
Process a single run object.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
run_data : dict
|
|
412
|
+
Run dictionary.
|
|
413
|
+
run_str : str
|
|
414
|
+
Original string for error messages.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
BatchExperimentRun
|
|
419
|
+
Processed run.
|
|
420
|
+
"""
|
|
421
|
+
_validate_run_fields(run_data, run_str)
|
|
422
|
+
return BatchExperimentRun(**run_data)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _validate_run_fields(run_data: dict, run_str: str, index: int | None = None) -> None:
|
|
426
|
+
"""
|
|
427
|
+
Validate that required run fields are present.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
run_data : dict
|
|
432
|
+
Run dictionary to validate.
|
|
433
|
+
run_str : str
|
|
434
|
+
Original string for error messages.
|
|
435
|
+
index : int | None
|
|
436
|
+
Index in array if validating array element.
|
|
437
|
+
"""
|
|
438
|
+
location = f"at index [magenta]{index}[/magenta] in " if index is not None else "in "
|
|
439
|
+
|
|
440
|
+
if run_data.get("input_id") is None:
|
|
441
|
+
error(
|
|
442
|
+
f"Invalid run format {location}"
|
|
443
|
+
f"[magenta]{run_str}[/magenta]. Each run must have an "
|
|
444
|
+
"[magenta]input_id[/magenta] field."
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if run_data.get("instance_id") is None and run_data.get("version_id") is None:
|
|
448
|
+
error(
|
|
449
|
+
f"Invalid run format {location}"
|
|
450
|
+
f"[magenta]{run_str}[/magenta]. Each run must have either an "
|
|
451
|
+
"[magenta]instance_id[/magenta] or [magenta]version_id[/magenta] field."
|
|
452
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the cloud batch 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, BatchExperimentIDOption, ProfileOption
|
|
13
|
+
|
|
14
|
+
# Set up subcommand application.
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def delete(
|
|
20
|
+
app_id: AppIDOption,
|
|
21
|
+
batch_experiment_id: BatchExperimentIDOption,
|
|
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 batch experiment.
|
|
34
|
+
|
|
35
|
+
This action is permanent and cannot be undone. The batch experiment and all
|
|
36
|
+
associated data, including runs, will be deleted. Use the --yes
|
|
37
|
+
flag to skip the confirmation prompt.
|
|
38
|
+
|
|
39
|
+
[bold][underline]Examples[/underline][/bold]
|
|
40
|
+
|
|
41
|
+
- Delete the batch experiment with the ID [magenta]hop-analysis[/magenta] from application
|
|
42
|
+
[magenta]hare-app[/magenta].
|
|
43
|
+
$ [dim]nextmv cloud batch delete --app-id hare-app --batch-experiment-id hop-analysis[/dim]
|
|
44
|
+
|
|
45
|
+
- Delete the batch experiment without confirmation prompt.
|
|
46
|
+
$ [dim]nextmv cloud batch delete --app-id hare-app --batch-experiment-id carrot-routes --yes[/dim]
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if not yes:
|
|
50
|
+
confirm = get_confirmation(
|
|
51
|
+
f"Are you sure you want to delete batch experiment [magenta]{batch_experiment_id}[/magenta] "
|
|
52
|
+
f"from application [magenta]{app_id}[/magenta]? This action cannot be undone.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not confirm:
|
|
56
|
+
info(f"Batch experiment [magenta]{batch_experiment_id}[/magenta] will not be deleted.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
cloud_app = build_app(app_id=app_id, profile=profile)
|
|
60
|
+
cloud_app.delete_batch_experiment(batch_id=batch_experiment_id)
|
|
61
|
+
success(
|
|
62
|
+
f"Batch experiment [magenta]{batch_experiment_id}[/magenta] deleted successfully "
|
|
63
|
+
f"from application [magenta]{app_id}[/magenta]."
|
|
64
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the cloud batch 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, BatchExperimentIDOption, ProfileOption
|
|
13
|
+
from nextmv.polling import default_polling_options
|
|
14
|
+
|
|
15
|
+
# Set up subcommand application.
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def get(
|
|
21
|
+
app_id: AppIDOption,
|
|
22
|
+
batch_experiment_id: BatchExperimentIDOption,
|
|
23
|
+
output: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Option(
|
|
26
|
+
"--output",
|
|
27
|
+
"-o",
|
|
28
|
+
help="Waits for the batch experiment to complete and saves the results to this location.",
|
|
29
|
+
metavar="OUTPUT_PATH",
|
|
30
|
+
),
|
|
31
|
+
] = None,
|
|
32
|
+
timeout: Annotated[
|
|
33
|
+
int,
|
|
34
|
+
typer.Option(
|
|
35
|
+
help="The maximum time in seconds to wait for results when polling. Poll indefinitely if not set.",
|
|
36
|
+
metavar="TIMEOUT_SECONDS",
|
|
37
|
+
),
|
|
38
|
+
] = -1,
|
|
39
|
+
wait: Annotated[
|
|
40
|
+
bool,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--wait",
|
|
43
|
+
"-w",
|
|
44
|
+
help="Wait for the batch experiment to complete. Results are printed to [magenta]stdout[/magenta]. "
|
|
45
|
+
"Specify output location with --output.",
|
|
46
|
+
),
|
|
47
|
+
] = False,
|
|
48
|
+
profile: ProfileOption = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Get a Nextmv Cloud batch experiment, including its runs.
|
|
52
|
+
|
|
53
|
+
Use the --wait flag to wait for the batch experiment to
|
|
54
|
+
complete, polling for results. Using the --output flag will
|
|
55
|
+
also activate waiting, and allows you to specify a destination file for the
|
|
56
|
+
results.
|
|
57
|
+
|
|
58
|
+
[bold][underline]Examples[/underline][/bold]
|
|
59
|
+
|
|
60
|
+
- Get the batch experiment with ID [magenta]carrot-optimization[/magenta] from application
|
|
61
|
+
[magenta]hare-app[/magenta].
|
|
62
|
+
$ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id carrot-optimization[/dim]
|
|
63
|
+
|
|
64
|
+
- Get the batch experiment and wait for it to complete if necessary.
|
|
65
|
+
$ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id bunny-hop-test --wait[/dim]
|
|
66
|
+
|
|
67
|
+
- Get the batch experiment and save the results to a file.
|
|
68
|
+
$ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id warren-planning \\
|
|
69
|
+
--output results.json[/dim]
|
|
70
|
+
|
|
71
|
+
- Get the batch experiment using a specific profile.
|
|
72
|
+
$ [dim]nextmv cloud batch get --app-id hare-app --batch-experiment-id lettuce-routes --profile prod[/dim]
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
cloud_app = build_app(app_id=app_id, profile=profile)
|
|
76
|
+
|
|
77
|
+
# Build the polling options.
|
|
78
|
+
polling_options = default_polling_options()
|
|
79
|
+
polling_options.max_duration = timeout
|
|
80
|
+
|
|
81
|
+
# Determine if we should wait
|
|
82
|
+
should_wait = wait or (output is not None and output != "")
|
|
83
|
+
|
|
84
|
+
in_progress(msg="Getting batch experiment...")
|
|
85
|
+
if should_wait:
|
|
86
|
+
batch_experiment = cloud_app.batch_experiment_with_polling(
|
|
87
|
+
batch_id=batch_experiment_id,
|
|
88
|
+
polling_options=polling_options,
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
batch_experiment = cloud_app.batch_experiment(batch_id=batch_experiment_id)
|
|
92
|
+
|
|
93
|
+
batch_experiment_dict = batch_experiment.to_dict()
|
|
94
|
+
|
|
95
|
+
# Handle output
|
|
96
|
+
if output is not None and output != "":
|
|
97
|
+
with open(output, "w") as f:
|
|
98
|
+
json.dump(batch_experiment_dict, f, indent=2)
|
|
99
|
+
|
|
100
|
+
success(msg=f"Batch experiment results saved to [magenta]{output}[/magenta].")
|
|
101
|
+
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
print_json(batch_experiment_dict)
|