dddesign 0.0.1__tar.gz
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.
- dddesign-0.0.1/LICENSE +21 -0
- dddesign-0.0.1/PKG-INFO +14 -0
- dddesign-0.0.1/README.md +1 -0
- dddesign-0.0.1/dddesign/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/applications/__init__.py +2 -0
- dddesign-0.0.1/dddesign/structure/applications/application.py +7 -0
- dddesign-0.0.1/dddesign/structure/applications/application_factory.py +230 -0
- dddesign-0.0.1/dddesign/structure/domains/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/aggregates/__init__.py +2 -0
- dddesign-0.0.1/dddesign/structure/domains/aggregates/aggregate.py +7 -0
- dddesign-0.0.1/dddesign/structure/domains/aggregates/aggregate_list_factory.py +172 -0
- dddesign-0.0.1/dddesign/structure/domains/constants/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/constants/base_enum.py +10 -0
- dddesign-0.0.1/dddesign/structure/domains/dto/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/dto/dto.py +7 -0
- dddesign-0.0.1/dddesign/structure/domains/entities/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/entities/entity.py +14 -0
- dddesign-0.0.1/dddesign/structure/domains/errors/__init__.py +2 -0
- dddesign-0.0.1/dddesign/structure/domains/errors/base_error.py +46 -0
- dddesign-0.0.1/dddesign/structure/domains/errors/collection_error.py +32 -0
- dddesign-0.0.1/dddesign/structure/domains/types/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/types/base_type.py +11 -0
- dddesign-0.0.1/dddesign/structure/domains/value_objects/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/domains/value_objects/value_object.py +7 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/adapters/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/adapters/external/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/adapters/external/external_adapter.py +7 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/adapters/internal/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/adapters/internal/internal_adapter.py +7 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/repositories/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/infrastructure/repositories/repository.py +7 -0
- dddesign-0.0.1/dddesign/structure/services/__init__.py +1 -0
- dddesign-0.0.1/dddesign/structure/services/service.py +13 -0
- dddesign-0.0.1/dddesign/types/__init__.py +2 -0
- dddesign-0.0.1/dddesign/types/email_str.py +13 -0
- dddesign-0.0.1/dddesign/types/string_uuid.py +47 -0
- dddesign-0.0.1/dddesign/unittest/__init__.py +1 -0
- dddesign-0.0.1/dddesign/unittest/magic_mock.py +19 -0
- dddesign-0.0.1/dddesign/utils/__init__.py +1 -0
- dddesign-0.0.1/dddesign/utils/base_model/__init__.py +3 -0
- dddesign-0.0.1/dddesign/utils/base_model/changes_tracker.py +45 -0
- dddesign-0.0.1/dddesign/utils/base_model/error_instance_factory.py +15 -0
- dddesign-0.0.1/dddesign/utils/base_model/error_wrapper.py +27 -0
- dddesign-0.0.1/dddesign/utils/convertors.py +2 -0
- dddesign-0.0.1/dddesign/utils/function_exceptions_extractor.py +76 -0
- dddesign-0.0.1/dddesign/utils/module_getter.py +20 -0
- dddesign-0.0.1/dddesign/utils/safe_decorators.py +48 -0
- dddesign-0.0.1/dddesign/utils/sequence_helpers.py +8 -0
- dddesign-0.0.1/dddesign/utils/type_helpers.py +67 -0
- dddesign-0.0.1/pyproject.toml +22 -0
dddesign-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Artem Davydov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dddesign-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dddesign
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Domain Driven Design Library
|
|
5
|
+
Author: davyddd
|
|
6
|
+
Requires-Python: >=3.8,<3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Requires-Dist: pydantic (>=1.10,<2.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# dddesign
|
dddesign-0.0.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# dddesign
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('structure', 'types', 'unittest', 'utils')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('applications', 'domains', 'infrastructure', 'services')
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from typing import Any, Dict, Generic, NamedTuple, Optional, Tuple, Type, TypeVar, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, PrivateAttr, root_validator, validator
|
|
4
|
+
|
|
5
|
+
from dddesign.structure.applications import Application
|
|
6
|
+
from dddesign.structure.domains.constants import BaseEnum
|
|
7
|
+
from dddesign.structure.domains.errors import BaseError
|
|
8
|
+
from dddesign.structure.infrastructure.adapters.external import ExternalAdapter
|
|
9
|
+
from dddesign.structure.infrastructure.adapters.internal import InternalAdapter
|
|
10
|
+
from dddesign.structure.infrastructure.repositories import Repository
|
|
11
|
+
from dddesign.structure.services.service import Service
|
|
12
|
+
from dddesign.utils.base_model import create_pydantic_error_instance
|
|
13
|
+
from dddesign.utils.convertors import convert_camel_case_to_snake_case
|
|
14
|
+
from dddesign.utils.type_helpers import is_subclass_smart
|
|
15
|
+
|
|
16
|
+
ApplicationT = TypeVar('ApplicationT')
|
|
17
|
+
|
|
18
|
+
DependencyValue = Union[
|
|
19
|
+
InternalAdapter,
|
|
20
|
+
ExternalAdapter,
|
|
21
|
+
Repository,
|
|
22
|
+
Application,
|
|
23
|
+
Service,
|
|
24
|
+
Type[InternalAdapter],
|
|
25
|
+
Type[ExternalAdapter],
|
|
26
|
+
Type[Repository],
|
|
27
|
+
Type[Application],
|
|
28
|
+
Type[Service],
|
|
29
|
+
]
|
|
30
|
+
DEPENDENCY_VALUE_TYPES = tuple(_v for _v in getattr(DependencyValue, '__args__', ()) if isinstance(_v, type))
|
|
31
|
+
|
|
32
|
+
RequestAttributeName = str
|
|
33
|
+
RequestAttributeValue = Any
|
|
34
|
+
RequestAttributeValueCombination = Tuple[RequestAttributeValue, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RequestAttributeNotProvideError(BaseError):
|
|
38
|
+
message = 'Request attribute `{attribute_name}` not provide'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RequestAttributeValueError(BaseError):
|
|
42
|
+
message = 'Request attribute `{attribute_name}` has invalid value `{attribute_value}`'
|
|
43
|
+
status_code = 404
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RequestAttribute(NamedTuple):
|
|
47
|
+
name: RequestAttributeName
|
|
48
|
+
enum_class: Type[BaseEnum]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ApplicationDependencyMapper(BaseModel):
|
|
52
|
+
request_attribute_name: Optional[RequestAttributeName] = None
|
|
53
|
+
request_attribute_value_map: Dict[RequestAttributeValue, Any]
|
|
54
|
+
application_attribute_name: str
|
|
55
|
+
|
|
56
|
+
class Config:
|
|
57
|
+
allow_mutation = False
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _get_enum_class(request_attribute_value_map: Dict[RequestAttributeValue, DependencyValue]) -> Type[BaseEnum]:
|
|
61
|
+
return next(iter(request_attribute_value_map.keys())).__class__
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def enum_class(self) -> Type[BaseEnum]:
|
|
65
|
+
return self._get_enum_class(self.request_attribute_value_map)
|
|
66
|
+
|
|
67
|
+
def get_request_attribute_name(self) -> RequestAttributeName:
|
|
68
|
+
return self.request_attribute_name or convert_camel_case_to_snake_case(self.enum_class.__name__)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _is_dependency_value(value: Any) -> bool:
|
|
72
|
+
if isinstance(value, type):
|
|
73
|
+
return is_subclass_smart(value, *DEPENDENCY_VALUE_TYPES)
|
|
74
|
+
else:
|
|
75
|
+
return isinstance(value, DEPENDENCY_VALUE_TYPES)
|
|
76
|
+
|
|
77
|
+
@validator('request_attribute_value_map')
|
|
78
|
+
def validate_request_attribute_value_map(cls, request_attribute_value_map):
|
|
79
|
+
if len(request_attribute_value_map) == 0:
|
|
80
|
+
raise create_pydantic_error_instance(
|
|
81
|
+
base_error=ValueError,
|
|
82
|
+
code='empty_request_attribute_value_map',
|
|
83
|
+
msg_template='`request_attribute_value_map` must contain at least one element',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
enum_class = cls._get_enum_class(request_attribute_value_map)
|
|
87
|
+
if not is_subclass_smart(enum_class, BaseEnum):
|
|
88
|
+
raise create_pydantic_error_instance(
|
|
89
|
+
base_error=ValueError,
|
|
90
|
+
code='incorrect_request_attribute_value',
|
|
91
|
+
msg_template='All keys of `request_attribute_value_map` must be instances of `BaseEnum`',
|
|
92
|
+
)
|
|
93
|
+
elif not all(isinstance(key, enum_class) for key in request_attribute_value_map):
|
|
94
|
+
raise create_pydantic_error_instance(
|
|
95
|
+
base_error=ValueError,
|
|
96
|
+
code='another_types_request_attribute_values',
|
|
97
|
+
msg_template='All keys of `request_attribute_value_map` must be instances of the same enum class',
|
|
98
|
+
)
|
|
99
|
+
elif set(enum_class) ^ set(request_attribute_value_map.keys()):
|
|
100
|
+
raise create_pydantic_error_instance(
|
|
101
|
+
base_error=ValueError,
|
|
102
|
+
code='not_enough_request_attribute_values',
|
|
103
|
+
msg_template=(
|
|
104
|
+
f'All elements of `{enum_class.__name__}` enum must announced '
|
|
105
|
+
'as key in `request_attribute_value_map` attribute'
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
elif any(not cls._is_dependency_value(value) for value in request_attribute_value_map.values()):
|
|
109
|
+
raise create_pydantic_error_instance(
|
|
110
|
+
base_error=ValueError,
|
|
111
|
+
code='incorrect_type_dependency_value',
|
|
112
|
+
msg_template='All values of `request_attribute_value_map` must be instances of `DependencyValue`',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
dependency_values = tuple(request_attribute_value_map.values())
|
|
116
|
+
if dependency_values:
|
|
117
|
+
amount_equal_values = 0
|
|
118
|
+
first_dependency_value = dependency_values[0]
|
|
119
|
+
for dependency_value in dependency_values[1:]:
|
|
120
|
+
if dependency_value != first_dependency_value:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
not isinstance(dependency_value, type)
|
|
125
|
+
and dependency_value == first_dependency_value
|
|
126
|
+
and dependency_value.__class__ != first_dependency_value.__class__
|
|
127
|
+
):
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
amount_equal_values += 1
|
|
131
|
+
|
|
132
|
+
if amount_equal_values == len(dependency_values) - 1:
|
|
133
|
+
raise create_pydantic_error_instance(
|
|
134
|
+
base_error=ValueError,
|
|
135
|
+
code='not_unique_dependency_values',
|
|
136
|
+
msg_template='`request_attribute_value_map` must contain more than one unique value',
|
|
137
|
+
)
|
|
138
|
+
return request_attribute_value_map
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ApplicationFactory(BaseModel, Generic[ApplicationT]):
|
|
142
|
+
dependency_mappers: Tuple[ApplicationDependencyMapper, ...] = ()
|
|
143
|
+
application_class: Type[ApplicationT]
|
|
144
|
+
reuse_implementations: bool = True
|
|
145
|
+
|
|
146
|
+
# private attributes
|
|
147
|
+
_request_attributes: Tuple[RequestAttribute, ...] = PrivateAttr()
|
|
148
|
+
_application_implementations: Dict[RequestAttributeValueCombination, ApplicationT] = PrivateAttr()
|
|
149
|
+
|
|
150
|
+
class Config:
|
|
151
|
+
allow_mutation = False
|
|
152
|
+
|
|
153
|
+
def __init__(self, **data: Any) -> None:
|
|
154
|
+
super().__init__(**data)
|
|
155
|
+
self._request_attributes = self._get_request_attributes()
|
|
156
|
+
self._application_implementations = {}
|
|
157
|
+
|
|
158
|
+
def _get_request_attributes(self) -> Tuple[RequestAttribute, ...]:
|
|
159
|
+
return tuple(
|
|
160
|
+
RequestAttribute(name=mapper.get_request_attribute_name(), enum_class=mapper.enum_class)
|
|
161
|
+
for mapper in self.dependency_mappers
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@validator('dependency_mappers')
|
|
165
|
+
def validate_dependency_mappers(cls, dependency_mappers):
|
|
166
|
+
if len(dependency_mappers) != len({mapper.enum_class for mapper in dependency_mappers}):
|
|
167
|
+
raise ValueError('`dependency_mappers` must contain unique enum classes')
|
|
168
|
+
elif len(dependency_mappers) != len({mapper.application_attribute_name for mapper in dependency_mappers}):
|
|
169
|
+
raise ValueError('`dependency_mappers` must contain unique `application_attribute_name`')
|
|
170
|
+
elif len(dependency_mappers) != len({mapper.get_request_attribute_name() for mapper in dependency_mappers}):
|
|
171
|
+
raise ValueError('`dependency_mappers` must contain unique `request_attribute_name`')
|
|
172
|
+
|
|
173
|
+
return dependency_mappers
|
|
174
|
+
|
|
175
|
+
@root_validator
|
|
176
|
+
def validate_consistency(cls, values):
|
|
177
|
+
application_class = values['application_class']
|
|
178
|
+
dependency_mappers = values['dependency_mappers']
|
|
179
|
+
|
|
180
|
+
application_required_attribute_names = {name for name, field in application_class.__fields__.items() if field.required}
|
|
181
|
+
requested_dependency_attribute_names = {mapper.application_attribute_name for mapper in dependency_mappers}
|
|
182
|
+
|
|
183
|
+
if not (application_required_attribute_names <= requested_dependency_attribute_names):
|
|
184
|
+
raise ValueError('`dependency_mappers` must contain all required attributes of `application_class`')
|
|
185
|
+
|
|
186
|
+
return values
|
|
187
|
+
|
|
188
|
+
def _get_request_attribute_value_combination(self, **kwargs: RequestAttributeValue) -> RequestAttributeValueCombination:
|
|
189
|
+
request_attribute_value_combination = []
|
|
190
|
+
for attribute in self._request_attributes:
|
|
191
|
+
if attribute.name in kwargs:
|
|
192
|
+
try:
|
|
193
|
+
request_attribute_value_combination.append(attribute.enum_class(kwargs[attribute.name]))
|
|
194
|
+
except ValueError as err:
|
|
195
|
+
raise RequestAttributeValueError(
|
|
196
|
+
attribute_name=attribute.name, attribute_value=kwargs[attribute.name]
|
|
197
|
+
) from err
|
|
198
|
+
else:
|
|
199
|
+
raise RequestAttributeNotProvideError(attribute_name=attribute.name)
|
|
200
|
+
|
|
201
|
+
return tuple(request_attribute_value_combination)
|
|
202
|
+
|
|
203
|
+
def _get_application_implementation(self, **kwargs: RequestAttributeValue) -> ApplicationT:
|
|
204
|
+
request_attribute_value_combination = self._get_request_attribute_value_combination(**kwargs)
|
|
205
|
+
|
|
206
|
+
if request_attribute_value_combination in self._application_implementations:
|
|
207
|
+
return self._application_implementations[request_attribute_value_combination]
|
|
208
|
+
|
|
209
|
+
application_impl = self.application_class(
|
|
210
|
+
**{
|
|
211
|
+
self.dependency_mappers[index].application_attribute_name: dependency_value
|
|
212
|
+
for index in range(len(request_attribute_value_combination))
|
|
213
|
+
if (
|
|
214
|
+
dependency_value := self.dependency_mappers[index].request_attribute_value_map[
|
|
215
|
+
request_attribute_value_combination[index]
|
|
216
|
+
]
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
if self.reuse_implementations:
|
|
221
|
+
self._application_implementations[request_attribute_value_combination] = application_impl
|
|
222
|
+
|
|
223
|
+
return application_impl
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def request_attributes(self) -> Tuple[RequestAttribute, ...]:
|
|
227
|
+
return self._request_attributes
|
|
228
|
+
|
|
229
|
+
def get(self, **kwargs: RequestAttributeValue) -> ApplicationT:
|
|
230
|
+
return self._get_application_implementation(**kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('aggregates', 'constants', 'dto', 'entities', 'errors', 'value_objects')
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Generic, Iterable, List, NamedTuple, Tuple, Type, TypeVar, get_args
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, PrivateAttr, root_validator
|
|
4
|
+
|
|
5
|
+
from dddesign.structure.domains.aggregates.aggregate import Aggregate
|
|
6
|
+
from dddesign.structure.domains.entities import Entity
|
|
7
|
+
from dddesign.utils.type_helpers import get_type_without_optional
|
|
8
|
+
|
|
9
|
+
RelatedObject = Any
|
|
10
|
+
RelatedObjectId = Any
|
|
11
|
+
|
|
12
|
+
AggregateT = TypeVar('AggregateT', bound=Aggregate)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MethodArgument(NamedTuple):
|
|
16
|
+
name: str
|
|
17
|
+
argument_class: Any
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def factory(cls, name: str, argument_class: Any) -> 'MethodArgument':
|
|
21
|
+
return cls(name, get_type_without_optional(argument_class))
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_iterable(self) -> bool:
|
|
25
|
+
return hasattr(getattr(self.argument_class, '__origin__', self.argument_class), '__iter__')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AggregateDependencyMapper(BaseModel):
|
|
29
|
+
entity_attribute_name: str
|
|
30
|
+
aggregate_attribute_name: str
|
|
31
|
+
|
|
32
|
+
method_getter: Callable
|
|
33
|
+
method_extra_arguments: Dict[str, Any] = Field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
_method_related_object_id_argument: MethodArgument = PrivateAttr()
|
|
36
|
+
_related_object_id_attribute_name: str = PrivateAttr()
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
allow_mutation = False
|
|
40
|
+
arbitrary_types_allowed = True
|
|
41
|
+
|
|
42
|
+
def __init__(self, **data: Any) -> None:
|
|
43
|
+
super().__init__(**data)
|
|
44
|
+
self._method_related_object_id_argument = self._get_method_related_object_id_argument()
|
|
45
|
+
self._related_object_id_attribute_name = self._get_related_object_id_attribute_name()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def method_related_object_id_argument(self) -> MethodArgument:
|
|
49
|
+
return self._method_related_object_id_argument
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def related_object_id_attribute_name(self) -> str:
|
|
53
|
+
return self._related_object_id_attribute_name
|
|
54
|
+
|
|
55
|
+
def _get_method_related_object_id_argument(self) -> MethodArgument:
|
|
56
|
+
argument_name, argument_class = None, None
|
|
57
|
+
for _argument_name, _argument_class in self.method_getter.__annotations__.items():
|
|
58
|
+
if _argument_name in self.method_extra_arguments or _argument_name == 'return':
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if argument_name and argument_class:
|
|
62
|
+
raise ValueError('Method must have only one related argument')
|
|
63
|
+
|
|
64
|
+
argument_name, argument_class = _argument_name, _argument_class
|
|
65
|
+
|
|
66
|
+
if not (argument_name and argument_class):
|
|
67
|
+
raise ValueError('Method must have one related argument')
|
|
68
|
+
|
|
69
|
+
return MethodArgument.factory(argument_name, argument_class)
|
|
70
|
+
|
|
71
|
+
def _get_related_object_id_attribute_name(self) -> str:
|
|
72
|
+
related_object_class = get_type_without_optional(self.method_getter.__annotations__.get('return'))
|
|
73
|
+
if not related_object_class:
|
|
74
|
+
raise ValueError('Method must have return annotation')
|
|
75
|
+
|
|
76
|
+
related_object_id_attribute_class = self._method_related_object_id_argument.argument_class
|
|
77
|
+
|
|
78
|
+
if self._method_related_object_id_argument.is_iterable:
|
|
79
|
+
related_object_class = get_type_without_optional(get_args(related_object_class)[0])
|
|
80
|
+
related_object_id_attribute_class = get_type_without_optional(get_args(related_object_id_attribute_class)[0])
|
|
81
|
+
|
|
82
|
+
return {value: key for key, value in related_object_class.__annotations__.items()}[related_object_id_attribute_class]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AggregateListFactory(BaseModel, Generic[AggregateT]):
|
|
86
|
+
aggregate_class: Type[AggregateT]
|
|
87
|
+
aggregate_entity_attribute_name: str
|
|
88
|
+
dependency_mappers: Tuple[AggregateDependencyMapper, ...]
|
|
89
|
+
|
|
90
|
+
class Config:
|
|
91
|
+
allow_mutation = False
|
|
92
|
+
arbitrary_types_allowed = True
|
|
93
|
+
|
|
94
|
+
@root_validator
|
|
95
|
+
def validate_consistency(cls, values):
|
|
96
|
+
aggregate_class = values['aggregate_class']
|
|
97
|
+
aggregate_entity_attribute_name = values['aggregate_entity_attribute_name']
|
|
98
|
+
dependency_mappers = values['dependency_mappers']
|
|
99
|
+
|
|
100
|
+
if aggregate_entity_attribute_name not in aggregate_class.__annotations__:
|
|
101
|
+
raise ValueError(f"The aggregate class doesn't have `{aggregate_entity_attribute_name}` attribute")
|
|
102
|
+
|
|
103
|
+
if any(dependency.aggregate_attribute_name not in aggregate_class.__annotations__ for dependency in dependency_mappers):
|
|
104
|
+
raise ValueError("The aggregate class doesn't have a attribute from some declared dependency")
|
|
105
|
+
|
|
106
|
+
entity_class = get_type_without_optional(aggregate_class.__annotations__[aggregate_entity_attribute_name])
|
|
107
|
+
|
|
108
|
+
if any(dependency.entity_attribute_name not in entity_class.__annotations__ for dependency in dependency_mappers):
|
|
109
|
+
raise ValueError("The entity class doesn't have a attribute from some declared dependency")
|
|
110
|
+
|
|
111
|
+
return values
|
|
112
|
+
|
|
113
|
+
def create_list(self, entities: List[Entity]) -> List[AggregateT]:
|
|
114
|
+
dependency_related_object_ids_map: Dict[int, Tuple[RelatedObjectId, ...]] = {}
|
|
115
|
+
for dependency_item, dependency in enumerate(self.dependency_mappers):
|
|
116
|
+
_related_object_ids: List[RelatedObjectId] = []
|
|
117
|
+
for entity in entities:
|
|
118
|
+
related_value = getattr(entity, dependency.entity_attribute_name)
|
|
119
|
+
if isinstance(related_value, Iterable):
|
|
120
|
+
_related_object_ids.extend(related_value)
|
|
121
|
+
else:
|
|
122
|
+
_related_object_ids.append(related_value)
|
|
123
|
+
|
|
124
|
+
dependency_related_object_ids_map[dependency_item] = tuple(set(_related_object_ids))
|
|
125
|
+
|
|
126
|
+
dependency_related_object_map: Dict[int, Dict[RelatedObjectId, RelatedObject]] = {}
|
|
127
|
+
for dependency_item, related_object_ids in dependency_related_object_ids_map.items():
|
|
128
|
+
dependency = self.dependency_mappers[dependency_item]
|
|
129
|
+
|
|
130
|
+
if dependency.method_related_object_id_argument.is_iterable:
|
|
131
|
+
related_objects = dependency.method_getter(
|
|
132
|
+
**{
|
|
133
|
+
dependency.method_related_object_id_argument.name: related_object_ids,
|
|
134
|
+
**dependency.method_extra_arguments,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
related_objects = []
|
|
139
|
+
for related_object_id in related_object_ids:
|
|
140
|
+
related_object = dependency.method_getter(
|
|
141
|
+
**{
|
|
142
|
+
dependency.method_related_object_id_argument.name: related_object_id,
|
|
143
|
+
**dependency.method_extra_arguments,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
if related_object is not None:
|
|
147
|
+
related_objects.append(related_object)
|
|
148
|
+
|
|
149
|
+
related_object_map: Dict[RelatedObjectId, RelatedObject] = {
|
|
150
|
+
getattr(related_object, dependency.related_object_id_attribute_name): related_object
|
|
151
|
+
for related_object in related_objects
|
|
152
|
+
}
|
|
153
|
+
dependency_related_object_map[dependency_item] = related_object_map
|
|
154
|
+
|
|
155
|
+
aggregates: List[AggregateT] = []
|
|
156
|
+
for entity in entities:
|
|
157
|
+
aggregate_init: Dict[str, Any] = {self.aggregate_entity_attribute_name: entity}
|
|
158
|
+
for dependency_item, dependency in enumerate(self.dependency_mappers):
|
|
159
|
+
if dependency.method_related_object_id_argument.is_iterable:
|
|
160
|
+
aggregate_init[dependency.aggregate_attribute_name] = tuple(
|
|
161
|
+
dependency_related_object_map[dependency_item].get(related_object_id)
|
|
162
|
+
for related_object_id in getattr(entity, dependency.entity_attribute_name)
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
aggregate_init[dependency.aggregate_attribute_name] = dependency_related_object_map[dependency_item].get(
|
|
166
|
+
getattr(entity, dependency.entity_attribute_name)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
aggregate = self.aggregate_class(**aggregate_init)
|
|
170
|
+
aggregates.append(aggregate)
|
|
171
|
+
|
|
172
|
+
return aggregates
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base_enum import BaseEnum
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .dto import DataTransferObject
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .entity import Entity
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from dddesign.structure.domains.dto.dto import DataTransferObject
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Entity(BaseModel):
|
|
7
|
+
class Config:
|
|
8
|
+
validate_assignment = True
|
|
9
|
+
arbitrary_types_allowed = True
|
|
10
|
+
|
|
11
|
+
def update(self, data: DataTransferObject):
|
|
12
|
+
for field_name, value in data.dict(exclude_unset=True).items():
|
|
13
|
+
if field_name in self.__fields__:
|
|
14
|
+
setattr(self, field_name, value)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from dddesign.utils.convertors import convert_camel_case_to_snake_case
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseError(Exception):
|
|
7
|
+
message: str
|
|
8
|
+
error_code: str
|
|
9
|
+
status_code: int
|
|
10
|
+
field_name: Optional[str]
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: Optional[str] = None,
|
|
15
|
+
error_code: Optional[str] = None,
|
|
16
|
+
status_code: Optional[int] = None,
|
|
17
|
+
field_name: Optional[str] = None,
|
|
18
|
+
**kwargs: Any,
|
|
19
|
+
):
|
|
20
|
+
message = message or getattr(self, 'message', None)
|
|
21
|
+
if not message:
|
|
22
|
+
raise ValueError('Field `message` is required')
|
|
23
|
+
self.message = message.format(**kwargs)
|
|
24
|
+
|
|
25
|
+
self.error_code = error_code or getattr(self, 'error_code', None) or self.get_error_code()
|
|
26
|
+
|
|
27
|
+
self.status_code = status_code or getattr(self, 'status_code', None) or 400
|
|
28
|
+
|
|
29
|
+
self.field_name = field_name or getattr(self, 'field_name', None)
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return (
|
|
33
|
+
f'{self.__class__.__name__}(\n'
|
|
34
|
+
f" message='{self.message}',\n"
|
|
35
|
+
f" error_code='{self.error_code}',\n"
|
|
36
|
+
f" status_code='{self.status_code}',\n"
|
|
37
|
+
f" field_name='{self.field_name}'\n"
|
|
38
|
+
')'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return self.__str__()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_error_code(cls) -> str:
|
|
46
|
+
return convert_camel_case_to_snake_case(cls.__name__)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from dddesign.structure.domains.errors.base_error import BaseError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CollectionError(Exception):
|
|
7
|
+
errors: List[BaseError]
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.errors = []
|
|
11
|
+
self._index = 0
|
|
12
|
+
|
|
13
|
+
def __bool__(self) -> bool:
|
|
14
|
+
return bool(self.errors)
|
|
15
|
+
|
|
16
|
+
def __iter__(self):
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def __next__(self) -> BaseError:
|
|
20
|
+
if self._index >= len(self.errors):
|
|
21
|
+
self._index = 0
|
|
22
|
+
raise StopIteration
|
|
23
|
+
|
|
24
|
+
result = self.errors[self._index]
|
|
25
|
+
self._index += 1
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
def add(self, error: BaseError):
|
|
29
|
+
if not isinstance(error, BaseError):
|
|
30
|
+
raise TypeError('`error` must be an instance of `BaseError`')
|
|
31
|
+
|
|
32
|
+
self.errors.append(error)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base_type import BaseType
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from typing import Generator
|
|
3
|
+
|
|
4
|
+
from pydantic.typing import AnyCallable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseType(metaclass=ABCMeta):
|
|
8
|
+
@classmethod
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def __get_validators__(cls) -> Generator[AnyCallable, None, None]:
|
|
11
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .value_object import ValueObject
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('adapters', 'repositories')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('external', 'internal')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .external_adapter import ExternalAdapter
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .internal_adapter import InternalAdapter
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .repository import Repository
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .service import Service
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic.networks import EmailStr as PydanticEmailStr
|
|
4
|
+
|
|
5
|
+
from dddesign.structure.domains.types.base_type import BaseType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailStr(PydanticEmailStr, BaseType):
|
|
9
|
+
@classmethod
|
|
10
|
+
def validate(cls, value: Any) -> 'EmailStr':
|
|
11
|
+
if isinstance(value, str):
|
|
12
|
+
value = value.lower().strip()
|
|
13
|
+
return cls(super().validate(value))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Any, Callable, Generator
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from dddesign.structure.domains.types import BaseType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StringUUID(str, BaseType):
|
|
8
|
+
"""
|
|
9
|
+
A UUID that is serialized as a string.
|
|
10
|
+
|
|
11
|
+
The field will be validated as a UUID and then serialized (and stored internally) as a string.
|
|
12
|
+
This is safe to use in cases where json.dumps() is utilized for serialization.
|
|
13
|
+
You will not get punished by `TypeError: Object of type UUID is not JSON serializable` for this anymore.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```python
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
class MyModel(BaseModel):
|
|
20
|
+
id: StringUUID
|
|
21
|
+
|
|
22
|
+
my_model = MyModel(id='123e4567-e89b-12d3-a456-426655440000')
|
|
23
|
+
|
|
24
|
+
json.dumps(my_model.dict()) # it's working
|
|
25
|
+
|
|
26
|
+
# You can compare it with a string or a UUID
|
|
27
|
+
my_model.id == '123e4567-e89b-12d3-a456-426655440000'
|
|
28
|
+
my_model.id == UUID('123e4567-e89b-12d3-a456-426655440000')
|
|
29
|
+
```
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __eq__(self, other: Any) -> bool:
|
|
33
|
+
if isinstance(other, UUID):
|
|
34
|
+
other = type(self)(other)
|
|
35
|
+
return super().__eq__(other)
|
|
36
|
+
|
|
37
|
+
def __hash__(self) -> int:
|
|
38
|
+
return hash(str(self))
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def __get_validators__(cls) -> Generator[Callable[[Any], 'StringUUID'], None, None]:
|
|
42
|
+
yield cls.validate
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def validate(cls, value: Any) -> 'StringUUID':
|
|
46
|
+
value = UUID(str(value))
|
|
47
|
+
return cls(value)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('magic_mock',)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from unittest.mock import MagicMock as OriginalMagicMock
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from dddesign.utils.sequence_helpers import get_safe_element
|
|
6
|
+
from dddesign.utils.type_helpers import is_subclass_smart
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MagicMock(OriginalMagicMock):
|
|
10
|
+
def __init__(self, *args, **kwargs):
|
|
11
|
+
super().__init__(*args, **kwargs)
|
|
12
|
+
class_type = (
|
|
13
|
+
get_safe_element(args, 0) # spec
|
|
14
|
+
or get_safe_element(args, 5) # spec_set
|
|
15
|
+
or kwargs.get('spec')
|
|
16
|
+
or kwargs.get('spec_set')
|
|
17
|
+
)
|
|
18
|
+
if is_subclass_smart(class_type, BaseModel):
|
|
19
|
+
self._copy_and_set_values.return_value = self # some magic
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ('base_model', 'convertors', 'function_exceptions_extractor', 'module_getter', 'sequence_helpers', 'type_helpers')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Dict, Generator, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, PrivateAttr
|
|
4
|
+
|
|
5
|
+
UndefinedValue = object()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrackChangesMixin(BaseModel):
|
|
9
|
+
_initial_state: Dict[str, Any] = PrivateAttr()
|
|
10
|
+
|
|
11
|
+
def __init__(self, **data: Any) -> None:
|
|
12
|
+
super().__init__(**data)
|
|
13
|
+
self.update_initial_state()
|
|
14
|
+
|
|
15
|
+
def _get_changed_fields(self) -> Generator[str, None, None]:
|
|
16
|
+
for field, value in self.dict().items():
|
|
17
|
+
if self._initial_state.get(field, UndefinedValue) != value:
|
|
18
|
+
yield field
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def has_changed(self) -> bool:
|
|
22
|
+
return next(self._get_changed_fields(), None) is not None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def changed_fields(self) -> Tuple[str, ...]:
|
|
26
|
+
return tuple(self._get_changed_fields())
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def changed_data(self) -> Dict[str, Any]:
|
|
30
|
+
return {field: getattr(self, field, None) for field in self._get_changed_fields()}
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def diffs(self) -> Dict[str, Tuple[Any, Any]]:
|
|
34
|
+
return {field: (self._initial_state[field], getattr(self, field, None)) for field in self._get_changed_fields()}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def initial_state(self) -> Dict[str, Any]:
|
|
38
|
+
return self._initial_state
|
|
39
|
+
|
|
40
|
+
def update_initial_state(self, fields: Optional[tuple] = None):
|
|
41
|
+
if fields:
|
|
42
|
+
for field in fields:
|
|
43
|
+
self._initial_state[field] = self.dict()[field]
|
|
44
|
+
else:
|
|
45
|
+
self._initial_state = self.dict()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Optional, Type, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic.errors import PydanticErrorMixin
|
|
4
|
+
|
|
5
|
+
BaseError = TypeVar('BaseError', bound=Exception)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_pydantic_error_instance(
|
|
9
|
+
base_error: Type[BaseError], code: str, msg_template: str, context: Optional[dict] = None
|
|
10
|
+
) -> BaseError:
|
|
11
|
+
_class = type('PydanticError', (PydanticErrorMixin, base_error), {'code': code, 'msg_template': msg_template})
|
|
12
|
+
if isinstance(context, dict):
|
|
13
|
+
return _class(**context)
|
|
14
|
+
else:
|
|
15
|
+
return _class()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from dddesign.structure.domains.errors import BaseError, CollectionError
|
|
6
|
+
|
|
7
|
+
CONTEXT_MESSAGES_PARAM = '__messages__'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def wrap_error(error: ValidationError) -> CollectionError:
|
|
11
|
+
if not isinstance(error, ValidationError):
|
|
12
|
+
raise TypeError('`exception` must be an instance of `pydantic.ValidationError`')
|
|
13
|
+
|
|
14
|
+
errors = CollectionError()
|
|
15
|
+
|
|
16
|
+
for _error in error.errors():
|
|
17
|
+
field_name: Optional[str] = '.'.join(str(item) for item in _error['loc'])
|
|
18
|
+
if field_name == '__root_validator__':
|
|
19
|
+
field_name = None
|
|
20
|
+
|
|
21
|
+
if 'ctx' in _error and CONTEXT_MESSAGES_PARAM in _error['ctx']:
|
|
22
|
+
for msg in _error['ctx'][CONTEXT_MESSAGES_PARAM]:
|
|
23
|
+
errors.add(BaseError(message=msg, field_name=field_name))
|
|
24
|
+
else:
|
|
25
|
+
errors.add(BaseError(message=_error['msg'], field_name=field_name))
|
|
26
|
+
|
|
27
|
+
return errors
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import builtins
|
|
3
|
+
import inspect
|
|
4
|
+
import textwrap
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import Any, Callable, Dict, Generator, Optional, Tuple, Type
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, validator
|
|
9
|
+
|
|
10
|
+
from dddesign.utils.module_getter import get_module
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExceptionInfo(BaseModel):
|
|
14
|
+
exception_class: Type[Exception]
|
|
15
|
+
args: Tuple[Any, ...] = Field(default_factory=tuple)
|
|
16
|
+
kwargs: Dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
@validator('args')
|
|
19
|
+
def validate_args(cls, value):
|
|
20
|
+
return tuple(_v.value if isinstance(_v, ast.Constant) else None for _v in value)
|
|
21
|
+
|
|
22
|
+
@validator('kwargs')
|
|
23
|
+
def validate_kwargs(cls, value):
|
|
24
|
+
return {_k: _v.value if isinstance(_v, ast.Constant) else None for _k, _v in value.items()}
|
|
25
|
+
|
|
26
|
+
def get_kwargs(self):
|
|
27
|
+
annotations = tuple(
|
|
28
|
+
argument_name
|
|
29
|
+
for argument_name in inspect.signature(self.exception_class.__init__).parameters
|
|
30
|
+
if argument_name not in ('self', 'args', 'kwargs')
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
**{attribute_name: self.args[item] for item, attribute_name in enumerate(annotations[: len(self.args)])},
|
|
35
|
+
**self.kwargs,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def get_exception_instance(self):
|
|
39
|
+
kwargs = {k: f'<{k}>' if v is None else v for k, v in self.get_kwargs().items()}
|
|
40
|
+
return self.exception_class(**kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_function_exceptions(func: Callable) -> Generator[ExceptionInfo, None, None]:
|
|
44
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
45
|
+
tree = ast.parse(source)
|
|
46
|
+
|
|
47
|
+
for node in ast.walk(tree):
|
|
48
|
+
if isinstance(node, ast.Raise):
|
|
49
|
+
exception_name: Optional[str] = None
|
|
50
|
+
exception_args: Tuple[Any, ...] = ()
|
|
51
|
+
exception_kwargs: Dict[str, Any] = {}
|
|
52
|
+
|
|
53
|
+
if isinstance(node.exc, ast.Call):
|
|
54
|
+
exception_name = ast.unparse(node.exc.func)
|
|
55
|
+
exception_args = tuple(node.exc.args)
|
|
56
|
+
exception_kwargs = {kw.arg: kw.value for kw in node.exc.keywords if isinstance(kw.arg, str)}
|
|
57
|
+
elif isinstance(node.exc, (ast.Attribute, ast.Name)):
|
|
58
|
+
exception_name = ast.unparse(node.exc)
|
|
59
|
+
|
|
60
|
+
if not exception_name:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
exception_name_slices = exception_name.split('.')
|
|
64
|
+
class_name = exception_name_slices[-1]
|
|
65
|
+
sub_modules = exception_name_slices[:-1]
|
|
66
|
+
|
|
67
|
+
exception_module = get_module(import_module(func.__module__), sub_modules)
|
|
68
|
+
|
|
69
|
+
exception_class: Optional[Type[Exception]] = getattr(exception_module, class_name, None)
|
|
70
|
+
if exception_class is None and hasattr(builtins, exception_name):
|
|
71
|
+
exception_class = getattr(builtins, exception_name)
|
|
72
|
+
|
|
73
|
+
if exception_class is None:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
yield ExceptionInfo(exception_class=exception_class, args=exception_args, kwargs=exception_kwargs)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from types import ModuleType
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_module(module: ModuleType, sub_modules: List[str]) -> ModuleType:
|
|
6
|
+
if not module:
|
|
7
|
+
raise ValueError('Argument `module` is required')
|
|
8
|
+
elif not isinstance(sub_modules, list):
|
|
9
|
+
raise ValueError('Argument `sub_modules` must be a list of strings')
|
|
10
|
+
|
|
11
|
+
if len(sub_modules) == 0:
|
|
12
|
+
return module
|
|
13
|
+
|
|
14
|
+
sub_module = sub_modules.pop(0)
|
|
15
|
+
|
|
16
|
+
if hasattr(module, sub_module):
|
|
17
|
+
module = getattr(module, sub_module)
|
|
18
|
+
return get_module(module, sub_modules)
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError(f'Module {sub_module} not found in {module.__name__}')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, Callable, Optional, Tuple, Type
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safe_call(
|
|
9
|
+
func: Optional[Callable] = None,
|
|
10
|
+
capture_exception: bool = True,
|
|
11
|
+
default_result: Optional[Any] = None,
|
|
12
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
13
|
+
) -> Callable:
|
|
14
|
+
if func is None:
|
|
15
|
+
return lambda _func: safe_call(func=_func, capture_exception=capture_exception, default_result=default_result)
|
|
16
|
+
|
|
17
|
+
@wraps(func)
|
|
18
|
+
def wrapped_func(*args, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
result = func(*args, **kwargs)
|
|
21
|
+
except exceptions as error:
|
|
22
|
+
if capture_exception:
|
|
23
|
+
logger.exception(error)
|
|
24
|
+
|
|
25
|
+
result = default_result
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
return wrapped_func
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def retry_once_after_exception(
|
|
33
|
+
func: Optional[Callable] = None, capture_exception: bool = True, exceptions: Tuple[Type[Exception], ...] = (Exception,)
|
|
34
|
+
) -> Callable:
|
|
35
|
+
if func is None:
|
|
36
|
+
return lambda _func: retry_once_after_exception(func=_func, exceptions=exceptions)
|
|
37
|
+
|
|
38
|
+
@wraps(func)
|
|
39
|
+
def wrapped_func(*args, **kwargs):
|
|
40
|
+
try:
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
except exceptions as error:
|
|
43
|
+
if capture_exception:
|
|
44
|
+
logger.exception(error)
|
|
45
|
+
|
|
46
|
+
return func(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return wrapped_func
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
5
|
+
from types import CodeType, FunctionType
|
|
6
|
+
from typing import Any, Callable, Optional, Type, Union, get_args
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
PythonType = Union[str, float, bool, bytes, int, dict, date, time, datetime, timedelta, UUID, Decimal, IPv4Address, IPv6Address]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_subclass_smart(class_type: Any, *base_types: Any) -> bool:
|
|
13
|
+
class_type = getattr(class_type, '__origin__', class_type)
|
|
14
|
+
return inspect.isclass(class_type) and issubclass(class_type, base_types)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_type_without_optional(class_type: Any) -> Any:
|
|
18
|
+
return get_args(class_type)[0] if type(None) in get_args(class_type) else class_type
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_origin_class_of_method(cls: Any, method_name: str):
|
|
22
|
+
for base in inspect.getmro(cls):
|
|
23
|
+
if method_name in base.__dict__:
|
|
24
|
+
return base
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_python_type(type_) -> bool:
|
|
28
|
+
return is_subclass_smart(type_, getattr(PythonType, '__args__', None))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_python_type(type_) -> Type[PythonType]:
|
|
32
|
+
if hasattr(type_, '__supertype__'):
|
|
33
|
+
return get_python_type(type_.__supertype__)
|
|
34
|
+
elif hasattr(type_, '__origin__'):
|
|
35
|
+
return get_python_type(type_.__args__[0])
|
|
36
|
+
elif hasattr(type_, '__bases__'):
|
|
37
|
+
next_type = type_.__bases__[0]
|
|
38
|
+
if _is_python_type(type_) and not _is_python_type(next_type):
|
|
39
|
+
return type_
|
|
40
|
+
else:
|
|
41
|
+
return get_python_type(next_type)
|
|
42
|
+
else:
|
|
43
|
+
raise TypeError(f'Unknown type: {type(type_)}')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_new_function(func: Callable, new_name: Optional[str] = None) -> Callable:
|
|
47
|
+
base_code = func.__code__
|
|
48
|
+
func_name = new_name or base_code.co_name
|
|
49
|
+
new_code = CodeType(
|
|
50
|
+
base_code.co_argcount,
|
|
51
|
+
base_code.co_posonlyargcount,
|
|
52
|
+
base_code.co_kwonlyargcount,
|
|
53
|
+
base_code.co_nlocals,
|
|
54
|
+
base_code.co_stacksize,
|
|
55
|
+
base_code.co_flags,
|
|
56
|
+
base_code.co_code,
|
|
57
|
+
base_code.co_consts,
|
|
58
|
+
base_code.co_names,
|
|
59
|
+
base_code.co_varnames,
|
|
60
|
+
base_code.co_filename,
|
|
61
|
+
func_name,
|
|
62
|
+
base_code.co_firstlineno,
|
|
63
|
+
base_code.co_lnotab,
|
|
64
|
+
base_code.co_freevars,
|
|
65
|
+
base_code.co_cellvars,
|
|
66
|
+
)
|
|
67
|
+
return FunctionType(new_code, func.__globals__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "dddesign"
|
|
3
|
+
description = "Domain Driven Design Library"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
authors = ["davyddd"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "dddesign"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = ">=3.8,<3.11"
|
|
11
|
+
pydantic = "^1.10"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.group.dev.dependencies]
|
|
14
|
+
mypy = "1.1.1"
|
|
15
|
+
ruff = "0.1.7"
|
|
16
|
+
ipdb = "0.13.9"
|
|
17
|
+
ipython = "8.12.3"
|
|
18
|
+
pytest = "8.1.1"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=1.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|