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.
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 +27 -8
  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.4.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.4.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.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
  98. {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,9 @@
1
+ """消息队列后端实现。"""
2
+
3
+ from .rabbitmq import RabbitMQ
4
+ from .redis import RedisMQ
5
+
6
+ __all__ = [
7
+ "RabbitMQ",
8
+ "RedisMQ",
9
+ ]
@@ -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"]