nextmv 0.10.3.dev0__py3-none-any.whl → 0.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +39 -0
- nextmv/__init__.py +57 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +79 -9
- nextmv/cloud/__init__.py +71 -10
- nextmv/cloud/acceptance_test.py +888 -17
- nextmv/cloud/account.py +154 -10
- nextmv/cloud/application.py +3644 -437
- nextmv/cloud/batch_experiment.py +292 -33
- nextmv/cloud/client.py +354 -53
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +121 -4
- nextmv/cloud/instance.py +125 -0
- nextmv/cloud/package.py +474 -0
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +174 -0
- 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/main.py +37 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +883 -78
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1263 -0
- nextmv/local/executor.py +1040 -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 +1472 -0
- nextmv/model.py +431 -0
- nextmv/options.py +968 -78
- nextmv/output.py +1363 -231
- nextmv/polling.py +287 -0
- nextmv/run.py +1623 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/METADATA +51 -288
- nextmv-0.35.0.dist-info/RECORD +50 -0
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/WHEEL +1 -1
- nextmv/cloud/status.py +0 -29
- nextmv/nextroute/__init__.py +0 -2
- nextmv/nextroute/check/__init__.py +0 -26
- nextmv/nextroute/check/schema.py +0 -141
- nextmv/nextroute/schema/__init__.py +0 -19
- nextmv/nextroute/schema/input.py +0 -52
- nextmv/nextroute/schema/location.py +0 -13
- nextmv/nextroute/schema/output.py +0 -136
- nextmv/nextroute/schema/stop.py +0 -61
- nextmv/nextroute/schema/vehicle.py +0 -68
- nextmv-0.10.3.dev0.dist-info/RECORD +0 -28
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/licenses/LICENSE +0 -0
nextmv/options.py
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
"""
|
|
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
|
|
19
|
+
import builtins
|
|
20
|
+
import copy
|
|
21
|
+
import json
|
|
4
22
|
import os
|
|
5
23
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any
|
|
24
|
+
from typing import Any
|
|
7
25
|
|
|
8
26
|
from nextmv.base_model import BaseModel
|
|
27
|
+
from nextmv.deprecated import deprecated
|
|
9
28
|
|
|
10
29
|
|
|
11
30
|
@dataclass
|
|
12
31
|
class Parameter:
|
|
13
32
|
"""
|
|
33
|
+
!!! warning
|
|
34
|
+
`Parameter` is deprecated, use `Option` instead.
|
|
35
|
+
|
|
14
36
|
Parameter that is used in a `Configuration`. When a parameter is required,
|
|
15
37
|
it is a good practice to provide a default value for it. This is because
|
|
16
38
|
the configuration will raise an error if a required parameter is not
|
|
@@ -21,18 +43,30 @@ class Parameter:
|
|
|
21
43
|
----------
|
|
22
44
|
name : str
|
|
23
45
|
The name of the parameter.
|
|
46
|
+
|
|
24
47
|
param_type : type
|
|
25
48
|
The type of the parameter.
|
|
49
|
+
|
|
26
50
|
default : Any, optional
|
|
27
51
|
The default value of the parameter. Even though this is optional, it is
|
|
28
52
|
recommended to provide a default value for all parameters.
|
|
53
|
+
|
|
29
54
|
description : str, optional
|
|
30
55
|
An optional description of the parameter. This is useful for generating
|
|
31
56
|
help messages for the configuration.
|
|
57
|
+
|
|
32
58
|
required : bool, optional
|
|
33
59
|
Whether the parameter is required. If a parameter is required, it will
|
|
34
|
-
be an error to not provide a value for it, either
|
|
60
|
+
be an error to not provide a value for it, either through a command-line
|
|
35
61
|
argument, an environment variable or a default value.
|
|
62
|
+
|
|
63
|
+
choices : list[Optional[Any]], optional
|
|
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)
|
|
36
70
|
"""
|
|
37
71
|
|
|
38
72
|
name: str
|
|
@@ -40,74 +74,778 @@ class Parameter:
|
|
|
40
74
|
param_type: type
|
|
41
75
|
"""The type of the parameter."""
|
|
42
76
|
|
|
43
|
-
default:
|
|
77
|
+
default: Any | None = None
|
|
44
78
|
"""The default value of the parameter. Even though this is optional, it is
|
|
45
79
|
recommended to provide a default value for all parameters."""
|
|
46
|
-
description:
|
|
80
|
+
description: str | None = None
|
|
47
81
|
"""An optional description of the parameter. This is useful for generating
|
|
48
82
|
help messages for the configuration."""
|
|
49
83
|
required: bool = False
|
|
50
84
|
"""Whether the parameter is required. If a parameter is required, it will
|
|
51
85
|
be an error to not provide a value for it, either trough a command-line
|
|
52
86
|
argument, an environment variable or a default value."""
|
|
87
|
+
choices: list[Any | None] = None
|
|
88
|
+
"""Limits values to a specific set of choices."""
|
|
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
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
|
|
104
|
+
"""
|
|
105
|
+
!!! warning
|
|
106
|
+
`Parameter` is deprecated, use `Option` instead.
|
|
107
|
+
`Parameter.from_dict` -> `Option.from_dict`
|
|
108
|
+
|
|
109
|
+
Creates an instance of `Parameter` from a dictionary.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
data : dict[str, Any]
|
|
114
|
+
The dictionary representation of a parameter.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
Parameter
|
|
119
|
+
An instance of `Parameter`.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
deprecated(
|
|
123
|
+
name="Parameter.from_dict",
|
|
124
|
+
reason="`Parameter` is deprecated, use `Option` instead. Parameter.from_dict -> Option.from_dict",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
param_type_string = data["param_type"]
|
|
128
|
+
param_type = getattr(builtins, param_type_string.split("'")[1])
|
|
129
|
+
|
|
130
|
+
return Parameter(
|
|
131
|
+
name=data["name"],
|
|
132
|
+
param_type=param_type,
|
|
133
|
+
default=data.get("default"),
|
|
134
|
+
description=data.get("description"),
|
|
135
|
+
required=data.get("required", False),
|
|
136
|
+
choices=data.get("choices"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def to_dict(self) -> dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
!!! warning
|
|
142
|
+
`Parameter` is deprecated, use `Option` instead.
|
|
143
|
+
`Parameter.to_dict` -> `Option.to_dict`
|
|
144
|
+
|
|
145
|
+
Converts the parameter to a dict.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
dict[str, Any]
|
|
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
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
deprecated(
|
|
164
|
+
name="Parameter.to_dict",
|
|
165
|
+
reason="`Parameter` is deprecated, use `Option` instead. Parameter.to_dict -> Option.to_dict",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"name": self.name,
|
|
170
|
+
"param_type": str(self.param_type),
|
|
171
|
+
"default": self.default,
|
|
172
|
+
"description": self.description,
|
|
173
|
+
"required": self.required,
|
|
174
|
+
"choices": self.choices,
|
|
175
|
+
}
|
|
176
|
+
|
|
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
|
+
}
|
|
53
369
|
|
|
54
370
|
|
|
55
371
|
class Options:
|
|
56
372
|
"""
|
|
57
|
-
Options for
|
|
58
|
-
objects. The options will look for the values of the given parameters in
|
|
59
|
-
the following order: command-line arguments, environment variables, default
|
|
60
|
-
values.
|
|
373
|
+
Options container for application configuration.
|
|
61
374
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
65
388
|
`options.duration`.
|
|
66
389
|
|
|
67
|
-
If
|
|
390
|
+
If an option is required and not provided through a command-line
|
|
68
391
|
argument, an environment variable or a default value, an error will be
|
|
69
392
|
raised.
|
|
70
393
|
|
|
71
394
|
Options works as a Namespace, so you can assign new attributes to it. For
|
|
72
395
|
example, you can do `options.foo = "bar"`.
|
|
73
396
|
|
|
397
|
+
Options are parsed from the given sources when an attribute is accessed.
|
|
398
|
+
Alternatively, you can call the `parse` method to parse the options
|
|
399
|
+
manually. Options that are _not_ parsed may be merged with other unparsed
|
|
400
|
+
options, by using the `merge` method. Once options are parsed, they cannot
|
|
401
|
+
be merged with other options. After options are parsed, you may get the
|
|
402
|
+
help message by running the script with the `-h/--help` flag.
|
|
403
|
+
|
|
74
404
|
Parameters
|
|
75
405
|
----------
|
|
76
|
-
*
|
|
77
|
-
The
|
|
78
|
-
|
|
406
|
+
*options : Option
|
|
407
|
+
The list of `Option` objects that are used in the options. At least one
|
|
408
|
+
option is required.
|
|
79
409
|
|
|
80
410
|
Examples
|
|
81
411
|
--------
|
|
82
412
|
>>> import nextmv
|
|
83
413
|
>>>
|
|
84
414
|
>>> options = nextmv.Options(
|
|
85
|
-
... nextmv.
|
|
86
|
-
... nextmv.
|
|
415
|
+
... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
|
|
416
|
+
... nextmv.Option("threads", int, 4, description="computer threads", required=False),
|
|
87
417
|
... )
|
|
88
418
|
>>>
|
|
89
419
|
>>> print(options.duration, options.threads, options.to_dict())
|
|
90
|
-
|
|
91
420
|
30s 4 {"duration": "30s", "threads": 4}
|
|
92
421
|
|
|
93
422
|
Raises
|
|
94
423
|
------
|
|
95
424
|
ValueError
|
|
96
|
-
If
|
|
97
|
-
ValueError
|
|
98
|
-
If a required parameter is not provided through a command-line
|
|
425
|
+
If a required option is not provided through a command-line
|
|
99
426
|
argument, an environment variable or a default value.
|
|
100
427
|
TypeError
|
|
101
|
-
If
|
|
428
|
+
If an option is not either an `Option` or `Parameter` (deprecated)
|
|
429
|
+
object.
|
|
102
430
|
ValueError
|
|
103
431
|
If an environment variable is not of the type of the corresponding
|
|
104
432
|
parameter.
|
|
105
433
|
"""
|
|
106
434
|
|
|
107
|
-
|
|
108
|
-
|
|
435
|
+
PARSED = False
|
|
436
|
+
|
|
437
|
+
def __init__(self, *options: Option):
|
|
438
|
+
"""
|
|
439
|
+
Initialize an Options instance with the provided option objects.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
*options : Option
|
|
444
|
+
The option objects to include in this Options instance.
|
|
445
|
+
"""
|
|
446
|
+
self.options = copy.deepcopy(options)
|
|
447
|
+
|
|
448
|
+
def to_dict(self) -> dict[str, Any]:
|
|
449
|
+
"""
|
|
450
|
+
Converts the options to a dict. As a side effect, this method parses
|
|
451
|
+
the options if they have not been parsed yet. See the `parse` method
|
|
452
|
+
for more information.
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
dict[str, Any]
|
|
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
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
if not self.PARSED:
|
|
471
|
+
self._parse()
|
|
472
|
+
|
|
473
|
+
class model(BaseModel):
|
|
474
|
+
config: dict[str, Any]
|
|
475
|
+
|
|
476
|
+
self_dict = copy.deepcopy(self.__dict__)
|
|
477
|
+
|
|
478
|
+
rm_keys = ["PARSED", "options"]
|
|
479
|
+
for key in rm_keys:
|
|
480
|
+
if key in self_dict:
|
|
481
|
+
self_dict.pop(key)
|
|
482
|
+
|
|
483
|
+
m = model.from_dict(data={"config": self_dict})
|
|
484
|
+
|
|
485
|
+
return m.to_dict()["config"]
|
|
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
|
+
|
|
525
|
+
def parameters_dict(self) -> list[dict[str, Any]]:
|
|
526
|
+
"""
|
|
527
|
+
!!! warning
|
|
528
|
+
`Parameter` is deprecated, use `Option` instead. `Options.parameters_dict` -> `Options.options_dict`
|
|
529
|
+
|
|
530
|
+
Converts the options to a list of dicts. Each dict is the dict
|
|
531
|
+
representation of a `Parameter`.
|
|
532
|
+
|
|
533
|
+
Returns
|
|
534
|
+
-------
|
|
535
|
+
list[dict[str, Any]]
|
|
536
|
+
The list of dictionaries (parameter entries).
|
|
537
|
+
"""
|
|
538
|
+
|
|
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]
|
|
567
|
+
|
|
568
|
+
def parse(self):
|
|
569
|
+
"""
|
|
570
|
+
Parses the options using command-line arguments, environment variables
|
|
571
|
+
and default values, in that order. Under the hood, the `argparse`
|
|
572
|
+
library is used. When command-line arguments are parsed, the help menu
|
|
573
|
+
is created, thus parsing Options more than once may result in
|
|
574
|
+
unexpected behavior.
|
|
575
|
+
|
|
576
|
+
This method is called automatically when an attribute is accessed. If
|
|
577
|
+
you want to parse the options manually, you can call this method.
|
|
578
|
+
|
|
579
|
+
After Options have been parsed, they cannot be merged with other
|
|
580
|
+
Options. If you need to merge Options, do so before parsing them.
|
|
581
|
+
|
|
582
|
+
Examples
|
|
583
|
+
-------
|
|
584
|
+
>>> import nextmv
|
|
585
|
+
>>>
|
|
586
|
+
>>> options = nextmv.Options(
|
|
587
|
+
... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
|
|
588
|
+
... nextmv.Option("threads", int, 4, description="computer threads", required=False),
|
|
589
|
+
... )
|
|
590
|
+
>>> options.parse() # Does not raise an exception.
|
|
591
|
+
|
|
592
|
+
>>> import nextmv
|
|
593
|
+
>>>
|
|
594
|
+
>>> options = nextmv.Options(
|
|
595
|
+
... nextmv.Option("duration", str, "30s", description="solver duration", required=False),
|
|
596
|
+
... nextmv.Option("threads", int, 4, description="computer threads", required=False),
|
|
597
|
+
... )
|
|
598
|
+
>>> print(options.duration) # Parses the options.
|
|
599
|
+
>>> options.parse() # Raises an exception because the options have already been parsed.
|
|
600
|
+
|
|
601
|
+
Raises
|
|
602
|
+
------
|
|
603
|
+
RuntimeError
|
|
604
|
+
If the options have already been parsed.
|
|
605
|
+
ValueError
|
|
606
|
+
If a required option is not provided through a command-line
|
|
607
|
+
argument, an environment variable or a default value.
|
|
608
|
+
TypeError
|
|
609
|
+
If an option is not an `Option` or `Parameter` (deprecated) object.
|
|
610
|
+
ValueError
|
|
611
|
+
If an environment variable is not of the type of the corresponding
|
|
612
|
+
parameter.
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
if self.PARSED:
|
|
616
|
+
raise RuntimeError("options have already been parsed")
|
|
617
|
+
|
|
618
|
+
self._parse()
|
|
619
|
+
|
|
620
|
+
def merge(self, *new: "Options", skip_parse: bool = False) -> "Options":
|
|
621
|
+
"""
|
|
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.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
new : Options
|
|
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).
|
|
642
|
+
|
|
643
|
+
Raises
|
|
644
|
+
------
|
|
645
|
+
RuntimeError
|
|
646
|
+
If the current options have already been parsed.
|
|
647
|
+
RuntimeError
|
|
648
|
+
If the new options have already been parsed.
|
|
649
|
+
|
|
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
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
if self.PARSED:
|
|
665
|
+
raise RuntimeError(
|
|
666
|
+
"base options have already been parsed, cannot merge. See `Options.parse()` for more information."
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
if not new:
|
|
670
|
+
raise ValueError("at least one new Options instance is required to merge")
|
|
671
|
+
|
|
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
|
+
)
|
|
680
|
+
|
|
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()
|
|
687
|
+
|
|
688
|
+
return self
|
|
689
|
+
|
|
690
|
+
@classmethod
|
|
691
|
+
def from_dict(cls, data: dict[str, Any]) -> "Options":
|
|
692
|
+
"""
|
|
693
|
+
Creates an instance of `Options` from a dictionary.
|
|
694
|
+
|
|
695
|
+
The dictionary should have the following structure:
|
|
696
|
+
|
|
697
|
+
```python
|
|
698
|
+
{
|
|
699
|
+
"duration": "30",
|
|
700
|
+
"threads": 4,
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Parameters
|
|
705
|
+
----------
|
|
706
|
+
data : dict[str, Any]
|
|
707
|
+
The dictionary representation of the options.
|
|
708
|
+
|
|
709
|
+
Returns
|
|
710
|
+
-------
|
|
711
|
+
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
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
options = []
|
|
725
|
+
for key, value in data.items():
|
|
726
|
+
opt = Option(name=key, option_type=type(value), default=value)
|
|
727
|
+
options.append(opt)
|
|
728
|
+
|
|
729
|
+
return cls(*options)
|
|
730
|
+
|
|
731
|
+
@classmethod
|
|
732
|
+
def from_parameters_dict(cls, parameters_dict: list[dict[str, Any]]) -> "Options":
|
|
733
|
+
"""
|
|
734
|
+
!!! warning
|
|
735
|
+
|
|
736
|
+
`Parameter` is deprecated, use `Option` instead.
|
|
737
|
+
`Options.from_parameters_dict` -> `Options.from_options_dict`
|
|
738
|
+
|
|
739
|
+
Creates an instance of `Options` from parameters in dict form. Each
|
|
740
|
+
entry is the dict representation of a `Parameter`.
|
|
741
|
+
|
|
742
|
+
Parameters
|
|
743
|
+
----------
|
|
744
|
+
parameters_dict : list[dict[str, Any]]
|
|
745
|
+
The list of dictionaries (parameter entries).
|
|
746
|
+
|
|
747
|
+
Returns
|
|
748
|
+
-------
|
|
749
|
+
Options
|
|
750
|
+
An instance of `Options`.
|
|
751
|
+
"""
|
|
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
|
+
|
|
759
|
+
parameters = []
|
|
760
|
+
for parameter_dict in parameters_dict:
|
|
761
|
+
parameter = Parameter.from_dict(parameter_dict)
|
|
762
|
+
parameters.append(parameter)
|
|
763
|
+
|
|
764
|
+
return cls(*parameters)
|
|
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)
|
|
109
799
|
|
|
110
|
-
|
|
800
|
+
return cls(*options)
|
|
801
|
+
|
|
802
|
+
def __getattr__(self, name: str) -> Any:
|
|
803
|
+
"""
|
|
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.
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
if not self.PARSED:
|
|
821
|
+
self._parse()
|
|
822
|
+
|
|
823
|
+
return super().__getattribute__(name)
|
|
824
|
+
|
|
825
|
+
def _parse(self): # noqa: C901
|
|
826
|
+
"""
|
|
827
|
+
Parses the options using command-line arguments, environment variables
|
|
828
|
+
and default values.
|
|
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
|
+
|
|
834
|
+
Raises
|
|
835
|
+
------
|
|
836
|
+
ValueError
|
|
837
|
+
If a required option is not provided through a command-line
|
|
838
|
+
argument, an environment variable or a default value.
|
|
839
|
+
TypeError
|
|
840
|
+
If an option is not an `Option` or `Parameter` (deprecated) object.
|
|
841
|
+
ValueError
|
|
842
|
+
If an environment variable is not of the type of the corresponding
|
|
843
|
+
parameter.
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
self.PARSED = True
|
|
847
|
+
|
|
848
|
+
if not self.options:
|
|
111
849
|
return
|
|
112
850
|
|
|
113
851
|
parser = argparse.ArgumentParser(
|
|
@@ -115,112 +853,264 @@ class Options:
|
|
|
115
853
|
usage="%(prog)s [options]",
|
|
116
854
|
description="Options for %(prog)s. Use command-line arguments (highest precedence) "
|
|
117
855
|
+ "or environment variables.",
|
|
856
|
+
allow_abbrev=False,
|
|
118
857
|
)
|
|
119
|
-
|
|
858
|
+
options_by_field_name: dict[str, Option] = {}
|
|
859
|
+
|
|
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
|
+
)
|
|
120
865
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
866
|
+
# See comment below about ipykernel adding a `-f` argument. We
|
|
867
|
+
# restrict options from having the name 'f' or 'fff' for that
|
|
868
|
+
# reason.
|
|
869
|
+
if option.name == "f" or option.name == "fff":
|
|
870
|
+
raise ValueError("option names 'f', 'fff' are reserved for internal use")
|
|
871
|
+
|
|
872
|
+
if option.name == "PARSED":
|
|
873
|
+
raise ValueError("option name 'PARSED' is reserved for internal use")
|
|
874
|
+
|
|
875
|
+
# Remove any leading '-'. This is in line with argparse's behavior.
|
|
876
|
+
option.name = option.name.lstrip("-")
|
|
877
|
+
|
|
878
|
+
kwargs = {
|
|
879
|
+
"type": self._option_type(option) if self._option_type(option) is not bool else str,
|
|
880
|
+
"help": self._description(option),
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if option.choices is not None:
|
|
884
|
+
kwargs["choices"] = option.choices
|
|
124
885
|
|
|
125
886
|
parser.add_argument(
|
|
126
|
-
f"-{
|
|
127
|
-
f"--{
|
|
128
|
-
|
|
129
|
-
help=self._description(param),
|
|
887
|
+
f"-{option.name}",
|
|
888
|
+
f"--{option.name}",
|
|
889
|
+
**kwargs,
|
|
130
890
|
)
|
|
131
|
-
params_by_name[param.name] = param
|
|
132
891
|
|
|
892
|
+
# Store the option by its field name for easy access later. argparse
|
|
893
|
+
# replaces '-' with '_', so we do the same here.
|
|
894
|
+
options_by_field_name[option.name.replace("-", "_")] = option
|
|
895
|
+
|
|
896
|
+
# The ipkyernel uses a `-f` argument by default that it passes to the
|
|
897
|
+
# execution. We don't want to ignore this argument because we get an
|
|
898
|
+
# error. Fix source: https://stackoverflow.com/a/56349168
|
|
899
|
+
parser.add_argument(
|
|
900
|
+
"-f",
|
|
901
|
+
"--f",
|
|
902
|
+
"--fff",
|
|
903
|
+
help=argparse.SUPPRESS,
|
|
904
|
+
default="1",
|
|
905
|
+
)
|
|
133
906
|
args = parser.parse_args()
|
|
134
907
|
|
|
135
908
|
for arg in vars(args):
|
|
136
|
-
|
|
909
|
+
if arg == "fff" or arg == "f":
|
|
910
|
+
continue
|
|
137
911
|
|
|
138
|
-
|
|
912
|
+
option = options_by_field_name[arg]
|
|
913
|
+
|
|
914
|
+
# First, attempt to set the value of an option from the
|
|
139
915
|
# command-line args.
|
|
140
916
|
arg_value = getattr(args, arg)
|
|
141
917
|
if arg_value is not None:
|
|
142
|
-
value = self.
|
|
918
|
+
value = self._option_value(option, arg_value)
|
|
143
919
|
setattr(self, arg, value)
|
|
144
920
|
continue
|
|
145
921
|
|
|
146
|
-
# Second, attempt to set the value of
|
|
922
|
+
# Second, attempt to set the value of am option from the
|
|
147
923
|
# environment variables.
|
|
148
924
|
upper_name = arg.upper()
|
|
149
925
|
env_value = os.getenv(upper_name)
|
|
150
926
|
if env_value is not None:
|
|
151
927
|
try:
|
|
152
|
-
typed_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
|
+
)
|
|
153
931
|
except ValueError:
|
|
154
|
-
raise ValueError(
|
|
932
|
+
raise ValueError(
|
|
933
|
+
f'environment variable "{upper_name}" is not of type {self._option_type(option)}'
|
|
934
|
+
) from None
|
|
155
935
|
|
|
156
|
-
value = self.
|
|
936
|
+
value = self._option_value(option, typed_env_value)
|
|
157
937
|
setattr(self, arg, value)
|
|
158
938
|
continue
|
|
159
939
|
|
|
160
|
-
# Finally, attempt to set
|
|
161
|
-
#
|
|
162
|
-
if
|
|
163
|
-
setattr(self, arg,
|
|
164
|
-
continue
|
|
165
|
-
|
|
166
|
-
if not param.required:
|
|
940
|
+
# Finally, attempt to set a default value. This is only allowed
|
|
941
|
+
# for non-required options.
|
|
942
|
+
if not option.required:
|
|
943
|
+
setattr(self, arg, option.default)
|
|
167
944
|
continue
|
|
168
945
|
|
|
169
|
-
# At this point, the
|
|
946
|
+
# At this point, the option is required and no value was
|
|
170
947
|
# provided
|
|
171
948
|
raise ValueError(
|
|
172
|
-
f'
|
|
949
|
+
f'option "{arg}" is required but not provided through: command-line args, env vars, or default value'
|
|
173
950
|
)
|
|
174
951
|
|
|
175
|
-
def
|
|
952
|
+
def _description(self, option: Option) -> str:
|
|
176
953
|
"""
|
|
177
|
-
|
|
954
|
+
Returns a description for an option.
|
|
955
|
+
|
|
956
|
+
This is an internal method used to create the help text for options
|
|
957
|
+
in the command-line argument parser.
|
|
958
|
+
|
|
959
|
+
Parameters
|
|
960
|
+
----------
|
|
961
|
+
option : Option
|
|
962
|
+
The option to get the description for.
|
|
178
963
|
|
|
179
964
|
Returns
|
|
180
965
|
-------
|
|
181
|
-
|
|
182
|
-
|
|
966
|
+
str
|
|
967
|
+
A formatted description string for the option.
|
|
183
968
|
"""
|
|
184
969
|
|
|
185
|
-
|
|
186
|
-
|
|
970
|
+
description = ""
|
|
971
|
+
if isinstance(option, Parameter):
|
|
972
|
+
description = "DEPRECATED (initialized with <Parameter>, use <Option> instead) "
|
|
187
973
|
|
|
188
|
-
|
|
974
|
+
description += f"[env var: {option.name.upper()}]"
|
|
189
975
|
|
|
190
|
-
|
|
976
|
+
if option.required:
|
|
977
|
+
description += " (required)"
|
|
191
978
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"""Returns a description for a parameter."""
|
|
979
|
+
if option.default is not None:
|
|
980
|
+
description += f" (default: {option.default})"
|
|
195
981
|
|
|
196
|
-
description
|
|
982
|
+
description += f" (type: {self._option_type(option).__name__})"
|
|
197
983
|
|
|
198
|
-
if
|
|
199
|
-
description += " (
|
|
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})"
|
|
200
989
|
|
|
201
|
-
if
|
|
202
|
-
description += f" (
|
|
990
|
+
if isinstance(option, Option) and option.hidden_from:
|
|
991
|
+
description += f" (hidden from: {', '.join(option.hidden_from)})"
|
|
203
992
|
|
|
204
|
-
|
|
993
|
+
if isinstance(option, Option) and option.display_name is not None:
|
|
994
|
+
description += f" (display name: {option.display_name})"
|
|
205
995
|
|
|
206
|
-
if
|
|
207
|
-
description += f": {
|
|
996
|
+
if option.description is not None and option.description != "":
|
|
997
|
+
description += f": {option.description}"
|
|
208
998
|
|
|
209
999
|
return description
|
|
210
1000
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1001
|
+
def _option_value(self, option: Option, value: Any) -> Any:
|
|
1002
|
+
"""
|
|
1003
|
+
Handles how the value of an option is extracted.
|
|
1004
|
+
|
|
1005
|
+
This is an internal method that converts string values to boolean
|
|
1006
|
+
values for boolean options.
|
|
214
1007
|
|
|
215
|
-
|
|
216
|
-
|
|
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:
|
|
217
1025
|
return value
|
|
218
1026
|
|
|
219
1027
|
value = str(value).lower()
|
|
220
1028
|
|
|
221
1029
|
if value in ("true", "1", "t", "y", "yes"):
|
|
222
1030
|
return True
|
|
223
|
-
if value in ("false", "0", "f", "n", "no"):
|
|
224
|
-
return False
|
|
225
1031
|
|
|
226
|
-
|
|
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
|