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
nextmv/options.py CHANGED
@@ -1,18 +1,38 @@
1
- """Configuration for a run."""
1
+ """
2
+ Configuration management for application runs.
3
+
4
+ This module provides classes for handling configuration options for
5
+ applications. It supports reading options from command-line arguments,
6
+ environment variables, and default values in a prioritized manner. The module
7
+ includes classes for defining individual options (`Option`) and managing
8
+ collections of options (`Options`).
9
+
10
+ Classes
11
+ -------
12
+ Option
13
+ Class for defining individual options for configuration.
14
+ Options
15
+ Class for managing collections of options.
16
+ """
2
17
 
3
18
  import argparse
4
19
  import builtins
5
20
  import copy
21
+ import json
6
22
  import os
7
23
  from dataclasses import dataclass
8
- from typing import Any, Optional
24
+ from typing import Any
9
25
 
10
26
  from nextmv.base_model import BaseModel
27
+ from nextmv.deprecated import deprecated
11
28
 
12
29
 
13
30
  @dataclass
14
31
  class Parameter:
15
32
  """
33
+ !!! warning
34
+ `Parameter` is deprecated, use `Option` instead.
35
+
16
36
  Parameter that is used in a `Configuration`. When a parameter is required,
17
37
  it is a good practice to provide a default value for it. This is because
18
38
  the configuration will raise an error if a required parameter is not
@@ -23,20 +43,30 @@ class Parameter:
23
43
  ----------
24
44
  name : str
25
45
  The name of the parameter.
46
+
26
47
  param_type : type
27
48
  The type of the parameter.
49
+
28
50
  default : Any, optional
29
51
  The default value of the parameter. Even though this is optional, it is
30
52
  recommended to provide a default value for all parameters.
53
+
31
54
  description : str, optional
32
55
  An optional description of the parameter. This is useful for generating
33
56
  help messages for the configuration.
57
+
34
58
  required : bool, optional
35
59
  Whether the parameter is required. If a parameter is required, it will
36
- be an error to not provide a value for it, either trough a command-line
60
+ be an error to not provide a value for it, either through a command-line
37
61
  argument, an environment variable or a default value.
62
+
38
63
  choices : list[Optional[Any]], optional
39
64
  Limits values to a specific set of choices.
65
+
66
+ Examples
67
+ --------
68
+ >>> from nextmv.options import Parameter
69
+ >>> parameter = Parameter("timeout", int, 60, "The maximum timeout in seconds", required=True)
40
70
  """
41
71
 
42
72
  name: str
@@ -44,22 +74,38 @@ class Parameter:
44
74
  param_type: type
45
75
  """The type of the parameter."""
46
76
 
47
- default: Optional[Any] = None
77
+ default: Any | None = None
48
78
  """The default value of the parameter. Even though this is optional, it is
49
79
  recommended to provide a default value for all parameters."""
50
- description: Optional[str] = None
80
+ description: str | None = None
51
81
  """An optional description of the parameter. This is useful for generating
52
82
  help messages for the configuration."""
53
83
  required: bool = False
54
84
  """Whether the parameter is required. If a parameter is required, it will
55
85
  be an error to not provide a value for it, either trough a command-line
56
86
  argument, an environment variable or a default value."""
57
- choices: list[Optional[Any]] = None
87
+ choices: list[Any | None] = None
58
88
  """Limits values to a specific set of choices."""
59
89
 
90
+ def __post_init__(self):
91
+ """
92
+ Post-initialization hook that marks this class as deprecated.
93
+
94
+ This method is automatically called after the object is initialized.
95
+ It displays a deprecation warning to inform users to use the `Option` class instead.
96
+ """
97
+ deprecated(
98
+ name="Parameter",
99
+ reason="`Parameter` is deprecated, use `Option` instead",
100
+ )
101
+
60
102
  @classmethod
61
103
  def from_dict(cls, data: dict[str, Any]) -> "Parameter":
62
104
  """
105
+ !!! warning
106
+ `Parameter` is deprecated, use `Option` instead.
107
+ `Parameter.from_dict` -> `Option.from_dict`
108
+
63
109
  Creates an instance of `Parameter` from a dictionary.
64
110
 
65
111
  Parameters
@@ -73,6 +119,11 @@ class Parameter:
73
119
  An instance of `Parameter`.
74
120
  """
75
121
 
122
+ deprecated(
123
+ name="Parameter.from_dict",
124
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.from_dict -> Option.from_dict",
125
+ )
126
+
76
127
  param_type_string = data["param_type"]
77
128
  param_type = getattr(builtins, param_type_string.split("'")[1])
78
129
 
@@ -87,14 +138,33 @@ class Parameter:
87
138
 
88
139
  def to_dict(self) -> dict[str, Any]:
89
140
  """
141
+ !!! warning
142
+ `Parameter` is deprecated, use `Option` instead.
143
+ `Parameter.to_dict` -> `Option.to_dict`
144
+
90
145
  Converts the parameter to a dict.
91
146
 
92
147
  Returns
93
148
  -------
94
149
  dict[str, Any]
95
- The parameter as a dict.
150
+ The parameter as a dict with its name, type, default value,
151
+ description, required flag, and choices.
152
+
153
+ Examples
154
+ --------
155
+ >>> param = Parameter("timeout", int, 60, "Maximum time in seconds", True)
156
+ >>> param_dict = param.to_dict()
157
+ >>> param_dict["name"]
158
+ 'timeout'
159
+ >>> param_dict["default"]
160
+ 60
96
161
  """
