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.
Files changed (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -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."""