aury-boot 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aury/boot/__init__.py +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +45 -36
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +2 -0
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +200 -0
- aury/boot/application/config/settings.py +341 -12
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +8 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/init.py +27 -8
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/config.py.tpl +1 -1
- aury/boot/commands/templates/project/env.example.tpl +73 -5
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +1 -2
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/manager.py +47 -29
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
- aury_boot-0.0.5.dist-info/RECORD +176 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.4.dist-info/RECORD +0 -137
- /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
- 存储管理
|
|
7
7
|
- 调度器
|
|
8
8
|
- 任务队列
|
|
9
|
-
-
|
|
9
|
+
- 消息队列
|
|
10
|
+
- 通道 (SSE/PubSub)
|
|
11
|
+
- 事件总线
|
|
12
|
+
- Redis 客户端
|
|
13
|
+
- RabbitMQ 客户端
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
# 数据库
|
|
@@ -20,16 +24,49 @@ from .cache import (
|
|
|
20
24
|
MemoryCache,
|
|
21
25
|
RedisCache,
|
|
22
26
|
)
|
|
27
|
+
|
|
28
|
+
# 通道 (SSE/PubSub)
|
|
29
|
+
from .channel import (
|
|
30
|
+
ChannelBackend,
|
|
31
|
+
ChannelManager,
|
|
32
|
+
ChannelMessage,
|
|
33
|
+
IChannel,
|
|
34
|
+
MemoryChannel,
|
|
35
|
+
RedisChannel,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# RabbitMQ 客户端
|
|
39
|
+
from .clients.rabbitmq import RabbitMQClient, RabbitMQConfig
|
|
40
|
+
|
|
41
|
+
# Redis 客户端
|
|
42
|
+
from .clients.redis import RedisClient, RedisConfig
|
|
23
43
|
from .database import DatabaseManager
|
|
24
44
|
|
|
25
|
-
#
|
|
26
|
-
|
|
45
|
+
# 依赖注入
|
|
46
|
+
from .di import Container, Lifetime, Scope, ServiceDescriptor
|
|
27
47
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
# 事件总线
|
|
49
|
+
from .events import (
|
|
50
|
+
Event,
|
|
51
|
+
EventBackend,
|
|
52
|
+
EventBusManager,
|
|
53
|
+
EventHandler,
|
|
54
|
+
EventType,
|
|
55
|
+
IEventBus,
|
|
56
|
+
MemoryEventBus,
|
|
57
|
+
RabbitMQEventBus,
|
|
58
|
+
RedisEventBus,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 消息队列
|
|
62
|
+
from .mq import (
|
|
63
|
+
IMQ,
|
|
64
|
+
MQBackend,
|
|
65
|
+
MQManager,
|
|
66
|
+
MQMessage,
|
|
67
|
+
RabbitMQ,
|
|
68
|
+
RedisMQ,
|
|
69
|
+
)
|
|
33
70
|
|
|
34
71
|
# 存储(基于 aury-sdk-storage)
|
|
35
72
|
from .storage import (
|
|
@@ -44,45 +81,67 @@ from .storage import (
|
|
|
44
81
|
UploadResult,
|
|
45
82
|
)
|
|
46
83
|
|
|
84
|
+
# 调度器(可选依赖)
|
|
85
|
+
try:
|
|
86
|
+
from .scheduler import SchedulerManager
|
|
87
|
+
except ImportError:
|
|
88
|
+
SchedulerManager = None # type: ignore[assignment, misc]
|
|
89
|
+
|
|
47
90
|
# 任务队列(可选依赖)
|
|
48
91
|
try:
|
|
49
|
-
from .tasks import TaskManager, TaskProxy,
|
|
92
|
+
from .tasks import TaskManager, TaskProxy, conditional_task
|
|
50
93
|
except ImportError:
|
|
51
94
|
TaskManager = None # type: ignore[assignment, misc]
|
|
52
95
|
TaskProxy = None # type: ignore[assignment, misc]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# 事件总线
|
|
56
|
-
# 依赖注入
|
|
57
|
-
from .di import Container, Lifetime, Scope, ServiceDescriptor
|
|
58
|
-
from .events import (
|
|
59
|
-
EventBus,
|
|
60
|
-
EventConsumer,
|
|
61
|
-
EventLoggingMiddleware,
|
|
62
|
-
EventMiddleware,
|
|
63
|
-
)
|
|
96
|
+
conditional_task = None # type: ignore[assignment, misc]
|
|
64
97
|
|
|
65
98
|
__all__ = [
|
|
99
|
+
# 消息队列
|
|
100
|
+
"IMQ",
|
|
101
|
+
# 缓存
|
|
66
102
|
"CacheBackend",
|
|
67
103
|
"CacheFactory",
|
|
68
|
-
# 缓存
|
|
69
104
|
"CacheManager",
|
|
105
|
+
# 通道
|
|
106
|
+
"ChannelBackend",
|
|
107
|
+
"ChannelManager",
|
|
108
|
+
"ChannelMessage",
|
|
70
109
|
# 依赖注入
|
|
71
110
|
"Container",
|
|
72
111
|
# 数据库
|
|
73
112
|
"DatabaseManager",
|
|
74
113
|
# 事件总线
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
114
|
+
"Event",
|
|
115
|
+
"EventBackend",
|
|
116
|
+
"EventBusManager",
|
|
117
|
+
"EventHandler",
|
|
118
|
+
"EventType",
|
|
79
119
|
"ICache",
|
|
120
|
+
"IChannel",
|
|
121
|
+
"IEventBus",
|
|
122
|
+
# 存储
|
|
80
123
|
"IStorage",
|
|
81
124
|
"Lifetime",
|
|
82
125
|
"LocalStorage",
|
|
126
|
+
"MQBackend",
|
|
127
|
+
"MQManager",
|
|
128
|
+
"MQMessage",
|
|
83
129
|
"MemcachedCache",
|
|
84
130
|
"MemoryCache",
|
|
131
|
+
"MemoryChannel",
|
|
132
|
+
"MemoryEventBus",
|
|
133
|
+
"RabbitMQ",
|
|
134
|
+
# RabbitMQ 客户端
|
|
135
|
+
"RabbitMQClient",
|
|
136
|
+
"RabbitMQConfig",
|
|
137
|
+
"RabbitMQEventBus",
|
|
85
138
|
"RedisCache",
|
|
139
|
+
"RedisChannel",
|
|
140
|
+
# Redis 客户端
|
|
141
|
+
"RedisClient",
|
|
142
|
+
"RedisConfig",
|
|
143
|
+
"RedisEventBus",
|
|
144
|
+
"RedisMQ",
|
|
86
145
|
"S3Storage",
|
|
87
146
|
# 调度器
|
|
88
147
|
"SchedulerManager",
|
|
@@ -92,13 +151,11 @@ __all__ = [
|
|
|
92
151
|
"StorageConfig",
|
|
93
152
|
"StorageFactory",
|
|
94
153
|
"StorageFile",
|
|
95
|
-
# 存储
|
|
96
154
|
"StorageManager",
|
|
97
|
-
"UploadResult",
|
|
98
155
|
# 任务队列
|
|
99
156
|
"TaskManager",
|
|
100
157
|
"TaskProxy",
|
|
101
|
-
"
|
|
102
|
-
|
|
158
|
+
"UploadResult",
|
|
159
|
+
"conditional_task",
|
|
103
160
|
]
|
|
104
161
|
|
|
@@ -10,7 +10,7 @@ from collections.abc import Callable
|
|
|
10
10
|
from datetime import timedelta
|
|
11
11
|
import json
|
|
12
12
|
import pickle
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
14
|
|
|
15
15
|
from redis.asyncio import Redis
|
|
16
16
|
|
|
@@ -18,36 +18,65 @@ from aury.boot.common.logging import logger
|
|
|
18
18
|
|
|
19
19
|
from .base import ICache
|
|
20
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class RedisCache(ICache):
|
|
23
|
-
"""Redis缓存实现。
|
|
26
|
+
"""Redis缓存实现。
|
|
27
|
+
|
|
28
|
+
支持两种初始化方式:
|
|
29
|
+
1. 传入 URL 自行创建连接
|
|
30
|
+
2. 传入 RedisClient 实例(推荐)
|
|
31
|
+
"""
|
|
24
32
|
|
|
25
|
-
def __init__(
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
url: str | None = None,
|
|
36
|
+
*,
|
|
37
|
+
redis_client: RedisClient | None = None,
|
|
38
|
+
serializer: str = "json",
|
|
39
|
+
):
|
|
26
40
|
"""初始化Redis缓存。
|
|
27
41
|
|
|
28
42
|
Args:
|
|
29
43
|
url: Redis连接URL
|
|
44
|
+
redis_client: RedisClient 实例(推荐)
|
|
30
45
|
serializer: 序列化方式(json/pickle)
|
|
31
46
|
"""
|
|
32
47
|
self._url = url
|
|
48
|
+
self._redis_client = redis_client
|
|
33
49
|
self._serializer = serializer
|
|
34
50
|
self._redis: Redis | None = None
|
|
51
|
+
self._owns_connection = False # 是否自己拥有连接(需要自己关闭)
|
|
35
52
|
|
|
36
53
|
async def initialize(self) -> None:
|
|
37
54
|
"""初始化连接。"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
# 优先使用 RedisClient
|
|
56
|
+
if self._redis_client is not None:
|
|
57
|
+
self._redis = self._redis_client.connection
|
|
58
|
+
self._owns_connection = False
|
|
59
|
+
logger.info("Redis缓存初始化成功(使用 RedisClient)")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# 使用 URL 创建连接
|
|
63
|
+
if self._url:
|
|
64
|
+
try:
|
|
65
|
+
self._redis = Redis.from_url(
|
|
66
|
+
self._url,
|
|
67
|
+
encoding="utf-8",
|
|
68
|
+
decode_responses=False,
|
|
69
|
+
socket_connect_timeout=5,
|
|
70
|
+
socket_timeout=5,
|
|
71
|
+
)
|
|
72
|
+
await self._redis.ping()
|
|
73
|
+
self._owns_connection = True
|
|
74
|
+
logger.info("Redis缓存初始化成功")
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
logger.error(f"Redis连接失败: {exc}")
|
|
77
|
+
raise
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError("Redis缓存需要提供 url 或 redis_client 参数")
|
|
51
80
|
|
|
52
81
|
async def get(self, key: str, default: Any = None) -> Any:
|
|
53
82
|
"""获取缓存。"""
|
|
@@ -134,11 +163,40 @@ class RedisCache(ICache):
|
|
|
134
163
|
await self._redis.flushdb()
|
|
135
164
|
logger.info("Redis缓存已清空")
|
|
136
165
|
|
|
166
|
+
async def delete_pattern(self, pattern: str) -> int:
|
|
167
|
+
"""按模式删除缓存。
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
pattern: 通配符模式,如 "todo:*"
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
int: 删除的键数量
|
|
174
|
+
"""
|
|
175
|
+
if not self._redis:
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# 使用 SCAN 遍历匹配的键(比 KEYS 更安全,不会阻塞)
|
|
180
|
+
count = 0
|
|
181
|
+
cursor = 0
|
|
182
|
+
while True:
|
|
183
|
+
cursor, keys = await self._redis.scan(cursor, match=pattern, count=100)
|
|
184
|
+
if keys:
|
|
185
|
+
count += await self._redis.delete(*keys)
|
|
186
|
+
if cursor == 0:
|
|
187
|
+
break
|
|
188
|
+
logger.debug(f"按模式删除缓存: {pattern}, 删除 {count} 个键")
|
|
189
|
+
return count
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
logger.error(f"Redis模式删除失败: {pattern}, {exc}")
|
|
192
|
+
return 0
|
|
193
|
+
|
|
137
194
|
async def close(self) -> None:
|
|
138
|
-
"""
|
|
139
|
-
if self._redis:
|
|
195
|
+
"""关闭连接(仅当自己拥有连接时)。"""
|
|
196
|
+
if self._redis and self._owns_connection:
|
|
140
197
|
await self._redis.close()
|
|
141
198
|
logger.info("Redis连接已关闭")
|
|
199
|
+
self._redis = None
|
|
142
200
|
|
|
143
201
|
@property
|
|
144
202
|
def redis(self) -> Redis | None:
|
|
@@ -228,6 +286,27 @@ class MemoryCache(ICache):
|
|
|
228
286
|
self._cache.clear()
|
|
229
287
|
logger.info("内存缓存已清空")
|
|
230
288
|
|
|
289
|
+
async def delete_pattern(self, pattern: str) -> int:
|
|
290
|
+
"""按模式删除缓存。
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
pattern: 通配符模式,支持 * 和 ?
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
int: 删除的键数量
|
|
297
|
+
"""
|
|
298
|
+
import fnmatch
|
|
299
|
+
|
|
300
|
+
async with self._lock:
|
|
301
|
+
keys_to_delete = [
|
|
302
|
+
key for key in self._cache
|
|
303
|
+
if fnmatch.fnmatch(key, pattern)
|
|
304
|
+
]
|
|
305
|
+
for key in keys_to_delete:
|
|
306
|
+
del self._cache[key]
|
|
307
|
+
logger.debug(f"按模式删除缓存: {pattern}, 删除 {len(keys_to_delete)} 个键")
|
|
308
|
+
return len(keys_to_delete)
|
|
309
|
+
|
|
231
310
|
async def close(self) -> None:
|
|
232
311
|
"""关闭连接(内存缓存无需关闭)。"""
|
|
233
312
|
await self.clear()
|
|
@@ -333,6 +412,11 @@ class MemcachedCache(ICache):
|
|
|
333
412
|
"""清空所有缓存(Memcached不支持)。"""
|
|
334
413
|
logger.warning("Memcached不支持清空所有缓存")
|
|
335
414
|
|
|
415
|
+
async def delete_pattern(self, pattern: str) -> int:
|
|
416
|
+
"""按模式删除缓存(Memcached 不支持)。"""
|
|
417
|
+
logger.warning("Memcached 不支持模式删除,请使用 Redis 或 Memory 后端")
|
|
418
|
+
return 0
|
|
419
|
+
|
|
336
420
|
async def close(self) -> None:
|
|
337
421
|
"""关闭连接。"""
|
|
338
422
|
if self._client:
|
|
@@ -55,6 +55,18 @@ class ICache(ABC):
|
|
|
55
55
|
"""清空所有缓存。"""
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def delete_pattern(self, pattern: str) -> int:
|
|
60
|
+
"""按模式删除缓存。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
pattern: 通配符模式,如 "todo:*" 或 "api:todo:list:*"
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
int: 删除的键数量
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
58
70
|
@abstractmethod
|
|
59
71
|
async def close(self) -> None:
|
|
60
72
|
"""关闭连接。"""
|
|
@@ -21,16 +21,12 @@ from .factory import CacheFactory
|
|
|
21
21
|
class CacheManager:
|
|
22
22
|
"""缓存管理器(命名多实例)。
|
|
23
23
|
|
|
24
|
-
类似Flask-Cache的API设计,优雅简洁。
|
|
25
24
|
支持多个命名实例,如不同的 Redis 实例或缓存策略。
|
|
26
25
|
|
|
27
26
|
使用示例:
|
|
28
27
|
# 默认实例
|
|
29
28
|
cache = CacheManager.get_instance()
|
|
30
|
-
await cache.
|
|
31
|
-
"CACHE_TYPE": "redis",
|
|
32
|
-
"CACHE_URL": "redis://localhost:6379"
|
|
33
|
-
})
|
|
29
|
+
await cache.initialize(backend="redis", url="redis://localhost:6379")
|
|
34
30
|
|
|
35
31
|
# 命名实例
|
|
36
32
|
session_cache = CacheManager.get_instance("session")
|
|
@@ -81,109 +77,77 @@ class CacheManager:
|
|
|
81
77
|
elif name in cls._instances:
|
|
82
78
|
del cls._instances[name]
|
|
83
79
|
|
|
84
|
-
async def init_app(self, config: dict[str, Any]) -> None:
|
|
85
|
-
"""初始化缓存(类似Flask-Cache)。
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
config: 配置字典
|
|
89
|
-
- CACHE_TYPE: 缓存类型(redis/memory/memcached)
|
|
90
|
-
- CACHE_URL: 缓存服务 URL(通用)
|
|
91
|
-
- CACHE_MAX_SIZE: 内存缓存最大容量
|
|
92
|
-
- CACHE_SERIALIZER: 序列化方式(json/pickle)
|
|
93
|
-
"""
|
|
94
|
-
self._config = config.copy()
|
|
95
|
-
cache_type = config.get("CACHE_TYPE", "redis")
|
|
96
|
-
|
|
97
|
-
# 构建后端配置
|
|
98
|
-
backend_config = self._build_backend_config(cache_type, config)
|
|
99
|
-
|
|
100
|
-
# 使用工厂创建后端
|
|
101
|
-
self._backend = await CacheFactory.create(cache_type, **backend_config)
|
|
102
|
-
logger.info(f"缓存管理器初始化完成: {cache_type}")
|
|
103
|
-
|
|
104
|
-
def _build_backend_config(self, cache_type: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
-
"""构建后端配置。
|
|
106
|
-
|
|
107
|
-
使用函数式编程处理配置构建逻辑。
|
|
108
|
-
"""
|
|
109
|
-
# 配置构建函数字典(函数式编程)
|
|
110
|
-
config_builders: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
|
|
111
|
-
"redis": lambda cfg: {
|
|
112
|
-
"url": cfg.get("CACHE_URL"),
|
|
113
|
-
"serializer": cfg.get("CACHE_SERIALIZER", "json"),
|
|
114
|
-
},
|
|
115
|
-
"memory": lambda cfg: {
|
|
116
|
-
"max_size": cfg.get("CACHE_MAX_SIZE", 1000),
|
|
117
|
-
},
|
|
118
|
-
"memcached": lambda cfg: {
|
|
119
|
-
"servers": cfg.get("CACHE_URL"), # memcached 也用 URL
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if cache_type not in config_builders:
|
|
124
|
-
available = ", ".join(config_builders.keys())
|
|
125
|
-
raise ValueError(
|
|
126
|
-
f"不支持的缓存类型: {cache_type}。可用类型: {available}"
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
builder = config_builders[cache_type]
|
|
130
|
-
backend_config = builder(config)
|
|
131
|
-
|
|
132
|
-
# 验证必需配置
|
|
133
|
-
if cache_type == "redis" and not backend_config.get("url"):
|
|
134
|
-
raise ValueError("缓存 URL 未配置,请设置 CACHE_URL")
|
|
135
|
-
if cache_type == "memcached" and not backend_config.get("servers"):
|
|
136
|
-
raise ValueError("缓存 URL 未配置,请设置 CACHE_URL")
|
|
137
|
-
|
|
138
|
-
return backend_config
|
|
139
|
-
|
|
140
80
|
async def initialize(
|
|
141
81
|
self,
|
|
142
|
-
backend: CacheBackend = CacheBackend.REDIS,
|
|
82
|
+
backend: CacheBackend | str = CacheBackend.REDIS,
|
|
143
83
|
*,
|
|
144
84
|
url: str | None = None,
|
|
145
85
|
max_size: int = 1000,
|
|
146
86
|
serializer: str = "json",
|
|
147
87
|
servers: list[str] | None = None,
|
|
148
|
-
) ->
|
|
149
|
-
"""
|
|
88
|
+
) -> CacheManager:
|
|
89
|
+
"""初始化缓存(链式调用)。
|
|
150
90
|
|
|
151
91
|
Args:
|
|
152
|
-
backend:
|
|
153
|
-
url: Redis连接URL
|
|
154
|
-
max_size:
|
|
155
|
-
serializer:
|
|
156
|
-
servers: Memcached
|
|
92
|
+
backend: 缓存后端类型(redis/memory/memcached)
|
|
93
|
+
url: Redis/Memcached 连接 URL
|
|
94
|
+
max_size: 内存缓存最大容量(仅 memory 后端)
|
|
95
|
+
serializer: 序列化方式(json/pickle)
|
|
96
|
+
servers: Memcached 服务器列表(已弃用,请使用 url)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
self: 支持链式调用
|
|
157
100
|
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
CacheBackend.MEMCACHED: lambda: {
|
|
170
|
-
"CACHE_TYPE": backend.value,
|
|
171
|
-
"CACHE_MEMCACHED_SERVERS": servers,
|
|
172
|
-
},
|
|
173
|
-
}
|
|
101
|
+
if self._backend is not None:
|
|
102
|
+
logger.warning(f"缓存管理器 [{self.name}] 已初始化,跳过")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
# 处理字符串类型的 backend
|
|
106
|
+
if isinstance(backend, str):
|
|
107
|
+
try:
|
|
108
|
+
backend = CacheBackend(backend.lower())
|
|
109
|
+
except ValueError:
|
|
110
|
+
supported = ", ".join(b.value for b in CacheBackend)
|
|
111
|
+
raise ValueError(f"不支持的缓存后端: {backend}。支持: {supported}")
|
|
174
112
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
113
|
+
# 保存配置
|
|
114
|
+
self._config = {"CACHE_TYPE": backend.value}
|
|
115
|
+
|
|
116
|
+
# 根据后端类型构建配置并创建后端
|
|
117
|
+
if backend == CacheBackend.REDIS:
|
|
118
|
+
if not url:
|
|
119
|
+
raise ValueError("Redis 缓存需要提供 url 参数")
|
|
120
|
+
self._backend = await CacheFactory.create(
|
|
121
|
+
"redis", url=url, serializer=serializer
|
|
122
|
+
)
|
|
123
|
+
elif backend == CacheBackend.MEMORY:
|
|
124
|
+
self._backend = await CacheFactory.create(
|
|
125
|
+
"memory", max_size=max_size
|
|
126
|
+
)
|
|
127
|
+
elif backend == CacheBackend.MEMCACHED:
|
|
128
|
+
cache_url = url or (servers[0] if servers else None)
|
|
129
|
+
if not cache_url:
|
|
130
|
+
raise ValueError("Memcached 缓存需要提供 url 参数")
|
|
131
|
+
self._backend = await CacheFactory.create(
|
|
132
|
+
"memcached", servers=cache_url
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
supported = ", ".join(b.value for b in CacheBackend)
|
|
136
|
+
raise ValueError(f"不支持的缓存后端: {backend}。支持: {supported}")
|
|
178
137
|
|
|
179
|
-
|
|
180
|
-
|
|
138
|
+
logger.info(f"缓存管理器 [{self.name}] 初始化完成: {backend.value}")
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def is_initialized(self) -> bool:
|
|
143
|
+
"""检查是否已初始化。"""
|
|
144
|
+
return self._backend is not None
|
|
181
145
|
|
|
182
146
|
@property
|
|
183
147
|
def backend(self) -> ICache:
|
|
184
148
|
"""获取缓存后端。"""
|
|
185
149
|
if self._backend is None:
|
|
186
|
-
raise RuntimeError("缓存管理器未初始化,请先调用
|
|
150
|
+
raise RuntimeError("缓存管理器未初始化,请先调用 initialize()")
|
|
187
151
|
return self._backend
|
|
188
152
|
|
|
189
153
|
@property
|
|
@@ -216,6 +180,24 @@ class CacheManager:
|
|
|
216
180
|
"""清空所有缓存。"""
|
|
217
181
|
await self.backend.clear()
|
|
218
182
|
|
|
183
|
+
async def delete_pattern(self, pattern: str) -> int:
|
|
184
|
+
"""按模式删除缓存。
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
pattern: 通配符模式,如 "todo:*" 或 "api:todo:list:*"
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
int: 删除的键数量
|
|
191
|
+
|
|
192
|
+
示例:
|
|
193
|
+
# 删除所有 todo 相关缓存
|
|
194
|
+
await cache.delete_pattern("todo:*")
|
|
195
|
+
|
|
196
|
+
# 删除所有列表缓存
|
|
197
|
+
await cache.delete_pattern("api:todo:list:*")
|
|
198
|
+
"""
|
|
199
|
+
return await self.backend.delete_pattern(pattern)
|
|
200
|
+
|
|
219
201
|
def cached[T](
|
|
220
202
|
self,
|
|
221
203
|
expire: int | timedelta | None = None,
|
|
@@ -255,6 +237,86 @@ class CacheManager:
|
|
|
255
237
|
return wrapper
|
|
256
238
|
return decorator
|
|
257
239
|
|
|
240
|
+
def cache_response[T](
|
|
241
|
+
self,
|
|
242
|
+
expire: int | timedelta | None = 300,
|
|
243
|
+
*,
|
|
244
|
+
key_builder: Callable[..., str] | None = None,
|
|
245
|
+
key_prefix: str = "api",
|
|
246
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
247
|
+
"""API 响应缓存装饰器。
|
|
248
|
+
|
|
249
|
+
专为 FastAPI 路由设计,自动从路径参数和查询参数生成缓存键。
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
expire: 过期时间(秒),默认 300 秒
|
|
253
|
+
key_builder: 自定义缓存键生成函数,接收与被装饰函数相同的参数
|
|
254
|
+
key_prefix: 缓存键前缀,默认 "api"
|
|
255
|
+
|
|
256
|
+
示例:
|
|
257
|
+
cache = CacheManager.get_instance()
|
|
258
|
+
|
|
259
|
+
# 基本用法:自动生成缓存键
|
|
260
|
+
@router.get("/todos/{{id}}")
|
|
261
|
+
@cache.cache_response(expire=300)
|
|
262
|
+
async def get_todo(id: UUID):
|
|
263
|
+
return await service.get(id)
|
|
264
|
+
# 缓存键: api:get_todo:<hash>
|
|
265
|
+
|
|
266
|
+
# 自定义缓存键
|
|
267
|
+
@router.get("/todos/{{id}}")
|
|
268
|
+
@cache.cache_response(
|
|
269
|
+
expire=300,
|
|
270
|
+
key_builder=lambda id: f"todo:{{id}}"
|
|
271
|
+
)
|
|
272
|
+
async def get_todo(id: UUID):
|
|
273
|
+
return await service.get(id)
|
|
274
|
+
# 缓存键: api:todo:<id>
|
|
275
|
+
"""
|
|
276
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
277
|
+
@wraps(func)
|
|
278
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
279
|
+
# 生成缓存键
|
|
280
|
+
if key_builder:
|
|
281
|
+
# 使用自定义的 key_builder
|
|
282
|
+
custom_key = key_builder(*args, **kwargs)
|
|
283
|
+
cache_key = f"{key_prefix}:{custom_key}" if key_prefix else custom_key
|
|
284
|
+
else:
|
|
285
|
+
# 自动生成:函数名 + 参数哈希
|
|
286
|
+
func_name = func.__name__
|
|
287
|
+
args_str = str(args) + str(sorted(kwargs.items()))
|
|
288
|
+
key_hash = hashlib.md5(args_str.encode()).hexdigest()[:8]
|
|
289
|
+
cache_key = f"{key_prefix}:{func_name}:{key_hash}"
|
|
290
|
+
|
|
291
|
+
# 尝试从缓存获取
|
|
292
|
+
cached_value = await self.get(cache_key)
|
|
293
|
+
if cached_value is not None:
|
|
294
|
+
logger.debug(f"API 缓存命中: {cache_key}")
|
|
295
|
+
return cached_value
|
|
296
|
+
|
|
297
|
+
# 执行函数
|
|
298
|
+
result = await func(*args, **kwargs)
|
|
299
|
+
|
|
300
|
+
# 存入缓存(尝试序列化)
|
|
301
|
+
try:
|
|
302
|
+
# 如果结果有 model_dump 方法(Pydantic model),先序列化
|
|
303
|
+
if hasattr(result, "model_dump"):
|
|
304
|
+
cache_data = result.model_dump()
|
|
305
|
+
elif hasattr(result, "dict"):
|
|
306
|
+
cache_data = result.dict()
|
|
307
|
+
else:
|
|
308
|
+
cache_data = result
|
|
309
|
+
|
|
310
|
+
await self.set(cache_key, cache_data, expire)
|
|
311
|
+
logger.debug(f"API 缓存更新: {cache_key}")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.warning(f"API 缓存存储失败: {cache_key}, {e}")
|
|
314
|
+
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
return wrapper
|
|
318
|
+
return decorator
|
|
319
|
+
|
|
258
320
|
async def cleanup(self) -> None:
|
|
259
321
|
"""清理资源。"""
|
|
260
322
|
if self._backend:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""流式通道模块。
|
|
2
|
+
|
|
3
|
+
提供发布/订阅模式的通道功能,用于 SSE、WebSocket 等实时通信场景。
|
|
4
|
+
|
|
5
|
+
支持的后端:
|
|
6
|
+
- memory: 内存通道(单进程)
|
|
7
|
+
- redis: Redis Pub/Sub(多进程/多实例)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .backends import MemoryChannel, RedisChannel
|
|
11
|
+
from .base import ChannelBackend, ChannelMessage, IChannel
|
|
12
|
+
from .manager import ChannelManager
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# 接口和类型
|
|
16
|
+
"ChannelBackend",
|
|
17
|
+
# 管理器
|
|
18
|
+
"ChannelManager",
|
|
19
|
+
"ChannelMessage",
|
|
20
|
+
"IChannel",
|
|
21
|
+
# 后端实现
|
|
22
|
+
"MemoryChannel",
|
|
23
|
+
"RedisChannel",
|
|
24
|
+
]
|