97
162
 
163
+ deprecated(
164
+ name="Parameter.to_dict",
165
+ reason="`Parameter` is deprecated, use `Option` instead. Parameter.to_dict -> Option.to_dict",
166
+ )
167
+
98
168
  return {
99
169
  "name": self.name,
100
170
  "param_type": str(self.param_type),
@@ -105,19 +175,219 @@ class Parameter:
105
175
  }
106
176
 
107
177
 
178
+ @dataclass
179
+ class Option:
180
+ """
181
+ An option that is used in `Options`.
182
+
183
+ You can import the `Option` class directly from `nextmv`:
184
+
185
+ ```python
186
+ from nextmv import Option
187
+ ```
188
+
189
+ Options provide a way to configure application behavior. When an `Option`
190
+ is required, it is a good practice to provide a default value for it. This
191
+ is because the `Options` will raise an error if a required `Option` is not
192
+ provided through a command-line argument, an environment variable or a
193
+ default value.
194
+
195
+ Parameters
196
+ ----------
197
+ name : str
198
+ `name`. The name of the option.
199
+ option_type : type
200
+ The type of the option.
201
+ default : Any, optional
202
+ The default value of the option. Even though this is optional, it is
203
+ recommended to provide a default value for all options.
204
+ description : str, optional
205
+ An optional description of the option. This is useful for generating
206
+ help messages for the `Options`.
207
+ required : bool, optional
208
+ Whether the option is required. If an option is required, it will
209
+ be an error to not provide a value for it, either through a command-line
210
+ argument, an environment variable or a default value.
211
+ choices : list[Optional[Any]], optional
212
+ Limits values to a specific set of choices.
213
+ additional_attributes : dict[str, Any], optional
214
+ Optional additional attributes for the option. The Nextmv Cloud may
215
+ perform validation on these attributes. For example, the maximum length
216
+ of a string or the maximum value of an integer. These additional
217
+ attributes will be shown in the help message of the `Options`.
218
+ control_type : str, optional
219
+ The type of control to use for the option in the Nextmv Cloud UI. This is
220
+ useful for defining how the option should be presented in the Nextmv
221
+ Cloud UI. Current control types include "input", "select", "slider", and
222
+ "toggle". This attribute is not used in the local `Options` class, but '
223
+ it is used in the Nextmv Cloud UI to define the type of control to use for
224
+ the option. This will be validated by the Nextmv Cloud, and availability
225
+ is based on options_type.
226
+ hidden_from : list[str], optional
227
+ A list of team roles to which this option will be hidden in the UI. For
228
+ example, if you want to hide an option from the "operator" role, you can
229
+ pass `hidden_from=["operator"]`.
230
+ display_name : str, optional
231
+ An optional display name for the option. This is useful for making
232
+ the option more user-friendly in the UI.
233
+
234
+ Examples
235
+ --------
236
+ ```python
237
+ from nextmv.options import Option
238
+ opt = Option("duration", str, "30s", description="solver duration", required=False)
239
+ opt.name
240
+ opt.default
241
+ ```
242
+ """
243
+
244
+ name: str
245
+ """The name of the option."""
246
+ option_type: type
247
+ """The type of the option."""
248
+
249
+ default: Any | None = None
250
+ """
251
+ The default value of the option. Even though this is optional, it is
252
+ recommended to provide a default value for all options.
253
+ """
254
+ description: str | None = None
255
+ """
256
+ An optional description of the option. This is useful for generating help
257
+ messages for the `Options`.
258
+ """
259
+ required: bool = False
260
+ """
261
+ Whether the option is required. If a option is required, it will be an
262
+ error to not provide a value for it, either trough a command-line argument,
263
+ an environment variable or a default value.
264
+ """
265
+ choices: list[Any] | None = None
266
+ """Limits values to a specific set of choices."""
267
+ additional_attributes: dict[str, Any] | None = None
268
+ """
269
+ Optional additional attributes for the option. The Nextmv Cloud may
270
+ perform validation on these attributes. For example, the maximum length of
271
+ a string or the maximum value of an integer. These additional attributes
272
+ will be shown in the help message of the `Options`.
273
+ """
274
+ control_type: str | None = None
275
+ """
276
+ The type of control to use for the option in the Nextmv Cloud UI. This is
277
+ useful for defining how the option should be presented in the Nextmv
278
+ Cloud UI. Current control types include "input", "select", "slider", and
279
+ "toggle". This attribute is not used in the local `Options` class, but it
280
+ is used in the Nextmv Cloud UI to define the type of control to use for
281
+ the option. This will be validated by the Nextmv Cloud, and availability
282
+ is based on options_type.
283
+ """
284
+ hidden_from: list[str] | None = None
285
+ """
286
+ A list of team roles for which this option will be hidden in the UI. For
287
+ example, if you want to hide an option from the "operator" role, you can
288
+ pass `hidden_from=["operator"]`.
289
+ """
290
+ display_name: str | None = None
291
+ """
292
+ An optional display name for the option. This is useful for making
293
+ the option more user-friendly in the UI.
294
+ """
295
+
296
+ @classmethod
297
+ def from_dict(cls, data: dict[str, Any]) -> "Option":
298
+ """
299
+ Creates an instance of `Option` from a dictionary.
300
+
301
+ Parameters
302
+ ----------
303
+
304
+ data: dict[str, Any]
305
+ The dictionary representation of an option.
306
+
307
+ Returns
308
+ -------
309
+ Option
310
+ An instance of `Option`.
311
+
312
+ Examples
313
+ --------
314
+ >>> opt_dict = {"name": "timeout", "option_type": "<class 'int'>", "default": 60}
315
+ >>> option = Option.from_dict(opt_dict)
316
+ >>> option.name
317
+ 'timeout'
318
+ >>> option.default
319
+ 60
320
+ """
321
+
322
+ option_type_string = data["option_type"]
323
+ option_type = getattr(builtins, option_type_string.split("'")[1])
324
+
325
+ return cls(
326
+ name=data["name"],
327
+ option_type=option_type,
328
+ default=data.get("default"),
329
+ description=data.get("description"),
330
+ required=data.get("required", False),
331
+ choices=data.get("choices"),
332
+ additional_attributes=data.get("additional_attributes"),
333
+ control_type=data.get("control_type"),
334
+ hidden_from=data.get("hidden_from"),
335
+ display_name=data.get("display_name"),
336
+ )
337
+
338
+ def to_dict(self) -> dict[str, Any]:
339
+ """
340
+ Converts the option to a dict.
341
+
342
+ Returns
343
+ -------
344
+ dict[str, Any]
345
+ The option as a dict with all its attributes.
346
+
347
+ Examples
348
+ --------
349
+ >>> opt = Option("duration", str, "30s", description="solver duration")
350
+ >>> opt_dict = opt.to_dict()
351
+ >>> opt_dict["name"]
352
+ 'duration'
353
+ >>> opt_dict["default"]
354
+ '30s'
355
+ """
356
+
357
+ return {
358
+ "name": self.name,
359
+ "option_type": str(self.option_type),
360
+ "default": self.default,
361
+ "description": self.description,
362
+ "required": self.required,
363
+ "choices": self.choices,
364
+ "additional_attributes": self.additional_attributes,
365
+ "control_type": self.control_type,
366
+ "hidden_from": self.hidden_from,
367
+ "display_name": self.display_name,
368
+ }
369
+
370
+
108
371
  class Options:
