nextmv 0.23.0__py3-none-any.whl → 0.25.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 +7 -10
- nextmv/__init__.py +3 -0
- nextmv/cloud/__init__.py +7 -1
- nextmv/cloud/application.py +555 -50
- nextmv/cloud/batch_experiment.py +58 -22
- nextmv/cloud/client.py +2 -0
- nextmv/cloud/input_set.py +26 -0
- nextmv/cloud/manifest.py +157 -8
- nextmv/cloud/run.py +8 -7
- nextmv/cloud/safe.py +83 -0
- nextmv/cloud/scenario.py +229 -0
- nextmv/deprecated.py +13 -0
- nextmv/input.py +74 -0
- nextmv/options.py +293 -78
- nextmv/output.py +64 -7
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/METADATA +1 -1
- nextmv-0.25.0.dist-info/RECORD +30 -0
- nextmv-0.23.0.dist-info/RECORD +0 -27
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/WHEEL +0 -0
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/scenario.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""This module contains definitions for scenario tests."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ScenarioConfiguration:
|
|
11
|
+
"""
|
|
12
|
+
Configuration for a scenario.
|
|
13
|
+
|
|
14
|
+
You can define multiple values for a single option, which will result in
|
|
15
|
+
multiple runs being created. For example, if you have a configuration
|
|
16
|
+
option "x" with values [1, 2], and a configuration option "y" with values
|
|
17
|
+
[3, 4], then the following runs will be created:
|
|
18
|
+
- x=1, y=3
|
|
19
|
+
- x=1, y=4
|
|
20
|
+
- x=2, y=3
|
|
21
|
+
- x=2, y=4
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
name : str
|
|
26
|
+
Name of the configuration option.
|
|
27
|
+
values : list[str]
|
|
28
|
+
List of values for the configuration option.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
"""Name of the configuration option."""
|
|
33
|
+
values: list[str]
|
|
34
|
+
"""List of values for the configuration option."""
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
"""
|
|
38
|
+
Post-initialization method to ensure that the values are unique.
|
|
39
|
+
"""
|
|
40
|
+
if len(self.values) <= 0:
|
|
41
|
+
raise ValueError("Configuration values must be non-empty.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ScenarioInputType(str, Enum):
|
|
45
|
+
"""
|
|
46
|
+
Type of input for a scenario. This is used to determine how the input
|
|
47
|
+
should be processed.
|
|
48
|
+
|
|
49
|
+
Attributes
|
|
50
|
+
----------
|
|
51
|
+
INPUT_SET : str
|
|
52
|
+
The data in the scenario is an input set.
|
|
53
|
+
INPUT : str
|
|
54
|
+
The data in the scenario is an input.
|
|
55
|
+
NEW : str
|
|
56
|
+
The data in the scenario is new data.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
INPUT_SET = "input_set"
|
|
60
|
+
"""The data in the scenario is an input set."""
|
|
61
|
+
INPUT = "input"
|
|
62
|
+
"""The data in the scenario is an input."""
|
|
63
|
+
NEW = "new"
|
|
64
|
+
"""The data in the scenario is new data."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ScenarioInput:
|
|
69
|
+
"""
|
|
70
|
+
Input to be processed in a scenario. The type of input is determined by
|
|
71
|
+
the `scenario_input_type` attribute. The input can be a single input set
|
|
72
|
+
ID, a list of input IDs, or raw data. The data itself of the scenario input
|
|
73
|
+
is tracked by the `scenario_input_data` attribute.
|
|
74
|
+
|
|
75
|
+
Attributes
|
|
76
|
+
----------
|
|
77
|
+
scenario_input_type : ScenarioInputType
|
|
78
|
+
Type of input for the scenario. This is used to determine how the input
|
|
79
|
+
should be processed.
|
|
80
|
+
scenario_input_data : Union[str, list[str], list[dict[str, Any]]]
|
|
81
|
+
Input data for the scenario. This can be a single input set ID
|
|
82
|
+
(`str`), a list of input IDs (`list[str]`), or raw data
|
|
83
|
+
(`list[dict[str, Any]]`). If you provide a `list[str]` (list of
|
|
84
|
+
inputs), a new input set will be created using these inputs. A similar
|
|
85
|
+
behavior occurs when providing raw data (`list[dict[str, Any]]`). All
|
|
86
|
+
the entries in the list of raw dicts will be collected to create a new
|
|
87
|
+
input set.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
scenario_input_type: ScenarioInputType
|
|
91
|
+
"""
|
|
92
|
+
Type of input for the scenario. This is used to determine how the input
|
|
93
|
+
should be processed.
|
|
94
|
+
"""
|
|
95
|
+
scenario_input_data: Union[
|
|
96
|
+
str, # Input set ID
|
|
97
|
+
list[str], # List of Input IDs
|
|
98
|
+
list[dict[str, Any]], # Raw data
|
|
99
|
+
]
|
|
100
|
+
"""
|
|
101
|
+
Input data for the scenario. This can be a single input set ID (`str`), a
|
|
102
|
+
list of input IDs (`list[str]`), or raw data (`list[dict[str, Any]]`).
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __post_init__(self):
|
|
106
|
+
"""
|
|
107
|
+
Post-initialization method to ensure that the input data is valid.
|
|
108
|
+
"""
|
|
109
|
+
if self.scenario_input_type == ScenarioInputType.INPUT_SET and not isinstance(self.scenario_input_data, str):
|
|
110
|
+
raise ValueError("Scenario input type must be a string when using an input set.")
|
|
111
|
+
elif self.scenario_input_type == ScenarioInputType.INPUT and not isinstance(self.scenario_input_data, list):
|
|
112
|
+
raise ValueError("Scenario input type must be a list when using an input.")
|
|
113
|
+
elif self.scenario_input_type == ScenarioInputType.NEW and not isinstance(self.scenario_input_data, list):
|
|
114
|
+
raise ValueError("Scenario input type must be a list when using new data.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class Scenario:
|
|
119
|
+
"""
|
|
120
|
+
A scenario is a test case that is used to compare a decision model being
|
|
121
|
+
executed with a set of inputs and configurations.
|
|
122
|
+
|
|
123
|
+
Attributes
|
|
124
|
+
----------
|
|
125
|
+
scenario_input : ScenarioInput
|
|
126
|
+
Input for the scenario. The input is composed of a type and data. Make
|
|
127
|
+
sure you use the `ScenarioInput` class to create the input.
|
|
128
|
+
scenario_id : Optional[str]
|
|
129
|
+
Optional ID of the scenario. The default value will be set as
|
|
130
|
+
`scenario-<index>` if not set.
|
|
131
|
+
instance_id : str
|
|
132
|
+
ID of the instance to be used for the scenario.
|
|
133
|
+
configuration : Optional[ScenarioConfiguration]
|
|
134
|
+
Optional configuration for the scenario. Use this attribute to
|
|
135
|
+
configure variation of options for the scenario.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
scenario_input: ScenarioInput
|
|
139
|
+
"""
|
|
140
|
+
Input for the scenario. The input is composed of a type and data. Make sure
|
|
141
|
+
you use the `ScenarioInput` class to create the input.
|
|
142
|
+
"""
|
|
143
|
+
instance_id: str
|
|
144
|
+
"""ID of the instance to be used for the scenario."""
|
|
145
|
+
|
|
146
|
+
scenario_id: Optional[str] = None
|
|
147
|
+
"""
|
|
148
|
+
Optional ID of the scenario. The default value will be set as
|
|
149
|
+
`scenario-<index>` if not set.
|
|
150
|
+
"""
|
|
151
|
+
configuration: Optional[list[ScenarioConfiguration]] = None
|
|
152
|
+
"""Optional configuration for the scenario. Use this attribute to configure
|
|
153
|
+
variation of options for the scenario.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def option_combinations(self) -> list[dict[str, str]]:
|
|
157
|
+
"""
|
|
158
|
+
Creates the combination of options that are derived from the
|
|
159
|
+
`configuration` property. The cross-product of the configuration
|
|
160
|
+
options is created to generate all possible combinations of options.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
list[dict[str, str]]
|
|
165
|
+
A list of dictionaries where each dictionary represents a set of
|
|
166
|
+
options derived from the configuration.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
if self.configuration is None or len(self.configuration) == 0:
|
|
170
|
+
return [{}]
|
|
171
|
+
|
|
172
|
+
keys, value_lists = zip(*((config.name, config.values) for config in self.configuration))
|
|
173
|
+
combinations = [dict(zip(keys, values)) for values in itertools.product(*value_lists)]
|
|
174
|
+
|
|
175
|
+
return combinations
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _option_sets(scenarios: list[Scenario]) -> dict[str, dict[str, dict[str, str]]]:
|
|
179
|
+
"""
|
|
180
|
+
Creates options sets that are derived from `scenarios`. The options sets
|
|
181
|
+
are grouped by scenario ID. The cross-product of the configuration
|
|
182
|
+
options is created to generate all possible combinations of options.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
scenarios : list[Scenario]
|
|
187
|
+
List of scenarios to be tested.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
dict[str, dict[str, dict[str, str]]]
|
|
192
|
+
A dictionary where the keys are scenario IDs and the values are
|
|
193
|
+
dictionaries of option sets. Each option set is a dictionary where the
|
|
194
|
+
keys are option names and the values are the corresponding option
|
|
195
|
+
values.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
sets_by_scenario = {}
|
|
199
|
+
scenarios_by_id = _scenarios_by_id(scenarios)
|
|
200
|
+
for scenario_id, scenario in scenarios_by_id.items():
|
|
201
|
+
combinations = scenario.option_combinations()
|
|
202
|
+
option_sets = {}
|
|
203
|
+
for comb_ix, combination in enumerate(combinations):
|
|
204
|
+
option_sets[f"{scenario_id}_{comb_ix}"] = combination
|
|
205
|
+
|
|
206
|
+
sets_by_scenario[scenario_id] = option_sets
|
|
207
|
+
|
|
208
|
+
return sets_by_scenario
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _scenarios_by_id(scenarios: list[Scenario]) -> dict[str, Scenario]:
|
|
212
|
+
"""
|
|
213
|
+
This function maps a scenario to its ID. A scenario ID is created if it
|
|
214
|
+
wasn’t defined. This function also checks that there are no duplicate
|
|
215
|
+
scenario IDs.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
scenario_by_id = {}
|
|
219
|
+
ids_used = {}
|
|
220
|
+
for scenario_ix, scenario in enumerate(scenarios, start=1):
|
|
221
|
+
scenario_id = f"scenario-{scenario_ix}" if scenario.scenario_id is None else scenario.scenario_id
|
|
222
|
+
used = ids_used.get(scenario_id) is not None
|
|
223
|
+
if used:
|
|
224
|
+
raise ValueError(f"Duplicate scenario ID found: {scenario_id}")
|
|
225
|
+
|
|
226
|
+
ids_used[scenario_id] = True
|
|
227
|
+
scenario_by_id[scenario_id] = scenario
|
|
228
|
+
|
|
229
|
+
return scenario_by_id
|
nextmv/deprecated.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def deprecated(name: str, reason: str):
|
|
5
|
+
"""A very simple functon to mark something as deprecated."""
|
|
6
|
+
|
|
7
|
+
warnings.simplefilter("always", DeprecationWarning)
|
|
8
|
+
warnings.warn(
|
|
9
|
+
f"{name}: {reason}. This functionality will be removed in a future release",
|
|
10
|
+
category=DeprecationWarning,
|
|
11
|
+
stacklevel=2,
|
|
12
|
+
)
|
|
13
|
+
warnings.simplefilter("default", DeprecationWarning)
|
nextmv/input.py
CHANGED
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from typing import Any, Optional, Union
|
|
11
11
|
|
|
12
|
+
from nextmv.deprecated import deprecated
|
|
12
13
|
from nextmv.options import Options
|
|
13
14
|
|
|
14
15
|
|
|
@@ -311,6 +312,10 @@ def load_local(
|
|
|
311
312
|
csv_configurations: Optional[dict[str, Any]] = None,
|
|
312
313
|
) -> Input:
|
|
313
314
|
"""
|
|
315
|
+
DEPRECATION WARNING
|
|
316
|
+
----------
|
|
317
|
+
`load_local` is deprecated, use `load` instead.
|
|
318
|
+
|
|
314
319
|
This is a convenience function for instantiating a `LocalInputLoader`
|
|
315
320
|
and calling its `load` method.
|
|
316
321
|
|
|
@@ -357,5 +362,74 @@ def load_local(
|
|
|
357
362
|
If the path is not a directory when working with CSV_ARCHIVE.
|
|
358
363
|
"""
|
|
359
364
|
|
|
365
|
+
deprecated(
|
|
366
|
+
name="load_local",
|
|
367
|
+
reason="`load_local` is deprecated, use `load` instead.",
|
|
368
|
+
)
|
|
369
|
+
|
|
360
370
|
loader = LocalInputLoader()
|
|
361
371
|
return loader.load(input_format, options, path, csv_configurations)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
_LOCAL_INPUT_LOADER = LocalInputLoader()
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def load(
|
|
378
|
+
input_format: Optional[InputFormat] = InputFormat.JSON,
|
|
379
|
+
options: Optional[Options] = None,
|
|
380
|
+
path: Optional[str] = None,
|
|
381
|
+
csv_configurations: Optional[dict[str, Any]] = None,
|
|
382
|
+
loader: Optional[InputLoader] = _LOCAL_INPUT_LOADER,
|
|
383
|
+
) -> Input:
|
|
384
|
+
"""
|
|
385
|
+
This is a convenience function for loading an `Input`, i.e.: load the input
|
|
386
|
+
data. The `loader` is used to call the `.load` method. Note that the
|
|
387
|
+
default loader is the `LocalInputLoader`.
|
|
388
|
+
|
|
389
|
+
The input data can be in various formats. For
|
|
390
|
+
`InputFormat.JSON`, `InputFormat.TEXT`, and `InputFormat.CSV`, the data can
|
|
391
|
+
be streamed from stdin or read from a file. When the `path` argument is
|
|
392
|
+
provided (and valid), the input data is read from the file specified by
|
|
393
|
+
`path`, otherwise, it is streamed from stdin. For
|
|
394
|
+
`InputFormat.CSV_ARCHIVE`, the input data is read from the directory
|
|
395
|
+
specified by `path`. If the `path` is not provided, the default location
|
|
396
|
+
`input` is used. The directory should contain one or more files, where each
|
|
397
|
+
file in the directory is a CSV file.
|
|
398
|
+
|
|
399
|
+
The `Input` that is returned contains the `data` attribute. This data can
|
|
400
|
+
be of different types, depending on the provided `input_format`:
|
|
401
|
+
|
|
402
|
+
- `InputFormat.JSON`: the data is a `dict[str, Any]`.
|
|
403
|
+
- `InputFormat.TEXT`: the data is a `str`.
|
|
404
|
+
- `InputFormat.CSV`: the data is a `list[dict[str, Any]]`.
|
|
405
|
+
- `InputFormat.CSV_ARCHIVE`: the data is a `dict[str, list[dict[str, Any]]]`.
|
|
406
|
+
Each key is the name of the CSV file, minus the `.csv` extension.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
input_format: InputFormat, optional
|
|
411
|
+
Format of the input data. Default is `InputFormat.JSON`.
|
|
412
|
+
options: Options, optional
|
|
413
|
+
Options for loading the input data.
|
|
414
|
+
path: str, optional
|
|
415
|
+
Path to the input data.
|
|
416
|
+
csv_configurations: dict[str, Any], optional
|
|
417
|
+
Configurations for loading CSV files. The default `DictReader` is used
|
|
418
|
+
when loading a CSV file, so you have the option to pass in a dictionary
|
|
419
|
+
with custom kwargs for the `DictReader`.
|
|
420
|
+
loader: InputLoader, optional
|
|
421
|
+
The loader to use for loading the input data. Default is
|
|
422
|
+
`LocalInputLoader`.
|
|
423
|
+
|
|
424
|
+
Returns
|
|
425
|
+
-------
|
|
426
|
+
Input
|
|
427
|
+
The input data.
|
|
428
|
+
|
|
429
|
+
Raises
|
|
430
|
+
------
|
|
431
|
+
ValueError
|
|
432
|
+
If the path is not a directory when working with CSV_ARCHIVE.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
return loader.load(input_format, options, path, csv_configurations)
|