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.
Files changed (52) hide show
  1. dddesign-0.0.1/LICENSE +21 -0
  2. dddesign-0.0.1/PKG-INFO +14 -0
  3. dddesign-0.0.1/README.md +1 -0
  4. dddesign-0.0.1/dddesign/__init__.py +1 -0
  5. dddesign-0.0.1/dddesign/structure/__init__.py +1 -0
  6. dddesign-0.0.1/dddesign/structure/applications/__init__.py +2 -0
  7. dddesign-0.0.1/dddesign/structure/applications/application.py +7 -0
  8. dddesign-0.0.1/dddesign/structure/applications/application_factory.py +230 -0
  9. dddesign-0.0.1/dddesign/structure/domains/__init__.py +1 -0
  10. dddesign-0.0.1/dddesign/structure/domains/aggregates/__init__.py +2 -0
  11. dddesign-0.0.1/dddesign/structure/domains/aggregates/aggregate.py +7 -0
  12. dddesign-0.0.1/dddesign/structure/domains/aggregates/aggregate_list_factory.py +172 -0
  13. dddesign-0.0.1/dddesign/structure/domains/constants/__init__.py +1 -0
  14. dddesign-0.0.1/dddesign/structure/domains/constants/base_enum.py +10 -0
  15. dddesign-0.0.1/dddesign/structure/domains/dto/__init__.py +1 -0
  16. dddesign-0.0.1/dddesign/structure/domains/dto/dto.py +7 -0
  17. dddesign-0.0.1/dddesign/structure/domains/entities/__init__.py +1 -0
  18. dddesign-0.0.1/dddesign/structure/domains/entities/entity.py +14 -0
  19. dddesign-0.0.1/dddesign/structure/domains/errors/__init__.py +2 -0
  20. dddesign-0.0.1/dddesign/structure/domains/errors/base_error.py +46 -0
  21. dddesign-0.0.1/dddesign/structure/domains/errors/collection_error.py +32 -0
  22. dddesign-0.0.1/dddesign/structure/domains/types/__init__.py +1 -0
  23. dddesign-0.0.1/dddesign/structure/domains/types/base_type.py +11 -0
  24. dddesign-0.0.1/dddesign/structure/domains/value_objects/__init__.py +1 -0
  25. dddesign-0.0.1/dddesign/structure/domains/value_objects/value_object.py +7 -0
  26. dddesign-0.0.1/dddesign/structure/infrastructure/__init__.py +1 -0
  27. dddesign-0.0.1/dddesign/structure/infrastructure/adapters/__init__.py +1 -0
  28. dddesign-0.0.1/dddesign/structure/infrastructure/adapters/external/__init__.py +1 -0
  29. dddesign-0.0.1/dddesign/structure/infrastructure/adapters/external/external_adapter.py +7 -0
  30. dddesign-0.0.1/dddesign/structure/infrastructure/adapters/internal/__init__.py +1 -0
  31. dddesign-0.0.1/dddesign/structure/infrastructure/adapters/internal/internal_adapter.py +7 -0
  32. dddesign-0.0.1/dddesign/structure/infrastructure/repositories/__init__.py +1 -0
  33. dddesign-0.0.1/dddesign/structure/infrastructure/repositories/repository.py +7 -0
  34. dddesign-0.0.1/dddesign/structure/services/__init__.py +1 -0
  35. dddesign-0.0.1/dddesign/structure/services/service.py +13 -0
  36. dddesign-0.0.1/dddesign/types/__init__.py +2 -0
  37. dddesign-0.0.1/dddesign/types/email_str.py +13 -0
  38. dddesign-0.0.1/dddesign/types/string_uuid.py +47 -0
  39. dddesign-0.0.1/dddesign/unittest/__init__.py +1 -0
  40. dddesign-0.0.1/dddesign/unittest/magic_mock.py +19 -0
  41. dddesign-0.0.1/dddesign/utils/__init__.py +1 -0
  42. dddesign-0.0.1/dddesign/utils/base_model/__init__.py +3 -0
  43. dddesign-0.0.1/dddesign/utils/base_model/changes_tracker.py +45 -0
  44. dddesign-0.0.1/dddesign/utils/base_model/error_instance_factory.py +15 -0
  45. dddesign-0.0.1/dddesign/utils/base_model/error_wrapper.py +27 -0
  46. dddesign-0.0.1/dddesign/utils/convertors.py +2 -0
  47. dddesign-0.0.1/dddesign/utils/function_exceptions_extractor.py +76 -0
  48. dddesign-0.0.1/dddesign/utils/module_getter.py +20 -0
  49. dddesign-0.0.1/dddesign/utils/safe_decorators.py +48 -0
  50. dddesign-0.0.1/dddesign/utils/sequence_helpers.py +8 -0
  51. dddesign-0.0.1/dddesign/utils/type_helpers.py +67 -0
  52. 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.
@@ -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
@@ -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,2 @@
1
+ from .application import Application
2
+ from .application_factory import ApplicationDependencyMapper, ApplicationFactory
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Application(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -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,2 @@
1
+ from .aggregate import Aggregate
2
+ from .aggregate_list_factory import AggregateListFactory
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Aggregate(BaseModel):
5
+ class Config:
6
+ validate_assignment = True
7
+ arbitrary_types_allowed = True
@@ -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,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class BaseEnum(Enum):
5
+ def __str__(self):
6
+ return str(self.value)
7
+
8
+ @classmethod
9
+ def has_value(cls, value) -> bool:
10
+ return value in cls._value2member_map_
@@ -0,0 +1 @@
1
+ from .dto import DataTransferObject
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class DataTransferObject(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -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,2 @@
1
+ from .base_error import BaseError
2
+ from .collection_error import CollectionError
@@ -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,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ValueObject(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -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,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ExternalAdapter(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -0,0 +1 @@
1
+ from .internal_adapter import InternalAdapter
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class InternalAdapter(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -0,0 +1 @@
1
+ from .repository import Repository
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class Repository(BaseModel):
5
+ class Config:
6
+ allow_mutation = False
7
+ arbitrary_types_allowed = True
@@ -0,0 +1 @@
1
+ from .service import Service
@@ -0,0 +1,13 @@
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Service(BaseModel, metaclass=ABCMeta):
7
+ class Config:
8
+ allow_mutation = False
9
+ arbitrary_types_allowed = True
10
+
11
+ @abstractmethod
12
+ def handle(self):
13
+ ...
@@ -0,0 +1,2 @@
1
+ from .email_str import EmailStr
2
+ from .string_uuid import StringUUID
@@ -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,3 @@
1
+ from .changes_tracker import TrackChangesMixin
2
+ from .error_instance_factory import create_pydantic_error_instance
3
+ from .error_wrapper import wrap_error
@@ -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,2 @@
1
+ def convert_camel_case_to_snake_case(string: str) -> str:
2
+ return ''.join(f'_{char.lower()}' if char.isupper() else char for char in string).lstrip('_')
@@ -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,8 @@
1
+ from typing import Any, Sequence
2
+
3
+
4
+ def get_safe_element(seq: Sequence[Any], index: int) -> Any:
5
+ try:
6
+ return seq[index]
7
+ except IndexError:
8
+ return None
@@ -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"