109
372
  """
110
- Options for a run. To initialize options, pass in one or more `Parameter`
111
- objects. The options will look for the values of the given parameters in
112
- the following order: command-line arguments, environment variables, default
113
- values.
114
-
115
- Once the options are initialized, you can access the parameters as
116
- attributes of the `Options` object. For example, if you have a
117
- `Parameter` object with the name "duration", you can access it as
373
+ Options container for application configuration.
374
+
375
+ You can import the `Options` class directly from `nextmv`:
376
+
377
+ ```python
378
+ from nextmv import Options
379
+ ```
380
+
381
+ To initialize options, pass in one or more `Option` objects. The options
382
+ will look for the values of the given parameters in the following order:
383
+ command-line arguments, environment variables, default values.
384
+
385
+ Once the `Options` are initialized, you can access the underlying options as
386
+ attributes of the `Options` object. For example, if you have an
387
+ `Option` object with the name "duration", you can access it as
118
388
  `options.duration`.
119
389
 
120
- If a parameter is required and not provided through a command-line
390
+ If an option is required and not provided through a command-line
121
391
  argument, an environment variable or a default value, an error will be
122
392
  raised.
123
393
 
@@ -133,30 +403,30 @@ class Options:
133
403
 
134
404
  Parameters
135
405
  ----------
136
- *parameters : Parameter
137
- The parameters that are used in the options. At least one
138
- parameter is required.
406
+ *options : Option
407
+ The list of `Option` objects that are used in the options. At least one
408
+ option is required.
139
409
 
140
410
  Examples
141
411
  --------
142
412
  >>> import nextmv
143
413
  >>>
144
414
  >>> options = nextmv.Options(
145
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
146
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
415
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
416
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
147
417
  ... )
148
418
  >>>
149
419
  >>> print(options.duration, options.threads, options.to_dict())
150
-
151
420
  30s 4 {"duration": "30s", "threads": 4}
152
421
 
153
422
  Raises
154
423
  ------
155
424
  ValueError
156
- If a required parameter is not provided through a command-line
425
+ If a required option is not provided through a command-line
157
426
  argument, an environment variable or a default value.
158
427
  TypeError
159
- If a parameter is not a `Parameter` object.
428
+ If an option is not either an `Option` or `Parameter` (deprecated)
429
+ object.
160
430
  ValueError
161
431
  If an environment variable is not of the type of the corresponding
162
432
  parameter.
@@ -164,10 +434,16 @@ class Options:
164
434
 
165
435
  PARSED = False
166
436
 
167
- def __init__(self, *parameters: Parameter):
168
- """Initializes the options."""
437
+ def __init__(self, *options: Option):
438
+ """
439
+ Initialize an Options instance with the provided option objects.
169
440
 
