fastapi-augment 0.1.0__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 (50) hide show
  1. fastapi_augment/__init__.py +24 -0
  2. fastapi_augment/common/__init__.py +61 -0
  3. fastapi_augment/common/constants.py +26 -0
  4. fastapi_augment/common/exception_handlers.py +178 -0
  5. fastapi_augment/common/exceptions.py +162 -0
  6. fastapi_augment/common/utils/__init__.py +5 -0
  7. fastapi_augment/common/utils/strings.py +175 -0
  8. fastapi_augment/config/__init__.py +8 -0
  9. fastapi_augment/config/settings.py +104 -0
  10. fastapi_augment/db/__init__.py +5 -0
  11. fastapi_augment/db/sqlalchemy/__init__.py +20 -0
  12. fastapi_augment/db/sqlalchemy/alembic/__init__.py +5 -0
  13. fastapi_augment/db/sqlalchemy/alembic/env.py +141 -0
  14. fastapi_augment/db/sqlalchemy/base.py +9 -0
  15. fastapi_augment/db/sqlalchemy/crud_base.py +426 -0
  16. fastapi_augment/db/sqlalchemy/engine.py +238 -0
  17. fastapi_augment/db/sqlalchemy/migrate.py +356 -0
  18. fastapi_augment/db/sqlalchemy/mixins/__init__.py +18 -0
  19. fastapi_augment/db/sqlalchemy/mixins/audit.py +61 -0
  20. fastapi_augment/db/sqlalchemy/mixins/soft_delete.py +80 -0
  21. fastapi_augment/db/sqlalchemy/mixins/timestamp.py +48 -0
  22. fastapi_augment/db/sqlalchemy/model_base.py +47 -0
  23. fastapi_augment/db/sqlalchemy/session.py +160 -0
  24. fastapi_augment/factory.py +238 -0
  25. fastapi_augment/health/__init__.py +34 -0
  26. fastapi_augment/health/checker.py +101 -0
  27. fastapi_augment/health/checkers.py +109 -0
  28. fastapi_augment/health/router.py +87 -0
  29. fastapi_augment/lifespan.py +450 -0
  30. fastapi_augment/log/__init__.py +26 -0
  31. fastapi_augment/log/config.py +201 -0
  32. fastapi_augment/log/factory.py +32 -0
  33. fastapi_augment/log/filters.py +23 -0
  34. fastapi_augment/log/handlers.py +81 -0
  35. fastapi_augment/middlewares/__init__.py +20 -0
  36. fastapi_augment/middlewares/base.py +79 -0
  37. fastapi_augment/middlewares/request_id.py +82 -0
  38. fastapi_augment/openapi.py +110 -0
  39. fastapi_augment/py.typed +0 -0
  40. fastapi_augment/schemas/__init__.py +29 -0
  41. fastapi_augment/schemas/base.py +32 -0
  42. fastapi_augment/schemas/pagination.py +46 -0
  43. fastapi_augment/schemas/request.py +28 -0
  44. fastapi_augment/schemas/response.py +139 -0
  45. fastapi_augment/schemas/types.py +11 -0
  46. fastapi_augment-0.1.0.dist-info/METADATA +654 -0
  47. fastapi_augment-0.1.0.dist-info/RECORD +50 -0
  48. fastapi_augment-0.1.0.dist-info/WHEEL +5 -0
  49. fastapi_augment-0.1.0.dist-info/entry_points.txt +2 -0
  50. fastapi_augment-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,356 @@
