mlrl-util 0.12.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.
mlrl/__init__.py ADDED
File without changes
mlrl/util/__init__.py ADDED
File without changes
mlrl/util/arrays.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ Author: Michael Rapp (michael.rapp.ml@gmail.com)
3
+
4
+ Provides utility functions for handling arrays.
5
+ """
6
+ from enum import Enum
7
+ from typing import Optional, Set
8
+
9
+ import numpy as np
10
+
11
+ from scipy.sparse import issparse, isspmatrix_coo, isspmatrix_csc, isspmatrix_csr, isspmatrix_dok, isspmatrix_lil, \
12
+ sparray
13
+
14
+ from mlrl.util.format import format_iterable
15
+
16
+
17
+ class SparseFormat(Enum):
18
+ """
19
+ Specifies all valid textual representations of sparse matrix formats.
20
+ """
21
+ LIL = 'lil'
22
+ COO = 'coo'
23
+ DOK = 'dok'
24
+ CSC = 'csc'
25
+ CSR = 'csr'
26
+
27
+
28
+ def is_lil(array) -> bool:
29
+ """
30
+ Returns whether a given `scipy.sparse.spmatrix` or `scipy.sparse.sparray` uses the LIL format or not.
31
+
32
+ :param array: A `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
33
+ :return: True, if the given array uses the LIL format, False otherwise
34
+ """
35
+ return isspmatrix_lil(array) or (isinstance(array, sparray) and array.format == 'lil')
36
+
37
+
38
+ def is_coo(array) -> bool:
39
+ """
40
+ Returns whether a given `scipy.sparse.spmatrix` or `scipy.sparse.sparray` uses the COO format or not.
41
+
42
+ :param array: A `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
43
+ :return: True, if the given array uses the COO format, False otherwise
44
+ """
45
+ return isspmatrix_coo(array) or (isinstance(array, sparray) and array.format == 'coo')
46
+
47
+
48
+ def is_dok(array) -> bool:
49
+ """
50
+ Returns whether a given `scipy.sparse.spmatrix` or `scipy.sparse.sparray` uses the DOK format or not.
51
+
52
+ :param array: A `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
53
+ :return: True, if the given array uses the DOK format, False otherwise
54
+ """
55
+ return isspmatrix_dok(array) or (isinstance(array, sparray) and array.format == 'dok')
56
+
57
+
58
+ def is_csc(array) -> bool:
59
+ """
60
+ Returns whether a given `scipy.sparse.spmatrix` or `scipy.sparse.sparray` uses the CSC format or not.
61
+
62
+ :param array: A `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
63
+ :return: True, if the given array uses the CSC format, False otherwise
64
+ """
65
+ return isspmatrix_csc(array) or (isinstance(array, sparray) and array.format == 'csc')
66
+
67
+
68
+ def is_csr(array) -> bool:
69
+ """
70
+ Returns whether a given `scipy.sparse.spmatrix` or `scipy.sparse.sparray` uses the CSR format or not.
71
+
72
+ :param array: A `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
73
+ :return: True, if the given array uses the CSR format, False otherwise
74
+ """
75
+ return isspmatrix_csr(array) or (isinstance(array, sparray) and array.format == 'csr')
76
+
77
+
78
+ def is_sparse(array, supported_formats: Optional[Set[SparseFormat]] = None) -> bool:
79
+ """
80
+ Returns whether a given array is a `scipy.sparse.spmatrix` or `scipy.sparse.sparray` or not.
81
+
82
+ :param array: A `np.ndarray`, `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
83
+ :param supported_formats: A set of supported `SparseFormat`s, the `scipy.sparse.spmatrix` or
84
+ `scipy.sparse.sparray` may use or None, if the format should not be checked
85
+ :return: True, if the given array is a `scipy.sparse.spmatrix` or `scipy.sparse.sparray` using
86
+ one of the supported formats, False otherwise
87
+ """
88
+ if supported_formats:
89
+ lil = SparseFormat.LIL in supported_formats and is_lil(array)
90
+ coo = SparseFormat.COO in supported_formats and is_coo(array)
91
+ dok = SparseFormat.DOK in supported_formats and is_dok(array)
92
+ csc = SparseFormat.CSC in supported_formats and is_csc(array)
93
+ csr = SparseFormat.CSR in supported_formats and is_csr(array)
94
+
95
+ if lil or coo or dok or csc or csr:
96
+ return True
97
+ return False
98
+
99
+ return issparse(array)
100
+
101
+
102
+ def is_sparse_and_memory_efficient(array,
103
+ sparse_format: SparseFormat,
104
+ dtype: Optional[np.dtype] = None,
105
+ sparse_values: bool = True) -> bool:
106
+ """
107
+ Returns whether a given matrix uses sparse format and is expected to occupy less memory than a dense matrix.
108
+
109
+ :param array: A `np.ndarray`, `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be checked
110
+ :param sparse_format: The `SparseFormat` to be used. Must be `SparseFormat.CSC` or `SparseFormat.CSR`
111
+ :param dtype: The type of the values that should be stored in the matrix or None, if it should be obtained
112
+ from the given array
113
+ :param sparse_values: True, if the values must explicitly be stored when using a sparse format, False otherwise
114
+ :return: True, if the given matrix uses a sparse format an is expected to occupy less memory than a
115
+ dense matrix, False otherwise
116
+ """
117
+ supported_formats = {SparseFormat.CSC, SparseFormat.CSR}
118
+
119
+ if sparse_format not in supported_formats:
120
+ raise ValueError('Unable to estimate memory requirements of given sparse format: Must be one of '
121
+ + format_iterable(supported_formats) + ', but is "' + str(sparse_format) + '"')
122
+
123
+ if is_sparse(array):
124
+ num_pointers = array.shape[1 if sparse_format == SparseFormat.CSC else 0]
125
+ size_int = np.dtype(np.uint32).itemsize
126
+ dtype = dtype if dtype else array.dtype
127
+ size_data = np.dtype(dtype).itemsize
128
+ size_sparse_data = size_data if sparse_values else 0
129
+ num_dense_elements = array.nnz
130
+ size_sparse = (num_dense_elements * size_sparse_data) + (num_dense_elements * size_int) + (num_pointers
131
+ * size_int)
132
+ size_dense = np.prod(array.shape) * size_data
133
+ return size_sparse < size_dense
134
+ return False
135
+
136
+
137
+ def enforce_dense(array, order: str, dtype: Optional[np.dtype] = None, sparse_value=0) -> np.ndarray:
138
+ """
139
+ Converts a given array into a `np.ndarray`, if necessary, and enforces a specific memory layout and data type to be
140
+ used.
141
+
142
+ :param array: A `np.ndarray`, `scipy.sparse.spmatrix` or `scipy.sparse.sparray` to be converted
143
+ :param order: The memory layout to be used. Must be `C` or `F`
144
+ :param dtype: The data type to be used or None, if the data type should not be changed
145
+ :param sparse_value: The value that should be used for sparse elements in the given array
146
+ :return: A `np.ndarray` that uses the given memory layout and data type
147
+ """
148
+ dtype = dtype if dtype else array.dtype
149
+
150
+ if is_sparse(array):
151
+ if sparse_value != 0:
152
+ dense_array = np.full(shape=array.shape, fill_value=sparse_value, dtype=dtype, order=order)
153
+ dense_array[array.nonzero()] = 0
154
+ dense_array += array
155
+ return np.asarray(dense_array, dtype=dtype, order=order)
156
+ return np.require(array.toarray(order=order), dtype=dtype)
157
+ return np.require(array, dtype=dtype, requirements=[order])
158
+
159
+
160
+ def enforce_2d(array: np.ndarray) -> np.ndarray:
161
+ """
162
+ Converts a given `np.ndarray` into a two-dimensional array if it is one-dimensional.
163
+
164
+ :param array: A `np.ndarray` to be converted
165
+ :return: A `np.ndarray` with at least two dimensions
166
+ """
167
+ if array.ndim == 1:
168
+ return np.expand_dims(array, axis=1)
169
+ return array
mlrl/util/cli.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ Author: Michael Rapp (michael.rapp.ml@gmail.com)
3
+
4
+ Provides classes for configuring the arguments of a command line interface.
5
+ """
6
+ import sys
7
+
8
+ from argparse import ArgumentError, ArgumentParser, Namespace
9
+ from enum import Enum, EnumType
10
+ from functools import cached_property
11
+ from typing import Any, Dict, Optional, Set
12
+
13
+ from mlrl.util.format import format_enum_values, format_set
14
+ from mlrl.util.options import BooleanOption, parse_enum, parse_param, parse_param_and_options
15
+
16
+ NONE = 'none'
17
+
18
+
19
+ class Argument:
20
+ """
21
+ A single argument of a command line interface for which the user can provide a custom value.
22
+ """
23
+
24
+ def __init__(self, *names: str, required: Optional[bool] = False, default: Optional[Any] = None, **kwargs: Any):
25
+ """
26
+ :param names: One of several names of the argument
27
+ :param required: True, if the argument is mandatory, False otherwise
28
+ :param default: The default value of the argument, if any
29
+ :param kwargs: Optional keyword argument to be passed to an `ArgumentParser`
30
+ """
31
+ self.names = set(names)
32
+ self.required = required
33
+ self.default = default
34
+ self.kwargs = dict(kwargs)
35
+
36
+ @cached_property
37
+ def name(self) -> str:
38
+ """
39
+ The name of the argument.
40
+ """
41
+ return next(iter(self.names))
42
+
43
+ @cached_property
44
+ def key(self) -> str:
45
+ """
46
+ The key of the argument in a `Namespace`.
47
+ """
48
+ return self.name.lstrip('--').replace('-', '_')
49
+
50
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
51
+ """
52
+ Returns the value provided by the user for this argument.
53
+
54
+ :param args: A `Namespace` that provides access to the values provided by the user
55
+ :param default: The default value to be returned if no value is available
56
+ :return: The value provided by the user or `default`, if no value is available
57
+ """
58
+ value = getattr(args, self.key, None)
59
+ value = self.default if value is None else value
60
+ return default if value is None else value
61
+
62
+ def __hash__(self) -> int:
63
+ return hash(self.key)
64
+
65
+ def __eq__(self, other: Any) -> bool:
66
+ return isinstance(other, type(self)) and self.key == other.key
67
+
68
+
69
+ class StringArgument(Argument):
70
+ """
71
+ An argument of a command line interface for which the user can provide a custom string value.
72
+ """
73
+
74
+ def __init__(self,
75
+ *names: str,
76
+ description: Optional[str] = None,
77
+ default: Optional[str] = None,
78
+ required: bool = False):
79
+ """
80
+ :param names: One or several names of the argument
81
+ :param description: An optional description of the argument
82
+ :param default: The default value
83
+ :param required: True, if the argument is mandatory, False otherwise
84
+ """
85
+ super().__init__(*names, default=default, help=description, type=str, required=required)
86
+
87
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
88
+ value = super().get_value(args, default=default)
89
+ return None if value is None else str(value)
90
+
91
+
92
+ class IntArgument(Argument):
93
+ """
94
+ An argument of a command line interface for which the user can provide a custom integer value.
95
+ """
96
+
97
+ def __init__(self,
98
+ *names: str,
99
+ description: Optional[str] = None,
100
+ default: Optional[int] = None,
101
+ required: bool = False):
102
+ """
103
+ :param names: One or several names of the argument
104
+ :param description: An optional description of the argument
105
+ :param default: The default value
106
+ :param required: True, if the argument is mandatory, False otherwise
107
+ """
108
+ super().__init__(*names, default=default, help=description, type=int, required=required)
109
+
110
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
111
+ value = super().get_value(args, default=default)
112
+
113
+ try:
114
+ return None if value is None else int(value)
115
+ except ValueError as error:
116
+ raise ValueError('Expected value of argument ' + self.name + ' to be an integer, but got: '
117
+ + str(value)) from error
118
+
119
+
120
+ class FloatArgument(Argument):
121
+ """
122
+ An argument of a command line interface for which the user can provide a custom floating point value.
123
+ """
124
+
125
+ def __init__(self,
126
+ *names: str,
127
+ description: Optional[str] = None,
128
+ default: Optional[float] = None,
129
+ required: bool = False):
130
+ """
131
+ :param names: One or several names of the argument
132
+ :param description: An optional description of the argument
133
+ :param default: The default value
134
+ :param required: True, if the argument is mandatory, False otherwise
135
+ """
136
+ super().__init__(*names, default=default, help=description, type=float, required=required)
137
+
138
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
139
+ value = super().get_value(args, default=default)
140
+
141
+ try:
142
+ return None if value is None else float(value)
143
+ except ValueError as error:
144
+ raise ValueError('Expected value of argument ' + self.name + ' to be a float, but got: '
145
+ + str(value)) from error
146
+
147
+
148
+ class BoolArgument(Argument):
149
+ """
150
+ An argument of a command line interface for which the user can provide a custom boolean value.
151
+ """
152
+
153
+ @staticmethod
154
+ def __format_description(description: str, has_options: bool) -> str:
155
+ if not description.endswith('.'):
156
+ description += '.'
157
+
158
+ description += ' Must be one of ' + format_enum_values(BooleanOption) + '.'
159
+
160
+ if has_options:
161
+ description += ' For additional options refer to the documentation.'
162
+
163
+ return description
164
+
165
+ def __init__(self,
166
+ *names: str,
167
+ description: Optional[str] = None,
168
+ default: Optional[bool] = None,
169
+ required: bool = False,
170
+ true_options: Optional[Set[str]] = None,
171
+ false_options: Optional[Set[str]] = None):
172
+ """
173
+ :param names: One or several names of the argument
174
+ :param description: An optional description of the argument
175
+ :param default: The default value
176
+ :param required: True, if the argument is mandatory, False otherwise
177
+ :param true_options: The names of options that can be provided by the user in addition to the value "true"
178
+ :param false_options: The names of options that can be provided by the user in addition to the value "false"
179
+ """
180
+ super().__init__(*names,
181
+ default=None if default is None else
182
+ (BooleanOption.TRUE.value if default else BooleanOption.FALSE.value),
183
+ help=self.__format_description(description,
184
+ bool(true_options) or bool(false_options)),
185
+ type=str if true_options or false_options else BooleanOption.parse,
186
+ required=required)
187
+ self.true_options = true_options
188
+ self.false_options = false_options
189
+
190
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
191
+ value = str(super().get_value(args, default=default)).lower()
192
+
193
+ if value:
194
+ true_options = self.true_options
195
+ false_options = self.false_options
196
+
197
+ if true_options or false_options:
198
+ value, options = parse_param_and_options(self.key, value, {
199
+ BooleanOption.TRUE.value: true_options,
200
+ BooleanOption.FALSE.value: false_options
201
+ })
202
+ return BooleanOption.parse(value), options
203
+
204
+ return BooleanOption.parse(value)
205
+
206
+ return None
207
+
208
+
209
+ class SetArgument(Argument):
210
+ """
211
+ An argument of a command line interface for which the user can provide one out of a predefined set of string values.
212
+ """
213
+
214
+ @staticmethod
215
+ def __format_description(description: str, values: Set[str] | Dict[str, Set[str]]) -> str:
216
+ if not description.endswith('.'):
217
+ description += '.'
218
+
219
+ description += ' Must be one of ' + format_set(values.keys() if isinstance(values, dict) else values) + '.'
220
+
221
+ if isinstance(values, dict):
222
+ description += ' For additional options refer to the documentation.'
223
+
224
+ return description
225
+
226
+ def __init__(self,
227
+ *names: str,
228
+ values: Set[str] | Dict[str, Set[str]],
229
+ description: Optional[str] = None,
230
+ default: Optional[str] = None,
231
+ required: bool = False):
232
+ """
233
+ :param names: One or several names of the argument
234
+ :param values: A set that contains the predefined values or a dictionary that contains the predefined
235
+ values, as well as the names of options that can be provided by the user in addition to the
236
+ respective values
237
+ :param description: An optional description of the argument
238
+ :param default: The default value
239
+ :param required: True, if the argument is mandatory, False otherwise
240
+ """
241
+ super().__init__(*names,
242
+ default=default,
243
+ help=self.__format_description(description, values),
244
+ type=str,
245
+ required=required)
246
+ self.supported_values = values
247
+
248
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
249
+ value = super().get_value(args, default=default)
250
+
251
+ if value:
252
+ supported_values = self.supported_values
253
+
254
+ if isinstance(supported_values, dict):
255
+ return parse_param_and_options(self.key, value, supported_values)
256
+
257
+ return parse_param(self.key, value, supported_values)
258
+
259
+ return None
260
+
261
+
262
+ class EnumArgument(SetArgument):
263
+ """
264
+ An argument of a command line interface for which the user can provide one out of a predefined set enum values.
265
+ """
266
+
267
+ def __init__(self,
268
+ *names: str,
269
+ enum: EnumType,
270
+ description: Optional[str] = None,
271
+ default: Optional[Enum] = None,
272
+ required: bool = False):
273
+ """
274
+ :param names: One or several names of the argument
275
+ :param values: An enum that contains the predefined values
276
+ :param description: An optional description of the argument
277
+ :param default: The default value
278
+ :param required: True, if the argument is mandatory, False otherwise
279
+ """
280
+ super().__init__(
281
+ *names,
282
+ values={x.value if isinstance(x.value, str) else x.name.lower()
283
+ for x in enum},
284
+ description=description,
285
+ default=(default.value if isinstance(default.value, str) else default.name.lower()) if default else None,
286
+ required=required)
287
+ self.enum = enum
288
+
289
+ def get_value(self, args: Namespace, default: Optional[Any] = None) -> Optional[Any]:
290
+ value = super().get_value(args, default=default)
291
+ return parse_enum(self.name, value, self.enum) if value else None
292
+
293
+
294
+ class CommandLineInterface:
295
+ """
296
+ Allows to configure a command line interface for running a program.
297
+ """
298
+
299
+ def __init__(self, argument_parser: ArgumentParser, version_text: Optional[str] = None):
300
+ """
301
+ :param argument_parser: The parser that should be used for parsing arguments provided to the command line
302
+ interface by the user
303
+ :param version_text: A text to be shown when the "--version" flag is passed to the command line interface or
304
+ None, if the "--version" flag should not be added to the command line interface
305
+ """
306
+ self._argument_parser = argument_parser
307
+
308
+ if version_text:
309
+ argument_parser.add_argument('-v',
310
+ '--version',
311
+ action='version',
312
+ version=version_text,
313
+ help='Display information about the program.')
314
+
315
+ def add_arguments(self, *arguments: Argument) -> Optional[Namespace]:
316
+ """
317
+ Adds a new argument that enables the user to provide a value to the command line interface.
318
+
319
+ :param arguments: The arguments to be added
320
+ """
321
+ argument_parser = self._argument_parser
322
+
323
+ for argument in arguments:
324
+ try:
325
+ required = argument.required and '--help' not in sys.argv and '-h' not in sys.argv
326
+ argument_parser.add_argument(*argument.names,
327
+ required=required,
328
+ default=argument.default,
329
+ **argument.kwargs)
330
+ except ArgumentError:
331
+ # Argument has already been added
332
+ pass
333
+
334
+ def parse_known_args(self) -> Namespace:
335
+ """
336
+ Parses and returns the values of the arguments already added to the command line interface.
337
+
338
+ :return: A `Namespace` providing access to the values of the arguments
339
+ """
340
+ return self._argument_parser.parse_known_args()[0]
mlrl/util/format.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ Author: Michael Rapp (michael.rapp.ml@gmail.com)
3
+
4
+ Provides utility functions for creating textual representations.
5
+ """
6
+ from enum import EnumType
7
+ from functools import reduce
8
+ from typing import Any, Iterable
9
+
10
+
11
+ def format_iterable(objects: Iterable[Any], separator: str = ', ', delimiter: str = '') -> str:
12
+ """
13
+ Creates and returns a textual representation of objects in an iterable.
14
+
15
+ :param objects: The iterable of objects to be formatted
16
+ :param separator: The string that should be used as a separator
17
+ :param delimiter: The string that should be added at the beginning and end of each object
18
+ :return: The textual representation that has been created
19
+ """
20
+ return reduce(lambda aggr, obj: aggr + (separator if aggr else '') + delimiter + str(obj) + delimiter, objects, '')
21
+
22
+
23
+ def format_enum_values(enum: EnumType) -> str:
24
+ """
25
+ Creates and returns a textual representation of an enum's values.
26
+
27
+ :param enum: The enum to be formatted
28
+ :return: The textual representation that has been created
29
+ """
30
+ return format_set({x.value if isinstance(x.value, str) else x.name.lower() for x in enum})
31
+
32
+
33
+ def format_set(objects: Iterable[Any]) -> str:
34
+ """
35
+ Creates and returns a textual representation of the objects in a set.
36
+
37
+ :param objects: The iterable of objects to be formatted
38
+ :return: The textual representation that has been created
39
+ """
40
+ return '{' + format_iterable(sorted(objects, key=str), delimiter='"') + '}'
mlrl/util/options.py ADDED
@@ -0,0 +1,242 @@
1
+ """
2
+ Author: Michael Rapp (michael.rapp.ml@gmail.com)
3
+
4
+ Provides a data structure that allows to store and parse options that are provided as key-value pairs.
5
+ """
6
+ from enum import Enum, EnumType
7
+ from typing import Any, Dict, Optional, Set, Tuple
8
+
9
+ from mlrl.util.format import format_enum_values, format_set
10
+
11
+
12
+ class BooleanOption(Enum):
13
+ """
14
+ Specifies all valid textual representations of boolean values.
15
+ """
16
+ TRUE = 'true'
17
+ FALSE = 'false'
18
+
19
+ @staticmethod
20
+ def parse(text: str) -> bool:
21
+ """
22
+ Parses a given text that represents a boolean value. If the given text does not represent a valid boolean value,
23
+ a `ValueError` is raised.
24
+
25
+ :param text: The text to be parsed
26
+ :return: True or false, depending on the given text
27
+ """
28
+ if text == BooleanOption.TRUE.value:
29
+ return True
30
+ if text == BooleanOption.FALSE.value:
31
+ return False
32
+ raise ValueError('Invalid boolean value given. Must be one of ' + format_enum_values(BooleanOption)
33
+ + ', but is "' + str(text) + '".')
34
+
35
+
36
+ class Options:
37
+ """
38
+ Stores key-value pairs in a dictionary and provides methods to access and validate them.
39
+ """
40
+
41
+ ERROR_MESSAGE_INVALID_SYNTAX = 'Invalid syntax used to specify additional options'
42
+
43
+ ERROR_MESSAGE_INVALID_OPTION = 'Expected comma-separated list of key-value pairs'
44
+
45
+ def __init__(self, dictionary: Optional[Dict[str, Any]] = None):
46
+ self.dictionary = dictionary if dictionary is not None else {}
47
+
48
+ @classmethod
49
+ def create(cls, string: str, allowed_keys: Set[str]):
50
+ """
51
+ Parses the options that are provided via a given string that is formatted according to the following syntax:
52
+ "[key1=value1,key2=value2]". If the given string is malformed, a `ValueError` will be raised.
53
+
54
+ :param string: The string to be parsed
55
+ :param allowed_keys: A set that contains all valid keys
56
+ :return: An object of type `Options` that stores the key-value pairs that have been parsed from
57
+ the given string
58
+ """
59
+ options = cls()
60
+
61
+ if string:
62
+ if not string.startswith('{'):
63
+ raise ValueError(Options.ERROR_MESSAGE_INVALID_SYNTAX + '. Must start with "{", but is "' + string
64
+ + '"')
65
+ if not string.endswith('}'):
66
+ raise ValueError(Options.ERROR_MESSAGE_INVALID_SYNTAX + '. Must end with "}", but is "' + string + '"')
67
+
68
+ string = string[1:-1]
69
+
70
+ if string:
71
+ for option_index, option in enumerate(string.split(',')):
72
+ if option:
73
+ parts = option.split('=')
74
+
75
+ if len(parts) != 2:
76
+ raise ValueError(Options.ERROR_MESSAGE_INVALID_SYNTAX + '. '
77
+ + Options.ERROR_MESSAGE_INVALID_OPTION + ', but got element "' + option
78
+ + '" at index ' + str(option_index))
79
+
80
+ key = parts[0]
81
+
82
+ if len(key) == 0:
83
+ raise ValueError(Options.ERROR_MESSAGE_INVALID_SYNTAX + '. '
84
+ + Options.ERROR_MESSAGE_INVALID_OPTION
85
+ + ', but key is missing from element "' + option + '" at index '
86
+ + str(option_index))
87
+
88
+ if key not in allowed_keys:
89
+ raise ValueError('Key must be one of ' + format_set(allowed_keys) + ', but got key "' + key
90
+ + '" at index ' + str(option_index))
91
+
92
+ value = parts[1]
93
+
94
+ if len(value) == 0:
95
+ raise ValueError(Options.ERROR_MESSAGE_INVALID_SYNTAX + '. '
96
+ + Options.ERROR_MESSAGE_INVALID_OPTION
97
+ + ', but value is missing from element "' + option + '" at index '
98
+ + str(option_index))
99
+
100
+ options.dictionary[key] = value
101
+
102
+ return options
103
+
104
+ def get_string(self, key: str, default_value: Optional[str] = None) -> Optional[str]:
105
+ """
106
+ Returns a string that corresponds to a specific key.
107
+
108
+ :param key: The key
109
+ :param default_value: The default value to be returned, if no value is associated with the given key
110
+ :return: The value that is associated with the given key or the given default value
111
+ """
112
+ if key in self.dictionary:
113
+ return str(self.dictionary[key])
114
+
115
+ return default_value
116
+
117
+ def get_bool(self, key: str, default_value: bool) -> bool:
118
+ """
119
+ Returns a boolean that corresponds to a specific key.
120
+
121
+ :param key: The key
122
+ :param default_value: The default value to be returned, if no value is associated with the given key
123
+ :return: The value that is associated with the given key or the given default value
124
+ """
125
+ if key in self.dictionary:
126
+ value = str(self.dictionary[key])
127
+ return BooleanOption.parse(value)
128
+
129
+ return default_value
130
+
131
+ def get_int(self, key: str, default_value: int) -> int:
132
+ """
133
+ Returns an integer that corresponds to a specific key.
134
+
135
+ :param key: The key
136
+ :param default_value: The default value to be returned, if no value is associated with the given key
137
+ :return: The value that is associated with the given key or the given default value
138
+ """
139
+ if key in self.dictionary:
140
+ value = self.dictionary[key]
141
+
142
+ try:
143
+ value = int(value)
144
+ except ValueError as error:
145
+ raise ValueError('Value for key "' + key + '" is expected to be an integer, but is "' + str(value)
146
+ + '"') from error
147
+
148
+ return value
149
+
150
+ return default_value
151
+
152
+ def get_float(self, key: str, default_value: float) -> float:
153
+ """
154
+ Returns a float that corresponds to a specific key.
155
+
156
+ :param key: The key
157
+ :param default_value: The default value to be returned, if no value is associated with the given key
158
+ :return: The value that is associated with the given key or the given default value
159
+ """
160
+ if key in self.dictionary:
161
+ value = self.dictionary[key]
162
+
163
+ try:
164
+ value = float(value)
165
+ except ValueError as error:
166
+ raise ValueError('Value for key "' + key + '" is expected to be a float, but is "' + str(value)
167
+ + '"') from error
168
+
169
+ return value
170
+
171
+ return default_value
172
+
173
+
174
+ def parse_enum(parameter_name: str,
175
+ value: Optional[str],
176
+ enum: EnumType,
177
+ default: Optional[Enum] = None) -> Optional[EnumType]:
178
+ """
179
+ Parses and returns an enum value. If the given value is invalid, a `ValueError` is raised.
180
+
181
+ :param parameter_name: The name of the parameter
182
+ :param value: The value to be parsed
183
+ :param enum: The enum
184
+ :param default: The default value to be returned if `value` is None
185
+ :return: The value that has been parsed or `default`, if the given value is None
186
+ """
187
+ if value:
188
+ for enum_value in enum:
189
+ expected_value = enum_value.value if isinstance(enum_value.value, str) else enum_value.name.lower()
190
+
191
+ if expected_value == value:
192
+ return enum_value
193
+
194
+ raise ValueError('Invalid value given for parameter "' + parameter_name + '": Must be one of '
195
+ + format_enum_values(enum) + ', but is "' + str(value) + '"')
196
+
197
+ return default
198
+
199
+
200
+ def parse_param(parameter_name: str, value: str, allowed_values: Set[str]) -> str:
201
+ """
202
+ Parses and returns a parameter value. If the given value is invalid, a `ValueError` is raised.
203
+
204
+ :param parameter_name: The name of the parameter
205
+ :param value: The value to be parsed
206
+ :param allowed_values: A set that contains all valid values
207
+ :return: The value that has been parsed
208
+ """
209
+ if value in allowed_values:
210
+ return value
211
+
212
+ raise ValueError('Invalid value given for parameter "' + parameter_name + '": Must be one of '
213
+ + format_set(allowed_values) + ', but is "' + str(value) + '"')
214
+
215
+
216
+ def parse_param_and_options(parameter_name: str, value: str,
217
+ allowed_values_and_options: Dict[str, Set[str]]) -> Tuple[str, Options]:
218
+ """
219
+ Parses and returns a parameter value, as well as additional `Options` that may be associated with it. If the given
220
+ value is invalid, a `ValueError` is raised.
221
+
222
+ :param parameter_name: The name of the parameter
223
+ :param value: The value to be parsed
224
+ :param allowed_values_and_options: A dictionary that contains all valid values, as well as their options
225
+ :return: A tuple that contains the value that has been parsed, as well as additional
226
+ `Options`.
227
+ """
228
+ for allowed_value, allowed_options in allowed_values_and_options.items():
229
+ if value.startswith(allowed_value):
230
+ suffix = value[len(allowed_value):].strip()
231
+
232
+ if suffix:
233
+ try:
234
+ return allowed_value, Options.create(suffix, allowed_options)
235
+ except ValueError as error:
236
+ raise ValueError('Invalid options specified for parameter "' + parameter_name + '" with value "'
237
+ + allowed_value + '": ' + str(error)) from error
238
+
239
+ return allowed_value, Options()
240
+
241
+ raise ValueError('Invalid value given for parameter "' + parameter_name + '": Must be one of '
242
+ + format_set(allowed_values_and_options.keys()) + ', but is "' + value + '"')
@@ -0,0 +1,81 @@
1
+ """
2
+ Author: Michael Rapp (michael.rapp.ml@gmail.com)
3
+
4
+ Provides utility functions for validation.
5
+ """
6
+
7
+
8
+ def assert_greater(name: str, value, threshold):
9
+ """
10
+ Raises a `ValueError` if a given value is not greater than a specific threshold.
11
+
12
+ :param name: The name of the parameter, the value corresponds to
13
+ :param value: The value
14
+ :param threshold: The threshold
15
+ """
16
+ if value <= threshold:
17
+ raise ValueError('Invalid value given for parameter "' + name + '": Must be greater than ' + str(threshold)
18
+ + ', but is ' + str(value))
19
+
20
+
21
+ def assert_greater_or_equal(name: str, value, threshold):
22
+ """
23
+ Raises a `ValueError` if a given value is not greater or equal to a specific threshold.
24
+
25
+ :param name: The name of the parameter, the value corresponds to
26
+ :param value: The value
27
+ :param threshold: The threshold
28
+ """
29
+ if value < threshold:
30
+ raise ValueError('Invalid value given for parameter "' + name + '": Must be greater or equal to '
31
+ + str(threshold) + ', but is ' + str(value))
32
+
33
+
34
+ def assert_less(name: str, value, threshold):
35
+ """
36
+ Raises a `ValueError` if a given value is not less than a specific threshold.
37
+
38
+ :param name: The name of the parameter, the value corresponds to
39
+ :param value: The value
40
+ :param threshold: The threshold
41
+ """
42
+ if value >= threshold:
43
+ raise ValueError('Invalid value given for parameter "' + name + '": Must be less than ' + str(threshold)
44
+ + ', but is ' + str(value))
45
+
46
+
47
+ def assert_less_or_equal(name: str, value, threshold):
48
+ """
49
+ Raises a `ValueError` if a given value is not less or equal to a specific threshold.
50
+
51
+ :param name: The name of the parameter, the value corresponds to
52
+ :param value: The value
53
+ :param threshold: The threshold
54
+ """
55
+ if value > threshold:
56
+ raise ValueError('Invalid value given for parameter "' + name + '": Must be less or equal to ' + str(threshold)
57
+ + ', but is ' + str(value))
58
+
59
+
60
+ def assert_multiple(name: str, value, other):
61
+ """
62
+ Raises a `ValueError` if a given value is not a multiple of another value.
63
+
64
+ :param name: The name of the parameter, the value corresponds to
65
+ :param value: The value that should be a multiple of `other`
66
+ :param other: The other value
67
+ """
68
+ if value % other != 0:
69
+ raise ValueError('Invalid value given for parameter "' + name + '": Must be a multiple of ' + str(other)
70
+ + ', but is ' + str(value))
71
+
72
+
73
+ def assert_not_none(name: str, value):
74
+ """
75
+ Raises a `ValueError` if a given value is None.
76
+
77
+ :param name: The name of the parameter, the value corresponds to
78
+ :param value: The value
79
+ """
80
+ if value is None:
81
+ raise ValueError('Invalid value given for parameter "' + name + '": Must not be None')
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlrl-util
3
+ Version: 0.12.0
4
+ Summary: Provides common utilities used by the mlrl-* packages.
5
+ Author-email: Michael Rapp <michael.rapp.ml@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/mrapp-ke/MLRL-Boomer
8
+ Project-URL: source, https://github.com/mrapp-ke/MLRL-Boomer.git
9
+ Project-URL: download, https://github.com/mrapp-ke/MLRL-Boomer/releases
10
+ Project-URL: changelog, https://raw.githubusercontent.com/mrapp-ke/MLRL-Boomer/refs/heads/main/CHANGELOG.md
11
+ Project-URL: documentation, https://mlrl-boomer.readthedocs.io/en/latest
12
+ Project-URL: issues, https://github.com/mrapp-ke/MLRL-Boomer/issues
13
+ Keywords: utilities
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: numpy<2.4,>=2.3
22
+ Requires-Dist: scipy<1.17,>=1.16
23
+
24
+ # "MLRL-Util"
25
+
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI version](https://badge.fury.io/py/mlrl-util.svg)](https://badge.fury.io/py/mlrl-util) [![Documentation Status](https://readthedocs.org/projects/mlrl-boomer/badge/?version=latest)](https://mlrl-boomer.readthedocs.io/en/latest/?badge=latest)
27
+
28
+ **:link: Important links:** [API Reference](https://mlrl-boomer.readthedocs.io/en/latest/developer_guide/api/python/util/mlrl.util.html) | [Issue Tracker](https://github.com/mrapp-ke/MLRL-Boomer/issues) | [Changelog](https://mlrl-boomer.readthedocs.io/en/latest/misc/CHANGELOG.html) | [Contributors](https://mlrl-boomer.readthedocs.io/en/latest/misc/CONTRIBUTORS.html) | [Code of Conduct](https://mlrl-boomer.readthedocs.io/en/latest/misc/CODE_OF_CONDUCT.html) | [License](https://mlrl-boomer.readthedocs.io/en/latest/misc/LICENSE.html)
29
+
30
+ This software package provides common utilities used by the packages [mlrl-common](https://pypi.org/project/mlrl-common/) and [mlrl-testbed](https://pypi.org/project/mlrl-testbed/). It is not of much use on its own.
31
+
32
+ ## :scroll: License
33
+
34
+ This project is open source software licensed under the terms of the [MIT license](https://mlrl-boomer.readthedocs.io/en/latest/misc/LICENSE.html). We welcome contributions to the project to enhance its functionality and make it more accessible to a broader audience. A frequently updated list of contributors is available [here](https://mlrl-boomer.readthedocs.io/en/latest/misc/CONTRIBUTORS.html).
35
+
36
+ All contributions to the project and discussions on the [issue tracker](https://github.com/mrapp-ke/MLRL-Boomer/issues) are expected to follow the [code of conduct](https://mlrl-boomer.readthedocs.io/en/latest/misc/CODE_OF_CONDUCT.html).
@@ -0,0 +1,11 @@
1
+ mlrl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mlrl/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ mlrl/util/arrays.py,sha256=yGhiDCBfdgmX7SpaZtWgnLd3929o9qTFpGGpV2UWXhQ,7450
4
+ mlrl/util/cli.py,sha256=lIc5oJ6-VQrE6us7qA_7amkrL9Dwcd4TnbFTNOgs_e4,13777
5
+ mlrl/util/format.py,sha256=inBiNMYPEke3BlItHicZRC_NYb2r_jgQ1lF0SmxRyn8,1519
6
+ mlrl/util/options.py,sha256=mWTWGQZgw1AyKNJg4BFMpb32v9Q4fVfuv7i1BMC96yc,10303
7
+ mlrl/util/validation.py,sha256=mkYKbwrrb5zIig0sIX3JnT_baWPN1WvPwe9R8rCWyWA,2962
8
+ mlrl_util-0.12.0.dist-info/METADATA,sha256=HlCWNH6tNraQWoCC2LLv4j-tgf35fDO9Y3Mr82yXiJg,2856
9
+ mlrl_util-0.12.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
10
+ mlrl_util-0.12.0.dist-info/top_level.txt,sha256=o9x5YHV4Syk8twE0D3Or7SqyG69Ryp7e-HEYe8aB4j4,5
11
+ mlrl_util-0.12.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mlrl