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.
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.1
2
+ Name: explicit-python
3
+ Version: 2.2.1
4
+ Summary: Набор компонентов для построения многослойной архитектуры
5
+ Author: BARS Group
6
+ Author-email: education_dev@bars-open.ru
7
+ License: MIT
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Natural Language :: Russian
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Development Status :: 5 - Production/Stable
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pydantic <2.0
20
+ Requires-Dist: more-itertools
21
+
22
+ # Explicit
23
+ ## Набор компонентов для построения явной (Explicit) многослойной архитектуры
24
+
25
+ [<img alt="explicit architecture" src="doc/100-explicit-architecture-svg.png" width=700>][explicit architecture]
26
+
27
+ ## Решаемые проблемы
28
+ - Образование [BBoM] в приложении
29
+ - Смешивание [бизнес-логики][domain-model], [логики выборки данных][service-layer], запросов UI
30
+ - [Зацепление][coupling] модулей приложения
31
+ - [Дублирование логики][dry]
32
+
33
+ ## Основные принципы построения
34
+ - [CQRS]:
35
+ - Поток команд (Command), модифицирующий состояние приложения (БД) идёт через [слой предметной области][domain-model]
36
+ - Поток запросов (Query) не меняет состояние и не проходит через предметную область
37
+ - В основе лежит [Явная архитектура][explicit architecture]: слой предметной области не зависит от сервисного слоя или конкретных реализаций ORM, СУБД, API сторонних служб и т.д.; используются порты и адаптеры абстрагируют вызовы к API; ядро приложения отделено от взаимодействующих с ним API, GUI, webservices.
38
+
39
+ ## Реализуемые компоненты
40
+ ### Команда (command)
41
+ Инкапсулирует передачу параметров запроса обработчику команды, стандартизирует передачу параметров:
42
+
43
+ ```python
44
+ # Application core
45
+ class RegisterStudent(Command):
46
+ last_name: str
47
+ first_name: str
48
+ ```
49
+
50
+ ### Обработчик команды (command handler)
51
+ Принимает команду и выполняет действия над переданными в команде данными.
52
+ ```python
53
+ # Application core
54
+ def register_student(command: RegisterStudent): # handler
55
+ student = Student(last_name=command.last_name, first_name=command.first_name)
56
+ repository.add(student)
57
+ ```
58
+
59
+ ### Шина (bus, messagebus)
60
+ Обеспечивает доставку команды соответствующему обработчику, уменьшает зацепление между модулями и слоями приложения.
61
+ ```python
62
+ from core import bus
63
+
64
+ # Django Rest Framework
65
+ class StudentViewSet(ModelViewSet):
66
+
67
+ def perform_create(self, serializer):
68
+ command = RegisterStudent(**serializer.validated_data)
69
+ bus.handle(command)
70
+
71
+ # Spyne webservices
72
+ @rpc(
73
+ StudentData,
74
+ )
75
+ def RegisterStudent(ctx, data: 'StudentData'):
76
+ command = RegisterStudent(last_name=data.last_name, first_name=data.first_name)
77
+ bus.handle(command)
78
+
79
+ ```
80
+
81
+ ### Unit of Work
82
+
83
+ [Unit of Work]:
84
+ - Единица работы, логическая бизнес-транзация
85
+ - Обеспечивает атомарность выполняемых операций
86
+ - Предоставляет доступ к репозиториям приложения
87
+ - Устраняет зависимость логики от конкретного фреймворка или технологии БД
88
+
89
+ ```python
90
+ def register_student(command: RegisterStudent, uow: 'UnitOfWork'):
91
+ with uow.wrap():
92
+ uow.users.add(user)
93
+ uow.persons.add(person)
94
+ uow.students.add(student)
95
+ ```
96
+
97
+ ### Репозиторий (Repository)
98
+ [Repository]:
99
+ - Является адаптером к СУБД
100
+ - Выполняет роль хранилища объектов предметной области в соответствующем слое
101
+ - Инкапсулирует логику выборки данных
102
+ - Устраняет зависимость логики от конкретного фреймворка или технологии БД
103
+
104
+ ```python
105
+ class Repository:
106
+ def get_object_by_id(self, identifier: int) -> Student:
107
+ try:
108
+ dbinstance = DBStudent.objects.get(pk=identifier)
109
+ return self._to_domain(dbinstance)
110
+ except ObjectDoesNotExist as e:
111
+ raise StudentNotFound() from e
112
+
113
+ def get_by_persons(self, *persons: 'Person') -> Generator[Student, None, None]:
114
+ query = DBStudent.objects.filter(person_id__in=(person.id for person in persons))
115
+ for dbinstance in query.iterator():
116
+ yield self._to_domain(dbinstance)
117
+ ```
118
+
119
+ ### Фабрика (Factory)
120
+ Инкапсулирует логику создания нового объекта предметной области по известным параметрам.
121
+ ```python
122
+ class Factory(AbstractDomainFactory):
123
+ def create(self, data: StudentDTO) -> Student:
124
+ return Student(
125
+ person_id=data.person.id,
126
+ unit_id=data.unit.id,
127
+ )
128
+ ```
129
+
130
+ ### Объект передачи данных (DTO, Data Transfer Object)
131
+ [DTO] используется при передаче большого количества параметров между объектами приложения и для стандартизации передачи данных и контрактов.
132
+
133
+ ```python
134
+
135
+ def create_person(data: 'PersonDTO'):
136
+ person = factory.create(data)
137
+
138
+ def update_person(person, data: 'PersonDTO'):
139
+ domain.update_person(data)
140
+
141
+ ```
142
+
143
+ ## Обработка входящего запроса и события
144
+
145
+ ### 1. Обработка запроса
146
+
147
+ 1.1 Запрос приходит в контроллер (View, Spyne @rpc и т.д.)
148
+
149
+ 1.2 Контроллер извлекает параметры запроса, валидирует их, формирует команду с параметрами запроса
150
+
151
+ 1.3 Сформированная команда направляется в шину ядра приложения
152
+
153
+ ```python
154
+ def post(self, request, *args, **kwargs):
155
+ serializer = self.get_serializer(data=request.data)
156
+ serializer.is_valid(raise_exception=True)
157
+ last_name = serializer.validated_data['last_name']
158
+ first_name = serializer.validated_data['first_name']
159
+
160
+ command = RegisterStudent(
161
+ last_name=last_name,
162
+ first_name=first_name,
163
+ )
164
+ bus.handle(command)
165
+ return JsonResponse(data={'registered': True})
166
+ ```
167
+
168
+ 1.4 Шина по типу команды определяет соответствующий обработчик и передает ему команду
169
+
170
+ ```python
171
+ class CommandBusMixin(ABC):
172
+
173
+ _command_handlers: Dict[Type[Command], Callable] = {}
174
+
175
+ def handle_command(self, command: Command):
176
+ """Передача запроса обработчику типа данного запроса."""
177
+ return self._command_handlers[type(command)](command)
178
+ ```
179
+
180
+ 1.5 Обработчик выполняет требуемые действия, инициирует событие, соответствующее результату обработки, возвращает результат
181
+
182
+ ```python
183
+ def register_student(
184
+ command: RegisterStudent, uow: 'UnitOfWork'
185
+ ) -> 'Student':
186
+ with uow.wrap():
187
+ student: Student = domain_register_student(
188
+ StudentDTO(**command.dict())
189
+ )
190
+ uow.add_event(events.StudentCreated(
191
+ **command.dict(), **asdict(student)
192
+ ))
193
+ return student
194
+ ```
195
+
196
+ 1.6 Событие попадает в шину ядра
197
+
198
+ 1.7 Ядро определяет список обработчиков, соответствующих типу события, передает им инстанс события
199
+
200
+ ```python
201
+ class EventBusMixin(ABC):
202
+
203
+ _event_handlers: Dict[Type[Event], List[EventHandler]] = {}
204
+
205
+ def handle_event(self, event: Event):
206
+ """Передача события списку обработчиков типа данного события."""
207
+ consume(
208
+ handler(event)
209
+ for handler in self._event_handlers[type(event)]
210
+ )
211
+ ```
212
+
213
+ 1.8 Обработчик события выполняет требуемые действия
214
+
215
+ 1.9 Обработчик события может передать событие на внешнюю шину, воспользовавшись соответствующим адаптером шины
216
+
217
+ ```python
218
+ def on_student_created(
219
+ event: events.StudentCreated, adapter: 'AbstractAdapter'
220
+ ):
221
+ adapter.publish(
222
+ 'edu.students.created',
223
+ json.dumps(asdict(event), default=encoder)
224
+ )
225
+ ```
226
+
227
+ ### 2. Обработка внешнего события
228
+
229
+ 2.1 Подписчик получает событие из внешней шины
230
+
231
+ 2.2 Подписчик десериализует внешнее событие и инстанцирует внутреннее
232
+
233
+ ```python
234
+ def bootstrap():
235
+ from students.core.adapters.messaging import adapter
236
+ from students.core.domain import events
237
+
238
+ TOPIC_EVENTS = {
239
+ 'edu.persons.created': events.PersonCreated,
240
+ }
241
+
242
+ for message in adapter.subscribe(*TOPIC_EVENTS):
243
+ event = TOPIC_EVENTS[message.topic()](
244
+ **json.loads(message.value())
245
+ )
246
+ bus.handle(event)
247
+
248
+ ```
249
+
250
+ 2.3 Внутренее событие попадает в шину ядра
251
+
252
+ 2.4 Ядро определяет список обработчиков, соответствующих типу события, передает им инстанс события
253
+
254
+ 2.5 Обработчик события выполняет требуемые действия
255
+
256
+ 2.6 Обработчик события может инстанцировать новое событие
257
+
258
+ ```python
259
+ def on_person_created(
260
+ event: events.PersonCreated, uow: 'UnitOfWork'
261
+ ) -> None:
262
+ with uow.wrap():
263
+ transaction_id = event.meta.transaction_id
264
+
265
+ saga = uow.sagas.get_object_by_uuid(transaction_id)
266
+ student = uow.students.get_object_by_id(saga.student_id)
267
+
268
+ student.person_id = event.id
269
+ uow.students.update(student)
270
+
271
+ uow.add_event(events.StudentCreated(
272
+ **command.dict(), **asdict(student)
273
+ ))
274
+ ```
275
+
276
+
277
+ ## Минимальный пример реализации
278
+ Можно увидеть в тестовом приложении src/testapp.
279
+
280
+ ## Запуск тестов
281
+ ```sh
282
+ $ tox
283
+ ```
284
+
285
+
286
+ [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)
287
+
288
+ [bbom]: <https://ru.wikipedia.org/wiki/Большой_комок_грязи>
289
+ [ddd]: <https://ru.wikipedia.org/wiki/Предметно-ориентированное_проектирование>
290
+ [domain-model]: <http://design-pattern.ru/patterns/domain-model.html>
291
+ [service-layer]: <http://design-pattern.ru/patterns/service-layer.html>
292
+ [coupling]: <https://ru.wikipedia.org/wiki/Зацепление_(программирование)>
293
+ [dry]: <https://ru.wikipedia.org/wiki/Don’t_repeat_yourself>
294
+ [cqrs]: <https://martinfowler.com/bliki/CQRS.html>
295
+ [explicit architecture]: <https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/>
296
+ [unit of work]: <http://design-pattern.ru/patterns/unit-of-work.html>
297
+ [repository]: <http://design-pattern.ru/patterns/repository.html>
298
+ [dto]: <http://design-pattern.ru/patterns/data-transfer-object.html>
@@ -0,0 +1,30 @@
1
+ explicit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ explicit/unit_of_work.py,sha256=dqFYlttzHof4ueJpuy_rArkfOKBgX9sZpaVmqqEi-kY,5439
3
+ explicit/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ explicit/adapters/db.py,sha256=DJ21cvBCR4VNG6ipC7HKPrz5ej_hLnofWb3nRj6xF38,2363
5
+ explicit/adapters/fs.py,sha256=jEVHe4YxNvSZZWUOFpfZu-TtGb9bWIUhNmXeScFfVak,949
6
+ explicit/adapters/messaging.py,sha256=EKAFTrZ1jDJ4ugAm3r2Xdo_PjP8XPriI5OCBdkwqguA,367
7
+ explicit/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ explicit/contrib/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ explicit/contrib/domain/model/__init__.py,sha256=vbUym41oxPQD8yUMqK2XoYJBOb2w4KxHsse5nT1bQ1s,210
10
+ explicit/contrib/domain/model/fields.py,sha256=4mTjyQOIGc8eLEmECWJdwmr1DR0Pmdb01_ryCnRoBGE,970
11
+ explicit/contrib/messagebus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ explicit/contrib/messagebus/event_registry.py,sha256=XNZEzlCw95fvPmvqCDrkAoelNsZ-J4-jNGTxtHIWoJ8,1630
13
+ explicit/domain/__init__.py,sha256=1m5sbbZWz7leJfWVLiSV5C8et3RpvjpMLxlSaEIA_wI,858
14
+ explicit/domain/factories.py,sha256=c2nPIJ57OEPWGYQ91Q5qt_HJdCYQWZlSxjCR_0DOnIg,1014
15
+ explicit/domain/fields.py,sha256=goSehIKqAfyu0tBejoMtZl2OE8Lajba8B2HJ5zvSYOs,229
16
+ explicit/domain/model.py,sha256=MCc38W43KfwzkM5idANhHYhWNRvWjyswphqlnTc2zo0,3919
17
+ explicit/domain/types.py,sha256=CTC-CV4aRXQvJ1hy0LOAOtkDnJA7MTTMEw6UHBFKGL0,994
18
+ explicit/domain/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ explicit/domain/validation/exceptions.py,sha256=qHmLuFnsrweNDXGTkG2baPmMYcjkDdgQefANrq1xwx8,5055
20
+ explicit/messagebus/__init__.py,sha256=u5D-bx-IqhG1LFv-78RGK9Tgbv-uPQL_bQgDr586GsM,126
21
+ explicit/messagebus/commands.py,sha256=5kEoL23ZaYLRuUdwrurNBmwZL7Ss5TgQaulbn0he8Mc,3147
22
+ explicit/messagebus/dependency_injection.py,sha256=qoQJ_TBXP8QhWulCstYYkg6bh0e58TTFo8BPSeiJwkk,1950
23
+ explicit/messagebus/events.py,sha256=BKAqEqIC_V6sYMeHZQpAZBsigf9NanSgGZVTJlKX3-I,3487
24
+ explicit/messagebus/messagebus.py,sha256=Yc55SCvCQjS1F5fZtLpjjDOFes7P6gXb5uFo7xeXRw4,3071
25
+ explicit/messagebus/queries.py,sha256=kMtT8Bm2YeZbrORAIqq0unou1V2XBz8hvLPXmmzHP_4,1616
26
+ explicit_python-2.2.1.dist-info/METADATA,sha256=fuVWfjFSJqjBINPnCbenRrVvditc_uG8SFDXdSP_n9Y,12835
27
+ explicit_python-2.2.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
28
+ explicit_python-2.2.1.dist-info/dependency_links.txt,sha256=z2Fi-sEElLMsunjPT4vpjRUYQ4oGu_tX_9NeLaEKlgo,44
29
+ explicit_python-2.2.1.dist-info/top_level.txt,sha256=tt6T8l4Yji4ww87qZQcD4CbcwTIHy7NAPmU7QAfMcpY,9
30
+ explicit_python-2.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (74.1.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ https://pypi.bars-open.ru/simple/m3-builder
@@ -0,0 +1 @@
1
+ explicit