aury-boot 0.0.4__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.
- aury/boot/__init__.py +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +45 -36
- 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 +2 -0
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +200 -0
- aury/boot/application/config/settings.py +341 -12
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +8 -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/init.py +27 -8
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +217 -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 +183 -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 +92 -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 +92 -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/config.py.tpl +1 -1
- aury/boot/commands/templates/project/env.example.tpl +73 -5
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- 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 +1 -2
- 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/manager.py +47 -29
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
- aury_boot-0.0.5.dist-info/RECORD +176 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- 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/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""事件总线基础接口定义。
|
|
2
|
+
|
|
3
|
+
提供事件总线的抽象接口,用于模块间解耦通信。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, TypeVar
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventBackend(Enum):
|
|
18
|
+
"""事件总线后端类型。"""
|
|
19
|
+
|
|
20
|
+
MEMORY = "memory"
|
|
21
|
+
REDIS = "redis"
|
|
22
|
+
RABBITMQ = "rabbitmq"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Event:
|
|
27
|
+
"""事件基类。"""
|
|
28
|
+
|
|
29
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
30
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
31
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def event_name(self) -> str:
|
|
35
|
+
"""获取事件名称。"""
|
|
36
|
+
return self.__class__.__name__
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
"""转换为字典。"""
|
|
40
|
+
return {
|
|
41
|
+
"id": self.id,
|
|
42
|
+
"event_name": self.event_name,
|
|
43
|
+
"timestamp": self.timestamp.isoformat(),
|
|
44
|
+
"metadata": self.metadata,
|
|
45
|
+
"data": self._get_data(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def _get_data(self) -> dict[str, Any]:
|
|
49
|
+
"""获取事件数据(子类应重写)。"""
|
|
50
|
+
# 获取所有非基类的字段
|
|
51
|
+
base_fields = {"id", "timestamp", "metadata"}
|
|
52
|
+
return {
|
|
53
|
+
k: v
|
|
54
|
+
for k, v in self.__dict__.items()
|
|
55
|
+
if k not in base_fields and not k.startswith("_")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: dict[str, Any]) -> Event:
|
|
60
|
+
"""从字典创建事件。"""
|
|
61
|
+
return cls(
|
|
62
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
63
|
+
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
64
|
+
if data.get("timestamp")
|
|
65
|
+
else datetime.now(),
|
|
66
|
+
metadata=data.get("metadata", {}),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 事件处理器类型
|
|
71
|
+
EventHandler = Callable[[Event], Any]
|
|
72
|
+
EventType = TypeVar("EventType", bound=Event)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class IEventBus(ABC):
|
|
76
|
+
"""事件总线接口。"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def subscribe(
|
|
80
|
+
self,
|
|
81
|
+
event_type: type[Event] | str,
|
|
82
|
+
handler: EventHandler,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""订阅事件。
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
event_type: 事件类型或事件名称
|
|
88
|
+
handler: 事件处理函数
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def unsubscribe(
|
|
94
|
+
self,
|
|
95
|
+
event_type: type[Event] | str,
|
|
96
|
+
handler: EventHandler,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""取消订阅事件。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
event_type: 事件类型或事件名称
|
|
102
|
+
handler: 事件处理函数
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
async def publish(self, event: Event) -> None:
|
|
108
|
+
"""发布事件。
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
event: 事件对象
|
|
112
|
+
"""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def close(self) -> None:
|
|
117
|
+
"""关闭事件总线。"""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"Event",
|
|
123
|
+
"EventBackend",
|
|
124
|
+
"EventHandler",
|
|
125
|
+
"EventType",
|
|
126
|
+
"IEventBus",
|
|
127
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""事件总线管理器 - 命名多实例模式。
|
|
2
|
+
|
|
3
|
+
提供统一的事件总线管理接口,支持多后端和多实例。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from aury.boot.common.logging import logger
|
|
12
|
+
|
|
13
|
+
from .backends.memory import MemoryEventBus
|
|
14
|
+
from .backends.rabbitmq import RabbitMQEventBus
|
|
15
|
+
from .backends.redis import RedisEventBus
|
|
16
|
+
from .base import Event, EventBackend, EventHandler, IEventBus
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from aury.boot.application.config import EventInstanceConfig
|
|
20
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EventBusManager:
|
|
24
|
+
"""事件总线管理器(命名多实例)。
|
|
25
|
+
|
|
26
|
+
提供统一的事件总线管理接口,支持:
|
|
27
|
+
- 多实例管理(如 local、distributed 各自独立)
|
|
28
|
+
- 多后端支持(memory、redis、rabbitmq)
|
|
29
|
+
- 发布/订阅模式
|
|
30
|
+
|
|
31
|
+
使用示例:
|
|
32
|
+
# 默认实例(内存)
|
|
33
|
+
events = EventBusManager.get_instance()
|
|
34
|
+
await events.initialize(backend="memory")
|
|
35
|
+
|
|
36
|
+
# 分布式实例
|
|
37
|
+
distributed = EventBusManager.get_instance("distributed")
|
|
38
|
+
await distributed.initialize(
|
|
39
|
+
backend="redis",
|
|
40
|
+
redis_client=redis_client,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 订阅事件
|
|
44
|
+
@events.on(MyEvent)
|
|
45
|
+
async def handle_my_event(event: MyEvent):
|
|
46
|
+
print(event)
|
|
47
|
+
|
|
48
|
+
# 发布事件
|
|
49
|
+
await events.publish(MyEvent(...))
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
_instances: dict[str, EventBusManager] = {}
|
|
53
|
+
|
|
54
|
+
def __init__(self, name: str = "default") -> None:
|
|
55
|
+
"""初始化事件总线管理器。
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
name: 实例名称
|
|
59
|
+
"""
|
|
60
|
+
self.name = name
|
|
61
|
+
self._backend: IEventBus | None = None
|
|
62
|
+
self._backend_type: EventBackend | None = None
|
|
63
|
+
self._initialized: bool = False
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_instance(cls, name: str = "default") -> EventBusManager:
|
|
67
|
+
"""获取指定名称的实例。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: 实例名称,默认为 "default"
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
EventBusManager: 事件总线管理器实例
|
|
74
|
+
"""
|
|
75
|
+
if name not in cls._instances:
|
|
76
|
+
cls._instances[name] = cls(name)
|
|
77
|
+
return cls._instances[name]
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
81
|
+
"""重置实例(仅用于测试)。
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
85
|
+
|
|
86
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
87
|
+
"""
|
|
88
|
+
if name is None:
|
|
89
|
+
cls._instances.clear()
|
|
90
|
+
elif name in cls._instances:
|
|
91
|
+
del cls._instances[name]
|
|
92
|
+
|
|
93
|
+
async def initialize(
|
|
94
|
+
self,
|
|
95
|
+
backend: EventBackend | str = EventBackend.MEMORY,
|
|
96
|
+
*,
|
|
97
|
+
config: EventInstanceConfig | None = None,
|
|
98
|
+
redis_client: RedisClient | None = None,
|
|
99
|
+
url: str | None = None,
|
|
100
|
+
channel_prefix: str = "events:",
|
|
101
|
+
exchange_name: str = "events",
|
|
102
|
+
) -> EventBusManager:
|
|
103
|
+
"""初始化事件总线(链式调用)。
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
backend: 后端类型(当 config 不为 None 时忽略)
|
|
107
|
+
config: Event 实例配置(推荐,自动根据 backend 初始化)
|
|
108
|
+
redis_client: Redis 客户端(当 backend=redis 且 config=None 时需要)
|
|
109
|
+
url: 连接 URL(当 config=None 时需要)
|
|
110
|
+
channel_prefix: Redis 频道前缀
|
|
111
|
+
exchange_name: RabbitMQ 交换机名称
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
self: 支持链式调用
|
|
115
|
+
"""
|
|
116
|
+
if self._initialized:
|
|
117
|
+
logger.warning(f"事件总线管理器 [{self.name}] 已初始化,跳过")
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
# 使用配置对象时,从配置中提取参数
|
|
121
|
+
if config is not None:
|
|
122
|
+
backend = config.backend
|
|
123
|
+
url = config.url
|
|
124
|
+
|
|
125
|
+
# 处理字符串类型的 backend
|
|
126
|
+
if isinstance(backend, str):
|
|
127
|
+
try:
|
|
128
|
+
backend = EventBackend(backend.lower())
|
|
129
|
+
except ValueError:
|
|
130
|
+
supported = ", ".join(b.value for b in EventBackend)
|
|
131
|
+
raise ValueError(f"不支持的事件总线后端: {backend}。支持: {supported}")
|
|
132
|
+
|
|
133
|
+
self._backend_type = backend
|
|
134
|
+
|
|
135
|
+
# 根据后端类型创建实例,参数校验由后端自己处理
|
|
136
|
+
if backend == EventBackend.MEMORY:
|
|
137
|
+
self._backend = MemoryEventBus()
|
|
138
|
+
elif backend == EventBackend.REDIS:
|
|
139
|
+
self._backend = RedisEventBus(url=url, redis_client=redis_client, channel_prefix=channel_prefix)
|
|
140
|
+
elif backend == EventBackend.RABBITMQ:
|
|
141
|
+
self._backend = RabbitMQEventBus(url=url, exchange_name=exchange_name)
|
|
142
|
+
else:
|
|
143
|
+
supported = ", ".join(b.value for b in EventBackend)
|
|
144
|
+
raise ValueError(f"不支持的事件总线后端: {backend}。支持: {supported}")
|
|
145
|
+
|
|
146
|
+
self._initialized = True
|
|
147
|
+
logger.info(f"事件总线管理器 [{self.name}] 初始化完成: {backend.value}")
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def backend(self) -> IEventBus:
|
|
152
|
+
"""获取事件总线后端。"""
|
|
153
|
+
if self._backend is None:
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
f"事件总线管理器 [{self.name}] 未初始化,请先调用 initialize()"
|
|
156
|
+
)
|
|
157
|
+
return self._backend
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def backend_type(self) -> str:
|
|
161
|
+
"""获取当前后端类型。"""
|
|
162
|
+
return self._backend_type.value if self._backend_type else "unknown"
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def is_initialized(self) -> bool:
|
|
166
|
+
"""检查是否已初始化。"""
|
|
167
|
+
return self._initialized
|
|
168
|
+
|
|
169
|
+
def subscribe(
|
|
170
|
+
self,
|
|
171
|
+
event_type: type[Event] | str,
|
|
172
|
+
handler: EventHandler | None = None,
|
|
173
|
+
) -> EventHandler | Callable[[EventHandler], EventHandler]:
|
|
174
|
+
"""订阅事件(可作为装饰器使用)。
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
event_type: 事件类型或事件名称
|
|
178
|
+
handler: 事件处理函数(作为装饰器时为 None)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
EventHandler | Callable: 处理器或装饰器
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def decorator(fn: EventHandler) -> EventHandler:
|
|
185
|
+
self.backend.subscribe(event_type, fn)
|
|
186
|
+
return fn
|
|
187
|
+
|
|
188
|
+
if handler is not None:
|
|
189
|
+
return decorator(handler)
|
|
190
|
+
return decorator
|
|
191
|
+
|
|
192
|
+
# 别名方法
|
|
193
|
+
on = subscribe
|
|
194
|
+
|
|
195
|
+
def unsubscribe(
|
|
196
|
+
self,
|
|
197
|
+
event_type: type[Event] | str,
|
|
198
|
+
handler: EventHandler,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""取消订阅事件。"""
|
|
201
|
+
self.backend.unsubscribe(event_type, handler)
|
|
202
|
+
|
|
203
|
+
async def publish(self, event: Event) -> None:
|
|
204
|
+
"""发布事件。"""
|
|
205
|
+
await self.backend.publish(event)
|
|
206
|
+
|
|
207
|
+
# 别名方法
|
|
208
|
+
emit = publish
|
|
209
|
+
|
|
210
|
+
async def cleanup(self) -> None:
|
|
211
|
+
"""清理资源,关闭事件总线。"""
|
|
212
|
+
if self._backend:
|
|
213
|
+
await self._backend.close()
|
|
214
|
+
self._backend = None
|
|
215
|
+
self._initialized = False
|
|
216
|
+
logger.info(f"事件总线管理器 [{self.name}] 已关闭")
|
|
217
|
+
|
|
218
|
+
def __repr__(self) -> str:
|
|
219
|
+
"""字符串表示。"""
|
|
220
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
221
|
+
return f"<EventBusManager name={self.name} backend={self.backend_type} status={status}>"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
__all__ = ["EventBusManager"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""消息队列模块。
|
|
2
|
+
|
|
3
|
+
提供生产者/消费者模式的消息队列功能,用于异步任务处理、服务间通信等场景。
|
|
4
|
+
|
|
5
|
+
支持的后端:
|
|
6
|
+
- redis: Redis List 实现
|
|
7
|
+
- rabbitmq: RabbitMQ (aio-pika)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .backends import RabbitMQ, RedisMQ
|
|
11
|
+
from .base import IMQ, MQBackend, MQMessage
|
|
12
|
+
from .manager import MQManager
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# 接口和类型
|
|
16
|
+
"IMQ",
|
|
17
|
+
"MQBackend",
|
|
18
|
+
# 管理器
|
|
19
|
+
"MQManager",
|
|
20
|
+
"MQMessage",
|
|
21
|
+
# 后端实现
|
|
22
|
+
"RabbitMQ",
|
|
23
|
+
"RedisMQ",
|
|
24
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""RabbitMQ 消息队列后端。
|
|
2
|
+
|
|
3
|
+
使用 aio-pika 实现 RabbitMQ 消息队列。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from aury.boot.common.logging import logger
|
|
14
|
+
|
|
15
|
+
from ..base import IMQ, MQMessage
|
|
16
|
+
|
|
17
|
+
# 延迟导入 aio-pika(可选依赖)
|
|
18
|
+
try:
|
|
19
|
+
import aio_pika
|
|
20
|
+
from aio_pika import Message as AioPikaMessage
|
|
21
|
+
from aio_pika.abc import AbstractChannel, AbstractConnection, AbstractQueue
|
|
22
|
+
|
|
23
|
+
_AIO_PIKA_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_AIO_PIKA_AVAILABLE = False
|
|
26
|
+
aio_pika = None
|
|
27
|
+
AioPikaMessage = None
|
|
28
|
+
AbstractChannel = None
|
|
29
|
+
AbstractConnection = None
|
|
30
|
+
AbstractQueue = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RabbitMQ(IMQ):
|
|
34
|
+
"""RabbitMQ 消息队列实现。
|
|
35
|
+
|
|
36
|
+
使用 aio-pika 实现 AMQP 0.9.1 协议的消息队列。
|
|
37
|
+
|
|
38
|
+
注意:需要安装 aio-pika: pip install aio-pika
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, url: str) -> None:
|
|
42
|
+
"""初始化 RabbitMQ 消息队列。
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
url: RabbitMQ 连接 URL (amqp://user:pass@host:port/vhost)
|
|
46
|
+
"""
|
|
47
|
+
if not _AIO_PIKA_AVAILABLE:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"aio-pika 未安装。请安装: pip install aio-pika"
|
|
50
|
+
)
|
|
51
|
+
self._url = url
|
|
52
|
+
self._connection: AbstractConnection | None = None
|
|
53
|
+
self._channel: AbstractChannel | None = None
|
|
54
|
+
self._queues: dict[str, AbstractQueue] = {}
|
|
55
|
+
self._consuming = False
|
|
56
|
+
|
|
57
|
+
async def _ensure_connection(self) -> None:
|
|
58
|
+
"""确保连接已建立。"""
|
|
59
|
+
if self._connection is None or self._connection.is_closed:
|
|
60
|
+
self._connection = await aio_pika.connect_robust(self._url)
|
|
61
|
+
self._channel = await self._connection.channel()
|
|
62
|
+
logger.info("RabbitMQ 连接已建立")
|
|
63
|
+
|
|
64
|
+
async def _get_queue(self, queue: str) -> AbstractQueue:
|
|
65
|
+
"""获取或创建队列。"""
|
|
66
|
+
await self._ensure_connection()
|
|
67
|
+
if queue not in self._queues:
|
|
68
|
+
self._queues[queue] = await self._channel.declare_queue(
|
|
69
|
+
queue, durable=True
|
|
70
|
+
)
|
|
71
|
+
return self._queues[queue]
|
|
72
|
+
|
|
73
|
+
async def send(self, queue: str, message: MQMessage) -> str:
|
|
74
|
+
"""发送消息到队列。"""
|
|
75
|
+
await self._ensure_connection()
|
|
76
|
+
message.queue = queue
|
|
77
|
+
|
|
78
|
+
aio_message = AioPikaMessage(
|
|
79
|
+
body=json.dumps(message.to_dict()).encode(),
|
|
80
|
+
message_id=message.id,
|
|
81
|
+
headers=message.headers,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
await self._channel.default_exchange.publish(
|
|
85
|
+
aio_message,
|
|
86
|
+
routing_key=queue,
|
|
87
|
+
)
|
|
88
|
+
return message.id
|
|
89
|
+
|
|
90
|
+
async def receive(
|
|
91
|
+
self,
|
|
92
|
+
queue: str,
|
|
93
|
+
timeout: float | None = None,
|
|
94
|
+
) -> MQMessage | None:
|
|
95
|
+
"""从队列接收消息。"""
|
|
96
|
+
queue_obj = await self._get_queue(queue)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if timeout:
|
|
100
|
+
incoming = await asyncio.wait_for(
|
|
101
|
+
queue_obj.get(no_ack=False),
|
|
102
|
+
timeout=timeout,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
incoming = await queue_obj.get(no_ack=False)
|
|
106
|
+
|
|
107
|
+
if incoming is None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
data = json.loads(incoming.body.decode())
|
|
111
|
+
message = MQMessage.from_dict(data)
|
|
112
|
+
# 存储原始消息用于 ack/nack
|
|
113
|
+
message.headers["_raw_message"] = incoming
|
|
114
|
+
return message
|
|
115
|
+
|
|
116
|
+
except TimeoutError:
|
|
117
|
+
return None
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"接收消息失败: {e}")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
async def ack(self, message: MQMessage) -> None:
|
|
123
|
+
"""确认消息已处理。"""
|
|
124
|
+
raw_message = message.headers.get("_raw_message")
|
|
125
|
+
if raw_message:
|
|
126
|
+
await raw_message.ack()
|
|
127
|
+
|
|
128
|
+
async def nack(self, message: MQMessage, requeue: bool = True) -> None:
|
|
129
|
+
"""拒绝消息。"""
|
|
130
|
+
raw_message = message.headers.get("_raw_message")
|
|
131
|
+
if raw_message:
|
|
132
|
+
await raw_message.nack(requeue=requeue)
|
|
133
|
+
|
|
134
|
+
async def consume(
|
|
135
|
+
self,
|
|
136
|
+
queue: str,
|
|
137
|
+
handler: Callable[[MQMessage], Any],
|
|
138
|
+
*,
|
|
139
|
+
prefetch: int = 1,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""消费队列消息。"""
|
|
142
|
+
await self._ensure_connection()
|
|
143
|
+
await self._channel.set_qos(prefetch_count=prefetch)
|
|
144
|
+
|
|
145
|
+
queue_obj = await self._get_queue(queue)
|
|
146
|
+
self._consuming = True
|
|
147
|
+
logger.info(f"开始消费队列: {queue}")
|
|
148
|
+
|
|
149
|
+
async with queue_obj.iterator() as queue_iter:
|
|
150
|
+
async for incoming in queue_iter:
|
|
151
|
+
if not self._consuming:
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(incoming.body.decode())
|
|
156
|
+
message = MQMessage.from_dict(data)
|
|
157
|
+
message.headers["_raw_message"] = incoming
|
|
158
|
+
|
|
159
|
+
result = handler(message)
|
|
160
|
+
if asyncio.iscoroutine(result):
|
|
161
|
+
await result
|
|
162
|
+
await incoming.ack()
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"处理消息失败: {e}")
|
|
166
|
+
await incoming.nack(requeue=True)
|
|
167
|
+
|
|
168
|
+
async def close(self) -> None:
|
|
169
|
+
"""关闭连接。"""
|
|
170
|
+
self._consuming = False
|
|
171
|
+
if self._connection:
|
|
172
|
+
await self._connection.close()
|
|
173
|
+
self._connection = None
|
|
174
|
+
self._channel = None
|
|
175
|
+
self._queues.clear()
|
|
176
|
+
logger.debug("RabbitMQ 连接已关闭")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = ["RabbitMQ"]
|