aury-boot 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""事件总线实现。
|
|
2
|
+
|
|
3
|
+
提供事件发布/订阅机制,支持本地和分布式模式。
|
|
4
|
+
|
|
5
|
+
**架构说明**:
|
|
6
|
+
本模块不依赖具体的 domain.events 实现,而是通过接口和类型变量来保持通用性。
|
|
7
|
+
事件总线作为 infrastructure 组件只需要事件对象能够被序列化。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from kombu import Connection, Exchange
|
|
17
|
+
|
|
18
|
+
from aury.boot.common.logging import logger
|
|
19
|
+
|
|
20
|
+
from .config import EventConfig
|
|
21
|
+
from .consumer import EventConsumer
|
|
22
|
+
from .models import Event, EventHandler, EventType
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EventBus:
|
|
26
|
+
"""事件总线(单例模式)。
|
|
27
|
+
|
|
28
|
+
职责:
|
|
29
|
+
1. 管理事件订阅者
|
|
30
|
+
2. 分发事件(支持本地和分布式模式)
|
|
31
|
+
3. 异步事件处理
|
|
32
|
+
|
|
33
|
+
模式:
|
|
34
|
+
- 本地模式:内存中的观察者模式(默认,无需消息队列)
|
|
35
|
+
- 分布式模式:使用 Kombu 消息队列(需要配置 broker_url)
|
|
36
|
+
|
|
37
|
+
使用示例:
|
|
38
|
+
# 本地模式(内存)
|
|
39
|
+
event_bus = EventBus.get_instance()
|
|
40
|
+
|
|
41
|
+
@event_bus.subscribe(MyEvent)
|
|
42
|
+
async def on_my_event(event: MyEvent):
|
|
43
|
+
print(f"Event {event.event_name} received!")
|
|
44
|
+
|
|
45
|
+
await event_bus.publish(MyEvent(...))
|
|
46
|
+
|
|
47
|
+
# 分布式模式(Kombu)
|
|
48
|
+
await event_bus.initialize(broker_url="redis://localhost:6379/0")
|
|
49
|
+
await event_bus.publish(MyEvent(...)) # 通过消息队列发布
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
_instance: EventBus | None = None
|
|
53
|
+
|
|
54
|
+
def __init__(self, settings: EventConfig | None = None) -> None:
|
|
55
|
+
"""私有构造函数,使用 get_instance() 获取实例。
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
settings: 事件总线配置(如果为 None 则使用默认配置)
|
|
59
|
+
"""
|
|
60
|
+
if EventBus._instance is not None:
|
|
61
|
+
raise RuntimeError("EventBus 是单例类,请使用 get_instance() 获取实例")
|
|
62
|
+
|
|
63
|
+
self._settings = settings or EventConfig()
|
|
64
|
+
self._handlers: dict[type[Event], list[EventHandler]] = {}
|
|
65
|
+
self._event_history: list[Event] = []
|
|
66
|
+
|
|
67
|
+
# Kombu 相关
|
|
68
|
+
self._connection: Any = None # Connection | None
|
|
69
|
+
self._exchange: Any = None # Exchange | None
|
|
70
|
+
self._producer: Any = None # Producer | None
|
|
71
|
+
self._consumer: Any = None # EventConsumer | None
|
|
72
|
+
self._consumer_task: Any = None # asyncio.Task | None
|
|
73
|
+
self._initialized: bool = False
|
|
74
|
+
self._use_distributed: bool = False
|
|
75
|
+
self._broker_url: str | None = None
|
|
76
|
+
|
|
77
|
+
logger.debug("事件总线已创建(本地模式)")
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_instance(cls) -> EventBus:
|
|
81
|
+
"""获取单例实例。"""
|
|
82
|
+
if cls._instance is None:
|
|
83
|
+
cls._instance = cls()
|
|
84
|
+
return cls._instance
|
|
85
|
+
|
|
86
|
+
async def initialize(
|
|
87
|
+
self,
|
|
88
|
+
broker_url: str | None = None,
|
|
89
|
+
*,
|
|
90
|
+
exchange_name: str | None = None,
|
|
91
|
+
queue_prefix: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""初始化分布式事件总线(使用 Kombu)。
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
broker_url: 消息队列 URL(如 "redis://localhost:6379/0" 或 "amqp://guest:guest@localhost:5672//")
|
|
97
|
+
如果不指定则使用配置中的值
|
|
98
|
+
exchange_name: 交换机名称,如果不指定则使用配置中的值
|
|
99
|
+
queue_prefix: 队列名称前缀,如果不指定则使用配置中的值
|
|
100
|
+
"""
|
|
101
|
+
if self._initialized:
|
|
102
|
+
logger.warning("事件总线已初始化,跳过")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# 使用提供的参数或配置中的值
|
|
106
|
+
final_broker_url = broker_url or self._settings.broker_url
|
|
107
|
+
final_exchange_name = exchange_name or self._settings.exchange_name
|
|
108
|
+
final_queue_prefix = queue_prefix or self._settings.queue_prefix
|
|
109
|
+
|
|
110
|
+
if not final_broker_url:
|
|
111
|
+
logger.info("未提供 broker_url,使用本地模式(内存)")
|
|
112
|
+
self._use_distributed = False
|
|
113
|
+
self._initialized = True
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
self._broker_url = final_broker_url
|
|
118
|
+
self._connection = Connection(final_broker_url)
|
|
119
|
+
self._exchange = Exchange(final_exchange_name, type="topic", durable=True)
|
|
120
|
+
|
|
121
|
+
# 创建生产者
|
|
122
|
+
self._producer = self._connection.Producer(
|
|
123
|
+
exchange=self._exchange,
|
|
124
|
+
serializer="json",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 启动消费者(在后台任务中)
|
|
128
|
+
self._consumer = EventConsumer(
|
|
129
|
+
connection=self._connection,
|
|
130
|
+
exchange=self._exchange,
|
|
131
|
+
queue_prefix=final_queue_prefix,
|
|
132
|
+
handlers=self._handlers,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# 在后台启动消费者
|
|
136
|
+
self._consumer_task = asyncio.create_task(self._start_consumer())
|
|
137
|
+
|
|
138
|
+
self._use_distributed = True
|
|
139
|
+
self._initialized = True
|
|
140
|
+
logger.info(f"事件总线已初始化(分布式模式,broker: {broker_url})")
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
logger.error(f"事件总线初始化失败: {exc}")
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
async def _start_consumer(self) -> None:
|
|
146
|
+
"""启动消费者(在后台运行)。"""
|
|
147
|
+
if self._consumer:
|
|
148
|
+
try:
|
|
149
|
+
# 在事件循环中运行消费者
|
|
150
|
+
loop = asyncio.get_event_loop()
|
|
151
|
+
await loop.run_in_executor(None, self._consumer.run)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
logger.error(f"事件消费者运行失败: {exc}")
|
|
154
|
+
|
|
155
|
+
def subscribe(
|
|
156
|
+
self,
|
|
157
|
+
event_type: type[EventType],
|
|
158
|
+
handler: EventHandler | None = None,
|
|
159
|
+
) -> EventHandler | Callable[[EventHandler], EventHandler]:
|
|
160
|
+
"""订阅事件。
|
|
161
|
+
|
|
162
|
+
可以作为装饰器使用:
|
|
163
|
+
@event_bus.subscribe(MyEvent)
|
|
164
|
+
async def handler(event):
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
或直接调用:
|
|
168
|
+
event_bus.subscribe(MyEvent, handler)
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
event_type: 事件类型
|
|
172
|
+
handler: 事件处理器
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
EventHandler | Callable: 处理器或装饰器
|
|
176
|
+
"""
|
|
177
|
+
def decorator(fn: EventHandler) -> EventHandler:
|
|
178
|
+
if event_type not in self._handlers:
|
|
179
|
+
self._handlers[event_type] = []
|
|
180
|
+
|
|
181
|
+
self._handlers[event_type].append(fn)
|
|
182
|
+
logger.debug(f"订阅事件: {event_type.__name__} -> {fn.__name__}")
|
|
183
|
+
|
|
184
|
+
# 如果使用分布式模式,需要注册队列
|
|
185
|
+
if self._use_distributed and self._consumer:
|
|
186
|
+
self._consumer.register_event_type(event_type)
|
|
187
|
+
|
|
188
|
+
return fn
|
|
189
|
+
|
|
190
|
+
if handler is not None:
|
|
191
|
+
return decorator(handler)
|
|
192
|
+
return decorator
|
|
193
|
+
|
|
194
|
+
def unsubscribe(self, event_type: type[EventType], handler: EventHandler) -> None:
|
|
195
|
+
"""取消订阅事件。
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
event_type: 事件类型
|
|
199
|
+
handler: 事件处理器
|
|
200
|
+
"""
|
|
201
|
+
if event_type in self._handlers:
|
|
202
|
+
try:
|
|
203
|
+
self._handlers[event_type].remove(handler)
|
|
204
|
+
logger.debug(f"取消订阅事件: {event_type.__name__} -> {handler.__name__}")
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
async def publish(self, event: EventType) -> None:
|
|
209
|
+
"""发布事件。
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
event: 事件对象
|
|
213
|
+
"""
|
|
214
|
+
logger.info(f"发布事件: {event}")
|
|
215
|
+
|
|
216
|
+
# 记录事件历史
|
|
217
|
+
self._add_to_history(event)
|
|
218
|
+
|
|
219
|
+
if self._use_distributed:
|
|
220
|
+
# 分布式模式:通过消息队列发布
|
|
221
|
+
await self._publish_distributed(event)
|
|
222
|
+
else:
|
|
223
|
+
# 本地模式:直接调用处理器
|
|
224
|
+
await self._publish_local(event)
|
|
225
|
+
|
|
226
|
+
async def _publish_local(self, event: EventType) -> None:
|
|
227
|
+
"""本地模式:直接调用处理器。"""
|
|
228
|
+
handlers = self._handlers.get(type(event), [])
|
|
229
|
+
if not handlers:
|
|
230
|
+
logger.debug(f"事件 {event.event_name} 没有订阅者")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# 执行处理器
|
|
234
|
+
for handler in handlers:
|
|
235
|
+
try:
|
|
236
|
+
if asyncio.iscoroutinefunction(handler):
|
|
237
|
+
await handler(event)
|
|
238
|
+
else:
|
|
239
|
+
handler(event)
|
|
240
|
+
logger.debug(f"事件处理成功: {handler.__name__}")
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
logger.error(f"事件处理失败: {handler.__name__}, 错误: {exc}")
|
|
243
|
+
|
|
244
|
+
async def _publish_distributed(self, event: EventType) -> None:
|
|
245
|
+
"""分布式模式:通过消息队列发布。"""
|
|
246
|
+
if not self._producer:
|
|
247
|
+
logger.warning("生产者未初始化,回退到本地模式")
|
|
248
|
+
await self._publish_local(event)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# 序列化事件(使用 model_dump 而不是 model_dump_json,因为 kombu 会自动序列化)
|
|
253
|
+
event_data = event.model_dump()
|
|
254
|
+
routing_key = event.event_name.lower().replace("event", "")
|
|
255
|
+
|
|
256
|
+
# 发布到消息队列
|
|
257
|
+
self._producer.publish(
|
|
258
|
+
event_data,
|
|
259
|
+
routing_key=routing_key,
|
|
260
|
+
declare=[self._exchange],
|
|
261
|
+
)
|
|
262
|
+
logger.debug(f"事件已发布到消息队列: {routing_key}")
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
logger.error(f"发布事件到消息队列失败: {exc}")
|
|
265
|
+
# 失败时回退到本地模式
|
|
266
|
+
await self._publish_local(event)
|
|
267
|
+
|
|
268
|
+
async def publish_many(self, events: list[Event]) -> None:
|
|
269
|
+
"""批量发布事件。
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
events: 事件列表
|
|
273
|
+
"""
|
|
274
|
+
for event in events:
|
|
275
|
+
await self.publish(event)
|
|
276
|
+
|
|
277
|
+
def _add_to_history(self, event: Event) -> None:
|
|
278
|
+
"""添加事件到历史记录。
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
event: 事件对象
|
|
282
|
+
"""
|
|
283
|
+
# 检查是否启用了历史记录
|
|
284
|
+
if not self._settings.enable_history:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
self._event_history.append(event)
|
|
288
|
+
|
|
289
|
+
# 限制历史记录大小
|
|
290
|
+
if (
|
|
291
|
+
self._settings.max_history_size is not None
|
|
292
|
+
and len(self._event_history) > self._settings.max_history_size
|
|
293
|
+
):
|
|
294
|
+
self._event_history = self._event_history[-self._settings.max_history_size:]
|
|
295
|
+
|
|
296
|
+
def get_history(
|
|
297
|
+
self,
|
|
298
|
+
event_type: type[EventType] | None = None,
|
|
299
|
+
limit: int = 100,
|
|
300
|
+
) -> list[Event]:
|
|
301
|
+
"""获取事件历史。
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
event_type: 事件类型(可选,不指定则返回所有事件)
|
|
305
|
+
limit: 返回数量限制
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
list[Event]: 事件列表
|
|
309
|
+
"""
|
|
310
|
+
if event_type is None:
|
|
311
|
+
return self._event_history[-limit:]
|
|
312
|
+
|
|
313
|
+
filtered = [e for e in self._event_history if isinstance(e, event_type)]
|
|
314
|
+
return filtered[-limit:]
|
|
315
|
+
|
|
316
|
+
def clear_history(self) -> None:
|
|
317
|
+
"""清空事件历史。"""
|
|
318
|
+
self._event_history.clear()
|
|
319
|
+
logger.debug("事件历史已清空")
|
|
320
|
+
|
|
321
|
+
def clear_handlers(self) -> None:
|
|
322
|
+
"""清空所有事件处理器。"""
|
|
323
|
+
self._handlers.clear()
|
|
324
|
+
logger.debug("事件处理器已清空")
|
|
325
|
+
|
|
326
|
+
def get_handler_count(self, event_type: type[EventType] | None = None) -> int:
|
|
327
|
+
"""获取处理器数量。
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
event_type: 事件类型(可选)
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
int: 处理器数量
|
|
334
|
+
"""
|
|
335
|
+
if event_type is None:
|
|
336
|
+
return sum(len(handlers) for handlers in self._handlers.values())
|
|
337
|
+
return len(self._handlers.get(event_type, []))
|
|
338
|
+
|
|
339
|
+
async def cleanup(self) -> None:
|
|
340
|
+
"""清理资源。"""
|
|
341
|
+
if self._consumer:
|
|
342
|
+
self._consumer.should_stop = True
|
|
343
|
+
|
|
344
|
+
if self._connection:
|
|
345
|
+
self._connection.close()
|
|
346
|
+
|
|
347
|
+
self._initialized = False
|
|
348
|
+
self._use_distributed = False
|
|
349
|
+
logger.info("事件总线已清理")
|
|
350
|
+
|
|
351
|
+
def __repr__(self) -> str:
|
|
352
|
+
"""字符串表示。"""
|
|
353
|
+
event_count = len(self._handlers)
|
|
354
|
+
history_size = len(self._event_history)
|
|
355
|
+
mode = "distributed" if self._use_distributed else "local"
|
|
356
|
+
return f"<EventBus mode={mode} events={event_count} history={history_size}>"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
__all__ = [
|
|
360
|
+
"EventBus",
|
|
361
|
+
]
|
|
362
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""事件总线配置。
|
|
2
|
+
|
|
3
|
+
提供事件系统的配置管理。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EventConfig(BaseSettings):
|
|
13
|
+
"""事件总线基础设施配置。
|
|
14
|
+
|
|
15
|
+
Infrastructure 层直接使用的事件总线配置。
|
|
16
|
+
|
|
17
|
+
环境变量前缀: EVENT_
|
|
18
|
+
示例: EVENT_BROKER_URL, EVENT_EXCHANGE_NAME, EVENT_QUEUE_PREFIX, EVENT_MAX_HISTORY_SIZE
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
broker_url: str | None = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description="消息队列 URL(如 redis://localhost:6379/0 或 amqp://guest:guest@localhost:5672//),为空时使用本地模式"
|
|
24
|
+
)
|
|
25
|
+
exchange_name: str = Field(
|
|
26
|
+
default="events",
|
|
27
|
+
description="交换机名称"
|
|
28
|
+
)
|
|
29
|
+
queue_prefix: str = Field(
|
|
30
|
+
default="event",
|
|
31
|
+
description="队列名称前缀"
|
|
32
|
+
)
|
|
33
|
+
max_history_size: int | None = Field(
|
|
34
|
+
default=1000,
|
|
35
|
+
description="事件历史记录最大条数(None 表示不限制,生产环境建议设置或禁用历史记录)"
|
|
36
|
+
)
|
|
37
|
+
enable_history: bool = Field(
|
|
38
|
+
default=True,
|
|
39
|
+
description="是否启用事件历史记录(生产环境建议关闭以节省内存)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
model_config = SettingsConfigDict(
|
|
43
|
+
env_prefix="EVENT_",
|
|
44
|
+
case_sensitive=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"EventConfig",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""事件消费者实现。
|
|
2
|
+
|
|
3
|
+
基于 Kombu ConsumerMixin 实现消息队列事件消费。
|
|
4
|
+
|
|
5
|
+
**架构说明**:
|
|
6
|
+
本模块使用通用的 Any 类型而非直接导入 domain.events,保持 infrastructure 层的独立性。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from kombu import Queue
|
|
15
|
+
from kombu.mixins import ConsumerMixin
|
|
16
|
+
|
|
17
|
+
from aury.boot.common.logging import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EventConsumer(ConsumerMixin):
|
|
21
|
+
"""事件消费者(基于 Kombu ConsumerMixin)。
|
|
22
|
+
|
|
23
|
+
在后台消费消息队列中的事件并调用本地处理器。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
connection: Any, # Connection
|
|
29
|
+
exchange: Any, # Exchange
|
|
30
|
+
queue_prefix: str,
|
|
31
|
+
handlers: dict[Any, list[Any]], # dict[type[EventType], list[EventHandler]]
|
|
32
|
+
) -> None:
|
|
33
|
+
"""初始化事件消费者。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
connection: Kombu 连接
|
|
37
|
+
exchange: 交换机
|
|
38
|
+
queue_prefix: 队列名称前缀
|
|
39
|
+
handlers: 事件处理器字典 {事件类型: [处理器列表]}
|
|
40
|
+
"""
|
|
41
|
+
self.connection = connection
|
|
42
|
+
self._exchange = exchange
|
|
43
|
+
self._queue_prefix = queue_prefix
|
|
44
|
+
self._handlers = handlers
|
|
45
|
+
self._queues: dict[str, Any] = {} # dict[str, Queue]
|
|
46
|
+
self._event_types: dict[str, Any] = {} # dict[str, type[EventType]]
|
|
47
|
+
|
|
48
|
+
def register_event_type(self, event_type: Any) -> None:
|
|
49
|
+
"""注册事件类型(创建对应的队列)。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
event_type: 事件类型(应实现 __name__ 和 model_validate())
|
|
53
|
+
"""
|
|
54
|
+
event_name = event_type.__name__
|
|
55
|
+
routing_key = event_name.lower().replace("event", "")
|
|
56
|
+
|
|
57
|
+
if routing_key not in self._queues:
|
|
58
|
+
queue_name = f"{self._queue_prefix}.{routing_key}"
|
|
59
|
+
queue = Queue(
|
|
60
|
+
queue_name,
|
|
61
|
+
exchange=self._exchange,
|
|
62
|
+
routing_key=routing_key,
|
|
63
|
+
durable=True,
|
|
64
|
+
)
|
|
65
|
+
self._queues[routing_key] = queue
|
|
66
|
+
self._event_types[routing_key] = event_type
|
|
67
|
+
logger.debug(f"注册事件队列: {queue_name} -> {routing_key}")
|
|
68
|
+
|
|
69
|
+
def get_consumers(self, consumer_class: type, channel: Any) -> list:
|
|
70
|
+
"""获取消费者列表。
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
consumer_class: Kombu Consumer 类
|
|
74
|
+
channel: 消息通道
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
list: 消费者列表
|
|
78
|
+
"""
|
|
79
|
+
consumers = []
|
|
80
|
+
for _routing_key, queue in self._queues.items():
|
|
81
|
+
consumer = consumer_class(
|
|
82
|
+
queues=[queue],
|
|
83
|
+
callbacks=[self.on_message],
|
|
84
|
+
accept=["json"],
|
|
85
|
+
)
|
|
86
|
+
consumers.append(consumer)
|
|
87
|
+
return consumers
|
|
88
|
+
|
|
89
|
+
def on_message(self, body: dict[str, Any], message: Any) -> None:
|
|
90
|
+
"""处理接收到的消息。
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
body: 消息体
|
|
94
|
+
message: 消息对象
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
routing_key = message.delivery_info.get("routing_key", "")
|
|
98
|
+
event_type = self._event_types.get(routing_key)
|
|
99
|
+
|
|
100
|
+
if not event_type:
|
|
101
|
+
logger.warning(f"未知的事件类型: {routing_key}")
|
|
102
|
+
message.ack()
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# 反序列化事件
|
|
106
|
+
event = event_type.model_validate(body)
|
|
107
|
+
|
|
108
|
+
# 获取处理器
|
|
109
|
+
handlers = self._handlers.get(event_type, [])
|
|
110
|
+
|
|
111
|
+
# 在事件循环中执行处理器
|
|
112
|
+
loop = asyncio.get_event_loop()
|
|
113
|
+
for handler in handlers:
|
|
114
|
+
try:
|
|
115
|
+
if asyncio.iscoroutinefunction(handler):
|
|
116
|
+
task = loop.create_task(handler(event))
|
|
117
|
+
# 保存任务引用以避免警告
|
|
118
|
+
_ = task
|
|
119
|
+
else:
|
|
120
|
+
handler(event)
|
|
121
|
+
logger.debug(f"事件处理成功: {handler.__name__}")
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.error(f"事件处理失败: {handler.__name__}, 错误: {exc}")
|
|
124
|
+
|
|
125
|
+
message.ack()
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
logger.error(f"处理事件消息失败: {exc}")
|
|
128
|
+
message.reject(requeue=False)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"EventConsumer",
|
|
133
|
+
]
|
|
134
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""事件中间件实现。
|
|
2
|
+
|
|
3
|
+
提供事件拦截、转换、日志记录等功能。
|
|
4
|
+
|
|
5
|
+
**架构说明**:
|
|
6
|
+
本模块使用通用的 Any 类型而非直接导入 domain.events,保持 infrastructure 层的独立性。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from aury.boot.common.logging import logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventMiddleware:
|
|
18
|
+
"""事件中间件基类。
|
|
19
|
+
|
|
20
|
+
可用于实现事件拦截、转换、日志记录等功能。
|
|
21
|
+
注意:这是一个抽象接口,用于扩展事件总线的功能。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
async def process(self, event: Any, next_handler: Callable) -> None:
|
|
25
|
+
"""处理事件。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
event: 事件对象
|
|
29
|
+
next_handler: 下一个处理器
|
|
30
|
+
"""
|
|
31
|
+
await next_handler(event)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventLoggingMiddleware(EventMiddleware):
|
|
35
|
+
"""事件日志中间件。
|
|
36
|
+
|
|
37
|
+
记录事件的详细信息,包括事件处理前后。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
async def process(self, event: Any, next_handler: Callable) -> None:
|
|
41
|
+
"""记录事件日志。"""
|
|
42
|
+
logger.info(f"[EventMiddleware] 处理事件: {event}")
|
|
43
|
+
await next_handler(event)
|
|
44
|
+
logger.info(f"[EventMiddleware] 事件处理完成: {event}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"EventLoggingMiddleware",
|
|
49
|
+
"EventMiddleware",
|
|
50
|
+
]
|
|
51
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""事件模型定义 - 独立文件避免循环导入。
|
|
2
|
+
|
|
3
|
+
提供事件基类、类型变量、处理器定义等基础数据结构。
|
|
4
|
+
此模块不依赖任何其他 infrastructure 模块,可被安全导入。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Callable, Coroutine
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, ClassVar, TypeVar
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
# 事件类型定义(基础数据结构)
|
|
18
|
+
EventType = TypeVar("EventType", bound="Event")
|
|
19
|
+
EventHandler = (
|
|
20
|
+
Callable[[EventType], None] |
|
|
21
|
+
Callable[[EventType], Coroutine[Any, Any, None]]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Event(BaseModel, ABC):
|
|
26
|
+
"""事件基类(Pydantic)。
|
|
27
|
+
|
|
28
|
+
所有业务事件应继承此类。
|
|
29
|
+
Pydantic 提供自动验证和序列化功能。
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
event_id: 事件ID(自动生成UUID)
|
|
33
|
+
timestamp: 事件时间戳
|
|
34
|
+
metadata: 事件元数据
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
event_id: str = Field(default_factory=lambda: str(uuid4()), description="事件ID")
|
|
38
|
+
timestamp: datetime = Field(default_factory=datetime.now, description="事件时间戳")
|
|
39
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="事件元数据")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def event_name(self) -> str:
|
|
44
|
+
"""事件名称(子类必须实现)。"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class Config:
|
|
48
|
+
"""Pydantic配置。"""
|
|
49
|
+
json_encoders: ClassVar[dict] = {
|
|
50
|
+
datetime: lambda v: v.isoformat(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
"""字符串表示。"""
|
|
55
|
+
return f"<{self.__class__.__name__} id={self.event_id} time={self.timestamp}>"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"Event",
|
|
60
|
+
"EventHandler",
|
|
61
|
+
"EventType",
|
|
62
|
+
]
|
|
63
|
+
|