nextmv 0.26.3__py3-none-any.whl → 0.28.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/options.py CHANGED
@@ -1,4 +1,19 @@
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
@@ -15,9 +30,8 @@ from nextmv.deprecated import deprecated
15
30
  @dataclass
16
31
  class Parameter:
17
32
  """
18
- DEPRECATION WARNING
19
- ----------
20
- `Parameter` is deprecated, use `Option` instead.
33
+ !!! warning
34
+ `Parameter` is deprecated, use `Option` instead.
21
35
 
22
36
  Parameter that is used in a `Configuration`. When a parameter is required,
23
37
  it is a good practice to provide a default value for it. This is because
@@ -29,20 +43,30 @@ class Parameter:
29
43
  ----------
30
44
  name : str
31
45
  The name of the parameter.
46
+
32
47
  param_type : type
33
48
  The type of the parameter.
49
+
34
50
  default : Any, optional
35
51
  The default value of the parameter. Even though this is optional, it is
36
52
  recommended to provide a default value for all parameters.
53
+
37
54
  description : str, optional
38
55
  An optional description of the parameter. This is useful for generating
39
56
  help messages for the configuration.
57
+
40
58
  required : bool, optional
41
59
  Whether the parameter is required. If a parameter is required, it will
42
- 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
43
61
  argument, an environment variable or a default value.
62
+
44
63
  choices : list[Optional[Any]], optional
45
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)
46
70
  """
47
71
 
48
72
  name: str
@@ -64,18 +88,23 @@ class Parameter:
64
88
  """Limits values to a specific set of choices."""
65
89
 
66
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
+ """
67
97
  deprecated(
68
98
  name="Parameter",
69
- reason="`Parameter` is deprecated, use `Option` instead.",
99
+ reason="`Parameter` is deprecated, use `Option` instead",
70
100
  )
71
101
 
72
102
  @classmethod
73
103
  def from_dict(cls, data: dict[str, Any]) -> "Parameter":
74
104
  """
75
- DEPRECATION WARNING
76
- ----------
77
- `Parameter` is deprecated, use `Option` instead. Parameter.from_dict ->
78
- Option.from_dict
105
+ !!! warning
106
+ `Parameter` is deprecated, use `Option` instead.
107
+ `Parameter.from_dict` -> `Option.from_dict`
79
108
 
80
109
  Creates an instance of `Parameter` from a dictionary.
81
110
 
@@ -109,17 +138,26 @@ class Parameter:
109
138
 
110
139
  def to_dict(self) -> dict[str, Any]:
111
140
  """
112
- DEPRECATION WARNING
113
- ----------
114
- `Parameter` is deprecated, use `Option` instead. Parameter.to_dict ->
115
- Option.to_dict
141
+ !!! warning
142
+ `Parameter` is deprecated, use `Option` instead.
143
+ `Parameter.to_dict` -> `Option.to_dict`
116
144
 
117
145
  Converts the parameter to a dict.
118
146
 
119
147
  Returns
120
148
  -------
121
149
  dict[str, Any]
122
- 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
123
161
  """
124
162
 
