aury-boot 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +45 -36
  4. aury/boot/application/app/__init__.py +12 -8
  5. aury/boot/application/app/base.py +12 -0
  6. aury/boot/application/app/components.py +137 -44
  7. aury/boot/application/app/middlewares.py +2 -0
  8. aury/boot/application/app/startup.py +249 -0
  9. aury/boot/application/config/__init__.py +36 -1
  10. aury/boot/application/config/multi_instance.py +200 -0
  11. aury/boot/application/config/settings.py +341 -12
  12. aury/boot/application/constants/components.py +6 -0
  13. aury/boot/application/errors/handlers.py +17 -3
  14. aury/boot/application/middleware/logging.py +8 -120
  15. aury/boot/application/rpc/__init__.py +2 -2
  16. aury/boot/commands/__init__.py +30 -10
  17. aury/boot/commands/app.py +131 -1
  18. aury/boot/commands/docs.py +104 -17
  19. aury/boot/commands/init.py +30 -9
  20. aury/boot/commands/server/app.py +2 -3
  21. aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
  22. aury/boot/commands/templates/project/README.md.tpl +2 -2
  23. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  24. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
  25. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  26. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  27. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  28. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  29. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  30. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  31. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  32. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  33. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  34. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
  35. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  36. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
  37. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  38. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  39. aury/boot/commands/templates/project/config.py.tpl +1 -1
  40. aury/boot/commands/templates/project/env.example.tpl +73 -5
  41. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  42. aury/boot/contrib/admin_console/auth.py +2 -3
  43. aury/boot/contrib/admin_console/install.py +1 -1
  44. aury/boot/domain/models/mixins.py +48 -1
  45. aury/boot/domain/pagination/__init__.py +94 -0
  46. aury/boot/domain/repository/impl.py +1 -1
  47. aury/boot/domain/repository/interface.py +1 -1
  48. aury/boot/domain/transaction/__init__.py +8 -9
  49. aury/boot/infrastructure/__init__.py +86 -29
  50. aury/boot/infrastructure/cache/backends.py +102 -18
  51. aury/boot/infrastructure/cache/base.py +12 -0
  52. aury/boot/infrastructure/cache/manager.py +153 -91
  53. aury/boot/infrastructure/channel/__init__.py +24 -0
  54. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  55. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  56. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  57. aury/boot/infrastructure/channel/base.py +92 -0
  58. aury/boot/infrastructure/channel/manager.py +203 -0
  59. aury/boot/infrastructure/clients/__init__.py +22 -0
  60. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  61. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  62. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  63. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  64. aury/boot/infrastructure/clients/redis/config.py +51 -0
  65. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  66. aury/boot/infrastructure/database/config.py +1 -2
  67. aury/boot/infrastructure/database/manager.py +16 -38
  68. aury/boot/infrastructure/events/__init__.py +18 -21
  69. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  70. aury/boot/infrastructure/events/backends/memory.py +86 -0
  71. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  72. aury/boot/infrastructure/events/backends/redis.py +162 -0
  73. aury/boot/infrastructure/events/base.py +127 -0
  74. aury/boot/infrastructure/events/manager.py +224 -0
  75. aury/boot/infrastructure/mq/__init__.py +24 -0
  76. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  77. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  78. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  79. aury/boot/infrastructure/mq/base.py +143 -0
  80. aury/boot/infrastructure/mq/manager.py +239 -0
  81. aury/boot/infrastructure/scheduler/manager.py +7 -3
  82. aury/boot/infrastructure/storage/__init__.py +9 -9
  83. aury/boot/infrastructure/storage/base.py +17 -5
  84. aury/boot/infrastructure/storage/factory.py +0 -1
  85. aury/boot/infrastructure/tasks/__init__.py +2 -2
  86. aury/boot/infrastructure/tasks/manager.py +47 -29
  87. aury/boot/testing/base.py +2 -2
  88. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
  89. aury_boot-0.0.5.dist-info/RECORD +176 -0
  90. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  91. aury/boot/infrastructure/events/bus.py +0 -362
  92. aury/boot/infrastructure/events/config.py +0 -52
  93. aury/boot/infrastructure/events/consumer.py +0 -134
  94. aury/boot/infrastructure/events/models.py +0 -63
  95. aury_boot-0.0.3.dist-info/RECORD +0 -137
  96. /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
  97. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
  98. {aury_boot-0.0.3.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -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