170
- self.parameters = copy.deepcopy(parameters)
441
+ Parameters
442
+ ----------
443
+ *options : Option
444
+ The option objects to include in this Options instance.
445
+ """
446
+ self.options = copy.deepcopy(options)
171
447
 
172
448
  def to_dict(self) -> dict[str, Any]:
173
449
  """
@@ -178,7 +454,17 @@ class Options:
178
454
  Returns
179
455
  -------
180
456
  dict[str, Any]
181
- The options as a dict.
457
+ The options as a dict where keys are option names and values
458
+ are the corresponding option values.
459
+
460
+ Examples
461
+ --------
462
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
463
+ >>> options_dict = options.to_dict()
464
+ >>> options_dict["duration"]
465
+ '30s'
466
+ >>> options_dict["threads"]
467
+ 4
182
468
  """
183
469
 
184
470
  if not self.PARSED:
@@ -189,7 +475,7 @@ class Options:
189
475
 
190
476
  self_dict = copy.deepcopy(self.__dict__)
191
477
 
192
- rm_keys = ["parameters", "PARSED"]
478
+ rm_keys = ["PARSED", "options"]
193
479
  for key in rm_keys:
194
480
  if key in self_dict:
195
481
  self_dict.pop(key)
@@ -198,8 +484,49 @@ class Options:
198
484
 
199
485
  return m.to_dict()["config"]
200
486
 
487
+ def to_dict_cloud(self) -> dict[str, str]:
488
+ """
489
+ Converts the options to a dict that can be used in the Nextmv Cloud.
490
+
491
+ Cloud has a hard requirement that options are passed as strings. This
492
+ method converts the options to a dict with string values. This is
493
+ useful for passing options to the Nextmv Cloud.
494
+
495
+ As a side effect, this method parses the options if they have not been
496
+ parsed yet. See the `parse` method for more information.
497
+
498
+ Returns
499
+ -------
500
+ dict[str, str]
501
+ The options as a dict with string values where non-string values
502
+ are JSON-encoded.
503
+
504
+ Examples
505
+ --------
506
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
507
+ >>> cloud_dict = options.to_dict_cloud()
508
+ >>> cloud_dict["duration"]
509
+ '30s'
510
+ >>> cloud_dict["threads"]
511
+ '4'
512
+ """
513
+
514
+ options_dict = self.to_dict()
515
+
516
+ cloud_dict = {}
517
+ for k, v in options_dict.items():
518
+ if isinstance(v, str):
519
+ cloud_dict[k] = v
520
+ else:
521
+ cloud_dict[k] = json.dumps(v)
522
+
523
+ return cloud_dict
524
+
201
525
  def parameters_dict(self) -> list[dict[str, Any]]:
202
526
  """
527
+ !!! warning
528
+ `Parameter` is deprecated, use `Option` instead. `Options.parameters_dict` -> `Options.options_dict`
529
+
203
530
  Converts the options to a list of dicts. Each dict is the dict
204
531
  representation of a `Parameter`.
205
532
 
@@ -209,7 +536,34 @@ class Options:
209
536
  The list of dictionaries (parameter entries).
210
537
  """
211
538
 
212
- return [param.to_dict() for param in self.parameters]
539
+ deprecated(
540
+ name="Options.parameters_dict",
541
+ reason="`Parameter` is deprecated, use `Option` instead. Options.parameters_dict -> Options.options_dict",
542
+ )
543
+
544
+ return [param.to_dict() for param in self.options]
545
+
546
+ def options_dict(self) -> list[dict[str, Any]]:
547
+ """
548
+ Converts the `Options` to a list of dicts. Each dict is the dict
549
+ representation of an `Option`.
550
+
551
+ Returns
552
+ -------
553
+ list[dict[str, Any]]
554
+ The list of dictionaries (`Option` entries).
555
+
556
+ Examples
557
+ --------
558
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
559
+ >>> opt_dicts = options.options_dict()
560
+ >>> opt_dicts[0]["name"]
561
+ 'duration'
562
+ >>> opt_dicts[1]["name"]
563
+ 'threads'
564
+ """
565
+
566
+ return [opt.to_dict() for opt in self.options]
213
567
 
214
568
  def parse(self):
215
569
  """
@@ -225,23 +579,21 @@ class Options:
225
579
  After Options have been parsed, they cannot be merged with other
226
580
  Options. If you need to merge Options, do so before parsing them.
227
581
 
228
- Example 1
582
+ Examples
229
583
  -------
230
584
  >>> import nextmv
231
585
  >>>
232
586
  >>> options = nextmv.Options(
233
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
234
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
587
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
588
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
235
589
  ... )
236
590
  >>> options.parse() # Does not raise an exception.
237
591
 
238
- Example 2
239
- -------
240
592
  >>> import nextmv
241
593
  >>>
242
594
  >>> options = nextmv.Options(
243
- ... nextmv.Parameter("duration", str, "30s", description="solver duration", required=False),
244
- ... nextmv.Parameter("threads", int, 4, description="computer threads", required=False),
595
+ ... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
596
+ ... nextmv.Option("threads", int, 4, description="computer threads", required=False),
245
597
  ... )
246
598
  >>> print(options.duration) # Parses the options.
247
599
  >>> options.parse() # Raises an exception because the options have already been parsed.