125
163
  deprecated(
@@ -140,16 +178,24 @@ class Parameter:
140
178
  @dataclass
141
179
  class Option:
142
180
  """
143
- `Option` that is used in `Options`. When an `Option` is required,
144
- it is a good practice to provide a default value for it. This is because
145
- the `Options` will raise an error if a required `Option` is not
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
146
192
  provided through a command-line argument, an environment variable or a
147
193
  default value.
148
194
 
149
- Attributes
195
+ Parameters
150
196
  ----------
151
197
  name : str
152
- The name of the option.
198
+ `name`. The name of the option.
153
199
  option_type : type
154
200
  The type of the option.
155
201
  default : Any, optional
@@ -160,10 +206,24 @@ class Option:
160
206
  help messages for the `Options`.
161
207
  required : bool, optional
162
208
  Whether the option is required. If an option is required, it will
163
- be an error to not provide a value for it, either trough a command-line
209
+ be an error to not provide a value for it, either through a command-line
164
210
  argument, an environment variable or a default value.
165
211
  choices : list[Optional[Any]], optional
166
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
+
219
+ Examples
220
+ --------
221
+ ```python
222
+ from nextmv.options import Option
223
+ opt = Option("duration", str, "30s", description="solver duration", required=False)
224
+ opt.name
225
+ opt.default
226
+ ```
167
227
  """
168
228
 
169
229
  name: str
@@ -189,6 +249,13 @@ class Option:
189
249
  """
190
250
  choices: Optional[list[Any]] = None
191
251
  """Limits values to a specific set of choices."""
252
+ additional_attributes: Optional[dict[str, Any]] = None
253
+ """
254
+ Optional additional attributes for the option. The Nextmv Cloud may
255
+ perform validation on these attributes. For example, the maximum length of
256
+ a string or the maximum value of an integer. These additional attributes
257
+ will be shown in the help message of the `Options`.
258
+ """
192
259
 
193
260
  @classmethod
194
261
  def from_dict(cls, data: dict[str, Any]) -> "Option":
@@ -197,25 +264,36 @@ class Option:
197
264
 
198
265
  Parameters
199
266
  ----------
200
- data : dict[str, Any]
267
+
268
+ data: dict[str, Any]
201
269
  The dictionary representation of an option.
202
270
 
203
271
  Returns
204
272
  -------
205
273
  Option
206
274
  An instance of `Option`.
275
+
276
+ Examples
277
+ --------
278
+ >>> opt_dict = {"name": "timeout", "option_type": "<class 'int'>", "default": 60}
279
+ >>> option = Option.from_dict(opt_dict)
280
+ >>> option.name
281
+ 'timeout'
282
+ >>> option.default
283
+ 60
207
284
  """
208
285
 
209
286
  option_type_string = data["option_type"]
210
287
  option_type = getattr(builtins, option_type_string.split("'")[1])
211
288
 
212
- return Option(
289
+ return cls(
213
290
  name=data["name"],
214
291
  option_type=option_type,
215
292
  default=data.get("default"),
216
293
  description=data.get("description"),
217
294
  required=data.get("required", False),
218
295
  choices=data.get("choices"),
296
+ additional_attributes=data.get("additional_attributes"),
219
297
  )
220
298
 
221
299
  def to_dict(self) -> dict[str, Any]:
@@ -225,7 +303,16 @@ class Option:
225
303
  Returns
226
304
  -------
227
305
  dict[str, Any]
228
- The option as a dict.
306
+ The option as a dict with all its attributes.
307
+
308
+ Examples
309
+ --------
310
+ >>> opt = Option("duration", str, "30s", description="solver duration")
311
+ >>> opt_dict = opt.to_dict()
312
+ >>> opt_dict["name"]
313
+ 'duration'
314
+ >>> opt_dict["default"]
315
+ '30s'
229
316
  """
230
317
 
231
318
  return {
@@ -235,15 +322,23 @@ class Option:
235
322
  "description": self.description,
236
323
  "required": self.required,
237
324
  "choices": self.choices,
325
+ "additional_attributes": self.additional_attributes,
238
326
  }
239
327
 
240
328
 
241
329
  class Options:
242
330
  """
243
- Options for a run. To initialize options, pass in one or more `Option`
244
- objects. The options will look for the values of the given parameters in
245
- the following order: command-line arguments, environment variables, default
246
- values.
331
+ Options container for application configuration.
332
+
333
+ You can import the `Options` class directly from `nextmv`:
334
+
335
+ ```python
336
+ from nextmv import Options
337
+ ```
338
+
339
+ To initialize options, pass in one or more `Option` objects. The options
340
+ will look for the values of the given parameters in the following order:
341
+ command-line arguments, environment variables, default values.
247
342
 
248
343
  Once the `Options` are initialized, you can access the underlying options as
249
344
  attributes of the `Options` object. For example, if you have an
@@ -264,7 +359,7 @@ class Options:
264
359
  be merged with other options. After options are parsed, you may get the
265
360
  help message by running the script with the `-h/--help` flag.
266
361
 
267
- Attributes
362
+ Parameters
268
363
  ----------
269
364
  *options : Option
270
365
  The list of `Option` objects that are used in the options. At least one
@@ -280,7 +375,6 @@ class Options:
280
375
  ... )
281
376
  >>>
282
377
  >>> print(options.duration, options.threads, options.to_dict())
283
-
284
378
  30s 4 {"duration": "30s", "threads": 4}
285
379
 
286
380
  Raises
@@ -299,8 +393,14 @@ class Options:
299
393
  PARSED = False
300
394
 
301
395
  def __init__(self, *options: Option):
302
- """Initializes the options."""
396
+ """
397
+ Initialize an Options instance with the provided option objects.
303
398
 
399
+ Parameters
400
+ ----------
401
+ *options : Option
402
+ The option objects to include in this Options instance.
403
+ """
304
404
  self.options = copy.deepcopy(options)
305
405
 
306
406
  def to_dict(self) -> dict[str, Any]:
@@ -312,7 +412,17 @@ class Options:
312
412
  Returns
313
413
  -------
314
414
  dict[str, Any]
315
- The options as a dict.
415
+ The options as a dict where keys are option names and values
416
+ are the corresponding option values.
417
+
418
+ Examples
419
+ --------
420
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
421
+ >>> options_dict = options.to_dict()
422
+ >>> options_dict["duration"]
423
+ '30s'
424
+ >>> options_dict["threads"]
425
+ 4
316
426
  """
317
427
 
318
428
  if not self.PARSED:
@@ -335,16 +445,28 @@ class Options:
335
445
  def to_dict_cloud(self) -> dict[str, str]:
336
446
  """
337
447
  Converts the options to a dict that can be used in the Nextmv Cloud.
448
+
338
449
  Cloud has a hard requirement that options are passed as strings. This
339
450
  method converts the options to a dict with string values. This is
340
451
  useful for passing options to the Nextmv Cloud.
452
+
341
453
  As a side effect, this method parses the options if they have not been
342
454
  parsed yet. See the `parse` method for more information.
343
455
 
344
456
  Returns
345
457
  -------
346
458
  dict[str, str]
347
- The options as a dict with string values.
459
+ The options as a dict with string values where non-string values
460
+ are JSON-encoded.
461
+
462
+ Examples
463
+ --------
464
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
465
+ >>> cloud_dict = options.to_dict_cloud()
466
+ >>> cloud_dict["duration"]
467
+ '30s'
468
+ >>> cloud_dict["threads"]
469
+ '4'
348
470
  """
349
471
 
350
472
  options_dict = self.to_dict()
@@ -360,10 +482,8 @@ class Options:
360
482
 
361
483
  def parameters_dict(self) -> list[dict[str, Any]]:
362
484
  """
363
- DEPRECATION WARNING
364
- ----------
365
- `Parameter` is deprecated, use `Option` instead. Options.parameters_dict
366
- -> Options.options_dict
485
+ !!! warning
486
+ `Parameter` is deprecated, use `Option` instead. `Options.parameters_dict` -> `Options.options_dict`
367
487
 
368
488
  Converts the options to a list of dicts. Each dict is the dict
369
489
  representation of a `Parameter`.
@@ -390,6 +510,15 @@ class Options:
390
510
  -------
391
511
  list[dict[str, Any]]
392
512
  The list of dictionaries (`Option` entries).
513
+
514
+ Examples
515
+ --------
516
+ >>> options = Options(Option("duration", str, "30s"), Option("threads", int, 4))
517
+ >>> opt_dicts = options.options_dict()
518
+ >>> opt_dicts[0]["name"]
519
+ 'duration'
520
+ >>> opt_dicts[1]["name"]
521
+ 'threads'
393
522
  """
394
523
 
395
524
  return [opt.to_dict() for opt in self.options]
@@ -408,7 +537,7 @@ class Options:
408
537
  After Options have been parsed, they cannot be merged with other
409
538
  Options. If you need to merge Options, do so before parsing them.
410
539
 
411
- Example 1
540
+ Examples
412
541
  -------
413
542
  >>> import nextmv
414
543
  >>>
@@ -418,8 +547,6 @@ class Options:
418
547
  ... )
419
548
  >>> options.parse() # Does not raise an exception.
420
549
 
421
- Example 2
422
- -------
423
550
  >>> import nextmv
424
551
  >>>
425
552
  >>> options = nextmv.Options(
@@ -450,16 +577,22 @@ class Options:
450
577
 
451
578
  def merge(self, new: "Options") -> "Options":
452
579
  """
453
- Merges the current options with the new options. This method cannot be
454
- used if any of the options have been parsed. When options are parsed,
455
- values are read from the command-line arguments, environment variables
456
- and default values. Merging options after parsing would result in
457
- unpredictable behavior.
580
+ Merges the current options with the new options.
581
+
582
+ This method cannot be used if any of the options have been parsed. When
583
+ options are parsed, values are read from the command-line arguments,
584
+ environment variables and default values. Merging options after parsing
585
+ would result in unpredictable behavior.
458
586
 
459
587
  Parameters
460
588
  ----------
461
589
  new : Options
462
- The new options to merge.
590
+ The new options to merge with the current options.
591
+
592
+ Returns
593
+ -------
594
+ Options
595
+ The merged options object (self).
463
596
 
464
597
  Raises
465
598
  ------
@@ -468,10 +601,15 @@ class Options:
468
601
  RuntimeError
469
602
  If the new options have already been parsed.
470
603
 
471
- Returns
472
- -------
473
- Options
474
- The merged options.
604
+ Examples
605
+ --------
606
+ >>> opt1 = Options(Option("duration", str, "30s"))
607
+ >>> opt2 = Options(Option("threads", int, 4))
608
+ >>> merged = opt1.merge(opt2)
609
+ >>> merged.duration
610
+ '30s'
611
+ >>> merged.threads
612
+ 4
475
613
  """
476
614
 
477
615
  if self.PARSED:
@@ -493,13 +631,16 @@ class Options:
493
631
  @classmethod
494
632
  def from_dict(cls, data: dict[str, Any]) -> "Options":
495
633
  """
496
- Creates an instance of `Options` from a dictionary. The dictionary
497
- should have the following structure:
634
+ Creates an instance of `Options` from a dictionary.
635
+
636
+ The dictionary should have the following structure:
498
637
 
638
+ ```python
499
639
  {
500
640
  "duration": "30",
501
641
  "threads": 4,
502
642
  }
643
+ ```
503
644
 
504
645
  Parameters
505
646
  ----------
@@ -509,7 +650,16 @@ class Options:
509
650
  Returns
510
651
  -------
511
652
  Options
512
- An instance of `Options`.
653
+ An instance of `Options` with options created from the dictionary.
654
+
655
+ Examples
656
+ --------
657
+ >>> data = {"duration": "30s", "threads": 4}
658
+ >>> options = Options.from_dict(data)
659
+ >>> options.duration
660
+ '30s'
661
+ >>> options.threads
662
+ 4
513
663
  """
514
664
 
515
665
  options = []
@@ -522,17 +672,17 @@ class Options:
522
672
  @classmethod
523
673
  def from_parameters_dict(cls, parameters_dict: list[dict[str, Any]]) -> "Options":
524
674
  """
525
- DEPRECATION WARNING
526
- ----------
527
- `Parameter` is deprecated, use `Option` instead. Options.from_parameters_dict
528
- -> Options.from_options_dict
675
+ !!! warning
676
+
677
+ `Parameter` is deprecated, use `Option` instead.
678
+ `Options.from_parameters_dict` -> `Options.from_options_dict`
529
679
 
530
680
  Creates an instance of `Options` from parameters in dict form. Each
531
681
  entry is the dict representation of a `Parameter`.
532
682
 
533
683
  Parameters
534
684
  ----------
535
- data : list[dict[str, Any]]
685
+ parameters_dict : list[dict[str, Any]]
536
686
  The list of dictionaries (parameter entries).
537
687
 
538
688
  Returns
@@ -562,13 +712,25 @@ class Options:
562
712
 
563
713
  Parameters
564
714
  ----------
565
- data : list[dict[str, Any]]
715
+ options_dict : list[dict[str, Any]]
566
716
  The list of dictionaries (`Option` entries).
567
717
 
568
718
  Returns
569
719
  -------
570
720
  Options
571
721
  An instance of `Options`.
722
+
723
+ Examples
724
+ --------
725
+ >>> options_dict = [
726
+ ... {"name": "duration", "option_type": "<class 'str'>", "default": "30s"},
727
+ ... {"name": "threads", "option_type": "<class 'int'>", "default": 4}
728
+ ... ]
729
+ >>> options = Options.from_options_dict(options_dict)
730
+ >>> options.duration
731
+ '30s'
732
+ >>> options.threads
733
+ 4
572
734
  """
573
735
 
574
736
  options = []
@@ -580,8 +742,20 @@ class Options:
580
742
 
581
743
  def __getattr__(self, name: str) -> Any:
582
744
  """
583
- Gets an attribute of the options. This is called when an attribute
584
- is accessed. It parses the options if they have not been parsed yet.
745
+ Gets an attribute of the options.
746
+
747
+ This is called when an attribute is accessed. It parses the options
748
+ if they have not been parsed yet.
749
+
750
+ Parameters
751
+ ----------
752
+ name : str
753
+ The name of the attribute to get.
754
+
755
+ Returns
756
+ -------
757
+ Any
758
+ The value of the attribute.
585
759
  """
586
760
 
587
761
  if not self.PARSED:
@@ -594,6 +768,10 @@ class Options:
594
768
  Parses the options using command-line arguments, environment variables
595
769
  and default values.
596
770
 
771
+ This is an internal method that is called by `parse()` and `__getattr__()`.
772
+ It sets the `PARSED` flag to True and sets the values of the options
773
+ based on command-line arguments, environment variables, and default values.
774
+
597
775
  Raises
598
776
  ------
599
777
  ValueError
@@ -657,7 +835,7 @@ class Options:
657
835
  options_by_field_name[option.name.replace("-", "_")] = option
658
836
 
659
837
  # The ipkyernel uses a `-f` argument by default that it passes to the
660
- # execution. We dont want to ignore this argument because we get an
838
+ # execution. We don't want to ignore this argument because we get an
661
839
  # error. Fix source: https://stackoverflow.com/a/56349168
662
840
  parser.add_argument(
663
841
  "-f",
@@ -713,7 +891,22 @@ class Options:
713
891
  )
714
892
 
715
893
  def _description(self, option: Option) -> str:
716
- """Returns a description for an option."""
894
+ """
895
+ Returns a description for an option.
896
+
897
+ This is an internal method used to create the help text for options
898
+ in the command-line argument parser.
899
+
900
+ Parameters
901
+ ----------
902
+ option : Option
903
+ The option to get the description for.
904
+
905
+ Returns
906
+ -------
907
+ str
908
+ A formatted description string for the option.
909
+ """
717
910
 
718
911
  description = ""
719
912
  if isinstance(option, Parameter):
@@ -729,13 +922,35 @@ class Options:
729
922
 
730
923
  description += f" (type: {self._option_type(option).__name__})"
731
924
 
925
+ if isinstance(option, Option) and option.additional_attributes is not None:
926
+ description += f" (additional attributes: {option.additional_attributes})"
927
+
732
928
  if option.description is not None and option.description != "":
733
929
  description += f": {option.description}"
734
930
 
735
931
  return description
736
932
 
737
933
  def _option_value(self, option: Option, value: Any) -> Any:
738
- """Handles how the value of an option is extracted."""
934
+ """
935
+ Handles how the value of an option is extracted.
936
+
937
+ This is an internal method that converts string values to boolean
938
+ values for boolean options.
939
+
940
+ Parameters
941
+ ----------
942
+ option : Option
943
+ The option to extract the value for.
944
+ value : Any
945
+ The value to extract.
946
+
947
+ Returns
948
+ -------
949
+ Any
950
+ The extracted value. For boolean options, string values like
951
+ "true", "1", "t", "y", and "yes" are converted to True, and
952
+ other values are converted to False.
953
+ """
739
954
 
740
955
  opt_type = self._option_type(option)
741
956
  if opt_type is not bool:
@@ -750,11 +965,28 @@ class Options:
750
965
 
751
966
  @staticmethod
752
967
  def _option_type(option: Union[Option, Parameter]) -> type:
753
- """Auxiliary function for handling the type of an option. This function
754
- was introduced for backwards compatibility with the deprecated
755
- `Parameter` class. Once `Parameter` is removed, this function can be removed
756
- as well. When the function is removed, use the `option.option_type`
757
- attribute directly, instead of calling this function.
968
+ """
969
+ Get the type of an option.
970
+
971
+ This auxiliary function was introduced for backwards compatibility with
972
+ the deprecated `Parameter` class. Once `Parameter` is removed, this function
973
+ can be removed as well. When the function is removed, use the
974
+ `option.option_type` attribute directly, instead of calling this function.
975
+
976
+ Parameters
977
+ ----------
978
+ option : Union[Option, Parameter]
979
+ The option to get the type for.
980
+
981
+ Returns
982
+ -------
983
+ type
984
+ The type of the option.
985
+
986
+ Raises
987
+ ------
988
+ TypeError
989
+ If the option is not an `Option` or `Parameter` object.
758
990
  """
759
991
 
760
992
  if isinstance(option, Option):