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.
- aioeventbus-0.1.0/PKG-INFO +281 -0
- aioeventbus-0.1.0/README.md +255 -0
- aioeventbus-0.1.0/pyproject.toml +56 -0
- aioeventbus-0.1.0/setup.cfg +4 -0
- aioeventbus-0.1.0/src/aioeventbus.egg-info/PKG-INFO +281 -0
- aioeventbus-0.1.0/src/aioeventbus.egg-info/SOURCES.txt +16 -0
- aioeventbus-0.1.0/src/aioeventbus.egg-info/dependency_links.txt +1 -0
- aioeventbus-0.1.0/src/aioeventbus.egg-info/requires.txt +4 -0
- aioeventbus-0.1.0/src/aioeventbus.egg-info/top_level.txt +1 -0
- aioeventbus-0.1.0/src/eventbus/__init__.py +15 -0
- aioeventbus-0.1.0/src/eventbus/base.py +19 -0
- aioeventbus-0.1.0/src/eventbus/brokers/__init__.py +0 -0
- aioeventbus-0.1.0/src/eventbus/brokers/nats.py +104 -0
- aioeventbus-0.1.0/src/eventbus/brokers/rabbitmq.py +115 -0
- aioeventbus-0.1.0/src/eventbus/brokers/radis.py +105 -0
- aioeventbus-0.1.0/src/eventbus/events.py +13 -0
- aioeventbus-0.1.0/src/eventbus/exceptions.py +0 -0
- aioeventbus-0.1.0/src/eventbus/factory.py +42 -0
|
@@ -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
|
+
[](https://badge.fury.io/py/aioeventbus)
|
|
32
|
+
[](https://pypi.org/project/aioeventbus/)
|
|
33
|
+
[](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
|
+
[](https://badge.fury.io/py/aioeventbus)
|
|
6
|
+
[](https://pypi.org/project/aioeventbus/)
|
|
7
|
+
[](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,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
|
+
[](https://badge.fury.io/py/aioeventbus)
|
|
32
|
+
[](https://pypi.org/project/aioeventbus/)
|
|
33
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eventbus
|
|
@@ -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}")
|