algomancy-utils 0.3.12__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.
@@ -0,0 +1,13 @@
1
+ from .logger import Message, MessageStatus, Logger
2
+ from .unit import Unit, Quantity, BaseMeasurement, Measurement, QUANTITIES
3
+
4
+ __all__ = [
5
+ "Message",
6
+ "MessageStatus",
7
+ "Logger",
8
+ "Unit",
9
+ "Quantity",
10
+ "BaseMeasurement",
11
+ "Measurement",
12
+ "QUANTITIES",
13
+ ]
@@ -0,0 +1,479 @@
1
+ from abc import ABC, abstractmethod, ABCMeta
2
+ from enum import StrEnum
3
+ from typing import Any, Dict, TypeVar
4
+ from datetime import datetime
5
+
6
+
7
+ class ParameterError(Exception):
8
+ def __init__(self, message: str) -> None:
9
+ self.message = message
10
+ super().__init__(self.message)
11
+
12
+
13
+ class ParameterType(StrEnum):
14
+ STRING = "string"
15
+ INTEGER = "integer"
16
+ FLOAT = "float"
17
+ BOOLEAN = "boolean"
18
+ ENUM = "enum"
19
+ MULTI_ENUM = "multi_enum"
20
+ TIME = "time"
21
+ INTERVAL = "interval"
22
+
23
+
24
+ class TypedParameter(ABC):
25
+ def __init__(
26
+ self, name: str, parameter_type: ParameterType, required: bool
27
+ ) -> None:
28
+ self.name = name
29
+ self.parameter_type = parameter_type
30
+ self.required = required
31
+ self._value = None
32
+ self.is_list = False
33
+
34
+ @property
35
+ @abstractmethod
36
+ def value(self) -> Any:
37
+ pass
38
+
39
+ @abstractmethod
40
+ def _validate(self, value) -> None:
41
+ pass
42
+
43
+ def _check_required(self, value) -> None:
44
+ if self.required and value is None:
45
+ raise ParameterError(f"Parameter '{self.name}' is required.")
46
+
47
+ def _set_value(self, value: Any) -> None:
48
+ self._value = value
49
+
50
+ def set_validated_value(self, value: Any) -> None:
51
+ self._check_required(value)
52
+ self._validate(value)
53
+ self._set_value(value)
54
+
55
+ def __str__(self):
56
+ return self._serialize()
57
+
58
+
59
+ class StringParameter(TypedParameter):
60
+ def __init__(
61
+ self,
62
+ name: str,
63
+ value: str = None,
64
+ required: bool = True,
65
+ default: str = "default",
66
+ ) -> None:
67
+ super().__init__(name, ParameterType.STRING, required)
68
+ self.default = default
69
+ if value is not None:
70
+ self.set_validated_value(value)
71
+
72
+ def _validate(self, value):
73
+ if not isinstance(value, str):
74
+ raise ParameterError(f"Parameter '{self.name}' must be a string.")
75
+
76
+ def __str__(self) -> str:
77
+ return f"{self.name}: {self.value}"
78
+
79
+ @property
80
+ def value(self) -> str:
81
+ if self._value is None:
82
+ return self.default
83
+ return self._value
84
+
85
+
86
+ class EnumParameter(TypedParameter):
87
+ def __init__(
88
+ self, name: str, choices: list[str], value: str = None, required: bool = True
89
+ ) -> None:
90
+ super().__init__(name, ParameterType.ENUM, required)
91
+ assert len(choices) > 0, "Parameter must have at least one choice."
92
+ self.choices = choices
93
+ if value is not None:
94
+ self.set_validated_value(value)
95
+
96
+ def __str__(self) -> str:
97
+ return f"{self.name}: {self.value}"
98
+
99
+ def _validate(self, value: str):
100
+ if not isinstance(value, str):
101
+ raise ParameterError(f"Parameter '{self.name}' must be a string.")
102
+ if value not in self.choices:
103
+ raise ParameterError(
104
+ f"Parameter '{self.name}' must be one of {self.choices}."
105
+ )
106
+
107
+ @property
108
+ def value(self) -> str:
109
+ if self._value is None:
110
+ return self.choices[0]
111
+ return self._value
112
+
113
+
114
+ class MultiEnumParameter(TypedParameter):
115
+ def __init__(
116
+ self,
117
+ name: str,
118
+ choices: list[str],
119
+ value: list[str] = None,
120
+ required: bool = True,
121
+ ) -> None:
122
+ super().__init__(name, ParameterType.MULTI_ENUM, required)
123
+ assert len(choices) > 0, "Parameter must have at least one choice."
124
+ self.choices = choices
125
+ if value is not None:
126
+ self.set_validated_value(value)
127
+
128
+ def __str__(self) -> str:
129
+ return f"{self.name}: {self.value}"
130
+
131
+ def _validate(self, value_lst: list[str]):
132
+ if not isinstance(value_lst, list):
133
+ raise ParameterError(f"Parameter '{self.name}' must be a list.")
134
+ for value in value_lst:
135
+ if not isinstance(value, str):
136
+ raise ParameterError(f"Parameter '{self.name}' must be a string.")
137
+ if value not in self.choices:
138
+ raise ParameterError(
139
+ f"Parameter '{self.name}' must be one of {self.choices}."
140
+ )
141
+
142
+ @property
143
+ def value(self) -> list[str]:
144
+ if self._value is None:
145
+ return [self.choices[0]]
146
+ return self._value
147
+
148
+
149
+ class NumericParameter(TypedParameter, ABC):
150
+ def __init__(
151
+ self,
152
+ name: str,
153
+ parameter_type: ParameterType,
154
+ required: bool,
155
+ default,
156
+ minvalue: float = None,
157
+ maxvalue: float = None,
158
+ value: float = None,
159
+ ) -> None:
160
+ super().__init__(name, parameter_type, required)
161
+ self.default = default
162
+ assert parameter_type in [
163
+ ParameterType.INTEGER,
164
+ ParameterType.FLOAT,
165
+ ], "Numeric parameter must be of type integer or float."
166
+ self.min = minvalue
167
+ self.max = maxvalue
168
+
169
+ if minvalue is not None and maxvalue is not None:
170
+ assert minvalue <= maxvalue, (
171
+ "Minimum value must be less than or equal to maximum value."
172
+ )
173
+
174
+ if value is not None:
175
+ self.set_validated_value(value)
176
+
177
+ def _validate(self, value) -> None:
178
+ if self.parameter_type == ParameterType.FLOAT and not (
179
+ isinstance(value, float) or isinstance(value, int)
180
+ ):
181
+ raise ParameterError(f"Parameter '{self.name}' must be a float.")
182
+ elif self.parameter_type == ParameterType.INTEGER and not isinstance(
183
+ value, int
184
+ ):
185
+ raise ParameterError(f"Parameter '{self.name}' must be an integer.")
186
+ if self.min is not None and value < self.min:
187
+ raise ParameterError(
188
+ f"Parameter '{self.name}' must be greater than or equal to {self.min}."
189
+ )
190
+ if self.max is not None and value > self.max:
191
+ raise ParameterError(
192
+ f"Parameter '{self.name}' must be less than or equal to {self.max}."
193
+ )
194
+
195
+
196
+ class FloatParameter(NumericParameter):
197
+ EPSILON = 1e-6
198
+
199
+ def __init__(
200
+ self,
201
+ name: str,
202
+ minvalue: float = None,
203
+ maxvalue: float = None,
204
+ value: float = None,
205
+ required: bool = True,
206
+ default: float = 1.0,
207
+ ) -> None:
208
+ super().__init__(
209
+ name, ParameterType.FLOAT, required, default, minvalue, maxvalue, value
210
+ )
211
+
212
+ def __str__(self) -> str:
213
+ return f"{self.name}: {self.value:.2f}"
214
+
215
+ @property
216
+ def value(self) -> float:
217
+ if self._value is None:
218
+ return self.default
219
+ return self._value
220
+
221
+
222
+ class IntegerParameter(NumericParameter):
223
+ def __init__(
224
+ self,
225
+ name: str,
226
+ minvalue: int = None,
227
+ maxvalue: int = None,
228
+ value: int = None,
229
+ required: bool = True,
230
+ default: int = 1,
231
+ ) -> None:
232
+ super().__init__(
233
+ name, ParameterType.INTEGER, required, default, minvalue, maxvalue, value
234
+ )
235
+
236
+ def __str__(self) -> str:
237
+ return f"{self.name}: {self.value}"
238
+
239
+ @property
240
+ def value(self) -> int:
241
+ if self._value is None:
242
+ return self.default
243
+ return self._value
244
+
245
+
246
+ class BooleanParameter(TypedParameter):
247
+ def __init__(
248
+ self,
249
+ name: str,
250
+ value: bool = None,
251
+ required: bool = True,
252
+ default: bool = False,
253
+ ) -> None:
254
+ super().__init__(name, ParameterType.BOOLEAN, required)
255
+ self.default = default
256
+ if value is not None:
257
+ self.set_validated_value(value)
258
+
259
+ def __str__(self) -> str:
260
+ return f"{self.name}: {self.value}"
261
+
262
+ def serialize(self) -> str:
263
+ return str(self.value)
264
+
265
+ def _validate(self, value):
266
+ if not isinstance(value, bool):
267
+ raise ParameterError(f"Parameter '{self.name}' must be a boolean.")
268
+
269
+ @property
270
+ def value(self) -> bool:
271
+ if self._value is None:
272
+ return self.default
273
+ return self._value
274
+
275
+
276
+ class TimeParameter(TypedParameter):
277
+ def __init__(
278
+ self,
279
+ name: str,
280
+ value: datetime | None = None,
281
+ required: bool = True,
282
+ default: datetime | None = None,
283
+ ) -> None:
284
+ super().__init__(name, ParameterType.TIME, required)
285
+ self._default = default
286
+ if value is not None:
287
+ self.set_validated_value(value)
288
+
289
+ def __str__(self) -> str:
290
+ return f"{self.name}: {self.value.isoformat()}"
291
+
292
+ def _validate(self, value) -> None:
293
+ if not isinstance(value, datetime):
294
+ raise ParameterError(f"Parameter '{self.name}' must be a datetime.")
295
+
296
+ @property
297
+ def default(self) -> datetime:
298
+ if self._default is None:
299
+ return datetime.today()
300
+ else:
301
+ return self._default
302
+
303
+ @property
304
+ def value(self) -> datetime:
305
+ if self._value is None:
306
+ return self._default
307
+ return self._value
308
+
309
+
310
+ class IntervalParameter(TypedParameter):
311
+ def __init__(
312
+ self,
313
+ name: str,
314
+ value: list[datetime] | tuple[datetime, datetime] | None = None,
315
+ required: bool = True,
316
+ default: tuple[datetime, datetime] | None = None,
317
+ ) -> None:
318
+ super().__init__(name, ParameterType.INTERVAL, required)
319
+ self.default = default
320
+ if value is not None:
321
+ self.set_validated_value(value)
322
+
323
+ def __str__(self) -> str:
324
+ s, e = self.value
325
+ return f"{self.name}: [{s.isoformat()}, {e.isoformat()}]"
326
+
327
+ def _validate(self, value) -> None:
328
+ if not (isinstance(value, (list, tuple)) and len(value) == 2):
329
+ raise ParameterError(
330
+ f"Parameter '{self.name}' must be a list/tuple of two datetimes."
331
+ )
332
+ start, end = value[0], value[1]
333
+ if not isinstance(start, datetime) or not isinstance(end, datetime):
334
+ raise ParameterError(
335
+ f"Parameter '{self.name}' must contain datetime values."
336
+ )
337
+ if end < start:
338
+ raise ParameterError(
339
+ f"Parameter '{self.name}' interval end must be greater than or equal to start."
340
+ )
341
+
342
+ @property
343
+ def default_start(self) -> datetime:
344
+ if self.default:
345
+ return self.default[0]
346
+ else:
347
+ now = datetime.today()
348
+ return datetime(now.year, 1, 1)
349
+
350
+ @property
351
+ def default_end(self) -> datetime:
352
+ if self.default:
353
+ return self.default[1]
354
+ else:
355
+ now = datetime.today()
356
+ return datetime(now.year, 12, 31)
357
+
358
+ @property
359
+ def value(self) -> tuple[datetime, datetime]:
360
+ if self._value is None:
361
+ return self.default_start, self.default_end
362
+ # Normalize internal storage to tuple
363
+ if isinstance(self._value, list):
364
+ return (self._value[0], self._value[1])
365
+ return self._value
366
+
367
+
368
+ class PostInitMeta(ABCMeta):
369
+ def __call__(cls, *args, **kwargs):
370
+ instance = super().__call__(*args, **kwargs)
371
+ post_init = getattr(instance, "_post_init", None)
372
+ if callable(post_init):
373
+ post_init()
374
+ return instance
375
+
376
+
377
+ class BaseParameterSet(ABC, metaclass=PostInitMeta):
378
+ def __init__(self, name: str) -> None:
379
+ self.name: str = name
380
+ self._parameters: Dict[str, TypedParameter] = {}
381
+ self._is_locked = False
382
+
383
+ def __str__(self):
384
+ return str(self.serialize())
385
+
386
+ def __dict__(self):
387
+ return {p.name: p.value for p in self._parameters.values()}
388
+
389
+ def __getitem__(self, key):
390
+ return self._parameters[key].value
391
+
392
+ def _post_init(self):
393
+ """is called directly after the __init__ method in PostInitMeta classes"""
394
+ self._is_locked = True
395
+
396
+ def copy(self):
397
+ return self.deserialize(self.serialize())
398
+
399
+ @abstractmethod
400
+ def validate(self):
401
+ """Validates parameters, must be implemented in subclass."""
402
+ pass
403
+
404
+ def get_parameters(self) -> Dict[str, TypedParameter]:
405
+ return self._parameters
406
+
407
+ def contains(self, param_name: str) -> bool:
408
+ return param_name in self._parameters
409
+
410
+ def serialize(self):
411
+ import json
412
+
413
+ dct = {"name": self.name, "parameters": self.get_values()}
414
+ return json.dumps(dct)
415
+
416
+ @classmethod
417
+ def deserialize(cls, json_str: str):
418
+ import json
419
+
420
+ data = json.loads(json_str)
421
+ rv = cls()
422
+
423
+ # apply the stored values to the newly created instance.
424
+ if "parameters" in data:
425
+ rv.set_values(data["parameters"])
426
+
427
+ return rv
428
+
429
+ def add_parameters(self, parameters: list[TypedParameter]):
430
+ if self._is_locked:
431
+ raise ParameterError("Cannot add parameter after initialization.")
432
+ for parameter in parameters:
433
+ self._parameters[parameter.name] = parameter
434
+
435
+ def set_values(self, values: dict[str, Any]):
436
+ self.repair_param_dict(values)
437
+ for name, value in values.items():
438
+ if name in self._parameters:
439
+ self._parameters[name].set_validated_value(value)
440
+ else:
441
+ raise ParameterError(f"Parameter '{name}' not found.")
442
+
443
+ def set_validated_values(self, values: dict[str, Any]) -> None:
444
+ self.set_values(values)
445
+ self.validate()
446
+
447
+ def get_values(self) -> dict[str, Any]:
448
+ return {key: p.value for key, p in self._parameters.items()}
449
+
450
+ def has_inputs(self) -> bool:
451
+ return len(self._parameters) > 0
452
+
453
+ def get_boolean_parameter_names(self) -> list[str]:
454
+ return [
455
+ p.name for p in self._parameters.values() if type(p) is BooleanParameter
456
+ ]
457
+
458
+ def repair_param_dict(self, dct):
459
+ # retrieve the boolean variables
460
+ boolean_keys = self.get_boolean_parameter_names()
461
+
462
+ # set value appropriately
463
+ for key in boolean_keys:
464
+ if key in dct:
465
+ if dct[key]:
466
+ dct[key] = True
467
+ else:
468
+ dct[key] = False
469
+
470
+
471
+ BASE_PARAMS_BOUND = TypeVar("BASE_PARAMS_BOUND", bound=BaseParameterSet)
472
+
473
+
474
+ class EmptyParameters(BaseParameterSet):
475
+ def __init__(self) -> None:
476
+ super().__init__(name="empty")
477
+
478
+ def validate(self):
479
+ pass
@@ -0,0 +1,102 @@
1
+ import datetime
2
+ import traceback
3
+ from enum import StrEnum, auto
4
+ from typing import List, Optional
5
+
6
+
7
+ class MessageStatus(StrEnum):
8
+ INFO = auto()
9
+ SUCCESS = auto()
10
+ WARNING = auto()
11
+ ERROR = auto()
12
+
13
+
14
+ class Message:
15
+ def __init__(
16
+ self, message: str, status: MessageStatus = MessageStatus.INFO
17
+ ) -> None:
18
+ self.message = message
19
+ self.status = status
20
+ self.timestamp = datetime.datetime.now()
21
+
22
+ def __str__(self):
23
+ return f"[{self.timestamp.isoformat()}] {self.status.name.rjust(7)}: {self.message}"
24
+
25
+ def print(self):
26
+ RESET = "\033[0m"
27
+ GREEN = "\033[92m"
28
+ ORANGE = "\033[93m"
29
+ RED = "\033[91m"
30
+
31
+ match self.status:
32
+ case MessageStatus.INFO:
33
+ print(f"{RESET}{self.__str__()}")
34
+ case MessageStatus.SUCCESS:
35
+ print(f"{GREEN}{self.__str__()}")
36
+ case MessageStatus.WARNING:
37
+ print(f"{ORANGE}{self.__str__()}")
38
+ case MessageStatus.ERROR:
39
+ print(f"{RED}{self.__str__()}")
40
+ case _:
41
+ print(f"{self.__str__()}")
42
+
43
+
44
+ class Logger:
45
+ """
46
+ Eenvoudige logger die berichten opslaat met status en timestamp.
47
+ """
48
+
49
+ def __init__(self) -> None:
50
+ self._logs: List[Message] = []
51
+ self.latest_log: Optional[Message] = None
52
+ self._print_to_console = True
53
+
54
+ def toggle_print_to_console(self, value: bool = None) -> None:
55
+ if not value:
56
+ value = not self._print_to_console
57
+
58
+ self._print_to_console = value
59
+
60
+ def log(self, message: str, status: MessageStatus = MessageStatus.INFO) -> None:
61
+ """
62
+ Voeg een logbericht toe.
63
+
64
+ :param message: Het bericht dat gelogd wordt
65
+ :param status: Status/type van het bericht (bijv. 'info', 'success', 'warning', 'error')
66
+ """
67
+ self._logs.append(Message(message=message, status=status))
68
+ self.latest_log = self._logs[-1]
69
+
70
+ if self._print_to_console:
71
+ self.latest_log.print()
72
+
73
+ def success(self, message: str):
74
+ self.log(message, status=MessageStatus.SUCCESS)
75
+
76
+ def warning(self, message: str):
77
+ self.log(message, status=MessageStatus.WARNING)
78
+
79
+ def error(self, message: str):
80
+ self.log(message, status=MessageStatus.ERROR)
81
+
82
+ def get_logs(self, status_filter: Optional[MessageStatus] = None) -> List[Message]:
83
+ """
84
+ Haal alle logs op, eventueel gefilterd op status.
85
+
86
+ :param status_filter: Optioneel, filter op status (bijv. 'info')
87
+ :return: Lijst van logs (dicts)
88
+ """
89
+ if status_filter:
90
+ return [log for log in self._logs if log.status == status_filter]
91
+ return list(self._logs)
92
+
93
+ def clear(self) -> None:
94
+ """
95
+ Verwijdert alle opgeslagen logs.
96
+ """
97
+ self._logs.clear()
98
+
99
+ def log_traceback(self, e: Exception):
100
+ self.error(f"An error occurred: {e.__class__.__name__}: {e}")
101
+ for msg in traceback.format_tb(e.__traceback__):
102
+ self.error(msg)
File without changes