aury-boot 0.0.2__py3-none-any.whl → 0.0.3__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 (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +890 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.3.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,317 @@
1
+ """数据库管理器 - 命名多实例实现。
2
+
3
+ 提供统一的数据库连接管理、会话创建和健康检查功能。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from collections.abc import AsyncGenerator
10
+ from contextlib import asynccontextmanager
11
+
12
+ from sqlalchemy import text
13
+ from sqlalchemy.exc import DisconnectionError, OperationalError
14
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
15
+
16
+ from aury.boot.common.logging import logger
17
+ from aury.boot.infrastructure.database.config import DatabaseConfig
18
+
19
+
20
+ class DatabaseManager:
21
+ """数据库管理器(命名多实例)。
22
+
23
+ 职责:
24
+ 1. 管理数据库引擎和连接池
25
+ 2. 提供会话工厂
26
+ 3. 健康检查和重连机制
27
+ 4. 生命周期管理
28
+ 5. 支持多个命名实例,如主库/从库、不同业务数据库等
29
+
30
+ 使用示例:
31
+ # 默认实例
32
+ db_manager = DatabaseManager.get_instance()
33
+ await db_manager.initialize()
34
+
35
+ # 命名实例
36
+ primary = DatabaseManager.get_instance("primary")
37
+ replica = DatabaseManager.get_instance("replica")
38
+
39
+ # 获取会话
40
+ async with db_manager.session() as session:
41
+ # 使用 session 进行数据库操作
42
+ pass
43
+
44
+ # 清理
45
+ await db_manager.cleanup()
46
+ """
47
+
48
+ _instances: dict[str, DatabaseManager] = {}
49
+
50
+ def __init__(self, name: str = "default") -> None:
51
+ """初始化数据库管理器。
52
+
53
+ Args:
54
+ name: 实例名称
55
+ """
56
+ self.name = name
57
+ self._initialized: bool = False
58
+ self._config: DatabaseConfig | None = None
59
+ self._engine: AsyncEngine | None = None
60
+ self._session_factory: async_sessionmaker | None = None
61
+ self._max_retries: int = 3
62
+ self._retry_delay: float = 1.0
63
+
64
+ @classmethod
65
+ def get_instance(cls, name: str = "default") -> DatabaseManager:
66
+ """获取指定名称的实例。
67
+
68
+ Args:
69
+ name: 实例名称,默认为 "default"
70
+
71
+ Returns:
72
+ DatabaseManager: 数据库管理器实例
73
+ """
74
+ if name not in cls._instances:
75
+ cls._instances[name] = cls(name)
76
+ return cls._instances[name]
77
+
78
+ @classmethod
79
+ def reset_instance(cls, name: str | None = None) -> None:
80
+ """重置实例(仅用于测试)。
81
+
82
+ Args:
83
+ name: 要重置的实例名称。如果为 None,则重置所有实例。
84
+
85
+ 注意:调用此方法前应先调用 cleanup() 释放资源。
86
+ """
87
+ if name is None:
88
+ cls._instances.clear()
89
+ elif name in cls._instances:
90
+ del cls._instances[name]
91
+
92
+ def configure(self, config: DatabaseConfig) -> None:
93
+ """配置数据库管理器。
94
+
95
+ Args:
96
+ config: 数据库配置
97
+ """
98
+ self._config = config
99
+
100
+ @property
101
+ def engine(self) -> AsyncEngine:
102
+ """获取数据库引擎。"""
103
+ if self._engine is None:
104
+ raise RuntimeError("数据库管理器未初始化,请先调用 initialize()")
105
+ return self._engine
106
+
107
+ @property
108
+ def session_factory(self) -> async_sessionmaker:
109
+ """获取会话工厂。"""
110
+ if self._session_factory is None:
111
+ raise RuntimeError("数据库管理器未初始化,请先调用 initialize()")
112
+ return self._session_factory
113
+
114
+ @property
115
+ def is_initialized(self) -> bool:
116
+ """检查是否已初始化。"""
117
+ return self._initialized
118
+
119
+ async def initialize(
120
+ self,
121
+ url: str | None = None,
122
+ *,
123
+ echo: bool | None = None,
124
+ pool_size: int | None = None,
125
+ max_overflow: int | None = None,
126
+ pool_timeout: int | None = None,
127
+ pool_recycle: int | None = None,
128
+ isolation_level: str | None = None,
129
+ ) -> None:
130
+ """初始化数据库连接。
131
+
132
+ Args:
133
+ url: 数据库连接字符串,默认从配置读取
134
+ echo: 是否打印SQL语句
135
+ pool_size: 连接池大小
136
+ max_overflow: 最大溢出连接数
137
+ pool_timeout: 连接超时时间(秒)
138
+ pool_recycle: 连接回收时间(秒)
139
+ isolation_level: 事务隔离级别
140
+ """
141
+ if self._initialized:
142
+ logger.warning("数据库管理器已初始化,跳过重复初始化")
143
+ return
144
+
145
+ # 使用提供的参数或配置中的默认值
146
+ db_isolation_level: str | None = None
147
+ if self._config is not None:
148
+ database_url = url or self._config.url
149
+ db_echo = echo if echo is not None else self._config.echo
150
+ db_pool_size = pool_size or self._config.pool_size
151
+ db_max_overflow = max_overflow or self._config.max_overflow
152
+ db_pool_timeout = pool_timeout or self._config.pool_timeout
153
+ db_pool_recycle = pool_recycle or self._config.pool_recycle
154
+ db_isolation_level = isolation_level or self._config.isolation_level
155
+ else:
156
+ # 如果没有配置,使用环境变量
157
+ import os
158
+ database_url = url or os.getenv("DATABASE_URL")
159
+ if not database_url:
160
+ raise ValueError(
161
+ "数据库 URL 未配置。请通过以下方式之一提供:"
162
+ "1. 使用 DatabaseManager.configure() 设置配置"
163
+ "2. 通过 initialize(url=...) 参数传入"
164
+ "3. 设置环境变量 DATABASE_URL"
165
+ )
166
+ db_echo = echo if echo is not None else os.getenv("DB_ECHO", "false").lower() == "true"
167
+ db_pool_size = pool_size or int(os.getenv("DB_POOL_SIZE", "5"))
168
+ db_max_overflow = max_overflow or int(os.getenv("DB_MAX_OVERFLOW", "10"))
169
+ db_pool_timeout = pool_timeout or int(os.getenv("DB_POOL_TIMEOUT", "30"))
170
+ db_pool_recycle = pool_recycle or int(os.getenv("DB_POOL_RECYCLE", "1800"))
171
+ db_isolation_level = isolation_level or os.getenv("DATABASE_ISOLATION_LEVEL")
172
+
173
+ # 构建引擎参数
174
+ engine_kwargs: dict = {
175
+ "echo": db_echo,
176
+ "future": True,
177
+ "pool_pre_ping": True,
178
+ "pool_size": db_pool_size,
179
+ "max_overflow": db_max_overflow,
180
+ "pool_timeout": db_pool_timeout,
181
+ "pool_recycle": db_pool_recycle,
182
+ }
183
+
184
+ # 添加隔离级别(如果配置了)
185
+ if db_isolation_level:
186
+ engine_kwargs["isolation_level"] = db_isolation_level
187
+ logger.info(f"事务隔离级别设置为: {db_isolation_level}")
188
+
189
+ self._engine = create_async_engine(database_url, **engine_kwargs)
190
+
191
+ self._session_factory = async_sessionmaker(
192
+ self._engine,
193
+ expire_on_commit=False,
194
+ autoflush=False,
195
+ class_=AsyncSession,
196
+ )
197
+
198
+ # 验证连接
199
+ await self.health_check()
200
+
201
+ self._initialized = True
202
+ logger.info("数据库管理器初始化完成")
203
+
204
+ async def health_check(self) -> bool:
205
+ """健康检查。
206
+
207
+ Returns:
208
+ bool: 连接是否正常
209
+ """
210
+ try:
211
+ async with self.engine.begin() as conn:
212
+ await conn.execute(text("SELECT 1"))
213
+ logger.debug("数据库健康检查通过")
214
+ return True
215
+ except Exception as exc:
216
+ logger.error(f"数据库健康检查失败: {exc}")
217
+ return False
218
+
219
+ async def _check_session_connection(self, session: AsyncSession) -> None:
220
+ """检查会话连接状态,必要时重连。
221
+
222
+ 使用 SQLAlchemy 的通用异常类,支持所有数据库后端。
223
+
224
+ Args:
225
+ session: 数据库会话
226
+
227
+ Raises:
228
+ Exception: 重试次数耗尽后抛出异常
229
+ """
230
+ retries = self._max_retries
231
+ while retries > 0:
232
+ try:
233
+ await session.execute(text("SELECT 1"))
234
+ return
235
+ except (DisconnectionError, OperationalError) as exc:
236
+ logger.warning(f"数据库连接丢失,剩余重试次数: {retries}, 错误: {exc}")
237
+ retries -= 1
238
+ if retries == 0:
239
+ logger.error("数据库连接重试失败")
240
+ raise
241
+ await asyncio.sleep(self._retry_delay)
242
+ except Exception as exc:
243
+ # 其他异常直接抛出,不重试
244
+ logger.error(f"数据库连接检查失败: {exc}")
245
+ raise
246
+
247
+ @asynccontextmanager
248
+ async def session(self) -> AsyncGenerator[AsyncSession]:
249
+ """获取数据库会话(上下文管理器)。
250
+
251
+ Yields:
252
+ AsyncSession: 数据库会话
253
+
254
+ 使用示例:
255
+ async with db_manager.session() as session:
256
+ result = await session.execute(query)
257
+ """
258
+ session = self.session_factory()
259
+ try:
260
+ await self._check_session_connection(session)
261
+ yield session
262
+ except Exception as exc:
263
+ await session.rollback()
264
+ logger.exception(f"数据库会话异常: {exc}")
265
+ raise
266
+ finally:
267
+ await session.close()
268
+
269
+ async def create_session(self) -> AsyncSession:
270
+ """创建新的数据库会话(需要手动关闭)。
271
+
272
+ Returns:
273
+ AsyncSession: 数据库会话
274
+
275
+ 注意:使用后需要手动调用 await session.close()
276
+ 建议使用 session() 上下文管理器代替此方法。
277
+ """
278
+ session = self.session_factory()
279
+ await self._check_session_connection(session)
280
+ return session
281
+
282
+ async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
283
+ """FastAPI 依赖注入专用的会话获取器。
284
+
285
+ Yields:
286
+ AsyncSession: 数据库会话
287
+
288
+ 使用示例(FastAPI 路由):
289
+ @router.get("/items")
290
+ async def get_items(
291
+ session: AsyncSession = Depends(db_manager.get_session),
292
+ ):
293
+ ...
294
+ """
295
+ async with self.session() as session:
296
+ yield session
297
+
298
+ async def cleanup(self) -> None:
299
+ """清理资源,关闭所有连接。"""
300
+ if self._engine is not None:
301
+ await self._engine.dispose()
302
+ logger.info("数据库连接已关闭")
303
+
304
+ self._engine = None
305
+ self._session_factory = None
306
+ self._initialized = False
307
+
308
+ def __repr__(self) -> str:
309
+ """字符串表示。"""
310
+ status = "initialized" if self._initialized else "not initialized"
311
+ return f"<DatabaseManager status={status}>"
312
+
313
+
314
+ __all__ = [
315
+ "DatabaseManager",
316
+ ]
317
+
@@ -0,0 +1,164 @@
1
+ """查询优化工具。
2
+
3
+ 提供缓存和性能监控装饰器。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from functools import wraps
10
+ import hashlib
11
+ import time
12
+
13
+ from aury.boot.common.logging import logger
14
+
15
+ # 查询性能监控配置
16
+ QUERY_SLOW_THRESHOLD = 1.0 # 慢查询阈值(秒)
17
+
18
+
19
+ def cache_query(
20
+ ttl: int = 300,
21
+ key_prefix: str = "",
22
+ key_func: Callable | None = None,
23
+ ) -> Callable:
24
+ """查询结果缓存装饰器。
25
+
26
+ 缓存查询结果,减少数据库访问。
27
+ 集成现有的 CacheManager。
28
+
29
+ Args:
30
+ ttl: 缓存过期时间(秒),默认 300 秒
31
+ key_prefix: 缓存键前缀
32
+ key_func: 自定义缓存键生成函数
33
+
34
+ 用法:
35
+ class UserRepository(BaseRepository):
36
+ @cache_query(ttl=600, key_prefix="user")
37
+ async def get_by_email(self, email: str):
38
+ return await self.get_by(email=email)
39
+ """
40
+
41
+ def decorator(func: Callable) -> Callable:
42
+ @wraps(func)
43
+ async def wrapper(self, *args, **kwargs):
44
+ try:
45
+ from aury.boot.infrastructure.cache import CacheManager
46
+
47
+ cache = CacheManager.get_instance()
48
+
49
+ # 生成缓存键
50
+ if key_func:
51
+ cache_key = key_func(self, *args, **kwargs)
52
+ else:
53
+ # 默认缓存键生成策略
54
+ key_parts = [key_prefix, func.__name__]
55
+ if args:
56
+ key_parts.extend(str(arg) for arg in args)
57
+ if kwargs:
58
+ key_parts.extend(f"{k}:{v}" for k, v in sorted(kwargs.items()))
59
+
60
+ # 使用 MD5 生成固定长度的键
61
+ key_str = ":".join(str(p) for p in key_parts)
62
+ cache_key = f"repo:{hashlib.md5(key_str.encode()).hexdigest()}"
63
+
64
+ # 尝试从缓存获取
65
+ cached_result = await cache.get(cache_key)
66
+ if cached_result is not None:
67
+ logger.debug(f"缓存命中: {cache_key}")
68
+ return cached_result
69
+
70
+ # 执行查询
71
+ result = await func(self, *args, **kwargs)
72
+
73
+ # 存入缓存
74
+ await cache.set(cache_key, result, expire=ttl)
75
+ logger.debug(f"缓存写入: {cache_key}, TTL={ttl}s")
76
+
77
+ return result
78
+ except Exception as e:
79
+ # 缓存失败不影响主流程
80
+ logger.warning(f"查询缓存失败: {e},继续执行查询")
81
+ return await func(self, *args, **kwargs)
82
+
83
+ return wrapper
84
+
85
+ return decorator
86
+
87
+
88
+ def monitor_query(
89
+ slow_threshold: float = QUERY_SLOW_THRESHOLD,
90
+ enable_explain: bool = False,
91
+ ) -> Callable:
92
+ """查询性能监控装饰器。
93
+
94
+ 监控查询执行时间,记录慢查询日志。
95
+ 支持 SQLAlchemy explain() 功能。
96
+
97
+ Args:
98
+ slow_threshold: 慢查询阈值(秒),默认 1.0 秒
99
+ enable_explain: 是否启用查询计划分析
100
+
101
+ 用法:
102
+ class UserRepository(BaseRepository):
103
+ @monitor_query(slow_threshold=0.5)
104
+ async def list(self, **filters):
105
+ return await super().list(**filters)
106
+ """
107
+
108
+ def decorator(func: Callable) -> Callable:
109
+ @wraps(func)
110
+ async def wrapper(self, *args, **kwargs):
111
+ start_time = time.time()
112
+
113
+ try:
114
+ # 执行查询
115
+ result = await func(self, *args, **kwargs)
116
+
117
+ # 计算执行时间
118
+ duration = time.time() - start_time
119
+
120
+ # 记录慢查询
121
+ if duration >= slow_threshold:
122
+ logger.warning(
123
+ f"慢查询检测: {func.__name__} 执行时间 {duration:.3f}s "
124
+ f"(阈值: {slow_threshold}s)"
125
+ )
126
+
127
+ # 如果启用 explain,尝试获取查询计划
128
+ if enable_explain and hasattr(self, "_last_query"):
129
+ try:
130
+ from sqlalchemy import text
131
+ explain_result = await self._session.execute(
132
+ text(f"EXPLAIN {self._last_query!s}")
133
+ )
134
+ explain_text = "\n".join(str(row) for row in explain_result)
135
+ logger.debug(f"查询计划:\n{explain_text}")
136
+ except Exception as e:
137
+ logger.debug(f"无法获取查询计划: {e}")
138
+
139
+ # 记录正常查询(调试级别)
140
+ else:
141
+ logger.debug(
142
+ f"查询执行: {func.__name__} 耗时 {duration:.3f}s"
143
+ )
144
+
145
+ return result
146
+ except Exception as e:
147
+ duration = time.time() - start_time
148
+ logger.error(
149
+ f"查询失败: {func.__name__} 执行时间 {duration:.3f}s, 错误: {e}"
150
+ )
151
+ raise
152
+
153
+ return wrapper
154
+
155
+ return decorator
156
+
157
+
158
+ __all__ = [
159
+ "cache_query",
160
+ "monitor_query",
161
+ ]
162
+
163
+
164
+
@@ -0,0 +1,198 @@
1
+ """UPSERT 策略实现。
2
+
3
+ 为不同数据库提供 UPSERT 操作的策略实现。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, ClassVar
10
+
11
+ from sqlalchemy import Table, text
12
+ from sqlalchemy.ext.asyncio import AsyncSession
13
+
14
+ from aury.boot.common.logging import logger
15
+
16
+
17
+ class UpsertStrategy(ABC):
18
+ """UPSERT 策略抽象基类。
19
+
20
+ 为不同数据库提供 UPSERT 操作的统一接口。
21
+ """
22
+
23
+ @abstractmethod
24
+ async def execute(
25
+ self,
26
+ session: AsyncSession,
27
+ table: Table,
28
+ data_list: list[dict[str, Any]],
29
+ index_elements: list[str],
30
+ update_columns: list[str],
31
+ ) -> None:
32
+ """执行 UPSERT 操作。
33
+
34
+ Args:
35
+ session: 数据库会话
36
+ table: 表对象
37
+ data_list: 数据字典列表
38
+ index_elements: 索引字段列表
39
+ update_columns: 需要更新的字段列表
40
+ """
41
+ pass
42
+
43
+
44
+ class PostgreSQLUpsertStrategy(UpsertStrategy):
45
+ """PostgreSQL UPSERT 策略。
46
+
47
+ 使用 ON CONFLICT DO UPDATE 语法。
48
+ """
49
+
50
+ async def execute(
51
+ self,
52
+ session: AsyncSession,
53
+ table: Table,
54
+ data_list: list[dict[str, Any]],
55
+ index_elements: list[str],
56
+ update_columns: list[str],
57
+ ) -> None:
58
+ """执行 PostgreSQL UPSERT。"""
59
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
60
+
61
+ stmt = pg_insert(table).values(data_list)
62
+ stmt = stmt.on_conflict_do_update(
63
+ index_elements=index_elements,
64
+ set_={col: stmt.excluded[col] for col in update_columns}
65
+ )
66
+
67
+ await session.execute(stmt)
68
+ await session.flush()
69
+ logger.debug(f"批量插入或更新 {len(data_list)} 条记录(PostgreSQL)")
70
+
71
+
72
+ class SQLiteUpsertStrategy(UpsertStrategy):
73
+ """SQLite UPSERT 策略。
74
+
75
+ 使用 ON CONFLICT DO UPDATE 语法(SQLite 3.24.0+)。
76
+ """
77
+
78
+ async def execute(
79
+ self,
80
+ session: AsyncSession,
81
+ table: Table,
82
+ data_list: list[dict[str, Any]],
83
+ index_elements: list[str],
84
+ update_columns: list[str],
85
+ ) -> None:
86
+ """执行 SQLite UPSERT。"""
87
+ from sqlalchemy.dialects.sqlite import insert as sqlite_insert
88
+
89
+ stmt = sqlite_insert(table).values(data_list)
90
+ stmt = stmt.on_conflict_do_update(
91
+ index_elements=index_elements,
92
+ set_={col: stmt.excluded[col] for col in update_columns}
93
+ )
94
+
95
+ await session.execute(stmt)
96
+ await session.flush()
97
+ logger.debug(f"批量插入或更新 {len(data_list)} 条记录(SQLite)")
98
+
99
+
100
+ class MySQLUpsertStrategy(UpsertStrategy):
101
+ """MySQL UPSERT 策略。
102
+
103
+ 使用 ON DUPLICATE KEY UPDATE 语法。
104
+ """
105
+
106
+ async def execute(
107
+ self,
108
+ session: AsyncSession,
109
+ table: Table,
110
+ data_list: list[dict[str, Any]],
111
+ index_elements: list[str],
112
+ update_columns: list[str],
113
+ ) -> None:
114
+ """执行 MySQL UPSERT。"""
115
+ # 构建批量 INSERT ... ON DUPLICATE KEY UPDATE 语句
116
+ columns = list(data_list[0].keys())
117
+ column_names = ", ".join(columns)
118
+
119
+ # 构建 VALUES 子句(批量插入)
120
+ values_placeholders = []
121
+ params = {}
122
+ for idx, data in enumerate(data_list):
123
+ row_placeholders = []
124
+ for col in columns:
125
+ param_name = f"{col}_{idx}"
126
+ row_placeholders.append(f":{param_name}")
127
+ params[param_name] = data[col]
128
+ values_placeholders.append(f"({', '.join(row_placeholders)})")
129
+
130
+ values_clause = ", ".join(values_placeholders)
131
+
132
+ # 构建 ON DUPLICATE KEY UPDATE 子句
133
+ update_clauses = [f"{col} = VALUES({col})" for col in update_columns]
134
+ update_clause = ", ".join(update_clauses)
135
+
136
+ # 执行 SQL
137
+ sql = f"INSERT INTO {table.name} ({column_names}) VALUES {values_clause} ON DUPLICATE KEY UPDATE {update_clause}"
138
+ await session.execute(text(sql), params)
139
+ await session.flush()
140
+ logger.debug(f"批量插入或更新 {len(data_list)} 条记录(MySQL)")
141
+
142
+
143
+ class UpsertStrategyFactory:
144
+ """UPSERT 策略工厂。
145
+
146
+ 根据数据库方言创建相应的策略实例。
147
+ """
148
+
149
+ _strategies: ClassVar[dict[str, type[UpsertStrategy]]] = {
150
+ "postgresql": PostgreSQLUpsertStrategy,
151
+ "sqlite": SQLiteUpsertStrategy,
152
+ "mysql": MySQLUpsertStrategy,
153
+ }
154
+
155
+ @classmethod
156
+ def create(cls, dialect_name: str) -> UpsertStrategy:
157
+ """创建 UPSERT 策略实例。
158
+
159
+ Args:
160
+ dialect_name: 数据库方言名称
161
+
162
+ Returns:
163
+ UpsertStrategy: 策略实例
164
+
165
+ Raises:
166
+ NotImplementedError: 如果数据库不支持 UPSERT 操作
167
+ """
168
+ strategy_class = cls._strategies.get(dialect_name)
169
+ if strategy_class is None:
170
+ raise NotImplementedError(
171
+ f"数据库 {dialect_name} 不支持 UPSERT 操作。"
172
+ "请使用 bulk_insert() 或实现自定义的插入/更新逻辑。"
173
+ )
174
+
175
+ return strategy_class()
176
+
177
+ @classmethod
178
+ def register(cls, dialect_name: str, strategy_class: type[UpsertStrategy]) -> None:
179
+ """注册自定义策略。
180
+
181
+ Args:
182
+ dialect_name: 数据库方言名称
183
+ strategy_class: 策略类
184
+ """
185
+ cls._strategies[dialect_name] = strategy_class
186
+ logger.debug(f"注册 UPSERT 策略: {dialect_name} -> {strategy_class.__name__}")
187
+
188
+
189
+ __all__ = [
190
+ "MySQLUpsertStrategy",
191
+ "PostgreSQLUpsertStrategy",
192
+ "SQLiteUpsertStrategy",
193
+ "UpsertStrategy",
194
+ "UpsertStrategyFactory",
195
+ ]
196
+
197
+
198
+
@@ -0,0 +1,15 @@
1
+ """依赖注入模块。
2
+
3
+ 提供依赖注入容器和相关功能。
4
+ """
5
+
6
+ from .container import Container, Lifetime, Scope, ServiceDescriptor
7
+
8
+ __all__ = [
9
+ "Container",
10
+ "Lifetime",
11
+ "Scope",
12
+ "ServiceDescriptor",
13
+ ]
14
+
15
+