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.
- 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 +30 -9
- 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.3.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.3.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.3.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""RabbitMQ 客户端管理器 - 命名多实例模式。
|
|
2
|
+
|
|
3
|
+
提供统一的 RabbitMQ 连接管理,支持多实例。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from aury.boot.common.logging import logger
|
|
11
|
+
|
|
12
|
+
from .config import RabbitMQConfig
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from aio_pika import Channel, Connection, RobustConnection
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RabbitMQClient:
|
|
19
|
+
"""RabbitMQ 客户端管理器(命名多实例)。
|
|
20
|
+
|
|
21
|
+
提供统一的 RabbitMQ 连接管理接口,支持:
|
|
22
|
+
- 多实例管理(如 events、tasks 各自独立)
|
|
23
|
+
- 连接和通道管理
|
|
24
|
+
- 健康检查
|
|
25
|
+
- 链式配置
|
|
26
|
+
|
|
27
|
+
使用示例:
|
|
28
|
+
# 默认实例
|
|
29
|
+
client = RabbitMQClient.get_instance()
|
|
30
|
+
client.configure(url="amqp://guest:guest@localhost:5672/")
|
|
31
|
+
await client.initialize()
|
|
32
|
+
|
|
33
|
+
# 命名实例
|
|
34
|
+
events_mq = RabbitMQClient.get_instance("events")
|
|
35
|
+
tasks_mq = RabbitMQClient.get_instance("tasks")
|
|
36
|
+
|
|
37
|
+
# 获取通道
|
|
38
|
+
channel = await client.get_channel()
|
|
39
|
+
await channel.default_exchange.publish(message, routing_key="test")
|
|
40
|
+
|
|
41
|
+
# 清理
|
|
42
|
+
await client.cleanup()
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_instances: dict[str, RabbitMQClient] = {}
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str = "default") -> None:
|
|
48
|
+
"""初始化 RabbitMQ 客户端管理器。
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name: 实例名称
|
|
52
|
+
"""
|
|
53
|
+
self.name = name
|
|
54
|
+
self._config: RabbitMQConfig | None = None
|
|
55
|
+
self._connection: RobustConnection | None = None
|
|
56
|
+
self._channel: Channel | None = None
|
|
57
|
+
self._initialized: bool = False
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_instance(cls, name: str = "default") -> RabbitMQClient:
|
|
61
|
+
"""获取指定名称的实例。
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: 实例名称,默认为 "default"
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
RabbitMQClient: RabbitMQ 客户端实例
|
|
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
|
+
def configure(
|
|
88
|
+
self,
|
|
89
|
+
url: str | None = None,
|
|
90
|
+
*,
|
|
91
|
+
heartbeat: int | None = None,
|
|
92
|
+
connection_timeout: float | None = None,
|
|
93
|
+
blocked_connection_timeout: float | None = None,
|
|
94
|
+
prefetch_count: int | None = None,
|
|
95
|
+
publisher_confirms: bool | None = None,
|
|
96
|
+
config: RabbitMQConfig | None = None,
|
|
97
|
+
) -> RabbitMQClient:
|
|
98
|
+
"""配置 RabbitMQ 客户端(链式调用)。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
url: AMQP 连接 URL
|
|
102
|
+
heartbeat: 心跳间隔
|
|
103
|
+
connection_timeout: 连接超时
|
|
104
|
+
blocked_connection_timeout: 阻塞连接超时
|
|
105
|
+
prefetch_count: 预取消息数量
|
|
106
|
+
publisher_confirms: 是否启用发布确认
|
|
107
|
+
config: 直接传入 RabbitMQConfig 对象
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
self: 支持链式调用
|
|
111
|
+
"""
|
|
112
|
+
if config:
|
|
113
|
+
self._config = config
|
|
114
|
+
else:
|
|
115
|
+
config_dict = {}
|
|
116
|
+
if url is not None:
|
|
117
|
+
config_dict["url"] = url
|
|
118
|
+
if heartbeat is not None:
|
|
119
|
+
config_dict["heartbeat"] = heartbeat
|
|
120
|
+
if connection_timeout is not None:
|
|
121
|
+
config_dict["connection_timeout"] = connection_timeout
|
|
122
|
+
if blocked_connection_timeout is not None:
|
|
123
|
+
config_dict["blocked_connection_timeout"] = blocked_connection_timeout
|
|
124
|
+
if prefetch_count is not None:
|
|
125
|
+
config_dict["prefetch_count"] = prefetch_count
|
|
126
|
+
if publisher_confirms is not None:
|
|
127
|
+
config_dict["publisher_confirms"] = publisher_confirms
|
|
128
|
+
|
|
129
|
+
self._config = RabbitMQConfig(**config_dict)
|
|
130
|
+
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def initialize(self) -> RabbitMQClient:
|
|
134
|
+
"""初始化 RabbitMQ 连接。
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
self: 支持链式调用
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
RuntimeError: 未配置时调用
|
|
141
|
+
ConnectionError: 连接失败
|
|
142
|
+
"""
|
|
143
|
+
if self._initialized:
|
|
144
|
+
logger.warning(f"RabbitMQ 客户端 [{self.name}] 已初始化,跳过")
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
if not self._config:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"RabbitMQ 客户端 [{self.name}] 未配置,请先调用 configure()"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
import aio_pika
|
|
154
|
+
|
|
155
|
+
# 创建连接
|
|
156
|
+
self._connection = await aio_pika.connect_robust(
|
|
157
|
+
self._config.url,
|
|
158
|
+
heartbeat=self._config.heartbeat,
|
|
159
|
+
timeout=self._config.connection_timeout,
|
|
160
|
+
blocked_connection_timeout=self._config.blocked_connection_timeout,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# 创建默认通道
|
|
164
|
+
self._channel = await self._connection.channel(
|
|
165
|
+
publisher_confirms=self._config.publisher_confirms
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 设置 QoS
|
|
169
|
+
await self._channel.set_qos(prefetch_count=self._config.prefetch_count)
|
|
170
|
+
|
|
171
|
+
self._initialized = True
|
|
172
|
+
masked_url = self._mask_url(self._config.url)
|
|
173
|
+
logger.info(f"RabbitMQ 客户端 [{self.name}] 初始化完成: {masked_url}")
|
|
174
|
+
|
|
175
|
+
return self
|
|
176
|
+
except ImportError:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
"需要安装 aio-pika: pip install aio-pika"
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"RabbitMQ 客户端 [{self.name}] 初始化失败: {e}")
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
def _mask_url(self, url: str) -> str:
|
|
185
|
+
"""URL 脱敏(隐藏密码)。"""
|
|
186
|
+
if "@" in url:
|
|
187
|
+
# amqp://user:password@host:port/ -> amqp://user:***@host:port/
|
|
188
|
+
parts = url.split("@")
|
|
189
|
+
prefix = parts[0]
|
|
190
|
+
suffix = parts[1]
|
|
191
|
+
if ":" in prefix:
|
|
192
|
+
# 找到最后一个冒号(密码前)
|
|
193
|
+
last_colon = prefix.rfind(":")
|
|
194
|
+
scheme_and_user = prefix[:last_colon]
|
|
195
|
+
return f"{scheme_and_user}:***@{suffix}"
|
|
196
|
+
return url
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def is_initialized(self) -> bool:
|
|
200
|
+
"""检查是否已初始化。"""
|
|
201
|
+
return self._initialized
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def connection(self) -> Connection:
|
|
205
|
+
"""获取 RabbitMQ 连接。
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Connection: RabbitMQ 连接实例
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
RuntimeError: 未初始化时调用
|
|
212
|
+
"""
|
|
213
|
+
if not self._connection:
|
|
214
|
+
raise RuntimeError(
|
|
215
|
+
f"RabbitMQ 客户端 [{self.name}] 未初始化,请先调用 initialize()"
|
|
216
|
+
)
|
|
217
|
+
return self._connection
|
|
218
|
+
|
|
219
|
+
async def get_channel(self, *, new: bool = False) -> Channel:
|
|
220
|
+
"""获取 RabbitMQ 通道。
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
new: 是否创建新通道(默认使用共享通道)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Channel: RabbitMQ 通道实例
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
RuntimeError: 未初始化时调用
|
|
230
|
+
"""
|
|
231
|
+
if not self._connection:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
f"RabbitMQ 客户端 [{self.name}] 未初始化,请先调用 initialize()"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if new:
|
|
237
|
+
channel = await self._connection.channel(
|
|
238
|
+
publisher_confirms=self._config.publisher_confirms if self._config else True
|
|
239
|
+
)
|
|
240
|
+
if self._config:
|
|
241
|
+
await channel.set_qos(prefetch_count=self._config.prefetch_count)
|
|
242
|
+
return channel
|
|
243
|
+
|
|
244
|
+
if not self._channel or self._channel.is_closed:
|
|
245
|
+
self._channel = await self._connection.channel(
|
|
246
|
+
publisher_confirms=self._config.publisher_confirms if self._config else True
|
|
247
|
+
)
|
|
248
|
+
if self._config:
|
|
249
|
+
await self._channel.set_qos(prefetch_count=self._config.prefetch_count)
|
|
250
|
+
|
|
251
|
+
return self._channel
|
|
252
|
+
|
|
253
|
+
async def health_check(self) -> bool:
|
|
254
|
+
"""健康检查。
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
bool: 连接是否正常
|
|
258
|
+
"""
|
|
259
|
+
if not self._connection:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
return not self._connection.is_closed
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"RabbitMQ 客户端 [{self.name}] 健康检查失败: {e}")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
async def cleanup(self) -> None:
|
|
269
|
+
"""清理资源,关闭连接。"""
|
|
270
|
+
if self._channel and not self._channel.is_closed:
|
|
271
|
+
await self._channel.close()
|
|
272
|
+
logger.debug(f"RabbitMQ 通道 [{self.name}] 已关闭")
|
|
273
|
+
|
|
274
|
+
if self._connection and not self._connection.is_closed:
|
|
275
|
+
await self._connection.close()
|
|
276
|
+
logger.info(f"RabbitMQ 客户端 [{self.name}] 已关闭")
|
|
277
|
+
|
|
278
|
+
self._channel = None
|
|
279
|
+
self._connection = None
|
|
280
|
+
self._initialized = False
|
|
281
|
+
|
|
282
|
+
def __repr__(self) -> str:
|
|
283
|
+
"""字符串表示。"""
|
|
284
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
285
|
+
return f"<RabbitMQClient name={self.name} status={status}>"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
__all__ = ["RabbitMQClient"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Redis 客户端模块。
|
|
2
|
+
|
|
3
|
+
提供统一的 Redis 连接管理,支持多实例。
|
|
4
|
+
|
|
5
|
+
使用示例:
|
|
6
|
+
# 默认实例
|
|
7
|
+
client = RedisClient.get_instance()
|
|
8
|
+
client.configure(url="redis://localhost:6379/0")
|
|
9
|
+
await client.initialize()
|
|
10
|
+
|
|
11
|
+
# 命名实例
|
|
12
|
+
cache_redis = RedisClient.get_instance("cache")
|
|
13
|
+
queue_redis = RedisClient.get_instance("queue")
|
|
14
|
+
|
|
15
|
+
# 使用
|
|
16
|
+
redis = client.connection
|
|
17
|
+
await redis.set("key", "value")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .config import RedisConfig
|
|
23
|
+
from .manager import RedisClient
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"RedisClient",
|
|
27
|
+
"RedisConfig",
|
|
28
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Redis 客户端配置。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RedisConfig(BaseModel):
|
|
9
|
+
"""Redis 连接配置。
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
url: Redis 连接 URL,如 redis://localhost:6379/0
|
|
13
|
+
max_connections: 最大连接数
|
|
14
|
+
socket_timeout: 套接字超时时间(秒)
|
|
15
|
+
socket_connect_timeout: 连接超时时间(秒)
|
|
16
|
+
retry_on_timeout: 超时是否重试
|
|
17
|
+
health_check_interval: 健康检查间隔(秒)
|
|
18
|
+
decode_responses: 是否自动解码响应
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
url: str = Field(
|
|
22
|
+
default="redis://localhost:6379/0",
|
|
23
|
+
description="Redis 连接 URL"
|
|
24
|
+
)
|
|
25
|
+
max_connections: int = Field(
|
|
26
|
+
default=10,
|
|
27
|
+
description="最大连接数"
|
|
28
|
+
)
|
|
29
|
+
socket_timeout: float = Field(
|
|
30
|
+
default=5.0,
|
|
31
|
+
description="套接字超时时间(秒)"
|
|
32
|
+
)
|
|
33
|
+
socket_connect_timeout: float = Field(
|
|
34
|
+
default=5.0,
|
|
35
|
+
description="连接超时时间(秒)"
|
|
36
|
+
)
|
|
37
|
+
retry_on_timeout: bool = Field(
|
|
38
|
+
default=True,
|
|
39
|
+
description="超时是否重试"
|
|
40
|
+
)
|
|
41
|
+
health_check_interval: int = Field(
|
|
42
|
+
default=30,
|
|
43
|
+
description="健康检查间隔(秒)"
|
|
44
|
+
)
|
|
45
|
+
decode_responses: bool = Field(
|
|
46
|
+
default=False,
|
|
47
|
+
description="是否自动解码响应(设为 False 以支持二进制数据)"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["RedisConfig"]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Redis 客户端管理器 - 命名多实例模式。
|
|
2
|
+
|
|
3
|
+
提供统一的 Redis 连接管理,支持多实例。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from redis.asyncio import ConnectionPool, Redis
|
|
9
|
+
|
|
10
|
+
from aury.boot.common.logging import logger
|
|
11
|
+
|
|
12
|
+
from .config import RedisConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisClient:
|
|
16
|
+
"""Redis 客户端管理器(命名多实例)。
|
|
17
|
+
|
|
18
|
+
提供统一的 Redis 连接管理接口,支持:
|
|
19
|
+
- 多实例管理(如 cache、session、queue 各自独立)
|
|
20
|
+
- 连接池管理
|
|
21
|
+
- 健康检查
|
|
22
|
+
- 链式配置
|
|
23
|
+
|
|
24
|
+
使用示例:
|
|
25
|
+
# 默认实例
|
|
26
|
+
client = RedisClient.get_instance()
|
|
27
|
+
client.configure(url="redis://localhost:6379/0")
|
|
28
|
+
await client.initialize()
|
|
29
|
+
|
|
30
|
+
# 命名实例
|
|
31
|
+
cache_redis = RedisClient.get_instance("cache")
|
|
32
|
+
queue_redis = RedisClient.get_instance("queue")
|
|
33
|
+
|
|
34
|
+
# 获取连接
|
|
35
|
+
redis = client.connection
|
|
36
|
+
await redis.set("key", "value")
|
|
37
|
+
|
|
38
|
+
# 或直接使用
|
|
39
|
+
await client.execute("set", "key", "value")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_instances: dict[str, RedisClient] = {}
|
|
43
|
+
|
|
44
|
+
def __init__(self, name: str = "default") -> None:
|
|
45
|
+
"""初始化 Redis 客户端管理器。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name: 实例名称
|
|
49
|
+
"""
|
|
50
|
+
self.name = name
|
|
51
|
+
self._config: RedisConfig | None = None
|
|
52
|
+
self._pool: ConnectionPool | None = None
|
|
53
|
+
self._redis: Redis | None = None
|
|
54
|
+
self._initialized: bool = False
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_instance(cls, name: str = "default") -> RedisClient:
|
|
58
|
+
"""获取指定名称的实例。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: 实例名称,默认为 "default"
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
RedisClient: Redis 客户端实例
|
|
65
|
+
"""
|
|
66
|
+
if name not in cls._instances:
|
|
67
|
+
cls._instances[name] = cls(name)
|
|
68
|
+
return cls._instances[name]
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def reset_instance(cls, name: str | None = None) -> None:
|
|
72
|
+
"""重置实例(仅用于测试)。
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
name: 要重置的实例名称。如果为 None,则重置所有实例。
|
|
76
|
+
|
|
77
|
+
注意:调用此方法前应先调用 cleanup() 释放资源。
|
|
78
|
+
"""
|
|
79
|
+
if name is None:
|
|
80
|
+
cls._instances.clear()
|
|
81
|
+
elif name in cls._instances:
|
|
82
|
+
del cls._instances[name]
|
|
83
|
+
|
|
84
|
+
def configure(
|
|
85
|
+
self,
|
|
86
|
+
url: str | None = None,
|
|
87
|
+
*,
|
|
88
|
+
max_connections: int | None = None,
|
|
89
|
+
socket_timeout: float | None = None,
|
|
90
|
+
socket_connect_timeout: float | None = None,
|
|
91
|
+
retry_on_timeout: bool | None = None,
|
|
92
|
+
health_check_interval: int | None = None,
|
|
93
|
+
decode_responses: bool | None = None,
|
|
94
|
+
config: RedisConfig | None = None,
|
|
95
|
+
) -> RedisClient:
|
|
96
|
+
"""配置 Redis 客户端(链式调用)。
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
url: Redis 连接 URL
|
|
100
|
+
max_connections: 最大连接数
|
|
101
|
+
socket_timeout: 套接字超时
|
|
102
|
+
socket_connect_timeout: 连接超时
|
|
103
|
+
retry_on_timeout: 超时是否重试
|
|
104
|
+
health_check_interval: 健康检查间隔
|
|
105
|
+
decode_responses: 是否解码响应
|
|
106
|
+
config: 直接传入 RedisConfig 对象
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
self: 支持链式调用
|
|
110
|
+
"""
|
|
111
|
+
if config:
|
|
112
|
+
self._config = config
|
|
113
|
+
else:
|
|
114
|
+
# 构建配置
|
|
115
|
+
config_dict = {}
|
|
116
|
+
if url is not None:
|
|
117
|
+
config_dict["url"] = url
|
|
118
|
+
if max_connections is not None:
|
|
119
|
+
config_dict["max_connections"] = max_connections
|
|
120
|
+
if socket_timeout is not None:
|
|
121
|
+
config_dict["socket_timeout"] = socket_timeout
|
|
122
|
+
if socket_connect_timeout is not None:
|
|
123
|
+
config_dict["socket_connect_timeout"] = socket_connect_timeout
|
|
124
|
+
if retry_on_timeout is not None:
|
|
125
|
+
config_dict["retry_on_timeout"] = retry_on_timeout
|
|
126
|
+
if health_check_interval is not None:
|
|
127
|
+
config_dict["health_check_interval"] = health_check_interval
|
|
128
|
+
if decode_responses is not None:
|
|
129
|
+
config_dict["decode_responses"] = decode_responses
|
|
130
|
+
|
|
131
|
+
self._config = RedisConfig(**config_dict)
|
|
132
|
+
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
async def initialize(self) -> RedisClient:
|
|
136
|
+
"""初始化 Redis 连接。
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
self: 支持链式调用
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
RuntimeError: 未配置时调用
|
|
143
|
+
ConnectionError: 连接失败
|
|
144
|
+
"""
|
|
145
|
+
if self._initialized:
|
|
146
|
+
logger.warning(f"Redis 客户端 [{self.name}] 已初始化,跳过")
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
if not self._config:
|
|
150
|
+
raise RuntimeError(
|
|
151
|
+
f"Redis 客户端 [{self.name}] 未配置,请先调用 configure()"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# 创建连接池
|
|
156
|
+
self._pool = ConnectionPool.from_url(
|
|
157
|
+
self._config.url,
|
|
158
|
+
max_connections=self._config.max_connections,
|
|
159
|
+
socket_timeout=self._config.socket_timeout,
|
|
160
|
+
socket_connect_timeout=self._config.socket_connect_timeout,
|
|
161
|
+
retry_on_timeout=self._config.retry_on_timeout,
|
|
162
|
+
health_check_interval=self._config.health_check_interval,
|
|
163
|
+
decode_responses=self._config.decode_responses,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 创建 Redis 客户端
|
|
167
|
+
self._redis = Redis(connection_pool=self._pool)
|
|
168
|
+
|
|
169
|
+
# 验证连接
|
|
170
|
+
await self._redis.ping()
|
|
171
|
+
|
|
172
|
+
self._initialized = True
|
|
173
|
+
# 脱敏日志
|
|
174
|
+
masked_url = self._mask_url(self._config.url)
|
|
175
|
+
logger.info(f"Redis 客户端 [{self.name}] 初始化完成: {masked_url}")
|
|
176
|
+
|
|
177
|
+
return self
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Redis 客户端 [{self.name}] 初始化失败: {e}")
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def _mask_url(self, url: str) -> str:
|
|
183
|
+
"""URL 脱敏(隐藏密码)。"""
|
|
184
|
+
if "@" in url:
|
|
185
|
+
# redis://:password@host:port/db -> redis://***@host:port/db
|
|
186
|
+
parts = url.split("@")
|
|
187
|
+
prefix = parts[0]
|
|
188
|
+
suffix = parts[1]
|
|
189
|
+
# 隐藏密码部分
|
|
190
|
+
if ":" in prefix:
|
|
191
|
+
scheme_and_user = prefix.rsplit(":", 1)[0]
|
|
192
|
+
return f"{scheme_and_user}:***@{suffix}"
|
|
193
|
+
return url
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_initialized(self) -> bool:
|
|
197
|
+
"""检查是否已初始化。"""
|
|
198
|
+
return self._initialized
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def connection(self) -> Redis:
|
|
202
|
+
"""获取 Redis 连接。
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Redis: Redis 客户端实例
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
RuntimeError: 未初始化时调用
|
|
209
|
+
"""
|
|
210
|
+
if not self._redis:
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
f"Redis 客户端 [{self.name}] 未初始化,请先调用 initialize()"
|
|
213
|
+
)
|
|
214
|
+
return self._redis
|
|
215
|
+
|
|
216
|
+
async def execute(self, command: str, *args, **kwargs):
|
|
217
|
+
"""执行 Redis 命令。
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
command: Redis 命令名
|
|
221
|
+
*args: 命令参数
|
|
222
|
+
**kwargs: 命令关键字参数
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
命令执行结果
|
|
226
|
+
"""
|
|
227
|
+
return await self.connection.execute_command(command, *args, **kwargs)
|
|
228
|
+
|
|
229
|
+
async def health_check(self) -> bool:
|
|
230
|
+
"""健康检查。
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
bool: 连接是否正常
|
|
234
|
+
"""
|
|
235
|
+
if not self._redis:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
await self._redis.ping()
|
|
240
|
+
return True
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning(f"Redis 客户端 [{self.name}] 健康检查失败: {e}")
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
async def cleanup(self) -> None:
|
|
246
|
+
"""清理资源,关闭连接。"""
|
|
247
|
+
if self._redis:
|
|
248
|
+
await self._redis.close()
|
|
249
|
+
logger.info(f"Redis 客户端 [{self.name}] 已关闭")
|
|
250
|
+
|
|
251
|
+
if self._pool:
|
|
252
|
+
await self._pool.disconnect()
|
|
253
|
+
|
|
254
|
+
self._redis = None
|
|
255
|
+
self._pool = None
|
|
256
|
+
self._initialized = False
|
|
257
|
+
|
|
258
|
+
def __repr__(self) -> str:
|
|
259
|
+
"""字符串表示。"""
|
|
260
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
261
|
+
return f"<RedisClient name={self.name} status={status}>"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
__all__ = ["RedisClient"]
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
# 支持的事务隔离级别
|
|
13
12
|
ISOLATION_LEVELS = (
|
|
14
13
|
"READ UNCOMMITTED",
|
|
@@ -63,8 +62,8 @@ class DatabaseConfig(BaseSettings):
|
|
|
63
62
|
|
|
64
63
|
|
|
65
64
|
__all__ = [
|
|
66
|
-
"DatabaseConfig",
|
|
67
65
|
"ISOLATION_LEVELS",
|
|
66
|
+
"DatabaseConfig",
|
|
68
67
|
]
|
|
69
68
|
|
|
70
69
|
|