aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""领域模型模块。
|
|
2
|
+
|
|
3
|
+
提供 ORM 模型基类、Mixin 和常用组合模型。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .base import GUID, Base
|
|
7
|
+
from .mixins import (
|
|
8
|
+
AuditableStateMixin,
|
|
9
|
+
IDMixin,
|
|
10
|
+
TimestampMixin,
|
|
11
|
+
UUIDMixin,
|
|
12
|
+
VersionMixin,
|
|
13
|
+
)
|
|
14
|
+
from .models import (
|
|
15
|
+
AuditableStateModel,
|
|
16
|
+
FullFeaturedModel,
|
|
17
|
+
FullFeaturedUUIDModel,
|
|
18
|
+
IDOnlyModel,
|
|
19
|
+
Model,
|
|
20
|
+
UUIDAuditableStateModel,
|
|
21
|
+
UUIDModel,
|
|
22
|
+
UUIDOnlyModel,
|
|
23
|
+
VersionedModel,
|
|
24
|
+
VersionedTimestampedModel,
|
|
25
|
+
VersionedUUIDModel,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"GUID",
|
|
30
|
+
"AuditableStateMixin",
|
|
31
|
+
"AuditableStateModel",
|
|
32
|
+
# 基类和类型装饰器
|
|
33
|
+
"Base",
|
|
34
|
+
"FullFeaturedModel",
|
|
35
|
+
"FullFeaturedUUIDModel",
|
|
36
|
+
# Mixins
|
|
37
|
+
"IDMixin",
|
|
38
|
+
"IDOnlyModel",
|
|
39
|
+
# 组合模型
|
|
40
|
+
"Model",
|
|
41
|
+
"TimestampMixin",
|
|
42
|
+
"UUIDAuditableStateModel",
|
|
43
|
+
"UUIDMixin",
|
|
44
|
+
"UUIDModel",
|
|
45
|
+
"UUIDOnlyModel",
|
|
46
|
+
"VersionMixin",
|
|
47
|
+
"VersionedModel",
|
|
48
|
+
"VersionedTimestampedModel",
|
|
49
|
+
"VersionedUUIDModel",
|
|
50
|
+
]
|
|
51
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""模型基类和类型装饰器。
|
|
2
|
+
|
|
3
|
+
提供 SQLAlchemy 2.0 声明式基类和跨数据库类型装饰器。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import String
|
|
12
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
13
|
+
from sqlalchemy.types import TypeDecorator
|
|
14
|
+
|
|
15
|
+
from aury.boot.common.logging import logger
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GUID(TypeDecorator):
|
|
22
|
+
"""跨数据库 UUID 类型装饰器。
|
|
23
|
+
|
|
24
|
+
自动适配不同数据库:
|
|
25
|
+
- PostgreSQL: 使用原生 UUID 类型(需要安装 [pg] 可选依赖)
|
|
26
|
+
- 其他数据库: 使用 CHAR(36) 存储字符串格式的 UUID
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
impl = String(36)
|
|
30
|
+
cache_ok = True
|
|
31
|
+
|
|
32
|
+
def load_dialect_impl(self, dialect):
|
|
33
|
+
if dialect.name == "postgresql":
|
|
34
|
+
# PostgreSQL: 使用原生 UUID 类型
|
|
35
|
+
try:
|
|
36
|
+
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|
37
|
+
return dialect.type_descriptor(PGUUID(as_uuid=True))
|
|
38
|
+
except ImportError:
|
|
39
|
+
# 如果未安装 PostgreSQL 支持,回退到字符串
|
|
40
|
+
logger.warning(
|
|
41
|
+
"PostgreSQL UUID 支持需要安装 [pg] 可选依赖:"
|
|
42
|
+
"pip install aury-boot[pg]"
|
|
43
|
+
)
|
|
44
|
+
return dialect.type_descriptor(String(36))
|
|
45
|
+
else:
|
|
46
|
+
# 其他数据库: 使用字符串
|
|
47
|
+
return dialect.type_descriptor(String(36))
|
|
48
|
+
|
|
49
|
+
def process_bind_param(self, value, dialect):
|
|
50
|
+
if value is None:
|
|
51
|
+
return value
|
|
52
|
+
if not isinstance(value, UUID):
|
|
53
|
+
raise TypeError(f"Expected UUID, got {type(value)}")
|
|
54
|
+
return str(value)
|
|
55
|
+
|
|
56
|
+
def process_result_value(self, value, dialect):
|
|
57
|
+
if value is None:
|
|
58
|
+
return value
|
|
59
|
+
if isinstance(value, UUID):
|
|
60
|
+
return value
|
|
61
|
+
return UUID(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Base(DeclarativeBase):
|
|
65
|
+
"""SQLAlchemy 2.0 声明式基类"""
|
|
66
|
+
|
|
67
|
+
# 自动生成表名逻辑(可选,例如将 UserProfile 转为 user_profile)
|
|
68
|
+
# pass
|
|
69
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""模型功能 Mixins。
|
|
2
|
+
|
|
3
|
+
提供可组合的功能 Mixin,包括主键、时间戳、软删除、乐观锁等。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import BigInteger, DateTime, Integer, func, text
|
|
14
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
15
|
+
from sqlalchemy.types import Uuid as SQLAlchemyUuid
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from sqlalchemy.sql import Select
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IDMixin:
|
|
22
|
+
"""标准自增主键 Mixin"""
|
|
23
|
+
|
|
24
|
+
id: Mapped[int] = mapped_column(
|
|
25
|
+
Integer,
|
|
26
|
+
primary_key=True,
|
|
27
|
+
autoincrement=True,
|
|
28
|
+
sort_order=-1, # 确保 ID 在 DDL 中排在前面
|
|
29
|
+
comment="主键ID",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UUIDMixin:
|
|
34
|
+
"""UUID 主键 Mixin"""
|
|
35
|
+
|
|
36
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
37
|
+
SQLAlchemyUuid(as_uuid=True), # 2.0 会自动适配 PG(uuid) 和 MySQL(char(36))
|
|
38
|
+
primary_key=True,
|
|
39
|
+
default=uuid.uuid4,
|
|
40
|
+
sort_order=-1,
|
|
41
|
+
comment="UUID主键",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TimestampMixin:
|
|
46
|
+
"""创建/更新时间 Mixin"""
|
|
47
|
+
|
|
48
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
49
|
+
DateTime(timezone=True),
|
|
50
|
+
server_default=func.now(),
|
|
51
|
+
sort_order=99,
|
|
52
|
+
comment="创建时间",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
56
|
+
DateTime(timezone=True),
|
|
57
|
+
server_default=func.now(),
|
|
58
|
+
onupdate=func.now(), # 使用数据库层面的更新,比 Python lambda 更准确
|
|
59
|
+
sort_order=99,
|
|
60
|
+
comment="更新时间",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AuditableStateMixin:
|
|
65
|
+
"""可审计状态 Mixin (软删除核心优化版)
|
|
66
|
+
|
|
67
|
+
采用 '默认 0' 策略:
|
|
68
|
+
- deleted_at = 0: 未删除
|
|
69
|
+
- deleted_at > 0: 已删除 (Unix 时间戳)
|
|
70
|
+
|
|
71
|
+
优势:
|
|
72
|
+
1. 完美支持 MySQL 唯一索引 UNIQUE(email, deleted_at)。
|
|
73
|
+
2. 整数索引查询极快。
|
|
74
|
+
3. 可以记录删除时间(审计支持)。
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
deleted_at: Mapped[int] = mapped_column(
|
|
78
|
+
BigInteger,
|
|
79
|
+
default=0,
|
|
80
|
+
server_default=text("0"), # 确保数据库层面默认值也是 0
|
|
81
|
+
index=True,
|
|
82
|
+
comment="删除时间戳(0=未删)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def is_deleted(self) -> bool:
|
|
87
|
+
"""判断是否已删除。"""
|
|
88
|
+
return self.deleted_at > 0
|
|
89
|
+
|
|
90
|
+
def mark_deleted(self) -> None:
|
|
91
|
+
"""标记为已删除。"""
|
|
92
|
+
self.deleted_at = int(time.time())
|
|
93
|
+
|
|
94
|
+
def restore(self) -> None:
|
|
95
|
+
"""恢复数据。"""
|
|
96
|
+
self.deleted_at = 0
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def not_deleted(cls) -> Select:
|
|
100
|
+
"""返回未删除记录的查询条件(用于 SQLAlchemy 查询)。
|
|
101
|
+
|
|
102
|
+
注意:这是简单的工具方法,仅构建查询对象,不执行查询。
|
|
103
|
+
实际查询应在 Repository 层执行。
|
|
104
|
+
"""
|
|
105
|
+
from sqlalchemy import select # 延迟导入避免循环依赖
|
|
106
|
+
|
|
107
|
+
return select(cls).where(cls.deleted_at == 0)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def with_deleted(cls) -> Select:
|
|
111
|
+
"""返回包含已删除记录的查询(用于审计)。
|
|
112
|
+
|
|
113
|
+
注意:这是简单的工具方法,仅构建查询对象,不执行查询。
|
|
114
|
+
实际查询应在 Repository 层执行。
|
|
115
|
+
"""
|
|
116
|
+
from sqlalchemy import select # 延迟导入避免循环依赖
|
|
117
|
+
|
|
118
|
+
return select(cls)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class VersionMixin:
|
|
122
|
+
"""乐观锁版本控制 Mixin"""
|
|
123
|
+
|
|
124
|
+
version: Mapped[int] = mapped_column(
|
|
125
|
+
Integer,
|
|
126
|
+
default=1,
|
|
127
|
+
server_default=text("1"),
|
|
128
|
+
nullable=False,
|
|
129
|
+
comment="乐观锁版本号",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
__mapper_args__ = {
|
|
133
|
+
"version_id_col": "version" # SQLAlchemy 自动处理乐观锁逻辑
|
|
134
|
+
}
|
|
135
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""常用组合模型。
|
|
2
|
+
|
|
3
|
+
提供预定义的模型组合,方便直接使用。
|
|
4
|
+
|
|
5
|
+
**重要提示**:不要直接继承 Base 类用于 Repository,必须使用下列预定义模型或自己组合 Mixin。
|
|
6
|
+
直接使用 Base 会导致缺少 id 等必要字段,Repository 会报错。
|
|
7
|
+
|
|
8
|
+
可用模型:
|
|
9
|
+
- Model: 标准模型(int主键 + 时间戳)
|
|
10
|
+
- AuditableStateModel: 标准模型 + 软删除
|
|
11
|
+
- UUIDModel: UUID 主键模型(UUID 主键 + 时间戳)
|
|
12
|
+
- UUIDAuditableStateModel: UUID 主键模型 + 软删除
|
|
13
|
+
- VersionedModel: 乐观锁模型(int主键 + 乐观锁)
|
|
14
|
+
- VersionedTimestampedModel: 乐观锁 + 时间戳
|
|
15
|
+
- VersionedUUIDModel: UUID 主键 + 乐观锁 + 时间戳
|
|
16
|
+
- FullFeaturedModel: 完整功能(int主键 + 时间戳 + 软删除 + 乐观锁)
|
|
17
|
+
- FullFeaturedUUIDModel: 完整功能 UUID 版本
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .base import Base
|
|
23
|
+
from .mixins import (
|
|
24
|
+
AuditableStateMixin,
|
|
25
|
+
IDMixin,
|
|
26
|
+
TimestampMixin,
|
|
27
|
+
UUIDMixin,
|
|
28
|
+
VersionMixin,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IDOnlyModel(IDMixin, Base):
|
|
33
|
+
"""纯 int 主键模型(无时间戳,适合关系表)"""
|
|
34
|
+
|
|
35
|
+
__abstract__ = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UUIDOnlyModel(UUIDMixin, Base):
|
|
39
|
+
"""纯 UUID 主键模型(无时间戳,适合关系表)"""
|
|
40
|
+
|
|
41
|
+
__abstract__ = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Model(IDMixin, TimestampMixin, Base):
|
|
45
|
+
"""【常用】标准整数主键模型"""
|
|
46
|
+
|
|
47
|
+
__abstract__ = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AuditableStateModel(IDMixin, TimestampMixin, AuditableStateMixin, Base):
|
|
51
|
+
"""【常用】带可审计状态的标准模型(整数主键 + 时间戳 + 软删除)"""
|
|
52
|
+
|
|
53
|
+
__abstract__ = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UUIDModel(UUIDMixin, TimestampMixin, Base):
|
|
57
|
+
"""【常用】UUID 主键模型"""
|
|
58
|
+
|
|
59
|
+
__abstract__ = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UUIDAuditableStateModel(UUIDMixin, TimestampMixin, AuditableStateMixin, Base):
|
|
63
|
+
"""【常用】带可审计状态的 UUID 主键模型(UUID 主键 + 时间戳 + 软删除)"""
|
|
64
|
+
|
|
65
|
+
__abstract__ = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VersionedModel(IDMixin, VersionMixin, Base):
|
|
69
|
+
"""整数主键 + 乐观锁"""
|
|
70
|
+
|
|
71
|
+
__abstract__ = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class VersionedTimestampedModel(IDMixin, TimestampMixin, VersionMixin, Base):
|
|
75
|
+
"""整数主键 + 时间戳 + 乐观锁"""
|
|
76
|
+
|
|
77
|
+
__abstract__ = True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class VersionedUUIDModel(UUIDMixin, TimestampMixin, VersionMixin, Base):
|
|
81
|
+
"""UUID 主键 + 时间戳 + 乐观锁"""
|
|
82
|
+
|
|
83
|
+
__abstract__ = True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FullFeaturedModel(IDMixin, TimestampMixin, AuditableStateMixin, VersionMixin, Base):
|
|
87
|
+
"""完整功能:整数主键 + 时间戳 + 可审计状态 + 乐观锁"""
|
|
88
|
+
|
|
89
|
+
__abstract__ = True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class FullFeaturedUUIDModel(UUIDMixin, TimestampMixin, AuditableStateMixin, VersionMixin, Base):
|
|
93
|
+
"""完整功能:UUID 主键 + 时间戳 + 可审计状态 + 乐观锁"""
|
|
94
|
+
|
|
95
|
+
__abstract__ = True
|
|
96
|
+
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""分页和排序标准化模块。
|
|
2
|
+
|
|
3
|
+
提供统一的分页和排序参数定义,以及分页结果封装。
|
|
4
|
+
使用 Pydantic 2.5 进行数据验证和序列化。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PaginationParams(BaseModel):
|
|
15
|
+
"""分页参数。
|
|
16
|
+
|
|
17
|
+
定义分页查询的参数,包括页码和每页记录数。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
page: int = Field(default=1, ge=1, description="页码,从1开始")
|
|
21
|
+
page_size: int = Field(default=20, ge=1, le=100, description="每页记录数,最大100")
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def offset(self) -> int:
|
|
25
|
+
"""计算偏移量。
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
int: 偏移量
|
|
29
|
+
"""
|
|
30
|
+
return (self.page - 1) * self.page_size
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def limit(self) -> int:
|
|
34
|
+
"""获取限制数量。
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
int: 限制数量
|
|
38
|
+
"""
|
|
39
|
+
return self.page_size
|
|
40
|
+
|
|
41
|
+
@field_validator("page")
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_page(cls, v: int) -> int:
|
|
44
|
+
"""验证页码。
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
v: 页码值
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
int: 验证后的页码
|
|
51
|
+
"""
|
|
52
|
+
if v < 1:
|
|
53
|
+
raise ValueError("页码必须大于0")
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
@field_validator("page_size")
|
|
57
|
+
@classmethod
|
|
58
|
+
def validate_page_size(cls, v: int) -> int:
|
|
59
|
+
"""验证每页记录数。
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
v: 每页记录数
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
int: 验证后的每页记录数
|
|
66
|
+
"""
|
|
67
|
+
if v < 1:
|
|
68
|
+
raise ValueError("每页记录数必须大于0")
|
|
69
|
+
if v > 100:
|
|
70
|
+
raise ValueError("每页记录数不能超过100")
|
|
71
|
+
return v
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SortParams(BaseModel):
|
|
75
|
+
"""排序参数。
|
|
76
|
+
|
|
77
|
+
定义排序的字段和方向。
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
sorts: list[tuple[str, str]] = Field(default_factory=list, description="排序字段列表")
|
|
81
|
+
|
|
82
|
+
def add_sort(self, field: str, direction: str = "asc") -> None:
|
|
83
|
+
"""添加排序字段。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
field: 字段名
|
|
87
|
+
direction: 排序方向,asc 或 desc
|
|
88
|
+
"""
|
|
89
|
+
if direction.lower() not in ("asc", "desc"):
|
|
90
|
+
raise ValueError("排序方向必须是 'asc' 或 'desc'")
|
|
91
|
+
self.sorts.append((field, direction.lower()))
|
|
92
|
+
|
|
93
|
+
def add_sorts(self, *sorts: tuple[str, str]) -> None:
|
|
94
|
+
"""添加多个排序字段。
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
*sorts: 排序字段元组列表
|
|
98
|
+
"""
|
|
99
|
+
for field, direction in sorts:
|
|
100
|
+
self.add_sort(field, direction)
|
|
101
|
+
|
|
102
|
+
@field_validator("sorts", mode="before")
|
|
103
|
+
@classmethod
|
|
104
|
+
def validate_sorts(cls, v: Any) -> list[tuple[str, str]]:
|
|
105
|
+
"""验证排序字段列表。
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
v: 排序字段列表
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
list[tuple[str, str]]: 验证后的排序字段列表
|
|
112
|
+
"""
|
|
113
|
+
if not v:
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
result = []
|
|
117
|
+
for item in v:
|
|
118
|
+
if isinstance(item, str):
|
|
119
|
+
# 如果是字符串,解析为字段和方向
|
|
120
|
+
if item.startswith("-"):
|
|
121
|
+
field = item[1:]
|
|
122
|
+
direction = "desc"
|
|
123
|
+
else:
|
|
124
|
+
field = item
|
|
125
|
+
direction = "asc"
|
|
126
|
+
result.append((field, direction))
|
|
127
|
+
elif isinstance(item, list | tuple) and len(item) == 2:
|
|
128
|
+
field, direction = item
|
|
129
|
+
if direction.lower() not in ("asc", "desc"):
|
|
130
|
+
raise ValueError(f"排序方向必须是 'asc' 或 'desc',得到: {direction}")
|
|
131
|
+
result.append((field, direction.lower()))
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"无效的排序参数: {item}")
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class PaginationResult[ModelType](BaseModel):
|
|
139
|
+
"""分页结果。
|
|
140
|
+
|
|
141
|
+
封装分页查询的结果,包括数据列表和分页信息。
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
items: list[ModelType] = Field(description="数据列表")
|
|
145
|
+
total: int = Field(ge=0, description="总记录数")
|
|
146
|
+
page: int = Field(ge=1, description="当前页码")
|
|
147
|
+
page_size: int = Field(ge=1, description="每页记录数")
|
|
148
|
+
total_pages: int = Field(ge=0, description="总页数")
|
|
149
|
+
has_next: bool = Field(description="是否有下一页")
|
|
150
|
+
has_prev: bool = Field(description="是否有上一页")
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def create(
|
|
154
|
+
cls,
|
|
155
|
+
items: list[ModelType],
|
|
156
|
+
total: int,
|
|
157
|
+
pagination_params: PaginationParams
|
|
158
|
+
) -> PaginationResult[ModelType]:
|
|
159
|
+
"""创建分页结果。
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
items: 数据列表
|
|
163
|
+
total: 总记录数
|
|
164
|
+
pagination_params: 分页参数
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
PaginationResult[ModelType]: 分页结果
|
|
168
|
+
"""
|
|
169
|
+
total_pages = (total + pagination_params.page_size - 1) // pagination_params.page_size
|
|
170
|
+
has_next = pagination_params.page < total_pages
|
|
171
|
+
has_prev = pagination_params.page > 1
|
|
172
|
+
|
|
173
|
+
return cls(
|
|
174
|
+
items=items,
|
|
175
|
+
total=total,
|
|
176
|
+
page=pagination_params.page,
|
|
177
|
+
page_size=pagination_params.page_size,
|
|
178
|
+
total_pages=total_pages,
|
|
179
|
+
has_next=has_next,
|
|
180
|
+
has_prev=has_prev,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def get_next_params(self) -> PaginationParams | None:
|
|
184
|
+
"""获取下一页的分页参数。
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
PaginationParams | None: 下一页的分页参数,如果没有下一页则返回None
|
|
188
|
+
"""
|
|
189
|
+
if not self.has_next:
|
|
190
|
+
return None
|
|
191
|
+
return PaginationParams(page=self.page + 1, page_size=self.page_size)
|
|
192
|
+
|
|
193
|
+
def get_prev_params(self) -> PaginationParams | None:
|
|
194
|
+
"""获取上一页的分页参数。
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
PaginationParams | None: 上一页的分页参数,如果没有上一页则返回None
|
|
198
|
+
"""
|
|
199
|
+
if not self.has_prev:
|
|
200
|
+
return None
|
|
201
|
+
return PaginationParams(page=self.page - 1, page_size=self.page_size)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CursorPaginationParams(BaseModel):
|
|
205
|
+
"""游标分页参数。
|
|
206
|
+
|
|
207
|
+
基于游标的分页,适用于大数据集和高性能场景。
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
cursor: str | None = Field(default=None, description="游标,用于定位数据位置")
|
|
211
|
+
limit: int = Field(default=20, ge=1, le=100, description="每页记录数,最大100")
|
|
212
|
+
direction: str = Field(default="next", description="分页方向:next 或 prev")
|
|
213
|
+
|
|
214
|
+
@field_validator("direction")
|
|
215
|
+
@classmethod
|
|
216
|
+
def validate_direction(cls, v: str) -> str:
|
|
217
|
+
"""验证分页方向。
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
v: 分页方向
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
str: 验证后的分页方向
|
|
224
|
+
"""
|
|
225
|
+
if v.lower() not in ("next", "prev"):
|
|
226
|
+
raise ValueError("分页方向必须是 'next' 或 'prev'")
|
|
227
|
+
return v.lower()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class CursorPaginationResult[ModelType](BaseModel):
|
|
231
|
+
"""游标分页结果。
|
|
232
|
+
|
|
233
|
+
封装游标分页查询的结果。
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
items: list[ModelType] = Field(description="数据列表")
|
|
237
|
+
next_cursor: str | None = Field(description="下一页游标")
|
|
238
|
+
prev_cursor: str | None = Field(description="上一页游标")
|
|
239
|
+
has_next: bool = Field(description="是否有下一页")
|
|
240
|
+
has_prev: bool = Field(description="是否有上一页")
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def create(
|
|
244
|
+
cls,
|
|
245
|
+
items: list[ModelType],
|
|
246
|
+
next_cursor: str | None = None,
|
|
247
|
+
prev_cursor: str | None = None,
|
|
248
|
+
limit: int = 20
|
|
249
|
+
) -> CursorPaginationResult[ModelType]:
|
|
250
|
+
"""创建游标分页结果。
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
items: 数据列表
|
|
254
|
+
next_cursor: 下一页游标
|
|
255
|
+
prev_cursor: 上一页游标
|
|
256
|
+
limit: 每页记录数
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
CursorPaginationResult[ModelType]: 游标分页结果
|
|
260
|
+
"""
|
|
261
|
+
has_next = next_cursor is not None
|
|
262
|
+
has_prev = prev_cursor is not None
|
|
263
|
+
|
|
264
|
+
return cls(
|
|
265
|
+
items=items,
|
|
266
|
+
next_cursor=next_cursor,
|
|
267
|
+
prev_cursor=prev_cursor,
|
|
268
|
+
has_next=has_next,
|
|
269
|
+
has_prev=has_prev,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
__all__ = [
|
|
274
|
+
"CursorPaginationParams",
|
|
275
|
+
"CursorPaginationResult",
|
|
276
|
+
"PaginationParams",
|
|
277
|
+
"PaginationResult",
|
|
278
|
+
"SortParams",
|
|
279
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""仓储模块 - 接口、实现和工具。
|
|
2
|
+
|
|
3
|
+
提供:
|
|
4
|
+
- IRepository: 仓储接口
|
|
5
|
+
- BaseRepository: 通用 CRUD 实现
|
|
6
|
+
- QueryBuilder: 查询构建器
|
|
7
|
+
- QueryInterceptor: 查询拦截器
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .impl import BaseRepository, SimpleRepository
|
|
11
|
+
from .interceptors import QueryInterceptor
|
|
12
|
+
from .interface import IRepository
|
|
13
|
+
from .query_builder import QueryBuilder
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseRepository",
|
|
17
|
+
"IRepository",
|
|
18
|
+
"QueryBuilder",
|
|
19
|
+
"QueryInterceptor",
|
|
20
|
+
"SimpleRepository",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|