explicit-python 2.2.1__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.
- explicit/adapters/__init__.py +0 -0
- explicit/adapters/db.py +63 -0
- explicit/adapters/fs.py +40 -0
- explicit/adapters/messaging.py +14 -0
- explicit/contrib/__init__.py +0 -0
- explicit/contrib/domain/__init__.py +0 -0
- explicit/contrib/domain/model/__init__.py +9 -0
- explicit/contrib/domain/model/fields.py +31 -0
- explicit/contrib/messagebus/__init__.py +0 -0
- explicit/contrib/messagebus/event_registry.py +56 -0
- explicit/domain/__init__.py +28 -0
- explicit/domain/factories.py +37 -0
- explicit/domain/fields.py +9 -0
- explicit/domain/model.py +152 -0
- explicit/domain/types.py +36 -0
- explicit/domain/validation/__init__.py +0 -0
- explicit/domain/validation/exceptions.py +148 -0
- explicit/messagebus/__init__.py +8 -0
- explicit/messagebus/commands.py +100 -0
- explicit/messagebus/dependency_injection.py +48 -0
- explicit/messagebus/events.py +121 -0
- explicit/messagebus/messagebus.py +90 -0
- explicit/messagebus/queries.py +51 -0
- explicit/py.typed +0 -0
- explicit/unit_of_work.py +184 -0
- explicit_python-2.2.1.dist-info/METADATA +298 -0
- explicit_python-2.2.1.dist-info/RECORD +30 -0
- explicit_python-2.2.1.dist-info/WHEEL +5 -0
- explicit_python-2.2.1.dist-info/dependency_links.txt +1 -0
- explicit_python-2.2.1.dist-info/top_level.txt +1 -0
|
File without changes
|
explicit/adapters/db.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Абстракции адаптера СУБД."""
|
|
2
|
+
from abc import ABCMeta
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractSpecification(metaclass=ABCMeta):
|
|
8
|
+
|
|
9
|
+
"""Набор условий для проверки соответствия объекта этим условиям."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def as_filter(self):
|
|
13
|
+
"""Представление в виде фильтра."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def is_satisfied_by(self, instance) -> bool:
|
|
17
|
+
"""Соответствует ли объект условиям."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbstractRepository(metaclass=ABCMeta):
|
|
21
|
+
|
|
22
|
+
"""Репозиторий - абстракция над хранилищем данных (БД).
|
|
23
|
+
|
|
24
|
+
Должен содержать методы получения объектов со всеми вариантами выборки:
|
|
25
|
+
* Все записи
|
|
26
|
+
* По идентификатору
|
|
27
|
+
* По условию (фильтрация)
|
|
28
|
+
* Стандартные наборы (администраторы, министерства,
|
|
29
|
+
виды занятий для очного обучения и т.п.)
|
|
30
|
+
|
|
31
|
+
.. note::
|
|
32
|
+
Репозиторий должен возвращать объекты модели модели предметной области,
|
|
33
|
+
но не хранилища.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def _to_domain(self, dbinstance: Any) -> Any:
|
|
38
|
+
"""Преобразование из объекта БД в объект модели."""
|
|
39
|
+
raise NotImplementedError()
|
|
40
|
+
|
|
41
|
+
def _to_db(self, modelinstance):
|
|
42
|
+
"""Преобразование из объекта модели в объект БД."""
|
|
43
|
+
raise NotImplementedError()
|
|
44
|
+
|
|
45
|
+
def add(self, obj):
|
|
46
|
+
"""Добавление объекта."""
|
|
47
|
+
raise NotImplementedError()
|
|
48
|
+
|
|
49
|
+
def update(self, obj):
|
|
50
|
+
"""Обновление объекта."""
|
|
51
|
+
raise NotImplementedError()
|
|
52
|
+
|
|
53
|
+
def delete(self, obj):
|
|
54
|
+
"""Удаление объекта."""
|
|
55
|
+
raise NotImplementedError()
|
|
56
|
+
|
|
57
|
+
def get_all_objects(self):
|
|
58
|
+
"""Возвращает все объекты."""
|
|
59
|
+
raise NotImplementedError()
|
|
60
|
+
|
|
61
|
+
def get_object_by_id(self, identifier):
|
|
62
|
+
"""Возвращает объект по его идентификатору."""
|
|
63
|
+
raise NotImplementedError()
|
explicit/adapters/fs.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Абстракции адаптера к хранилищу файлов."""
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AbstractAdapter(ABC):
|
|
6
|
+
|
|
7
|
+
"""Абстрактный адаптер к хранилищу файлов.
|
|
8
|
+
|
|
9
|
+
.. code-block:: python
|
|
10
|
+
|
|
11
|
+
from ..adapters.fs import adapter as fs
|
|
12
|
+
|
|
13
|
+
content = request.FILES['file_photo']
|
|
14
|
+
path = fs.save(content.name, content)
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def open(self, name, mode='rb'):
|
|
19
|
+
raise NotImplementedError()
|
|
20
|
+
|
|
21
|
+
def save(self, name, content, max_length=None):
|
|
22
|
+
raise NotImplementedError()
|
|
23
|
+
|
|
24
|
+
def delete(self, name):
|
|
25
|
+
raise NotImplementedError()
|
|
26
|
+
|
|
27
|
+
def exists(self, name):
|
|
28
|
+
raise NotImplementedError()
|
|
29
|
+
|
|
30
|
+
def size(self, name):
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
def generate_filename(self, filename):
|
|
34
|
+
raise NotImplementedError()
|
|
35
|
+
|
|
36
|
+
def path(self, name):
|
|
37
|
+
raise NotImplementedError()
|
|
38
|
+
|
|
39
|
+
def url(self, name):
|
|
40
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Абстракции адаптера обмена сообщениями."""
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractAdapter(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def publish(self, message, *args, **kwargs):
|
|
10
|
+
raise NotImplementedError()
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def subscribe(self, *args, **kwargs):
|
|
14
|
+
raise NotImplementedError()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Поля модели предметной области."""
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from pydantic.fields import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def identifier(**kwargs):
|
|
8
|
+
return Field(
|
|
9
|
+
None,
|
|
10
|
+
title='Идентификатор',
|
|
11
|
+
description=(
|
|
12
|
+
'Идентификатор записи. '
|
|
13
|
+
'Может отсутствовать у создаваемых объектов, '
|
|
14
|
+
'но обязан присутствовать у существующих.'
|
|
15
|
+
),
|
|
16
|
+
gt=0,
|
|
17
|
+
**kwargs
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def uuid_identifier(**kwargs):
|
|
22
|
+
return Field(
|
|
23
|
+
default_factory=uuid4,
|
|
24
|
+
title='Идентификатор UUID',
|
|
25
|
+
description=(
|
|
26
|
+
'Идентификатор записи. '
|
|
27
|
+
'Может отсутствовать у создаваемых объектов, '
|
|
28
|
+
'но обязан присутствовать у существующих.'
|
|
29
|
+
),
|
|
30
|
+
**kwargs
|
|
31
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Реестр событий по топикам."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from typing import Type
|
|
7
|
+
from typing import Union
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from explicit.messagebus.events import Event
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
logger.setLevel(logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Message:
|
|
20
|
+
topic: str
|
|
21
|
+
"""Наименование топика."""
|
|
22
|
+
|
|
23
|
+
type: str
|
|
24
|
+
"""Наименование типа события."""
|
|
25
|
+
|
|
26
|
+
body: dict
|
|
27
|
+
"""Тело события."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Registry:
|
|
31
|
+
|
|
32
|
+
"""Реестр событий по топикам."""
|
|
33
|
+
|
|
34
|
+
_registry: Dict[str, list[Type['Event']]]
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self._registry = defaultdict(list)
|
|
38
|
+
|
|
39
|
+
def register(self, topic: str, *events: Type['Event']) -> None:
|
|
40
|
+
"""Регистрирует сопоставление топика с типом события."""
|
|
41
|
+
self._registry[topic].extend(events)
|
|
42
|
+
|
|
43
|
+
def resolve(self, message: Message) -> Union['Event', None]:
|
|
44
|
+
"""
|
|
45
|
+
Определяет подходящий тип события и возвращает экземпляр для передачи на обработку в шину событий либо None.
|
|
46
|
+
"""
|
|
47
|
+
event = None
|
|
48
|
+
|
|
49
|
+
for event_cls in self._registry[message.topic]:
|
|
50
|
+
if event_cls.__name__ == message.type:
|
|
51
|
+
event = event_cls(**message.body)
|
|
52
|
+
|
|
53
|
+
if event is None:
|
|
54
|
+
logger.warning('Событие "%s" не определено', message.type)
|
|
55
|
+
|
|
56
|
+
return event
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Объекты предметной области."""
|
|
2
|
+
from .factories import AbstractDomainFactory
|
|
3
|
+
from .factories import DTOBase
|
|
4
|
+
from .fields import Field
|
|
5
|
+
from .model import HasUnsetValues
|
|
6
|
+
from .model import Unset
|
|
7
|
+
from .model import asdict
|
|
8
|
+
from .model import is_unset
|
|
9
|
+
from .model import unset
|
|
10
|
+
from .model import unset_validator
|
|
11
|
+
from .types import Bool
|
|
12
|
+
from .types import Date
|
|
13
|
+
from .types import Int
|
|
14
|
+
from .types import NoneBool
|
|
15
|
+
from .types import NoneDate
|
|
16
|
+
from .types import NoneInt
|
|
17
|
+
from .types import NoneStr
|
|
18
|
+
from .types import NoneUnsetUUID
|
|
19
|
+
from .types import Str
|
|
20
|
+
from .types import UnsetUUID
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'AbstractDomainFactory', 'DTOBase',
|
|
25
|
+
'Unset', 'unset', 'is_unset', 'unset_validator', 'HasUnsetValues', 'asdict',
|
|
26
|
+
'Field',
|
|
27
|
+
'UnsetUUID', 'Bool', 'Date', 'Int', 'NoneBool', 'NoneDate', 'NoneInt', 'NoneStr', 'NoneUnsetUUID', 'Str'
|
|
28
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Объекты передачи данных."""
|
|
2
|
+
from abc import ABCMeta
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
from pydantic.main import BaseModel # pylint: disable=no-name-in-module
|
|
6
|
+
|
|
7
|
+
from .model import HasUnsetValues
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DTOBase(HasUnsetValues, BaseModel):
|
|
11
|
+
|
|
12
|
+
"""Базовый объект передачи данных.
|
|
13
|
+
|
|
14
|
+
.. code-block::python
|
|
15
|
+
|
|
16
|
+
dto = DeclarationDTO(last_name='Иванов', first_name='Иван')
|
|
17
|
+
declaration: Declaration = declaration_factory.create(dto)
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
class Config:
|
|
22
|
+
arbitrary_types_allowed = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AbstractDomainFactory(metaclass=ABCMeta):
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def create(self, data):
|
|
29
|
+
"""Сборка объекта модели предметной области из переданных данных.
|
|
30
|
+
|
|
31
|
+
Например:
|
|
32
|
+
|
|
33
|
+
.. code-block:: python
|
|
34
|
+
|
|
35
|
+
dto = DeclarationDTO(last_name='Иванов', first_name='Иван')
|
|
36
|
+
declaration: Declaration = declaration_factory.create(dto)
|
|
37
|
+
"""
|
explicit/domain/model.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Вспомогательные утилиты и типы предметной области."""
|
|
2
|
+
from dataclasses import asdict as asdict_
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Generic
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from pydantic import validator
|
|
13
|
+
from pydantic.main import BaseModel
|
|
14
|
+
from pydantic.validators import _VALIDATORS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pydantic.typing import AbstractSetIntStr
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class __IsDataclass(Protocol): # pylint: disable=invalid-name
|
|
22
|
+
"""Протокол датакласса.
|
|
23
|
+
|
|
24
|
+
Лучший из известных способов сделать соответствующую подсказку.
|
|
25
|
+
"""
|
|
26
|
+
__dataclass_fields__: ClassVar[Dict]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
T = TypeVar('T')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class __SingletonMeta(type, Generic[T]): # pylint: disable=invalid-name
|
|
33
|
+
|
|
34
|
+
_instances: 'Dict[__SingletonMeta[T], T]' = {} # pylint: disable=invalid-sequence-index
|
|
35
|
+
|
|
36
|
+
def __call__(cls: '__SingletonMeta', *args, **kwargs):
|
|
37
|
+
if cls not in cls._instances:
|
|
38
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
|
39
|
+
return cls._instances[cls]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Unset(metaclass=__SingletonMeta):
|
|
43
|
+
"""Sentinel type.
|
|
44
|
+
|
|
45
|
+
Обозначает отсутствие значения, в т.ч. None.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __bool__(self):
|
|
49
|
+
"""Проверка на истинность.
|
|
50
|
+
|
|
51
|
+
Всегда возвращает False, т.к. обозначает отсутствующее значение.
|
|
52
|
+
|
|
53
|
+
Например:
|
|
54
|
+
|
|
55
|
+
class Model:
|
|
56
|
+
some_attr = unset
|
|
57
|
+
|
|
58
|
+
if Model().some_attr:
|
|
59
|
+
print('Model has some_attr')
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def __repr__(self):
|
|
65
|
+
return '<unset>'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
unset = Unset() # Sentinel value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_unset(obj) -> bool:
|
|
72
|
+
return isinstance(obj, Unset)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def unset_validator(v):
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_VALIDATORS.append((Unset, [unset_validator]))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HasUnsetValues(BaseModel):
|
|
83
|
+
|
|
84
|
+
"""Примесь к моделям, значения которых могут быть не заданы."""
|
|
85
|
+
|
|
86
|
+
@validator('*', pre=True)
|
|
87
|
+
def _unset_validator(cls, v): # pylint: disable=no-self-argument
|
|
88
|
+
return v
|
|
89
|
+
|
|
90
|
+
def dict(self, *args, **kwargs):
|
|
91
|
+
"""Формирует словарь из данных объекта, отбрасывая не установленные значения.
|
|
92
|
+
|
|
93
|
+
.. code-block:: python
|
|
94
|
+
|
|
95
|
+
class DocumentDTO(DTOBase):
|
|
96
|
+
series = unset
|
|
97
|
+
number = unser
|
|
98
|
+
|
|
99
|
+
DocumentDTO(number='1').dict() == {'number': '1'}
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
return dict(
|
|
103
|
+
(k, v)
|
|
104
|
+
for k, v in super().dict(*args, **kwargs).items()
|
|
105
|
+
if not is_unset(v)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def filter_unset(d: Any):
|
|
110
|
+
if not isinstance(d, dict):
|
|
111
|
+
return d
|
|
112
|
+
|
|
113
|
+
return dict(
|
|
114
|
+
(k, filter_unset(v))
|
|
115
|
+
for k, v in d.items() if not is_unset(v)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def asdict(
|
|
120
|
+
obj: Union[__IsDataclass, 'HasUnsetValues'],
|
|
121
|
+
include: Union['AbstractSetIntStr', None] = None,
|
|
122
|
+
exclude: Union['AbstractSetIntStr', None] = None,
|
|
123
|
+
) -> dict:
|
|
124
|
+
|
|
125
|
+
"""Формирует словарь из данных объекта, отбрасывая не установленные значения.
|
|
126
|
+
|
|
127
|
+
.. code-block:: python
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class Document:
|
|
131
|
+
series = unset
|
|
132
|
+
number = unser
|
|
133
|
+
|
|
134
|
+
asdict(Document(number='1')) == {'number': '1'}
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
if isinstance(obj, BaseModel):
|
|
139
|
+
data = obj.dict(exclude=exclude, include=include)
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
unfiltered_data = asdict_(obj)
|
|
143
|
+
data = dict(
|
|
144
|
+
(k, v)
|
|
145
|
+
for k, v in unfiltered_data.items()
|
|
146
|
+
if (
|
|
147
|
+
k in (include or unfiltered_data) and
|
|
148
|
+
k not in (exclude or ())
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return filter_unset(data)
|
explicit/domain/types.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Набор типов полей объектов предметной области.
|
|
2
|
+
|
|
3
|
+
Пример использования:
|
|
4
|
+
|
|
5
|
+
class SomeData(DTOBase):
|
|
6
|
+
|
|
7
|
+
global_id: Int = Field(alias='external_id')
|
|
8
|
+
first_name: Str = Field(alias='firstname')
|
|
9
|
+
second_name: NoneStr = Field(alias='patronymic')
|
|
10
|
+
birth_date: NoneStr = Field(alias='date_of_birth')
|
|
11
|
+
gender: NoneInt = Field(alias='gender_id')
|
|
12
|
+
inn_num: NoneStr = Field(alias='inn')
|
|
13
|
+
snils_num: NoneStr = Field(alias='snils')
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
from typing import Union
|
|
17
|
+
from uuid import UUID as UUID_
|
|
18
|
+
import datetime
|
|
19
|
+
|
|
20
|
+
from .model import Unset
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Bool = Union[bool, Unset]
|
|
24
|
+
NoneBool = Union[Bool, None]
|
|
25
|
+
|
|
26
|
+
Str = Union[str, Unset]
|
|
27
|
+
NoneStr = Union[Str, None]
|
|
28
|
+
|
|
29
|
+
Int = Union[int, Unset]
|
|
30
|
+
NoneInt = Union[Int, None]
|
|
31
|
+
|
|
32
|
+
Date = Union[datetime.date, Unset]
|
|
33
|
+
NoneDate = Union[Date, None]
|
|
34
|
+
|
|
35
|
+
UnsetUUID = Union[UUID_, Unset] # pylint: disable=invalid-name
|
|
36
|
+
NoneUnsetUUID = Union[UnsetUUID, None] # pylint: disable=invalid-name
|
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Исключения предметной области."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Mapping
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
NON_FIELD_ERRORS = '__all__'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def init_messages_dict() -> Mapping[str, List[str]]:
|
|
14
|
+
return defaultdict(list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DomainValidationError(Exception):
|
|
18
|
+
|
|
19
|
+
"""Ошибка валидации данных модели предметной области.
|
|
20
|
+
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
def handler(...):
|
|
24
|
+
errors = init_messages_dict()
|
|
25
|
+
if not password:
|
|
26
|
+
errors['Пароль'].append('Обязательное поле')
|
|
27
|
+
if password != confirm:
|
|
28
|
+
errors['Пароль'].append('Пароль и подтверждение не совпадают')
|
|
29
|
+
if errors:
|
|
30
|
+
raise DomainValidationError(errors)
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message, code=None, params=None):
|
|
35
|
+
"""
|
|
36
|
+
The `message` argument can be a single error, a list of errors, or a
|
|
37
|
+
dictionary that maps field names to lists of errors.
|
|
38
|
+
|
|
39
|
+
What we define as an "error" can be either a simple string or an
|
|
40
|
+
instance of DomainValidationError with its message attribute set, and
|
|
41
|
+
what we define as list or dictionary can be an actual `list` or `dict`
|
|
42
|
+
or an instance of DomainValidationError with its `error_list` or
|
|
43
|
+
`error_dict` attribute set.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(message, code, params)
|
|
46
|
+
|
|
47
|
+
if isinstance(message, DomainValidationError):
|
|
48
|
+
if hasattr(message, 'error_dict'):
|
|
49
|
+
message = message.error_dict
|
|
50
|
+
elif not hasattr(message, 'message'):
|
|
51
|
+
message = message.error_list
|
|
52
|
+
else:
|
|
53
|
+
message, code, params = (
|
|
54
|
+
message.message, message.code, message.params
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if isinstance(message, dict):
|
|
58
|
+
self.error_dict = {}
|
|
59
|
+
for field, messages in message.items():
|
|
60
|
+
if not isinstance(messages, DomainValidationError):
|
|
61
|
+
messages = DomainValidationError(messages)
|
|
62
|
+
self.error_dict[field] = messages.error_list
|
|
63
|
+
|
|
64
|
+
elif isinstance(message, list):
|
|
65
|
+
self.error_list = []
|
|
66
|
+
for sub_message in message:
|
|
67
|
+
# Normalize plain strings to instances of
|
|
68
|
+
# DomainValidationError.
|
|
69
|
+
if not isinstance(sub_message, DomainValidationError):
|
|
70
|
+
sub_message = DomainValidationError(sub_message)
|
|
71
|
+
if hasattr(sub_message, 'error_dict'):
|
|
72
|
+
self.error_list.extend(
|
|
73
|
+
sum(sub_message.error_dict.values(), []))
|
|
74
|
+
else:
|
|
75
|
+
self.error_list.extend(sub_message.error_list)
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
self.message = message
|
|
79
|
+
self.code = code
|
|
80
|
+
self.params = params
|
|
81
|
+
self.error_list = [self]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def message_dict(self):
|
|
85
|
+
# Trigger an AttributeError if this DomainValidationError
|
|
86
|
+
# doesn't have an error_dict.
|
|
87
|
+
getattr(self, 'error_dict')
|
|
88
|
+
|
|
89
|
+
return dict(self)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def messages(self):
|
|
93
|
+
if hasattr(self, 'error_dict'):
|
|
94
|
+
return sum(dict(self).values(), [])
|
|
95
|
+
return list(self)
|
|
96
|
+
|
|
97
|
+
def update_error_dict(self, error_dict):
|
|
98
|
+
if hasattr(self, 'error_dict'):
|
|
99
|
+
for field, error_list in self.error_dict.items():
|
|
100
|
+
error_dict.setdefault(field, []).extend(error_list)
|
|
101
|
+
else:
|
|
102
|
+
error_dict.setdefault(NON_FIELD_ERRORS, []).extend(self.error_list)
|
|
103
|
+
return error_dict
|
|
104
|
+
|
|
105
|
+
def __iter__(self):
|
|
106
|
+
if hasattr(self, 'error_dict'):
|
|
107
|
+
for field, errors in self.error_dict.items():
|
|
108
|
+
yield field, list(DomainValidationError(errors))
|
|
109
|
+
else:
|
|
110
|
+
for error in self.error_list:
|
|
111
|
+
message = error.message
|
|
112
|
+
if error.params:
|
|
113
|
+
message %= error.params
|
|
114
|
+
yield str(message)
|
|
115
|
+
|
|
116
|
+
def __str__(self):
|
|
117
|
+
if hasattr(self, 'error_dict'):
|
|
118
|
+
return repr(dict(self))
|
|
119
|
+
return repr(list(self))
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return f'DomainValidationError({self})'
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def handle_pydantic_validation_error(fn):
|
|
126
|
+
|
|
127
|
+
"""Конвертирует PydanticValidationError в DomainValidationError.
|
|
128
|
+
|
|
129
|
+
.. code-block:: python
|
|
130
|
+
|
|
131
|
+
@handle_domain_validation_error
|
|
132
|
+
def create_object(uow: 'UnitOfWork', data: 'SessionOrderDTO',):
|
|
133
|
+
...
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@wraps(fn)
|
|
137
|
+
def wrapper(*args, **kwargs):
|
|
138
|
+
try:
|
|
139
|
+
return fn(*args, **kwargs)
|
|
140
|
+
except PydanticValidationError as error:
|
|
141
|
+
errors_dict = defaultdict(list)
|
|
142
|
+
for i in error.errors():
|
|
143
|
+
for field in i['loc']:
|
|
144
|
+
errors_dict[field].append(i['msg'])
|
|
145
|
+
if errors_dict:
|
|
146
|
+
raise DomainValidationError(errors_dict) from error
|
|
147
|
+
return fn
|
|
148
|
+
return wrapper
|