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.
File without changes
@@ -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()
@@ -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,9 @@
1
+ """Дополнения для моделей предметной области."""
2
+ from .fields import identifier
3
+ from .fields import uuid_identifier
4
+
5
+
6
+ __all__ = [
7
+ 'identifier',
8
+ 'uuid_identifier',
9
+ ]
@@ -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
+ """
@@ -0,0 +1,9 @@
1
+ """Набор полей объектов предметной области."""
2
+ from functools import partial
3
+
4
+ from pydantic import Field as DefaultField
5
+
6
+ from .model import unset
7
+
8
+
9
+ Field = partial(DefaultField, default=unset)
@@ -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)
@@ -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
@@ -0,0 +1,8 @@
1
+ """Абстракции шины сообщений."""
2
+
3
+ from .messagebus import MessageBus
4
+
5
+
6
+ __all__ = [
7
+ 'MessageBus',
8
+ ]