@@ -251,10 +603,10 @@ class Options:
251
603
  RuntimeError
252
604
  If the options have already been parsed.
253
605
  ValueError
254
- If a required parameter is not provided through a command-line
606
+ If a required option is not provided through a command-line
255
607
  argument, an environment variable or a default value.
256
608
  TypeError
257
- If a parameter is not a `Parameter` object.
609
+ If an option is not an `Option` or `Parameter` (deprecated) object.
258
610
  ValueError
259
611
  If an environment variable is not of the type of the corresponding
260
612
  parameter.
@@ -265,18 +617,28 @@ class Options:
265
617
 
266
618
  self._parse()
267
619
 
268
- def merge(self, new: "Options") -> "Options":
620
+ def merge(self, *new: "Options", skip_parse: bool = False) -> "Options":
269
621
  """
270
- Merges the current options with the new options. This method cannot be
271
- used if any of the options have been parsed. When options are parsed,
272
- values are read from the command-line arguments, environment variables
273
- and default values. Merging options after parsing would result in
274
- unpredictable behavior.
622
+ Merges the current options with the new options.
623
+
624
+ This method cannot be used if any of the options have been parsed. When
625
+ options are parsed, values are read from the command-line arguments,
626
+ environment variables and default values. Merging options after parsing
627
+ would result in unpredictable behavior.
275
628
 
276
629
  Parameters
277
630
  ----------
278
631
  new : Options
279
- The new options to merge.
632
+ The new options to merge with the current options. At least one new option set
633
+ is required to merge. Multiple `Options` instances can be passed.
634
+ skip_parse : bool, optional
635
+ If True, the merged options will not be parsed after merging. This is useful
636
+ if you want to merge further options after this merge. The default is False.
637
+
638
+ Returns
639
+ -------
640
+ Options
641
+ The merged options object (self).
280
642
 
281
643
  Raises
282
644
  ------
@@ -285,10 +647,18 @@ class Options:
285
647
  RuntimeError
286
648
  If the new options have already been parsed.
287
649
 
288
- Returns
289
- -------
290
- Options
291
- The merged options.
650
+ Examples
651
+ --------
652
+ >>> opt1 = Options(Option("duration", str, "30s"))
653
+ >>> opt2 = Options(Option("threads", int, 4))
654
+ >>> opt3 = Options(Option("verbose", bool, False))
655
+ >>> merged = opt1.merge(opt2, opt3)
656
+ >>> merged.duration
657
+ '30s'
658
+ >>> merged.threads
659
+ 4
660
+ >>> merged.verbose
661
+ False
292
662
  """
293
663
 
294
664
  if self.PARSED:
@@ -296,27 +666,40 @@ class Options:
296
666
  "base options have already been parsed, cannot merge. See `Options.parse()` for more information."
297
667
  )
298
668
 
299
- if new.PARSED:
300
- raise RuntimeError(
301
- "new options have already been parsed, cannot merge. See `Options.parse()` for more information."
302
- )
669
+ if not new:
670
+ raise ValueError("at least one new Options instance is required to merge")
303
671
 
304
- self.parameters += new.parameters
672
+ for i, opt in enumerate(new):
673
+ if not isinstance(opt, Options):
674
+ raise TypeError(f"expected an <Options> object, but got {type(opt)} in index {i}")
675
+ if opt.PARSED:
676
+ raise RuntimeError(
677
+ f"new options at index {i} have already been parsed, cannot merge. "
678
+ + "See `Options.parse()` for more information."
679
+ )
305
680
 
306
- self._parse()
681
+ # Add the new options to the current options.
682
+ for n in new:
683
+ self.options += n.options
684
+
685
+ if not skip_parse:
686
+ self.parse()
307
687
 
308
688
  return self
309
689
 
310
690
  @classmethod
311
691
  def from_dict(cls, data: dict[str, Any]) -> "Options":
312
692
  """
313
- Creates an instance of `Options` from a dictionary. The dictionary
314
- should have the following structure:
693
+ Creates an instance of `Options` from a dictionary.
315
694
 
695
+ The dictionary should have the following structure:
696
+
697
+ ```python
316
698
  {
317
699
  "duration": "30",
318
700
  "threads": 4,
319
701
  }
702
+ ```
320
703
 
321
704
  Parameters
322
705
  ----------
@@ -326,25 +709,39 @@ class Options:
326
709
  Returns
327
710
  -------
328
711
  Options
329
- An instance of `Options`.
712
+ An instance of `Options` with options created from the dictionary.
713
+
714
+ Examples
715
+ --------
716
+ >>> data = {"duration": "30s", "threads": 4}
717
+ >>> options = Options.from_dict(data)
718
+ >>> options.duration
719
+ '30s'
720
+ >>> options.threads
721
+ 4
330
722
  """
331
723
 
332
- parameters = []
724
+ options = []
333
725
  for key, value in data.items():
334
- parameter = Parameter(name=key, param_type=type(value), default=value)
335
- parameters.append(parameter)
726
+ opt = Option(name=key, option_type=type(value), default=value)
727
+ options.append(opt)
336
728
 
337
- return cls(*parameters)
729
+ return cls(*options)
338
730
 
339
731
  @classmethod
340
732
  def from_parameters_dict(cls, parameters_dict: list[dict[str, Any]]) -> "Options":
