sdpy-kit 0.1.0__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.
kit/db/transaction.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ Project: sdpy
17
+ Author: Clark Chang
18
+ """
19
+
20
+ # date: 2026-08-23
21
+ """事务管理模块
22
+
23
+ 提供 @transactional 装饰器:捕获 SQLAlchemyError(含 DBError 及其所有子类),
24
+ 确保重试逻辑对数据库异常有效。
25
+ """
26
+
27
+ import functools
28
+ import inspect
29
+ import time
30
+ from collections.abc import Callable
31
+ from typing import TypeVar
32
+
33
+ from sqlalchemy.exc import SQLAlchemyError
34
+
35
+ from kit.config.log_config import get_default_logger
36
+ from kit.db.exceptions import DBError
37
+ from kit.db.session import session_scope
38
+
39
+ logger = get_default_logger(__name__)
40
+
41
+ F = TypeVar("F", bound=Callable[..., object])
42
+
43
+
44
+ def transactional(
45
+ db_type: str | None = None,
46
+ *,
47
+ retry_count: int = 0,
48
+ retry_delay: float = 0.5,
49
+ ) -> Callable[[F], F]:
50
+ """事务装饰器:自动注入 session,统一捕获 SQLAlchemyError 并重试
51
+
52
+ 被装饰的函数必须接受 ``session`` 关键字参数。
53
+ 捕获 SQLAlchemyError(含 DBError、IntegrityError、OperationalError 等所有子类)。
54
+
55
+ Args:
56
+ db_type: 数据库类型,None 时使用配置默认值
57
+ retry_count: 失败后的额外重试次数(0 表示不重试)
58
+ retry_delay: 初始重试延迟(秒),每次重试翻倍(指数退避)
59
+
60
+ 用法::
61
+
62
+ @transactional()
63
+ def transfer(session: Session, from_id: int, to_id: int, amount: float):
64
+ # 多个 CRUD 操作在同一事务中
65
+ sender = user_crud.get_or_raise(session, from_id)
66
+ receiver = user_crud.get_or_raise(session, to_id)
67
+ sender.balance -= amount
68
+ receiver.balance += amount
69
+ # 退出时自动提交
70
+ """
71
+
72
+ def decorator(func: F) -> F:
73
+ sig = inspect.signature(func)
74
+
75
+ @functools.wraps(func)
76
+ def wrapper(*args: object, **kwargs: object) -> object:
77
+ # 检查函数是否接受 session 参数
78
+ if "session" not in sig.parameters:
79
+ raise TypeError(
80
+ f"被 @transactional 装饰的函数 '{func.__name__}' "
81
+ "必须接受 'session' 关键字参数"
82
+ )
83
+
84
+ last_exception: Exception | None = None
85
+
86
+ for attempt in range(retry_count + 1):
87
+ try:
88
+ with session_scope(db_type, autocommit=True) as session:
89
+ kwargs["session"] = session
90
+ return func(*args, **kwargs)
91
+ except SQLAlchemyError as e:
92
+ last_exception = e
93
+ if attempt < retry_count:
94
+ delay = retry_delay * (2**attempt)
95
+ logger.warning(
96
+ f"⚠️ 事务失败,{delay:.1f}s 后重试 "
97
+ f"({attempt + 1}/{retry_count}): {e}"
98
+ )
99
+ time.sleep(delay)
100
+ else:
101
+ break
102
+
103
+ raise DBError(
104
+ f"事务执行失败(重试 {retry_count} 次后仍失败): {last_exception}"
105
+ )
106
+
107
+ return wrapper # type: ignore
108
+
109
+ return decorator