1
+ """
2
+ @Author : zarkhan
3
+ @CreateDate : 2026/9/5
4
+ @Description: 数据库迁移脚本
5
+ """
6
+ import argparse
7
+ import subprocess
8
+ import sys
9
+ from logging import getLogger
10
+ from os import environ
11
+ from pathlib import Path
12
+
13
+ from alembic import command
14
+ from alembic.config import Config
15
+
16
+ _logger = getLogger(__name__)
17
+
18
+ # ===================== alembic.ini 模板 =====================
19
+
20
+ _ALEMBIC_INI_TEMPLATE = """\
21
+ # A generic, single database configuration.
22
+
23
+ [alembic]
24
+ # path to migration scripts.
25
+ # this is typically a path given in POSIX (e.g. forward slashes)
26
+ # format, relative to the token %(here)s which refers to the location of this
27
+ # ini file
28
+ script_location = fastapi_augment.db.sqlalchemy:alembic
29
+ version_locations = %(here)s/migrations/versions
30
+
31
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
32
+ # Uncomment the line below if you want the files to be prepended with date and time
33
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
34
+ # for all available tokens
35
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
36
+ # Or organize into date-based subdirectories (requires recursive_version_locations = true)
37
+ # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
38
+
39
+ # sys.path path, will be prepended to sys.path if present.
40
+ # defaults to the current working directory. for multiple paths, the path separator
41
+ # is defined by "path_separator" below.
42
+ prepend_sys_path = .
43
+
44
+
45
+ # timezone to use when rendering the date within the migration file
46
+ # as well as the filename.
47
+ # If specified, requires the tzdata library which can be installed by adding
48
+ # `alembic[tz]` to the pip requirements.
49
+ # string value is passed to ZoneInfo()
50
+ # leave blank for localtime
51
+ # timezone =
52
+
53
+ # max length of characters to apply to the "slug" field
54
+ # truncate_slug_length = 40
55
+
56
+ # set to 'true' to run the environment during
57
+ # the 'revision' command, regardless of autogenerate
58
+ # revision_environment = false
59
+
60
+ # set to 'true' to allow .pyc and .pyo files without
61
+ # a source .py file to be detected as revisions in the
62
+ # versions/ directory
63
+ # sourceless = false
64
+
65
+ # version location specification; This defaults
66
+ # to <script_location>/versions. When using multiple version
67
+ # directories, initial revisions must be specified with --version-path.
68
+ # The path separator used here should be the separator specified by "path_separator"
69
+ # below.
70
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
71
+
72
+ # path_separator; This indicates what character is used to split lists of file
73
+ # paths, including version_locations and prepend_sys_path within configparser
74
+ # files such as alembic.ini.
75
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
76
+ # to provide os-dependent path splitting.
77
+ #
78
+ # Note that in order to support legacy alembic.ini files, this default does NOT
79
+ # take place if path_separator is not present in alembic.ini. If this
80
+ # option is omitted entirely, fallback logic is as follows:
81
+ #
82
+ # 1. Parsing of the version_locations option falls back to using the legacy
83
+ # "version_path_separator" key, which if absent then falls back to the legacy
84
+ # behavior of splitting on spaces and/or commas.
85
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
86
+ # behavior of splitting on spaces, commas, or colons.
87
+ #
88
+ # Valid values for path_separator are:
89
+ #
90
+ # path_separator = :
91
+ # path_separator = ;
92
+ # path_separator = space
93
+ # path_separator = newline
94
+ #
95
+ # Use os.pathsep. Default configuration used for new projects.
96
+ path_separator = os
97
+
98
+ # set to 'true' to search source files recursively
99
+ # in each "version_locations" directory
100
+ # new in Alembic version 1.10
101
+ # recursive_version_locations = false
102
+
103
+ # the output encoding used when revision files
104
+ # are written from script.py.mako
105
+ # output_encoding = utf-8
106
+
107
+ # database URL. This is consumed by the user-maintained env.py script only.
108
+ # other means of configuring database URLs may be customized within the env.py
109
+ # file.
110
+ sqlalchemy.url = {db_url}
111
+
112
+ # set the version table name
113
+ version_table = migration_version
114
+
115
+ [post_write_hooks]
116
+ # post_write_hooks defines scripts or Python functions that are run
117
+ # on newly generated revision scripts. See the documentation for further
118
+ # detail and examples
119
+
120
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
121
+ # hooks = black
122
+ # black.type = console_scripts
123
+ # black.entrypoint = black
124
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
125
+
126
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
127
+ # hooks = ruff
128
+ # ruff.type = module
129
+ # ruff.module = ruff
130
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
131
+
132
+ # Alternatively, use the exec runner to execute a binary found on your PATH
133
+ # hooks = ruff
134
+ # ruff.type = exec
135
+ # ruff.executable = ruff
136
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
137
+
138
+ # Logging configuration. This is also consumed by the user-maintained
139
+ # env.py script only.
140
+ [loggers]
141
+ keys = root,sqlalchemy,alembic
142
+
143
+ [handlers]
144
+ keys = console
145
+
146
+ [formatters]
147
+ keys = generic
148
+
149
+ [logger_root]
150
+ level = WARNING
151
+ handlers = console
152
+ qualname =
153
+
154
+ [logger_sqlalchemy]
155
+ level = WARNING
156
+ handlers =
157
+ qualname = sqlalchemy.engine
158
+
159
+ [logger_alembic]
160
+ level = INFO
161
+ handlers =
162
+ qualname = alembic
163
+
164
+ [handler_console]
165
+ class = StreamHandler
166
+ args = (sys.stderr,)
167
+ level = NOTSET
168
+ formatter = generic
169
+
170
+ [formatter_generic]
171
+ format = %(levelname)-5.5s [%(name)s] %(message)s
172
+ datefmt = %H:%M:%S
173
+ """
174
+
175
+
176
+ # ===================== 内部工具 =====================
177
+
178
+ def _resolve_alembic_config(db_url: str | None = None, project_dir: Path | None = None) -> Config:
179
+ """构造 Alembic Config 对象,读取项目根目录的 alembic.ini
180
+
181
+ Args:
182
+ db_url: 数据库连接URL(可选,不传则从 alembic.ini 读取)
183
+ project_dir: 项目根目录(默认当前工作目录)
184
+
185
+ Returns:
186
+ Alembic 配置对象
187
+
188
+ Raises:
189
+ FileNotFoundError: 若根目录 alembic.ini 不存在,提示用户先执行 init
190
+ """
191
+ here = project_dir or Path.cwd()
192
+ root_ini = here / 'alembic.ini'
193
+
194
+ if not root_ini.exists():
195
+ raise FileNotFoundError(
196
+ f'未找到 {root_ini},请先执行: fastapi-augment-migrate init --db-url "<数据库URL>"'
197
+ )
198
+
199
+ cfg = Config(str(root_ini))
200
+ if db_url:
201
+ cfg.set_main_option('sqlalchemy.url', db_url)
202
+ return cfg
203
+
204
+
205
+ # ===================== init =====================
206
+
207
+ def init_project(db_url: str, project_dir: Path | None = None) -> None:
208
+ """初始化项目迁移环境
209
+
210
+ 在项目根目录生成 alembic.ini 和 alembic/versions/ 目录。
211
+ 若 alembic.ini 已存在则跳过,不覆盖。
212
+
213
+ Args:
214
+ db_url: 数据库连接URL
215
+ project_dir: 项目根目录
216
+ """
217
+ here = project_dir or Path.cwd()
218
+ alembic_ini = here / 'alembic.ini'
219
+ versions_dir = here / 'migrations' / 'versions'
220
+
221
+ if alembic_ini.exists():
222
+ _logger.info('%s 已存在,跳过(不会覆盖)', alembic_ini)
223
+ else:
224
+ ini_content = _ALEMBIC_INI_TEMPLATE.format(db_url=db_url)
225
+ alembic_ini.write_text(ini_content)
226
+ _logger.info('已创建 %s', alembic_ini)
227
+
228
+ versions_dir.mkdir(parents=True, exist_ok=True)
229
+ _logger.info('已确保 %s 存在', versions_dir)
230
+ _logger.info('初始化完成,现在可以使用 generate 生成迁移了')
231
+
232
+
233
+ # ===================== upgrade / downgrade =====================
234
+
235
+ def upgrade(db_url: str | None = None, revision: str = 'head', project_dir: Path | None = None) -> None:
236
+ """升级数据库
237
+
238
+ Args:
239
+ db_url: 数据库连接URL(可选,不传则从 alembic.ini 读取)
240
+ revision: 目标修订版本
241
+ project_dir: 项目根目录
242
+ """
243
+ cfg = _resolve_alembic_config(db_url, project_dir)
244
+ command.upgrade(cfg, revision)
245
+
246
+
247
+ def downgrade(db_url: str | None = None, revision: str = '-1', project_dir: Path | None = None) -> None:
248
+ """降级数据库
249
+
250
+ Args:
251
+ db_url: 数据库连接URL(可选,不传则从 alembic.ini 读取)
252
+ revision: 目标修订版本
253
+ project_dir: 项目根目录
254
+ """
255
+ cfg = _resolve_alembic_config(db_url, project_dir)
256
+ command.downgrade(cfg, revision)
257
+
258
+
259
+ # ===================== generate =====================
260
+
261
+ def generate_migration(message: str, models: str, project_dir: Path | None = None) -> None:
262
+ """生成迁移文件
263
+
264
+ 调用 alembic revision --autogenerate 生成迁移脚本。
265
+ 需要项目根目录已存在 alembic.ini(通过 init 命令创建)。
266
+
267
+ Args:
268
+ message: 迁移描述信息
269
+ models: 模型模块,多个用逗号分隔 (如 myapp.models)
270
+ project_dir: 项目根目录
271
+ """
272
+ here = project_dir or Path.cwd()
273
+ alembic_ini = here / 'alembic.ini'
274
+ versions_dir = here / 'migrations' / 'versions'
275
+
276
+ # 检查 alembic.ini 是否存在(不自动创建,由 init 负责)
277
+ if not alembic_ini.exists():
278
+ raise FileNotFoundError(
279
+ f'未找到 {alembic_ini},请先执行: fastapi-augment-migrate init --db-url "<数据库URL>"'
280
+ )
281
+
282
+ # 确保 versions 目录存在
283
+ versions_dir.mkdir(parents=True, exist_ok=True)
284
+
285
+ # 设置环境变量,让库内的 env.py 能加载用户模型
286
+ env = environ.copy()
287
+ env['FASTAPI_AUGMENT_MODELS'] = models
288
+
289
+ # 先 stamp head,同步数据库版本标记,
290
+ # 避免 "Target database is not up to date" 错误
291
+ stamp_cmd = [
292
+ sys.executable, '-m', 'alembic',
293
+ '-c', str(alembic_ini),
294
+ 'stamp', 'head'
295
+ ]
296
+ subprocess.run(stamp_cmd, env=env, cwd=str(here))
297
+
298
+ # 调用 alembic 生成迁移
299
+ cmd = [
300
+ sys.executable, '-m', 'alembic',
301
+ '-c', str(alembic_ini),
302
+ 'revision', '--autogenerate', '-m', message
303
+ ]
304
+ try:
305
+ subprocess.run(cmd, env=env, cwd=str(here), check=True)
306
+ _logger.info('迁移脚本已生成于 %s', versions_dir)
307
+ except subprocess.CalledProcessError as e:
308
+ raise RuntimeError(f'生成迁移失败 (返回码 {e.returncode})') from e
309
+
310
+
311
+ # ===================== CLI 入口 =====================
312
+
313
+ def cli_main() -> None:
314
+ """数据库迁移工具 (fastapi-augment)"""
315
+ parser = argparse.ArgumentParser(description='数据库迁移工具 (fastapi-augment)')
316
+ subparsers = parser.add_subparsers(dest='command', required=True, help='子命令')
317
+
318
+ # --- init ---
319
+ init_parser = subparsers.add_parser('init', help='初始化项目迁移环境')
320
+ init_parser.add_argument('--db-url', default='sqlite:///app.db', help='数据库连接 URL')
321
+ init_parser.add_argument('--project-dir', type=Path, default=None, help='项目根目录(默认为当前工作目录)')
322
+
323
+ # --- upgrade ---
324
+ up_parser = subparsers.add_parser('upgrade', help='执行数据库迁移')
325
+ up_parser.add_argument('--db-url', help='数据库连接 URL(不传则从 alembic.ini 读取)')
326
+ up_parser.add_argument('--revision', default='head', help='目标版本 (默认: head)')
327
+ up_parser.add_argument('--downgrade', action='store_true', help='降级而非升级')
328
+ up_parser.add_argument('--project-dir', type=Path, default=None, help='项目根目录(默认为当前工作目录)')
329
+
330
+ # --- generate ---
331
+ gen_parser = subparsers.add_parser('generate', help='生成迁移脚本')
332
+ gen_parser.add_argument('--message', required=True, help='迁移描述信息')
333
+ gen_parser.add_argument('--models', required=True, help='模型模块,多个用逗号分隔 (如 myapp.models)')
334
+ gen_parser.add_argument('--project-dir', type=Path, default=None, help='项目根目录(默认为当前工作目录)')
335
+
336
+ args = parser.parse_args()
337
+
338
+ try:
339
+ if args.command == 'init':
340
+ init_project(args.db_url, args.project_dir)
341
+ elif args.command == 'upgrade':
342
+ if args.downgrade:
343
+ downgrade(args.db_url, args.revision, args.project_dir)
344
+ else:
345
+ upgrade(args.db_url, args.revision, args.project_dir)
346
+ elif args.command == 'generate':
347
+ generate_migration(args.message, args.models, args.project_dir)
348
+ else:
349
+ parser.print_help()
350
+ except (FileNotFoundError, RuntimeError) as e:
351
+ _logger.error('%s', e)
352
+ sys.exit(1)
353
+
354
+
355
+ if __name__ == "__main__":
356
+ cli_main()
@@ -0,0 +1,18 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/8/31
4
+ @Description : 通用模型混入类 — 时间戳、软删除、审计,均支持细粒度组合。
5
+ """
6
+ from .audit import CreatedByMixin, UpdatedByMixin, AuditMixin
7
+ from .soft_delete import SoftDeleteMixin, SoftDeleteAuditMixin
8
+ from .timestamp import CreatedAtMixin, TimestampMixin
9
+
10
+ __all__ = [
11
+ 'CreatedByMixin',
12
+ 'UpdatedByMixin',
13
+ 'AuditMixin',
14
+ 'SoftDeleteMixin',
15
+ 'SoftDeleteAuditMixin',
16
+ 'CreatedAtMixin',
17
+ 'TimestampMixin',
18
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/1
4
+ @Description : 审计混入类,可整体或细粒度组合 created_by / updated_by 列。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from sqlalchemy import String
9
+ from sqlalchemy.orm import Mapped, mapped_column
10
+
11
+
12
+ class CreatedByMixin:
13
+ """创建人混入类
14
+
15
+ 仅添加 created_by 一列,记录创建操作人ID,
16
+ 默认对齐 ModelBase 的 ULID 主键长度(String(26))。
17
+ """
18
+
19
+ created_by: Mapped[str | None] = mapped_column(
20
+ String(26),
21
+ default=None,
22
+ comment='创建人ID',
23
+ sort_order=901
24
+ )
25
+
26
+
27
+ class UpdatedByMixin:
28
+ """更新人混入类
29
+
30
+ 仅添加 updated_by 一列,记录最近更新操作人ID。
31
+ """
32
+
33
+ updated_by: Mapped[str | None] = mapped_column(
34
+ String(26),
35
+ default=None,
36
+ comment='更新人ID',
37
+ sort_order=911
38
+ )
39
+
40
+
41
+ class AuditMixin(CreatedByMixin, UpdatedByMixin):
42
+ """审计混入类
43
+
44
+ 组合 CreatedByMixin + UpdatedByMixin,即常见的
45
+ created_by / updated_by 成对出现;只要其中一列时,
46
+ 直接继承对应的细粒度混入类::
47
+
48
+ class Article(CreatedByMixin, ModelBase): # 只记录创建人
49
+ ...
50
+
51
+ 两列均为可空:系统任务(无登录用户上下文)写入时留空;
52
+ 由 service 层从当前请求上下文取用户ID填充::
53
+
54
+ await crud.create(
55
+ session,
56
+ User(name='alice', created_by=current_user.id),
57
+ )
58
+ await crud.update_by_id(
59
+ session, uid, updated_by=current_user.id, **changes,
60
+ )
61
+ """
@@ -0,0 +1,80 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/1
4
+ @Description : 软删除混入类,提供 is_deleted / deleted_at 通用列。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+
10
+ from sqlalchemy import Boolean, DateTime, ColumnExpressionArgument, String
11
+ from sqlalchemy.orm import Mapped, mapped_column
12
+
13
+
14
+ class SoftDeleteMixin:
15
+ """软删除混入类
16
+
17
+ 为模型添加 is_deleted / deleted_at 两列,配合 CrudBase 使用::
18
+
19
+ # 标记删除(service 层负责)
20
+ await crud.update_by_id(
21
+ session, uid,
22
+ is_deleted=True,
23
+ deleted_at=datetime.now(timezone.utc),
24
+ )
25
+
26
+ # 只查询未删除数据
27
+ await crud.list(session, expressions=(User.not_deleted(),))
28
+
29
+ 注意:软删除只是应用层约定,数据库的唯一约束、外键等
30
+ 不会感知软删状态(软删行仍占用唯一键),需在业务层处理。
31
+ """
32
+
33
+ is_deleted: Mapped[bool] = mapped_column(
34
+ Boolean,
35
+ default=False,
36
+ index=True,
37
+ comment='是否已删除',
38
+ sort_order=920
39
+ )
40
+ deleted_at: Mapped[datetime | None] = mapped_column(
41
+ DateTime(timezone=True),
42
+ default=None,
43
+ comment='删除时间',
44
+ sort_order=921
45
+ )
46
+
47
+ @classmethod
48
+ def not_deleted(cls) -> ColumnExpressionArgument[bool]:
49
+ """未删除的过滤条件"""
50
+ return cls.is_deleted.is_(False)
51
+
52
+ @classmethod
53
+ def only_deleted(cls) -> ColumnExpressionArgument[bool]:
54
+ """已删除的过滤条件"""
55
+ return cls.is_deleted.is_(True)
56
+
57
+
58
+ class SoftDeleteAuditMixin(SoftDeleteMixin):
59
+ """带操作人审计的软删除扩展 Mixin
60
+
61
+ 在基础软删之上增加 deleted_by,记录是谁执行软删除。
62
+ 需要审计删除人的模型再继承本类,不要全局滥用。
63
+
64
+ 使用示例::
65
+ await crud.update_by_id(
66
+ session,
67
+ uid,
68
+ is_deleted=True,
69
+ deleted_at=datetime.now(timezone.utc),
70
+ deleted_by=operator_user_id,
71
+ )
72
+ """
73
+
74
+ # 根据你的主键类型调整:ULID / str / int,这里示例用 str,可改成 ULID
75
+ deleted_by: Mapped[str | None] = mapped_column(
76
+ String(26),
77
+ default=None,
78
+ comment='执行软删除操作人ID',
79
+ sort_order=922
80
+ )
@@ -0,0 +1,48 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/1
4
+ @Description : 时间戳混入类,可整体或细粒度组合 created_at / updated_at 列。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+
10
+ from sqlalchemy import DateTime, func
11
+ from sqlalchemy.orm import Mapped, mapped_column
12
+
13
+
14
+ class CreatedAtMixin:
15
+ """创建时间混入类
16
+
17
+ 仅添加 created_at 一列,插入时由数据库时钟生成,之后不再变化。
18
+ 适用于只增不改的表(日志、流水、快照等)。
19
+ """
20
+
21
+ created_at: Mapped[datetime] = mapped_column(
22
+ DateTime(timezone=True),
23
+ server_default=func.now(),
24
+ comment='创建时间',
25
+ sort_order=900
26
+ )
27
+
28
+
29
+ class TimestampMixin(CreatedAtMixin):
30
+ """时间戳混入类
31
+
32
+ 继承 CreatedAtMixin ,即常见的
33
+ created_at / updated_at 成对出现;只要其中一列时,
34
+ 直接继承对应的细粒度混入类::
35
+
36
+ class Log(CreatedAtMixin, ModelBase): # 只要创建时间
37
+ ...
38
+
39
+ class User(TimestampMixin, ModelBase): # 两列都要
40
+ ...
41
+ """
42
+ updated_at: Mapped[datetime] = mapped_column(
43
+ DateTime(timezone=True),
44
+ server_default=func.now(),
45
+ onupdate=func.now(),
46
+ comment='更新时间',
47
+ sort_order=910
48
+ )
@@ -0,0 +1,47 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/8/31
4
+ @Description : SQLAlchemy 模型基础类
5
+ """
6
+ from sqlalchemy import String
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+ from ulid import ULID
9
+
10
+ from .base import Base
11
+
12
+
13
+ def _generate_ulid() -> str:
14
+ """生成ULID
15
+
16
+ Returns:
17
+ ULID字符串
18
+ """
19
+ return str(ULID()).lower()
20
+
21
+
22
+ class ModelBase(Base):
23
+ """SQLAlchemy模型基础类
24
+
25
+ 仅提供通用的 ULID 主键字段;时间戳、软删除、审计等
26
+ 通用列通过 mixins 按需组合,成对的列可整体使用,
27
+ 也可用细粒度混入类只取其中一列:
28
+
29
+ from .mixins import (
30
+ CreatedAtMixin, SoftDeleteMixin, TimestampMixin,
31
+ )
32
+
33
+ class User(TimestampMixin, SoftDeleteMixin, ModelBase):
34
+ ... # created_at + updated_at
35
+
36
+ class Log(CreatedAtMixin, ModelBase): # 只要创建时间
37
+ ...
38
+ """
39
+ __abstract__ = True
40
+
41
+ id: Mapped[str] = mapped_column(
42
+ String(26),
43
+ default=_generate_ulid,
44
+ primary_key=True,
45
+ comment='主键ID',
46
+ sort_order=-1
47
+ )