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.
Files changed (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +892 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.4.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
  138. {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
+