pydasa 0.4.7__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.
- pydasa/__init__.py +103 -0
- pydasa/_version.py +6 -0
- pydasa/analysis/__init__.py +0 -0
- pydasa/analysis/scenario.py +584 -0
- pydasa/analysis/simulation.py +1158 -0
- pydasa/context/__init__.py +0 -0
- pydasa/context/conversion.py +11 -0
- pydasa/context/system.py +17 -0
- pydasa/context/units.py +15 -0
- pydasa/core/__init__.py +15 -0
- pydasa/core/basic.py +287 -0
- pydasa/core/cfg/default.json +136 -0
- pydasa/core/constants.py +27 -0
- pydasa/core/io.py +102 -0
- pydasa/core/setup.py +269 -0
- pydasa/dimensional/__init__.py +0 -0
- pydasa/dimensional/buckingham.py +728 -0
- pydasa/dimensional/fundamental.py +146 -0
- pydasa/dimensional/model.py +1077 -0
- pydasa/dimensional/vaschy.py +633 -0
- pydasa/elements/__init__.py +19 -0
- pydasa/elements/parameter.py +218 -0
- pydasa/elements/specs/__init__.py +22 -0
- pydasa/elements/specs/conceptual.py +161 -0
- pydasa/elements/specs/numerical.py +469 -0
- pydasa/elements/specs/statistical.py +229 -0
- pydasa/elements/specs/symbolic.py +394 -0
- pydasa/serialization/__init__.py +27 -0
- pydasa/serialization/parser.py +133 -0
- pydasa/structs/__init__.py +0 -0
- pydasa/structs/lists/__init__.py +0 -0
- pydasa/structs/lists/arlt.py +578 -0
- pydasa/structs/lists/dllt.py +18 -0
- pydasa/structs/lists/ndlt.py +262 -0
- pydasa/structs/lists/sllt.py +746 -0
- pydasa/structs/tables/__init__.py +0 -0
- pydasa/structs/tables/htme.py +182 -0
- pydasa/structs/tables/scht.py +774 -0
- pydasa/structs/tools/__init__.py +0 -0
- pydasa/structs/tools/hashing.py +53 -0
- pydasa/structs/tools/math.py +149 -0
- pydasa/structs/tools/memory.py +54 -0
- pydasa/structs/types/__init__.py +0 -0
- pydasa/structs/types/functions.py +131 -0
- pydasa/structs/types/generics.py +54 -0
- pydasa/validations/__init__.py +0 -0
- pydasa/validations/decorators.py +510 -0
- pydasa/validations/error.py +100 -0
- pydasa/validations/patterns.py +32 -0
- pydasa/workflows/__init__.py +1 -0
- pydasa/workflows/influence.py +497 -0
- pydasa/workflows/phenomena.py +529 -0
- pydasa/workflows/practical.py +765 -0
- pydasa-0.4.7.dist-info/METADATA +320 -0
- pydasa-0.4.7.dist-info/RECORD +58 -0
- pydasa-0.4.7.dist-info/WHEEL +5 -0
- pydasa-0.4.7.dist-info/licenses/LICENSE +674 -0
- pydasa-0.4.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Module decorators.py
|
|
4
|
+
===========================================
|
|
5
|
+
|
|
6
|
+
Decorator-based validation system for PyDASA attributes.
|
|
7
|
+
|
|
8
|
+
This module provides reusable decorators for property setters, eliminating
|
|
9
|
+
the need for separate validation methods and reducing boilerplate code.
|
|
10
|
+
|
|
11
|
+
Functions:
|
|
12
|
+
**validate_type**: Validates value against expected type(s)
|
|
13
|
+
**validate_emptiness**: Ensures string values are non-empty
|
|
14
|
+
**validate_choices**: Validates value is in allowed set of choices
|
|
15
|
+
**validate_range**: Validates numeric value is within specified range
|
|
16
|
+
**validate_index**: Validates integer values with negativity control
|
|
17
|
+
**validate_pattern**: Validates string matches regex pattern(s) or is alphanumeric
|
|
18
|
+
**validate_custom**: Custom validation logic
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# native python modules
|
|
22
|
+
from functools import wraps
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import Callable, Any, Union, Type, Optional # , Tuple
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_type(*expected_types: type, allow_none: bool = True) -> Callable:
|
|
29
|
+
"""*validate_type()* Decorator to validate argument type against expected type(s).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
*expected_types (type): One or more expected types for validation.
|
|
33
|
+
allow_none (bool, optional): Whether None values are allowed. Defaults to True.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If value is None when allow_none is False.
|
|
37
|
+
ValueError: If value type does not match any of the expected types.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Callable: Decorated function with type validation.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
@property
|
|
44
|
+
def unit(self) -> str:
|
|
45
|
+
return self._unit
|
|
46
|
+
|
|
47
|
+
@unit.setter
|
|
48
|
+
@validate_type(str)
|
|
49
|
+
def unit(self, val: str) -> None:
|
|
50
|
+
self._unit = val
|
|
51
|
+
|
|
52
|
+
# Multiple types
|
|
53
|
+
@value.setter
|
|
54
|
+
@validate_type(int, float)
|
|
55
|
+
def value(self, val: Union[int, float]) -> None:
|
|
56
|
+
self._value = val
|
|
57
|
+
"""
|
|
58
|
+
def decorator(func: Callable) -> Callable:
|
|
59
|
+
@wraps(func)
|
|
60
|
+
def wrapper(self, value: Any) -> Any:
|
|
61
|
+
# if value is None
|
|
62
|
+
if value is None:
|
|
63
|
+
if not allow_none:
|
|
64
|
+
_msg = f"{func.__name__} cannot be None."
|
|
65
|
+
raise ValueError(_msg)
|
|
66
|
+
return func(self, value)
|
|
67
|
+
|
|
68
|
+
# if value type is incorrect
|
|
69
|
+
if not isinstance(value, expected_types):
|
|
70
|
+
type_names = " or ".join(t.__name__ for t in expected_types)
|
|
71
|
+
_msg = f"{func.__name__} must be {type_names}, "
|
|
72
|
+
_msg += f"got {type(value).__name__}."
|
|
73
|
+
raise ValueError(_msg)
|
|
74
|
+
# otherwise, return the original function
|
|
75
|
+
return func(self, value)
|
|
76
|
+
return wrapper # return the wrapper
|
|
77
|
+
return decorator # return the decorator
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_emptiness(strip: bool = True) -> Callable:
|
|
81
|
+
"""*validate_emptiness()* Decorator to ensure values are non-empty.
|
|
82
|
+
|
|
83
|
+
Handles strings, dictionaries, lists, tuples, and other collections.
|
|
84
|
+
For strings, optionally strips whitespace before checking.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
strip (bool, optional): Whether to strip whitespace before checking strings. Defaults to True.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If string is empty/whitespace-only, or if collection has no elements.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Callable: Decorated function with non-empty validation.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
@unit.setter
|
|
97
|
+
@validate_type(str)
|
|
98
|
+
@validate_emptiness()
|
|
99
|
+
def unit(self, val: str) -> None:
|
|
100
|
+
self._unit = val
|
|
101
|
+
|
|
102
|
+
@variables.setter
|
|
103
|
+
@validate_type(dict)
|
|
104
|
+
@validate_emptiness()
|
|
105
|
+
def variables(self, val: dict) -> None:
|
|
106
|
+
self._variables = val
|
|
107
|
+
"""
|
|
108
|
+
def decorator(func: Callable) -> Callable:
|
|
109
|
+
@wraps(func)
|
|
110
|
+
def wrapper(self, value: Any) -> Any:
|
|
111
|
+
# if value is not None, check for emptiness
|
|
112
|
+
if value is not None:
|
|
113
|
+
# Handle strings separately to allow strip functionality
|
|
114
|
+
if isinstance(value, str):
|
|
115
|
+
check_val = value.strip() if strip else value
|
|
116
|
+
if not check_val:
|
|
117
|
+
_msg = f"{func.__name__} must be a non-empty string. "
|
|
118
|
+
_msg += f"Provided: {repr(value)}."
|
|
119
|
+
raise ValueError(_msg)
|
|
120
|
+
# Handle collections (dict, list, tuple, set, etc.)
|
|
121
|
+
elif hasattr(value, '__len__'):
|
|
122
|
+
if len(value) == 0:
|
|
123
|
+
type_name = type(value).__name__
|
|
124
|
+
_msg = f"{func.__name__} must be a non-empty {type_name}. "
|
|
125
|
+
_msg += f"Provided: {repr(value)}."
|
|
126
|
+
raise ValueError(_msg)
|
|
127
|
+
# otherwise, call the original function
|
|
128
|
+
return func(self, value)
|
|
129
|
+
return wrapper # return the wrapper
|
|
130
|
+
return decorator # return the decorator
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_choices(choices: Union[dict, set, list, tuple, Type[Enum]],
|
|
134
|
+
allow_none: bool = False,
|
|
135
|
+
case_sensitive: bool = False) -> Callable:
|
|
136
|
+
"""*validate_choices()* Decorator to validate value is in allowed set of choices.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
choices (Union[dict, set, list, tuple, Type[Enum]]): Dictionary, set, list, tuple, or Enum type of allowed values.
|
|
140
|
+
allow_none (bool, optional): Whether None values are allowed. Defaults to False.
|
|
141
|
+
case_sensitive (bool, optional): Whether string comparison is case-sensitive. Defaults to False.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ValueError: If value is not in the allowed choices.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Callable: Decorated function with choice validation.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
from pydasa.core.setup import Frameworks
|
|
151
|
+
|
|
152
|
+
@fwk.setter
|
|
153
|
+
@validate_type(str)
|
|
154
|
+
@validate_choices(Frameworks.values())
|
|
155
|
+
def fwk(self, val: str) -> None:
|
|
156
|
+
self._fwk = val.upper()
|
|
157
|
+
|
|
158
|
+
# Case-sensitive choices
|
|
159
|
+
@status.setter
|
|
160
|
+
@validate_choices(["Active", "Inactive"], case_sensitive=True)
|
|
161
|
+
def status(self, val: str) -> None:
|
|
162
|
+
self._status = val
|
|
163
|
+
"""
|
|
164
|
+
# Convert choices to set for O(1) lookup - extract enum names when needed
|
|
165
|
+
if isinstance(choices, dict):
|
|
166
|
+
valid_choices = set(choices.keys())
|
|
167
|
+
elif isinstance(choices, type) and issubclass(choices, Enum):
|
|
168
|
+
# Enum class passed directly (e.g., Frameworks)
|
|
169
|
+
valid_choices = {member.name for member in choices}
|
|
170
|
+
else:
|
|
171
|
+
# Handle collections: check if they contain Enum members
|
|
172
|
+
first_elem = next(iter(choices), None) if choices else None
|
|
173
|
+
if first_elem and isinstance(first_elem, Enum):
|
|
174
|
+
# Collection of Enum members (e.g., tuple of Frameworks members)
|
|
175
|
+
valid_choices = {member.name for member in choices}
|
|
176
|
+
else:
|
|
177
|
+
# Plain collection of strings or other values
|
|
178
|
+
valid_choices = set(choices)
|
|
179
|
+
|
|
180
|
+
def decorator(func: Callable) -> Callable:
|
|
181
|
+
@wraps(func)
|
|
182
|
+
def wrapper(self, value: Any) -> Any:
|
|
183
|
+
# if value is None
|
|
184
|
+
if value is None:
|
|
185
|
+
if not allow_none:
|
|
186
|
+
_msg = f"{func.__name__} cannot be None."
|
|
187
|
+
raise ValueError(_msg)
|
|
188
|
+
return func(self, value)
|
|
189
|
+
|
|
190
|
+
# Extract the actual value to check
|
|
191
|
+
# If value is an Enum member, use its name
|
|
192
|
+
if isinstance(value, Enum):
|
|
193
|
+
actual_value = value.name
|
|
194
|
+
else:
|
|
195
|
+
actual_value = value
|
|
196
|
+
|
|
197
|
+
# if case-insensitive, adjust value and choices
|
|
198
|
+
if case_sensitive:
|
|
199
|
+
check_val = actual_value
|
|
200
|
+
compare_set = valid_choices
|
|
201
|
+
# otherwise, use upper-case for comparison
|
|
202
|
+
else:
|
|
203
|
+
check_val = str(actual_value).upper()
|
|
204
|
+
compare_set = {str(c).upper() for c in valid_choices}
|
|
205
|
+
|
|
206
|
+
# if value not in choices, raise error
|
|
207
|
+
if check_val not in compare_set:
|
|
208
|
+
_msg = f"Invalid {func.__name__}: {value}. "
|
|
209
|
+
# Format choices nicely - if Enum members, use their name/value only
|
|
210
|
+
choice_strs = [c.name if isinstance(c, Enum) else str(c) for c in valid_choices]
|
|
211
|
+
_msg += f"Must be one of: {', '.join(choice_strs)}."
|
|
212
|
+
raise ValueError(_msg)
|
|
213
|
+
|
|
214
|
+
# otherwise, call the original function
|
|
215
|
+
return func(self, value)
|
|
216
|
+
return wrapper # return the wrapper
|
|
217
|
+
return decorator # return the decorator
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_index(allow_zero: bool = True,
|
|
221
|
+
allow_negative: bool = False) -> Callable:
|
|
222
|
+
"""*validate_index()* Decorator to validate integer values with negativity and zero control.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
allow_zero (bool, optional): Whether zero is allowed. Defaults to True.
|
|
226
|
+
allow_negative (bool, optional): Whether negative integers are allowed. Defaults to False.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: If value is not an integer.
|
|
230
|
+
ValueError: If negative value when allow_negative is False.
|
|
231
|
+
ValueError: If zero value when allow_zero is False.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Callable: Decorated function with integer validation.
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
# Non-negative integers only
|
|
238
|
+
@idx.setter
|
|
239
|
+
@validate_index(allow_negative=False)
|
|
240
|
+
def idx(self, val: int) -> None:
|
|
241
|
+
self._idx = val
|
|
242
|
+
|
|
243
|
+
# Positive integers only (no zero)
|
|
244
|
+
@count.setter
|
|
245
|
+
@validate_index(allow_negative=False, allow_zero=False)
|
|
246
|
+
def count(self, val: int) -> None:
|
|
247
|
+
self._count = val
|
|
248
|
+
"""
|
|
249
|
+
def decorator(func: Callable) -> Callable:
|
|
250
|
+
@wraps(func)
|
|
251
|
+
def wrapper(self, value: Any) -> Any:
|
|
252
|
+
# if value is not None, perform checks
|
|
253
|
+
if value is not None:
|
|
254
|
+
# if not an integer, raise error
|
|
255
|
+
if not isinstance(value, int):
|
|
256
|
+
_msg = f"{func.__name__} must be an integer. "
|
|
257
|
+
_msg += f"Provided: {value} ({type(value).__name__})."
|
|
258
|
+
raise ValueError(_msg)
|
|
259
|
+
|
|
260
|
+
# if negative not allowed and value es velow zero
|
|
261
|
+
if not allow_negative and value < 0:
|
|
262
|
+
_msg = f"{func.__name__} must be non-negative. "
|
|
263
|
+
_msg += f"Provided: {value}."
|
|
264
|
+
raise ValueError(_msg)
|
|
265
|
+
|
|
266
|
+
# if zero not allowed and value is zero
|
|
267
|
+
if not allow_zero and value == 0:
|
|
268
|
+
_msg = f"{func.__name__} cannot be zero. "
|
|
269
|
+
_msg += f"Provided: {value}."
|
|
270
|
+
raise ValueError(_msg)
|
|
271
|
+
|
|
272
|
+
# otherwise, call the original function
|
|
273
|
+
return func(self, value)
|
|
274
|
+
return wrapper # return the wrapper
|
|
275
|
+
return decorator # return the decorator
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def validate_range(min_value: Optional[float] = None,
|
|
279
|
+
max_value: Optional[float] = None,
|
|
280
|
+
min_inclusive: bool = True,
|
|
281
|
+
max_inclusive: bool = True,
|
|
282
|
+
min_attr: Optional[str] = None,
|
|
283
|
+
max_attr: Optional[str] = None) -> Callable:
|
|
284
|
+
"""Decorator to validate numeric value is within specified range.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
min_value (Optional[float], optional): Static minimum value. Defaults to None.
|
|
288
|
+
max_value (Optional[float], optional): Static maximum value. Defaults to None.
|
|
289
|
+
min_inclusive (bool, optional): Whether minimum is inclusive (>=) or exclusive (>). Defaults to True.
|
|
290
|
+
max_inclusive (bool, optional): Whether maximum is inclusive (<=) or exclusive (<). Defaults to True.
|
|
291
|
+
min_attr (Optional[str], optional): Attribute name for dynamic minimum (e.g., '_min'). Defaults to None.
|
|
292
|
+
max_attr (Optional[str], optional): Attribute name for dynamic maximum (e.g., '_max'). Defaults to None.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
ValueError: If value is outside the specified range.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Callable: Decorated function with range validation.
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
# Static range
|
|
302
|
+
@age.setter
|
|
303
|
+
@validate_type(int)
|
|
304
|
+
@validate_range(min_value=0, max_value=150)
|
|
305
|
+
def age(self, val: int) -> None:
|
|
306
|
+
self._age = val
|
|
307
|
+
|
|
308
|
+
# Dynamic range based on other attributes
|
|
309
|
+
@mean.setter
|
|
310
|
+
@validate_type(int, float)
|
|
311
|
+
@validate_range(min_attr='_min', max_attr='_max')
|
|
312
|
+
def mean(self, val: float) -> None:
|
|
313
|
+
self._mean = val
|
|
314
|
+
"""
|
|
315
|
+
def decorator(func: Callable) -> Callable:
|
|
316
|
+
@wraps(func)
|
|
317
|
+
def wrapper(self, value: Any) -> Any:
|
|
318
|
+
if value is not None:
|
|
319
|
+
# Check static minimum
|
|
320
|
+
if min_value is not None:
|
|
321
|
+
if min_inclusive:
|
|
322
|
+
if value < min_value:
|
|
323
|
+
_msg = f"{func.__name__} must be >= {min_value}, "
|
|
324
|
+
_msg += f"got {value}."
|
|
325
|
+
raise ValueError(_msg)
|
|
326
|
+
else:
|
|
327
|
+
if value <= min_value:
|
|
328
|
+
_msg = f"{func.__name__} must be > {min_value}, "
|
|
329
|
+
_msg += f"got {value}."
|
|
330
|
+
raise ValueError(_msg)
|
|
331
|
+
|
|
332
|
+
# Check static maximum
|
|
333
|
+
if max_value is not None:
|
|
334
|
+
if max_inclusive:
|
|
335
|
+
if value > max_value:
|
|
336
|
+
_msg = f"{func.__name__} must be <= {max_value}, "
|
|
337
|
+
_msg += f"got {value}."
|
|
338
|
+
raise ValueError(_msg)
|
|
339
|
+
else:
|
|
340
|
+
if value >= max_value:
|
|
341
|
+
_msg = f"{func.__name__} must be < {max_value}, "
|
|
342
|
+
_msg += f"got {value}."
|
|
343
|
+
raise ValueError(_msg)
|
|
344
|
+
|
|
345
|
+
# Check dynamic minimum from attribute
|
|
346
|
+
if min_attr and hasattr(self, min_attr):
|
|
347
|
+
min_val = getattr(self, min_attr)
|
|
348
|
+
if min_val is not None and value < min_val:
|
|
349
|
+
_msg = f"{func.__name__} ({value}) cannot be less than "
|
|
350
|
+
_msg += f"minimum ({min_val})."
|
|
351
|
+
raise ValueError(_msg)
|
|
352
|
+
|
|
353
|
+
# Check dynamic maximum from attribute
|
|
354
|
+
if max_attr and hasattr(self, max_attr):
|
|
355
|
+
max_val = getattr(self, max_attr)
|
|
356
|
+
if max_val is not None and value > max_val:
|
|
357
|
+
_msg = f"{func.__name__} ({value}) cannot be greater than "
|
|
358
|
+
_msg += f"maximum ({max_val})."
|
|
359
|
+
raise ValueError(_msg)
|
|
360
|
+
|
|
361
|
+
# otherwise, call the original function
|
|
362
|
+
return func(self, value)
|
|
363
|
+
return wrapper # return the wrapper
|
|
364
|
+
return decorator # return the decorator
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def validate_pattern(pattern: Optional[Union[str, list, tuple]] = None,
|
|
368
|
+
allow_alnum: bool = False,
|
|
369
|
+
error_msg: Optional[str] = None,
|
|
370
|
+
examples: Optional[str] = None) -> Callable:
|
|
371
|
+
"""Decorator to validate string matches regex pattern(s) or is alphanumeric.
|
|
372
|
+
|
|
373
|
+
This unified decorator handles:
|
|
374
|
+
- Single pattern matching
|
|
375
|
+
- Multiple pattern matching (OR logic - matches any pattern)
|
|
376
|
+
- Optional alphanumeric validation
|
|
377
|
+
- Scientific/mathematical symbols (alphanumeric OR LaTeX)
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
pattern (Union[str, list, tuple]): Single regex pattern string, or list/tuple of patterns to match (OR logic).
|
|
381
|
+
allow_alnum (bool, optional): Whether to accept alphanumeric strings. Defaults to False.
|
|
382
|
+
error_msg (Optional[str], optional): Custom error message (overrides default). Defaults to None.
|
|
383
|
+
examples (Optional[str], optional): Example strings to show in error messages. Defaults to None.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
ValueError: If value does not match any pattern and is not alphanumeric (when allowed).
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Callable: Decorated function with pattern validation.
|
|
390
|
+
|
|
391
|
+
Examples:
|
|
392
|
+
# Simple pattern matching
|
|
393
|
+
@code.setter
|
|
394
|
+
@validate_pattern(r'^[A-Z]\\d{3}$')
|
|
395
|
+
def code(self, val: str) -> None:
|
|
396
|
+
self._code = val
|
|
397
|
+
|
|
398
|
+
# Symbol validation (alphanumeric OR LaTeX)
|
|
399
|
+
from pydasa.validations.patterns import LATEX_RE
|
|
400
|
+
|
|
401
|
+
@sym.setter
|
|
402
|
+
@validate_type(str)
|
|
403
|
+
@validate_emptiness()
|
|
404
|
+
@validate_pattern(LATEX_RE, allow_alnum=True)
|
|
405
|
+
def sym(self, val: str) -> None:
|
|
406
|
+
self._sym = val
|
|
407
|
+
|
|
408
|
+
# Multiple patterns (match any)
|
|
409
|
+
@validate_pattern([r'^\\\\[a-z]+$', r'^\\d+$'])
|
|
410
|
+
def value(self, val: str) -> None:
|
|
411
|
+
self._value = val
|
|
412
|
+
"""
|
|
413
|
+
# Validate decorator configuration
|
|
414
|
+
if pattern is None and not allow_alnum:
|
|
415
|
+
_msg = "Provide either 'pattern' or 'allow_alnum' must be True"
|
|
416
|
+
raise ValueError(_msg)
|
|
417
|
+
|
|
418
|
+
# Compile pattern(s) into list
|
|
419
|
+
if pattern is None:
|
|
420
|
+
compiled_patterns = []
|
|
421
|
+
elif isinstance(pattern, (list, tuple)):
|
|
422
|
+
compiled_patterns = [re.compile(p) for p in pattern]
|
|
423
|
+
else:
|
|
424
|
+
compiled_patterns = [re.compile(pattern)]
|
|
425
|
+
|
|
426
|
+
def decorator(func: Callable) -> Callable:
|
|
427
|
+
@wraps(func)
|
|
428
|
+
def wrapper(self, value: Any) -> Any:
|
|
429
|
+
if value is not None:
|
|
430
|
+
is_valid = False
|
|
431
|
+
|
|
432
|
+
# Check if alphanumeric (if allowed)
|
|
433
|
+
if allow_alnum and value.isalnum():
|
|
434
|
+
is_valid = True
|
|
435
|
+
|
|
436
|
+
# Check if matches any pattern
|
|
437
|
+
if not is_valid and compiled_patterns:
|
|
438
|
+
for compiled_pattern in compiled_patterns:
|
|
439
|
+
if compiled_pattern.match(value):
|
|
440
|
+
is_valid = True
|
|
441
|
+
break
|
|
442
|
+
|
|
443
|
+
# Raise error if validation failed
|
|
444
|
+
# TODO improve msg construction
|
|
445
|
+
if not is_valid:
|
|
446
|
+
if error_msg:
|
|
447
|
+
_msg = error_msg
|
|
448
|
+
elif allow_alnum and compiled_patterns:
|
|
449
|
+
_msg = f"{func.__name__} must be alphanumeric or match pattern. "
|
|
450
|
+
_msg += f"Provided: '{value}'. "
|
|
451
|
+
if examples:
|
|
452
|
+
_msg += f"Examples: {examples}"
|
|
453
|
+
else:
|
|
454
|
+
_msg += "Examples: 'V', 'd', '\\\\Pi_{{0}}', '\\\\rho'."
|
|
455
|
+
elif allow_alnum:
|
|
456
|
+
_msg = f"{func.__name__} must be alphanumeric. "
|
|
457
|
+
_msg += f"Provided: '{value}'."
|
|
458
|
+
elif len(compiled_patterns) == 1:
|
|
459
|
+
_msg = f"{func.__name__} must match pattern. "
|
|
460
|
+
_msg += f"Provided: {repr(value)}."
|
|
461
|
+
elif len(compiled_patterns) > 1:
|
|
462
|
+
_msg = f"{func.__name__} must match one of {len(compiled_patterns)} patterns. "
|
|
463
|
+
_msg += f"Provided: {repr(value)}."
|
|
464
|
+
else:
|
|
465
|
+
_msg = f"{func.__name__} validation failed for: {repr(value)}."
|
|
466
|
+
|
|
467
|
+
raise ValueError(_msg)
|
|
468
|
+
|
|
469
|
+
# otherwise, call the original function
|
|
470
|
+
return func(self, value)
|
|
471
|
+
return wrapper # return the wrapper
|
|
472
|
+
return decorator # return the decorator
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def validate_custom(validator_func: Callable[[Any, Any], None]) -> Callable:
|
|
476
|
+
"""*validate_custom()* Decorator for custom validation logic. Allows implementing custom validation logic by providing a validator function.
|
|
477
|
+
|
|
478
|
+
The validator function should raise ValueError if validation fails.
|
|
479
|
+
NOTE: this is too abstract and should be used sparingly.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
validator_func (Callable[[Any, Any], None]): Function(self, value) that raises ValueError if invalid.
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
ValueError: If custom validator function raises ValueError.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Callable: Decorated function with custom validation.
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
def check_range_consistency(self, value):
|
|
492
|
+
'''Ensure minimum does not exceed maximum.'''
|
|
493
|
+
if value is not None and self._max is not None and value > self._max:
|
|
494
|
+
raise ValueError(f"min {value} > max {self._max}")
|
|
495
|
+
|
|
496
|
+
@min.setter
|
|
497
|
+
@validate_type(int, float)
|
|
498
|
+
@validate_custom(check_range_consistency)
|
|
499
|
+
def min(self, val: float) -> None:
|
|
500
|
+
self._min = val
|
|
501
|
+
"""
|
|
502
|
+
def decorator(func: Callable) -> Callable:
|
|
503
|
+
@wraps(func)
|
|
504
|
+
def wrapper(self, value: Any) -> Any:
|
|
505
|
+
# Run custom validator
|
|
506
|
+
validator_func(self, value)
|
|
507
|
+
# otherwise, call the original function
|
|
508
|
+
return func(self, value)
|
|
509
|
+
return wrapper # return the wrapper
|
|
510
|
+
return decorator # return the decorator
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Module error.py
|
|
4
|
+
===========================================
|
|
5
|
+
|
|
6
|
+
General error handling module/function for the PyDASA Data Structures and Algorithms in PyDASA package.
|
|
7
|
+
"""
|
|
8
|
+
# native python modules
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any
|
|
11
|
+
# custom modules
|
|
12
|
+
# import global variables
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_error(ctx: str, func: str, exc: Exception) -> None:
|
|
16
|
+
"""*handle_error()* generic function to handle errors iacross the whole PyDASA library.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
ctx (str): The context (e.g., package/class) where the error occurred.
|
|
20
|
+
func (str): The name of the function or method where the error occurred.
|
|
21
|
+
exc (Exception): The exception that was raised.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
TypeError: If the context is not a string.
|
|
25
|
+
TypeError: If the function name is not a string.
|
|
26
|
+
TypeError: If the exception is not an instance of Exception.
|
|
27
|
+
type: If the error message is not a string.
|
|
28
|
+
"""
|
|
29
|
+
# Validate the context
|
|
30
|
+
if not isinstance(ctx, str):
|
|
31
|
+
_msg = f"Invalid context: {ctx}. Context must be a string."
|
|
32
|
+
raise TypeError(_msg)
|
|
33
|
+
|
|
34
|
+
# Validate the function name
|
|
35
|
+
if not isinstance(func, str):
|
|
36
|
+
_msg = f"Invalid function name: {func}. "
|
|
37
|
+
_msg += "Function name must be a string."
|
|
38
|
+
raise TypeError(_msg)
|
|
39
|
+
|
|
40
|
+
# Validate the exception
|
|
41
|
+
if not isinstance(exc, Exception):
|
|
42
|
+
_msg = f"Invalid exception: {exc}. "
|
|
43
|
+
_msg += "Exception must be an instance of Exception."
|
|
44
|
+
raise TypeError(_msg)
|
|
45
|
+
|
|
46
|
+
# Format and raise the error with additional context
|
|
47
|
+
_err_msg = f"Error in {ctx}.{func}: {exc}"
|
|
48
|
+
raise type(exc)(_err_msg).with_traceback(exc.__traceback__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def inspect_var(var: Any) -> str:
|
|
52
|
+
"""*inspect_var() inspect a variable an gets its name in the source code.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
var (Any): The variable to inspect.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: The name of the variable.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If the variable name cannot be found in the current scope.
|
|
62
|
+
"""
|
|
63
|
+
# Get the current frame
|
|
64
|
+
frame = inspect.currentframe()
|
|
65
|
+
try:
|
|
66
|
+
# Check if frame exists
|
|
67
|
+
if frame is None:
|
|
68
|
+
return "<unknown>"
|
|
69
|
+
|
|
70
|
+
# Get the caller's frame
|
|
71
|
+
caller_frame = frame.f_back
|
|
72
|
+
|
|
73
|
+
# Check if caller_frame exists
|
|
74
|
+
if caller_frame is None:
|
|
75
|
+
return "<unknown>"
|
|
76
|
+
|
|
77
|
+
# Get local variables from caller's frame
|
|
78
|
+
caller_locals = caller_frame.f_locals
|
|
79
|
+
|
|
80
|
+
# Search for the variable in caller's locals
|
|
81
|
+
for name, value in caller_locals.items():
|
|
82
|
+
if value is var:
|
|
83
|
+
return name
|
|
84
|
+
|
|
85
|
+
# If not found in locals, check globals
|
|
86
|
+
caller_globals = caller_frame.f_globals
|
|
87
|
+
for name, value in caller_globals.items():
|
|
88
|
+
if value is var:
|
|
89
|
+
return name
|
|
90
|
+
|
|
91
|
+
return "<unknown>"
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
# If introspection fails for any reason, return unknown
|
|
95
|
+
_msg = f"Could not inspect variable: {var}"
|
|
96
|
+
_msg += f" due to error: {e}"
|
|
97
|
+
raise ValueError(_msg)
|
|
98
|
+
finally:
|
|
99
|
+
# Clean up frame references to avoid reference cycles
|
|
100
|
+
del frame
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Module patterns.py
|
|
4
|
+
===========================================
|
|
5
|
+
|
|
6
|
+
Regex patterns for validation and parsing in PyDASA.
|
|
7
|
+
|
|
8
|
+
Contains:
|
|
9
|
+
- LaTeX validation patterns
|
|
10
|
+
- FDU (Fundamental Dimensional Unit) matching patterns
|
|
11
|
+
- Default and working pattern sets for dimensional analysis
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# LaTeX Patterns
|
|
15
|
+
# Allow valid LaTeX strings starting with a backslash or alphanumeric strings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# NOTE: OG REGEX!
|
|
19
|
+
# LATEX_RE: str = r"([a-zA-Z]+)(?:_\{\d+\})?"
|
|
20
|
+
# :attr: LATEX_RE
|
|
21
|
+
LATEX_RE: str = r"\\?[a-zA-Z]+(?:_\{\d+\})?"
|
|
22
|
+
"""
|
|
23
|
+
LaTeX regex pattern to match LaTeX symbols (e.g., '\\alpha', '\\beta_{1}') in *PyDASA*.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# NOTE: OG REGEX!
|
|
27
|
+
# DFLT_POW_RE: str = r"\-?\d+" # r'\^(-?\d+)'
|
|
28
|
+
# :attr: DFLT_POW_RE
|
|
29
|
+
DFLT_POW_RE: str = r"\-?\d+"
|
|
30
|
+
"""
|
|
31
|
+
Default regex to match FDUs with exponents (e.g., 'M*L^-1*T^-2' to 'M^(1)*L^(-1)*T^(-2)').
|
|
32
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# TODO deprecate after migration
|