fastapp-cli 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.
- fastapp_cli/__init__.py +3 -0
- fastapp_cli/create.py +157 -0
- fastapp_cli/main.py +33 -0
- fastapp_cli/naming.py +39 -0
- fastapp_cli/prompts.py +35 -0
- fastapp_cli/render.py +130 -0
- fastapp_cli/templates/__init__.py +1 -0
- fastapp_cli/templates/project/.env.development.example.j2 +21 -0
- fastapp_cli/templates/project/.env.example.j2 +29 -0
- fastapp_cli/templates/project/.env.j2 +27 -0
- fastapp_cli/templates/project/.gitignore +178 -0
- fastapp_cli/templates/project/.pre-commit-config.yaml.j2 +80 -0
- fastapp_cli/templates/project/.python-version.j2 +1 -0
- fastapp_cli/templates/project/Dockerfile.j2 +17 -0
- fastapp_cli/templates/project/Makefile.j2 +31 -0
- fastapp_cli/templates/project/README.md.j2 +68 -0
- fastapp_cli/templates/project/alembic/env.py.j2 +84 -0
- fastapp_cli/templates/project/alembic/script.py.mako +28 -0
- fastapp_cli/templates/project/alembic/versions/.gitkeep +0 -0
- fastapp_cli/templates/project/alembic.ini.j2 +50 -0
- fastapp_cli/templates/project/app/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/deps.py.j2 +33 -0
- fastapp_cli/templates/project/app/api/v1/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/health.py.j2 +33 -0
- fastapp_cli/templates/project/app/api/v1/endpoints/items.py.j2 +90 -0
- fastapp_cli/templates/project/app/api/v1/router.py.j2 +9 -0
- fastapp_cli/templates/project/app/core/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/core/celery_app.py.j2 +62 -0
- fastapp_cli/templates/project/app/core/config.py.j2 +105 -0
- fastapp_cli/templates/project/app/core/context_var.py.j2 +13 -0
- fastapp_cli/templates/project/app/core/database.py.j2 +50 -0
- fastapp_cli/templates/project/app/core/exceptions.py.j2 +175 -0
- fastapp_cli/templates/project/app/core/logging.py.j2 +125 -0
- fastapp_cli/templates/project/app/core/middleware.py.j2 +39 -0
- fastapp_cli/templates/project/app/crud/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/crud/base.py.j2 +229 -0
- fastapp_cli/templates/project/app/crud/item.py.j2 +10 -0
- fastapp_cli/templates/project/app/main.py.j2 +118 -0
- fastapp_cli/templates/project/app/models/__init__.py.j2 +10 -0
- fastapp_cli/templates/project/app/models/base.py.j2 +59 -0
- fastapp_cli/templates/project/app/models/item.py.j2 +22 -0
- fastapp_cli/templates/project/app/schemas/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/schemas/common.py.j2 +81 -0
- fastapp_cli/templates/project/app/schemas/item.py.j2 +35 -0
- fastapp_cli/templates/project/app/services/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/services/base.py.j2 +79 -0
- fastapp_cli/templates/project/app/services/item_service.py.j2 +10 -0
- fastapp_cli/templates/project/app/tasks/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/app/tasks/sample_tasks.py.j2 +28 -0
- fastapp_cli/templates/project/app/utils/__init__.py.j2 +1 -0
- fastapp_cli/templates/project/docs/SQLModel/345/256/232/344/271/211/347/244/272/344/276/213.md +400 -0
- fastapp_cli/templates/project/pm2.config.json.j2 +47 -0
- fastapp_cli/templates/project/pyproject.toml.j2 +195 -0
- fastapp_cli/templates/project/scripts/celery_beat.sh.j2 +9 -0
- fastapp_cli/templates/project/scripts/celery_flower.sh.j2 +22 -0
- fastapp_cli/templates/project/scripts/celery_worker.sh.j2 +15 -0
- fastapp_cli/templates/project/scripts/start.sh.j2 +17 -0
- fastapp_cli/templates/project/tests/api/test_health.py.j2 +15 -0
- fastapp_cli/templates/project/tests/api/test_items.py.j2 +61 -0
- fastapp_cli/templates/project/tests/conftest.py.j2 +61 -0
- fastapp_cli/templates/project/tests/services/test_item_service.py.j2 +44 -0
- fastapp_cli-0.1.0.dist-info/METADATA +102 -0
- fastapp_cli-0.1.0.dist-info/RECORD +67 -0
- fastapp_cli-0.1.0.dist-info/WHEEL +4 -0
- fastapp_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Pre-commit 配置
|
|
2
|
+
# 官方文档: https://pre-commit.com/
|
|
3
|
+
#
|
|
4
|
+
# 快速开始:
|
|
5
|
+
# uv sync --group dev # 安装含 pre-commit 的 dev 依赖
|
|
6
|
+
# uv run pre-commit install # 安装 git hook
|
|
7
|
+
# uv run pre-commit run --all-files # 对全量文件执行一次
|
|
8
|
+
#
|
|
9
|
+
# 升级所有 hook 版本:
|
|
10
|
+
# uv run pre-commit autoupdate
|
|
11
|
+
|
|
12
|
+
default_language_version:
|
|
13
|
+
python: python{{ python_version }}
|
|
14
|
+
|
|
15
|
+
# 对于较慢的 hook(例如 mypy),默认仅在 pre-push 阶段运行
|
|
16
|
+
default_stages: [pre-commit]
|
|
17
|
+
|
|
18
|
+
# 全局排除目录/文件
|
|
19
|
+
exclude: |
|
|
20
|
+
(?x)^(
|
|
21
|
+
alembic/versions/.*|
|
|
22
|
+
\.venv/.*|
|
|
23
|
+
build/.*|
|
|
24
|
+
dist/.*|
|
|
25
|
+
uv\.lock
|
|
26
|
+
)$
|
|
27
|
+
|
|
28
|
+
repos:
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# 通用基础检查(空白字符、文件末尾、大文件、合法 YAML/TOML 等)
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
33
|
+
rev: v6.0.0
|
|
34
|
+
hooks:
|
|
35
|
+
- id: trailing-whitespace
|
|
36
|
+
args: [--markdown-linebreak-ext=md]
|
|
37
|
+
- id: end-of-file-fixer
|
|
38
|
+
- id: mixed-line-ending
|
|
39
|
+
args: [--fix=lf]
|
|
40
|
+
- id: check-yaml
|
|
41
|
+
- id: check-toml
|
|
42
|
+
- id: check-json
|
|
43
|
+
- id: check-added-large-files
|
|
44
|
+
args: [--maxkb=1024]
|
|
45
|
+
- id: check-merge-conflict
|
|
46
|
+
- id: check-case-conflict
|
|
47
|
+
- id: debug-statements
|
|
48
|
+
- id: detect-private-key
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Ruff: linter + formatter(替代 flake8 / isort / black / pyupgrade)
|
|
52
|
+
# 规则与 pyproject.toml 的 [tool.ruff] 配置一致
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
55
|
+
rev: v0.15.12
|
|
56
|
+
hooks:
|
|
57
|
+
# 先执行 ruff 检查并自动修复
|
|
58
|
+
- id: ruff
|
|
59
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
60
|
+
# 再执行 ruff 格式化
|
|
61
|
+
- id: ruff-format
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Mypy: 静态类型检查
|
|
65
|
+
# 规则与 pyproject.toml 的 [tool.mypy] 配置一致
|
|
66
|
+
# 说明:mypy 较慢,默认仅在 pre-push 阶段运行;如需每次提交都跑,删掉 stages 即可
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
69
|
+
rev: v1.20.2
|
|
70
|
+
hooks:
|
|
71
|
+
- id: mypy
|
|
72
|
+
stages: [pre-push]
|
|
73
|
+
files: ^app/
|
|
74
|
+
args: [--config-file=pyproject.toml]
|
|
75
|
+
additional_dependencies:
|
|
76
|
+
- pydantic>=2.0
|
|
77
|
+
- pydantic-settings>=2.0
|
|
78
|
+
- sqlmodel>=0.0.38
|
|
79
|
+
- fastapi>=0.115
|
|
80
|
+
- types-pymysql
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ python_version }}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% if use_docker %}
|
|
2
|
+
# FastAPI 服务镜像:基于 uv 官方镜像
|
|
3
|
+
FROM ghcr.io/astral-sh/uv:python{{ python_version }}-bookworm-slim
|
|
4
|
+
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
# 先拷贝依赖清单以利用镜像层缓存
|
|
8
|
+
COPY pyproject.toml ./
|
|
9
|
+
RUN uv sync --no-dev --no-install-project || uv sync --no-dev
|
|
10
|
+
|
|
11
|
+
# 拷贝项目代码
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
EXPOSE 8000
|
|
15
|
+
|
|
16
|
+
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
17
|
+
{% endif %}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.PHONY: sync dev test lint format migrate revision
|
|
2
|
+
|
|
3
|
+
# 安装依赖(含 dev 组)
|
|
4
|
+
sync:
|
|
5
|
+
uv sync --group dev
|
|
6
|
+
|
|
7
|
+
# 本地开发启动(热重载)
|
|
8
|
+
dev:
|
|
9
|
+
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
|
10
|
+
|
|
11
|
+
# 运行测试
|
|
12
|
+
test:
|
|
13
|
+
uv run pytest
|
|
14
|
+
|
|
15
|
+
# 代码检查(lint + 类型检查)
|
|
16
|
+
lint:
|
|
17
|
+
uv run ruff check .
|
|
18
|
+
uv run mypy app
|
|
19
|
+
|
|
20
|
+
# 代码格式化
|
|
21
|
+
format:
|
|
22
|
+
uv run ruff check --fix .
|
|
23
|
+
uv run ruff format .
|
|
24
|
+
|
|
25
|
+
# 执行数据库迁移
|
|
26
|
+
migrate:
|
|
27
|
+
uv run alembic upgrade head
|
|
28
|
+
|
|
29
|
+
# 生成迁移脚本:make revision m="add xxx table"
|
|
30
|
+
revision:
|
|
31
|
+
uv run alembic revision --autogenerate -m "$(m)"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# {{ project_name }}
|
|
2
|
+
|
|
3
|
+
> {{ project_description }}
|
|
4
|
+
> 由 [fastapp-cli](https://pypi.org/project/fastapp-cli/) v{{ fastapp_cli_version }} 生成于 {{ year }},作者:{{ author_name }}
|
|
5
|
+
|
|
6
|
+
FastAPI + SQLModel + Celery 工程模板,含分层架构、统一响应 / 异常 / TraceID / 日志、分页 CRUD、Alembic 迁移与完整测试。
|
|
7
|
+
|
|
8
|
+
## 快速开始
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv sync --group dev
|
|
12
|
+
make dev # 或 uv run uvicorn app.main:app --reload
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- 接口文档:http://localhost:8000/api/v1/docs
|
|
16
|
+
- 健康检查:http://localhost:8000/api/v1/health
|
|
17
|
+
|
|
18
|
+
## 降级行为说明
|
|
19
|
+
|
|
20
|
+
| 组件 | 默认 | 缺席时行为 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| MySQL | `.env` 中配置 | 启动不阻塞,`/api/v1/health` 返回 `database: "down"`,数据接口统一返回 500 |
|
|
23
|
+
| Redis (Celery) | `redis://localhost:6379` | 启动不阻塞,自动降级为 `memory://` broker 并输出告警日志 |
|
|
24
|
+
|
|
25
|
+
生产环境只需在 `.env` 配置真实 MySQL / Redis 地址,无需改代码。
|
|
26
|
+
|
|
27
|
+
## 常用命令
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
make dev # 本地开发启动(热重载)
|
|
31
|
+
make test # 运行测试(sqlite 内存库,无需 MySQL)
|
|
32
|
+
make lint # ruff + mypy
|
|
33
|
+
make format # ruff 格式化
|
|
34
|
+
make migrate # alembic upgrade head
|
|
35
|
+
make revision m="add xxx" # 生成迁移脚本
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Celery:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bash scripts/celery_worker.sh # worker
|
|
42
|
+
bash scripts/celery_beat.sh # 定时任务
|
|
43
|
+
bash scripts/celery_flower.sh # 监控面板(:5555)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 目录结构
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
app/
|
|
50
|
+
├── main.py # 工厂函数 create_app + lifespan(DB/redis 探测降级)
|
|
51
|
+
├── core/ # 配置、数据库、日志、异常、中间件、Celery
|
|
52
|
+
├── api/
|
|
53
|
+
│ ├── deps.py # DBSession / CurrentUser(认证占位)
|
|
54
|
+
│ └── v1/endpoints/ # health / items(分页 CRUD 示例)
|
|
55
|
+
├── models/ # SQLModel 模型(建模参考 docs/SQLModel定义示例.md)
|
|
56
|
+
├── schemas/ # Pydantic Schema(统一响应、过滤器)
|
|
57
|
+
├── crud/ # CRUD 泛型基类 + 业务实现
|
|
58
|
+
├── services/ # Service 层
|
|
59
|
+
├── tasks/ # Celery 任务
|
|
60
|
+
└── utils/ # 工具扩展位
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 扩展指南
|
|
64
|
+
|
|
65
|
+
1. **新增业务域**:仿照 `item` 五件套(model / schema / crud / service / endpoint),在 `models/__init__.py` 注册新模型,在 `api/v1/router.py` 挂载新路由;
|
|
66
|
+
2. **生成迁移**:`make revision m="add xxx table"` 后检查 `alembic/versions/`,再 `make migrate`;
|
|
67
|
+
3. **接入真实认证**:替换 `app/api/deps.py::get_current_user` 的占位实现;
|
|
68
|
+
4. **新增 Celery 任务**:在 `app/tasks/` 中使用 `@celery_app.task` 装饰器编写。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Alembic 迁移环境配置."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from alembic import context
|
|
10
|
+
from sqlalchemy import engine_from_config, pool
|
|
11
|
+
from sqlmodel import SQLModel
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
14
|
+
|
|
15
|
+
from app.core.config import settings
|
|
16
|
+
|
|
17
|
+
# 导入所有 model 以便 SQLModel.metadata 包含全部表
|
|
18
|
+
from app import models # noqa: F401
|
|
19
|
+
|
|
20
|
+
config = context.config
|
|
21
|
+
if config.config_file_name is not None:
|
|
22
|
+
fileConfig(config.config_file_name)
|
|
23
|
+
|
|
24
|
+
# 注入数据库链接
|
|
25
|
+
config.set_main_option("sqlalchemy.url", str(settings.DATABASE_URI))
|
|
26
|
+
|
|
27
|
+
target_metadata = SQLModel.metadata
|
|
28
|
+
|
|
29
|
+
# 只允许 alembic 管理的表名集合:仅包含 app/models 中显式定义的表
|
|
30
|
+
# 其他存量表(历史遗留 / 跨项目共用)一律忽略,避免被 autogenerate 误判为应删除
|
|
31
|
+
MANAGED_TABLES = set(target_metadata.tables.keys())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def include_object(object_, name, type_, reflected, compare_to):
|
|
35
|
+
"""Alembic autogenerate 过滤钩子.
|
|
36
|
+
|
|
37
|
+
- 表对象:仅当表在 SQLModel.metadata 中已声明(即本项目管理的表)时才参与对比,
|
|
38
|
+
其余表视为外部表,跳过 (不会被 drop)。
|
|
39
|
+
- 索引/约束等子对象:若所属表不在管理范围内,则一并忽略。
|
|
40
|
+
"""
|
|
41
|
+
if type_ == "table":
|
|
42
|
+
return name in MANAGED_TABLES
|
|
43
|
+
# 非表对象:通过其归属表过滤
|
|
44
|
+
parent_table = getattr(object_, "table", None)
|
|
45
|
+
return not (parent_table is not None and parent_table.name not in MANAGED_TABLES)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_migrations_offline() -> None:
|
|
49
|
+
"""离线模式运行迁移."""
|
|
50
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
51
|
+
context.configure(
|
|
52
|
+
url=url,
|
|
53
|
+
target_metadata=target_metadata,
|
|
54
|
+
literal_binds=True,
|
|
55
|
+
dialect_opts={"paramstyle": "named"},
|
|
56
|
+
compare_type=True,
|
|
57
|
+
include_object=include_object,
|
|
58
|
+
)
|
|
59
|
+
with context.begin_transaction():
|
|
60
|
+
context.run_migrations()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_migrations_online() -> None:
|
|
64
|
+
"""在线模式运行迁移."""
|
|
65
|
+
connectable = engine_from_config(
|
|
66
|
+
config.get_section(config.config_ini_section, {}),
|
|
67
|
+
prefix="sqlalchemy.",
|
|
68
|
+
poolclass=pool.NullPool,
|
|
69
|
+
)
|
|
70
|
+
with connectable.connect() as connection:
|
|
71
|
+
context.configure(
|
|
72
|
+
connection=connection,
|
|
73
|
+
target_metadata=target_metadata,
|
|
74
|
+
compare_type=True,
|
|
75
|
+
include_object=include_object,
|
|
76
|
+
)
|
|
77
|
+
with context.begin_transaction():
|
|
78
|
+
context.run_migrations()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if context.is_offline_mode():
|
|
82
|
+
run_migrations_offline()
|
|
83
|
+
else:
|
|
84
|
+
run_migrations_online()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""${message}.
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
import sqlmodel
|
|
14
|
+
${imports if imports else ""}
|
|
15
|
+
|
|
16
|
+
# revision identifiers, used by Alembic.
|
|
17
|
+
revision: str = ${repr(up_revision)}
|
|
18
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
19
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
20
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def upgrade() -> None:
|
|
24
|
+
${upgrades if upgrades else "pass"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def downgrade() -> None:
|
|
28
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Alembic 迁移配置:数据库连接串由 alembic/env.py 从 app.core.config 读取,这里留空
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# path to migration scripts
|
|
5
|
+
script_location = alembic
|
|
6
|
+
|
|
7
|
+
# template used to generate migration file names; default is %%(rev)s_%%(slug)s
|
|
8
|
+
# file_template = %%(rev)s_%%(slug)s
|
|
9
|
+
|
|
10
|
+
# timezone to use when rendering the date within the migration file
|
|
11
|
+
# timezone =
|
|
12
|
+
|
|
13
|
+
sqlalchemy.url =
|
|
14
|
+
|
|
15
|
+
[post_write_hooks]
|
|
16
|
+
# 此处可配置生成迁移文件后的自动格式化钩子
|
|
17
|
+
|
|
18
|
+
[loggers]
|
|
19
|
+
keys = root,sqlalchemy,alembic
|
|
20
|
+
|
|
21
|
+
[handlers]
|
|
22
|
+
keys = console
|
|
23
|
+
|
|
24
|
+
[formatters]
|
|
25
|
+
keys = generic
|
|
26
|
+
|
|
27
|
+
[logger_root]
|
|
28
|
+
level = WARN
|
|
29
|
+
handlers = console
|
|
30
|
+
qualname =
|
|
31
|
+
|
|
32
|
+
[logger_sqlalchemy]
|
|
33
|
+
level = WARN
|
|
34
|
+
handlers =
|
|
35
|
+
qualname = sqlalchemy.engine
|
|
36
|
+
|
|
37
|
+
[logger_alembic]
|
|
38
|
+
level = INFO
|
|
39
|
+
handlers =
|
|
40
|
+
qualname = alembic
|
|
41
|
+
|
|
42
|
+
[handler_console]
|
|
43
|
+
class = StreamHandler
|
|
44
|
+
args = (sys.stderr,)
|
|
45
|
+
level = NOTSET
|
|
46
|
+
formatter = generic
|
|
47
|
+
|
|
48
|
+
[formatter_generic]
|
|
49
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
50
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app · api"""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""API 全局依赖."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, Request
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from sqlmodel import Session
|
|
10
|
+
|
|
11
|
+
from app.core.config import settings
|
|
12
|
+
from app.core.database import get_session
|
|
13
|
+
from app.core.exceptions import UnauthorizedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_current_user(request: Request) -> str:
|
|
17
|
+
"""获取当前登录用户(认证占位).
|
|
18
|
+
|
|
19
|
+
非生产环境直接返回 settings.MOCK_USER,便于本地开发与测试;
|
|
20
|
+
生产环境抛出 UnauthorizedError。接入真实认证时在此处替换实现,
|
|
21
|
+
参考:从请求头解析 token -> 校验 -> 返回用户标识。
|
|
22
|
+
|
|
23
|
+
:raises UnauthorizedError: 生产环境下未接入认证
|
|
24
|
+
"""
|
|
25
|
+
_ = request.headers # 接入真实认证时从 headers 解析凭证
|
|
26
|
+
if not settings.is_production:
|
|
27
|
+
return settings.MOCK_USER
|
|
28
|
+
logger.warning("生产环境尚未接入认证实现,拒绝访问")
|
|
29
|
+
raise UnauthorizedError("Invalid Auth")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
CurrentUser = Annotated[str, Depends(get_current_user)]
|
|
33
|
+
DBSession = Annotated[Session, Depends(get_session)]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app · api · v1"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app · api · v1 · endpoints"""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""健康检查端点."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
from sqlalchemy import text
|
|
7
|
+
from sqlmodel import Session
|
|
8
|
+
|
|
9
|
+
from app.core.config import settings
|
|
10
|
+
from app.core.database import engine
|
|
11
|
+
|
|
12
|
+
router = APIRouter()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _database_status() -> str:
|
|
16
|
+
"""探测数据库连通性,返回 up / down."""
|
|
17
|
+
try:
|
|
18
|
+
with Session(engine) as session:
|
|
19
|
+
session.execute(text("SELECT 1"))
|
|
20
|
+
return "up"
|
|
21
|
+
except Exception: # pylint: disable=broad-except
|
|
22
|
+
return "down"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("", summary="健康检查")
|
|
26
|
+
def health() -> dict:
|
|
27
|
+
"""健康检查:应用状态 + 数据库连通状态."""
|
|
28
|
+
return {
|
|
29
|
+
"status": "ok",
|
|
30
|
+
"app": settings.APP_NAME,
|
|
31
|
+
"env": settings.APP_ENV,
|
|
32
|
+
"database": _database_status(),
|
|
33
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Items 分页 CRUD 示例端点(demo 业务域,可替换为实际业务接口)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends
|
|
8
|
+
from fastapi_filter import FilterDepends
|
|
9
|
+
|
|
10
|
+
from app.api.deps import CurrentUser, DBSession
|
|
11
|
+
from app.crud.item import CRUDItem
|
|
12
|
+
from app.models.item import Item
|
|
13
|
+
from app.schemas.common import PageResponse, Response
|
|
14
|
+
from app.schemas.item import ItemCreate, ItemFilter, ItemRead, ItemUpdate
|
|
15
|
+
from app.services.item_service import ItemService
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_service(db: DBSession) -> ItemService:
|
|
21
|
+
return ItemService(db, CRUDItem(Item))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ItemServiceDep = Annotated[ItemService, Depends(get_service)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get(
|
|
28
|
+
"",
|
|
29
|
+
response_model=PageResponse[ItemRead],
|
|
30
|
+
summary="分页查询 Item",
|
|
31
|
+
)
|
|
32
|
+
def list_items(
|
|
33
|
+
filter_params: Annotated[ItemFilter, FilterDepends(ItemFilter, by_alias=True)],
|
|
34
|
+
service: ItemServiceDep,
|
|
35
|
+
):
|
|
36
|
+
return service.list_page_objs(filter_params)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.post(
|
|
40
|
+
"",
|
|
41
|
+
response_model=Response[ItemRead],
|
|
42
|
+
summary="创建 Item",
|
|
43
|
+
)
|
|
44
|
+
def create_item(
|
|
45
|
+
payload: ItemCreate,
|
|
46
|
+
service: ItemServiceDep,
|
|
47
|
+
current_user: CurrentUser,
|
|
48
|
+
) -> Response[ItemRead]:
|
|
49
|
+
data: ItemRead = service.create(payload)
|
|
50
|
+
return Response(data=data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get(
|
|
54
|
+
"/{item_id}",
|
|
55
|
+
response_model=Response[ItemRead],
|
|
56
|
+
summary="获取单个 Item",
|
|
57
|
+
)
|
|
58
|
+
def get_item(
|
|
59
|
+
item_id: int,
|
|
60
|
+
service: ItemServiceDep,
|
|
61
|
+
) -> Response[ItemRead]:
|
|
62
|
+
data: ItemRead = service.get(item_id)
|
|
63
|
+
return Response(data=data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.patch(
|
|
67
|
+
"/{item_id}",
|
|
68
|
+
response_model=Response[ItemRead],
|
|
69
|
+
summary="更新 Item",
|
|
70
|
+
)
|
|
71
|
+
def update_item(
|
|
72
|
+
item_id: int,
|
|
73
|
+
payload: ItemUpdate,
|
|
74
|
+
service: ItemServiceDep,
|
|
75
|
+
) -> Response[ItemRead]:
|
|
76
|
+
data: ItemRead = service.update(item_id, payload)
|
|
77
|
+
return Response(data=data)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.delete(
|
|
81
|
+
"/{item_id}",
|
|
82
|
+
response_model=Response[ItemRead],
|
|
83
|
+
summary="删除 Item",
|
|
84
|
+
)
|
|
85
|
+
def delete_item(
|
|
86
|
+
item_id: int,
|
|
87
|
+
service: ItemServiceDep,
|
|
88
|
+
) -> Response[ItemRead]:
|
|
89
|
+
data: ItemRead = service.delete(item_id)
|
|
90
|
+
return Response(data=data)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""v1 总路由."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from app.api.v1.endpoints import health, items
|
|
6
|
+
|
|
7
|
+
api_router = APIRouter()
|
|
8
|
+
api_router.include_router(items.router, prefix="/items", tags=["items"])
|
|
9
|
+
api_router.include_router(health.router, prefix="/health", tags=["health"])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""app · core"""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Celery 应用实例.
|
|
2
|
+
|
|
3
|
+
主应用与 worker 共用 ``build_celery_app`` 构造入口:
|
|
4
|
+
|
|
5
|
+
- 默认 broker/backend 来自 settings(redis);
|
|
6
|
+
- Web 应用在 lifespan 中探测 redis,不可达时自动降级为 memory(见 app/main.py);
|
|
7
|
+
- worker 启动入口固定使用本模块的 ``celery_app``(生产环境请确保 redis 可达)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from celery import Celery
|
|
13
|
+
|
|
14
|
+
from app.core.config import settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_celery_app(broker_url: str, backend_url: str, *, name: str = "worker") -> Celery:
|
|
18
|
+
"""构造 Celery 实例.
|
|
19
|
+
|
|
20
|
+
:param broker_url: broker 地址(redis:// 或 memory://)
|
|
21
|
+
:param backend_url: result backend 地址
|
|
22
|
+
:param name: Celery 应用名
|
|
23
|
+
:return: 配置完成的 Celery 实例
|
|
24
|
+
"""
|
|
25
|
+
celery = Celery(name, broker=broker_url, backend=backend_url)
|
|
26
|
+
celery.conf.update(
|
|
27
|
+
task_default_queue=settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
28
|
+
task_serializer="json",
|
|
29
|
+
result_serializer="json",
|
|
30
|
+
accept_content=["json"],
|
|
31
|
+
timezone="Asia/Shanghai",
|
|
32
|
+
enable_utc=False,
|
|
33
|
+
task_track_started=True,
|
|
34
|
+
task_time_limit=60 * 60,
|
|
35
|
+
task_soft_time_limit=50 * 60,
|
|
36
|
+
worker_max_tasks_per_child=200,
|
|
37
|
+
worker_prefetch_multiplier=4,
|
|
38
|
+
broker_connection_retry_on_startup=True,
|
|
39
|
+
)
|
|
40
|
+
return celery
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def degrade_to_memory(celery: Celery) -> None:
|
|
44
|
+
"""将 Celery 降级为内存 broker/backend(本地无 redis 时使用)."""
|
|
45
|
+
celery.conf.broker_url = "memory://"
|
|
46
|
+
celery.conf.result_backend = "cache+memory://"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
celery_app = build_celery_app(
|
|
50
|
+
settings.CELERY_BROKER_URL,
|
|
51
|
+
settings.CELERY_RESULT_BACKEND,
|
|
52
|
+
name="{{ project_name | replace('-', '_') }}_worker",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 定时任务示例(如不需要可删除)
|
|
56
|
+
# from celery.schedules import crontab
|
|
57
|
+
celery_app.conf.beat_schedule = {
|
|
58
|
+
# "daily-report": {
|
|
59
|
+
# "task": "app.tasks.sample_tasks.sample_task",
|
|
60
|
+
# "schedule": crontab(hour=2, minute=0),
|
|
61
|
+
# },
|
|
62
|
+
}
|