lush-sqlalchemyx 0.2.1__tar.gz → 0.3.0__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.0}/PKG-INFO +1 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/pyproject.toml +21 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/_compat.py +1 -1
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/__init__.py +72 -24
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_async.py +164 -28
- lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_async_v2.py +184 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_common.py +37 -19
- lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_pagination.py +90 -0
- lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_params.py +32 -0
- lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_repository.py +311 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/dal/_sync.py +164 -63
- lush_sqlalchemyx-0.3.0/src/lush_sqlalchemyx/base/dal/_sync_v2.py +183 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/flask/ext.py +2 -2
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/README.md +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/base/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/fastapi/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/fastapi/depends/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/integrations/flask/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/manager.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/mapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_manager.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_mapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/py.typed +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/same_impl_just_warn_wrapper.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/src/lush_sqlalchemyx/shortcuts/__init__.py +0 -0
- {lush_sqlalchemyx-0.2.1 → lush_sqlalchemyx-0.3.0}/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.0"
|
|
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,24 @@ 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"
|
|
@@ -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
|
|
@@ -140,10 +141,23 @@ class ReadOnlyBasicAsyncBaseTable(AsyncSqlATableBase, ReadOnlyMixin):
|
|
|
140
141
|
|
|
141
142
|
|
|
142
143
|
class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
|
|
143
|
-
"""
|
|
144
|
+
"""标准异步表类: 包含 id/时间戳/操作人/软删除等标准字段.
|
|
145
|
+
|
|
146
|
+
.. deprecated::
|
|
147
|
+
此类预设了特定业务字段, 下游应自行继承 ``BasicAsyncBaseTable`` 定义所需字段.
|
|
148
|
+
"""
|
|
144
149
|
|
|
145
150
|
__abstract__ = True
|
|
146
151
|
|
|
152
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
153
|
+
super().__init_subclass__(**kwargs)
|
|
154
|
+
if not cls.__dict__.get("__abstract__", False):
|
|
155
|
+
warnings.warn(
|
|
156
|
+
f"{cls.__name__} 继承了已废弃的 StdAsyncBaseTable, 请改为直接继承 BasicAsyncBaseTable 并自行定义所需字段",
|
|
157
|
+
DeprecationWarning,
|
|
158
|
+
stacklevel=2,
|
|
159
|
+
)
|
|
160
|
+
|
|
147
161
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
148
162
|
|
|
149
163
|
create_datetime: Mapped[datetime.datetime] = mapped_column(
|
|
@@ -177,10 +191,23 @@ class StdAsyncBaseTable(BasicAsyncBaseTable, SoftDeleteTableMixin):
|
|
|
177
191
|
|
|
178
192
|
|
|
179
193
|
class StdReadOnlyBasicAsyncBaseTable(ReadOnlyBasicAsyncBaseTable):
|
|
180
|
-
"""
|
|
194
|
+
"""标准只读异步表基类: 包含 id/时间戳/操作人等标准字段.
|
|
195
|
+
|
|
196
|
+
.. deprecated::
|
|
197
|
+
此类预设了特定业务字段, 下游应自行继承 ``ReadOnlyBasicAsyncBaseTable`` 定义所需字段.
|
|
198
|
+
"""
|
|
181
199
|
|
|
182
200
|
__abstract__ = True
|
|
183
201
|
|
|
202
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
203
|
+
super().__init_subclass__(**kwargs)
|
|
204
|
+
if not cls.__dict__.get("__abstract__", False):
|
|
205
|
+
warnings.warn(
|
|
206
|
+
f"{cls.__name__} 继承了已废弃的 StdReadOnlyBasicAsyncBaseTable, 请改为直接继承 ReadOnlyBasicAsyncBaseTable 并自行定义所需字段",
|
|
207
|
+
DeprecationWarning,
|
|
208
|
+
stacklevel=2,
|
|
209
|
+
)
|
|
210
|
+
|
|
184
211
|
id: Mapped[int] = mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
185
212
|
|
|
186
213
|
create_datetime: Mapped[datetime.datetime] = mapped_column(
|
|
@@ -252,7 +279,7 @@ class AsyncRawReadDAL:
|
|
|
252
279
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
253
280
|
with_deleted: bool = False,
|
|
254
281
|
batch_size: int = 500,
|
|
255
|
-
) ->
|
|
282
|
+
) -> AsyncGenerator[AsyncSQLATableT, None]:
|
|
256
283
|
if not hasattr(table_class, "id") or not isinstance(getattr(table_class, "id", None), InstrumentedAttribute):
|
|
257
284
|
raise ValueError(f"表 {table_class.__name__} 必须有 id 字段才能使用迭代方法")
|
|
258
285
|
|
|
@@ -398,15 +425,15 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
398
425
|
return entity is not None
|
|
399
426
|
|
|
400
427
|
@classmethod
|
|
401
|
-
async def
|
|
428
|
+
async def _get_by_id_for_update_core(
|
|
402
429
|
cls,
|
|
403
430
|
session: AsyncSession,
|
|
404
431
|
entity_id: int,
|
|
405
432
|
*,
|
|
406
|
-
|
|
433
|
+
timeout: int | None = None,
|
|
407
434
|
) -> AsyncSQLATableT | None:
|
|
408
435
|
try:
|
|
409
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
436
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
410
437
|
stmt = (
|
|
411
438
|
sa.select(cls._Table)
|
|
412
439
|
.where(cls._Table.id == entity_id) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -421,19 +448,29 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
421
448
|
raise
|
|
422
449
|
|
|
423
450
|
@classmethod
|
|
424
|
-
async def
|
|
451
|
+
async def get_by_id_for_update(
|
|
425
452
|
cls,
|
|
426
453
|
session: AsyncSession,
|
|
427
|
-
|
|
454
|
+
entity_id: int,
|
|
428
455
|
*,
|
|
429
456
|
lock_wait_timeout: int | None = None,
|
|
457
|
+
) -> AsyncSQLATableT | None:
|
|
458
|
+
return await cls._get_by_id_for_update_core(session, entity_id, timeout=lock_wait_timeout)
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
async def _batch_get_for_update_core(
|
|
462
|
+
cls,
|
|
463
|
+
session: AsyncSession,
|
|
464
|
+
entity_ids: Iterable[int],
|
|
465
|
+
*,
|
|
466
|
+
timeout: int | None = None,
|
|
430
467
|
) -> list[AsyncSQLATableT]:
|
|
431
468
|
filtered_ids = filtered_in_sql_values(entity_ids, int)
|
|
432
469
|
if not filtered_ids:
|
|
433
470
|
return []
|
|
434
471
|
|
|
435
472
|
try:
|
|
436
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
473
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
437
474
|
stmt = (
|
|
438
475
|
sa.select(cls._Table)
|
|
439
476
|
.where(cls._Table.id.in_(filtered_ids)) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -448,15 +485,25 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
448
485
|
raise
|
|
449
486
|
|
|
450
487
|
@classmethod
|
|
451
|
-
async def
|
|
488
|
+
async def batch_get_for_update(
|
|
452
489
|
cls,
|
|
453
490
|
session: AsyncSession,
|
|
491
|
+
entity_ids: Iterable[int],
|
|
454
492
|
*,
|
|
455
|
-
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
456
493
|
lock_wait_timeout: int | None = None,
|
|
494
|
+
) -> list[AsyncSQLATableT]:
|
|
495
|
+
return await cls._batch_get_for_update_core(session, entity_ids, timeout=lock_wait_timeout)
|
|
496
|
+
|
|
497
|
+
@classmethod
|
|
498
|
+
async def _get_one_for_update_core(
|
|
499
|
+
cls,
|
|
500
|
+
session: AsyncSession,
|
|
501
|
+
*,
|
|
502
|
+
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
503
|
+
timeout: int | None = None,
|
|
457
504
|
) -> AsyncSQLATableT | None:
|
|
458
505
|
try:
|
|
459
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
506
|
+
async with async_temp_set_lock_wait_timeout(session, timeout):
|
|
460
507
|
stmt = sa.select(cls._Table).with_for_update()
|
|
461
508
|
|
|
462
509
|
for clause in where_clauses:
|
|
@@ -470,6 +517,16 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
470
517
|
raise DBRetryableError(f"{PESSIMISTIC_LOCK_ERROR_MSG_TRAIT}-条件锁等待超时: {error_msg}") from e
|
|
471
518
|
raise
|
|
472
519
|
|
|
520
|
+
@classmethod
|
|
521
|
+
async def get_one_for_update(
|
|
522
|
+
cls,
|
|
523
|
+
session: AsyncSession,
|
|
524
|
+
*,
|
|
525
|
+
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
526
|
+
lock_wait_timeout: int | None = None,
|
|
527
|
+
) -> AsyncSQLATableT | None:
|
|
528
|
+
return await cls._get_one_for_update_core(session, where_clauses=where_clauses, timeout=lock_wait_timeout)
|
|
529
|
+
|
|
473
530
|
@classmethod
|
|
474
531
|
async def iter_record_dtos(
|
|
475
532
|
cls,
|
|
@@ -478,7 +535,7 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
478
535
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
479
536
|
with_deleted: bool = False,
|
|
480
537
|
batch_size: int = 500,
|
|
481
|
-
) ->
|
|
538
|
+
) -> AsyncGenerator[DTOModelT, None]:
|
|
482
539
|
async for entity in cls._iter_records(
|
|
483
540
|
session,
|
|
484
541
|
cls._Table,
|
|
@@ -522,7 +579,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
522
579
|
) -> AsyncSQLATableT:
|
|
523
580
|
if session.info.get(READONLY_SESSION_FLAG):
|
|
524
581
|
raise TypeError("当前会话被标记为只读, 不允许执行写入操作")
|
|
525
|
-
entity = cu.
|
|
582
|
+
entity = cu.to_orm_model()
|
|
526
583
|
session.add(entity)
|
|
527
584
|
await session.flush()
|
|
528
585
|
if need_refresh:
|
|
@@ -568,10 +625,10 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
568
625
|
cls,
|
|
569
626
|
session: AsyncSession,
|
|
570
627
|
entity_id: int,
|
|
571
|
-
|
|
628
|
+
cu: CUModelT,
|
|
572
629
|
need_refresh: bool = True,
|
|
573
630
|
) -> DTOModelT | None:
|
|
574
|
-
entity = await cls.update_only_set_by_id(session, entity_id,
|
|
631
|
+
entity = await cls.update_only_set_by_id(session, entity_id, cu, need_refresh)
|
|
575
632
|
if entity:
|
|
576
633
|
return cls._DTO.model_validate(entity)
|
|
577
634
|
return None
|
|
@@ -586,7 +643,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
586
643
|
_ensure_strict_fields(provided_keys=provided_keys, allowed_names=allowed_names, strict=strict)
|
|
587
644
|
|
|
588
645
|
@classmethod
|
|
589
|
-
async def
|
|
646
|
+
async def _update_full_by_id_core(
|
|
590
647
|
cls,
|
|
591
648
|
session: AsyncSession,
|
|
592
649
|
entity_id: int,
|
|
@@ -620,7 +677,19 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
620
677
|
return entity
|
|
621
678
|
|
|
622
679
|
@classmethod
|
|
623
|
-
async def
|
|
680
|
+
async def update_full_by_id(
|
|
681
|
+
cls,
|
|
682
|
+
session: AsyncSession,
|
|
683
|
+
entity_id: int,
|
|
684
|
+
cu: CUModelT,
|
|
685
|
+
*,
|
|
686
|
+
need_refresh: bool = False,
|
|
687
|
+
strict_missing: bool = True,
|
|
688
|
+
) -> AsyncSQLATableT | None:
|
|
689
|
+
return await cls._update_full_by_id_core(session, entity_id, cu, need_refresh=need_refresh, strict_missing=strict_missing)
|
|
690
|
+
|
|
691
|
+
@classmethod
|
|
692
|
+
async def _update_partial_by_id_core(
|
|
624
693
|
cls,
|
|
625
694
|
session: AsyncSession,
|
|
626
695
|
entity_id: int,
|
|
@@ -687,6 +756,30 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
687
756
|
await session.refresh(entity)
|
|
688
757
|
return entity
|
|
689
758
|
|
|
759
|
+
@classmethod
|
|
760
|
+
async def update_partial_by_id(
|
|
761
|
+
cls,
|
|
762
|
+
session: AsyncSession,
|
|
763
|
+
entity_id: int,
|
|
764
|
+
cu: CUModelT,
|
|
765
|
+
*,
|
|
766
|
+
need_refresh: bool = False,
|
|
767
|
+
fields: set[InstrumentedAttribute[Any]] | set[sa.Column[Any]] | None = None,
|
|
768
|
+
none_policy: Literal["ignore", "allow", "forbid"] = "ignore",
|
|
769
|
+
none_policy_overrides: dict[InstrumentedAttribute[Any] | sa.Column[Any], Literal["ignore", "allow", "forbid"]] | None = None,
|
|
770
|
+
strict: bool = False,
|
|
771
|
+
) -> AsyncSQLATableT | None:
|
|
772
|
+
return await cls._update_partial_by_id_core(
|
|
773
|
+
session,
|
|
774
|
+
entity_id,
|
|
775
|
+
cu,
|
|
776
|
+
need_refresh=need_refresh,
|
|
777
|
+
fields=fields,
|
|
778
|
+
none_policy=none_policy,
|
|
779
|
+
none_policy_overrides=none_policy_overrides,
|
|
780
|
+
strict=strict,
|
|
781
|
+
)
|
|
782
|
+
|
|
690
783
|
@classmethod
|
|
691
784
|
async def delete_by_id(
|
|
692
785
|
cls,
|
|
@@ -711,7 +804,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
711
804
|
where_clauses: list[ColumnExpressionArgument[bool]] | None = None,
|
|
712
805
|
with_deleted: bool = False,
|
|
713
806
|
batch_size: int = 500,
|
|
714
|
-
) ->
|
|
807
|
+
) -> AsyncGenerator[AsyncSQLATableT, None]:
|
|
715
808
|
async for entity in cls._iter_records(
|
|
716
809
|
session,
|
|
717
810
|
cls._Table,
|
|
@@ -722,11 +815,11 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
722
815
|
yield entity
|
|
723
816
|
|
|
724
817
|
@classmethod
|
|
725
|
-
async def
|
|
818
|
+
async def _batch_update_by_conditions_core(
|
|
726
819
|
cls,
|
|
727
820
|
session: AsyncSession,
|
|
728
821
|
*,
|
|
729
|
-
|
|
822
|
+
conditions: list[ColumnExpressionArgument[bool]],
|
|
730
823
|
update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
|
|
731
824
|
updater_id: int | None = None,
|
|
732
825
|
) -> int:
|
|
@@ -751,13 +844,24 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
751
844
|
if hasattr(cls._Table, "update_operator_id") and updater_id is not None:
|
|
752
845
|
final_update_data["update_operator_id"] = updater_id
|
|
753
846
|
|
|
754
|
-
stmt = sa.update(cls._Table).where(*
|
|
847
|
+
stmt = sa.update(cls._Table).where(*conditions).values(**final_update_data)
|
|
755
848
|
|
|
756
849
|
result = await session.execute(stmt)
|
|
757
850
|
await session.flush()
|
|
758
851
|
|
|
759
852
|
return result.rowcount # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
|
|
760
853
|
|
|
854
|
+
@classmethod
|
|
855
|
+
async def batch_update_by_conditions(
|
|
856
|
+
cls,
|
|
857
|
+
session: AsyncSession,
|
|
858
|
+
*,
|
|
859
|
+
whereclause: list[ColumnExpressionArgument[bool]],
|
|
860
|
+
update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
|
|
861
|
+
updater_id: int | None = None,
|
|
862
|
+
) -> int:
|
|
863
|
+
return await cls._batch_update_by_conditions_core(session, conditions=whereclause, update_data=update_data, updater_id=updater_id)
|
|
864
|
+
|
|
761
865
|
@classmethod
|
|
762
866
|
async def batch_update_by_ids(
|
|
763
867
|
cls,
|
|
@@ -771,15 +875,15 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
771
875
|
if not filtered_ids:
|
|
772
876
|
return 0
|
|
773
877
|
_id_column = cls._Table.id # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType, reportUnknownMemberType]
|
|
774
|
-
return await cls.
|
|
878
|
+
return await cls._batch_update_by_conditions_core(
|
|
775
879
|
session,
|
|
776
|
-
|
|
880
|
+
conditions=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
|
|
777
881
|
update_data=update_data,
|
|
778
882
|
updater_id=updater_id,
|
|
779
883
|
)
|
|
780
884
|
|
|
781
885
|
@classmethod
|
|
782
|
-
async def
|
|
886
|
+
async def _update_only_set_with_optimistic_lock_core(
|
|
783
887
|
cls,
|
|
784
888
|
session: AsyncSession,
|
|
785
889
|
entity_id: int,
|
|
@@ -830,13 +934,45 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
830
934
|
|
|
831
935
|
raise DBRetryableError(f"{OPTIMISTIC_LOCK_ERROR_MSG_TRAIT}-版本号不匹配({entity_id=}, {expected_version=})")
|
|
832
936
|
|
|
937
|
+
@classmethod
|
|
938
|
+
async def update_only_set_with_optimistic_lock(
|
|
939
|
+
cls,
|
|
940
|
+
session: AsyncSession,
|
|
941
|
+
entity_id: int,
|
|
942
|
+
cu: CUModelT,
|
|
943
|
+
*,
|
|
944
|
+
expected_version: int,
|
|
945
|
+
need_refresh: bool = False,
|
|
946
|
+
version_field: str = "version",
|
|
947
|
+
) -> AsyncSQLATableT | None:
|
|
948
|
+
return await cls._update_only_set_with_optimistic_lock_core(
|
|
949
|
+
session,
|
|
950
|
+
entity_id,
|
|
951
|
+
cu,
|
|
952
|
+
expected_version=expected_version,
|
|
953
|
+
need_refresh=need_refresh,
|
|
954
|
+
version_field=version_field,
|
|
955
|
+
)
|
|
956
|
+
|
|
833
957
|
|
|
834
958
|
class AsyncXDALOp(AsyncRawReadDAL, AsyncRawDAL):
|
|
835
959
|
"""扩展数据访问操作类."""
|
|
836
960
|
|
|
837
961
|
|
|
838
962
|
class AsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, DTOModelT], AsyncWriteDAL[AsyncSQLATableT, DTOModelT, CUModelT]):
|
|
839
|
-
"""基础数据访问层.
|
|
963
|
+
"""基础数据访问层.
|
|
964
|
+
|
|
965
|
+
.. deprecated:: 0.3.0
|
|
966
|
+
V1 DAL 将在 1.0 移除, 请迁移至 ``AsyncBaseDALV2``.
|
|
967
|
+
"""
|
|
968
|
+
|
|
969
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
970
|
+
super().__init_subclass__(**kwargs)
|
|
971
|
+
warnings.warn(
|
|
972
|
+
f"{cls.__name__} 继承了 V1 AsyncBaseDAL, 建议迁移至 AsyncBaseDALV2",
|
|
973
|
+
DeprecationWarning,
|
|
974
|
+
stacklevel=2,
|
|
975
|
+
)
|
|
840
976
|
|
|
841
977
|
|
|
842
978
|
class ReadOnlyAsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, ReadOnlyDTOModelT]):
|