aury-boot 0.0.3__py3-none-any.whl → 0.0.5__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.
Files changed (98) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +45 -36
  4. aury/boot/application/app/__init__.py +12 -8
  5. aury/boot/application/app/base.py +12 -0
  6. aury/boot/application/app/components.py +137 -44
  7. aury/boot/application/app/middlewares.py +2 -0
  8. aury/boot/application/app/startup.py +249 -0
  9. aury/boot/application/config/__init__.py +36 -1
  10. aury/boot/application/config/multi_instance.py +200 -0
  11. aury/boot/application/config/settings.py +341 -12
  12. aury/boot/application/constants/components.py +6 -0
  13. aury/boot/application/errors/handlers.py +17 -3
  14. aury/boot/application/middleware/logging.py +8 -120
  15. aury/boot/application/rpc/__init__.py +2 -2
  16. aury/boot/commands/__init__.py +30 -10
  17. aury/boot/commands/app.py +131 -1
  18. aury/boot/commands/docs.py +104 -17
  19. aury/boot/commands/init.py +30 -9
  20. aury/boot/commands/server/app.py +2 -3
  21. aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
  22. aury/boot/commands/templates/project/README.md.tpl +2 -2
  23. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  24. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
  25. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  26. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  27. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  28. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  29. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  30. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  31. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  32. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  33. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  34. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
  35. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  36. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
  37. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  38. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  39. aury/boot/commands/templates/project/config.py.tpl +1 -1
  40. aury/boot/commands/templates/project/env.example.tpl +73 -5
  41. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  42. aury/boot/contrib/admin_console/auth.py +2 -3
  43. aury/boot/contrib/admin_console/install.py +1 -1
  44. aury/boot/domain/models/mixins.py +48 -1
  45. aury/boot/domain/pagination/__init__.py +94 -0
  46. aury/boot/domain/repository/impl.py +1 -1
  47. aury/boot/domain/repository/interface.py +1 -1
  48. aury/boot/domain/transaction/__init__.py +8 -9
  49. aury/boot/infrastructure/__init__.py +86 -29
  50. aury/boot/infrastructure/cache/backends.py +102 -18
  51. aury/boot/infrastructure/cache/base.py +12 -0
  52. aury/boot/infrastructure/cache/manager.py +153 -91
  53. aury/boot/infrastructure/channel/__init__.py +24 -0
  54. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  55. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  56. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  57. aury/boot/infrastructure/channel/base.py +92 -0
  58. aury/boot/infrastructure/channel/manager.py +203 -0
  59. aury/boot/infrastructure/clients/__init__.py +22 -0
  60. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  61. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  62. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  63. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  64. aury/boot/infrastructure/clients/redis/config.py +51 -0
  65. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  66. aury/boot/infrastructure/database/config.py +1 -2
  67. aury/boot/infrastructure/database/manager.py +16 -38
  68. aury/boot/infrastructure/events/__init__.py +18 -21
  69. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  70. aury/boot/infrastructure/events/backends/memory.py +86 -0
  71. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  72. aury/boot/infrastructure/events/backends/redis.py +162 -0
  73. aury/boot/infrastructure/events/base.py +127 -0
  74. aury/boot/infrastructure/events/manager.py +224 -0
  75. aury/boot/infrastructure/mq/__init__.py +24 -0
  76. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  77. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  78. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  79. aury/boot/infrastructure/mq/base.py +143 -0
  80. aury/boot/infrastructure/mq/manager.py +239 -0
  81. aury/boot/infrastructure/scheduler/manager.py +7 -3
  82. aury/boot/infrastructure/storage/__init__.py +9 -9
  83. aury/boot/infrastructure/storage/base.py +17 -5
  84. aury/boot/infrastructure/storage/factory.py +0 -1
  85. aury/boot/infrastructure/tasks/__init__.py +2 -2
  86. aury/boot/infrastructure/tasks/manager.py +47 -29
  87. aury/boot/testing/base.py +2 -2
  88. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
  89. aury_boot-0.0.5.dist-info/RECORD +176 -0
  90. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  91. aury/boot/infrastructure/events/bus.py +0 -362
  92. aury/boot/infrastructure/events/config.py +0 -52
  93. aury/boot/infrastructure/events/consumer.py +0 -134
  94. aury/boot/infrastructure/events/models.py +0 -63
  95. aury_boot-0.0.3.dist-info/RECORD +0 -137
  96. /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
  97. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
  98. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.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
- db_isolation_level: str | None = None
147
- if self._config is not None:
148
- database_url = url or self._config.url
149
- db_echo = echo if echo is not None else self._config.echo
150
- db_pool_size = pool_size or self._config.pool_size
151
- db_max_overflow = max_overflow or self._config.max_overflow
152
- db_pool_timeout = pool_timeout or self._config.pool_timeout
153
- db_pool_recycle = pool_recycle or self._config.pool_recycle
154
- db_isolation_level = isolation_level or self._config.isolation_level
155
- else:
156
- # 如果没有配置,使用环境变量
157
- import os
158
- database_url = url or os.getenv("DATABASE_URL")
159
- if not database_url:
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, None]:
260
+ async def get_session(self) -> AsyncGenerator[AsyncSession]:
283
261
  """FastAPI 依赖注入专用的会话获取器。
284
262
 
285
263
  Yields:
@@ -1,33 +1,30 @@
1
- """事件系统 - 基于 Kombu 消息队列的分布式事件总线。
1
+ """事件总线模块。
2
2
 
3
- 提供事件基础定义、发布/订阅机制,实现模块间的解耦。
4
- 支持本地模式(内存)和分布式模式(Kombu 消息队列)。
3
+ 提供发布/订阅模式的事件总线功能,用于模块间解耦通信。
5
4
 
6
- **架构说明**:
7
- Event 基类定义在 infrastructure 层,这是最底层的公共数据结构。
8
- Domain 层依赖 infrastructure.events 获取 Event 基类。
9
- 这样完全断开了 infrastructure domain 的循环依赖。
10
-
11
- 事件模型定义在单独的 models.py 文件中,避免循环导入问题。
5
+ 支持的后端:
6
+ - memory: 内存事件总线(单进程)
7
+ - redis: Redis Pub/Sub(多进程/多实例)
8
+ - rabbitmq: RabbitMQ Exchange(分布式)
12
9
  """
13
10
 
14
- from __future__ import annotations
15
-
16
- from .bus import EventBus
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
- "EventBus",
25
- "EventConfig",
26
- "EventConsumer",
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,11 @@
1
+ """事件总线后端实现。"""
2
+
3
+ from .memory import MemoryEventBus
4
+ from .rabbitmq import RabbitMQEventBus
5
+ from .redis import RedisEventBus
6
+
7
+ __all__ = [
8
+ "MemoryEventBus",
9
+ "RabbitMQEventBus",
10
+ "RedisEventBus",
11
+ ]
@@ -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"]