lush-sqlalchemyx 0.2.1__tar.gz → 0.3.1__tar.gz
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.
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/PKG-INFO +1 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/pyproject.toml +26 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/_compat.py +1 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/__init__.py +72 -24
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_async.py +166 -29
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_async_v2.py +184 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_common.py +42 -21
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_pagination.py +90 -0
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_params.py +32 -0
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_repository.py +311 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/dal/_sync.py +166 -64
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_sync_v2.py +183 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/flask/ext.py +2 -2
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/README.md +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/base/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/fastapi/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/fastapi/depends/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/integrations/flask/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/manager.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/mapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/sync_manager.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/mgrs/mysql/sync_mapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/py.typed +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/same_impl_just_warn_wrapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/shortcuts/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.1}/src/lush_sqlalchemyx/shortcuts/meta.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lush-sqlalchemyx"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "SQLAlchemy helpers (DAL) and async MySQL managers, with some web frameworks integrations"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -47,6 +47,8 @@ dev = [
|
|
|
47
47
|
"sqlalchemy[asyncio]>=2.0.43",
|
|
48
48
|
"flask-sqlalchemy>=3.1.1",
|
|
49
49
|
"flask>=3.0.0",
|
|
50
|
+
"pytest-benchmark>=5.2.3",
|
|
51
|
+
"pymysql>=1.1.2",
|
|
50
52
|
]
|
|
51
53
|
|
|
52
54
|
[tool.pytest.ini_options]
|
|
@@ -147,6 +149,9 @@ ignore = [
|
|
|
147
149
|
]
|
|
148
150
|
|
|
149
151
|
[tool.ruff.lint.per-file-ignores]
|
|
152
|
+
"**/dal/_*_v2.py" = [
|
|
153
|
+
"ARG003", # extra 参数是 ABC 合规所必需但在部分方法中未使用
|
|
154
|
+
]
|
|
150
155
|
"tests/**/*.py" = [
|
|
151
156
|
# === 测试代码质量 ===
|
|
152
157
|
"B011", # assert False应改为raise AssertionError
|
|
@@ -249,6 +254,7 @@ reportAny = "none"
|
|
|
249
254
|
reportExplicitAny = "none"
|
|
250
255
|
reportConstantRedefinition = "none"
|
|
251
256
|
reportUnnecessaryComparison = "none"
|
|
257
|
+
reportImplicitOverride = false
|
|
252
258
|
|
|
253
259
|
[[tool.basedpyright.executionEnvironments]]
|
|
254
260
|
root = "tests"
|
|
@@ -277,10 +283,29 @@ reportMissingTypeArgument = false
|
|
|
277
283
|
reportOptionalMemberAccess = false
|
|
278
284
|
reportAttributeAccessIssue = "none"
|
|
279
285
|
reportGeneralTypeIssues = false
|
|
286
|
+
reportAssignmentType = false
|
|
280
287
|
reportOperatorIssue = false
|
|
281
288
|
reportIndexIssue = false
|
|
282
289
|
reportInvalidTypeArguments = "none"
|
|
290
|
+
reportUnknownLambdaType = false
|
|
291
|
+
reportImplicitOverride = false
|
|
292
|
+
reportUnusedClass = false
|
|
293
|
+
|
|
283
294
|
|
|
295
|
+
[[tool.basedpyright.executionEnvironments]]
|
|
296
|
+
root = "src/lush_sqlalchemyx/integrations"
|
|
297
|
+
reportAttributeAccessIssue = "none"
|
|
298
|
+
reportAssignmentType = false
|
|
299
|
+
reportImportCycles = false
|
|
300
|
+
reportUnknownVariableType = false
|
|
301
|
+
reportUnknownParameterType = false
|
|
302
|
+
reportUnknownMemberType = false
|
|
303
|
+
reportUnknownArgumentType = false
|
|
284
304
|
|
|
285
305
|
[[tool.basedpyright.executionEnvironments]]
|
|
286
306
|
root = "src"
|
|
307
|
+
|
|
308
|
+
[[tool.basedpyright.executionEnvironments]]
|
|
309
|
+
root = "examples"
|
|
310
|
+
# examples 作为下游类型检查 SSOT, 仅继承全局设置, 不做额外放宽.
|
|
311
|
+
# 如果此处报错, 说明库的类型变更破坏了合法下游用法.
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
_HAS_ASYNC: bool
|
|
6
6
|
try:
|
|
7
|
-
from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession # noqa: F401
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession # noqa: F401 # pyright: ignore[reportUnusedImport]
|
|
8
8
|
|
|
9
9
|
_HAS_ASYNC = True
|
|
10
10
|
except ImportError: # pragma: no cover
|
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
# --- shared (sync/async agnostic) ---
|
|
7
|
-
# --- lush-dal-protocol
|
|
7
|
+
# --- lush-dal-protocol ABCs (ORM 无关的抽象层) ---
|
|
8
8
|
from lush_dal_protocol import (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
AbstractAsyncAdvancedWriteDAL,
|
|
10
|
+
AbstractAsyncBaseDAL,
|
|
11
|
+
AbstractAsyncBatchFieldDAL,
|
|
12
|
+
AbstractAsyncLockDAL,
|
|
13
|
+
AbstractAsyncRawSQLDAL,
|
|
14
|
+
AbstractAsyncReadDAL,
|
|
15
|
+
AbstractAsyncWriteDAL,
|
|
16
|
+
AbstractSyncAdvancedWriteDAL,
|
|
17
|
+
AbstractSyncBaseDAL,
|
|
18
|
+
AbstractSyncBatchFieldDAL,
|
|
19
|
+
AbstractSyncLockDAL,
|
|
20
|
+
AbstractSyncRawSQLDAL,
|
|
21
|
+
AbstractSyncReadDAL,
|
|
22
|
+
AbstractSyncWriteDAL,
|
|
15
23
|
)
|
|
16
24
|
|
|
17
25
|
# --- async (requires sqlalchemy[asyncio]) ---
|
|
@@ -32,6 +40,9 @@ from ._async import (
|
|
|
32
40
|
async_temp_set_lock_wait_timeout,
|
|
33
41
|
async_with_retry,
|
|
34
42
|
)
|
|
43
|
+
|
|
44
|
+
# --- V2 (ABC-compliant, options-based) ---
|
|
45
|
+
from ._async_v2 import AsyncBaseDALV2, AsyncReadDALV2, AsyncWriteDALV2
|
|
35
46
|
from ._common import (
|
|
36
47
|
DEFAULT_RETRY_CONFIG,
|
|
37
48
|
OPTIMISTIC_LOCK_ERROR_MSG_TRAIT,
|
|
@@ -39,7 +50,6 @@ from ._common import (
|
|
|
39
50
|
READONLY_SESSION_FLAG,
|
|
40
51
|
BaseCU,
|
|
41
52
|
BaseDTO,
|
|
42
|
-
BaseModelT,
|
|
43
53
|
CUModelT,
|
|
44
54
|
DBRetryableError,
|
|
45
55
|
DTOModelT,
|
|
@@ -50,16 +60,27 @@ from ._common import (
|
|
|
50
60
|
SQLATableT,
|
|
51
61
|
StdBaseCU,
|
|
52
62
|
StdBaseDTO,
|
|
53
|
-
T,
|
|
54
|
-
V,
|
|
55
|
-
_ensure_strict_fields,
|
|
56
63
|
escape_like,
|
|
57
64
|
filtered_in_sql_values,
|
|
58
65
|
)
|
|
59
66
|
|
|
60
67
|
# Event listener references — accessed by tests via getattr(module, name).
|
|
61
|
-
from ._common import __prevent_readonly_write as __prevent_readonly_write
|
|
62
|
-
from ._common import __receive_before_flush as __receive_before_flush
|
|
68
|
+
from ._common import __prevent_readonly_write as __prevent_readonly_write # pyright: ignore[reportPrivateUsage]
|
|
69
|
+
from ._common import __receive_before_flush as __receive_before_flush # pyright: ignore[reportPrivateUsage]
|
|
70
|
+
from ._pagination import (
|
|
71
|
+
CursorPagination,
|
|
72
|
+
CursorResult,
|
|
73
|
+
OffsetPagination,
|
|
74
|
+
PageResult,
|
|
75
|
+
build_cursor_stmt,
|
|
76
|
+
build_offset_stmt,
|
|
77
|
+
decode_cursor,
|
|
78
|
+
encode_cursor,
|
|
79
|
+
make_cursor_result,
|
|
80
|
+
make_page_result,
|
|
81
|
+
)
|
|
82
|
+
from ._params import SQLAExtra
|
|
83
|
+
from ._repository import AsyncSQLAlchemyRepository, SyncSQLAlchemyRepository
|
|
63
84
|
|
|
64
85
|
# --- sync ---
|
|
65
86
|
from ._sync import (
|
|
@@ -79,6 +100,7 @@ from ._sync import (
|
|
|
79
100
|
sync_temp_set_lock_wait_timeout,
|
|
80
101
|
sync_with_retry,
|
|
81
102
|
)
|
|
103
|
+
from ._sync_v2 import SyncBaseDALV2, SyncReadDALV2, SyncWriteDALV2
|
|
82
104
|
|
|
83
105
|
__all__ = (
|
|
84
106
|
# common
|
|
@@ -88,7 +110,6 @@ __all__ = (
|
|
|
88
110
|
"READONLY_SESSION_FLAG",
|
|
89
111
|
"BaseCU",
|
|
90
112
|
"BaseDTO",
|
|
91
|
-
"BaseModelT",
|
|
92
113
|
"CUModelT",
|
|
93
114
|
"DBRetryableError",
|
|
94
115
|
"DTOModelT",
|
|
@@ -99,9 +120,6 @@ __all__ = (
|
|
|
99
120
|
"SoftDeleteTableMixin",
|
|
100
121
|
"StdBaseCU",
|
|
101
122
|
"StdBaseDTO",
|
|
102
|
-
"T",
|
|
103
|
-
"V",
|
|
104
|
-
"_ensure_strict_fields",
|
|
105
123
|
"escape_like",
|
|
106
124
|
"filtered_in_sql_values",
|
|
107
125
|
# async
|
|
@@ -136,11 +154,41 @@ __all__ = (
|
|
|
136
154
|
"SyncXDALOp",
|
|
137
155
|
"sync_temp_set_lock_wait_timeout",
|
|
138
156
|
"sync_with_retry",
|
|
139
|
-
#
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
157
|
+
# pagination
|
|
158
|
+
"CursorPagination",
|
|
159
|
+
"CursorResult",
|
|
160
|
+
"OffsetPagination",
|
|
161
|
+
"PageResult",
|
|
162
|
+
"build_cursor_stmt",
|
|
163
|
+
"build_offset_stmt",
|
|
164
|
+
"decode_cursor",
|
|
165
|
+
"encode_cursor",
|
|
166
|
+
"make_cursor_result",
|
|
167
|
+
"make_page_result",
|
|
168
|
+
# V2
|
|
169
|
+
"AsyncBaseDALV2",
|
|
170
|
+
"AsyncReadDALV2",
|
|
171
|
+
"AsyncWriteDALV2",
|
|
172
|
+
"SQLAExtra",
|
|
173
|
+
"SyncBaseDALV2",
|
|
174
|
+
"SyncReadDALV2",
|
|
175
|
+
"SyncWriteDALV2",
|
|
176
|
+
# repository
|
|
177
|
+
"AsyncSQLAlchemyRepository",
|
|
178
|
+
"SyncSQLAlchemyRepository",
|
|
179
|
+
# lush-dal-protocol ABCs
|
|
180
|
+
"AbstractAsyncAdvancedWriteDAL",
|
|
181
|
+
"AbstractAsyncBaseDAL",
|
|
182
|
+
"AbstractAsyncBatchFieldDAL",
|
|
183
|
+
"AbstractAsyncLockDAL",
|
|
184
|
+
"AbstractAsyncRawSQLDAL",
|
|
185
|
+
"AbstractAsyncReadDAL",
|
|
186
|
+
"AbstractAsyncWriteDAL",
|
|
187
|
+
"AbstractSyncAdvancedWriteDAL",
|
|
188
|
+
"AbstractSyncBaseDAL",
|
|
189
|
+
"AbstractSyncBatchFieldDAL",
|
|
190
|
+
"AbstractSyncLockDAL",
|
|
191
|
+
"AbstractSyncRawSQLDAL",
|
|
192
|
+
"AbstractSyncReadDAL",
|
|
193
|
+
"AbstractSyncWriteDAL",
|
|
146
194
|
)
|
|
@@ -9,7 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import datetime
|
|
11
11
|
import logging
|
|
12
|
-
|
|
12
|
+
import warnings
|
|
13
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
|
|
13
14
|
from contextlib import asynccontextmanager, suppress
|
|
14
15
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, TypeVar, cast
|
|
15
16
|
|
|
@@ -101,7 +102,7 @@ def async_with_retry(
|
|
|
101
102
|
async def async_temp_set_lock_wait_timeout(
|
|
102
103
|
session: AsyncSession,
|
|
103
104
|
timeout_seconds: int | None,
|
|
104
|
-
) ->
|
|
105
|
+
) -> AsyncGenerator[None, None]:
|
|
105
106
|
"""临时设置锁等待超时时间的上下文管理器"""
|
|
106
107
|
if timeout_seconds is None:
|
|
107
108
|
yield
|
|
@@ -120,7 +121,8 @@ async def async_temp_set_lock_wait_timeout(
|
|
|
120
121
|
# Async Table bases
|
|
121
122
|
# ---------------------------------------------------------------------------
|
|
122
123
|
|
|
123
|
-
AsyncSQLATableT
|
|
124
|
+
# 隐含约束: AsyncSQLATableT 应为 AsyncSqlATableBase (AsyncAttrs + DeclarativeBase) 子类.
|
|
125
|
+
AsyncSQLATableT = TypeVar("AsyncSQLATableT")
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
class AsyncSqlATableBase(AsyncAttrs, DeclarativeBase):
|
|
@@ -140,10 +142,23 @@ class ReadOnlyBasicAsyncBaseTable(AsyncSqlATableBase, ReadOnlyMixin):
|
|
|
140
142
|
|
|
141
143
|
|
|
142
144
|
class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
|
|
143
|
-
"""
|
|
145
|
+
"""标准异步表类: 包含 id/时间戳/操作人/软删除等标准字段.
|
|
146
|
+
|
|
147
|
+
.. deprecated::
|
|
148
|
+
此类预设了特定业务字段, 下游应自行继承 ``BasicAsyncBaseTable`` 定义所需字段.
|
|
149
|
+
"""
|
|
144
150
|
|
|
145
151
|
__abstract__ = True
|
|
146
152
|
|
|
153
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
154
|
+
super().__init_subclass__(**kwargs)
|
|
155
|
+
if not cls.__dict__.get("__abstract__", False):
|
|
156
|
+
warnings.warn(
|
|
157
|
+
f"{cls.__name__} 继承了已废弃的 StdAsyncBaseTable, 请改为直接继承 BasicAsyncBaseTable 并自行定义所需字段",
|
|
158
|
+
DeprecationWarning,
|
|
159
|
+
stacklevel=2,
|
|
160
|
+
)
|
|
161
|
+
|
|
147
162
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
148
163
|
|
|
149
164
|
create_datetime: Mapped[datetime.datetime] = mapped_column(
|
|
@@ -177,10 +192,23 @@ class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
|
|
|
177
192
|
|
|
178
193
|
|
|
179
194
|
class StdReadOnlyBasicAsyncBaseTable(ReadOnlyBasicAsyncBaseTable):
|
|
180
|
-
"""
|
|
195
|
+
"""标准只读异步表基类: 包含 id/时间戳/操作人等标准字段.
|
|
196
|
+
|
|
197
|
+
.. deprecated::
|
|
198
|
+
此类预设了特定业务字段, 下游应自行继承 ``ReadOnlyBasicAsyncBaseTable`` 定义所需字段.
|
|
199
|
+
"""
|
|
181
200
|
|
|
182
201
|
__abstract__ = True
|
|
183
202
|
|
|
203
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
204
|
+
super().__init_subclass__(**kwargs)
|
|
205
|
+
if not cls.__dict__.get("__abstract__", False):
|
|
206
|
+
warnings.warn(
|
|
207
|
+
f"{cls.__name__} 继承了已废弃的 StdReadOnlyBasicAsyncBaseTable, 请改为直接继承 ReadOnlyBasicAsyncBaseTable 并自行定义所需字段",
|
|
208
|
+
DeprecationWarning,
|
|
209
|
+
stacklevel=2,
|
|
210
|
+
)
|
|
211
|
+
|
|
184
212
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
185
213
|
|
|
186
214
|
create_datetime: Mapped[datetime.datetime] = mapped_column(
|
|
@@ -252,7 +280,7 @@ class AsyncRawReadDAL:
|
|
|
252
280
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
253
281
|
with_deleted: bool = False,
|
|
254
282
|
batch_size: int = 500,
|
|
255
|
-
) ->
|
|
283
|
+
) -> AsyncGenerator[AsyncSQLATableT, None]:
|
|
256
284
|
if not hasattr(table_class, "id") or not isinstance(getattr(table_class, "id", None), InstrumentedAttribute):
|
|
257
285
|
raise ValueError(f"表 {table_class.__name__} 必须有 id 字段才能使用迭代方法")
|
|
258
286
|
|
|
@@ -398,15 +426,15 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
398
426
|
return entity is not None
|
|
399
427
|
|
|
400
428
|
@classmethod
|
|
401
|
-
async def
|
|
429
|
+
async def _get_by_id_for_update_core(
|
|
402
430
|
cls,
|
|
403
431
|
session: AsyncSession,
|
|
404
432
|
entity_id: int,
|
|
405
433
|
*,
|
|
406
|
-
|
|
434
|
+
timeout: int | None = None,
|
|
407
435
|
) -> AsyncSQLATableT | None:
|
|
408
436
|
try:
|
|
409
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
437
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
410
438
|
stmt = (
|
|
411
439
|
sa.select(cls._Table)
|
|
412
440
|
.where(cls._Table.id == entity_id) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -421,19 +449,29 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
421
449
|
raise
|
|
422
450
|
|
|
423
451
|
@classmethod
|
|
424
|
-
async def
|
|
452
|
+
async def get_by_id_for_update(
|
|
425
453
|
cls,
|
|
426
454
|
session: AsyncSession,
|
|
427
|
-
|
|
455
|
+
entity_id: int,
|
|
428
456
|
*,
|
|
429
457
|
lock_wait_timeout: int | None = None,
|
|
458
|
+
) -> AsyncSQLATableT | None:
|
|
459
|
+
return await cls._get_by_id_for_update_core(session, entity_id, timeout=lock_wait_timeout)
|
|
460
|
+
|
|
461
|
+
@classmethod
|
|
462
|
+
async def _batch_get_for_update_core(
|
|
463
|
+
cls,
|
|
464
|
+
session: AsyncSession,
|
|
465
|
+
entity_ids: Iterable[int],
|
|
466
|
+
*,
|
|
467
|
+
timeout: int | None = None,
|
|
430
468
|
) -> list[AsyncSQLATableT]:
|
|
431
469
|
filtered_ids = filtered_in_sql_values(entity_ids, int)
|
|
432
470
|
if not filtered_ids:
|
|
433
471
|
return []
|
|
434
472
|
|
|
435
473
|
try:
|
|
436
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
474
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
437
475
|
stmt = (
|
|
438
476
|
sa.select(cls._Table)
|
|
439
477
|
.where(cls._Table.id.in_(filtered_ids)) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -448,15 +486,25 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
448
486
|
raise
|
|
449
487
|
|
|
450
488
|
@classmethod
|
|
451
|
-
async def
|
|
489
|
+
async def batch_get_for_update(
|
|
452
490
|
cls,
|
|
453
491
|
session: AsyncSession,
|
|
492
|
+
entity_ids: Iterable[int],
|
|
454
493
|
*,
|
|
455
|
-
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
456
494
|
lock_wait_timeout: int | None = None,
|
|
495
|
+
) -> list[AsyncSQLATableT]:
|
|
496
|
+
return await cls._batch_get_for_update_core(session, entity_ids, timeout=lock_wait_timeout)
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
async def _get_one_for_update_core(
|
|
500
|
+
cls,
|
|
501
|
+
session: AsyncSession,
|
|
502
|
+
*,
|
|
503
|
+
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
504
|
+
timeout: int | None = None,
|
|
457
505
|
) -> AsyncSQLATableT | None:
|
|
458
506
|
try:
|
|
459
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
507
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
460
508
|
stmt = sa.select(cls._Table).with_for_update()
|
|
461
509
|
|
|
462
510
|
for clause in where_clauses:
|
|
@@ -470,6 +518,16 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
470
518
|
raise DBRetryableError(f"{PESSIMISTIC_LOCK_ERROR_MSG_TRAIT}-条件锁等待超时: {error_msg}") from e
|
|
471
519
|
raise
|
|
472
520
|
|
|
521
|
+
@classmethod
|
|
522
|
+
async def get_one_for_update(
|
|
523
|
+
cls,
|
|
524
|
+
session: AsyncSession,
|
|
525
|
+
*,
|
|
526
|
+
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
527
|
+
lock_wait_timeout: int | None = None,
|
|
528
|
+
) -> AsyncSQLATableT | None:
|
|
529
|
+
return await cls._get_one_for_update_core(session, where_clauses=where_clauses, timeout=lock_wait_timeout)
|
|
530
|
+
|
|
473
531
|
@classmethod
|
|
474
532
|
async def iter_record_dtos(
|
|
475
533
|
cls,
|
|
@@ -478,7 +536,7 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
478
536
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
479
537
|
with_deleted: bool = False,
|
|
480
538
|
batch_size: int = 500,
|
|
481
|
-
) ->
|
|
539
|
+
) -> AsyncGenerator[DTOModelT, None]:
|
|
482
540
|
async for entity in cls._iter_records(
|
|
483
541
|
session,
|
|
484
542
|
cls._Table,
|
|
@@ -522,7 +580,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
522
580
|
) -> AsyncSQLATableT:
|
|
523
581
|
if session.info.get(READONLY_SESSION_FLAG):
|
|
524
582
|
raise TypeError("当前会话被标记为只读, 不允许执行写入操作")
|
|
525
|
-
entity = cu.
|
|
583
|
+
entity = cu.to_orm_model()
|
|
526
584
|
session.add(entity)
|
|
527
585
|
await session.flush()
|
|
528
586
|
if need_refresh:
|
|
@@ -568,10 +626,10 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
568
626
|
cls,
|
|
569
627
|
session: AsyncSession,
|
|
570
628
|
entity_id: int,
|
|
571
|
-
|
|
629
|
+
cu: CUModelT,
|
|
572
630
|
need_refresh: bool = True,
|
|
573
631
|
) -> DTOModelT | None:
|
|
574
|
-
entity = await cls.update_only_set_by_id(session, entity_id,
|
|
632
|
+
entity = await cls.update_only_set_by_id(session, entity_id, cu, need_refresh)
|
|
575
633
|
if entity:
|
|
576
634
|
return cls._DTO.model_validate(entity)
|
|
577
635
|
return None
|
|
@@ -586,7 +644,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
586
644
|
_ensure_strict_fields(provided_keys=provided_keys, allowed_names=allowed_names, strict=strict)
|
|
587
645
|
|
|
588
646
|
@classmethod
|
|
589
|
-
async def
|
|
647
|
+
async def _update_full_by_id_core(
|
|
590
648
|
cls,
|
|
591
649
|
session: AsyncSession,
|
|
592
650
|
entity_id: int,
|
|
@@ -620,7 +678,19 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
620
678
|
return entity
|
|
621
679
|
|
|
622
680
|
@classmethod
|
|
623
|
-
async def
|
|
681
|
+
async def update_full_by_id(
|
|
682
|
+
cls,
|
|
683
|
+
session: AsyncSession,
|
|
684
|
+
entity_id: int,
|
|
685
|
+
cu: CUModelT,
|
|
686
|
+
*,
|
|
687
|
+
need_refresh: bool = False,
|
|
688
|
+
strict_missing: bool = True,
|
|
689
|
+
) -> AsyncSQLATableT | None:
|
|
690
|
+
return await cls._update_full_by_id_core(session, entity_id, cu, need_refresh=need_refresh, strict_missing=strict_missing)
|
|
691
|
+
|
|
692
|
+
@classmethod
|
|
693
|
+
async def _update_partial_by_id_core(
|
|
624
694
|
cls,
|
|
625
695
|
session: AsyncSession,
|
|
626
696
|
entity_id: int,
|
|
@@ -687,6 +757,30 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
687
757
|
await session.refresh(entity)
|
|
688
758
|
return entity
|
|
689
759
|
|
|
760
|
+
@classmethod
|
|
761
|
+
async def update_partial_by_id(
|
|
762
|
+
cls,
|
|
763
|
+
session: AsyncSession,
|
|
764
|
+
entity_id: int,
|
|
765
|
+
cu: CUModelT,
|
|
766
|
+
*,
|
|
767
|
+
need_refresh: bool = False,
|
|
768
|
+
fields: set[InstrumentedAttribute[Any]] | set[sa.Column[Any]] | None = None,
|
|
769
|
+
none_policy: Literal["ignore", "allow", "forbid"] = "ignore",
|
|
770
|
+
none_policy_overrides: dict[InstrumentedAttribute[Any] | sa.Column[Any], Literal["ignore", "allow", "forbid"]] | None = None,
|
|
771
|
+
strict: bool = False,
|
|
772
|
+
) -> AsyncSQLATableT | None:
|
|
773
|
+
return await cls._update_partial_by_id_core(
|
|
774
|
+
session,
|
|
775
|
+
entity_id,
|
|
776
|
+
cu,
|
|
777
|
+
need_refresh=need_refresh,
|
|
778
|
+
fields=fields,
|
|
779
|
+
none_policy=none_policy,
|
|
780
|
+
none_policy_overrides=none_policy_overrides,
|
|
781
|
+
strict=strict,
|
|
782
|
+
)
|
|
783
|
+
|
|
690
784
|
@classmethod
|
|
691
785
|
async def delete_by_id(
|
|
692
786
|
cls,
|
|
@@ -711,7 +805,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
711
805
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
712
806
|
with_deleted: bool = False,
|
|
713
807
|
batch_size: int = 500,
|
|
714
|
-
) ->
|
|
808
|
+
) -> AsyncGenerator[AsyncSQLATableT, None]:
|
|
715
809
|
async for entity in cls._iter_records(
|
|
716
810
|
session,
|
|
717
811
|
cls._Table,
|
|
@@ -722,11 +816,11 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
722
816
|
yield entity
|
|
723
817
|
|
|
724
818
|
@classmethod
|
|
725
|
-
async def
|
|
819
|
+
async def _batch_update_by_conditions_core(
|
|
726
820
|
cls,
|
|
727
821
|
session: AsyncSession,
|
|
728
822
|
*,
|
|
729
|
-
|
|
823
|
+
conditions: list[ColumnExpressionArgument[bool]],
|
|
730
824
|
update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
|
|
731
825
|
updater_id: int | None = None,
|
|
732
826
|
) -> int:
|
|
@@ -751,13 +845,24 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
751
845
|
if hasattr(cls._Table, "update_operator_id") and updater_id is not None:
|
|
752
846
|
final_update_data["update_operator_id"] = updater_id
|
|
753
847
|
|
|
754
|
-
stmt = sa.update(cls._Table).where(*
|
|
848
|
+
stmt = sa.update(cls._Table).where(*conditions).values(**final_update_data)
|
|
755
849
|
|
|
756
850
|
result = await session.execute(stmt)
|
|
757
851
|
await session.flush()
|
|
758
852
|
|
|
759
853
|
return result.rowcount # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
|
|
760
854
|
|
|
855
|
+
@classmethod
|
|
856
|
+
async def batch_update_by_conditions(
|
|
857
|
+
cls,
|
|
858
|
+
session: AsyncSession,
|
|
859
|
+
*,
|
|
860
|
+
whereclause: list[ColumnExpressionArgument[bool]],
|
|
861
|
+
update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
|
|
862
|
+
updater_id: int | None = None,
|
|
863
|
+
) -> int:
|
|
864
|
+
return await cls._batch_update_by_conditions_core(session, conditions=whereclause, update_data=update_data, updater_id=updater_id)
|
|
865
|
+
|
|
761
866
|
@classmethod
|
|
762
867
|
async def batch_update_by_ids(
|
|
763
868
|
cls,
|
|
@@ -771,15 +876,15 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
771
876
|
if not filtered_ids:
|
|
772
877
|
return 0
|
|
773
878
|
_id_column = cls._Table.id # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType, reportUnknownMemberType]
|
|
774
|
-
return await cls.
|
|
879
|
+
return await cls._batch_update_by_conditions_core(
|
|
775
880
|
session,
|
|
776
|
-
|
|
881
|
+
conditions=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
|
|
777
882
|
update_data=update_data,
|
|
778
883
|
updater_id=updater_id,
|
|
779
884
|
)
|
|
780
885
|
|
|
781
886
|
@classmethod
|
|
782
|
-
async def
|
|
887
|
+
async def _update_only_set_with_optimistic_lock_core(
|
|
783
888
|
cls,
|
|
784
889
|
session: AsyncSession,
|
|
785
890
|
entity_id: int,
|
|
@@ -830,13 +935,45 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
830
935
|
|
|
831
936
|
raise DBRetryableError(f"{OPTIMISTIC_LOCK_ERROR_MSG_TRAIT}-版本号不匹配({entity_id=}, {expected_version=})")
|
|
832
937
|
|
|
938
|
+
@classmethod
|
|
939
|
+
async def update_only_set_with_optimistic_lock(
|
|
940
|
+
cls,
|
|
941
|
+
session: AsyncSession,
|
|
942
|
+
entity_id: int,
|
|
943
|
+
cu: CUModelT,
|
|
944
|
+
*,
|
|
945
|
+
expected_version: int,
|
|
946
|
+
need_refresh: bool = False,
|
|
947
|
+
version_field: str = "version",
|
|
948
|
+
) -> AsyncSQLATableT | None:
|
|
949
|
+
return await cls._update_only_set_with_optimistic_lock_core(
|
|
950
|
+
session,
|
|
951
|
+
entity_id,
|
|
952
|
+
cu,
|
|
953
|
+
expected_version=expected_version,
|
|
954
|
+
need_refresh=need_refresh,
|
|
955
|
+
version_field=version_field,
|
|
956
|
+
)
|
|
957
|
+
|
|
833
958
|
|
|
834
959
|
class AsyncXDALOp(AsyncRawReadDAL, AsyncRawDAL):
|
|
835
960
|
"""扩展数据访问操作类."""
|
|
836
961
|
|
|
837
962
|
|
|
838
963
|
class AsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, DTOModelT], AsyncWriteDAL[AsyncSQLATableT, DTOModelT, CUModelT]):
|
|
839
|
-
"""基础数据访问层.
|
|
964
|
+
"""基础数据访问层.
|
|
965
|
+
|
|
966
|
+
.. deprecated:: 0.3.0
|
|
967
|
+
V1 DAL 将在 1.0 移除, 请迁移至 ``AsyncBaseDALV2``.
|
|
968
|
+
"""
|
|
969
|
+
|
|
970
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
971
|
+
super().__init_subclass__(**kwargs)
|
|
972
|
+
warnings.warn(
|
|
973
|
+
f"{cls.__name__} 继承了 V1 AsyncBaseDAL, 建议迁移至 AsyncBaseDALV2",
|
|
974
|
+
DeprecationWarning,
|
|
975
|
+
stacklevel=2,
|
|
976
|
+
)
|
|
840
977
|
|
|
841
978
|
|
|
842
979
|
class ReadOnlyAsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, ReadOnlyDTOModelT]):
|