aioeventbus 0.1.0__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.
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.4
2
+ Name: aioeventbus
3
+ Version: 0.1.0
4
+ Summary: An async event bus for RabbitMQ, Redis, and NATS
5
+ Author-email: Lumin Core <lumincore1@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Lumin-Core/aioeventbus
8
+ Project-URL: Bug-tracker, https://github.com/Lumin-Core/aioeventbus/issues
9
+ Keywords: aioeventbus,eventbus,rabbitmq,redis,nats,async,microservices
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Communications
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: aio-pika>=9.0.0
23
+ Requires-Dist: redis>=5.0.0
24
+ Requires-Dist: nats-py>=2.5.0
25
+ Requires-Dist: pyyaml>=6.0
26
+
27
+ # πŸ“‘ aioeventbus
28
+
29
+ **Асинхронная шина событий для RabbitMQ, Redis ΠΈ NATS**
30
+
31
+ [![PyPI version](https://badge.fury.io/py/aioeventbus.svg)](https://badge.fury.io/py/aioeventbus)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/aioeventbus.svg)](https://pypi.org/project/aioeventbus/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ > ΠŸΡ€ΠΎΡΡ‚Π°Ρ, гибкая ΠΈ готовая ΠΊ ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Ρƒ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° для ΠΎΠ±ΠΌΠ΅Π½Π° сообщСниями ΠΌΠ΅ΠΆΠ΄Ρƒ микросСрвисами.
36
+
37
+ **aioeventbus** прСдоставляСт Π΅Π΄ΠΈΠ½Ρ‹ΠΉ асинхронный API для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ подписки Π½Π° Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ события Ρ‡Π΅Ρ€Π΅Π· популярныС Π±Ρ€ΠΎΠΊΠ΅Ρ€Ρ‹:
38
+ - 🐰 **RabbitMQ** (fanout-обмСнники)
39
+ - πŸ”΄ **Redis** (Pub/Sub)
40
+ - πŸš€ **NATS** (базовая модСль подписки)
41
+
42
+ Π‘ΠΌΠ΅Π½ΠΈΡ‚Π΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€, ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π² ΠΎΠ΄Π½Ρƒ строку Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³Π΅ – ΠΊΠΎΠ΄ прилоТСния останСтся Π½Π΅ΠΈΠ·ΠΌΠ΅Π½Π½Ρ‹ΠΌ.
43
+
44
+ ---
45
+
46
+ ## ✨ ВозмоТности
47
+
48
+ - πŸ”Œ **ΠΠ΅Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ ΠΎΡ‚ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°** – ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΉ интСрфСйс для RabbitMQ, Redis, NATS
49
+ - ⚑ **ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ асинхронно** – построСно Π½Π° `asyncio`, идСально для FastAPI / Sanic / aiohttp
50
+ - 🧡 **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° синхронных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ²** – ваши Ρ…Π΅Π½Π΄Π»Π΅Ρ€Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΌΠΈ функциями; ΠΎΠ½ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ² Π±Π΅Π· Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ event loop
51
+ - πŸ“ **ΠŸΡ€ΠΎΡΡ‚Π°Ρ рСгистрация Ρ‡Π΅Ρ€Π΅Π· Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€** – `@bus.handler(EventClass)`
52
+ - πŸ” **АвтоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅** – Π½Π°Π΄Ρ‘ΠΆΠ½Ρ‹Π΅ соСдинСния для всСх Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠ²
53
+ - πŸ› οΈ **CLI для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΠΎΠ²** – Π½Π°Ρ‡Π½ΠΈΡ‚Π΅ Ρ€Π°Π±ΠΎΡ‚Ρƒ Π·Π° сСкунды
54
+ - πŸ“¦ **ΠœΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ зависимости** – Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ для Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°
55
+ - πŸ‘Œ **Удобство использования** – Π΅Π΄ΠΈΠ½Ρ‹ΠΉ интСрфСйс ΠΈ Ρ„Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` ΡΠΊΡ€Ρ‹Π²Π°ΡŽΡ‚ различия ΠΌΠ΅ΠΆΠ΄Ρƒ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°ΠΌΠΈ. НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½Ρ‹Π΅ API для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° – достаточно Π·Π½Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ класс `EventBus`. ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Ρ‡Π΅Ρ€Π΅Π· YAML ΠΈΠ»ΠΈ CLI Π΄Π΅Π»Π°Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° Π΄Π΅Π»ΠΎΠΌ ΠΎΠ΄Π½ΠΎΠΉ строки, Π° встроСнная ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок ΠΈ автоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ·Π±Π°Π²Π»ΡΡŽΡ‚ ΠΎΡ‚ Ρ€ΡƒΡ‚ΠΈΠ½Ρ‹.
56
+
57
+ ---
58
+
59
+ ## πŸ“¦ Установка
60
+
61
+ ```bash
62
+ pip install aioeventbus
63
+ ```
64
+
65
+ Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ установитС ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΡƒΡŽ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ для Π½ΡƒΠΆΠ½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
66
+ - `aio-pika` для RabbitMQ
67
+ - `redis` для Redis
68
+ - `nats-py` для NATS
69
+
70
+ ---
71
+
72
+ ## πŸš€ Быстрый старт
73
+
74
+ ### 1. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ»
75
+
76
+ Π€Π°ΠΉΠ» `config.yaml`:
77
+
78
+ ```yaml
79
+ broker: rabbitmq # rabbitmq, redis, nats
80
+ host: localhost
81
+ port: 5672 # ΠΏΠΎΡ€Ρ‚Ρ‹ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ: RabbitMQ=5672, Redis=6379, NATS=4222
82
+ exchange: domain_events # для RabbitMQ
83
+ max_workers: 10
84
+ ```
85
+
86
+ Или сгСнСрируйтС Π΅Π³ΠΎ Ρ‡Π΅Ρ€Π΅Π· CLI:
87
+
88
+ ```bash
89
+ aioeventbus init --broker nats --output config.yaml
90
+ ```
91
+
92
+ ### 2. ΠžΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚Π΅ события Π΄ΠΎΠΌΠ΅Π½Π°
93
+
94
+ ```python
95
+ from dataclasses import dataclass
96
+ from aioeventbus import DomainEvent
97
+
98
+ @dataclass
99
+ class OrderCreated(DomainEvent):
100
+ __event_type__ = "OrderCreated"
101
+ order_id: str
102
+ customer_id: str
103
+ amount: float
104
+ ```
105
+
106
+ ### 3. ЗарСгистрируйтС ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ
107
+
108
+ ```python
109
+ from aioeventbus import create_event_bus
110
+
111
+ bus = create_event_bus("config.yaml")
112
+
113
+ @bus.handler(OrderCreated)
114
+ def notify_admin(event: OrderCreated):
115
+ print(f"[ADMIN] Новый Π·Π°ΠΊΠ°Π· {event.order_id} ΠΎΡ‚ {event.customer_id}")
116
+
117
+ @bus.handler(OrderCreated)
118
+ def start_fulfillment(event: OrderCreated):
119
+ print(f"[FULFILLMENT] ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π·Π°ΠΊΠ°Π·Π° {event.order_id}")
120
+ ```
121
+
122
+ ### 4. ЗапуститС потрСбитСля (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Π² lifespan FastAPI)
123
+
124
+ ```python
125
+ from contextlib import asynccontextmanager
126
+ from fastapi import FastAPI
127
+
128
+ @asynccontextmanager
129
+ async def lifespan(app: FastAPI):
130
+ await bus.start_consuming()
131
+ yield
132
+ await bus.stop_consuming()
133
+
134
+ app = FastAPI(lifespan=lifespan)
135
+ ```
136
+
137
+ ### 5. ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠΉΡ‚Π΅ события ΠΈΠ· любого мСста
138
+
139
+ ```python
140
+ @app.post("/orders")
141
+ async def create_order(order_id: str, customer_id: str, amount: float):
142
+ event = OrderCreated(order_id, customer_id, amount)
143
+ await bus.publish(event)
144
+ return {"status": "ok"}
145
+ ```
146
+
147
+ ---
148
+
149
+ ## πŸ“‚ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ
150
+
151
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ YAML-Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ. ΠŸΡ€ΠΈΠΌΠ΅Ρ€ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
152
+
153
+ <details>
154
+ <summary><b>RabbitMQ</b></summary>
155
+
156
+ ```yaml
157
+ broker: rabbitmq
158
+ host: localhost
159
+ port: 5672
160
+ exchange: domain_events
161
+ max_workers: 10
162
+ durable: false # врСмСнная ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ (удаляСтся ΠΏΡ€ΠΈ остановкС потрСбитСля)
163
+ ```
164
+ </details>
165
+
166
+ <details>
167
+ <summary><b>Redis</b></summary>
168
+
169
+ ```yaml
170
+ broker: redis
171
+ host: localhost
172
+ port: 6379
173
+ channel: domain_events
174
+ max_workers: 10
175
+ ```
176
+ </details>
177
+
178
+ <details>
179
+ <summary><b>NATS</b></summary>
180
+
181
+ ```yaml
182
+ broker: nats
183
+ host: localhost
184
+ port: 4222
185
+ subject: domain_events
186
+ max_workers: 10
187
+ ```
188
+ </details>
189
+
190
+ ВсС поля, ΠΊΡ€ΠΎΠΌΠ΅ `broker`, ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ – значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚ с ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°ΠΌΠΈ Π²Ρ‹ΡˆΠ΅.
191
+
192
+ ---
193
+
194
+ ## 🧰 CLI ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Π°
195
+
196
+ `aioeventbus` поставляСтся с интСрфСйсом ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строки:
197
+
198
+ ```bash
199
+ aioeventbus init --broker redis --output my_config.yaml
200
+ ```
201
+
202
+ ΠžΠΏΡ†ΠΈΠΈ:
203
+ - `--broker` – Π²Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ `rabbitmq`, `redis` ΠΈΠ»ΠΈ `nats`
204
+ - `--output` – ΠΏΡƒΡ‚ΡŒ ΠΊ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΌΡƒ Ρ„Π°ΠΉΠ»Ρƒ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ `eventbus_config.yaml`)
205
+ - `--host`, `--port`, `--exchange`, `--channel`, `--subject`, `--max-workers` – ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
206
+
207
+ ---
208
+
209
+ ## πŸ§ͺ ΠŸΡ€ΠΈΠΌΠ΅Ρ€ с FastAPI
210
+
211
+ ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ находится Π² Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [`examples/fastapi_app`](examples/fastapi_app).
212
+
213
+ ЗапуститС Π΅Π³ΠΎ:
214
+
215
+ ```bash
216
+ cd examples/fastapi_app
217
+ pip install -r requirements.txt # fastapi, uvicorn, aioeventbus
218
+ uvicorn main:app --reload
219
+ ```
220
+
221
+ Π—Π°Ρ‚Π΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ запросы:
222
+
223
+ ```bash
224
+ curl -X POST "http://localhost:8000/orders?order_id=123&customer_id=alice&amount=99.9"
225
+ curl -X POST "http://localhost:8000/orders/123/pay?payment_id=pay_456"
226
+ ```
227
+
228
+ Π’ Ρ‚Π΅Ρ€ΠΌΠΈΠ½Π°Π»Π΅ появятся сообщСния ΠΎΡ‚ зарСгистрированных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ².
229
+
230
+ ---
231
+
232
+ ## πŸ—οΈ АрхитСктура
233
+
234
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° построСна Π²ΠΎΠΊΡ€ΡƒΠ³ абстрактного класса `EventBus`:
235
+
236
+ ```python
237
+ class EventBus(ABC):
238
+ def handler(self, event_cls): ...
239
+ async def publish(self, event) -> None: ...
240
+ async def start_consuming(self) -> None: ...
241
+ async def stop_consuming(self) -> None: ...
242
+ ```
243
+
244
+ Π’Ρ€ΠΈ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ: `RabbitMQEventBus`, `RedisEventBus` ΠΈ `NatsEventBus`.
245
+ Π€Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ ваш ΠΊΠΎΠ½Ρ„ΠΈΠ³ ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π½ΡƒΠΆΠ½Ρ‹ΠΉ экзСмпляр.
246
+
247
+ - ΠŸΠΎΡ‚Ρ€Π΅Π±ΠΈΡ‚Π΅Π»ΠΈ автоматичСски ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ (ΠΈΠ»ΠΈ подписки), Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ подписчик ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ копию ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ события.
248
+ - Π‘ΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½Ρ‹Π΅ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π·Π°ΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ², Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ event loop asyncio.
249
+ - БоСдинСния ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ Π½Π°Π΄Ρ‘ΠΆΠ½ΠΎ – ΠΎΠ½ΠΈ автоматичСски ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΠΎΠ±Ρ€Ρ‹Π²Π΅ связи с Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠΌ.
250
+
251
+ ---
252
+
253
+ ## 🀝 УчастиС Π² Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅
254
+
255
+ ΠŸΡ€ΠΈΠ²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‚ΡΡ Π»ΡŽΠ±Ρ‹Π΅ Π²ΠΊΠ»Π°Π΄Ρ‹!
256
+ ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΎΠ·Π½Π°ΠΊΠΎΠΌΡŒΡ‚Π΅ΡΡŒ с [Ρ‚Ρ€Π΅ΠΊΠ΅Ρ€ΠΎΠΌ Π·Π°Π΄Π°Ρ‡](https://github.com/yourusername/aioeventbus/issues) ΠΈ отправляйтС pull request с понятным описаниСм ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ.
257
+
258
+ 1. Π‘Π΄Π΅Π»Π°ΠΉΡ‚Π΅ Ρ„ΠΎΡ€ΠΊ рСпозитория
259
+ 2. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Π²Π΅Ρ‚ΠΊΡƒ для Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
260
+ 3. УстановитС зависимости для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ: `pip install -e .[dev]`
261
+ 4. ЗапуститС тСсты: `pytest`
262
+ 5. ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ pull request
263
+
264
+ ---
265
+
266
+ ## πŸ“„ ЛицСнзия
267
+
268
+ Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ распространяСтся ΠΏΠΎΠ΄ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ **MIT** – подробности Π² Ρ„Π°ΠΉΠ»Π΅ [LICENSE](LICENSE).
269
+
270
+ ---
271
+
272
+ ## πŸ™Œ Благодарности
273
+
274
+ Боздано с ❀️ с использованиСм:
275
+ - [aio-pika](https://github.com/mosquito/aio-pika) для RabbitMQ
276
+ - [redis-py](https://github.com/redis/redis-py) для Redis
277
+ - [nats.py](https://github.com/nats-io/nats.py) для NATS
278
+
279
+ ---
280
+
281
+ **ΠŸΡ€ΠΈΡΡ‚Π½ΠΎΠΉ событийно-ΠΎΡ€ΠΈΠ΅Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ!** πŸš€
@@ -0,0 +1,255 @@
1
+ # πŸ“‘ aioeventbus
2
+
3
+ **Асинхронная шина событий для RabbitMQ, Redis ΠΈ NATS**
4
+
5
+ [![PyPI version](https://badge.fury.io/py/aioeventbus.svg)](https://badge.fury.io/py/aioeventbus)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/aioeventbus.svg)](https://pypi.org/project/aioeventbus/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ > ΠŸΡ€ΠΎΡΡ‚Π°Ρ, гибкая ΠΈ готовая ΠΊ ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Ρƒ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° для ΠΎΠ±ΠΌΠ΅Π½Π° сообщСниями ΠΌΠ΅ΠΆΠ΄Ρƒ микросСрвисами.
10
+
11
+ **aioeventbus** прСдоставляСт Π΅Π΄ΠΈΠ½Ρ‹ΠΉ асинхронный API для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ подписки Π½Π° Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ события Ρ‡Π΅Ρ€Π΅Π· популярныС Π±Ρ€ΠΎΠΊΠ΅Ρ€Ρ‹:
12
+ - 🐰 **RabbitMQ** (fanout-обмСнники)
13
+ - πŸ”΄ **Redis** (Pub/Sub)
14
+ - πŸš€ **NATS** (базовая модСль подписки)
15
+
16
+ Π‘ΠΌΠ΅Π½ΠΈΡ‚Π΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€, ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π² ΠΎΠ΄Π½Ρƒ строку Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³Π΅ – ΠΊΠΎΠ΄ прилоТСния останСтся Π½Π΅ΠΈΠ·ΠΌΠ΅Π½Π½Ρ‹ΠΌ.
17
+
18
+ ---
19
+
20
+ ## ✨ ВозмоТности
21
+
22
+ - πŸ”Œ **ΠΠ΅Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ ΠΎΡ‚ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°** – ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΉ интСрфСйс для RabbitMQ, Redis, NATS
23
+ - ⚑ **ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ асинхронно** – построСно Π½Π° `asyncio`, идСально для FastAPI / Sanic / aiohttp
24
+ - 🧡 **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° синхронных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ²** – ваши Ρ…Π΅Π½Π΄Π»Π΅Ρ€Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΌΠΈ функциями; ΠΎΠ½ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ² Π±Π΅Π· Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ event loop
25
+ - πŸ“ **ΠŸΡ€ΠΎΡΡ‚Π°Ρ рСгистрация Ρ‡Π΅Ρ€Π΅Π· Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€** – `@bus.handler(EventClass)`
26
+ - πŸ” **АвтоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅** – Π½Π°Π΄Ρ‘ΠΆΠ½Ρ‹Π΅ соСдинСния для всСх Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠ²
27
+ - πŸ› οΈ **CLI для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΠΎΠ²** – Π½Π°Ρ‡Π½ΠΈΡ‚Π΅ Ρ€Π°Π±ΠΎΡ‚Ρƒ Π·Π° сСкунды
28
+ - πŸ“¦ **ΠœΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ зависимости** – Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ для Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°
29
+ - πŸ‘Œ **Удобство использования** – Π΅Π΄ΠΈΠ½Ρ‹ΠΉ интСрфСйс ΠΈ Ρ„Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` ΡΠΊΡ€Ρ‹Π²Π°ΡŽΡ‚ различия ΠΌΠ΅ΠΆΠ΄Ρƒ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°ΠΌΠΈ. НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½Ρ‹Π΅ API для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° – достаточно Π·Π½Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ класс `EventBus`. ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Ρ‡Π΅Ρ€Π΅Π· YAML ΠΈΠ»ΠΈ CLI Π΄Π΅Π»Π°Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° Π΄Π΅Π»ΠΎΠΌ ΠΎΠ΄Π½ΠΎΠΉ строки, Π° встроСнная ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок ΠΈ автоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ·Π±Π°Π²Π»ΡΡŽΡ‚ ΠΎΡ‚ Ρ€ΡƒΡ‚ΠΈΠ½Ρ‹.
30
+
31
+ ---
32
+
33
+ ## πŸ“¦ Установка
34
+
35
+ ```bash
36
+ pip install aioeventbus
37
+ ```
38
+
39
+ Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ установитС ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΡƒΡŽ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ для Π½ΡƒΠΆΠ½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
40
+ - `aio-pika` для RabbitMQ
41
+ - `redis` для Redis
42
+ - `nats-py` для NATS
43
+
44
+ ---
45
+
46
+ ## πŸš€ Быстрый старт
47
+
48
+ ### 1. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ»
49
+
50
+ Π€Π°ΠΉΠ» `config.yaml`:
51
+
52
+ ```yaml
53
+ broker: rabbitmq # rabbitmq, redis, nats
54
+ host: localhost
55
+ port: 5672 # ΠΏΠΎΡ€Ρ‚Ρ‹ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ: RabbitMQ=5672, Redis=6379, NATS=4222
56
+ exchange: domain_events # для RabbitMQ
57
+ max_workers: 10
58
+ ```
59
+
60
+ Или сгСнСрируйтС Π΅Π³ΠΎ Ρ‡Π΅Ρ€Π΅Π· CLI:
61
+
62
+ ```bash
63
+ aioeventbus init --broker nats --output config.yaml
64
+ ```
65
+
66
+ ### 2. ΠžΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚Π΅ события Π΄ΠΎΠΌΠ΅Π½Π°
67
+
68
+ ```python
69
+ from dataclasses import dataclass
70
+ from aioeventbus import DomainEvent
71
+
72
+ @dataclass
73
+ class OrderCreated(DomainEvent):
74
+ __event_type__ = "OrderCreated"
75
+ order_id: str
76
+ customer_id: str
77
+ amount: float
78
+ ```
79
+
80
+ ### 3. ЗарСгистрируйтС ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ
81
+
82
+ ```python
83
+ from aioeventbus import create_event_bus
84
+
85
+ bus = create_event_bus("config.yaml")
86
+
87
+ @bus.handler(OrderCreated)
88
+ def notify_admin(event: OrderCreated):
89
+ print(f"[ADMIN] Новый Π·Π°ΠΊΠ°Π· {event.order_id} ΠΎΡ‚ {event.customer_id}")
90
+
91
+ @bus.handler(OrderCreated)
92
+ def start_fulfillment(event: OrderCreated):
93
+ print(f"[FULFILLMENT] ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π·Π°ΠΊΠ°Π·Π° {event.order_id}")
94
+ ```
95
+
96
+ ### 4. ЗапуститС потрСбитСля (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Π² lifespan FastAPI)
97
+
98
+ ```python
99
+ from contextlib import asynccontextmanager
100
+ from fastapi import FastAPI
101
+
102
+ @asynccontextmanager
103
+ async def lifespan(app: FastAPI):
104
+ await bus.start_consuming()
105
+ yield
106
+ await bus.stop_consuming()
107
+
108
+ app = FastAPI(lifespan=lifespan)
109
+ ```
110
+
111
+ ### 5. ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠΉΡ‚Π΅ события ΠΈΠ· любого мСста
112
+
113
+ ```python
114
+ @app.post("/orders")
115
+ async def create_order(order_id: str, customer_id: str, amount: float):
116
+ event = OrderCreated(order_id, customer_id, amount)
117
+ await bus.publish(event)
118
+ return {"status": "ok"}
119
+ ```
120
+
121
+ ---
122
+
123
+ ## πŸ“‚ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ
124
+
125
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ YAML-Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ. ΠŸΡ€ΠΈΠΌΠ΅Ρ€ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
126
+
127
+ <details>
128
+ <summary><b>RabbitMQ</b></summary>
129
+
130
+ ```yaml
131
+ broker: rabbitmq
132
+ host: localhost
133
+ port: 5672
134
+ exchange: domain_events
135
+ max_workers: 10
136
+ durable: false # врСмСнная ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ (удаляСтся ΠΏΡ€ΠΈ остановкС потрСбитСля)
137
+ ```
138
+ </details>
139
+
140
+ <details>
141
+ <summary><b>Redis</b></summary>
142
+
143
+ ```yaml
144
+ broker: redis
145
+ host: localhost
146
+ port: 6379
147
+ channel: domain_events
148
+ max_workers: 10
149
+ ```
150
+ </details>
151
+
152
+ <details>
153
+ <summary><b>NATS</b></summary>
154
+
155
+ ```yaml
156
+ broker: nats
157
+ host: localhost
158
+ port: 4222
159
+ subject: domain_events
160
+ max_workers: 10
161
+ ```
162
+ </details>
163
+
164
+ ВсС поля, ΠΊΡ€ΠΎΠΌΠ΅ `broker`, ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ – значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚ с ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°ΠΌΠΈ Π²Ρ‹ΡˆΠ΅.
165
+
166
+ ---
167
+
168
+ ## 🧰 CLI ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Π°
169
+
170
+ `aioeventbus` поставляСтся с интСрфСйсом ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строки:
171
+
172
+ ```bash
173
+ aioeventbus init --broker redis --output my_config.yaml
174
+ ```
175
+
176
+ ΠžΠΏΡ†ΠΈΠΈ:
177
+ - `--broker` – Π²Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ `rabbitmq`, `redis` ΠΈΠ»ΠΈ `nats`
178
+ - `--output` – ΠΏΡƒΡ‚ΡŒ ΠΊ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΌΡƒ Ρ„Π°ΠΉΠ»Ρƒ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ `eventbus_config.yaml`)
179
+ - `--host`, `--port`, `--exchange`, `--channel`, `--subject`, `--max-workers` – ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
180
+
181
+ ---
182
+
183
+ ## πŸ§ͺ ΠŸΡ€ΠΈΠΌΠ΅Ρ€ с FastAPI
184
+
185
+ ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ находится Π² Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [`examples/fastapi_app`](examples/fastapi_app).
186
+
187
+ ЗапуститС Π΅Π³ΠΎ:
188
+
189
+ ```bash
190
+ cd examples/fastapi_app
191
+ pip install -r requirements.txt # fastapi, uvicorn, aioeventbus
192
+ uvicorn main:app --reload
193
+ ```
194
+
195
+ Π—Π°Ρ‚Π΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ запросы:
196
+
197
+ ```bash
198
+ curl -X POST "http://localhost:8000/orders?order_id=123&customer_id=alice&amount=99.9"
199
+ curl -X POST "http://localhost:8000/orders/123/pay?payment_id=pay_456"
200
+ ```
201
+
202
+ Π’ Ρ‚Π΅Ρ€ΠΌΠΈΠ½Π°Π»Π΅ появятся сообщСния ΠΎΡ‚ зарСгистрированных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ².
203
+
204
+ ---
205
+
206
+ ## πŸ—οΈ АрхитСктура
207
+
208
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° построСна Π²ΠΎΠΊΡ€ΡƒΠ³ абстрактного класса `EventBus`:
209
+
210
+ ```python
211
+ class EventBus(ABC):
212
+ def handler(self, event_cls): ...
213
+ async def publish(self, event) -> None: ...
214
+ async def start_consuming(self) -> None: ...
215
+ async def stop_consuming(self) -> None: ...
216
+ ```
217
+
218
+ Π’Ρ€ΠΈ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ: `RabbitMQEventBus`, `RedisEventBus` ΠΈ `NatsEventBus`.
219
+ Π€Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ ваш ΠΊΠΎΠ½Ρ„ΠΈΠ³ ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π½ΡƒΠΆΠ½Ρ‹ΠΉ экзСмпляр.
220
+
221
+ - ΠŸΠΎΡ‚Ρ€Π΅Π±ΠΈΡ‚Π΅Π»ΠΈ автоматичСски ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ (ΠΈΠ»ΠΈ подписки), Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ подписчик ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ копию ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ события.
222
+ - Π‘ΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½Ρ‹Π΅ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π·Π°ΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ², Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ event loop asyncio.
223
+ - БоСдинСния ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ Π½Π°Π΄Ρ‘ΠΆΠ½ΠΎ – ΠΎΠ½ΠΈ автоматичСски ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΠΎΠ±Ρ€Ρ‹Π²Π΅ связи с Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠΌ.
224
+
225
+ ---
226
+
227
+ ## 🀝 УчастиС Π² Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅
228
+
229
+ ΠŸΡ€ΠΈΠ²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‚ΡΡ Π»ΡŽΠ±Ρ‹Π΅ Π²ΠΊΠ»Π°Π΄Ρ‹!
230
+ ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΎΠ·Π½Π°ΠΊΠΎΠΌΡŒΡ‚Π΅ΡΡŒ с [Ρ‚Ρ€Π΅ΠΊΠ΅Ρ€ΠΎΠΌ Π·Π°Π΄Π°Ρ‡](https://github.com/yourusername/aioeventbus/issues) ΠΈ отправляйтС pull request с понятным описаниСм ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ.
231
+
232
+ 1. Π‘Π΄Π΅Π»Π°ΠΉΡ‚Π΅ Ρ„ΠΎΡ€ΠΊ рСпозитория
233
+ 2. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Π²Π΅Ρ‚ΠΊΡƒ для Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
234
+ 3. УстановитС зависимости для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ: `pip install -e .[dev]`
235
+ 4. ЗапуститС тСсты: `pytest`
236
+ 5. ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ pull request
237
+
238
+ ---
239
+
240
+ ## πŸ“„ ЛицСнзия
241
+
242
+ Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ распространяСтся ΠΏΠΎΠ΄ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ **MIT** – подробности Π² Ρ„Π°ΠΉΠ»Π΅ [LICENSE](LICENSE).
243
+
244
+ ---
245
+
246
+ ## πŸ™Œ Благодарности
247
+
248
+ Боздано с ❀️ с использованиСм:
249
+ - [aio-pika](https://github.com/mosquito/aio-pika) для RabbitMQ
250
+ - [redis-py](https://github.com/redis/redis-py) для Redis
251
+ - [nats.py](https://github.com/nats-io/nats.py) для NATS
252
+
253
+ ---
254
+
255
+ **ΠŸΡ€ΠΈΡΡ‚Π½ΠΎΠΉ событийно-ΠΎΡ€ΠΈΠ΅Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ!** πŸš€
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ # Π£ΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ инструмСнт, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΡΠΎΠ±ΠΈΡ€Π°Ρ‚ΡŒ ΠΏΠ°ΠΊΠ΅Ρ‚
3
+ requires = ["setuptools>=61.0", "wheel"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ # Π­Ρ‚ΠΎ имя, ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ Π±ΡƒΠ΄ΡƒΡ‚ ΡƒΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ (pip install eventbus)
8
+ name = "aioeventbus"
9
+ # НачнитС с вСрсии 0.1.0
10
+ version = "0.1.0"
11
+ # ΠšΡ€Π°Ρ‚ΠΊΠΎΠ΅ описаниС
12
+ description = "An async event bus for RabbitMQ, Redis, and NATS"
13
+ # Π§ΠΈΡ‚Π°Π΅ΠΌ описаниС ΠΈΠ· README.md
14
+ readme = "README.md"
15
+ # Π£ΠΊΠ°ΠΆΠΈΡ‚Π΅ ΠΌΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½ΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ Python, ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ ваша Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ°
16
+ requires-python = ">=3.8"
17
+ # ЛицСнзия
18
+ license = { text = "MIT" }
19
+ # Авторы
20
+ authors = [
21
+ { name = "Lumin Core", email = "lumincore1@gmail.com" },
22
+ ]
23
+
24
+ # ΠšΠ»ΡŽΡ‡Π΅Π²Ρ‹Π΅ слова для поиска Π½Π° PyPI
25
+ keywords = ["aioeventbus", "eventbus", "rabbitmq", "redis", "nats", "async", "microservices"]
26
+
27
+ # Зависимости, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½ΡƒΠΆΠ½Ρ‹ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ
28
+ dependencies = [
29
+ "aio-pika>=9.0.0",
30
+ "redis>=5.0.0",
31
+ "nats-py>=2.5.0",
32
+ "pyyaml>=6.0"
33
+ ]
34
+
35
+ # ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ, ΠΏΠΎΠΌΠΎΠ³Π°ΡŽΡ‰ΠΈΠ΅ Π½Π°ΠΉΡ‚ΠΈ ΠΏΠ°ΠΊΠ΅Ρ‚
36
+ classifiers = [
37
+ "Development Status :: 3 - Alpha",
38
+ "Intended Audience :: Developers",
39
+ "License :: OSI Approved :: MIT License",
40
+ "Programming Language :: Python :: 3",
41
+ "Programming Language :: Python :: 3.8",
42
+ "Programming Language :: Python :: 3.9",
43
+ "Programming Language :: Python :: 3.10",
44
+ "Programming Language :: Python :: 3.11",
45
+ "Topic :: Software Development :: Libraries",
46
+ "Topic :: Communications",
47
+ ]
48
+
49
+ [project.urls]
50
+ # Бсылки Π½Π° Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ рСсурсы
51
+ Homepage = "https://github.com/Lumin-Core/aioeventbus"
52
+ Bug-tracker = "https://github.com/Lumin-Core/aioeventbus/issues"
53
+
54
+ [tool.setuptools.packages.find]
55
+ # Π£ΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ, Π³Π΄Π΅ ΠΈΡΠΊΠ°Ρ‚ΡŒ ΠΊΠΎΠ΄ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ
56
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,281 @@
1
+ Metadata-Version: 2.4
2
+ Name: aioeventbus
3
+ Version: 0.1.0
4
+ Summary: An async event bus for RabbitMQ, Redis, and NATS
5
+ Author-email: Lumin Core <lumincore1@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Lumin-Core/aioeventbus
8
+ Project-URL: Bug-tracker, https://github.com/Lumin-Core/aioeventbus/issues
9
+ Keywords: aioeventbus,eventbus,rabbitmq,redis,nats,async,microservices
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Communications
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: aio-pika>=9.0.0
23
+ Requires-Dist: redis>=5.0.0
24
+ Requires-Dist: nats-py>=2.5.0
25
+ Requires-Dist: pyyaml>=6.0
26
+
27
+ # πŸ“‘ aioeventbus
28
+
29
+ **Асинхронная шина событий для RabbitMQ, Redis ΠΈ NATS**
30
+
31
+ [![PyPI version](https://badge.fury.io/py/aioeventbus.svg)](https://badge.fury.io/py/aioeventbus)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/aioeventbus.svg)](https://pypi.org/project/aioeventbus/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ > ΠŸΡ€ΠΎΡΡ‚Π°Ρ, гибкая ΠΈ готовая ΠΊ ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Ρƒ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° для ΠΎΠ±ΠΌΠ΅Π½Π° сообщСниями ΠΌΠ΅ΠΆΠ΄Ρƒ микросСрвисами.
36
+
37
+ **aioeventbus** прСдоставляСт Π΅Π΄ΠΈΠ½Ρ‹ΠΉ асинхронный API для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΈ подписки Π½Π° Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ события Ρ‡Π΅Ρ€Π΅Π· популярныС Π±Ρ€ΠΎΠΊΠ΅Ρ€Ρ‹:
38
+ - 🐰 **RabbitMQ** (fanout-обмСнники)
39
+ - πŸ”΄ **Redis** (Pub/Sub)
40
+ - πŸš€ **NATS** (базовая модСль подписки)
41
+
42
+ Π‘ΠΌΠ΅Π½ΠΈΡ‚Π΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€, ΠΎΡ‚Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π² ΠΎΠ΄Π½Ρƒ строку Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³Π΅ – ΠΊΠΎΠ΄ прилоТСния останСтся Π½Π΅ΠΈΠ·ΠΌΠ΅Π½Π½Ρ‹ΠΌ.
43
+
44
+ ---
45
+
46
+ ## ✨ ВозмоТности
47
+
48
+ - πŸ”Œ **ΠΠ΅Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ ΠΎΡ‚ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°** – ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΉ интСрфСйс для RabbitMQ, Redis, NATS
49
+ - ⚑ **ΠŸΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ асинхронно** – построСно Π½Π° `asyncio`, идСально для FastAPI / Sanic / aiohttp
50
+ - 🧡 **ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° синхронных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ²** – ваши Ρ…Π΅Π½Π΄Π»Π΅Ρ€Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΌΠΈ функциями; ΠΎΠ½ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ² Π±Π΅Π· Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ event loop
51
+ - πŸ“ **ΠŸΡ€ΠΎΡΡ‚Π°Ρ рСгистрация Ρ‡Π΅Ρ€Π΅Π· Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€** – `@bus.handler(EventClass)`
52
+ - πŸ” **АвтоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅** – Π½Π°Π΄Ρ‘ΠΆΠ½Ρ‹Π΅ соСдинСния для всСх Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠ²
53
+ - πŸ› οΈ **CLI для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΠΎΠ²** – Π½Π°Ρ‡Π½ΠΈΡ‚Π΅ Ρ€Π°Π±ΠΎΡ‚Ρƒ Π·Π° сСкунды
54
+ - πŸ“¦ **ΠœΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ зависимости** – Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ для Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°
55
+ - πŸ‘Œ **Удобство использования** – Π΅Π΄ΠΈΠ½Ρ‹ΠΉ интСрфСйс ΠΈ Ρ„Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` ΡΠΊΡ€Ρ‹Π²Π°ΡŽΡ‚ различия ΠΌΠ΅ΠΆΠ΄Ρƒ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°ΠΌΠΈ. НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π°Π·Π½Ρ‹Π΅ API для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° – достаточно Π·Π½Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ класс `EventBus`. ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Ρ‡Π΅Ρ€Π΅Π· YAML ΠΈΠ»ΠΈ CLI Π΄Π΅Π»Π°Π΅Ρ‚ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π° Π΄Π΅Π»ΠΎΠΌ ΠΎΠ΄Π½ΠΎΠΉ строки, Π° встроСнная ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок ΠΈ автоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΈΠ·Π±Π°Π²Π»ΡΡŽΡ‚ ΠΎΡ‚ Ρ€ΡƒΡ‚ΠΈΠ½Ρ‹.
56
+
57
+ ---
58
+
59
+ ## πŸ“¦ Установка
60
+
61
+ ```bash
62
+ pip install aioeventbus
63
+ ```
64
+
65
+ Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ установитС ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΡƒΡŽ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ для Π½ΡƒΠΆΠ½ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
66
+ - `aio-pika` для RabbitMQ
67
+ - `redis` для Redis
68
+ - `nats-py` для NATS
69
+
70
+ ---
71
+
72
+ ## πŸš€ Быстрый старт
73
+
74
+ ### 1. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ»
75
+
76
+ Π€Π°ΠΉΠ» `config.yaml`:
77
+
78
+ ```yaml
79
+ broker: rabbitmq # rabbitmq, redis, nats
80
+ host: localhost
81
+ port: 5672 # ΠΏΠΎΡ€Ρ‚Ρ‹ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ: RabbitMQ=5672, Redis=6379, NATS=4222
82
+ exchange: domain_events # для RabbitMQ
83
+ max_workers: 10
84
+ ```
85
+
86
+ Или сгСнСрируйтС Π΅Π³ΠΎ Ρ‡Π΅Ρ€Π΅Π· CLI:
87
+
88
+ ```bash
89
+ aioeventbus init --broker nats --output config.yaml
90
+ ```
91
+
92
+ ### 2. ΠžΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚Π΅ события Π΄ΠΎΠΌΠ΅Π½Π°
93
+
94
+ ```python
95
+ from dataclasses import dataclass
96
+ from aioeventbus import DomainEvent
97
+
98
+ @dataclass
99
+ class OrderCreated(DomainEvent):
100
+ __event_type__ = "OrderCreated"
101
+ order_id: str
102
+ customer_id: str
103
+ amount: float
104
+ ```
105
+
106
+ ### 3. ЗарСгистрируйтС ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ
107
+
108
+ ```python
109
+ from aioeventbus import create_event_bus
110
+
111
+ bus = create_event_bus("config.yaml")
112
+
113
+ @bus.handler(OrderCreated)
114
+ def notify_admin(event: OrderCreated):
115
+ print(f"[ADMIN] Новый Π·Π°ΠΊΠ°Π· {event.order_id} ΠΎΡ‚ {event.customer_id}")
116
+
117
+ @bus.handler(OrderCreated)
118
+ def start_fulfillment(event: OrderCreated):
119
+ print(f"[FULFILLMENT] ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π·Π°ΠΊΠ°Π·Π° {event.order_id}")
120
+ ```
121
+
122
+ ### 4. ЗапуститС потрСбитСля (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Π² lifespan FastAPI)
123
+
124
+ ```python
125
+ from contextlib import asynccontextmanager
126
+ from fastapi import FastAPI
127
+
128
+ @asynccontextmanager
129
+ async def lifespan(app: FastAPI):
130
+ await bus.start_consuming()
131
+ yield
132
+ await bus.stop_consuming()
133
+
134
+ app = FastAPI(lifespan=lifespan)
135
+ ```
136
+
137
+ ### 5. ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠΉΡ‚Π΅ события ΠΈΠ· любого мСста
138
+
139
+ ```python
140
+ @app.post("/orders")
141
+ async def create_order(order_id: str, customer_id: str, amount: float):
142
+ event = OrderCreated(order_id, customer_id, amount)
143
+ await bus.publish(event)
144
+ return {"status": "ok"}
145
+ ```
146
+
147
+ ---
148
+
149
+ ## πŸ“‚ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ
150
+
151
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ YAML-Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ. ΠŸΡ€ΠΈΠΌΠ΅Ρ€ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π±Ρ€ΠΎΠΊΠ΅Ρ€Π°:
152
+
153
+ <details>
154
+ <summary><b>RabbitMQ</b></summary>
155
+
156
+ ```yaml
157
+ broker: rabbitmq
158
+ host: localhost
159
+ port: 5672
160
+ exchange: domain_events
161
+ max_workers: 10
162
+ durable: false # врСмСнная ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ (удаляСтся ΠΏΡ€ΠΈ остановкС потрСбитСля)
163
+ ```
164
+ </details>
165
+
166
+ <details>
167
+ <summary><b>Redis</b></summary>
168
+
169
+ ```yaml
170
+ broker: redis
171
+ host: localhost
172
+ port: 6379
173
+ channel: domain_events
174
+ max_workers: 10
175
+ ```
176
+ </details>
177
+
178
+ <details>
179
+ <summary><b>NATS</b></summary>
180
+
181
+ ```yaml
182
+ broker: nats
183
+ host: localhost
184
+ port: 4222
185
+ subject: domain_events
186
+ max_workers: 10
187
+ ```
188
+ </details>
189
+
190
+ ВсС поля, ΠΊΡ€ΠΎΠΌΠ΅ `broker`, ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ – значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚ с ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°ΠΌΠΈ Π²Ρ‹ΡˆΠ΅.
191
+
192
+ ---
193
+
194
+ ## 🧰 CLI ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Π°
195
+
196
+ `aioeventbus` поставляСтся с интСрфСйсом ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строки:
197
+
198
+ ```bash
199
+ aioeventbus init --broker redis --output my_config.yaml
200
+ ```
201
+
202
+ ΠžΠΏΡ†ΠΈΠΈ:
203
+ - `--broker` – Π²Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ `rabbitmq`, `redis` ΠΈΠ»ΠΈ `nats`
204
+ - `--output` – ΠΏΡƒΡ‚ΡŒ ΠΊ Π²Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΌΡƒ Ρ„Π°ΠΉΠ»Ρƒ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ `eventbus_config.yaml`)
205
+ - `--host`, `--port`, `--exchange`, `--channel`, `--subject`, `--max-workers` – ΠΏΠ΅Ρ€Π΅ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ значСния ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
206
+
207
+ ---
208
+
209
+ ## πŸ§ͺ ΠŸΡ€ΠΈΠΌΠ΅Ρ€ с FastAPI
210
+
211
+ ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ находится Π² Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ [`examples/fastapi_app`](examples/fastapi_app).
212
+
213
+ ЗапуститС Π΅Π³ΠΎ:
214
+
215
+ ```bash
216
+ cd examples/fastapi_app
217
+ pip install -r requirements.txt # fastapi, uvicorn, aioeventbus
218
+ uvicorn main:app --reload
219
+ ```
220
+
221
+ Π—Π°Ρ‚Π΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ запросы:
222
+
223
+ ```bash
224
+ curl -X POST "http://localhost:8000/orders?order_id=123&customer_id=alice&amount=99.9"
225
+ curl -X POST "http://localhost:8000/orders/123/pay?payment_id=pay_456"
226
+ ```
227
+
228
+ Π’ Ρ‚Π΅Ρ€ΠΌΠΈΠ½Π°Π»Π΅ появятся сообщСния ΠΎΡ‚ зарСгистрированных ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ².
229
+
230
+ ---
231
+
232
+ ## πŸ—οΈ АрхитСктура
233
+
234
+ Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° построСна Π²ΠΎΠΊΡ€ΡƒΠ³ абстрактного класса `EventBus`:
235
+
236
+ ```python
237
+ class EventBus(ABC):
238
+ def handler(self, event_cls): ...
239
+ async def publish(self, event) -> None: ...
240
+ async def start_consuming(self) -> None: ...
241
+ async def stop_consuming(self) -> None: ...
242
+ ```
243
+
244
+ Π’Ρ€ΠΈ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ: `RabbitMQEventBus`, `RedisEventBus` ΠΈ `NatsEventBus`.
245
+ Π€Π°Π±Ρ€ΠΈΠΊΠ° `create_event_bus` Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ ваш ΠΊΠΎΠ½Ρ„ΠΈΠ³ ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π½ΡƒΠΆΠ½Ρ‹ΠΉ экзСмпляр.
246
+
247
+ - ΠŸΠΎΡ‚Ρ€Π΅Π±ΠΈΡ‚Π΅Π»ΠΈ автоматичСски ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ (ΠΈΠ»ΠΈ подписки), Ρ‚Π°ΠΊ Ρ‡Ρ‚ΠΎ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ подписчик ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ копию ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ события.
248
+ - Π‘ΠΈΠ½Ρ…Ρ€ΠΎΠ½Π½Ρ‹Π΅ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π·Π°ΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ², Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π½Π΅ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ event loop asyncio.
249
+ - БоСдинСния ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ Π½Π°Π΄Ρ‘ΠΆΠ½ΠΎ – ΠΎΠ½ΠΈ автоматичСски ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°ΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ ΠΎΠ±Ρ€Ρ‹Π²Π΅ связи с Π±Ρ€ΠΎΠΊΠ΅Ρ€ΠΎΠΌ.
250
+
251
+ ---
252
+
253
+ ## 🀝 УчастиС Π² Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅
254
+
255
+ ΠŸΡ€ΠΈΠ²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‚ΡΡ Π»ΡŽΠ±Ρ‹Π΅ Π²ΠΊΠ»Π°Π΄Ρ‹!
256
+ ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, ΠΎΠ·Π½Π°ΠΊΠΎΠΌΡŒΡ‚Π΅ΡΡŒ с [Ρ‚Ρ€Π΅ΠΊΠ΅Ρ€ΠΎΠΌ Π·Π°Π΄Π°Ρ‡](https://github.com/yourusername/aioeventbus/issues) ΠΈ отправляйтС pull request с понятным описаниСм ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ.
257
+
258
+ 1. Π‘Π΄Π΅Π»Π°ΠΉΡ‚Π΅ Ρ„ΠΎΡ€ΠΊ рСпозитория
259
+ 2. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Π²Π΅Ρ‚ΠΊΡƒ для Π½ΠΎΠ²ΠΎΠΉ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ
260
+ 3. УстановитС зависимости для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ: `pip install -e .[dev]`
261
+ 4. ЗапуститС тСсты: `pytest`
262
+ 5. ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ pull request
263
+
264
+ ---
265
+
266
+ ## πŸ“„ ЛицСнзия
267
+
268
+ Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ распространяСтся ΠΏΠΎΠ΄ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ **MIT** – подробности Π² Ρ„Π°ΠΉΠ»Π΅ [LICENSE](LICENSE).
269
+
270
+ ---
271
+
272
+ ## πŸ™Œ Благодарности
273
+
274
+ Боздано с ❀️ с использованиСм:
275
+ - [aio-pika](https://github.com/mosquito/aio-pika) для RabbitMQ
276
+ - [redis-py](https://github.com/redis/redis-py) для Redis
277
+ - [nats.py](https://github.com/nats-io/nats.py) для NATS
278
+
279
+ ---
280
+
281
+ **ΠŸΡ€ΠΈΡΡ‚Π½ΠΎΠΉ событийно-ΠΎΡ€ΠΈΠ΅Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ!** πŸš€
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/aioeventbus.egg-info/PKG-INFO
4
+ src/aioeventbus.egg-info/SOURCES.txt
5
+ src/aioeventbus.egg-info/dependency_links.txt
6
+ src/aioeventbus.egg-info/requires.txt
7
+ src/aioeventbus.egg-info/top_level.txt
8
+ src/eventbus/__init__.py
9
+ src/eventbus/base.py
10
+ src/eventbus/events.py
11
+ src/eventbus/exceptions.py
12
+ src/eventbus/factory.py
13
+ src/eventbus/brokers/__init__.py
14
+ src/eventbus/brokers/nats.py
15
+ src/eventbus/brokers/rabbitmq.py
16
+ src/eventbus/brokers/radis.py
@@ -0,0 +1,4 @@
1
+ aio-pika>=9.0.0
2
+ redis>=5.0.0
3
+ nats-py>=2.5.0
4
+ pyyaml>=6.0
@@ -0,0 +1,15 @@
1
+ from .base import EventBus
2
+ from .brokers.nats import NatsEventBus
3
+ from .brokers.rabbitmq import RabbitMQEventBus
4
+ from .brokers.radis import RedisEventBus
5
+ from .factory import create_event_bus
6
+ from .events import DomainEvent
7
+
8
+ __all__ = [
9
+ "EventBus",
10
+ "create_event_bus",
11
+ "DomainEvent",
12
+ "RabbitMQEventBus",
13
+ "RedisEventBus",
14
+ "NatsEventBus",
15
+ ]
@@ -0,0 +1,19 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class EventBus(ABC):
5
+ @abstractmethod
6
+ def handler(self, event_cls):
7
+ pass
8
+
9
+ @abstractmethod
10
+ async def publish(self, event) -> None:
11
+ pass
12
+
13
+ @abstractmethod
14
+ async def start_consuming(self) -> None:
15
+ pass
16
+
17
+ @abstractmethod
18
+ async def stop_consuming(self) -> None:
19
+ pass
File without changes
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import json
3
+ from collections import defaultdict
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Callable, Dict, List, Optional, Type
6
+ import nats
7
+ from nats.aio.client import Client as NATSClient
8
+
9
+ from aioeventbus.src.eventbus.base import EventBus
10
+ from aioeventbus.src.eventbus.events import DomainEvent
11
+
12
+
13
+ class NatsEventBus(EventBus):
14
+ def __init__(
15
+ self,
16
+ servers: str = "nats://localhost:4222",
17
+ subject: str = "domain_events",
18
+ max_workers: int = 10,
19
+ ) -> None:
20
+ self.broker_name = "NATS"
21
+ self.servers = servers
22
+ self.subject = subject
23
+ self._subscriptions: Dict[str, List[Callable]] = defaultdict(list)
24
+ self._event_classes: Dict[str, Type[DomainEvent]] = {}
25
+
26
+ # Publish connection
27
+ self._pub_nc: Optional[NATSClient] = None
28
+
29
+ # Consumer
30
+ self._sub_nc: Optional[NATSClient] = None
31
+ self._consumer_task: Optional[asyncio.Task] = None
32
+ self._stop_event = asyncio.Event()
33
+ self._executor = ThreadPoolExecutor(max_workers=max_workers)
34
+
35
+ def handler(self, event_cls: Type[DomainEvent]):
36
+ """Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для рСгистрации ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² событий."""
37
+ event_type = event_cls.__event_type__
38
+ self._event_classes[event_type] = event_cls
39
+
40
+ def decorator(func: Callable):
41
+ self._subscriptions[event_type].append(func)
42
+ return func
43
+
44
+ return decorator
45
+
46
+ async def _ensure_publish_connection(self):
47
+ """Π“Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ соСдинСниС для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎ."""
48
+ if self._pub_nc and self._pub_nc.is_connected:
49
+ return
50
+ if self._pub_nc:
51
+ await self._pub_nc.close()
52
+ self._pub_nc = await nats.connect(servers=self.servers)
53
+
54
+ async def publish(self, event: DomainEvent) -> None:
55
+ """Асинхронная публикация события Π² NATS."""
56
+ await self._ensure_publish_connection()
57
+ message = json.dumps(event.to_dict())
58
+ await self._pub_nc.publish(self.subject, message.encode())
59
+
60
+ async def start_consuming(self) -> None:
61
+ """ЗапускаСт Ρ„ΠΎΠ½ΠΎΠ²ΡƒΡŽ Π·Π°Π΄Π°Ρ‡Ρƒ-потрСбитСля."""
62
+ if self._consumer_task and not self._consumer_task.done():
63
+ return
64
+ self._stop_event.clear()
65
+ self._consumer_task = asyncio.create_task(self._consume_loop())
66
+
67
+ async def _consume_loop(self) -> None:
68
+ """Основной Ρ†ΠΈΠΊΠ» потрСблСния сообщСний (Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ Task)."""
69
+ # ΠžΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ΅ соСдинСниС для подписки
70
+ self._sub_nc = await nats.connect(servers=self.servers)
71
+ try:
72
+ async def on_message(msg):
73
+ raw = json.loads(msg.data.decode())
74
+ event_type = raw.get("event_type")
75
+ data = raw.get("data", {})
76
+ event_cls = self._event_classes.get(event_type)
77
+ if event_cls is None:
78
+ print(f"[WARN] Unknown event type: {event_type}")
79
+ return
80
+ event = event_cls(**data)
81
+
82
+ # Π’Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ синхронныС ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ²
83
+ loop = asyncio.get_running_loop()
84
+ for handler in self._subscriptions.get(event_type, []):
85
+ await loop.run_in_executor(self._executor, handler, event)
86
+
87
+ # Подписка Π½Π° subject (ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ всС сообщСния Π½Π° этот subject)
88
+ await self._sub_nc.subscribe(self.subject, cb=on_message)
89
+
90
+ # ОТидаСм сигнала остановки
91
+ await self._stop_event.wait()
92
+ finally:
93
+ await self._sub_nc.close()
94
+
95
+ async def stop_consuming(self) -> None:
96
+ """ΠžΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Π΅Ρ‚ потрСбитСля ΠΈ освобоТдаСт рСсурсы."""
97
+ self._stop_event.set()
98
+ if self._consumer_task:
99
+ await self._consumer_task
100
+ if self._pub_nc:
101
+ await self._pub_nc.close()
102
+ if self._sub_nc:
103
+ await self._sub_nc.close()
104
+ self._executor.shutdown(wait=False)
@@ -0,0 +1,115 @@
1
+ import asyncio
2
+ import json
3
+ from collections import defaultdict
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Callable, Dict, List, Optional, Type
6
+ import aio_pika
7
+ from aio_pika import Message, ExchangeType, connect_robust
8
+ from aioeventbus.src.eventbus.base import EventBus
9
+ from aioeventbus.src.eventbus.events import DomainEvent
10
+
11
+
12
+ class RabbitMQEventBus(EventBus):
13
+ def __init__(
14
+ self,
15
+ host: str = "localhost",
16
+ exchange: str = "domain_events",
17
+ max_workers: int = 10
18
+ ) -> None:
19
+ self.broker_name = "RabbitMQ"
20
+ self.host = host
21
+ self.exchange_name = exchange
22
+ self._subscriptions: Dict[str, List[Callable]] = defaultdict(list)
23
+ self._event_classes: Dict[str, Type[DomainEvent]] = {}
24
+
25
+ # Publish connection
26
+ self._pub_connection: Optional[aio_pika.RobustConnection] = None
27
+ self._pub_channel: Optional[aio_pika.RobustChannel] = None
28
+ self._pub_exchange: Optional[aio_pika.Exchange] = None
29
+
30
+ # Consumer
31
+ self._consumer_task: Optional[asyncio.Task] = None
32
+ self._stop_event = asyncio.Event()
33
+ self._executor = ThreadPoolExecutor(max_workers=max_workers)
34
+
35
+ def handler(self, event_cls: Type[DomainEvent]):
36
+ """Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для рСгистрации ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² событий."""
37
+ event_type = event_cls.__event_type__
38
+ self._event_classes[event_type] = event_cls
39
+
40
+ def decorator(func: Callable):
41
+ self._subscriptions[event_type].append(func)
42
+ return func
43
+
44
+ return decorator
45
+
46
+ async def _ensure_publish_connection(self):
47
+ """Π“Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ соСдинСниС для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎ."""
48
+ if self._pub_connection and not self._pub_connection.is_closed:
49
+ return
50
+ if self._pub_connection:
51
+ await self._pub_connection.close()
52
+ self._pub_connection = await connect_robust(host=self.host)
53
+ self._pub_channel = await self._pub_connection.channel()
54
+ self._pub_exchange = await self._pub_channel.declare_exchange(
55
+ self.exchange_name, ExchangeType.FANOUT, durable=False
56
+ )
57
+
58
+ async def publish(self, event: DomainEvent) -> None:
59
+ """Асинхронная публикация события Π² RabbitMQ."""
60
+ await self._ensure_publish_connection()
61
+ message = json.dumps(event.to_dict())
62
+ await self._pub_exchange.publish(
63
+ Message(body=message.encode()),
64
+ routing_key=""
65
+ )
66
+
67
+ async def start_consuming(self) -> None:
68
+ """ЗапускаСт Ρ„ΠΎΠ½ΠΎΠ²ΡƒΡŽ Π·Π°Π΄Π°Ρ‡Ρƒ-потрСбитСля."""
69
+ if self._consumer_task and not self._consumer_task.done():
70
+ return
71
+ self._stop_event.clear()
72
+ self._consumer_task = asyncio.create_task(self._consume_loop())
73
+
74
+ async def _consume_loop(self) -> None:
75
+ """Основной Ρ†ΠΈΠΊΠ» потрСблСния сообщСний (Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ Task)."""
76
+ # ΠžΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ΅ соСдинСниС для потрСблСния
77
+ connection = await connect_robust(host=self.host)
78
+ try:
79
+ channel = await connection.channel()
80
+ exchange = await channel.declare_exchange(
81
+ self.exchange_name, ExchangeType.FANOUT, durable=False
82
+ )
83
+ queue = await channel.declare_queue(exclusive=True)
84
+ await queue.bind(exchange)
85
+
86
+ async def on_message(message: aio_pika.IncomingMessage):
87
+ async with message.process():
88
+ raw = json.loads(message.body.decode())
89
+ event_type = raw.get("event_type")
90
+ data = raw.get("data", {})
91
+ event_cls = self._event_classes.get(event_type)
92
+ if event_cls is None:
93
+ print(f"[WARN] Unknown event type: {event_type}")
94
+ return
95
+ event = event_cls(**data)
96
+
97
+ # Π’Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ синхронныС ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ Π² ΠΏΡƒΠ»Π΅ ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ²
98
+ loop = asyncio.get_running_loop()
99
+ for handler in self._subscriptions.get(event_type, []):
100
+ await loop.run_in_executor(self._executor, handler, event)
101
+
102
+ await queue.consume(on_message)
103
+ # ОТидаСм сигнала остановки
104
+ await self._stop_event.wait()
105
+ finally:
106
+ await connection.close()
107
+
108
+ async def stop_consuming(self) -> None:
109
+ """ΠžΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Π΅Ρ‚ потрСбитСля ΠΈ освобоТдаСт рСсурсы."""
110
+ self._stop_event.set()
111
+ if self._consumer_task:
112
+ await self._consumer_task
113
+ if self._pub_connection and not self._pub_connection.is_closed:
114
+ await self._pub_connection.close()
115
+ self._executor.shutdown(wait=False)
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ import json
3
+ from collections import defaultdict
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Callable, Dict, List, Optional, Type
6
+ import redis.asyncio as redis
7
+ from redis.asyncio.client import PubSub
8
+
9
+ from aioeventbus.src.eventbus.base import EventBus
10
+ from aioeventbus.src.eventbus.events import DomainEvent
11
+
12
+
13
+ class RedisEventBus(EventBus):
14
+ def __init__(
15
+ self,
16
+ host: str = "localhost",
17
+ port: int = 6379,
18
+ channel: str = "domain_events",
19
+ max_workers: int = 10,
20
+ ) -> None:
21
+ self.broker_name = "Redis"
22
+ self.host = host
23
+ self.port = port
24
+ self.channel = channel
25
+ self._subscriptions: Dict[str, List[Callable]] = defaultdict(list)
26
+ self._event_classes: Dict[str, Type[DomainEvent]] = {}
27
+
28
+ # ΠŸΡƒΠ» соСдинСний для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ (ΠΈ для подписки Ρ‚ΠΎΠΆΠ΅ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ)
29
+ self._pub_redis: Optional[redis.Redis] = None
30
+ self._sub_redis: Optional[redis.Redis] = None
31
+ self._pubsub: Optional[PubSub] = None
32
+
33
+ self._consumer_task: Optional[asyncio.Task] = None
34
+ self._stop_event = asyncio.Event()
35
+ self._executor = ThreadPoolExecutor(max_workers=max_workers)
36
+
37
+ def handler(self, event_cls: Type[DomainEvent]):
38
+ """Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для рСгистрации ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΎΠ² событий."""
39
+ event_type = event_cls.__event_type__
40
+ self._event_classes[event_type] = event_cls
41
+
42
+ def decorator(func: Callable):
43
+ self._subscriptions[event_type].append(func)
44
+ return func
45
+
46
+ return decorator
47
+
48
+ async def _ensure_publish_connection(self) -> None:
49
+ """Π“Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ соСдинСниС для ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎ."""
50
+ if self._pub_redis is not None:
51
+ return
52
+ self._pub_redis = await redis.from_url(f"redis://{self.host}:{self.port}")
53
+
54
+ async def publish(self, event: DomainEvent) -> None:
55
+ """Асинхронная публикация события Π² Redis."""
56
+ await self._ensure_publish_connection()
57
+ message = json.dumps(event.to_dict())
58
+ await self._pub_redis.publish(self.channel, message)
59
+
60
+ async def start_consuming(self) -> None:
61
+ """ЗапускаСт Ρ„ΠΎΠ½ΠΎΠ²ΡƒΡŽ Π·Π°Π΄Π°Ρ‡Ρƒ-потрСбитСля."""
62
+ if self._consumer_task and not self._consumer_task.done():
63
+ return
64
+ self._stop_event.clear()
65
+ self._consumer_task = asyncio.create_task(self._consume_loop())
66
+
67
+ async def _consume_loop(self) -> None:
68
+ """Основной Ρ†ΠΈΠΊΠ» потрСблСния сообщСний (Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ Task)."""
69
+ # ΠžΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ΅ соСдинСниС для подписки
70
+ self._sub_redis = await redis.from_url(f"redis://{self.host}:{self.port}")
71
+ self._pubsub = self._sub_redis.pubsub()
72
+ await self._pubsub.subscribe(self.channel)
73
+
74
+ try:
75
+ async for raw_message in self._pubsub.listen():
76
+ if self._stop_event.is_set():
77
+ break
78
+ if raw_message["type"] != "message":
79
+ continue
80
+ # raw_message["data"] β€” это Π±Π°ΠΉΡ‚Ρ‹
81
+ raw = json.loads(raw_message["data"].decode())
82
+ event_type = raw.get("event_type")
83
+ data = raw.get("data", {})
84
+ event_cls = self._event_classes.get(event_type)
85
+ if event_cls is None:
86
+ print(f"[WARN] Unknown event type: {event_type}")
87
+ continue
88
+ event = event_cls(**data)
89
+
90
+ loop = asyncio.get_running_loop()
91
+ for handler in self._subscriptions.get(event_type, []):
92
+ await loop.run_in_executor(self._executor, handler, event)
93
+ finally:
94
+ await self._pubsub.unsubscribe(self.channel)
95
+ await self._pubsub.close()
96
+ await self._sub_redis.close()
97
+
98
+ async def stop_consuming(self) -> None:
99
+ """ΠžΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Π΅Ρ‚ потрСбитСля ΠΈ освобоТдаСт рСсурсы."""
100
+ self._stop_event.set()
101
+ if self._consumer_task:
102
+ await self._consumer_task
103
+ if self._pub_redis:
104
+ await self._pub_redis.close()
105
+ self._executor.shutdown(wait=False)
@@ -0,0 +1,13 @@
1
+ from dataclasses import asdict
2
+ from typing import Any
3
+
4
+
5
+ class DomainEvent:
6
+ __event_type__: str
7
+
8
+ @property
9
+ def event_type(self) -> str:
10
+ return self.__event_type__
11
+
12
+ def to_dict(self) -> dict[str, Any]:
13
+ return {"event_type": self.event_type, "data": asdict(self)}
File without changes
@@ -0,0 +1,42 @@
1
+ import yaml
2
+ from pathlib import Path
3
+ from aioeventbus.src.eventbus.brokers.nats import NatsEventBus
4
+ from aioeventbus.src.eventbus.brokers.rabbitmq import RabbitMQEventBus
5
+ from aioeventbus.src.eventbus.brokers.radis import RedisEventBus
6
+
7
+
8
+ def load_config(path: Path):
9
+ if not path.exists():
10
+ raise FileNotFoundError(f"Config file not found: {path}")
11
+ with open(path) as f:
12
+ return yaml.safe_load(f)
13
+
14
+
15
+ def create_event_bus(config_path=None):
16
+ if config_path is None:
17
+ config_path = Path.cwd() / "config.yaml"
18
+ cfg = load_config(config_path)
19
+ broker = cfg["broker"]
20
+
21
+ if broker == "rabbitmq":
22
+ return RabbitMQEventBus(
23
+ host=cfg.get("host", "localhost"),
24
+ exchange=cfg.get("exchange", "domain_events"),
25
+ max_workers=cfg.get("max_workers", 10),
26
+ )
27
+ elif broker == "redis":
28
+ return RedisEventBus(
29
+ host=cfg.get("host", "localhost"),
30
+ port=cfg.get("port", 6379),
31
+ channel=cfg.get("channel", "domain_events"),
32
+ max_workers=cfg.get("max_workers", 10),
33
+ )
34
+ elif broker == "nats":
35
+ servers = f"nats://{cfg.get('host', 'localhost')}:{cfg.get('port', 4222)}"
36
+ return NatsEventBus(
37
+ servers=servers,
38
+ subject=cfg.get("subject", "domain_events"),
39
+ max_workers=cfg.get("max_workers", 10),
40
+ )
41
+ else:
42
+ raise ValueError(f"Unknown broker: {broker}")