341
733
  """
734
+ !!! warning
735
+
736
+ `Parameter` is deprecated, use `Option` instead.
737
+ `Options.from_parameters_dict` -> `Options.from_options_dict`
738
+
342
739
  Creates an instance of `Options` from parameters in dict form. Each
343
740
  entry is the dict representation of a `Parameter`.
344
741
 
345
742
  Parameters
346
743
  ----------
347
- data : list[dict[str, Any]]
744
+ parameters_dict : list[dict[str, Any]]
348
745
  The list of dictionaries (parameter entries).
349
746
 
350
747
  Returns
@@ -353,6 +750,12 @@ class Options:
353
750
  An instance of `Options`.
354
751
  """
355
752
 
753
+ deprecated(
754
+ name="Options.from_parameters_dict",
755
+ reason="`Parameter` is deprecated, use `Option` instead. "
756
+ "Options.from_parameters_dict -> Options.from_options_dict",
757
+ )
758
+
356
759
  parameters = []
357
760
  for parameter_dict in parameters_dict:
358
761
  parameter = Parameter.from_dict(parameter_dict)
@@ -360,10 +763,58 @@ class Options:
360
763
 
361
764
  return cls(*parameters)
362
765
 
766
+ @classmethod
767
+ def from_options_dict(cls, options_dict: list[dict[str, Any]]) -> "Options":
768
+ """
769
+ Creates an instance of `Options` from a list of `Option` objects in
770
+ dict form. Each entry is the dict representation of an `Option`.
771
+
772
+ Parameters
773
+ ----------
774
+ options_dict : list[dict[str, Any]]
775
+ The list of dictionaries (`Option` entries).
776
+
777
+ Returns
778
+ -------
779
+ Options
780
+ An instance of `Options`.
781
+
782
+ Examples
783
+ --------
784
+ >>> options_dict = [
785
+ ... {"name": "duration", "option_type": "<class 'str'>", "default": "30s"},
786
+ ... {"name": "threads", "option_type": "<class 'int'>", "default": 4}
787
+ ... ]
788
+ >>> options = Options.from_options_dict(options_dict)
789
+ >>> options.duration
790
+ '30s'
791
+ >>> options.threads
792
+ 4
793
+ """
794
+
795
+ options = []
796
+ for opt_dict in options_dict:
797
+ opt = Option.from_dict(opt_dict)
798
+ options.append(opt)
799
+
800
+ return cls(*options)
801
+
363
802
  def __getattr__(self, name: str) -> Any:
364
803
  """
365
- Gets an attribute of the options. This is called when an attribute
366
- is accessed. It parses the options if they have not been parsed yet.
804
+ Gets an attribute of the options.
805
+
806
+ This is called when an attribute is accessed. It parses the options
807
+ if they have not been parsed yet.
808
+
809
+ Parameters
810
+ ----------
811
+ name : str
812
+ The name of the attribute to get.
813
+
814
+ Returns
815
+ -------
816
+ Any
817
+ The value of the attribute.
367
818
  """
368
819
 
369
820
  if not self.PARSED:
@@ -376,13 +827,17 @@ class Options:
376
827
  Parses the options using command-line arguments, environment variables
377
828
  and default values.
378
829
 
830
+ This is an internal method that is called by `parse()` and `__getattr__()`.
831
+ It sets the `PARSED` flag to True and sets the values of the options
832
+ based on command-line arguments, environment variables, and default values.
833
+
379
834
  Raises
380
835
  ------
381
836
  ValueError
382
- If a required parameter is not provided through a command-line
837
+ If a required option is not provided through a command-line
383
838
  argument, an environment variable or a default value.
384
839
  TypeError
385
- If a parameter is not a `Parameter` object.
840
+ If an option is not an `Option` or `Parameter` (deprecated) object.
386
841
  ValueError
387
842
  If an environment variable is not of the type of the corresponding
388
843
  parameter.
@@ -390,7 +845,7 @@ class Options:
390
845
 
391
846
  self.PARSED = True
392
847
 
393
- if not self.parameters:
848
+ if not self.options:
394
849
  return
395
850
 
396
851
  parser = argparse.ArgumentParser(
@@ -400,44 +855,46 @@ class Options:
400
855
  + "or environment variables.",
401
856
  allow_abbrev=False,
402
857
  )
403
- params_by_field_name: dict[str, Parameter] = {}
858
+ options_by_field_name: dict[str, Option] = {}
404
859
 
405
- for p, param in enumerate(self.parameters):
406
- if not isinstance(param, Parameter):
407
- raise TypeError(f"expected a <Parameter> object, but got {type(param)} in index {p}")
860
+ for ix, option in enumerate(self.options):
861
+ if not isinstance(option, Option) and not isinstance(option, Parameter):
862
+ raise TypeError(
863
+ f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)} in index {ix}"
864
+ )
408
865
 
409
866
  # See comment below about ipykernel adding a `-f` argument. We
410
- # restrict parameters from having the name 'f' or 'fff' for that
867
+ # restrict options from having the name 'f' or 'fff' for that
411
868
  # reason.
412
- if param.name == "f" or param.name == "fff":
413
- raise ValueError("parameter names 'f', 'fff' are reserved for internal use")
869
+ if option.name == "f" or option.name == "fff":
870
+ raise ValueError("option names 'f', 'fff' are reserved for internal use")
414
871
 
