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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Redis 消息队列后端。
|
|
2
|
+
|
|
3
|
+
使用 Redis List 实现简单的消息队列。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
import json
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from aury.boot.common.logging import logger
|
|
14
|
+
|
|
15
|
+
from ..base import IMQ, MQMessage
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RedisMQ(IMQ):
|
|
22
|
+
"""Redis 消息队列实现。
|
|
23
|
+
|
|
24
|
+
使用 Redis List (LPUSH/BRPOP) 实现可靠的消息队列。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
url: str | None = None,
|
|
30
|
+
*,
|
|
31
|
+
redis_client: RedisClient | None = None,
|
|
32
|
+
prefix: str = "mq:",
|
|
33
|
+
) -> None:
|
|
34
|
+
"""初始化 Redis 消息队列。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
url: Redis 连接 URL(当 redis_client 为 None 时必须提供)
|
|
38
|
+
redis_client: RedisClient 实例(可选,优先使用)
|
|
39
|
+
prefix: 队列名称前缀
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: 当 url 和 redis_client 都为 None 时
|
|
43
|
+
"""
|
|
44
|
+
if redis_client is None and url is None:
|
|
45
|
+
raise ValueError("Redis 消息队列需要提供 url 或 redis_client 参数")
|
|
46
|
+
|
|
47
|
+
self._url = url
|
|
48
|
+
self._client = redis_client
|
|
49
|
+
self._prefix = prefix
|
|
50
|
+
self._consuming = False
|
|
51
|
+
self._owns_client = False # 是否自己创建的客户端
|
|
52
|
+
|
|
53
|
+
async def _ensure_client(self) -> None:
|
|
54
|
+
"""确保 Redis 客户端已初始化。"""
|
|
55
|
+
if self._client is None and self._url:
|
|
56
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
57
|
+
self._client = RedisClient()
|
|
58
|
+
await self._client.initialize(url=self._url)
|
|
59
|
+
self._owns_client = True
|
|
60
|
+
|
|
61
|
+
def _queue_key(self, queue: str) -> str:
|
|
62
|
+
"""获取队列的 Redis key。"""
|
|
63
|
+
return f"{self._prefix}{queue}"
|
|
64
|
+
|
|
65
|
+
def _processing_key(self, queue: str) -> str:
|
|
66
|
+
"""获取处理中队列的 Redis key。"""
|
|
67
|
+
return f"{self._prefix}{queue}:processing"
|
|
68
|
+
|
|
69
|
+
async def send(self, queue: str, message: MQMessage) -> str:
|
|
70
|
+
"""发送消息到队列。"""
|
|
71
|
+
await self._ensure_client()
|
|
72
|
+
message.queue = queue
|
|
73
|
+
data = json.dumps(message.to_dict())
|
|
74
|
+
await self._client.connection.lpush(self._queue_key(queue), data)
|
|
75
|
+
return message.id
|
|
76
|
+
|
|
77
|
+
async def receive(
|
|
78
|
+
self,
|
|
79
|
+
queue: str,
|
|
80
|
+
timeout: float | None = None,
|
|
81
|
+
) -> MQMessage | None:
|
|
82
|
+
"""从队列接收消息。"""
|
|
83
|
+
await self._ensure_client()
|
|
84
|
+
timeout_int = int(timeout) if timeout else 0
|
|
85
|
+
result = await self._client.connection.brpop(
|
|
86
|
+
self._queue_key(queue),
|
|
87
|
+
timeout=timeout_int,
|
|
88
|
+
)
|
|
89
|
+
if result is None:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
_, data = result
|
|
93
|
+
try:
|
|
94
|
+
msg_dict = json.loads(data)
|
|
95
|
+
message = MQMessage.from_dict(msg_dict)
|
|
96
|
+
# 将消息放入处理中队列
|
|
97
|
+
await self._client.connection.hset(
|
|
98
|
+
self._processing_key(queue),
|
|
99
|
+
message.id,
|
|
100
|
+
data,
|
|
101
|
+
)
|
|
102
|
+
return message
|
|
103
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
104
|
+
logger.error(f"解析消息失败: {e}")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
async def ack(self, message: MQMessage) -> None:
|
|
108
|
+
"""确认消息已处理。"""
|
|
109
|
+
if message.queue:
|
|
110
|
+
await self._client.connection.hdel(
|
|
111
|
+
self._processing_key(message.queue),
|
|
112
|
+
message.id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def nack(self, message: MQMessage, requeue: bool = True) -> None:
|
|
116
|
+
"""拒绝消息。"""
|
|
117
|
+
if message.queue:
|
|
118
|
+
# 从处理中队列移除
|
|
119
|
+
await self._client.connection.hdel(
|
|
120
|
+
self._processing_key(message.queue),
|
|
121
|
+
message.id,
|
|
122
|
+
)
|
|
123
|
+
if requeue and message.retry_count < message.max_retries:
|
|
124
|
+
# 重新入队
|
|
125
|
+
message.retry_count += 1
|
|
126
|
+
await self.send(message.queue, message)
|
|
127
|
+
|
|
128
|
+
async def consume(
|
|
129
|
+
self,
|
|
130
|
+
queue: str,
|
|
131
|
+
handler: Callable[[MQMessage], Any],
|
|
132
|
+
*,
|
|
133
|
+
prefetch: int = 1,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""消费队列消息。"""
|
|
136
|
+
self._consuming = True
|
|
137
|
+
logger.info(f"开始消费队列: {queue}")
|
|
138
|
+
|
|
139
|
+
while self._consuming:
|
|
140
|
+
try:
|
|
141
|
+
message = await self.receive(queue, timeout=1.0)
|
|
142
|
+
if message is None:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = handler(message)
|
|
147
|
+
if asyncio.iscoroutine(result):
|
|
148
|
+
await result
|
|
149
|
+
await self.ack(message)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"处理消息失败: {e}")
|
|
152
|
+
await self.nack(message, requeue=True)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"消费消息异常: {e}")
|
|
156
|
+
await asyncio.sleep(1)
|
|
157
|
+
|
|
158
|
+
async def close(self) -> None:
|
|
159
|
+
"""关闭连接。"""
|
|
160
|
+
self._consuming = False
|
|
161
|
+
if self._owns_client and self._client:
|
|
162
|
+
await self._client.close()
|
|
163
|
+
self._client = None
|
|
164
|
+
logger.debug("Redis 消息队列已关闭")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["RedisMQ"]
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MQBackend(Enum):
|
|
18
|
+
"""消息队列后端类型。"""
|
|
19
|
+
|
|
20
|
+
REDIS = "redis"
|
|
21
|
+
RABBITMQ = "rabbitmq"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class MQMessage:
|
|
26
|
+
"""消息队列消息。"""
|
|
27
|
+
|
|
28
|
+
body: Any
|
|
29
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
30
|
+
queue: str | None = None
|
|
31
|
+
headers: dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
33
|
+
retry_count: int = 0
|
|
34
|
+
max_retries: int = 3
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, Any]:
|
|
37
|
+
"""转换为字典。"""
|
|
38
|
+
return {
|
|
39
|
+
"id": self.id,
|
|
40
|
+
"body": self.body,
|
|
41
|
+
"queue": self.queue,
|
|
42
|
+
"headers": self.headers,
|
|
43
|
+
"timestamp": self.timestamp.isoformat(),
|
|
44
|
+
"retry_count": self.retry_count,
|
|
45
|
+
"max_retries": self.max_retries,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, data: dict[str, Any]) -> MQMessage:
|
|
50
|
+
"""从字典创建消息。"""
|
|
51
|
+
return cls(
|
|
52
|
+
id=data.get("id", str(uuid.uuid4())),
|
|
53
|
+
body=data["body"],
|
|
54
|
+
queue=data.get("queue"),
|
|
55
|
+
headers=data.get("headers", {}),
|
|
56
|
+
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
57
|
+
if data.get("timestamp")
|
|
58
|
+
else datetime.now(),
|
|
59
|
+
retry_count=data.get("retry_count", 0),
|
|
60
|
+
max_retries=data.get("max_retries", 3),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class IMQ(ABC):
|
|
65
|
+
"""消息队列接口。"""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
async def send(self, queue: str, message: MQMessage) -> str:
|
|
69
|
+
"""发送消息到队列。
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
queue: 队列名称
|
|
73
|
+
message: 消息对象
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: 消息 ID
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
async def receive(
|
|
82
|
+
self,
|
|
83
|
+
queue: str,
|
|
84
|
+
timeout: float | None = None,
|
|
85
|
+
) -> MQMessage | None:
|
|
86
|
+
"""从队列接收消息。
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
queue: 队列名称
|
|
90
|
+
timeout: 超时时间(秒),None 表示阻塞等待
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
MQMessage | None: 消息对象,超时返回 None
|
|
94
|
+
"""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def ack(self, message: MQMessage) -> None:
|
|
99
|
+
"""确认消息已处理。
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
message: 消息对象
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
async def nack(self, message: MQMessage, requeue: bool = True) -> None:
|
|
108
|
+
"""拒绝消息。
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
message: 消息对象
|
|
112
|
+
requeue: 是否重新入队
|
|
113
|
+
"""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
async def consume(
|
|
118
|
+
self,
|
|
119
|
+
queue: str,
|
|
120
|
+
handler: Callable[[MQMessage], Any],
|
|
121
|
+
*,
|
|
122
|
+
prefetch: int = 1,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""消费队列消息。
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
queue: 队列名称
|
|
128
|
+
handler: 消息处理函数
|
|
129
|
+
prefetch: 预取数量
|
|
130
|
+
"""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def close(self) -> None:
|
|
135
|
+
"""关闭连接。"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"IMQ",
|
|
141
|
+
"MQBackend",
|
|
142
|
+
"MQMessage",
|
|
143
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
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.rabbitmq import RabbitMQ
|
|
14
|
+
from .backends.redis import RedisMQ
|
|
15
|
+
from .base import IMQ, MQBackend, MQMessage
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from aury.boot.application.config import MQInstanceConfig
|
|
19
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MQManager:
|
|
23
|
+
"""消息队列管理器(命名多实例)。
|
|
24
|
+
|
|
25
|
+
提供统一的消息队列管理接口,支持:
|
|
26
|
+
- 多实例管理(如 tasks、notifications 各自独立)
|
|
27
|
+
- 多后端支持(redis、rabbitmq)
|
|
28
|
+
- 生产者/消费者模式
|
|
29
|
+
|
|
30
|
+
使用示例:
|
|
31
|
+
# 默认实例
|
|
32
|
+
mq = MQManager.get_instance()
|
|
33
|
+
await mq.initialize(backend="redis", redis_client=redis_client)
|
|
34
|
+
|
|
35
|
+
# 命名实例
|
|
36
|
+
task_mq = MQManager.get_instance("tasks")
|
|
37
|
+
notification_mq = MQManager.get_instance("notifications")
|
|
38
|
+
|
|
39
|
+
# 发送消息
|
|
40
|
+
await mq.send("orders", MQMessage(body={"order_id": 123}))
|
|
41
|
+
|
|
42
|
+
# 消费消息
|
|
43
|
+
await mq.consume("orders", handler=process_order)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_instances: dict[str, MQManager] = {}
|
|
47
|
+
|
|
48
|
+
def __init__(self, name: str = "default") -> None:
|
|
49
|
+
"""初始化消息队列管理器。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: 实例名称
|
|
53
|
+
"""
|
|
54
|
+
self.name = name
|
|
55
|
+
self._backend: IMQ | None = None
|
|
56
|
+
self._backend_type: MQBackend | None = None
|
|
57
|
+
self._initialized: bool = False
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_instance(cls, name: str = "default") -> MQManager:
|
|
61
|
+
"""获取指定名称的实例。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: 实例名称,默认为 "default"
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
MQManager: 消息队列管理器实例
|
|
68
|
+
"""
|
|
69
|
+
if name not in cls._instances:
|
|
70
|
+
cls._instances[name] = cls(name)
|
|
71
|
+
return cls._instances[name]
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
75
|
+
"""重置实例(仅用于测试)。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
79
|
+
|
|
80
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
81
|
+
"""
|
|
82
|
+
if name is None:
|
|
83
|
+
cls._instances.clear()
|
|
84
|
+
elif name in cls._instances:
|
|
85
|
+
del cls._instances[name]
|
|
86
|
+
|
|
87
|
+
async def initialize(
|
|
88
|
+
self,
|
|
89
|
+
backend: MQBackend | str = MQBackend.REDIS,
|
|
90
|
+
*,
|
|
91
|
+
config: MQInstanceConfig | None = None,
|
|
92
|
+
redis_client: RedisClient | None = None,
|
|
93
|
+
url: str | None = None,
|
|
94
|
+
prefix: str = "mq:",
|
|
95
|
+
) -> MQManager:
|
|
96
|
+
"""初始化消息队列(链式调用)。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
backend: 后端类型(当 config 不为 None 时忽略)
|
|
100
|
+
config: MQ 实例配置(推荐,自动根据 backend 初始化)
|
|
101
|
+
redis_client: Redis 客户端(当 backend=redis 且 config=None 时需要)
|
|
102
|
+
url: 连接 URL(当 config=None 时需要)
|
|
103
|
+
prefix: Redis 队列名称前缀
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
self: 支持链式调用
|
|
107
|
+
"""
|
|
108
|
+
if self._initialized:
|
|
109
|
+
logger.warning(f"消息队列管理器 [{self.name}] 已初始化,跳过")
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
# 使用配置对象时,从配置中提取参数
|
|
113
|
+
if config is not None:
|
|
114
|
+
backend = config.backend
|
|
115
|
+
url = config.url
|
|
116
|
+
|
|
117
|
+
# 处理字符串类型的 backend
|
|
118
|
+
if isinstance(backend, str):
|
|
119
|
+
try:
|
|
120
|
+
backend = MQBackend(backend.lower())
|
|
121
|
+
except ValueError:
|
|
122
|
+
supported = ", ".join(b.value for b in MQBackend)
|
|
123
|
+
raise ValueError(f"不支持的消息队列后端: {backend}。支持: {supported}")
|
|
124
|
+
|
|
125
|
+
self._backend_type = backend
|
|
126
|
+
|
|
127
|
+
# 根据后端类型创建实例,参数校验由后端自己处理
|
|
128
|
+
if backend == MQBackend.REDIS:
|
|
129
|
+
self._backend = RedisMQ(url=url, redis_client=redis_client, prefix=prefix)
|
|
130
|
+
elif backend == MQBackend.RABBITMQ:
|
|
131
|
+
self._backend = RabbitMQ(url=url)
|
|
132
|
+
else:
|
|
133
|
+
supported = ", ".join(b.value for b in MQBackend)
|
|
134
|
+
raise ValueError(f"不支持的消息队列后端: {backend}。支持: {supported}")
|
|
135
|
+
|
|
136
|
+
self._initialized = True
|
|
137
|
+
logger.info(f"消息队列管理器 [{self.name}] 初始化完成: {backend.value}")
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def backend(self) -> IMQ:
|
|
142
|
+
"""获取消息队列后端。"""
|
|
143
|
+
if self._backend is None:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
f"消息队列管理器 [{self.name}] 未初始化,请先调用 initialize()"
|
|
146
|
+
)
|
|
147
|
+
return self._backend
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def backend_type(self) -> str:
|
|
151
|
+
"""获取当前后端类型。"""
|
|
152
|
+
return self._backend_type.value if self._backend_type else "unknown"
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_initialized(self) -> bool:
|
|
156
|
+
"""检查是否已初始化。"""
|
|
157
|
+
return self._initialized
|
|
158
|
+
|
|
159
|
+
async def send(
|
|
160
|
+
self,
|
|
161
|
+
queue: str,
|
|
162
|
+
message: MQMessage | dict | Any,
|
|
163
|
+
*,
|
|
164
|
+
headers: dict[str, Any] | None = None,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""发送消息到队列。
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
queue: 队列名称
|
|
170
|
+
message: 消息内容(MQMessage、字典或其他可序列化对象)
|
|
171
|
+
headers: 消息头
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: 消息 ID
|
|
175
|
+
"""
|
|
176
|
+
if isinstance(message, MQMessage):
|
|
177
|
+
msg = message
|
|
178
|
+
elif isinstance(message, dict):
|
|
179
|
+
msg = MQMessage(body=message, headers=headers or {})
|
|
180
|
+
else:
|
|
181
|
+
msg = MQMessage(body=message, headers=headers or {})
|
|
182
|
+
|
|
183
|
+
return await self.backend.send(queue, msg)
|
|
184
|
+
|
|
185
|
+
async def receive(
|
|
186
|
+
self,
|
|
187
|
+
queue: str,
|
|
188
|
+
timeout: float | None = None,
|
|
189
|
+
) -> MQMessage | None:
|
|
190
|
+
"""从队列接收消息。
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
queue: 队列名称
|
|
194
|
+
timeout: 超时时间(秒)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
MQMessage | None: 消息对象
|
|
198
|
+
"""
|
|
199
|
+
return await self.backend.receive(queue, timeout)
|
|
200
|
+
|
|
201
|
+
async def ack(self, message: MQMessage) -> None:
|
|
202
|
+
"""确认消息已处理。"""
|
|
203
|
+
await self.backend.ack(message)
|
|
204
|
+
|
|
205
|
+
async def nack(self, message: MQMessage, requeue: bool = True) -> None:
|
|
206
|
+
"""拒绝消息。"""
|
|
207
|
+
await self.backend.nack(message, requeue)
|
|
208
|
+
|
|
209
|
+
async def consume(
|
|
210
|
+
self,
|
|
211
|
+
queue: str,
|
|
212
|
+
handler: Callable[[MQMessage], Any],
|
|
213
|
+
*,
|
|
214
|
+
prefetch: int = 1,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""消费队列消息。
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
queue: 队列名称
|
|
220
|
+
handler: 消息处理函数
|
|
221
|
+
prefetch: 预取数量
|
|
222
|
+
"""
|
|
223
|
+
await self.backend.consume(queue, handler, prefetch=prefetch)
|
|
224
|
+
|
|
225
|
+
async def cleanup(self) -> None:
|
|
226
|
+
"""清理资源,关闭连接。"""
|
|
227
|
+
if self._backend:
|
|
228
|
+
await self._backend.close()
|
|
229
|
+
self._backend = None
|
|
230
|
+
self._initialized = False
|
|
231
|
+
logger.info(f"消息队列管理器 [{self.name}] 已关闭")
|
|
232
|
+
|
|
233
|
+
def __repr__(self) -> str:
|
|
234
|
+
"""字符串表示。"""
|
|
235
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
236
|
+
return f"<MQManager name={self.name} backend={self.backend_type} status={status}>"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["MQManager"]
|
|
@@ -117,10 +117,13 @@ class SchedulerManager:
|
|
|
117
117
|
elif name in cls._instances:
|
|
118
118
|
del cls._instances[name]
|
|
119
119
|
|
|
120
|
-
async def initialize(self) ->
|
|
121
|
-
"""
|
|
120
|
+
async def initialize(self) -> SchedulerManager:
|
|
121
|
+
"""初始化调度器(链式调用)。
|
|
122
122
|
|
|
123
|
-
调度器现在在 get_instance()
|
|
123
|
+
调度器现在在 get_instance() 时同步初始化,此方法保留以保持后向兼容。
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
self: 支持链式调用
|
|
124
127
|
"""
|
|
125
128
|
if not self._initialized:
|
|
126
129
|
# 如果还未初始化(理论上不会发生),进行初始化
|
|
@@ -131,6 +134,7 @@ class SchedulerManager:
|
|
|
131
134
|
self._scheduler = AsyncIOScheduler()
|
|
132
135
|
self._initialized = True
|
|
133
136
|
logger.debug("调度器已就绪")
|
|
137
|
+
return self
|
|
134
138
|
|
|
135
139
|
@property
|
|
136
140
|
def scheduler(self) -> AsyncIOScheduler:
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
本包基于 aury-sdk-storage 提供的实现,对外暴露统一接口与管理器。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from .base import StorageManager
|
|
7
|
-
from .exceptions import StorageBackendError, StorageError, StorageNotFoundError
|
|
8
|
-
from .factory import StorageFactory
|
|
9
|
-
|
|
10
6
|
# 从 SDK 直接导出核心类型
|
|
11
7
|
from aury.sdk.storage.storage import (
|
|
12
8
|
IStorage,
|
|
@@ -18,21 +14,25 @@ from aury.sdk.storage.storage import (
|
|
|
18
14
|
UploadResult,
|
|
19
15
|
)
|
|
20
16
|
|
|
17
|
+
from .base import StorageManager
|
|
18
|
+
from .exceptions import StorageBackendError, StorageError, StorageNotFoundError
|
|
19
|
+
from .factory import StorageFactory
|
|
20
|
+
|
|
21
21
|
__all__ = [
|
|
22
22
|
# SDK 类型
|
|
23
23
|
"IStorage",
|
|
24
24
|
"LocalStorage",
|
|
25
25
|
"S3Storage",
|
|
26
26
|
"StorageBackend",
|
|
27
|
+
"StorageBackendError",
|
|
27
28
|
"StorageConfig",
|
|
29
|
+
# 异常
|
|
30
|
+
"StorageError",
|
|
31
|
+
"StorageFactory",
|
|
28
32
|
"StorageFile",
|
|
29
|
-
"UploadResult",
|
|
30
33
|
# 管理器与工厂
|
|
31
34
|
"StorageManager",
|
|
32
|
-
"StorageFactory",
|
|
33
|
-
# 异常
|
|
34
|
-
"StorageError",
|
|
35
|
-
"StorageBackendError",
|
|
36
35
|
"StorageNotFoundError",
|
|
36
|
+
"UploadResult",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from aury.boot.common.logging import logger
|
|
8
9
|
from aury.sdk.storage.storage import (
|
|
9
10
|
IStorage,
|
|
10
11
|
LocalStorage,
|
|
@@ -14,8 +15,6 @@ from aury.sdk.storage.storage import (
|
|
|
14
15
|
StorageFile,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
|
-
from aury.boot.common.logging import logger
|
|
18
|
-
|
|
19
18
|
|
|
20
19
|
class StorageManager:
|
|
21
20
|
"""存储管理器(命名多实例)。
|
|
@@ -69,8 +68,15 @@ class StorageManager:
|
|
|
69
68
|
elif name in cls._instances:
|
|
70
69
|
del cls._instances[name]
|
|
71
70
|
|
|
72
|
-
async def
|
|
73
|
-
"""
|
|
71
|
+
async def initialize(self, config: StorageConfig) -> StorageManager:
|
|
72
|
+
"""初始化存储后端(链式调用)。
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
config: 存储配置对象
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
self: 支持链式调用
|
|
79
|
+
"""
|
|
74
80
|
self._config = config
|
|
75
81
|
if config.backend == StorageBackend.LOCAL:
|
|
76
82
|
self._backend = LocalStorage(base_path=config.base_path or "./storage")
|
|
@@ -78,11 +84,17 @@ class StorageManager:
|
|
|
78
84
|
# S3/COS/OSS/MinIO 统一走 S3Storage
|
|
79
85
|
self._backend = S3Storage(config)
|
|
80
86
|
logger.info(f"存储管理器初始化完成: {config.backend.value}")
|
|
87
|
+
return self
|
|
81
88
|
|
|
89
|
+
@property
|
|
90
|
+
def is_initialized(self) -> bool:
|
|
91
|
+
"""检查是否已初始化。"""
|
|
92
|
+
return self._backend is not None
|
|
93
|
+
|
|
82
94
|
@property
|
|
83
95
|
def backend(self) -> IStorage:
|
|
84
96
|
if self._backend is None:
|
|
85
|
-
raise RuntimeError("存储管理器未初始化,请先调用
|
|
97
|
+
raise RuntimeError("存储管理器未初始化,请先调用 initialize()")
|
|
86
98
|
return self._backend
|
|
87
99
|
|
|
88
100
|
async def upload_file(
|
|
@@ -9,7 +9,7 @@ from .exceptions import (
|
|
|
9
9
|
TaskExecutionError,
|
|
10
10
|
TaskQueueError,
|
|
11
11
|
)
|
|
12
|
-
from .manager import TaskManager, TaskProxy,
|
|
12
|
+
from .manager import TaskManager, TaskProxy, conditional_task
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
15
|
"TaskError",
|
|
@@ -19,6 +19,6 @@ __all__ = [
|
|
|
19
19
|
"TaskQueueError",
|
|
20
20
|
"TaskQueueName",
|
|
21
21
|
"TaskRunMode",
|
|
22
|
-
"
|
|
22
|
+
"conditional_task",
|
|
23
23
|
]
|
|
24
24
|
|