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
nextmv/cloud/scenario.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""This module contains definitions for scenario tests.
|
|
2
|
+
|
|
3
|
+
Classes
|
|
4
|
+
-------
|
|
5
|
+
ScenarioConfiguration
|
|
6
|
+
Configuration for a scenario with multiple option values.
|
|
7
|
+
ScenarioInputType
|
|
8
|
+
Enumeration of input types for a scenario.
|
|
9
|
+
ScenarioInput
|
|
10
|
+
Input to be processed in a scenario.
|
|
11
|
+
Scenario
|
|
12
|
+
A test case for comparing a decision model with inputs and configurations.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import itertools
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ScenarioConfiguration:
|
|
23
|
+
"""
|
|
24
|
+
Configuration for a scenario.
|
|
25
|
+
|
|
26
|
+
You can import the `ScenarioConfiguration` class directly from `cloud`:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from nextmv.cloud import ScenarioConfiguration
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You can define multiple values for a single option, which will result in
|
|
33
|
+
multiple runs being created. For example, if you have a configuration
|
|
34
|
+
option "x" with values [1, 2], and a configuration option "y" with values
|
|
35
|
+
[3, 4], then the following runs will be created:
|
|
36
|
+
- x=1, y=3
|
|
37
|
+
- x=1, y=4
|
|
38
|
+
- x=2, y=3
|
|
39
|
+
- x=2, y=4
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
name : str
|
|
44
|
+
Name of the configuration option.
|
|
45
|
+
values : list[str]
|
|
46
|
+
List of values for the configuration option.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
--------
|
|
50
|
+
>>> from nextmv.cloud import ScenarioConfiguration
|
|
51
|
+
>>> config = ScenarioConfiguration(name="solver", values=["simplex", "interior-point"])
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str
|
|
55
|
+
"""Name of the configuration option."""
|
|
56
|
+
values: list[str]
|
|
57
|
+
"""List of values for the configuration option."""
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
"""
|
|
61
|
+
Post-initialization method to ensure that the values are unique.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
ValueError
|
|
66
|
+
If the configuration values list is empty.
|
|
67
|
+
"""
|
|
68
|
+
if len(self.values) <= 0:
|
|
69
|
+
raise ValueError("Configuration values must be non-empty.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ScenarioInputType(str, Enum):
|
|
73
|
+
"""
|
|
74
|
+
Type of input for a scenario. This is used to determine how the input
|
|
75
|
+
should be processed.
|
|
76
|
+
|
|
77
|
+
You can import the `ScenarioInputType` class directly from `cloud`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from nextmv.cloud import ScenarioInputType
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
INPUT_SET : str
|
|
86
|
+
The data in the scenario is an input set.
|
|
87
|
+
INPUT : str
|
|
88
|
+
The data in the scenario is an input.
|
|
89
|
+
NEW : str
|
|
90
|
+
The data in the scenario is new data.
|
|
91
|
+
|
|
92
|
+
Examples
|
|
93
|
+
--------
|
|
94
|
+
>>> from nextmv.cloud import ScenarioInputType
|
|
95
|
+
>>> input_type = ScenarioInputType.INPUT_SET
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
INPUT_SET = "input_set"
|
|
99
|
+
"""The data in the scenario is an input set."""
|
|
100
|
+
INPUT = "input"
|
|
101
|
+
"""The data in the scenario is an input."""
|
|
102
|
+
NEW = "new"
|
|
103
|
+
"""The data in the scenario is new data."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ScenarioInput:
|
|
108
|
+
"""
|
|
109
|
+
Input to be processed in a scenario.
|
|
110
|
+
|
|
111
|
+
You can import the `ScenarioInput` class directly from `cloud`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from nextmv.cloud import ScenarioInput
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The type of input is determined by the `scenario_input_type` attribute.
|
|
118
|
+
The input can be a single input set ID, a list of input IDs, or raw data.
|
|
119
|
+
The data itself of the scenario input is tracked by the `scenario_input_data`
|
|
120
|
+
attribute.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
scenario_input_type : ScenarioInputType
|
|
125
|
+
Type of input for the scenario. This is used to determine how the input
|
|
126
|
+
should be processed.
|
|
127
|
+
scenario_input_data : Union[str, list[str], list[dict[str, Any]]]
|
|
128
|
+
Input data for the scenario. This can be a single input set ID
|
|
129
|
+
(`str`), a list of input IDs (`list[str]`), or raw data
|
|
130
|
+
(`list[dict[str, Any]]`). If you provide a `list[str]` (list of
|
|
131
|
+
inputs), a new input set will be created using these inputs. A similar
|
|
132
|
+
behavior occurs when providing raw data (`list[dict[str, Any]]`). All
|
|
133
|
+
the entries in the list of raw dicts will be collected to create a new
|
|
134
|
+
input set.
|
|
135
|
+
|
|
136
|
+
Examples
|
|
137
|
+
--------
|
|
138
|
+
>>> from nextmv.cloud import ScenarioInput, ScenarioInputType
|
|
139
|
+
>>> # Using an existing input set
|
|
140
|
+
>>> input_set = ScenarioInput(
|
|
141
|
+
... scenario_input_type=ScenarioInputType.INPUT_SET,
|
|
142
|
+
... scenario_input_data="input-set-id-123"
|
|
143
|
+
... )
|
|
144
|
+
>>> # Using a list of inputs
|
|
145
|
+
>>> inputs = ScenarioInput(
|
|
146
|
+
... scenario_input_type=ScenarioInputType.INPUT,
|
|
147
|
+
... scenario_input_data=["input-id-1", "input-id-2"]
|
|
148
|
+
... )
|
|
149
|
+
>>> # Using raw data
|
|
150
|
+
>>> raw_data = ScenarioInput(
|
|
151
|
+
... scenario_input_type=ScenarioInputType.NEW,
|
|
152
|
+
... scenario_input_data=[{"id": 1, "value": "data1"}, {"id": 2, "value": "data2"}]
|
|
153
|
+
... )
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
scenario_input_type: ScenarioInputType
|
|
157
|
+
"""
|
|
158
|
+
Type of input for the scenario. This is used to determine how the input
|
|
159
|
+
should be processed.
|
|
160
|
+
"""
|
|
161
|
+
scenario_input_data: str | list[str] | list[dict[str, Any]]
|
|
162
|
+
"""
|
|
163
|
+
Input data for the scenario. This can be a single input set ID (`str`), a
|
|
164
|
+
list of input IDs (`list[str]`), or raw data (`list[dict[str, Any]]`).
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __post_init__(self):
|
|
168
|
+
"""
|
|
169
|
+
Post-initialization method to ensure that the input data is valid.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
ValueError
|
|
174
|
+
If the scenario input type and data type don't match:
|
|
175
|
+
- When using INPUT_SET, scenario_input_data must be a string
|
|
176
|
+
- When using INPUT or NEW, scenario_input_data must be a list
|
|
177
|
+
"""
|
|
178
|
+
if self.scenario_input_type == ScenarioInputType.INPUT_SET and not isinstance(self.scenario_input_data, str):
|
|
179
|
+
raise ValueError("Scenario input type must be a string when using an input set.")
|
|
180
|
+
elif self.scenario_input_type == ScenarioInputType.INPUT and not isinstance(self.scenario_input_data, list):
|
|
181
|
+
raise ValueError("Scenario input type must be a list when using an input.")
|
|
182
|
+
elif self.scenario_input_type == ScenarioInputType.NEW and not isinstance(self.scenario_input_data, list):
|
|
183
|
+
raise ValueError("Scenario input type must be a list when using new data.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class Scenario:
|
|
188
|
+
"""
|
|
189
|
+
A scenario is a test case that is used to compare a decision model being
|
|
190
|
+
executed with a set of inputs and configurations.
|
|
191
|
+
|
|
192
|
+
You can import the `Scenario` class directly from `cloud`:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from nextmv.cloud import Scenario
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A scenario encapsulates all the necessary information to run a test case
|
|
199
|
+
against a decision model. Each scenario includes input data, an instance ID,
|
|
200
|
+
and can optionally include configuration options that define different
|
|
201
|
+
variations of the run.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
scenario_input : ScenarioInput
|
|
206
|
+
Input for the scenario. The input is composed of a type and data. Make
|
|
207
|
+
sure you use the `ScenarioInput` class to create the input.
|
|
208
|
+
instance_id : str
|
|
209
|
+
ID of the instance to be used for the scenario.
|
|
210
|
+
scenario_id : Optional[str]
|
|
211
|
+
Optional ID of the scenario. The default value will be set as
|
|
212
|
+
`scenario-<index>` if not set.
|
|
213
|
+
configuration : Optional[list[ScenarioConfiguration]]
|
|
214
|
+
Optional configuration for the scenario. Use this attribute to
|
|
215
|
+
configure variation of options for the scenario.
|
|
216
|
+
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
>>> from nextmv.cloud import Scenario, ScenarioInput, ScenarioInputType, ScenarioConfiguration
|
|
220
|
+
>>> # Creating a simple scenario with an input set
|
|
221
|
+
>>> input_data = ScenarioInput(
|
|
222
|
+
... scenario_input_type=ScenarioInputType.INPUT_SET,
|
|
223
|
+
... scenario_input_data="input-set-id-123"
|
|
224
|
+
... )
|
|
225
|
+
>>> scenario = Scenario(
|
|
226
|
+
... scenario_input=input_data,
|
|
227
|
+
... instance_id="instance-id-456",
|
|
228
|
+
... scenario_id="my-test-scenario"
|
|
229
|
+
... )
|
|
230
|
+
>>>
|
|
231
|
+
>>> # Creating a scenario with configuration options
|
|
232
|
+
>>> config_options = [
|
|
233
|
+
... ScenarioConfiguration(name="solver", values=["simplex", "interior-point"]),
|
|
234
|
+
... ScenarioConfiguration(name="timeout", values=["10", "30", "60"])
|
|
235
|
+
... ]
|
|
236
|
+
>>> scenario_with_config = Scenario(
|
|
237
|
+
... scenario_input=input_data,
|
|
238
|
+
... instance_id="instance-id-456",
|
|
239
|
+
... configuration=config_options
|
|
240
|
+
... )
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
scenario_input: ScenarioInput
|
|
244
|
+
"""
|
|
245
|
+
Input for the scenario. The input is composed of a type and data. Make sure
|
|
246
|
+
you use the `ScenarioInput` class to create the input.
|
|
247
|
+
"""
|
|
248
|
+
instance_id: str
|
|
249
|
+
"""ID of the instance to be used for the scenario."""
|
|
250
|
+
|
|
251
|
+
scenario_id: str | None = None
|
|
252
|
+
"""
|
|
253
|
+
Optional ID of the scenario. The default value will be set as
|
|
254
|
+
`scenario-<index>` if not set.
|
|
255
|
+
"""
|
|
256
|
+
configuration: list[ScenarioConfiguration] | None = None
|
|
257
|
+
"""Optional configuration for the scenario. Use this attribute to configure
|
|
258
|
+
variation of options for the scenario.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def option_combinations(self) -> list[dict[str, str]]:
|
|
262
|
+
"""
|
|
263
|
+
Creates the combination of options that are derived from the
|
|
264
|
+
`configuration` property.
|
|
265
|
+
|
|
266
|
+
This method calculates the cross-product of all configuration
|
|
267
|
+
options to generate all possible combinations. If no configuration
|
|
268
|
+
is provided, it returns a list with an empty dictionary.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
list[dict[str, str]]
|
|
273
|
+
A list of dictionaries where each dictionary represents a set of
|
|
274
|
+
options derived from the configuration.
|
|
275
|
+
|
|
276
|
+
Examples
|
|
277
|
+
--------
|
|
278
|
+
>>> from nextmv.cloud import Scenario, ScenarioInput, ScenarioInputType, ScenarioConfiguration
|
|
279
|
+
>>> input_data = ScenarioInput(
|
|
280
|
+
... scenario_input_type=ScenarioInputType.INPUT_SET,
|
|
281
|
+
... scenario_input_data="input-set-id"
|
|
282
|
+
... )
|
|
283
|
+
>>> config = [
|
|
284
|
+
... ScenarioConfiguration(name="x", values=["1", "2"]),
|
|
285
|
+
... ScenarioConfiguration(name="y", values=["3", "4"])
|
|
286
|
+
... ]
|
|
287
|
+
>>> scenario = Scenario(
|
|
288
|
+
... scenario_input=input_data,
|
|
289
|
+
... instance_id="instance-id",
|
|
290
|
+
... configuration=config
|
|
291
|
+
... )
|
|
292
|
+
>>> scenario.option_combinations()
|
|
293
|
+
[{'x': '1', 'y': '3'}, {'x': '1', 'y': '4'}, {'x': '2', 'y': '3'}, {'x': '2', 'y': '4'}]
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
if self.configuration is None or len(self.configuration) == 0:
|
|
297
|
+
return [{}]
|
|
298
|
+
|
|
299
|
+
keys, value_lists = zip(*((config.name, config.values) for config in self.configuration), strict=False)
|
|
300
|
+
combinations = [dict(zip(keys, values, strict=False)) for values in itertools.product(*value_lists)]
|
|
301
|
+
|
|
302
|
+
return combinations
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _option_sets(scenarios: list[Scenario]) -> dict[str, dict[str, dict[str, str]]]:
|
|
306
|
+
"""
|
|
307
|
+
Creates options sets that are derived from `scenarios`.
|
|
308
|
+
|
|
309
|
+
The options sets are grouped by scenario ID. The cross-product of the
|
|
310
|
+
configuration options is created to generate all possible combinations
|
|
311
|
+
of options. Each combination is given a unique key based on the scenario ID
|
|
312
|
+
and a combination index.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
scenarios : list[Scenario]
|
|
317
|
+
List of scenarios to be tested.
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
dict[str, dict[str, dict[str, str]]]
|
|
322
|
+
A dictionary where the keys are scenario IDs and the values are
|
|
323
|
+
dictionaries of option sets. Each option set is a dictionary where the
|
|
324
|
+
keys are option names and the values are the corresponding option
|
|
325
|
+
values.
|
|
326
|
+
|
|
327
|
+
Examples
|
|
328
|
+
--------
|
|
329
|
+
>>> from nextmv.cloud import Scenario, ScenarioInput, ScenarioInputType, ScenarioConfiguration
|
|
330
|
+
>>> input_data = ScenarioInput(
|
|
331
|
+
... scenario_input_type=ScenarioInputType.INPUT_SET,
|
|
332
|
+
... scenario_input_data="input-set-id"
|
|
333
|
+
... )
|
|
334
|
+
>>> config = [ScenarioConfiguration(name="x", values=["1", "2"])]
|
|
335
|
+
>>> scenario = Scenario(
|
|
336
|
+
... scenario_input=input_data,
|
|
337
|
+
... instance_id="instance-id",
|
|
338
|
+
... scenario_id="test-scenario",
|
|
339
|
+
... configuration=config
|
|
340
|
+
... )
|
|
341
|
+
>>> _option_sets([scenario])
|
|
342
|
+
{'test-scenario': {'test-scenario_0': {'x': '1'}, 'test-scenario_1': {'x': '2'}}}
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
sets_by_scenario = {}
|
|
346
|
+
scenarios_by_id = _scenarios_by_id(scenarios)
|
|
347
|
+
for scenario_id, scenario in scenarios_by_id.items():
|
|
348
|
+
combinations = scenario.option_combinations()
|
|
349
|
+
option_sets = {}
|
|
350
|
+
for comb_ix, combination in enumerate(combinations):
|
|
351
|
+
option_sets[f"{scenario_id}_{comb_ix}"] = combination
|
|
352
|
+
|
|
353
|
+
sets_by_scenario[scenario_id] = option_sets
|
|
354
|
+
|
|
355
|
+
return sets_by_scenario
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _scenarios_by_id(scenarios: list[Scenario]) -> dict[str, Scenario]:
|
|
359
|
+
"""
|
|
360
|
+
Maps scenarios to their IDs.
|
|
361
|
+
|
|
362
|
+
This function builds a dictionary that maps each scenario to its ID.
|
|
363
|
+
If a scenario doesn't have an ID defined, one is created using the format
|
|
364
|
+
"scenario-{index}". The function also checks that there are no duplicate
|
|
365
|
+
scenario IDs in the provided list.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
scenarios : list[Scenario]
|
|
370
|
+
List of scenarios to be mapped.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
dict[str, Scenario]
|
|
375
|
+
A dictionary where keys are scenario IDs and values are the corresponding
|
|
376
|
+
Scenario objects.
|
|
377
|
+
|
|
378
|
+
Raises
|
|
379
|
+
------
|
|
380
|
+
ValueError
|
|
381
|
+
If duplicate scenario IDs are found in the list.
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> from nextmv.cloud import Scenario, ScenarioInput, ScenarioInputType
|
|
386
|
+
>>> input_data = ScenarioInput(
|
|
387
|
+
... scenario_input_type=ScenarioInputType.INPUT_SET,
|
|
388
|
+
... scenario_input_data="input-set-id"
|
|
389
|
+
... )
|
|
390
|
+
>>> scenarios = [
|
|
391
|
+
... Scenario(scenario_input=input_data, instance_id="instance-1", scenario_id="test-1"),
|
|
392
|
+
... Scenario(scenario_input=input_data, instance_id="instance-2")
|
|
393
|
+
... ]
|
|
394
|
+
>>> result = _scenarios_by_id(scenarios)
|
|
395
|
+
>>> sorted(list(result.keys()))
|
|
396
|
+
['scenario-2', 'test-1']
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
scenario_by_id = {}
|
|
400
|
+
ids_used = {}
|
|
401
|
+
for scenario_ix, scenario in enumerate(scenarios, start=1):
|
|
402
|
+
scenario_id = f"scenario-{scenario_ix}" if scenario.scenario_id is None else scenario.scenario_id
|
|
403
|
+
used = ids_used.get(scenario_id) is not None
|
|
404
|
+
if used:
|
|
405
|
+
raise ValueError(f"Duplicate scenario ID found: {scenario_id}")
|
|
406
|
+
|
|
407
|
+
ids_used[scenario_id] = True
|
|
408
|
+
scenario_by_id[scenario_id] = scenario
|
|
409
|
+
|
|
410
|
+
return scenario_by_id
|
nextmv/cloud/secrets.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""This module contains the declarations for secrets management.
|
|
2
|
+
|
|
3
|
+
Classes
|
|
4
|
+
-------
|
|
5
|
+
SecretsCollectionSummary
|
|
6
|
+
Summary of a secrets collection in Nextmv Cloud.
|
|
7
|
+
SecretType
|
|
8
|
+
Enumeration of available secret types.
|
|
9
|
+
Secret
|
|
10
|
+
Representation of a sensitive piece of information.
|
|
11
|
+
SecretsCollection
|
|
12
|
+
Collection of secrets hosted in the Nextmv Cloud.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
from pydantic import AliasChoices, Field
|
|
20
|
+
|
|
21
|
+
from nextmv.base_model import BaseModel
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SecretsCollectionSummary(BaseModel):
|
|
25
|
+
"""The summary of a secrets collection in the Nextmv Cloud.
|
|
26
|
+
|
|
27
|
+
You can import the `SecretsCollectionSummary` class directly from `cloud`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from nextmv.cloud import SecretsCollectionSummary
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A secrets collection is a mechanism for hosting secrets securely in the
|
|
34
|
+
Nextmv Cloud. This class provides summary information about such a collection.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
collection_id : str
|
|
39
|
+
ID of the secrets collection. This is aliased from `id` for
|
|
40
|
+
serialization and validation.
|
|
41
|
+
application_id : str
|
|
42
|
+
ID of the application to which the secrets collection belongs.
|
|
43
|
+
name : str
|
|
44
|
+
Name of the secrets collection.
|
|
45
|
+
description : str
|
|
46
|
+
Description of the secrets collection.
|
|
47
|
+
created_at : datetime
|
|
48
|
+
Creation date of the secrets collection.
|
|
49
|
+
updated_at : datetime
|
|
50
|
+
Last update date of the secrets collection.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> from datetime import datetime
|
|
55
|
+
>>> collection_summary = SecretsCollectionSummary(
|
|
56
|
+
... collection_id="col_123",
|
|
57
|
+
... application_id="app_456",
|
|
58
|
+
... name="My API Credentials",
|
|
59
|
+
... description="Collection of API keys for external services",
|
|
60
|
+
... created_at=datetime.now(),
|
|
61
|
+
... updated_at=datetime.now()
|
|
62
|
+
... )
|
|
63
|
+
>>> print(collection_summary.name)
|
|
64
|
+
My API Credentials
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
collection_id: str = Field(
|
|
69
|
+
serialization_alias="id",
|
|
70
|
+
validation_alias=AliasChoices("id", "collection_id"),
|
|
71
|
+
)
|
|
72
|
+
"""ID of the secrets collection."""
|
|
73
|
+
application_id: str
|
|
74
|
+
"""ID of the application to which the secrets collection belongs."""
|
|
75
|
+
name: str
|
|
76
|
+
"""Name of the secrets collection."""
|
|
77
|
+
description: str
|
|
78
|
+
"""Description of the secrets collection."""
|
|
79
|
+
created_at: datetime
|
|
80
|
+
"""Creation date of the secrets collection."""
|
|
81
|
+
updated_at: datetime
|
|
82
|
+
"""Last update date of the secrets collection."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SecretType(str, Enum):
|
|
86
|
+
"""Type of the secret that is stored in the Nextmv Cloud.
|
|
87
|
+
|
|
88
|
+
You can import the `SecretType` class directly from `cloud`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from nextmv.cloud import SecretType
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This enumeration defines the types of secrets that can be managed.
|
|
95
|
+
|
|
96
|
+
Attributes
|
|
97
|
+
----------
|
|
98
|
+
ENV : str
|
|
99
|
+
Represents an environment variable secret. The value of the secret
|
|
100
|
+
will be available as an environment variable in the execution
|
|
101
|
+
environment.
|
|
102
|
+
FILE : str
|
|
103
|
+
Represents a file-based secret. The value of the secret will be
|
|
104
|
+
written to a file, and the path to this file will be available
|
|
105
|
+
via the `location` attribute of the `Secret`.
|
|
106
|
+
|
|
107
|
+
Examples
|
|
108
|
+
--------
|
|
109
|
+
>>> secret_type_env = SecretType.ENV
|
|
110
|
+
>>> print(secret_type_env.value)
|
|
111
|
+
env
|
|
112
|
+
>>> secret_type_file = SecretType.FILE
|
|
113
|
+
>>> print(secret_type_file.value)
|
|
114
|
+
file
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
ENV = "env"
|
|
119
|
+
"""Environment variable secret type."""
|
|
120
|
+
FILE = "file"
|
|
121
|
+
"""File secret type."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Secret(BaseModel):
|
|
125
|
+
"""A secret is a piece of sensitive information that is stored securely in
|
|
126
|
+
the Nextmv Cloud.
|
|
127
|
+
|
|
128
|
+
You can import the `Secret` class directly from `cloud`:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from nextmv.cloud import Secret
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This class represents an individual secret, detailing its type, location
|
|
135
|
+
(if applicable), and value.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
secret_type : SecretType
|
|
140
|
+
The type of the secret, indicating how it should be handled (e.g.,
|
|
141
|
+
as an environment variable or a file). This is aliased from `type`
|
|
142
|
+
for serialization and validation.
|
|
143
|
+
location : str
|
|
144
|
+
The location where the secret will be made available. For `ENV`
|
|
145
|
+
type secrets, this is the name of the environment variable. For
|
|
146
|
+
`FILE` type secrets, this is the path where the file will be
|
|
147
|
+
created.
|
|
148
|
+
value : str
|
|
149
|
+
The actual content of the secret.
|
|
150
|
+
|
|
151
|
+
Examples
|
|
152
|
+
--------
|
|
153
|
+
>>> env_secret = Secret(
|
|
154
|
+
... secret_type=SecretType.ENV,
|
|
155
|
+
... location="API_KEY",
|
|
156
|
+
... value="supersecretapikey123"
|
|
157
|
+
... )
|
|
158
|
+
>>> print(env_secret.location)
|
|
159
|
+
API_KEY
|
|
160
|
+
>>> file_secret = Secret(
|
|
161
|
+
... secret_type=SecretType.FILE,
|
|
162
|
+
... location="/mnt/secrets/config.json",
|
|
163
|
+
... value=\'\'\'{"user": "admin", "pass": "secure"}\'\'\'
|
|
164
|
+
... )
|
|
165
|
+
>>> print(file_secret.secret_type)
|
|
166
|
+
SecretType.FILE
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
secret_type: SecretType = Field(
|
|
171
|
+
serialization_alias="type",
|
|
172
|
+
validation_alias=AliasChoices("type", "secret_type"),
|
|
173
|
+
)
|
|
174
|
+
"""Type of the secret."""
|
|
175
|
+
location: str
|
|
176
|
+
"""Location of the secret."""
|
|
177
|
+
value: str
|
|
178
|
+
"""Value of the secret."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SecretsCollection(SecretsCollectionSummary, BaseModel):
|
|
182
|
+
"""A secrets collection is a mechanism for hosting secrets securely in the
|
|
183
|
+
Nextmv Cloud.
|
|
184
|
+
|
|
185
|
+
You can import the `SecretsCollection` class directly from `cloud`:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from nextmv.cloud import SecretsCollection
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This class extends `SecretsCollectionSummary` by including the actual list
|
|
192
|
+
of secrets contained within the collection.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
secrets : list[Secret]
|
|
197
|
+
A list of `Secret` objects that are part of this collection.
|
|
198
|
+
*args
|
|
199
|
+
Variable length argument list for `SecretsCollectionSummary`.
|
|
200
|
+
**kwargs
|
|
201
|
+
Arbitrary keyword arguments for `SecretsCollectionSummary`.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
Examples
|
|
205
|
+
--------
|
|
206
|
+
>>> from datetime import datetime
|
|
207
|
+
>>> secret1 = Secret(
|
|
208
|
+
... secret_type=SecretType.ENV,
|
|
209
|
+
... location="DATABASE_USER",
|
|
210
|
+
... value="nextmv_user"
|
|
211
|
+
... )
|
|
212
|
+
>>> secret2 = Secret(
|
|
213
|
+
... secret_type=SecretType.FILE,
|
|
214
|
+
... location="/etc/app/license.key",
|
|
215
|
+
... value="longlicensekeystring"
|
|
216
|
+
... )
|
|
217
|
+
>>> full_collection = SecretsCollection(
|
|
218
|
+
... collection_id="col_789",
|
|
219
|
+
... application_id="app_000",
|
|
220
|
+
... name="Full App Secrets",
|
|
221
|
+
... description="All secrets required by the main application",
|
|
222
|
+
... created_at=datetime.now(),
|
|
223
|
+
... updated_at=datetime.now(),
|
|
224
|
+
... secrets=[secret1, secret2]
|
|
225
|
+
... )
|
|
226
|
+
>>> print(full_collection.name)
|
|
227
|
+
Full App Secrets
|
|
228
|
+
>>> print(len(full_collection.secrets))
|
|
229
|
+
2
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
secrets: list[Secret]
|
|
234
|
+
"""List of secrets in the collection."""
|