415
- if param.name == "PARSED":
416
- raise ValueError("parameter name 'PARSED' is reserved for internal use")
872
+ if option.name == "PARSED":
873
+ raise ValueError("option name 'PARSED' is reserved for internal use")
417
874
 
418
875
  # Remove any leading '-'. This is in line with argparse's behavior.
419
- param.name = param.name.lstrip("-")
876
+ option.name = option.name.lstrip("-")
420
877
 
421
878
  kwargs = {
422
- "type": param.param_type if param.param_type is not bool else str,
423
- "help": self._description(param),
879
+ "type": self._option_type(option) if self._option_type(option) is not bool else str,
880
+ "help": self._description(option),
424
881
  }
425
882
 
426
- if param.choices is not None:
427
- kwargs["choices"] = param.choices
883
+ if option.choices is not None:
884
+ kwargs["choices"] = option.choices
428
885
 
429
886
  parser.add_argument(
430
- f"-{param.name}",
431
- f"--{param.name}",
887
+ f"-{option.name}",
888
+ f"--{option.name}",
432
889
  **kwargs,
433
890
  )
434
891
 
435
- # Store the parameter by its field name for easy access later. argparse
892
+ # Store the option by its field name for easy access later. argparse
436
893
  # replaces '-' with '_', so we do the same here.
437
- params_by_field_name[param.name.replace("-", "_")] = param
894
+ options_by_field_name[option.name.replace("-", "_")] = option
438
895
 
439
896
  # The ipkyernel uses a `-f` argument by default that it passes to the
440
- # execution. We dont want to ignore this argument because we get an
897
+ # execution. We don't want to ignore this argument because we get an
441
898
  # error. Fix source: https://stackoverflow.com/a/56349168
