value-object-pattern 0.1.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.
- value_object_pattern/__init__.py +10 -0
- value_object_pattern/decorators/__init__.py +7 -0
- value_object_pattern/decorators/value_object_process.py +83 -0
- value_object_pattern/decorators/value_object_validation.py +78 -0
- value_object_pattern/models/__init__.py +3 -0
- value_object_pattern/models/value_object.py +383 -0
- value_object_pattern/py.typed +0 -0
- value_object_pattern/usables/__init__.py +54 -0
- value_object_pattern/usables/dates/__init__.py +9 -0
- value_object_pattern/usables/dates/date/__init__.py +7 -0
- value_object_pattern/usables/dates/date/date_value_object.py +162 -0
- value_object_pattern/usables/dates/date/string_date_value_object.py +201 -0
- value_object_pattern/usables/dates/datetime/__init__.py +7 -0
- value_object_pattern/usables/dates/datetime/datetime_value_object.py +193 -0
- value_object_pattern/usables/dates/datetime/string_datetime_value_object.py +237 -0
- value_object_pattern/usables/identifiers/__init__.py +7 -0
- value_object_pattern/usables/identifiers/country_ids/__init__.py +3 -0
- value_object_pattern/usables/identifiers/country_ids/spain/__init__.py +3 -0
- value_object_pattern/usables/identifiers/country_ids/spain/dni_value_object.py +63 -0
- value_object_pattern/usables/identifiers/string_uuid_value_object.py +56 -0
- value_object_pattern/usables/identifiers/uuid_value_object.py +40 -0
- value_object_pattern/usables/internet/__init__.py +38 -0
- value_object_pattern/usables/internet/api_keys/__init__.py +13 -0
- value_object_pattern/usables/internet/api_keys/aws_access_key_id_value_object.py +40 -0
- value_object_pattern/usables/internet/api_keys/aws_secret_access_key_value_object.py +40 -0
- value_object_pattern/usables/internet/api_keys/github_personal_access_token_value_object.py +41 -0
- value_object_pattern/usables/internet/api_keys/openai_api_key_value_object.py +40 -0
- value_object_pattern/usables/internet/api_keys/resend_api_key_value_object.py +40 -0
- value_object_pattern/usables/internet/aws_cloud_region_value_object.py +77 -0
- value_object_pattern/usables/internet/domain_value_object.py +149 -0
- value_object_pattern/usables/internet/host_value_object.py +143 -0
- value_object_pattern/usables/internet/ipv4_address_value_object.py +305 -0
- value_object_pattern/usables/internet/ipv4_network_value_object.py +165 -0
- value_object_pattern/usables/internet/ipv6_address_value_object.py +288 -0
- value_object_pattern/usables/internet/ipv6_network_value_object.py +145 -0
- value_object_pattern/usables/internet/mac_address_value_object.py +390 -0
- value_object_pattern/usables/internet/port_value_object.py +682 -0
- value_object_pattern/usables/internet/uri/__init__.py +11 -0
- value_object_pattern/usables/internet/uri/http_https_url_value_object.py +39 -0
- value_object_pattern/usables/internet/uri/http_url_value_object.py +39 -0
- value_object_pattern/usables/internet/uri/https_url_value_object.py +39 -0
- value_object_pattern/usables/internet/uri/url_value_object.py +396 -0
- value_object_pattern/usables/primitives/__init__.py +45 -0
- value_object_pattern/usables/primitives/boolean/__init__.py +9 -0
- value_object_pattern/usables/primitives/boolean/boolean_value_object.py +36 -0
- value_object_pattern/usables/primitives/boolean/false_value_object.py +37 -0
- value_object_pattern/usables/primitives/boolean/true_value_object.py +37 -0
- value_object_pattern/usables/primitives/bytes/__init__.py +3 -0
- value_object_pattern/usables/primitives/bytes/bytes_value_object.py +36 -0
- value_object_pattern/usables/primitives/float/__init__.py +9 -0
- value_object_pattern/usables/primitives/float/float_value_object.py +36 -0
- value_object_pattern/usables/primitives/float/negative_float_value_object.py +37 -0
- value_object_pattern/usables/primitives/float/positive_float_value_object.py +37 -0
- value_object_pattern/usables/primitives/integer/__init__.py +13 -0
- value_object_pattern/usables/primitives/integer/even_integer_value_object.py +37 -0
- value_object_pattern/usables/primitives/integer/integer_value_object.py +36 -0
- value_object_pattern/usables/primitives/integer/negative_integer_value_object.py +37 -0
- value_object_pattern/usables/primitives/integer/odd_integer_value_object.py +37 -0
- value_object_pattern/usables/primitives/integer/positive_integer_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/__init__.py +21 -0
- value_object_pattern/usables/primitives/string/alpha_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/alphanumeric_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/digit_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/lowercase_string_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/non_empty_string_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/printable_string_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/string_value_object.py +36 -0
- value_object_pattern/usables/primitives/string/trimmed_string_value_object.py +37 -0
- value_object_pattern/usables/primitives/string/uppercase_string_value_object.py +37 -0
- value_object_pattern-0.1.0.dist-info/METADATA +95 -0
- value_object_pattern-0.1.0.dist-info/RECORD +73 -0
- value_object_pattern-0.1.0.dist-info/WHEEL +4 -0
- value_object_pattern-0.1.0.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
"""
|
2
|
+
Process decorator for value object pattern.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from functools import wraps
|
6
|
+
from typing import Any, Callable, TypeVar
|
7
|
+
|
8
|
+
T = TypeVar('T')
|
9
|
+
|
10
|
+
|
11
|
+
def process(order: int | None = None) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
12
|
+
"""
|
13
|
+
Decorator for process the value after the value is validated.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
order (int | None, optional): The order of the process that will be executed, if None the functions will be
|
17
|
+
executed alphabetically. Defaults to None.
|
18
|
+
|
19
|
+
Raises:
|
20
|
+
TypeError: If the order is not an integer.
|
21
|
+
ValueError: If the order is not equal or greater than 0.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
Callable[[Callable[..., T]], Callable[..., T]]: Wrapper function for the process.
|
25
|
+
|
26
|
+
Example:
|
27
|
+
```python
|
28
|
+
from value_object_pattern import ValueObject, process
|
29
|
+
|
30
|
+
|
31
|
+
class UpperStringValueObject(ValueObject[str]):
|
32
|
+
@process()
|
33
|
+
def ensure_value_is_upper(self, value: str) -> str:
|
34
|
+
return value.upper()
|
35
|
+
|
36
|
+
|
37
|
+
string = UpperStringValueObject(value='hello world')
|
38
|
+
print(string)
|
39
|
+
# >>> HELLO WORLD
|
40
|
+
```
|
41
|
+
"""
|
42
|
+
|
43
|
+
def decorator(function: Callable[..., T]) -> Callable[..., T]:
|
44
|
+
"""
|
45
|
+
Decorator for process the value after the value is validated.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
function (Callable[..., T]): Function to be execution after the value object is validated.
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
TypeError: If the order is not an integer.
|
52
|
+
ValueError: If the order is not equal or greater than 0.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Callable[..., T]: Wrapper function for the process.
|
56
|
+
"""
|
57
|
+
if order is not None:
|
58
|
+
if type(order) is not int:
|
59
|
+
raise TypeError(f'Process order <<<{order}>>> must be an integer. Got <<<{type(order).__name__}>>> type.') # noqa: E501 # fmt: skip
|
60
|
+
|
61
|
+
if order < 0:
|
62
|
+
raise ValueError(f'Process order <<<{order}>>> must be equal or greater than 0.')
|
63
|
+
|
64
|
+
function._is_process = True # type: ignore[attr-defined]
|
65
|
+
function._order = function.__name__ if order is None else str(order) # type: ignore[attr-defined]
|
66
|
+
|
67
|
+
@wraps(wrapped=function)
|
68
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> T:
|
69
|
+
"""
|
70
|
+
Wrapper for process.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
*args (tuple[Any, ...]): The arguments for the function.
|
74
|
+
**kwargs (dict[str, Any]): The keyword arguments for the function.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
T: The return value of the function.
|
78
|
+
"""
|
79
|
+
return function(*args, **kwargs)
|
80
|
+
|
81
|
+
return wrapper
|
82
|
+
|
83
|
+
return decorator
|
@@ -0,0 +1,78 @@
|
|
1
|
+
"""
|
2
|
+
Validation decorator for value object pattern.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from functools import wraps
|
6
|
+
from typing import Any, Callable
|
7
|
+
|
8
|
+
|
9
|
+
def validation(order: int | None = None) -> Callable[[Callable[..., None]], Callable[..., None]]:
|
10
|
+
"""
|
11
|
+
Decorator for validation the value before the value is created.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
order (int | None, optional): The order of the validation that will be executed, if None the functions will be
|
15
|
+
executed alphabetically. Defaults to None.
|
16
|
+
|
17
|
+
Raises:
|
18
|
+
TypeError: If the order is not an integer.
|
19
|
+
ValueError: If the order is not equal or greater than 0.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Callable[[Callable[..., None]], Callable[..., None]]: Wrapper function for the validation.
|
23
|
+
|
24
|
+
Example:
|
25
|
+
```python
|
26
|
+
from value_object_pattern import ValueObject, validation
|
27
|
+
|
28
|
+
|
29
|
+
class IntegerValueObject(ValueObject[int]):
|
30
|
+
@validation()
|
31
|
+
def ensure_value_is_integer(self, value: int) -> None:
|
32
|
+
if type(value) is not int:
|
33
|
+
raise TypeError(f'IntegerValueObject value <<<{value}>>> must be an integer. Got <<<{type(value).__name__}>>> type.')
|
34
|
+
|
35
|
+
|
36
|
+
integer = IntegerValueObject(value='invalid')
|
37
|
+
# >>> TypeError: IntegerValueObject value <<<invalid>>> must be an integer. Got <<<str>>> type.
|
38
|
+
```
|
39
|
+
""" # noqa: E501 # fmt: skip
|
40
|
+
|
41
|
+
def decorator(function: Callable[..., None]) -> Callable[..., None]:
|
42
|
+
"""
|
43
|
+
Decorator for validation the value before the value is created.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
function (Callable[..., None]): Function to be execution before the value object is created.
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
TypeError: If the order is not an integer.
|
50
|
+
ValueError: If the order is not equal or greater than 0.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Callable[..., None]: Wrapper function for the validation.
|
54
|
+
"""
|
55
|
+
if order is not None:
|
56
|
+
if type(order) is not int:
|
57
|
+
raise TypeError(f'Validation order <<<{order}>>> must be an integer. Got <<<{type(order).__name__}>>> type.') # noqa: E501 # fmt: skip
|
58
|
+
|
59
|
+
if order < 0:
|
60
|
+
raise ValueError(f'Validation order <<<{order}>>> must be equal or greater than 0.')
|
61
|
+
|
62
|
+
function._is_validation = True # type: ignore[attr-defined]
|
63
|
+
function._order = function.__name__ if order is None else str(order) # type: ignore[attr-defined]
|
64
|
+
|
65
|
+
@wraps(wrapped=function)
|
66
|
+
def wrapper(*args: tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
|
67
|
+
"""
|
68
|
+
Wrapper for validation.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
*args (tuple[Any, ...]): The arguments for the function.
|
72
|
+
**kwargs (dict[str, Any]): The keyword arguments for the function.
|
73
|
+
"""
|
74
|
+
function(*args, **kwargs)
|
75
|
+
|
76
|
+
return wrapper
|
77
|
+
|
78
|
+
return decorator
|
@@ -0,0 +1,383 @@
|
|
1
|
+
"""
|
2
|
+
Value object generic type.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC
|
6
|
+
from collections import deque
|
7
|
+
from sys import version_info
|
8
|
+
from typing import Any, Callable, Generic, NoReturn, TypeVar
|
9
|
+
|
10
|
+
if version_info >= (3, 12):
|
11
|
+
from typing import override # pragma: no cover
|
12
|
+
else:
|
13
|
+
from typing_extensions import override # pragma: no cover
|
14
|
+
|
15
|
+
T = TypeVar('T')
|
16
|
+
|
17
|
+
|
18
|
+
class ValueObject(ABC, Generic[T]):
|
19
|
+
"""
|
20
|
+
ValueObject generic type.
|
21
|
+
|
22
|
+
Example:
|
23
|
+
```python
|
24
|
+
from value_object_pattern import ValueObject
|
25
|
+
|
26
|
+
|
27
|
+
class IntegerValueObject(ValueObject[int]):
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
integer = IntegerValueObject(value=10)
|
32
|
+
print(repr(integer))
|
33
|
+
# >>> IntegerValueObject(value=10)
|
34
|
+
```
|
35
|
+
"""
|
36
|
+
|
37
|
+
__slots__ = ('_title', '_value')
|
38
|
+
__match_args__ = ('_title', '_value')
|
39
|
+
|
40
|
+
_value: T
|
41
|
+
_title: str
|
42
|
+
|
43
|
+
def __init__(self, *, value: T, title: str | None = None) -> None:
|
44
|
+
"""
|
45
|
+
ValueObject value object constructor.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
value (T): The value to store in the value object.
|
49
|
+
title (str | None, optional): The title of the value object when raising exceptions, if title is None, the
|
50
|
+
class name is used instead. Defaults to None.
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
TypeError: If the title is not a string.
|
54
|
+
ValueError: If the title contains leading or trailing whitespaces.
|
55
|
+
|
56
|
+
Example:
|
57
|
+
```python
|
58
|
+
from value_object_pattern import ValueObject
|
59
|
+
|
60
|
+
|
61
|
+
class IntegerValueObject(ValueObject[int]):
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
integer = IntegerValueObject(value=10)
|
66
|
+
print(repr(integer))
|
67
|
+
# >>> IntegerValueObject(value=10)
|
68
|
+
```
|
69
|
+
"""
|
70
|
+
if title is None:
|
71
|
+
title = self.__class__.__name__
|
72
|
+
|
73
|
+
if type(title) is not str:
|
74
|
+
raise TypeError(f'ValueObject value <<<{title}>>> must be a string. Got <<<{type(title).__name__}>>> instead.') # noqa: E501 # fmt: skip
|
75
|
+
|
76
|
+
if title.strip() != title:
|
77
|
+
raise ValueError(f'ValueObject title <<<{title}>>> contains leading or trailing whitespaces. Only trimmed values are allowed.') # noqa: E501 # fmt: skip
|
78
|
+
|
79
|
+
object.__setattr__(self, '_title', title)
|
80
|
+
|
81
|
+
self._validate(value=value)
|
82
|
+
value = self._process(value=value)
|
83
|
+
|
84
|
+
object.__setattr__(self, '_value', value)
|
85
|
+
|
86
|
+
@override
|
87
|
+
def __repr__(self) -> str:
|
88
|
+
"""
|
89
|
+
Returns a detailed string representation of the value object.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
str: A string representation of the value object in the format 'ClassName(value=value)'.
|
93
|
+
|
94
|
+
Example:
|
95
|
+
```python
|
96
|
+
from value_object_pattern import ValueObject
|
97
|
+
|
98
|
+
|
99
|
+
class IntegerValueObject(ValueObject[int]):
|
100
|
+
pass
|
101
|
+
|
102
|
+
|
103
|
+
integer = IntegerValueObject(value=10)
|
104
|
+
print(repr(integer))
|
105
|
+
# >>> IntegerValueObject(value=10)
|
106
|
+
```
|
107
|
+
"""
|
108
|
+
return f'{self.__class__.__name__}(value={self._value!s})'
|
109
|
+
|
110
|
+
@override
|
111
|
+
def __str__(self) -> str:
|
112
|
+
"""
|
113
|
+
Returns a simple string representation of the value object.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
str: The string representation of the value object value.
|
117
|
+
|
118
|
+
Example:
|
119
|
+
```python
|
120
|
+
from value_object_pattern import ValueObject
|
121
|
+
|
122
|
+
|
123
|
+
class IntegerValueObject(ValueObject[int]):
|
124
|
+
pass
|
125
|
+
|
126
|
+
|
127
|
+
integer = IntegerValueObject(value=10)
|
128
|
+
print(integer)
|
129
|
+
# >>> 10
|
130
|
+
```
|
131
|
+
"""
|
132
|
+
return str(object=self._value)
|
133
|
+
|
134
|
+
@override
|
135
|
+
def __hash__(self) -> int:
|
136
|
+
"""
|
137
|
+
Returns the hash of the value object.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
int: Hash of the value object.
|
141
|
+
|
142
|
+
Example:
|
143
|
+
```python
|
144
|
+
from value_object_pattern import ValueObject
|
145
|
+
|
146
|
+
|
147
|
+
class IntegerValueObject(ValueObject[int]):
|
148
|
+
pass
|
149
|
+
|
150
|
+
|
151
|
+
integer = IntegerValueObject(value=10)
|
152
|
+
print(hash(integer))
|
153
|
+
# >>> 10
|
154
|
+
```
|
155
|
+
"""
|
156
|
+
return hash(self._value)
|
157
|
+
|
158
|
+
@override
|
159
|
+
def __eq__(self, other: object) -> bool:
|
160
|
+
"""
|
161
|
+
Check if the value object is equal to another value object.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
other (object): Object to compare.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
bool: True if both objects are equal, otherwise False.
|
168
|
+
|
169
|
+
Example:
|
170
|
+
```python
|
171
|
+
from value_object_pattern import ValueObject
|
172
|
+
|
173
|
+
|
174
|
+
class IntegerValueObject(ValueObject[int]):
|
175
|
+
pass
|
176
|
+
|
177
|
+
|
178
|
+
integer_a = IntegerValueObject(value=10)
|
179
|
+
integer_b = IntegerValueObject(value=16)
|
180
|
+
print(integer_a == integer_b)
|
181
|
+
# >>> False
|
182
|
+
```
|
183
|
+
"""
|
184
|
+
if not isinstance(other, self.__class__):
|
185
|
+
return NotImplemented
|
186
|
+
|
187
|
+
return self._value == other.value
|
188
|
+
|
189
|
+
@override
|
190
|
+
def __setattr__(self, key: str, value: T) -> NoReturn:
|
191
|
+
"""
|
192
|
+
Prevents modification or addition of attributes in the value object.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
key (str): The name of the attribute.
|
196
|
+
value (T): The value to be assigned to the attribute.
|
197
|
+
|
198
|
+
Raises:
|
199
|
+
AttributeError: If there is an attempt to modify an existing attribute.
|
200
|
+
AttributeError: If there is an attempt to add a new attribute.
|
201
|
+
"""
|
202
|
+
public_key = key.replace('_', '')
|
203
|
+
public_slots1 = [slot.replace('_', '') for slot in self.__slots__]
|
204
|
+
|
205
|
+
if key in self.__slots__:
|
206
|
+
raise AttributeError(f'Cannot modify attribute "{key}" of immutable instance.')
|
207
|
+
|
208
|
+
if public_key in public_slots1:
|
209
|
+
raise AttributeError(f'Cannot modify attribute "{public_key}" of immutable instance.')
|
210
|
+
|
211
|
+
raise AttributeError(f'{self.__class__.__name__} object has no attribute "{key}".')
|
212
|
+
|
213
|
+
def _process(self, value: T) -> T:
|
214
|
+
"""
|
215
|
+
This method processes the value object value after validation. It ensure that the value object is stored in the
|
216
|
+
correct format, by executing all methods with the `@process` decorator.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
value (T): The value object value.
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
T: The processed value object value.
|
223
|
+
"""
|
224
|
+
methods = self._gather_decorated_methods(instance=self, attribute_name='_is_process')
|
225
|
+
while methods:
|
226
|
+
method: Callable[..., T] = methods.popleft().__get__(self, self.__class__)
|
227
|
+
value = method(value=value)
|
228
|
+
|
229
|
+
return value
|
230
|
+
|
231
|
+
def _validate(self, value: T) -> None:
|
232
|
+
"""
|
233
|
+
This method validates that the value follows the domain rules, by executing all methods with the `@validation`
|
234
|
+
decorator.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
value (T): The value object value.
|
238
|
+
"""
|
239
|
+
try:
|
240
|
+
methods = self._gather_decorated_methods(instance=self, attribute_name='_is_validation')
|
241
|
+
while methods:
|
242
|
+
method: Callable[..., T] = methods.popleft().__get__(self, self.__class__)
|
243
|
+
method(value=value)
|
244
|
+
|
245
|
+
except Exception as error:
|
246
|
+
classes = self._post_order_dfs_mro(cls=self.__class__, cut_off=ValueObject)
|
247
|
+
for class_name in {cls.__name__ for cls in classes}:
|
248
|
+
error.args = (str(object=error.args[0]).replace(class_name, self.title),)
|
249
|
+
|
250
|
+
raise error
|
251
|
+
|
252
|
+
def _post_order_dfs_mro(self, cls: type, visited: set[type] | None = None, cut_off: type = object) -> list[type]:
|
253
|
+
"""
|
254
|
+
Computes the Post-Order Depth-First Search (DFS) Method Resolution Order (MRO) of a class.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
cls (type): The class to process.
|
258
|
+
visited (set[type] | None, optional): A set of already visited classes (to prevent duplicates). Defaults
|
259
|
+
to None.
|
260
|
+
cut_off (type, optional): The class to stop the search. Defaults to object.
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
list[type]: A list of classes type sorted by post-order DFS MRO.
|
264
|
+
|
265
|
+
References:
|
266
|
+
DFS: https://en.wikipedia.org/wiki/Depth-first_search
|
267
|
+
MRO: https://docs.python.org/3/howto/mro.html
|
268
|
+
"""
|
269
|
+
if cls is cut_off:
|
270
|
+
return []
|
271
|
+
|
272
|
+
if visited is None:
|
273
|
+
visited = set()
|
274
|
+
|
275
|
+
result = []
|
276
|
+
for parent in cls.__bases__:
|
277
|
+
if parent not in visited and parent is not object: # pragma: no cover
|
278
|
+
result.extend(self._post_order_dfs_mro(cls=parent, visited=visited, cut_off=cut_off))
|
279
|
+
|
280
|
+
if cls not in visited: # pragma: no cover
|
281
|
+
visited.add(cls)
|
282
|
+
result.append(cls)
|
283
|
+
|
284
|
+
return result
|
285
|
+
|
286
|
+
def _gather_decorated_methods(self, instance: object, attribute_name: str) -> deque[Callable[..., Any]]:
|
287
|
+
"""
|
288
|
+
Gathers decorated methods from instance.__class__ and its parent classes following the post-order DFS MRO,
|
289
|
+
returning them in a deque with the methods sorted by class hierarchy, method order, and method name.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
instance (object): The object instance whose class hierarchy is inspected.
|
293
|
+
attribute_name (str): The attribute name used to identify the methods.
|
294
|
+
|
295
|
+
Returns
|
296
|
+
deque[Callable[..., Any]]: A deque of methods sorted by class hierarchy, method order, and method name.
|
297
|
+
|
298
|
+
References:
|
299
|
+
DFS: https://en.wikipedia.org/wiki/Depth-first_search
|
300
|
+
MRO: https://docs.python.org/3/howto/mro.html
|
301
|
+
"""
|
302
|
+
|
303
|
+
def sort_key(item: tuple[str, str, Callable[..., Any]]) -> tuple[int, str, str]:
|
304
|
+
"""
|
305
|
+
Sorts the methods by class hierarchy, method order attribute, and method name.
|
306
|
+
The only global variable used is classes_names.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
item (tuple[str, str, Callable[..., Any]]): The item to sort.
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
tuple[int, str, str]: A tuple with the class index, method order, and method name.
|
313
|
+
"""
|
314
|
+
class_name, method_name, method = item
|
315
|
+
class_index = classes_names.get(class_name, 999)
|
316
|
+
order = getattr(method, '_order', method_name)
|
317
|
+
|
318
|
+
return int(class_index), order, method_name
|
319
|
+
|
320
|
+
classes = self._post_order_dfs_mro(cls=instance.__class__, cut_off=ValueObject)
|
321
|
+
classes_names = {cls.__name__: index for index, cls in enumerate(iterable=classes)}
|
322
|
+
|
323
|
+
classes_methods: deque[tuple[str, str, Callable[..., Any]]] = deque()
|
324
|
+
for cls in classes:
|
325
|
+
for method_name, method in cls.__dict__.items():
|
326
|
+
if not callable(method):
|
327
|
+
continue
|
328
|
+
|
329
|
+
if not getattr(method, attribute_name, False):
|
330
|
+
continue # only methods with the attribute
|
331
|
+
|
332
|
+
classes_methods.append((method.__qualname__.split('.')[0], method_name, method))
|
333
|
+
|
334
|
+
# sort by class hierarchy, method order attribute, and method name
|
335
|
+
return deque([method for _, _, method in sorted(classes_methods, key=sort_key)])
|
336
|
+
|
337
|
+
@property
|
338
|
+
def value(self) -> T:
|
339
|
+
"""
|
340
|
+
Returns the value object value.
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
T: The value object value.
|
344
|
+
|
345
|
+
Example:
|
346
|
+
```python
|
347
|
+
from value_object_pattern import ValueObject
|
348
|
+
|
349
|
+
|
350
|
+
class IntegerValueObject(ValueObject[int]):
|
351
|
+
pass
|
352
|
+
|
353
|
+
|
354
|
+
integer = IntegerValueObject(value=10)
|
355
|
+
print(integer.value)
|
356
|
+
# >>> 10
|
357
|
+
```
|
358
|
+
"""
|
359
|
+
return self._value
|
360
|
+
|
361
|
+
@property
|
362
|
+
def title(self) -> str:
|
363
|
+
"""
|
364
|
+
Returns the value object title.
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
str: The value object title.
|
368
|
+
|
369
|
+
Example:
|
370
|
+
```python
|
371
|
+
from value_object_pattern import ValueObject
|
372
|
+
|
373
|
+
|
374
|
+
class IntegerValueObject(ValueObject[int]):
|
375
|
+
pass
|
376
|
+
|
377
|
+
|
378
|
+
integer = IntegerValueObject(value=10)
|
379
|
+
print(integer.title)
|
380
|
+
# >>> IntegerValueObject
|
381
|
+
```
|
382
|
+
"""
|
383
|
+
return self._title
|
File without changes
|
@@ -0,0 +1,54 @@
|
|
1
|
+
from .primitives import (
|
2
|
+
AlphaStringValueObject,
|
3
|
+
AlphanumericStringValueObject,
|
4
|
+
BooleanValueObject,
|
5
|
+
BytesValueObject,
|
6
|
+
DigitStringValueObject,
|
7
|
+
EvenIntegerValueObject,
|
8
|
+
FalseValueObject,
|
9
|
+
FloatValueObject,
|
10
|
+
IntegerValueObject,
|
11
|
+
LowercaseStringValueObject,
|
12
|
+
NegativeFloatValueObject,
|
13
|
+
NegativeIntegerValueObject,
|
14
|
+
NotEmptyStringValueObject,
|
15
|
+
OddIntegerValueObject,
|
16
|
+
PositiveFloatValueObject,
|
17
|
+
PositiveIntegerValueObject,
|
18
|
+
PrintableStringValueObject,
|
19
|
+
StringValueObject,
|
20
|
+
TrimmedStringValueObject,
|
21
|
+
TrueValueObject,
|
22
|
+
UppercaseStringValueObject,
|
23
|
+
)
|
24
|
+
|
25
|
+
__all__ = (
|
26
|
+
'AlphaStringValueObject',
|
27
|
+
'AlphanumericStringValueObject',
|
28
|
+
'BooleanValueObject',
|
29
|
+
'BytesValueObject',
|
30
|
+
'DigitStringValueObject',
|
31
|
+
'EvenIntegerValueObject',
|
32
|
+
'FalseValueObject',
|
33
|
+
'FloatValueObject',
|
34
|
+
'HexadecimalStringValueObject',
|
35
|
+
'IntegerValueObject',
|
36
|
+
'LowercaseStringValueObject',
|
37
|
+
'LowercaseStringValueObject',
|
38
|
+
'NegativeFloatValueObject',
|
39
|
+
'NegativeIntegerValueObject',
|
40
|
+
'NotEmptyStringValueObject',
|
41
|
+
'NotEmptyStringValueObject',
|
42
|
+
'OddIntegerValueObject',
|
43
|
+
'PositiveFloatValueObject',
|
44
|
+
'PositiveIntegerValueObject',
|
45
|
+
'PrintableStringValueObject',
|
46
|
+
'PrintableStringValueObject',
|
47
|
+
'StringValueObject',
|
48
|
+
'StringValueObject',
|
49
|
+
'TrimmedStringValueObject',
|
50
|
+
'TrimmedStringValueObject',
|
51
|
+
'TrueValueObject',
|
52
|
+
'UppercaseStringValueObject',
|
53
|
+
'UppercaseStringValueObject',
|
54
|
+
)
|