lush-sqlalchemyx 0.3.1__tar.gz → 0.4.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.3.1 → lush_sqlalchemyx-0.4.0}/PKG-INFO +1 -1
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/pyproject.toml +1 -4
- lush_sqlalchemyx-0.4.0/src/lush_sqlalchemyx/__init__.py +19 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/__init__.py +11 -33
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_async.py +41 -132
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_common.py +108 -8
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_pagination.py +2 -2
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_repository.py +36 -24
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_sync.py +41 -132
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_async_v2.py +0 -184
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_params.py +0 -32
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base/dal/_sync_v2.py +0 -183
- lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/shortcuts/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/README.md +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/_compat.py +0 -0
- {lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx → lush_sqlalchemyx-0.4.0/src/lush_sqlalchemyx/base}/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/base → lush_sqlalchemyx-0.4.0/src/lush_sqlalchemyx/integrations}/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/integrations/fastapi/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/integrations/fastapi/depends/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/integrations/flask/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/integrations/flask/ext.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/mysql/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/mysql/manager.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/mysql/mapper.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_manager.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/mgrs/mysql/sync_mapper.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/py.typed +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/same_impl_just_warn_wrapper.py +0 -0
- {lush_sqlalchemyx-0.3.1/src/lush_sqlalchemyx/integrations → lush_sqlalchemyx-0.4.0/src/lush_sqlalchemyx/shortcuts}/__init__.py +0 -0
- {lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.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.4.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"
|
|
@@ -149,9 +149,6 @@ ignore = [
|
|
|
149
149
|
]
|
|
150
150
|
|
|
151
151
|
[tool.ruff.lint.per-file-ignores]
|
|
152
|
-
"**/dal/_*_v2.py" = [
|
|
153
|
-
"ARG003", # extra 参数是 ABC 合规所必需但在部分方法中未使用
|
|
154
|
-
]
|
|
155
152
|
"tests/**/*.py" = [
|
|
156
153
|
# === 测试代码质量 ===
|
|
157
154
|
"B011", # assert False应改为raise AssertionError
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""lush-sqlalchemyx — SQLAlchemy DAL 实现 + async/sync MySQL manager + 框架集成.
|
|
2
|
+
|
|
3
|
+
导入本包时会自动注册软删除 Session 事件监听器.
|
|
4
|
+
如需显式管理钩子生命周期, 参见 :func:`register_soft_delete_hooks` 等.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lush_sqlalchemyx.base.dal import (
|
|
8
|
+
is_soft_delete_hooks_registered,
|
|
9
|
+
register_soft_delete_hooks,
|
|
10
|
+
setup_dal_hooks,
|
|
11
|
+
unregister_soft_delete_hooks,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"is_soft_delete_hooks_registered",
|
|
16
|
+
"register_soft_delete_hooks",
|
|
17
|
+
"setup_dal_hooks",
|
|
18
|
+
"unregister_soft_delete_hooks",
|
|
19
|
+
)
|
|
@@ -6,21 +6,14 @@
|
|
|
6
6
|
# --- shared (sync/async agnostic) ---
|
|
7
7
|
# --- lush-dal-protocol ABCs (ORM 无关的抽象层) ---
|
|
8
8
|
from lush_dal_protocol import (
|
|
9
|
-
AbstractAsyncAdvancedWriteDAL,
|
|
10
9
|
AbstractAsyncBaseDAL,
|
|
11
|
-
AbstractAsyncBatchFieldDAL,
|
|
12
|
-
AbstractAsyncLockDAL,
|
|
13
|
-
AbstractAsyncRawSQLDAL,
|
|
14
10
|
AbstractAsyncReadDAL,
|
|
15
11
|
AbstractAsyncWriteDAL,
|
|
16
|
-
AbstractSyncAdvancedWriteDAL,
|
|
17
12
|
AbstractSyncBaseDAL,
|
|
18
|
-
AbstractSyncBatchFieldDAL,
|
|
19
|
-
AbstractSyncLockDAL,
|
|
20
|
-
AbstractSyncRawSQLDAL,
|
|
21
13
|
AbstractSyncReadDAL,
|
|
22
14
|
AbstractSyncWriteDAL,
|
|
23
15
|
)
|
|
16
|
+
from lush_dal_protocol.params.pagination import CursorPagination, CursorResult, OffsetPagination, PageResult
|
|
24
17
|
|
|
25
18
|
# --- async (requires sqlalchemy[asyncio]) ---
|
|
26
19
|
from ._async import (
|
|
@@ -40,9 +33,6 @@ from ._async import (
|
|
|
40
33
|
async_temp_set_lock_wait_timeout,
|
|
41
34
|
async_with_retry,
|
|
42
35
|
)
|
|
43
|
-
|
|
44
|
-
# --- V2 (ABC-compliant, options-based) ---
|
|
45
|
-
from ._async_v2 import AsyncBaseDALV2, AsyncReadDALV2, AsyncWriteDALV2
|
|
46
36
|
from ._common import (
|
|
47
37
|
DEFAULT_RETRY_CONFIG,
|
|
48
38
|
OPTIMISTIC_LOCK_ERROR_MSG_TRAIT,
|
|
@@ -53,6 +43,7 @@ from ._common import (
|
|
|
53
43
|
CUModelT,
|
|
54
44
|
DBRetryableError,
|
|
55
45
|
DTOModelT,
|
|
46
|
+
FieldIsDeleteSoftDeleteTableMixin,
|
|
56
47
|
FieldMixin,
|
|
57
48
|
ReadOnlyMixin,
|
|
58
49
|
RetryConfig,
|
|
@@ -62,16 +53,16 @@ from ._common import (
|
|
|
62
53
|
StdBaseDTO,
|
|
63
54
|
escape_like,
|
|
64
55
|
filtered_in_sql_values,
|
|
56
|
+
is_soft_delete_hooks_registered,
|
|
57
|
+
register_soft_delete_hooks,
|
|
58
|
+
setup_dal_hooks,
|
|
59
|
+
unregister_soft_delete_hooks,
|
|
65
60
|
)
|
|
66
61
|
|
|
67
62
|
# Event listener references — accessed by tests via getattr(module, name).
|
|
68
63
|
from ._common import __prevent_readonly_write as __prevent_readonly_write # pyright: ignore[reportPrivateUsage]
|
|
69
64
|
from ._common import __receive_before_flush as __receive_before_flush # pyright: ignore[reportPrivateUsage]
|
|
70
65
|
from ._pagination import (
|
|
71
|
-
CursorPagination,
|
|
72
|
-
CursorResult,
|
|
73
|
-
OffsetPagination,
|
|
74
|
-
PageResult,
|
|
75
66
|
build_cursor_stmt,
|
|
76
67
|
build_offset_stmt,
|
|
77
68
|
decode_cursor,
|
|
@@ -79,7 +70,6 @@ from ._pagination import (
|
|
|
79
70
|
make_cursor_result,
|
|
80
71
|
make_page_result,
|
|
81
72
|
)
|
|
82
|
-
from ._params import SQLAExtra
|
|
83
73
|
from ._repository import AsyncSQLAlchemyRepository, SyncSQLAlchemyRepository
|
|
84
74
|
|
|
85
75
|
# --- sync ---
|
|
@@ -100,7 +90,6 @@ from ._sync import (
|
|
|
100
90
|
sync_temp_set_lock_wait_timeout,
|
|
101
91
|
sync_with_retry,
|
|
102
92
|
)
|
|
103
|
-
from ._sync_v2 import SyncBaseDALV2, SyncReadDALV2, SyncWriteDALV2
|
|
104
93
|
|
|
105
94
|
__all__ = (
|
|
106
95
|
# common
|
|
@@ -118,10 +107,15 @@ __all__ = (
|
|
|
118
107
|
"RetryConfig",
|
|
119
108
|
"SQLATableT",
|
|
120
109
|
"SoftDeleteTableMixin",
|
|
110
|
+
"FieldIsDeleteSoftDeleteTableMixin",
|
|
121
111
|
"StdBaseCU",
|
|
122
112
|
"StdBaseDTO",
|
|
123
113
|
"escape_like",
|
|
124
114
|
"filtered_in_sql_values",
|
|
115
|
+
"is_soft_delete_hooks_registered",
|
|
116
|
+
"register_soft_delete_hooks",
|
|
117
|
+
"setup_dal_hooks",
|
|
118
|
+
"unregister_soft_delete_hooks",
|
|
125
119
|
# async
|
|
126
120
|
"AsyncBaseDAL",
|
|
127
121
|
"AsyncRawDAL",
|
|
@@ -165,30 +159,14 @@ __all__ = (
|
|
|
165
159
|
"encode_cursor",
|
|
166
160
|
"make_cursor_result",
|
|
167
161
|
"make_page_result",
|
|
168
|
-
# V2
|
|
169
|
-
"AsyncBaseDALV2",
|
|
170
|
-
"AsyncReadDALV2",
|
|
171
|
-
"AsyncWriteDALV2",
|
|
172
|
-
"SQLAExtra",
|
|
173
|
-
"SyncBaseDALV2",
|
|
174
|
-
"SyncReadDALV2",
|
|
175
|
-
"SyncWriteDALV2",
|
|
176
162
|
# repository
|
|
177
163
|
"AsyncSQLAlchemyRepository",
|
|
178
164
|
"SyncSQLAlchemyRepository",
|
|
179
165
|
# lush-dal-protocol ABCs
|
|
180
|
-
"AbstractAsyncAdvancedWriteDAL",
|
|
181
166
|
"AbstractAsyncBaseDAL",
|
|
182
|
-
"AbstractAsyncBatchFieldDAL",
|
|
183
|
-
"AbstractAsyncLockDAL",
|
|
184
|
-
"AbstractAsyncRawSQLDAL",
|
|
185
167
|
"AbstractAsyncReadDAL",
|
|
186
168
|
"AbstractAsyncWriteDAL",
|
|
187
|
-
"AbstractSyncAdvancedWriteDAL",
|
|
188
169
|
"AbstractSyncBaseDAL",
|
|
189
|
-
"AbstractSyncBatchFieldDAL",
|
|
190
|
-
"AbstractSyncLockDAL",
|
|
191
|
-
"AbstractSyncRawSQLDAL",
|
|
192
170
|
"AbstractSyncReadDAL",
|
|
193
171
|
"AbstractSyncWriteDAL",
|
|
194
172
|
)
|
|
@@ -15,6 +15,7 @@ from contextlib import asynccontextmanager, suppress
|
|
|
15
15
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, TypeVar, cast
|
|
16
16
|
|
|
17
17
|
import sqlalchemy as sa
|
|
18
|
+
from lush_dal_protocol.abc import AbstractAsyncReadDAL, AbstractAsyncWriteDAL
|
|
18
19
|
from pydantic import BaseModel
|
|
19
20
|
from sqlalchemy import ColumnExpressionArgument
|
|
20
21
|
from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncSession
|
|
@@ -30,6 +31,7 @@ from ._common import (
|
|
|
30
31
|
CUModelT,
|
|
31
32
|
DBRetryableError,
|
|
32
33
|
DTOModelT,
|
|
34
|
+
FieldIsDeleteSoftDeleteTableMixin,
|
|
33
35
|
ReadOnlyMixin,
|
|
34
36
|
RetryConfig,
|
|
35
37
|
SoftDeleteTableMixin,
|
|
@@ -141,7 +143,7 @@ class ReadOnlyBasicAsyncBaseTable(AsyncSqlATableBase, ReadOnlyMixin):
|
|
|
141
143
|
__abstract__ = True
|
|
142
144
|
|
|
143
145
|
|
|
144
|
-
class StdAsyncBaseTable(BasicAsyncBaseTable,
|
|
146
|
+
class StdAsyncBaseTable(BasicAsyncBaseTable, FieldIsDeleteSoftDeleteTableMixin):
|
|
145
147
|
"""标准异步表类: 包含 id/时间戳/操作人/软删除等标准字段.
|
|
146
148
|
|
|
147
149
|
.. deprecated::
|
|
@@ -316,7 +318,11 @@ class AsyncRawReadDAL:
|
|
|
316
318
|
last_id = getattr(batch[-1], id_attr.key)
|
|
317
319
|
|
|
318
320
|
|
|
319
|
-
class AsyncReadDAL(
|
|
321
|
+
class AsyncReadDAL(
|
|
322
|
+
AsyncRawReadDAL,
|
|
323
|
+
AbstractAsyncReadDAL[AsyncSession, AsyncSQLATableT, DTOModelT, int],
|
|
324
|
+
Generic[AsyncSQLATableT, DTOModelT],
|
|
325
|
+
):
|
|
320
326
|
"""抽象只读数据访问层基类."""
|
|
321
327
|
|
|
322
328
|
_Table: ClassVar[type[AsyncSQLATableT]] # pyright: ignore[reportGeneralTypeIssues]
|
|
@@ -328,7 +334,10 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
328
334
|
session: AsyncSession,
|
|
329
335
|
entity_id: int,
|
|
330
336
|
) -> AsyncSQLATableT | None:
|
|
331
|
-
|
|
337
|
+
entity = await session.get(cls._Table, entity_id)
|
|
338
|
+
if entity is not None and isinstance(entity, SoftDeleteTableMixin) and entity.is_soft_deleted:
|
|
339
|
+
return None
|
|
340
|
+
return entity
|
|
332
341
|
|
|
333
342
|
@classmethod
|
|
334
343
|
async def batch_get_field__entity(
|
|
@@ -402,6 +411,8 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
402
411
|
) -> DTOModelT | None:
|
|
403
412
|
entity = await session.get(cls._Table, entity_id)
|
|
404
413
|
if entity:
|
|
414
|
+
if isinstance(entity, SoftDeleteTableMixin) and entity.is_soft_deleted:
|
|
415
|
+
return None
|
|
405
416
|
if need_refresh:
|
|
406
417
|
await session.refresh(entity)
|
|
407
418
|
return cls._DTO.model_validate(entity)
|
|
@@ -423,18 +434,20 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
423
434
|
@classmethod
|
|
424
435
|
async def exists(cls, session: AsyncSession, entity_id: int) -> bool:
|
|
425
436
|
entity = await session.get(cls._Table, entity_id)
|
|
426
|
-
|
|
437
|
+
if entity is None:
|
|
438
|
+
return False
|
|
439
|
+
return not (isinstance(entity, SoftDeleteTableMixin) and entity.is_soft_deleted)
|
|
427
440
|
|
|
428
441
|
@classmethod
|
|
429
|
-
async def
|
|
442
|
+
async def get_by_id_for_update(
|
|
430
443
|
cls,
|
|
431
444
|
session: AsyncSession,
|
|
432
445
|
entity_id: int,
|
|
433
446
|
*,
|
|
434
|
-
|
|
447
|
+
lock_wait_timeout: int | None = None,
|
|
435
448
|
) -> AsyncSQLATableT | None:
|
|
436
449
|
try:
|
|
437
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
450
|
+
async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
|
|
438
451
|
stmt = (
|
|
439
452
|
sa.select(cls._Table)
|
|
440
453
|
.where(cls._Table.id == entity_id) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -449,29 +462,19 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
449
462
|
raise
|
|
450
463
|
|
|
451
464
|
@classmethod
|
|
452
|
-
async def
|
|
453
|
-
cls,
|
|
454
|
-
session: AsyncSession,
|
|
455
|
-
entity_id: int,
|
|
456
|
-
*,
|
|
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(
|
|
465
|
+
async def batch_get_for_update(
|
|
463
466
|
cls,
|
|
464
467
|
session: AsyncSession,
|
|
465
468
|
entity_ids: Iterable[int],
|
|
466
469
|
*,
|
|
467
|
-
|
|
470
|
+
lock_wait_timeout: int | None = None,
|
|
468
471
|
) -> list[AsyncSQLATableT]:
|
|
469
472
|
filtered_ids = filtered_in_sql_values(entity_ids, int)
|
|
470
473
|
if not filtered_ids:
|
|
471
474
|
return []
|
|
472
475
|
|
|
473
476
|
try:
|
|
474
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
477
|
+
async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
|
|
475
478
|
stmt = (
|
|
476
479
|
sa.select(cls._Table)
|
|
477
480
|
.where(cls._Table.id.in_(filtered_ids)) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownArgumentType]
|
|
@@ -486,25 +489,15 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
486
489
|
raise
|
|
487
490
|
|
|
488
491
|
@classmethod
|
|
489
|
-
async def
|
|
490
|
-
cls,
|
|
491
|
-
session: AsyncSession,
|
|
492
|
-
entity_ids: Iterable[int],
|
|
493
|
-
*,
|
|
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(
|
|
492
|
+
async def get_one_for_update(
|
|
500
493
|
cls,
|
|
501
494
|
session: AsyncSession,
|
|
502
495
|
*,
|
|
503
496
|
where_clauses: list[ColumnExpressionArgument[bool]],
|
|
504
|
-
|
|
497
|
+
lock_wait_timeout: int | None = None,
|
|
505
498
|
) -> AsyncSQLATableT | None:
|
|
506
499
|
try:
|
|
507
|
-
async with async_temp_set_lock_wait_timeout(session,
|
|
500
|
+
async with async_temp_set_lock_wait_timeout(session, lock_wait_timeout):
|
|
508
501
|
stmt = sa.select(cls._Table).with_for_update()
|
|
509
502
|
|
|
510
503
|
for clause in where_clauses:
|
|
@@ -518,16 +511,6 @@ class AsyncReadDAL(AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOModelT]):
|
|
|
518
511
|
raise DBRetryableError(f"{PESSIMISTIC_LOCK_ERROR_MSG_TRAIT}-条件锁等待超时: {error_msg}") from e
|
|
519
512
|
raise
|
|
520
513
|
|
|
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
|
-
|
|
531
514
|
@classmethod
|
|
532
515
|
async def iter_record_dtos(
|
|
533
516
|
cls,
|
|
@@ -564,7 +547,12 @@ class AsyncRawDAL:
|
|
|
564
547
|
return await session.execute(stmt, params)
|
|
565
548
|
|
|
566
549
|
|
|
567
|
-
class AsyncWriteDAL(
|
|
550
|
+
class AsyncWriteDAL(
|
|
551
|
+
AsyncRawDAL,
|
|
552
|
+
AsyncRawReadDAL,
|
|
553
|
+
AbstractAsyncWriteDAL[AsyncSession, AsyncSQLATableT, DTOModelT, CUModelT, int],
|
|
554
|
+
Generic[AsyncSQLATableT, DTOModelT, CUModelT],
|
|
555
|
+
):
|
|
568
556
|
"""写入数据访问层基类."""
|
|
569
557
|
|
|
570
558
|
_Table: ClassVar[type[AsyncSQLATableT]] # pyright: ignore[reportGeneralTypeIssues]
|
|
@@ -644,7 +632,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
644
632
|
_ensure_strict_fields(provided_keys=provided_keys, allowed_names=allowed_names, strict=strict)
|
|
645
633
|
|
|
646
634
|
@classmethod
|
|
647
|
-
async def
|
|
635
|
+
async def update_full_by_id(
|
|
648
636
|
cls,
|
|
649
637
|
session: AsyncSession,
|
|
650
638
|
entity_id: int,
|
|
@@ -678,19 +666,7 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
678
666
|
return entity
|
|
679
667
|
|
|
680
668
|
@classmethod
|
|
681
|
-
async def
|
|
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(
|
|
669
|
+
async def update_partial_by_id(
|
|
694
670
|
cls,
|
|
695
671
|
session: AsyncSession,
|
|
696
672
|
entity_id: int,
|
|
@@ -757,30 +733,6 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
757
733
|
await session.refresh(entity)
|
|
758
734
|
return entity
|
|
759
735
|
|
|
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
|
-
|
|
784
736
|
@classmethod
|
|
785
737
|
async def delete_by_id(
|
|
786
738
|
cls,
|
|
@@ -816,11 +768,11 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
816
768
|
yield entity
|
|
817
769
|
|
|
818
770
|
@classmethod
|
|
819
|
-
async def
|
|
771
|
+
async def batch_update_by_conditions(
|
|
820
772
|
cls,
|
|
821
773
|
session: AsyncSession,
|
|
822
774
|
*,
|
|
823
|
-
|
|
775
|
+
whereclause: list[ColumnExpressionArgument[bool]],
|
|
824
776
|
update_data: dict[InstrumentedAttribute[Any], Any] | dict[sa.Column[Any], Any],
|
|
825
777
|
updater_id: int | None = None,
|
|
826
778
|
) -> int:
|
|
@@ -845,24 +797,13 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
845
797
|
if hasattr(cls._Table, "update_operator_id") and updater_id is not None:
|
|
846
798
|
final_update_data["update_operator_id"] = updater_id
|
|
847
799
|
|
|
848
|
-
stmt = sa.update(cls._Table).where(*
|
|
800
|
+
stmt = sa.update(cls._Table).where(*whereclause).values(**final_update_data)
|
|
849
801
|
|
|
850
802
|
result = await session.execute(stmt)
|
|
851
803
|
await session.flush()
|
|
852
804
|
|
|
853
805
|
return result.rowcount # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType, reportUnknownVariableType]
|
|
854
806
|
|
|
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
|
-
|
|
866
807
|
@classmethod
|
|
867
808
|
async def batch_update_by_ids(
|
|
868
809
|
cls,
|
|
@@ -876,15 +817,15 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
876
817
|
if not filtered_ids:
|
|
877
818
|
return 0
|
|
878
819
|
_id_column = cls._Table.id # pyright: ignore[reportAttributeAccessIssue,reportUnknownVariableType, reportUnknownMemberType]
|
|
879
|
-
return await cls.
|
|
820
|
+
return await cls.batch_update_by_conditions(
|
|
880
821
|
session,
|
|
881
|
-
|
|
822
|
+
whereclause=[_id_column.in_(filtered_ids)], # pyright: ignore[reportUnknownMemberType]
|
|
882
823
|
update_data=update_data,
|
|
883
824
|
updater_id=updater_id,
|
|
884
825
|
)
|
|
885
826
|
|
|
886
827
|
@classmethod
|
|
887
|
-
async def
|
|
828
|
+
async def update_only_set_with_optimistic_lock(
|
|
888
829
|
cls,
|
|
889
830
|
session: AsyncSession,
|
|
890
831
|
entity_id: int,
|
|
@@ -935,45 +876,13 @@ class AsyncWriteDAL(AsyncRawDAL, AsyncRawReadDAL, Generic[AsyncSQLATableT, DTOMo
|
|
|
935
876
|
|
|
936
877
|
raise DBRetryableError(f"{OPTIMISTIC_LOCK_ERROR_MSG_TRAIT}-版本号不匹配({entity_id=}, {expected_version=})")
|
|
937
878
|
|
|
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
|
-
|
|
958
879
|
|
|
959
880
|
class AsyncXDALOp(AsyncRawReadDAL, AsyncRawDAL):
|
|
960
881
|
"""扩展数据访问操作类."""
|
|
961
882
|
|
|
962
883
|
|
|
963
884
|
class AsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, DTOModelT], AsyncWriteDAL[AsyncSQLATableT, DTOModelT, CUModelT]):
|
|
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
|
-
)
|
|
885
|
+
"""基础数据访问层."""
|
|
977
886
|
|
|
978
887
|
|
|
979
888
|
class ReadOnlyAsyncBaseDAL(AsyncReadDAL[AsyncSQLATableT, ReadOnlyDTOModelT]):
|
|
@@ -212,15 +212,51 @@ class StdBaseDTO(BaseDTO[CUModelT]):
|
|
|
212
212
|
|
|
213
213
|
|
|
214
214
|
class SoftDeleteTableMixin:
|
|
215
|
-
"""
|
|
215
|
+
"""软删除表标记混入类 — 不强制任何列名或类型.
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
子类必须提供软删除列并覆写 ``is_soft_deleted`` property.
|
|
218
|
+
快捷方式: 继承 ``FieldIsDeleteSoftDeleteTableMixin`` 获得标准 ``is_delete`` 列.
|
|
219
|
+
|
|
220
|
+
自定义示例::
|
|
221
|
+
|
|
222
|
+
class MyTable(Base, SoftDeleteTableMixin):
|
|
223
|
+
__soft_delete_column__ = "deleted_at"
|
|
224
|
+
|
|
225
|
+
deleted_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True, default=None)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def is_soft_deleted(self) -> bool:
|
|
229
|
+
return self.deleted_at is not None
|
|
218
230
|
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
def soft_delete(self) -> None:
|
|
232
|
+
from datetime import datetime, timezone
|
|
221
233
|
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
self.deleted_at = datetime.now(timezone.utc)
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
__soft_delete_column__: ClassVar[str] = "is_delete"
|
|
238
|
+
|
|
239
|
+
def soft_delete(self) -> None:
|
|
240
|
+
"""标记为软删除."""
|
|
241
|
+
setattr(self, self.__soft_delete_column__, 1)
|
|
242
|
+
|
|
243
|
+
def soft_undelete(self) -> None:
|
|
244
|
+
"""恢复软删除."""
|
|
245
|
+
setattr(self, self.__soft_delete_column__, 0)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def is_soft_deleted(self) -> bool:
|
|
249
|
+
"""实体是否已被软删除(默认检查 ``is_delete != 0``,子类可覆写)."""
|
|
250
|
+
return bool(getattr(self, self.__soft_delete_column__))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class FieldIsDeleteSoftDeleteTableMixin(SoftDeleteTableMixin):
|
|
254
|
+
"""标准软删除混入类 — 提供 ``is_delete: Mapped[int]`` 列.
|
|
255
|
+
|
|
256
|
+
等价旧版 ``SoftDeleteTableMixin``,仅需 rename.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
is_delete: Mapped[int] = mapped_column(sa.Integer, default=0, comment="逻辑删除")
|
|
224
260
|
|
|
225
261
|
|
|
226
262
|
class ReadOnlyMixin:
|
|
@@ -267,6 +303,65 @@ class FieldMixin:
|
|
|
267
303
|
return
|
|
268
304
|
|
|
269
305
|
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Soft-delete hook management API (explicit register/unregister/check)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def register_soft_delete_hooks() -> None:
|
|
312
|
+
"""显式注册软删除 Session 事件监听器(幂等)。
|
|
313
|
+
|
|
314
|
+
在以下场景需要显式调用:
|
|
315
|
+
- 多进程 worker 子进程(import 可能未触发)
|
|
316
|
+
- 测试中自建 Session、未走应用启动链
|
|
317
|
+
- FastAPI lifespan / Flask app factory 显式初始化
|
|
318
|
+
"""
|
|
319
|
+
if not sa_event.contains(SyncSession, "before_flush", __receive_before_flush):
|
|
320
|
+
sa_event.listen(SyncSession, "before_flush", __receive_before_flush, insert=True)
|
|
321
|
+
if not sa_event.contains(SyncSession, "do_orm_execute", __add_filtering_criteria):
|
|
322
|
+
sa_event.listen(SyncSession, "do_orm_execute", __add_filtering_criteria, insert=True)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def unregister_soft_delete_hooks() -> None:
|
|
326
|
+
"""注销软删除 Session 事件监听器(幂等)。"""
|
|
327
|
+
if sa_event.contains(SyncSession, "before_flush", __receive_before_flush):
|
|
328
|
+
sa_event.remove(SyncSession, "before_flush", __receive_before_flush)
|
|
329
|
+
if sa_event.contains(SyncSession, "do_orm_execute", __add_filtering_criteria):
|
|
330
|
+
sa_event.remove(SyncSession, "do_orm_execute", __add_filtering_criteria)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def is_soft_delete_hooks_registered() -> bool:
|
|
334
|
+
"""检查软删除钩子是否已注册。"""
|
|
335
|
+
return sa_event.contains(SyncSession, "before_flush", __receive_before_flush) and sa_event.contains(
|
|
336
|
+
SyncSession, "do_orm_execute", __add_filtering_criteria
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def setup_dal_hooks() -> None:
|
|
341
|
+
"""注册所有必要的 Session 事件监听器(幂等)。
|
|
342
|
+
|
|
343
|
+
在应用生命周期开始时**调用一次**即可,无需关注具体注册了哪些钩子。
|
|
344
|
+
涵盖:软删除拦截、软删除查询过滤、只读保护。
|
|
345
|
+
|
|
346
|
+
FastAPI 示例::
|
|
347
|
+
|
|
348
|
+
@asynccontextmanager
|
|
349
|
+
async def lifespan(app):
|
|
350
|
+
setup_dal_hooks()
|
|
351
|
+
yield
|
|
352
|
+
|
|
353
|
+
Flask 示例::
|
|
354
|
+
|
|
355
|
+
def create_app():
|
|
356
|
+
app = Flask(__name__)
|
|
357
|
+
setup_dal_hooks()
|
|
358
|
+
return app
|
|
359
|
+
"""
|
|
360
|
+
register_soft_delete_hooks()
|
|
361
|
+
if not sa_event.contains(SyncSession, "before_flush", __prevent_readonly_write):
|
|
362
|
+
sa_event.listen(SyncSession, "before_flush", __prevent_readonly_write, insert=True)
|
|
363
|
+
|
|
364
|
+
|
|
270
365
|
# ---------------------------------------------------------------------------
|
|
271
366
|
# Session event listeners (registered on SyncSession — works for both sync
|
|
272
367
|
# and async since AsyncSession delegates to a SyncSession internally)
|
|
@@ -277,7 +372,7 @@ class FieldMixin:
|
|
|
277
372
|
def __receive_before_flush(session: SyncSession, flush_context: Any, instances: Any) -> None: # noqa: ARG001 # pyright: ignore[reportUnusedFunction, reportUnusedParameter]
|
|
278
373
|
for instance in session.deleted:
|
|
279
374
|
if isinstance(instance, SoftDeleteTableMixin):
|
|
280
|
-
instance.
|
|
375
|
+
instance.soft_delete()
|
|
281
376
|
session.add(instance)
|
|
282
377
|
|
|
283
378
|
|
|
@@ -292,7 +387,7 @@ def __add_filtering_criteria(execute_state: ORMExecuteState) -> None: # pyright
|
|
|
292
387
|
execute_state.statement = execute_state.statement.options(
|
|
293
388
|
with_loader_criteria(
|
|
294
389
|
SoftDeleteTableMixin,
|
|
295
|
-
lambda
|
|
390
|
+
lambda cls: getattr(cls, getattr(cls, "__soft_delete_column__", "is_delete"), sa.null()) == 0,
|
|
296
391
|
include_aliases=True,
|
|
297
392
|
)
|
|
298
393
|
)
|
|
@@ -352,6 +447,11 @@ __all__ = (
|
|
|
352
447
|
"T",
|
|
353
448
|
"V",
|
|
354
449
|
"_ensure_strict_fields",
|
|
450
|
+
"FieldIsDeleteSoftDeleteTableMixin",
|
|
355
451
|
"escape_like",
|
|
356
452
|
"filtered_in_sql_values",
|
|
453
|
+
"is_soft_delete_hooks_registered",
|
|
454
|
+
"register_soft_delete_hooks",
|
|
455
|
+
"setup_dal_hooks",
|
|
456
|
+
"unregister_soft_delete_hooks",
|
|
357
457
|
)
|
{lush_sqlalchemyx-0.3.1 → lush_sqlalchemyx-0.4.0}/src/lush_sqlalchemyx/base/dal/_pagination.py
RENAMED
|
@@ -42,7 +42,7 @@ def build_offset_stmt(
|
|
|
42
42
|
if order_by is not None:
|
|
43
43
|
stmt = stmt.order_by(order_by)
|
|
44
44
|
else:
|
|
45
|
-
stmt = stmt.order_by(table.id)
|
|
45
|
+
stmt = stmt.order_by(table.id)
|
|
46
46
|
return stmt.offset(p.skip).limit(p.limit)
|
|
47
47
|
|
|
48
48
|
|
|
@@ -55,7 +55,7 @@ def build_cursor_stmt(
|
|
|
55
55
|
使用 id > cursor_value 的 keyset 分页方式.
|
|
56
56
|
"""
|
|
57
57
|
p = pagination or CursorPagination()
|
|
58
|
-
id_col = table.id
|
|
58
|
+
id_col = table.id
|
|
59
59
|
stmt = sa.select(table).order_by(id_col)
|
|
60
60
|
|
|
61
61
|
if p.cursor is not None:
|