442
899
  parser.add_argument(
443
900
  "-f",
@@ -452,67 +909,119 @@ class Options:
452
909
  if arg == "fff" or arg == "f":
453
910
  continue
454
911
 
455
- param = params_by_field_name[arg]
912
+ option = options_by_field_name[arg]
456
913
 
457
- # First, attempt to set the value of a parameter from the
914
+ # First, attempt to set the value of an option from the
458
915
  # command-line args.
459
916
  arg_value = getattr(args, arg)
460
917
  if arg_value is not None:
461
- value = self._parameter_value(param, arg_value)
918
+ value = self._option_value(option, arg_value)
462
919
  setattr(self, arg, value)
463
920
  continue
464
921
 
465
- # Second, attempt to set the value of a parameter from the
922
+ # Second, attempt to set the value of am option from the
466
923
  # environment variables.
467
924
  upper_name = arg.upper()
468
925
  env_value = os.getenv(upper_name)
469
926
  if env_value is not None:
470
927
  try:
471
- typed_env_value = param.param_type(env_value) if param.param_type is not bool else env_value
928
+ typed_env_value = (
929
+ self._option_type(option)(env_value) if self._option_type(option) is not bool else env_value
930
+ )
472
931
  except ValueError:
473
- raise ValueError(f'environment variable "{upper_name}" is not of type {param.param_type}') from None
932
+ raise ValueError(
933
+ f'environment variable "{upper_name}" is not of type {self._option_type(option)}'
934
+ ) from None
474
935
 
475
- value = self._parameter_value(param, typed_env_value)
936
+ value = self._option_value(option, typed_env_value)
476
937
  setattr(self, arg, value)
477
938
  continue
478
939
 
479
940
  # Finally, attempt to set a default value. This is only allowed
480
- # for non-required parameters.
481
- if not param.required:
482
- setattr(self, arg, param.default)
941
+ # for non-required options.
942
+ if not option.required:
943
+ setattr(self, arg, option.default)
483
944
  continue
484
945
 
485
- # At this point, the parameter is required and no value was
946
+ # At this point, the option is required and no value was
486
947
  # provided
487
948
  raise ValueError(
488
- f'parameter "{arg}" is required but not provided through: command-line args, env vars, or default value'
949
+ f'option "{arg}" is required but not provided through: command-line args, env vars, or default value'
489
950
  )
490
951
 
491
- @staticmethod
492
- def _description(param: Parameter) -> str:
493
- """Returns a description for a parameter."""
952
+ def _description(self, option: Option) -> str:
953
+ """
954
+ Returns a description for an option.
494
955
 
495
- description = f"[env var: {param.name.upper()}]"
956
+ This is an internal method used to create the help text for options
957
+ in the command-line argument parser.
496
958
 
497
- if param.required:
959
+ Parameters
960
+ ----------
961
+ option : Option
962
+ The option to get the description for.
963
+
964
+ Returns
965
+ -------
966
+ str
967
+ A formatted description string for the option.
968
+ """
969
+
970
+ description = ""
971
+ if isinstance(option, Parameter):
972
+ description = "DEPRECATED (initialized with <Parameter>, use <Option> instead) "
973
+
974
+ description += f"[env var: {option.name.upper()}]"
975
+
976
+ if option.required:
498
977
  description += " (required)"
499
978
 
500
- if param.default is not None:
501
- description += f" (default: {param.default})"
979
+ if option.default is not None:
980
+ description += f" (default: {option.default})"
981
+
982
+ description += f" (type: {self._option_type(option).__name__})"
983
+
984
+ if isinstance(option, Option) and option.additional_attributes is not None:
985
+ description += f" (additional attributes: {option.additional_attributes})"
986
+
987
+ if isinstance(option, Option) and option.control_type is not None:
988
+ description += f" (control type: {option.control_type})"
502
989
 
503
- description += f" (type: {param.param_type.__name__})"
990
+ if isinstance(option, Option) and option.hidden_from:
991
+ description += f" (hidden from: {', '.join(option.hidden_from)})"
504
992
 
505
- if param.description is not None:
506
- description += f": {param.description}"
993
+ if isinstance(option, Option) and option.display_name is not None:
994
+ description += f" (display name: {option.display_name})"
995
+
996
+ if option.description is not None and option.description != "":
997
+ description += f": {option.description}"
507
998
 
508
999
  return description
509
1000
 
510
- @staticmethod
511
- def _parameter_value(parameter: Parameter, value: Any) -> Any:
512
- """Handles how the value of a parameter is extracted."""
1001
+ def _option_value(self, option: Option, value: Any) -> Any:
1002
+ """
1003
+ Handles how the value of an option is extracted.
513
1004
 
514
- param_type = parameter.param_type
515
- if param_type is not bool:
1005
+ This is an internal method that converts string values to boolean
1006
+ values for boolean options.
1007
+
1008
+ Parameters
1009
+ ----------
1010
+ option : Option
1011
+ The option to extract the value for.
1012
+ value : Any
1013
+ The value to extract.
1014
+
1015
+ Returns
1016
+ -------
1017
+ Any
1018
+ The extracted value. For boolean options, string values like
1019
+ "true", "1", "t", "y", and "yes" are converted to True, and
1020
+ other values are converted to False.
1021
+ """
1022
+
1023
+ opt_type = self._option_type(option)
1024
+ if opt_type is not bool:
516
1025
  return value
517
1026
 
518
1027
  value = str(value).lower()
@@ -521,3 +1030,87 @@ class Options:
521
1030
  return True
522
1031
 
523
1032
  return False
1033
+
1034
+ @staticmethod
1035
+ def _option_type(option: Option | Parameter) -> type:
1036
+ """
1037
+ Get the type of an option.
1038
+
1039
+ This auxiliary function was introduced for backwards compatibility with
1040
+ the deprecated `Parameter` class. Once `Parameter` is removed, this function
1041
+ can be removed as well. When the function is removed, use the
1042
+ `option.option_type` attribute directly, instead of calling this function.
1043
+
1044
+ Parameters
1045
+ ----------
1046
+ option : Union[Option, Parameter]
1047
+ The option to get the type for.
1048
+
1049
+ Returns
1050
+ -------
1051
+ type
1052
+ The type of the option.
1053
+
1054
+ Raises
1055
+ ------
1056
+ TypeError
1057
+ If the option is not an `Option` or `Parameter` object.
1058
+ """
1059
+
1060
+ if isinstance(option, Option):
1061
+ return option.option_type
1062
+ elif isinstance(option, Parameter):
1063
+ return option.param_type
1064
+ else:
1065
+ raise TypeError(f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)}")
1066
+
1067
+
1068
+ class OptionsEnforcement:
1069
+ """
1070
+ OptionsEnforcement is a class that provides rules for how the options
1071
+ are enforced on Nextmv Cloud.
1072
+
1073
+ This class is used to enforce options in the Nextmv Cloud. It is not used
1074
+ in the local `Options` class, but it is used to control validation when a run
1075
+ is submitted to the Nextmv Cloud.
1076
+
1077
+ Parameters
1078
+ ----------
1079
+ strict: bool default = False
1080
+ If True, the options additional options that are configured will not
1081
+ pass validation. This means that only the options that are defined in the
1082
+ `Options` class will be allowed. If False, additional options that are
1083
+ not defined in the `Options` class will be allowed.
1084
+ validation_enforce: bool default = False
1085
+ If True, the options will be validated against your option configuration
1086
+ validation rules. If False, the options will not be validated.
1087
+ """
1088
+
1089
+ strict: bool = False
1090
+ """
1091
+ If True, the options additional options that are configured will not
1092
+ pass validation. This means that only the options that are defined in the
1093
+ `Options` class will be allowed. If False, additional options that are
1094
+ not defined in the `Options` class will be allowed.
1095
+ """
1096
+ validation_enforce: bool = False
1097
+ """
1098
+ If True, the options will be validated against your option configuration
1099
+ validation rules. If False, the options will not be validated.
1100
+ """
1101
+
1102
+ def __init__(self, strict: bool = False, validation_enforce: bool = False):
1103
+ """
1104
+ Initialize an OptionsEnforcement instance with the provided rules.
1105
+
1106
+ Parameters
1107
+ ----------
1108
+ strict : bool, optional
1109
+ If True, only options defined in the `Options` class will be allowed.
1110
+ Defaults to False.
1111
+ validation_enforced : bool, optional
1112
+ If True, options will be validated against the configuration rules.
1113
+ Defaults to False.
1114
+ """
1115
+ self.strict = strict
1116
+ self.validation_enforce = validation_enforce