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.
@@ -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)