aury-boot 0.0.4__py3-none-any.whl → 0.0.7__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 +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +60 -36
- aury/boot/application/adapter/__init__.py +112 -0
- aury/boot/application/adapter/base.py +511 -0
- aury/boot/application/adapter/config.py +242 -0
- aury/boot/application/adapter/decorators.py +259 -0
- aury/boot/application/adapter/exceptions.py +202 -0
- aury/boot/application/adapter/http.py +325 -0
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +9 -4
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +216 -0
- aury/boot/application/config/settings.py +398 -149
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +21 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +68 -17
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
- aury/boot/commands/templates/project/config.py.tpl +10 -10
- aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
- aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
- aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
- aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
- aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
- aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
- aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
- aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/common/logging/__init__.py +26 -674
- aury/boot/common/logging/context.py +132 -0
- aury/boot/common/logging/decorators.py +118 -0
- aury/boot/common/logging/format.py +315 -0
- aury/boot/common/logging/setup.py +214 -0
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +7 -16
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +55 -33
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
- aury_boot-0.0.7.dist-info/RECORD +197 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/commands/templates/project/env.example.tpl +0 -213
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.4.dist-info/RECORD +0 -137
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -14,7 +14,6 @@ from sqlalchemy.exc import DisconnectionError, OperationalError
|
|
|
14
14
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
|
|
15
15
|
|
|
16
16
|
from aury.boot.common.logging import logger
|
|
17
|
-
from aury.boot.infrastructure.database.config import DatabaseConfig
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class DatabaseManager:
|
|
@@ -55,7 +54,6 @@ class DatabaseManager:
|
|
|
55
54
|
"""
|
|
56
55
|
self.name = name
|
|
57
56
|
self._initialized: bool = False
|
|
58
|
-
self._config: DatabaseConfig | None = None
|
|
59
57
|
self._engine: AsyncEngine | None = None
|
|
60
58
|
self._session_factory: async_sessionmaker | None = None
|
|
61
59
|
self._max_retries: int = 3
|
|
@@ -89,14 +87,6 @@ class DatabaseManager:
|
|
|
89
87
|
elif name in cls._instances:
|
|
90
88
|
del cls._instances[name]
|
|
91
89
|
|
|
92
|
-
def configure(self, config: DatabaseConfig) -> None:
|
|
93
|
-
"""配置数据库管理器。
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
config: 数据库配置
|
|
97
|
-
"""
|
|
98
|
-
self._config = config
|
|
99
|
-
|
|
100
90
|
@property
|
|
101
91
|
def engine(self) -> AsyncEngine:
|
|
102
92
|
"""获取数据库引擎。"""
|
|
@@ -142,33 +132,21 @@ class DatabaseManager:
|
|
|
142
132
|
logger.warning("数据库管理器已初始化,跳过重复初始化")
|
|
143
133
|
return
|
|
144
134
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
raise ValueError(
|
|
161
|
-
"数据库 URL 未配置。请通过以下方式之一提供:"
|
|
162
|
-
"1. 使用 DatabaseManager.configure() 设置配置"
|
|
163
|
-
"2. 通过 initialize(url=...) 参数传入"
|
|
164
|
-
"3. 设置环境变量 DATABASE_URL"
|
|
165
|
-
)
|
|
166
|
-
db_echo = echo if echo is not None else os.getenv("DB_ECHO", "false").lower() == "true"
|
|
167
|
-
db_pool_size = pool_size or int(os.getenv("DB_POOL_SIZE", "5"))
|
|
168
|
-
db_max_overflow = max_overflow or int(os.getenv("DB_MAX_OVERFLOW", "10"))
|
|
169
|
-
db_pool_timeout = pool_timeout or int(os.getenv("DB_POOL_TIMEOUT", "30"))
|
|
170
|
-
db_pool_recycle = pool_recycle or int(os.getenv("DB_POOL_RECYCLE", "1800"))
|
|
171
|
-
db_isolation_level = isolation_level or os.getenv("DATABASE_ISOLATION_LEVEL")
|
|
135
|
+
# 使用提供的参数或环境变量默认值
|
|
136
|
+
import os
|
|
137
|
+
database_url = url or os.getenv("DATABASE_URL")
|
|
138
|
+
if not database_url:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"数据库 URL 未配置。请通过以下方式之一提供:"
|
|
141
|
+
"1. 通过 initialize(url=...) 参数传入"
|
|
142
|
+
"2. 设置环境变量 DATABASE_URL"
|
|
143
|
+
)
|
|
144
|
+
db_echo = echo if echo is not None else os.getenv("DB_ECHO", "false").lower() == "true"
|
|
145
|
+
db_pool_size = pool_size or int(os.getenv("DB_POOL_SIZE", "5"))
|
|
146
|
+
db_max_overflow = max_overflow or int(os.getenv("DB_MAX_OVERFLOW", "10"))
|
|
147
|
+
db_pool_timeout = pool_timeout or int(os.getenv("DB_POOL_TIMEOUT", "30"))
|
|
148
|
+
db_pool_recycle = pool_recycle or int(os.getenv("DB_POOL_RECYCLE", "1800"))
|
|
149
|
+
db_isolation_level = isolation_level or os.getenv("DATABASE_ISOLATION_LEVEL")
|
|
172
150
|
|
|
173
151
|
# 构建引擎参数
|
|
174
152
|
engine_kwargs: dict = {
|
|
@@ -279,7 +257,7 @@ class DatabaseManager:
|
|
|
279
257
|
await self._check_session_connection(session)
|
|
280
258
|
return session
|
|
281
259
|
|
|
282
|
-
async def get_session(self) -> AsyncGenerator[AsyncSession
|
|
260
|
+
async def get_session(self) -> AsyncGenerator[AsyncSession]:
|
|
283
261
|
"""FastAPI 依赖注入专用的会话获取器。
|
|
284
262
|
|
|
285
263
|
Yields:
|
|
@@ -1,33 +1,30 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""事件总线模块。
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
支持本地模式(内存)和分布式模式(Kombu 消息队列)。
|
|
3
|
+
提供发布/订阅模式的事件总线功能,用于模块间解耦通信。
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
事件模型定义在单独的 models.py 文件中,避免循环导入问题。
|
|
5
|
+
支持的后端:
|
|
6
|
+
- memory: 内存事件总线(单进程)
|
|
7
|
+
- redis: Redis Pub/Sub(多进程/多实例)
|
|
8
|
+
- rabbitmq: RabbitMQ Exchange(分布式)
|
|
12
9
|
"""
|
|
13
10
|
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
from .
|
|
17
|
-
from .config import EventConfig
|
|
18
|
-
from .consumer import EventConsumer
|
|
19
|
-
from .middleware import EventLoggingMiddleware, EventMiddleware
|
|
20
|
-
from .models import Event, EventHandler, EventType
|
|
11
|
+
from .backends import MemoryEventBus, RabbitMQEventBus, RedisEventBus
|
|
12
|
+
from .base import Event, EventBackend, EventHandler, EventType, IEventBus
|
|
13
|
+
from .manager import EventBusManager
|
|
21
14
|
|
|
22
15
|
__all__ = [
|
|
16
|
+
# 接口和类型
|
|
23
17
|
"Event",
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
18
|
+
"EventBackend",
|
|
19
|
+
# 管理器
|
|
20
|
+
"EventBusManager",
|
|
27
21
|
"EventHandler",
|
|
28
|
-
"EventLoggingMiddleware",
|
|
29
|
-
"EventMiddleware",
|
|
30
22
|
"EventType",
|
|
23
|
+
"IEventBus",
|
|
24
|
+
# 后端实现
|
|
25
|
+
"MemoryEventBus",
|
|
26
|
+
"RabbitMQEventBus",
|
|
27
|
+
"RedisEventBus",
|
|
31
28
|
]
|
|
32
29
|
|
|
33
30
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""内存事件总线后端。
|
|
2
|
+
|
|
3
|
+
适用于单进程场景,如开发环境或简单应用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from aury.boot.common.logging import logger
|
|
13
|
+
|
|
14
|
+
from ..base import Event, EventHandler, IEventBus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryEventBus(IEventBus):
|
|
18
|
+
"""内存事件总线实现。
|
|
19
|
+
|
|
20
|
+
使用内存中的字典存储订阅关系,支持同步和异步处理器。
|
|
21
|
+
|
|
22
|
+
注意:仅适用于单进程,不支持跨进程事件传递。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""初始化内存事件总线。"""
|
|
27
|
+
# event_name -> list of handlers
|
|
28
|
+
self._handlers: dict[str, list[EventHandler]] = {}
|
|
29
|
+
|
|
30
|
+
def _get_event_name(self, event_type: type[Event] | str) -> str:
|
|
31
|
+
"""获取事件名称。"""
|
|
32
|
+
if isinstance(event_type, str):
|
|
33
|
+
return event_type
|
|
34
|
+
return event_type.__name__
|
|
35
|
+
|
|
36
|
+
def subscribe(
|
|
37
|
+
self,
|
|
38
|
+
event_type: type[Event] | str,
|
|
39
|
+
handler: EventHandler,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""订阅事件。"""
|
|
42
|
+
event_name = self._get_event_name(event_type)
|
|
43
|
+
if event_name not in self._handlers:
|
|
44
|
+
self._handlers[event_name] = []
|
|
45
|
+
if handler not in self._handlers[event_name]:
|
|
46
|
+
self._handlers[event_name].append(handler)
|
|
47
|
+
logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
|
|
48
|
+
|
|
49
|
+
def unsubscribe(
|
|
50
|
+
self,
|
|
51
|
+
event_type: type[Event] | str,
|
|
52
|
+
handler: EventHandler,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""取消订阅事件。"""
|
|
55
|
+
event_name = self._get_event_name(event_type)
|
|
56
|
+
if event_name in self._handlers:
|
|
57
|
+
try:
|
|
58
|
+
self._handlers[event_name].remove(handler)
|
|
59
|
+
logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
async def publish(self, event: Event) -> None:
|
|
64
|
+
"""发布事件。"""
|
|
65
|
+
event_name = event.event_name
|
|
66
|
+
handlers = self._handlers.get(event_name, [])
|
|
67
|
+
|
|
68
|
+
if not handlers:
|
|
69
|
+
logger.debug(f"事件 {event_name} 没有订阅者")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
for handler in handlers:
|
|
73
|
+
try:
|
|
74
|
+
result = handler(event)
|
|
75
|
+
if asyncio.iscoroutine(result):
|
|
76
|
+
await result
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"处理事件 {event_name} 失败: {e}")
|
|
79
|
+
|
|
80
|
+
async def close(self) -> None:
|
|
81
|
+
"""关闭事件总线。"""
|
|
82
|
+
self._handlers.clear()
|
|
83
|
+
logger.debug("内存事件总线已关闭")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["MemoryEventBus"]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""RabbitMQ 事件总线后端。
|
|
2
|
+
|
|
3
|
+
使用 aio-pika 实现 RabbitMQ 事件总线。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from aury.boot.common.logging import logger
|
|
13
|
+
|
|
14
|
+
from ..base import Event, EventHandler, IEventBus
|
|
15
|
+
|
|
16
|
+
# 延迟导入 aio-pika(可选依赖)
|
|
17
|
+
try:
|
|
18
|
+
import aio_pika
|
|
19
|
+
from aio_pika import ExchangeType
|
|
20
|
+
from aio_pika import Message as AioPikaMessage
|
|
21
|
+
from aio_pika.abc import AbstractChannel, AbstractConnection, AbstractExchange
|
|
22
|
+
|
|
23
|
+
_AIO_PIKA_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_AIO_PIKA_AVAILABLE = False
|
|
26
|
+
aio_pika = None
|
|
27
|
+
ExchangeType = None
|
|
28
|
+
AioPikaMessage = None
|
|
29
|
+
AbstractChannel = None
|
|
30
|
+
AbstractConnection = None
|
|
31
|
+
AbstractExchange = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RabbitMQEventBus(IEventBus):
|
|
35
|
+
"""RabbitMQ 事件总线实现。
|
|
36
|
+
|
|
37
|
+
使用 RabbitMQ Exchange (fanout/topic) 实现事件发布/订阅。
|
|
38
|
+
|
|
39
|
+
注意:需要安装 aio-pika: pip install aio-pika
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
url: str,
|
|
45
|
+
*,
|
|
46
|
+
exchange_name: str = "events",
|
|
47
|
+
exchange_type: str = "topic",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""初始化 RabbitMQ 事件总线。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
url: RabbitMQ 连接 URL
|
|
53
|
+
exchange_name: 交换机名称
|
|
54
|
+
exchange_type: 交换机类型 (topic/fanout)
|
|
55
|
+
"""
|
|
56
|
+
if not _AIO_PIKA_AVAILABLE:
|
|
57
|
+
raise ImportError("aio-pika 未安装。请安装: pip install aio-pika")
|
|
58
|
+
|
|
59
|
+
self._url = url
|
|
60
|
+
self._exchange_name = exchange_name
|
|
61
|
+
self._exchange_type = exchange_type
|
|
62
|
+
self._connection: AbstractConnection | None = None
|
|
63
|
+
self._channel: AbstractChannel | None = None
|
|
64
|
+
self._exchange: AbstractExchange | None = None
|
|
65
|
+
# event_name -> list of handlers
|
|
66
|
+
self._handlers: dict[str, list[EventHandler]] = {}
|
|
67
|
+
self._consumer_tasks: list[asyncio.Task] = []
|
|
68
|
+
self._running = False
|
|
69
|
+
|
|
70
|
+
async def _ensure_connection(self) -> None:
|
|
71
|
+
"""确保连接已建立。"""
|
|
72
|
+
if self._connection is None or self._connection.is_closed:
|
|
73
|
+
self._connection = await aio_pika.connect_robust(self._url)
|
|
74
|
+
self._channel = await self._connection.channel()
|
|
75
|
+
|
|
76
|
+
# 声明交换机
|
|
77
|
+
exchange_type = (
|
|
78
|
+
ExchangeType.TOPIC
|
|
79
|
+
if self._exchange_type == "topic"
|
|
80
|
+
else ExchangeType.FANOUT
|
|
81
|
+
)
|
|
82
|
+
self._exchange = await self._channel.declare_exchange(
|
|
83
|
+
self._exchange_name,
|
|
84
|
+
exchange_type,
|
|
85
|
+
durable=True,
|
|
86
|
+
)
|
|
87
|
+
logger.info("RabbitMQ 事件总线连接已建立")
|
|
88
|
+
|
|
89
|
+
def _get_event_name(self, event_type: type[Event] | str) -> str:
|
|
90
|
+
"""获取事件名称。"""
|
|
91
|
+
if isinstance(event_type, str):
|
|
92
|
+
return event_type
|
|
93
|
+
return event_type.__name__
|
|
94
|
+
|
|
95
|
+
def subscribe(
|
|
96
|
+
self,
|
|
97
|
+
event_type: type[Event] | str,
|
|
98
|
+
handler: EventHandler,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""订阅事件。"""
|
|
101
|
+
event_name = self._get_event_name(event_type)
|
|
102
|
+
if event_name not in self._handlers:
|
|
103
|
+
self._handlers[event_name] = []
|
|
104
|
+
if handler not in self._handlers[event_name]:
|
|
105
|
+
self._handlers[event_name].append(handler)
|
|
106
|
+
logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
|
|
107
|
+
|
|
108
|
+
def unsubscribe(
|
|
109
|
+
self,
|
|
110
|
+
event_type: type[Event] | str,
|
|
111
|
+
handler: EventHandler,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""取消订阅事件。"""
|
|
114
|
+
event_name = self._get_event_name(event_type)
|
|
115
|
+
if event_name in self._handlers:
|
|
116
|
+
try:
|
|
117
|
+
self._handlers[event_name].remove(handler)
|
|
118
|
+
logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
|
|
119
|
+
except ValueError:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
async def publish(self, event: Event) -> None:
|
|
123
|
+
"""发布事件。"""
|
|
124
|
+
await self._ensure_connection()
|
|
125
|
+
event_name = event.event_name
|
|
126
|
+
data = json.dumps(event.to_dict())
|
|
127
|
+
|
|
128
|
+
message = AioPikaMessage(
|
|
129
|
+
body=data.encode(),
|
|
130
|
+
content_type="application/json",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 使用事件名称作为 routing key
|
|
134
|
+
await self._exchange.publish(message, routing_key=event_name)
|
|
135
|
+
|
|
136
|
+
async def start_listening(self) -> None:
|
|
137
|
+
"""开始监听事件(需要在后台任务中运行)。"""
|
|
138
|
+
await self._ensure_connection()
|
|
139
|
+
self._running = True
|
|
140
|
+
|
|
141
|
+
# 为每个事件类型创建队列和消费者
|
|
142
|
+
for event_name in self._handlers:
|
|
143
|
+
queue = await self._channel.declare_queue(
|
|
144
|
+
f"events.{event_name}",
|
|
145
|
+
durable=True,
|
|
146
|
+
)
|
|
147
|
+
await queue.bind(self._exchange, routing_key=event_name)
|
|
148
|
+
|
|
149
|
+
async def process_message(message, en=event_name):
|
|
150
|
+
async with message.process():
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(message.body.decode())
|
|
153
|
+
handlers = self._handlers.get(en, [])
|
|
154
|
+
for handler in handlers:
|
|
155
|
+
try:
|
|
156
|
+
event = Event.from_dict(data)
|
|
157
|
+
result = handler(event)
|
|
158
|
+
if asyncio.iscoroutine(result):
|
|
159
|
+
await result
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"处理事件 {en} 失败: {e}")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning(f"解析事件消息失败: {e}")
|
|
164
|
+
|
|
165
|
+
task = asyncio.create_task(self._consume_queue(queue, process_message))
|
|
166
|
+
self._consumer_tasks.append(task)
|
|
167
|
+
|
|
168
|
+
async def _consume_queue(self, queue, callback) -> None:
|
|
169
|
+
"""消费队列消息。"""
|
|
170
|
+
async with queue.iterator() as queue_iter:
|
|
171
|
+
async for message in queue_iter:
|
|
172
|
+
if not self._running:
|
|
173
|
+
break
|
|
174
|
+
await callback(message)
|
|
175
|
+
|
|
176
|
+
async def close(self) -> None:
|
|
177
|
+
"""关闭事件总线。"""
|
|
178
|
+
self._running = False
|
|
179
|
+
for task in self._consumer_tasks:
|
|
180
|
+
task.cancel()
|
|
181
|
+
self._consumer_tasks.clear()
|
|
182
|
+
|
|
183
|
+
if self._connection:
|
|
184
|
+
await self._connection.close()
|
|
185
|
+
self._connection = None
|
|
186
|
+
self._channel = None
|
|
187
|
+
self._exchange = None
|
|
188
|
+
|
|
189
|
+
self._handlers.clear()
|
|
190
|
+
logger.debug("RabbitMQ 事件总线已关闭")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = ["RabbitMQEventBus"]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Redis 事件总线后端。
|
|
2
|
+
|
|
3
|
+
适用于多进程/多实例场景,支持跨进程事件传递。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from aury.boot.common.logging import logger
|
|
13
|
+
|
|
14
|
+
from ..base import Event, EventHandler, IEventBus
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RedisEventBus(IEventBus):
|
|
21
|
+
"""Redis 事件总线实现。
|
|
22
|
+
|
|
23
|
+
使用 Redis Pub/Sub 实现跨进程的事件发布/订阅。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
url: str | None = None,
|
|
29
|
+
*,
|
|
30
|
+
redis_client: RedisClient | None = None,
|
|
31
|
+
channel_prefix: str = "events:",
|
|
32
|
+
) -> None:
|
|
33
|
+
"""初始化 Redis 事件总线。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
url: Redis 连接 URL(当 redis_client 为 None 时必须提供)
|
|
37
|
+
redis_client: RedisClient 实例(可选,优先使用)
|
|
38
|
+
channel_prefix: 频道名称前缀
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: 当 url 和 redis_client 都为 None 时
|
|
42
|
+
"""
|
|
43
|
+
if redis_client is None and url is None:
|
|
44
|
+
raise ValueError("Redis 事件总线需要提供 url 或 redis_client 参数")
|
|
45
|
+
|
|
46
|
+
self._url = url
|
|
47
|
+
self._client = redis_client
|
|
48
|
+
self._channel_prefix = channel_prefix
|
|
49
|
+
# event_name -> list of handlers (本地订阅)
|
|
50
|
+
self._handlers: dict[str, list[EventHandler]] = {}
|
|
51
|
+
self._pubsub = None
|
|
52
|
+
self._listener_task: asyncio.Task | None = None
|
|
53
|
+
self._running = False
|
|
54
|
+
self._owns_client = False # 是否自己创建的客户端
|
|
55
|
+
|
|
56
|
+
async def _ensure_client(self) -> None:
|
|
57
|
+
"""确保 Redis 客户端已初始化。"""
|
|
58
|
+
if self._client is None and self._url:
|
|
59
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
60
|
+
self._client = RedisClient()
|
|
61
|
+
await self._client.initialize(url=self._url)
|
|
62
|
+
self._owns_client = True
|
|
63
|
+
|
|
64
|
+
def _get_event_name(self, event_type: type[Event] | str) -> str:
|
|
65
|
+
"""获取事件名称。"""
|
|
66
|
+
if isinstance(event_type, str):
|
|
67
|
+
return event_type
|
|
68
|
+
return event_type.__name__
|
|
69
|
+
|
|
70
|
+
def _get_channel(self, event_name: str) -> str:
|
|
71
|
+
"""获取 Redis 频道名称。"""
|
|
72
|
+
return f"{self._channel_prefix}{event_name}"
|
|
73
|
+
|
|
74
|
+
def subscribe(
|
|
75
|
+
self,
|
|
76
|
+
event_type: type[Event] | str,
|
|
77
|
+
handler: EventHandler,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""订阅事件。"""
|
|
80
|
+
event_name = self._get_event_name(event_type)
|
|
81
|
+
if event_name not in self._handlers:
|
|
82
|
+
self._handlers[event_name] = []
|
|
83
|
+
if handler not in self._handlers[event_name]:
|
|
84
|
+
self._handlers[event_name].append(handler)
|
|
85
|
+
logger.debug(f"订阅事件: {event_name} -> {handler.__name__}")
|
|
86
|
+
|
|
87
|
+
def unsubscribe(
|
|
88
|
+
self,
|
|
89
|
+
event_type: type[Event] | str,
|
|
90
|
+
handler: EventHandler,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""取消订阅事件。"""
|
|
93
|
+
event_name = self._get_event_name(event_type)
|
|
94
|
+
if event_name in self._handlers:
|
|
95
|
+
try:
|
|
96
|
+
self._handlers[event_name].remove(handler)
|
|
97
|
+
logger.debug(f"取消订阅事件: {event_name} -> {handler.__name__}")
|
|
98
|
+
except ValueError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def publish(self, event: Event) -> None:
|
|
102
|
+
"""发布事件。"""
|
|
103
|
+
await self._ensure_client()
|
|
104
|
+
event_name = event.event_name
|
|
105
|
+
channel = self._get_channel(event_name)
|
|
106
|
+
data = json.dumps(event.to_dict())
|
|
107
|
+
await self._client.connection.publish(channel, data)
|
|
108
|
+
|
|
109
|
+
async def start_listening(self) -> None:
|
|
110
|
+
"""开始监听事件(需要在后台任务中运行)。"""
|
|
111
|
+
if self._running:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
await self._ensure_client()
|
|
115
|
+
self._pubsub = self._client.connection.pubsub()
|
|
116
|
+
self._running = True
|
|
117
|
+
|
|
118
|
+
# 订阅所有已注册事件的频道
|
|
119
|
+
channels = [self._get_channel(name) for name in self._handlers]
|
|
120
|
+
if channels:
|
|
121
|
+
await self._pubsub.subscribe(*channels)
|
|
122
|
+
|
|
123
|
+
# 监听消息
|
|
124
|
+
async for message in self._pubsub.listen():
|
|
125
|
+
if not self._running:
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if message["type"] == "message":
|
|
129
|
+
try:
|
|
130
|
+
data = json.loads(message["data"])
|
|
131
|
+
event_name = data.get("event_name")
|
|
132
|
+
handlers = self._handlers.get(event_name, [])
|
|
133
|
+
|
|
134
|
+
for handler in handlers:
|
|
135
|
+
try:
|
|
136
|
+
# 创建事件对象
|
|
137
|
+
event = Event.from_dict(data)
|
|
138
|
+
result = handler(event)
|
|
139
|
+
if asyncio.iscoroutine(result):
|
|
140
|
+
await result
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"处理事件 {event_name} 失败: {e}")
|
|
143
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
144
|
+
logger.warning(f"解析事件消息失败: {e}")
|
|
145
|
+
|
|
146
|
+
async def close(self) -> None:
|
|
147
|
+
"""关闭事件总线。"""
|
|
148
|
+
self._running = False
|
|
149
|
+
if self._pubsub:
|
|
150
|
+
await self._pubsub.close()
|
|
151
|
+
self._pubsub = None
|
|
152
|
+
if self._listener_task:
|
|
153
|
+
self._listener_task.cancel()
|
|
154
|
+
self._listener_task = None
|
|
155
|
+
if self._owns_client and self._client:
|
|
156
|
+
await self._client.close()
|
|
157
|
+
self._client = None
|
|
158
|
+
self._handlers.clear()
|
|
159
|
+
logger.debug("Redis 事件总线已关闭")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = ["RedisEventBus"]
|