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.
Files changed (122) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +60 -36
  4. aury/boot/application/adapter/__init__.py +112 -0
  5. aury/boot/application/adapter/base.py +511 -0
  6. aury/boot/application/adapter/config.py +242 -0
  7. aury/boot/application/adapter/decorators.py +259 -0
  8. aury/boot/application/adapter/exceptions.py +202 -0
  9. aury/boot/application/adapter/http.py +325 -0
  10. aury/boot/application/app/__init__.py +12 -8
  11. aury/boot/application/app/base.py +12 -0
  12. aury/boot/application/app/components.py +137 -44
  13. aury/boot/application/app/middlewares.py +9 -4
  14. aury/boot/application/app/startup.py +249 -0
  15. aury/boot/application/config/__init__.py +36 -1
  16. aury/boot/application/config/multi_instance.py +216 -0
  17. aury/boot/application/config/settings.py +398 -149
  18. aury/boot/application/constants/components.py +6 -0
  19. aury/boot/application/errors/handlers.py +17 -3
  20. aury/boot/application/middleware/logging.py +21 -120
  21. aury/boot/application/rpc/__init__.py +2 -2
  22. aury/boot/commands/__init__.py +30 -10
  23. aury/boot/commands/app.py +131 -1
  24. aury/boot/commands/docs.py +104 -17
  25. aury/boot/commands/generate.py +22 -22
  26. aury/boot/commands/init.py +68 -17
  27. aury/boot/commands/server/app.py +2 -3
  28. aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
  29. aury/boot/commands/templates/project/README.md.tpl +2 -2
  30. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  31. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
  32. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  33. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  34. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  35. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  36. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  37. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  38. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  39. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  40. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  41. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
  42. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  43. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
  44. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  45. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  46. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  47. aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
  48. aury/boot/commands/templates/project/config.py.tpl +10 -10
  49. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  50. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  51. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  52. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  53. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  54. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  55. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  56. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  57. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  58. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  59. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  60. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  61. aury/boot/common/logging/__init__.py +26 -674
  62. aury/boot/common/logging/context.py +132 -0
  63. aury/boot/common/logging/decorators.py +118 -0
  64. aury/boot/common/logging/format.py +315 -0
  65. aury/boot/common/logging/setup.py +214 -0
  66. aury/boot/contrib/admin_console/auth.py +2 -3
  67. aury/boot/contrib/admin_console/install.py +1 -1
  68. aury/boot/domain/models/mixins.py +48 -1
  69. aury/boot/domain/pagination/__init__.py +94 -0
  70. aury/boot/domain/repository/impl.py +1 -1
  71. aury/boot/domain/repository/interface.py +1 -1
  72. aury/boot/domain/transaction/__init__.py +8 -9
  73. aury/boot/infrastructure/__init__.py +86 -29
  74. aury/boot/infrastructure/cache/backends.py +102 -18
  75. aury/boot/infrastructure/cache/base.py +12 -0
  76. aury/boot/infrastructure/cache/manager.py +153 -91
  77. aury/boot/infrastructure/channel/__init__.py +24 -0
  78. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  79. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  80. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  81. aury/boot/infrastructure/channel/base.py +92 -0
  82. aury/boot/infrastructure/channel/manager.py +203 -0
  83. aury/boot/infrastructure/clients/__init__.py +22 -0
  84. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  85. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  86. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  87. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  88. aury/boot/infrastructure/clients/redis/config.py +51 -0
  89. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  90. aury/boot/infrastructure/database/config.py +7 -16
  91. aury/boot/infrastructure/database/manager.py +16 -38
  92. aury/boot/infrastructure/events/__init__.py +18 -21
  93. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  94. aury/boot/infrastructure/events/backends/memory.py +86 -0
  95. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  96. aury/boot/infrastructure/events/backends/redis.py +162 -0
  97. aury/boot/infrastructure/events/base.py +127 -0
  98. aury/boot/infrastructure/events/manager.py +224 -0
  99. aury/boot/infrastructure/mq/__init__.py +24 -0
  100. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  101. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  102. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  103. aury/boot/infrastructure/mq/base.py +143 -0
  104. aury/boot/infrastructure/mq/manager.py +239 -0
  105. aury/boot/infrastructure/scheduler/manager.py +7 -3
  106. aury/boot/infrastructure/storage/__init__.py +9 -9
  107. aury/boot/infrastructure/storage/base.py +17 -5
  108. aury/boot/infrastructure/storage/factory.py +0 -1
  109. aury/boot/infrastructure/tasks/__init__.py +2 -2
  110. aury/boot/infrastructure/tasks/config.py +5 -13
  111. aury/boot/infrastructure/tasks/manager.py +55 -33
  112. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
  113. aury_boot-0.0.7.dist-info/RECORD +197 -0
  114. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  115. aury/boot/commands/templates/project/env.example.tpl +0 -213
  116. aury/boot/infrastructure/events/bus.py +0 -362
  117. aury/boot/infrastructure/events/config.py +0 -52
  118. aury/boot/infrastructure/events/consumer.py +0 -134
  119. aury/boot/infrastructure/events/models.py +0 -63
  120. aury_boot-0.0.4.dist-info/RECORD +0 -137
  121. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
  122. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
@@ -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__(self, url: str, *, serializer: str = "json"):
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
- try:
39
- self._redis = Redis.from_url(
40
- self._url,
41
- encoding="utf-8",
42
- decode_responses=False,
43
- socket_connect_timeout=5,
44
- socket_timeout=5,
45
- )
46
- await self._redis.ping()
47
- logger.info("Redis缓存初始化成功")
48
- except Exception as exc:
49
- logger.error(f"Redis连接失败: {exc}")
50
- raise
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.init_app({
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
- ) -> None:
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
- backend_config_map: dict[CacheBackend, Callable[[], dict[str, Any]]] = {
160
- CacheBackend.REDIS: lambda: {
161
- "CACHE_TYPE": backend.value,
162
- "CACHE_REDIS_URL": url, # TODO: 从应用配置中获取默认值
163
- "CACHE_SERIALIZER": serializer,
164
- },
165
- CacheBackend.MEMORY: lambda: {
166
- "CACHE_TYPE": backend.value,
167
- "CACHE_MAX_SIZE": max_size,
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
- config_builder = backend_config_map.get(backend)
176
- if config_builder is None:
177
- raise ValueError(f"不支持的缓存后端: {backend}")
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
- config = config_builder()
180
- await self.init_app(config)
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("缓存管理器未初始化,请先调用 init_app() 或 initialize()")
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
+ ]
@@ -0,0 +1,9 @@
1
+ """通道后端实现。"""
2
+
3
+ from .memory import MemoryChannel
4
+ from .redis import RedisChannel
5
+
6
+ __all__ = [
7
+ "MemoryChannel",
8
+ "RedisChannel",
9
+ ]
@@ -0,0 +1,83 @@
1
+ """内存通道后端。
2
+
3
+ 适用于单进程场景,如开发环境或简单应用。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from collections.abc import AsyncIterator
10
+ import contextlib
11
+
12
+ from aury.boot.common.logging import logger
13
+
14
+ from ..base import ChannelMessage, IChannel
15
+
16
+
17
+ class MemoryChannel(IChannel):
18
+ """内存通道实现。
19
+
20
+ 使用 asyncio.Queue 实现进程内的发布/订阅。
21
+
22
+ 注意:仅适用于单进程,不支持跨进程通信。
23
+ """
24
+
25
+ def __init__(self, max_subscribers: int = 1000) -> None:
26
+ """初始化内存通道。
27
+
28
+ Args:
29
+ max_subscribers: 每个通道最大订阅者数量
30
+ """
31
+ self._max_subscribers = max_subscribers
32
+ # channel -> list of queues
33
+ self._subscribers: dict[str, list[asyncio.Queue[ChannelMessage]]] = {}
34
+ self._lock = asyncio.Lock()
35
+
36
+ async def publish(self, channel: str, message: ChannelMessage) -> None:
37
+ """发布消息到通道。"""
38
+ message.channel = channel
39
+ async with self._lock:
40
+ subscribers = self._subscribers.get(channel, [])
41
+ for queue in subscribers:
42
+ try:
43
+ queue.put_nowait(message)
44
+ except asyncio.QueueFull:
45
+ logger.warning(f"通道 [{channel}] 订阅者队列已满,消息被丢弃")
46
+
47
+ async def subscribe(self, channel: str) -> AsyncIterator[ChannelMessage]:
48
+ """订阅通道。"""
49
+ queue: asyncio.Queue[ChannelMessage] = asyncio.Queue(maxsize=100)
50
+
51
+ async with self._lock:
52
+ if channel not in self._subscribers:
53
+ self._subscribers[channel] = []
54
+ if len(self._subscribers[channel]) >= self._max_subscribers:
55
+ raise RuntimeError(f"通道 [{channel}] 订阅者数量已达上限")
56
+ self._subscribers[channel].append(queue)
57
+
58
+ try:
59
+ while True:
60
+ message = await queue.get()
61
+ yield message
62
+ finally:
63
+ async with self._lock:
64
+ if channel in self._subscribers:
65
+ with contextlib.suppress(ValueError):
66
+ self._subscribers[channel].remove(queue)
67
+ if not self._subscribers[channel]:
68
+ del self._subscribers[channel]
69
+
70
+ async def unsubscribe(self, channel: str) -> None:
71
+ """取消订阅通道(清除所有订阅者)。"""
72
+ async with self._lock:
73
+ if channel in self._subscribers:
74
+ del self._subscribers[channel]
75
+
76
+ async def close(self) -> None:
77
+ """关闭通道,清理所有订阅。"""
78
+ async with self._lock:
79
+ self._subscribers.clear()
80
+ logger.debug("内存通道已关闭")
81
+
82
+
83
+ __all__ = ["MemoryChannel"]