fastapp-cli 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/PKG-INFO +8 -6
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/README.md +7 -5
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/pyproject.toml +1 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/__init__.py +1 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/create.py +8 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/prompts.py +8 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.env.development.example.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.env.example.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.env.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/README.md.j2 +9 -5
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/celery_app.py.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/config.py.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/main.py.j2 +11 -7
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/tasks/sample_tasks.py.j2 +2 -1
- fastapp_cli-0.3.0/src/fastapp_cli/templates/project/docs/agent_guide.md.j2 +307 -0
- fastapp_cli-0.3.0/src/fastapp_cli/templates/project/pm2.config.js.j2 +50 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/pyproject.toml.j2 +6 -6
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/celery_beat.sh.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/celery_flower.sh.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/celery_worker.sh.j2 +2 -1
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/tests/test_create.py +29 -6
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/tests/test_render.py +47 -3
- fastapp_cli-0.1.0/src/fastapp_cli/templates/project/pm2.config.json.j2 +0 -47
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/.gitignore +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/PLAN.md +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/main.py +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/naming.py +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/render.py +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/__init__.py +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.gitignore +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.pre-commit-config.yaml.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.python-version.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/Dockerfile.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/Makefile.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic/env.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic/script.py.mako +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic/versions/.gitkeep +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic.ini.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/deps.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/endpoints/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/endpoints/health.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/endpoints/items.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/router.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/context_var.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/database.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/exceptions.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/logging.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/middleware.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/base.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/item.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/base.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/item.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/common.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/item.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/services/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/services/base.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/services/item_service.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/tasks/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/utils/__init__.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/docs/SQLModel/345/256/232/344/271/211/347/244/272/344/276/213.md" +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/start.sh.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/api/test_health.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/api/test_items.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/conftest.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/services/test_item_service.py.j2 +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/tests/test_naming.py +0 -0
- {fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: FastAPI 项目脚手架:一条命令生成完整可运行的 FastAPI 工程
|
|
5
5
|
Author: aidenmo
|
|
6
6
|
License: MIT
|
|
@@ -31,11 +31,12 @@ fastapp create my-server
|
|
|
31
31
|
- **分层架构**:`api / schemas / crud / services / models` 分层,内置泛型 CRUD / Service 基类
|
|
32
32
|
- **开箱即用的工程能力**:统一响应 / 异常体系、TraceID 中间件、loguru 日志、pydantic-settings 多环境配置
|
|
33
33
|
- **分页 CRUD + 过滤**:基于 fastapi-pagination 与 fastapi-filter 的完整示例(`/api/v1/items`)
|
|
34
|
-
- **Celery
|
|
34
|
+
- **Celery 可选**:`--celery` 启用时生成完整任务骨架(默认 redis broker,本地无 redis 时启动自动降级 `memory://` 并告警);默认不启用
|
|
35
35
|
- **Alembic 迁移**:接入 SQLModel.metadata,含 autogenerate 过滤钩子
|
|
36
36
|
- **优雅降级**:MySQL 缺席不阻塞启动,`/api/v1/health` 返回 `database: up/down`
|
|
37
37
|
- **工程化配套**:Makefile、scripts/、pre-commit、ruff + mypy + pytest 配置全套
|
|
38
|
-
- **可选部署文件**:`--pm2` / `--docker`
|
|
38
|
+
- **可选部署文件**:`--pm2` / `--docker` 按需生成(pm2.config.js,celery 未启用时对应启动命令自动注释)
|
|
39
|
+
- **AI Agent 友好**:生成项目自带 `docs/agent_guide.md` 开发参考文档(架构、核心方法清单、扩展流程)
|
|
39
40
|
|
|
40
41
|
## 安装
|
|
41
42
|
|
|
@@ -60,8 +61,9 @@ fastapp create <project_name> [options]
|
|
|
60
61
|
|---|---|---|
|
|
61
62
|
| `project_name` | 必填 | 项目名;小写字母/数字/连字符,如 `my-server` |
|
|
62
63
|
| `--description` | 交互提问 | 项目描述 |
|
|
63
|
-
| `--pm2 / --no-pm2` | 交互提问 | 是否生成 `pm2.config.
|
|
64
|
+
| `--pm2 / --no-pm2` | 交互提问 | 是否生成 `pm2.config.js` |
|
|
64
65
|
| `--docker / --no-docker` | 交互提问 | 是否生成 `Dockerfile` |
|
|
66
|
+
| `--celery / --no-celery` | 交互提问(默认不启用) | 是否启用 Celery 异步任务 |
|
|
65
67
|
| `--python` | `3.12` | 目标 Python 版本(写入 `.python-version`) |
|
|
66
68
|
| `--author` | `git config user.name` | 作者署名 |
|
|
67
69
|
| `--force` | 关 | 目标目录已存在时覆盖重建 |
|
|
@@ -70,7 +72,7 @@ fastapp create <project_name> [options]
|
|
|
70
72
|
全部关键项通过 flag 提供时零交互:
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
fastapp create my-server --description "demo" --no-pm2 --no-docker
|
|
75
|
+
fastapp create my-server --description "demo" --no-pm2 --no-docker --no-celery
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
生成完成后:
|
|
@@ -82,7 +84,7 @@ make dev # 访问 /api/v1/docs
|
|
|
82
84
|
|
|
83
85
|
## 生成项目依赖
|
|
84
86
|
|
|
85
|
-
fastapi、sqlmodel、alembic、
|
|
87
|
+
fastapi、sqlmodel、alembic、fastapi-pagination、fastapi-filter、loguru、pydantic-settings 等,dev 组含 pytest / ruff / mypy / pre-commit;`--celery` 启用时额外引入 celery[redis] 与 flower。
|
|
86
88
|
|
|
87
89
|
## 开发 fastapp-cli 自身
|
|
88
90
|
|
|
@@ -11,11 +11,12 @@ fastapp create my-server
|
|
|
11
11
|
- **分层架构**:`api / schemas / crud / services / models` 分层,内置泛型 CRUD / Service 基类
|
|
12
12
|
- **开箱即用的工程能力**:统一响应 / 异常体系、TraceID 中间件、loguru 日志、pydantic-settings 多环境配置
|
|
13
13
|
- **分页 CRUD + 过滤**:基于 fastapi-pagination 与 fastapi-filter 的完整示例(`/api/v1/items`)
|
|
14
|
-
- **Celery
|
|
14
|
+
- **Celery 可选**:`--celery` 启用时生成完整任务骨架(默认 redis broker,本地无 redis 时启动自动降级 `memory://` 并告警);默认不启用
|
|
15
15
|
- **Alembic 迁移**:接入 SQLModel.metadata,含 autogenerate 过滤钩子
|
|
16
16
|
- **优雅降级**:MySQL 缺席不阻塞启动,`/api/v1/health` 返回 `database: up/down`
|
|
17
17
|
- **工程化配套**:Makefile、scripts/、pre-commit、ruff + mypy + pytest 配置全套
|
|
18
|
-
- **可选部署文件**:`--pm2` / `--docker`
|
|
18
|
+
- **可选部署文件**:`--pm2` / `--docker` 按需生成(pm2.config.js,celery 未启用时对应启动命令自动注释)
|
|
19
|
+
- **AI Agent 友好**:生成项目自带 `docs/agent_guide.md` 开发参考文档(架构、核心方法清单、扩展流程)
|
|
19
20
|
|
|
20
21
|
## 安装
|
|
21
22
|
|
|
@@ -40,8 +41,9 @@ fastapp create <project_name> [options]
|
|
|
40
41
|
|---|---|---|
|
|
41
42
|
| `project_name` | 必填 | 项目名;小写字母/数字/连字符,如 `my-server` |
|
|
42
43
|
| `--description` | 交互提问 | 项目描述 |
|
|
43
|
-
| `--pm2 / --no-pm2` | 交互提问 | 是否生成 `pm2.config.
|
|
44
|
+
| `--pm2 / --no-pm2` | 交互提问 | 是否生成 `pm2.config.js` |
|
|
44
45
|
| `--docker / --no-docker` | 交互提问 | 是否生成 `Dockerfile` |
|
|
46
|
+
| `--celery / --no-celery` | 交互提问(默认不启用) | 是否启用 Celery 异步任务 |
|
|
45
47
|
| `--python` | `3.12` | 目标 Python 版本(写入 `.python-version`) |
|
|
46
48
|
| `--author` | `git config user.name` | 作者署名 |
|
|
47
49
|
| `--force` | 关 | 目标目录已存在时覆盖重建 |
|
|
@@ -50,7 +52,7 @@ fastapp create <project_name> [options]
|
|
|
50
52
|
全部关键项通过 flag 提供时零交互:
|
|
51
53
|
|
|
52
54
|
```bash
|
|
53
|
-
fastapp create my-server --description "demo" --no-pm2 --no-docker
|
|
55
|
+
fastapp create my-server --description "demo" --no-pm2 --no-docker --no-celery
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
生成完成后:
|
|
@@ -62,7 +64,7 @@ make dev # 访问 /api/v1/docs
|
|
|
62
64
|
|
|
63
65
|
## 生成项目依赖
|
|
64
66
|
|
|
65
|
-
fastapi、sqlmodel、alembic、
|
|
67
|
+
fastapi、sqlmodel、alembic、fastapi-pagination、fastapi-filter、loguru、pydantic-settings 等,dev 组含 pytest / ruff / mypy / pre-commit;`--celery` 启用时额外引入 celery[redis] 与 flower。
|
|
66
68
|
|
|
67
69
|
## 开发 fastapp-cli 自身
|
|
68
70
|
|
|
@@ -68,13 +68,18 @@ def create(
|
|
|
68
68
|
pm2: bool = typer.Option(
|
|
69
69
|
None,
|
|
70
70
|
"--pm2/--no-pm2",
|
|
71
|
-
help="是否生成 pm2.config.
|
|
71
|
+
help="是否生成 pm2.config.js(未指定时交互提问)",
|
|
72
72
|
),
|
|
73
73
|
docker: bool = typer.Option(
|
|
74
74
|
None,
|
|
75
75
|
"--docker/--no-docker",
|
|
76
76
|
help="是否生成 Dockerfile(未指定时交互提问)",
|
|
77
77
|
),
|
|
78
|
+
celery: bool = typer.Option(
|
|
79
|
+
None,
|
|
80
|
+
"--celery/--no-celery",
|
|
81
|
+
help="是否启用 Celery 异步任务(未指定时交互提问,默认不启用)",
|
|
82
|
+
),
|
|
78
83
|
python_version: str = typer.Option(
|
|
79
84
|
"3.12",
|
|
80
85
|
"--python",
|
|
@@ -122,6 +127,7 @@ def create(
|
|
|
122
127
|
desc = prompts.ask_description(description)
|
|
123
128
|
use_pm2 = prompts.ask_use_pm2(pm2)
|
|
124
129
|
use_docker = prompts.ask_use_docker(docker)
|
|
130
|
+
use_celery = prompts.ask_use_celery(celery)
|
|
125
131
|
author_name = author or _git_user_name() or "fastapp-cli"
|
|
126
132
|
|
|
127
133
|
# 3. 渲染
|
|
@@ -132,6 +138,7 @@ def create(
|
|
|
132
138
|
"author_name": author_name,
|
|
133
139
|
"use_pm2": use_pm2,
|
|
134
140
|
"use_docker": use_docker,
|
|
141
|
+
"use_celery": use_celery,
|
|
135
142
|
"year": datetime.now(timezone.utc).year,
|
|
136
143
|
"fastapp_cli_version": __version__,
|
|
137
144
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""混合模式交互提问.
|
|
2
2
|
|
|
3
|
-
关键项(描述 / pm2 / Dockerfile)在未通过 flag 提供时交互提问;
|
|
3
|
+
关键项(描述 / pm2 / Dockerfile / celery)在未通过 flag 提供时交互提问;
|
|
4
4
|
flag 已提供则跳过提问,实现零交互。
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -33,3 +33,10 @@ def ask_use_docker(provided: bool | None) -> bool:
|
|
|
33
33
|
if provided is not None:
|
|
34
34
|
return provided
|
|
35
35
|
return typer.confirm("是否包含 Dockerfile?", default=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ask_use_celery(provided: bool | None) -> bool:
|
|
39
|
+
"""是否启用 Celery 异步任务:未指定时交互提问,默认不启用."""
|
|
40
|
+
if provided is not None:
|
|
41
|
+
return provided
|
|
42
|
+
return typer.confirm("是否启用 Celery 异步任务?", default=False)
|
|
@@ -17,7 +17,7 @@ MYSQL_DB={{ project_name | replace('-', '_') }}
|
|
|
17
17
|
# 是否打印 SQL(开发环境使用)
|
|
18
18
|
SQL_ECHO=false
|
|
19
19
|
|
|
20
|
-
# ========== Celery(redis,缺席时应用启动自动降级 memory://) ==========
|
|
20
|
+
{% if use_celery %}# ========== Celery(redis,缺席时应用启动自动降级 memory://) ==========
|
|
21
21
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
22
22
|
CELERY_RESULT_BACKEND=redis://localhost:6379/1
|
|
23
23
|
CELERY_TASK_DEFAULT_QUEUE=default
|
|
@@ -27,3 +27,4 @@ FLOWER_HOST=0.0.0.0
|
|
|
27
27
|
FLOWER_PORT=5555
|
|
28
28
|
# 基本认证(可选),格式:user:password,多个用逗号分隔
|
|
29
29
|
# FLOWER_BASIC_AUTH=admin:admin
|
|
30
|
+
{% endif %}
|
|
@@ -17,7 +17,7 @@ MYSQL_PASSWORD=
|
|
|
17
17
|
MYSQL_DB={{ project_name | replace('-', '_') }}_dev
|
|
18
18
|
SQL_ECHO=true
|
|
19
19
|
|
|
20
|
-
# ========== Celery(本地无 redis 时启动自动降级 memory://) ==========
|
|
20
|
+
{% if use_celery %}# ========== Celery(本地无 redis 时启动自动降级 memory://) ==========
|
|
21
21
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
22
22
|
CELERY_RESULT_BACKEND=redis://localhost:6379/1
|
|
23
23
|
CELERY_TASK_DEFAULT_QUEUE=default
|
|
@@ -25,3 +25,4 @@ CELERY_TASK_DEFAULT_QUEUE=default
|
|
|
25
25
|
# ========== Flower ==========
|
|
26
26
|
FLOWER_HOST=0.0.0.0
|
|
27
27
|
FLOWER_PORT=5555
|
|
28
|
+
{% endif %}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> {{ project_description }}
|
|
4
4
|
> 由 [fastapp-cli](https://pypi.org/project/fastapp-cli/) v{{ fastapp_cli_version }} 生成于 {{ year }},作者:{{ author_name }}
|
|
5
5
|
|
|
6
|
-
FastAPI + SQLModel + Celery 工程模板,含分层架构、统一响应 / 异常 / TraceID / 日志、分页 CRUD、Alembic 迁移与完整测试。
|
|
6
|
+
FastAPI + SQLModel{% if use_celery %} + Celery{% endif %} 工程模板,含分层架构、统一响应 / 异常 / TraceID / 日志、分页 CRUD、Alembic 迁移与完整测试。
|
|
7
7
|
|
|
8
8
|
## 快速开始
|
|
9
9
|
|
|
@@ -20,7 +20,8 @@ make dev # 或 uv run uvicorn app.main:app --reload
|
|
|
20
20
|
| 组件 | 默认 | 缺席时行为 |
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| MySQL | `.env` 中配置 | 启动不阻塞,`/api/v1/health` 返回 `database: "down"`,数据接口统一返回 500 |
|
|
23
|
-
| Redis (Celery) | `redis://localhost:6379` | 启动不阻塞,自动降级为 `memory://` broker 并输出告警日志 |
|
|
23
|
+
{% if use_celery %}| Redis (Celery) | `redis://localhost:6379` | 启动不阻塞,自动降级为 `memory://` broker 并输出告警日志 |
|
|
24
|
+
{% endif %}
|
|
24
25
|
|
|
25
26
|
生产环境只需在 `.env` 配置真实 MySQL / Redis 地址,无需改代码。
|
|
26
27
|
|
|
@@ -35,14 +36,14 @@ make migrate # alembic upgrade head
|
|
|
35
36
|
make revision m="add xxx" # 生成迁移脚本
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
Celery:
|
|
39
|
+
{% if use_celery %}Celery:
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
42
|
bash scripts/celery_worker.sh # worker
|
|
42
43
|
bash scripts/celery_beat.sh # 定时任务
|
|
43
44
|
bash scripts/celery_flower.sh # 监控面板(:5555)
|
|
44
45
|
```
|
|
45
|
-
|
|
46
|
+
{% endif %}
|
|
46
47
|
## 目录结构
|
|
47
48
|
|
|
48
49
|
```text
|
|
@@ -62,7 +63,10 @@ app/
|
|
|
62
63
|
|
|
63
64
|
## 扩展指南
|
|
64
65
|
|
|
66
|
+
> AI Agent / 新成员开发前请先阅读 [docs/agent_guide.md](docs/agent_guide.md)——包含架构说明、核心方法清单(优先复用)、标准开发流程与编码规范。
|
|
67
|
+
|
|
65
68
|
1. **新增业务域**:仿照 `item` 五件套(model / schema / crud / service / endpoint),在 `models/__init__.py` 注册新模型,在 `api/v1/router.py` 挂载新路由;
|
|
66
69
|
2. **生成迁移**:`make revision m="add xxx table"` 后检查 `alembic/versions/`,再 `make migrate`;
|
|
67
70
|
3. **接入真实认证**:替换 `app/api/deps.py::get_current_user` 的占位实现;
|
|
68
|
-
4. **新增 Celery 任务**:在 `app/tasks/` 中使用 `@celery_app.task` 装饰器编写。
|
|
71
|
+
{% if use_celery %}4. **新增 Celery 任务**:在 `app/tasks/` 中使用 `@celery_app.task` 装饰器编写。
|
|
72
|
+
{% endif %}
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/config.py.j2
RENAMED
|
@@ -80,7 +80,7 @@ class Settings(BaseSettings):
|
|
|
80
80
|
DB_POOL_SIZE: int = 10
|
|
81
81
|
DB_MAX_OVERFLOW: int = 20
|
|
82
82
|
|
|
83
|
-
# ========== Celery 配置 ==========
|
|
83
|
+
{% if use_celery %} # ========== Celery 配置 ==========
|
|
84
84
|
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
|
85
85
|
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
|
86
86
|
CELERY_TASK_DEFAULT_QUEUE: str = "default"
|
|
@@ -90,6 +90,7 @@ class Settings(BaseSettings):
|
|
|
90
90
|
FLOWER_PORT: int = 5555
|
|
91
91
|
# 基本认证,格式:user:password,多个用逗号分隔
|
|
92
92
|
FLOWER_BASIC_AUTH: str | None = None
|
|
93
|
+
{% endif %}
|
|
93
94
|
|
|
94
95
|
@property
|
|
95
96
|
def is_production(self) -> bool:
|
|
@@ -12,8 +12,8 @@ from loguru import logger
|
|
|
12
12
|
from sqlalchemy import text
|
|
13
13
|
|
|
14
14
|
from app.api.v1.router import api_router
|
|
15
|
-
from app.core.celery_app import celery_app, degrade_to_memory
|
|
16
|
-
from app.core.config import settings
|
|
15
|
+
{% if use_celery %}from app.core.celery_app import celery_app, degrade_to_memory
|
|
16
|
+
{% endif %}from app.core.config import settings
|
|
17
17
|
from app.core.database import engine
|
|
18
18
|
from app.core.exceptions import register_exception_handlers
|
|
19
19
|
from app.core.logging import setup_logging
|
|
@@ -30,7 +30,7 @@ def _probe_database() -> None:
|
|
|
30
30
|
logger.warning("数据库未就绪,数据接口将返回 500,请配置 .env: {}", exc)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _probe_redis() -> None:
|
|
33
|
+
{% if use_celery %}def _probe_redis() -> None:
|
|
34
34
|
"""启动时探测 redis(带超时),不可达时 Celery 自动降级为 memory broker."""
|
|
35
35
|
broker_url = settings.CELERY_BROKER_URL
|
|
36
36
|
if broker_url.startswith("memory"):
|
|
@@ -52,18 +52,18 @@ def _probe_redis() -> None:
|
|
|
52
52
|
logger.warning("redis 未就绪,Celery 已降级为 memory broker: {}", exc)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
@asynccontextmanager
|
|
55
|
+
{% endif %}@asynccontextmanager
|
|
56
56
|
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
57
57
|
"""应用生命周期.
|
|
58
58
|
|
|
59
|
-
- 启动:初始化日志、探测数据库 / redis(缺席降级,不阻塞启动)
|
|
59
|
+
- 启动:初始化日志、探测数据库{% if use_celery %} / redis(缺席降级,不阻塞启动){% endif %}
|
|
60
60
|
- 关闭:优雅释放资源
|
|
61
61
|
"""
|
|
62
62
|
setup_logging()
|
|
63
63
|
logger.info("{} starting | env={} | debug={}", settings.APP_NAME, settings.APP_ENV, settings.DEBUG)
|
|
64
64
|
_probe_database()
|
|
65
|
-
_probe_redis()
|
|
66
|
-
|
|
65
|
+
{% if use_celery %} _probe_redis()
|
|
66
|
+
{% endif %}
|
|
67
67
|
yield
|
|
68
68
|
|
|
69
69
|
logger.info("{} stopped", settings.APP_NAME)
|
|
@@ -71,6 +71,10 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
|
71
71
|
|
|
72
72
|
def create_app() -> FastAPI:
|
|
73
73
|
"""工厂函数:创建 FastAPI 应用实例."""
|
|
74
|
+
if settings.DEBUG:
|
|
75
|
+
# DEBUG 模式下打印全部配置,便于本地排查;生产环境(DEBUG=false)不输出,避免泄露敏感配置
|
|
76
|
+
logger.debug("settings: {}", settings.model_dump())
|
|
77
|
+
|
|
74
78
|
app_ = FastAPI(
|
|
75
79
|
title=settings.APP_NAME,
|
|
76
80
|
# 注意:固定 False。DEBUG 环境变量仅控制日志级别与 SQL echo;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Celery 示例任务."""
|
|
1
|
+
{% if use_celery %}"""Celery 示例任务."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -26,3 +26,4 @@ def sample_task(self, name: str = "world") -> dict:
|
|
|
26
26
|
logger.info("[task={}] sample_task running, name={}", self.request.id, name)
|
|
27
27
|
time.sleep(1)
|
|
28
28
|
return {"message": f"hello, {name}"}
|
|
29
|
+
{% endif %}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# {{ project_name }} — AI Agent 开发参考文档
|
|
2
|
+
|
|
3
|
+
> 本文档面向 AI Agent(或新加入的开发者),目标是帮助你在不了解项目历史的情况下,快速掌握项目设计并**正确地**进行开发。
|
|
4
|
+
> 核心原则:**优先复用已有方法与基础设施,不要重复造轮子。**
|
|
5
|
+
|
|
6
|
+
## 1. 项目概述与整体架构
|
|
7
|
+
|
|
8
|
+
本项目是由 fastapp-cli v{{ fastapp_cli_version }} 生成的 FastAPI 工程,技术栈:
|
|
9
|
+
|
|
10
|
+
| 层次 | 技术 | 说明 |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Web 框架 | FastAPI | 工厂函数 `create_app` 创建,lifespan 管理启动/关闭 |
|
|
13
|
+
| ORM | SQLModel (SQLAlchemy) | PEP 695 泛型 CRUD 基类 |
|
|
14
|
+
| 数据库 | MySQL(默认)/ SQLite(测试) | SQLAlchemy 引擎,连接池参数已调优 |
|
|
15
|
+
| 迁移 | Alembic | 增量迁移,`MANAGED_TABLES` 约束只管理业务表 |
|
|
16
|
+
{% if use_celery %}| 异步任务 | Celery + Redis | redis 不可达时自动降级 memory broker |
|
|
17
|
+
{% endif %}| 分页 | fastapi-pagination | 统一分页响应结构 |
|
|
18
|
+
| 过滤 | fastapi-filter | 声明式查询过滤器 |
|
|
19
|
+
| 日志 | loguru | 拦截标准 logging,全链路 trace_id |
|
|
20
|
+
| 配置 | pydantic-settings | 多环境 `.env` 加载 |
|
|
21
|
+
| 包管理 | uv | 依赖分组(dev 组含 pytest/ruff/mypy) |
|
|
22
|
+
| 质量工具 | ruff + mypy + pytest | 必须全绿才能合入 |
|
|
23
|
+
|
|
24
|
+
**分层架构(自上而下的调用方向,禁止跨层 / 反向依赖):**
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
Endpoint (app/api/v1/endpoints/) 路由层:参数解析、依赖注入、组装 Response
|
|
28
|
+
↓ 调用
|
|
29
|
+
Service (app/services/) 业务层:业务规则、事务编排,继承 ServiceBase
|
|
30
|
+
↓ 调用
|
|
31
|
+
CRUD (app/crud/) 数据访问层:只做数据读写,继承 CRUDBase
|
|
32
|
+
↓ 操作
|
|
33
|
+
Model (app/models/) SQLModel 表模型 + Pydantic 校验二合一
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**横切能力(在 core 中,所有层可直接使用):** 统一响应 / 统一异常 / TraceID / 日志 / 配置 / 数据库会话。
|
|
37
|
+
|
|
38
|
+
**请求处理链路:**
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
TraceIDMiddleware (最外层,生成/透传 X-Trace-Id)
|
|
42
|
+
→ CORS
|
|
43
|
+
→ register_exception_handlers 注册的异常处理器(兜底所有异常)
|
|
44
|
+
→ Endpoint(注入 DBSession / CurrentUser)
|
|
45
|
+
→ Response[T] 自动注入 trace_id
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2. 目录结构及各模块职责
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
.
|
|
52
|
+
├── app/
|
|
53
|
+
│ ├── main.py # create_app 工厂 + lifespan(DB{% if use_celery %}/redis{% endif %} 探测,缺席降级不阻塞启动)
|
|
54
|
+
│ ├── core/ # 横切基础设施(禁止在此写业务逻辑)
|
|
55
|
+
│ │ ├── config.py # settings 配置中心(多环境 .env 加载)
|
|
56
|
+
│ │ ├── database.py # engine / get_session / init_db
|
|
57
|
+
│ │ ├── logging.py # setup_logging / with_trace / InterceptHandler
|
|
58
|
+
│ │ ├── exceptions.py # AppExceptionError 体系 + 全局异常处理器
|
|
59
|
+
│ │ ├── middleware.py # TraceIDMiddleware(trace_id 生成与响应头注入)
|
|
60
|
+
│ │ ├── context_var.py # trace_id_var / get_trace_id
|
|
61
|
+
{% if use_celery %}│ │ └── celery_app.py # build_celery_app / degrade_to_memory / celery_app 单例
|
|
62
|
+
{% endif %}│ ├── api/
|
|
63
|
+
│ │ ├── deps.py # DBSession / CurrentUser 依赖(认证占位)
|
|
64
|
+
│ │ └── v1/
|
|
65
|
+
│ │ ├── router.py # v1 总路由,新业务路由在此挂载
|
|
66
|
+
│ │ └── endpoints/ # health.py(健康检查)、items.py(分页 CRUD 示例域)
|
|
67
|
+
│ ├── models/ # SQLModel 表模型;新模型必须在 __init__.py 注册
|
|
68
|
+
│ │ ├── base.py # IDMixin / TimestampMixin / utcnow
|
|
69
|
+
│ │ └── item.py # Item 示例模型
|
|
70
|
+
│ ├── schemas/ # Pydantic Schema:入参 / 出参 / 过滤器
|
|
71
|
+
│ │ ├── common.py # Response / PageParams / PageResult / PageResponse
|
|
72
|
+
│ │ └── item.py # ItemCreate / ItemUpdate / ItemRead / ItemFilter
|
|
73
|
+
│ ├── crud/ # 数据访问层
|
|
74
|
+
│ │ ├── base.py # CRUDBase 泛型基类(核心复用点)
|
|
75
|
+
│ │ └── item.py # CRUDItem(CRUDBase[Item])
|
|
76
|
+
│ ├── services/ # 业务逻辑层
|
|
77
|
+
│ │ ├── base.py # ServiceBase 泛型基类(核心复用点)
|
|
78
|
+
│ │ └── item_service.py # ItemService(ServiceBase[CRUDItem])
|
|
79
|
+
│ ├── tasks/{% if use_celery %} # Celery 任务(sample_tasks.py 为示例)
|
|
80
|
+
│ └── utils/ # 通用工具扩展位(空)
|
|
81
|
+
{% else %} # 预留任务目录
|
|
82
|
+
│ └── utils/ # 通用工具扩展位(空)
|
|
83
|
+
{% endif %}├── alembic/ # 迁移环境(env.py 已配置 MANAGED_TABLES 白名单过滤)
|
|
84
|
+
├── docs/ # 本文档 + SQLModel定义示例.md(建模规范)
|
|
85
|
+
├── scripts/ # start.sh{% if use_celery %} / celery_worker.sh / celery_beat.sh / celery_flower.sh{% endif %}
|
|
86
|
+
├── tests/ # pytest:conftest 用 sqlite 内存库,无需真实 MySQL
|
|
87
|
+
├── alembic.ini / Makefile / pyproject.toml
|
|
88
|
+
└── .env / .env.example # 配置文件(.env 入库,敏感信息按环境覆盖)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 3. 已有核心方法与工具函数清单(优先复用)
|
|
92
|
+
|
|
93
|
+
> **强制要求:开发新功能前先检索本清单。能用现有方法解决的,必须调用现有方法,禁止重写等价实现。**
|
|
94
|
+
|
|
95
|
+
### 3.1 数据访问层 — `CRUDBase`(`app/crud/base.py`)
|
|
96
|
+
|
|
97
|
+
泛型基类 `CRUDBase[ModelType: SQLModel]`,所有业务 CRUD 继承它即可获得以下能力,**任何手写 select/commit 循环前必须先确认此处没有现成方法**:
|
|
98
|
+
|
|
99
|
+
| 方法 | 签名 | 说明 |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `get` | `(db: Session, obj_id: Any) -> ModelType \| None` | 按主键查询 |
|
|
102
|
+
| `get_or_404` | `(db: Session, obj_id: Any) -> ModelType` | 按主键查询,不存在抛 `NotFoundError` |
|
|
103
|
+
| `get_by_field` | `(db: Session, field: str, value: Any) -> ModelType \| None` | 按任意字段查单个(字段不存在抛 `ValueError`) |
|
|
104
|
+
| `get_page_multi` | `(db: Session, *, filter_params: Filter \| None = None) -> PageResponse[Any]` | 分页 + 过滤 + 排序查询 |
|
|
105
|
+
| `get_multi` | `(db: Session, *, filter_params: Filter \| None = None) -> Sequence[ModelType]` | 非分页列表查询 |
|
|
106
|
+
| `count` | `(db: Session, query: Select \| SelectOfScalar \| None = None) -> int` | 记录总数(可传入自定义 query) |
|
|
107
|
+
| `create` | `(db: Session, *, obj_in: BaseModel \| dict[str, Any]) -> ModelType` | 创建单条(自动 model_dump、commit、refresh) |
|
|
108
|
+
| `create_multi` | `(db: Session, *, objs_in: list[...]) -> list[ModelType]` | 批量创建 |
|
|
109
|
+
| `update` | `(db: Session, *, db_obj: ModelType, obj_in: BaseModel \| dict[str, Any]) -> ModelType` | 更新(`exclude_unset` 语义,只更新显式传入字段) |
|
|
110
|
+
| `create_or_update` | `(db: Session, *, obj_in, unique_fields: str \| Sequence[str] \| None = None) -> ModelType` | 按唯一字段 upsert(未指定时用主键) |
|
|
111
|
+
| `create_or_update_multi` | `(db: Session, *, objs_in: list[...], unique_fields=None) -> list[ModelType]` | 批量 upsert(同一事务提交) |
|
|
112
|
+
| `remove` | `(db: Session, *, obj_id: int) -> ModelType` | 按主键删除(不存在抛 404) |
|
|
113
|
+
| `remove_multi` | `(db: Session, *, obj_ids: list[Any]) -> list[ModelType]` | 批量删除 |
|
|
114
|
+
| `exists` | `(db: Session, *, obj_id: Any) -> bool` | 判断是否存在 |
|
|
115
|
+
|
|
116
|
+
使用示例(新增业务域的 CRUD 通常只需一行继承):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from app.crud.base import CRUDBase
|
|
120
|
+
from app.models.product import Product
|
|
121
|
+
|
|
122
|
+
class CRUDProduct(CRUDBase[Product]):
|
|
123
|
+
pass # 复杂查询逻辑才在此扩展
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3.2 业务层 — `ServiceBase`(`app/services/base.py`)
|
|
127
|
+
|
|
128
|
+
泛型基类 `ServiceBase[CRUDType: CRUDBase]`,内部持有 `self.db` 与 `self.crud`,方法返回值标注为 `Any`(由子类按业务模型收窄):
|
|
129
|
+
|
|
130
|
+
| 方法 | 签名 | 说明 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `create` | `(obj_in: BaseModel \| dict) -> Any` | 创建 |
|
|
133
|
+
| `create_multi` | `(objs_in: list) -> list[Any]` | 批量创建 |
|
|
134
|
+
| `create_or_update` | `(obj_in, unique_fields=None) -> Any` | upsert |
|
|
135
|
+
| `create_or_update_multi` | `(objs_in, unique_fields=None) -> list[Any]` | 批量 upsert |
|
|
136
|
+
| `get` | `(obj_id: int) -> Any` | 获取单个,不存在抛 `NotFoundError` |
|
|
137
|
+
| `update` | `(obj_id: int, obj_in) -> Any` | 更新(不存在抛 `NotFoundError`) |
|
|
138
|
+
| `delete` | `(obj_id: int) -> Any` | 删除 |
|
|
139
|
+
| `list_objs` | `(filter_params: Filter \| None = None) -> Sequence[Any]` | 列表查询 |
|
|
140
|
+
| `list_page_objs` | `(filter_params: Filter \| None = None) -> PageResponse[Any]` | 分页查询 |
|
|
141
|
+
|
|
142
|
+
使用示例(纯 CRUD 型业务,Service 通常也只是继承):
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from app.crud.product import CRUDProduct
|
|
146
|
+
from app.services.base import ServiceBase
|
|
147
|
+
|
|
148
|
+
class ProductService(ServiceBase[CRUDProduct]):
|
|
149
|
+
def activate(self, obj_id: int) -> None: # 自定义业务方法
|
|
150
|
+
obj = self.get(obj_id)
|
|
151
|
+
self.crud.update(self.db, db_obj=obj, obj_in={"is_active": True})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3.3 API 依赖(`app/api/deps.py`)
|
|
155
|
+
|
|
156
|
+
| 名称 | 类型 | 说明 |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `DBSession` | `Annotated[Session, Depends(get_session)]` | 端点中直接作参数注解,获取数据库会话 |
|
|
159
|
+
| `CurrentUser` | `Annotated[str, Depends(get_current_user)]` | 当前登录用户。非生产环境返回 `settings.MOCK_USER`;生产环境抛 `UnauthorizedError`(认证占位,接入真实认证时只改 `get_current_user`) |
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
@router.post("", response_model=Response[ProductRead])
|
|
163
|
+
def create_product(payload: ProductCreate, service: ProductServiceDep, current_user: CurrentUser) -> Response[ProductRead]:
|
|
164
|
+
data: ProductRead = service.create(payload)
|
|
165
|
+
return Response(data=data)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 3.4 统一响应与分页(`app/schemas/common.py`)
|
|
169
|
+
|
|
170
|
+
| 名称 | 说明 |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `Response[T]` | 统一响应体 `{code, message, data, trace_id}`;`code=0` 为成功;trace_id 自动从请求上下文注入 |
|
|
173
|
+
| `PageParams` | 分页入参(`page` / `page_size`,`size` 的 URL alias 为 `page_size`) |
|
|
174
|
+
| `PageResult[T]` | 分页数据结构 `{total, page, page_size, content}` |
|
|
175
|
+
| `PageResponse[T]` | 分页统一响应(继承 `Response[PageResult[T]]`),作为端点 `response_model` 使用 |
|
|
176
|
+
|
|
177
|
+
### 3.5 统一异常(`app/core/exceptions.py`)
|
|
178
|
+
|
|
179
|
+
**业务代码中禁止直接 `raise HTTPException` 或返回错误 dict**,统一抛以下异常,由全局异常处理器转换成统一返回体 `{code, message, data, trace_id}`:
|
|
180
|
+
|
|
181
|
+
| 异常 | HTTP 状态码 | 业务码 | 默认消息 |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| `AppExceptionError(message, *, code, status_code, data)` | 400 | 1 | 业务异常基类,自定义业务异常继承它 |
|
|
184
|
+
| `NotFoundError` | 404 | 40400 | 资源不存在 |
|
|
185
|
+
| `UnauthorizedError` | 401 | 40100 | 未认证或凭证已失效 |
|
|
186
|
+
| `ForbiddenError` | 403 | 40300 | 无访问权限 |
|
|
187
|
+
| `ConflictError` | 409 | 40900 | 资源冲突 |
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from app.core.exceptions import ConflictError, NotFoundError, AppExceptionError
|
|
191
|
+
|
|
192
|
+
raise NotFoundError(message="订单不存在")
|
|
193
|
+
raise AppExceptionError(message="余额不足", code=10001, status_code=400)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
未捕获异常统一返回 HTTP 500 + `{"code": 50000, "message": "服务器内部错误"}`,不会裸抛堆栈(应用实例固定 `debug=False`,勿改)。
|
|
197
|
+
|
|
198
|
+
### 3.6 模型公共字段(`app/models/base.py`)
|
|
199
|
+
|
|
200
|
+
| 名称 | 说明 |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `IDMixin` | 主键 `id: int`(primary_key + index) |
|
|
203
|
+
| `TimestampMixin` | `create_time` / `update_time`,数据库自动生成与刷新,JSON 序列化为 `%Y-%m-%d %H:%M:%S` |
|
|
204
|
+
| `utcnow()` | 当前时间(`default_factory` 用) |
|
|
205
|
+
| `FormattedDatetime` | 可复用的 datetime 序列化类型标注 |
|
|
206
|
+
|
|
207
|
+
新表模型标准写法:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from sqlmodel import Field
|
|
211
|
+
from app.models.base import IDMixin, TimestampMixin
|
|
212
|
+
|
|
213
|
+
class ProductBase(TimestampMixin):
|
|
214
|
+
name: str = Field(max_length=128, description="名称")
|
|
215
|
+
|
|
216
|
+
class Product(IDMixin, ProductBase, table=True):
|
|
217
|
+
__tablename__ = "t_product"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 3.7 配置(`app/core/config.py`)
|
|
221
|
+
|
|
222
|
+
- 单例 `settings`:所有环境变量集中于此,**禁止在业务代码中直接读 `os.environ`**。
|
|
223
|
+
- 加载顺序(后者覆盖前者):`.env` → `.env.{APP_ENV}` → 系统环境变量。
|
|
224
|
+
- 常用字段:`APP_NAME` / `APP_ENV`(development|production|test)/ `DEBUG` / `API_V1_PREFIX` / `ALLOWED_ORIGINS` / `DATABASE_URI`(未设置时自动按 `MYSQL_*` 拼装)/ `SQL_ECHO`{% if use_celery %} / `CELERY_BROKER_URL`{% endif %} / `MOCK_USER`。
|
|
225
|
+
- 属性:`settings.is_production` / `settings.is_test`。
|
|
226
|
+
- 新增配置项:在 `Settings` 类中加字段 + 默认值,并在 `.env.example` / `.env` 中补充说明。
|
|
227
|
+
|
|
228
|
+
### 3.8 数据库(`app/core/database.py`)
|
|
229
|
+
|
|
230
|
+
| 名称 | 说明 |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `engine` | 全局引擎。sqlite(测试)与 MySQL(连接池 + `pool_recycle=3600` + `REPEATABLE READ` + 3 秒连接超时)已分别调优,勿重复配置 |
|
|
233
|
+
| `get_session()` | FastAPI 依赖,yield 语法保证 Session 关闭;**通过 `DBSession` 注解使用**,不要自建 Session |
|
|
234
|
+
| `init_db()` | 本地/测试快速建表(生产用 alembic 迁移) |
|
|
235
|
+
|
|
236
|
+
{% if use_celery %}### 3.9 Celery(`app/core/celery_app.py`)
|
|
237
|
+
|
|
238
|
+
| 名称 | 说明 |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `celery_app` | 全局单例(worker 与 web 共用),新任务直接 `@celery_app.task` |
|
|
241
|
+
| `build_celery_app(broker_url, backend_url, *, name)` | 构造入口(自定义 broker 时使用) |
|
|
242
|
+
| `degrade_to_memory(celery)` | 降级为 memory broker/backend(lifespan 探测失败时自动调用,勿手动调用) |
|
|
243
|
+
|
|
244
|
+
新任务示例:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from app.core.celery_app import celery_app
|
|
248
|
+
|
|
249
|
+
@celery_app.task
|
|
250
|
+
def send_notice(user_id: int) -> str:
|
|
251
|
+
...
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
{% endif %}{% if use_celery %}### 3.10{% else %}### 3.9{% endif %} 日志与 TraceID(`app/core/logging.py`、`app/core/context_var.py`)
|
|
255
|
+
|
|
256
|
+
| 名称 | 说明 |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `setup_logging()` | lifespan 启动时自动调用,勿重复调用 |
|
|
259
|
+
| `logger`(loguru) | 业务代码统一 `from loguru import logger`;标准 logging 已被拦截转发到 loguru |
|
|
260
|
+
| `with_trace` | 装饰器:把请求 trace_id 透传到后台任务(`BackgroundTasks.add_task` 场景**必须**用它包裹) |
|
|
261
|
+
| `get_trace_id()` | 获取当前请求 trace_id(无上下文返回空串) |
|
|
262
|
+
|
|
263
|
+
## 4. 新增功能的标准开发流程
|
|
264
|
+
|
|
265
|
+
### 4.1 新增业务域(以 `product` 为例,五件套 + 两处注册 + 迁移 + 测试)
|
|
266
|
+
|
|
267
|
+
1. **Model**:`app/models/product.py`,继承 `IDMixin, TimestampMixin`(见 §3.6 示例);在 `app/models/__init__.py` 中 import 并加入 `__all__`(否则 alembic autogenerate 无法发现)。
|
|
268
|
+
2. **Schema**:`app/schemas/product.py`,定义 `ProductCreate` / `ProductUpdate` / `ProductRead`(继承 `ProductBase` + `id` 字段)与 `ProductFilter(Filter)`(`Constants.model` 指向模型,过滤字段用 `__ilike` / `__gte` 等后缀,参考 `app/schemas/item.py`)。
|
|
269
|
+
3. **CRUD**:`app/crud/product.py`,`class CRUDProduct(CRUDBase[Product]): pass`。
|
|
270
|
+
4. **Service**:`app/services/product_service.py`,`class ProductService(ServiceBase[CRUDProduct])`,业务规则写在这里。
|
|
271
|
+
5. **Endpoint**:`app/api/v1/endpoints/product.py`,仿照 `items.py`:`get_service` 依赖 + `ServiceDep` + 各路由方法返回 `Response[...]` / `PageResponse[...]`。
|
|
272
|
+
6. **注册路由**:`app/api/v1/router.py` 中 `include_router(product_router, prefix="/products", tags=["Products"])`。
|
|
273
|
+
7. **迁移**:`make revision m="add t_product"` → 检查 `alembic/versions/` 生成脚本 → `make migrate`。注意 `alembic/env.py` 用 `MANAGED_TABLES` 白名单过滤,新表模型注册到 `app/models/__init__.py` 后会被自动纳入。
|
|
274
|
+
8. **测试**:在 `tests/` 下仿照 `tests/api/test_items.py` 编写(conftest 已用 sqlite 内存库,无需真实 MySQL),运行 `make test && make lint`。
|
|
275
|
+
|
|
276
|
+
### 4.2 其他扩展场景
|
|
277
|
+
|
|
278
|
+
| 场景 | 做法 |
|
|
279
|
+
|---|---|
|
|
280
|
+
| 新增配置项 | `Settings` 类加字段 + `.env.example` 补说明 |
|
|
281
|
+
| 接入真实认证 | 只替换 `app/api/deps.py::get_current_user` 实现,端点代码不动 |
|
|
282
|
+
| 新增中间件 | `app/core/middleware.py` 实现,并在 `main.py::create_app` 注册;**注意注册顺序**:TraceIDMiddleware 必须最后添加(处于最外层) |
|
|
283
|
+
{% if use_celery %}| 新增 Celery 定时任务 | `app/tasks/` 写任务,`celery_app.conf.beat_schedule` 中注册(该文件内有注释示例) |
|
|
284
|
+
{% endif %}| 自定义业务异常 | 继承 `AppExceptionError`,类属性覆盖 `status_code` / `code` / `message`,全局处理器自动生效 |
|
|
285
|
+
| 复杂查询 | 在业务 CRUD 子类中扩展方法,保持 Service 只编排不写 SQL 细节 |
|
|
286
|
+
|
|
287
|
+
## 5. 编码规范与注意事项
|
|
288
|
+
|
|
289
|
+
### 5.1 强制规范
|
|
290
|
+
|
|
291
|
+
1. **统一响应**:所有端点 `response_model` 必须是 `Response[T]` 或 `PageResponse[T]`;错误场景抛异常(§3.5),禁止返回裸 dict / 直接 `HTTPException`。
|
|
292
|
+
2. **禁止 `FastAPI(debug=True)`**:`main.py` 中已固定 `debug=False`。`DEBUG=true` 仅控制日志级别、SQL echo 与 uvicorn reload;传 True 会导致 Starlette 绕过统一异常处理器返回原始堆栈。
|
|
293
|
+
3. **分层依赖方向**:endpoint → service → crud → model,单向依赖;core 不依赖业务层。
|
|
294
|
+
4. **复用优先**:新写任何 select/commit/upsert/分页逻辑前,先查 §3 清单;CRUDBase / ServiceBase 未覆盖的场景才在子类扩展。
|
|
295
|
+
5. **模型注册**:新模型必须加入 `app/models/__init__.py`,否则 `init_db()` 与 alembic autogenerate 均无法感知。
|
|
296
|
+
6. **质量门禁**:提交前 `make format && make test && make lint`(ruff + mypy)必须全绿。类型注解用 PEP 695 语法(`class Foo[T]`),SQLModel `inspect()` 返回可空处沿用既有的 `cast(...)` + `# type: ignore[union-attr]` 惯例。
|
|
297
|
+
7. **导入风格**:`from __future__ import annotations` 置于模块首行(docstring 之后);第三方与本地导入分组,遵循 isort 规则(ruff 强制)。
|
|
298
|
+
|
|
299
|
+
### 5.2 注意事项
|
|
300
|
+
|
|
301
|
+
- **时间字段**:一律通过 `TimestampMixin`;JSON 输出格式 `%Y-%m-%d %H:%M:%S` 由 `FormattedDatetime` 统一处理,勿自行 strftime。
|
|
302
|
+
- **Session 生命周期**:请求内通过 `DBSession` 注入;后台任务/脚本中用 `with Session(engine) as session:`,不要复用请求 Session。
|
|
303
|
+
- **降级行为**:MySQL{% if use_celery %} / Redis 缺席不阻塞启动(health 返回 down、Celery 降级 memory)。本地开发无需强制装依赖,但数据接口在无库时统一返回 500{% else %} 缺席不阻塞启动(health 返回 down)。本地开发无需强制装数据库,数据接口在无库时统一返回 500{% endif %}。
|
|
304
|
+
- **测试环境**:测试用 sqlite 内存库(`tests/conftest.py`),依赖 sqlite 不支持的特性时需显式跳过。
|
|
305
|
+
- **`.env` 提交策略**:`.env` 与 `.env.example` 入库作默认配置;敏感信息(生产密码)放 `.env.production`(不入库)或真实环境变量。
|
|
306
|
+
- **docs/**:`SQLModel定义示例.md` 是建模规范,新表定义先对照该文档。
|
|
307
|
+
- **业务域隔离**:`item` 域为示例,可整体替换为实际业务;删除时同步清理 `router.py` 挂载与对应测试。
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{% if use_pm2 %}module.exports = {
|
|
2
|
+
apps: [
|
|
3
|
+
{
|
|
4
|
+
name: "{{ project_name }}-server",
|
|
5
|
+
script: "bash scripts/start.sh",
|
|
6
|
+
out_file: "./logs/server.log",
|
|
7
|
+
err_file: "./logs/server.log",
|
|
8
|
+
},
|
|
9
|
+
{% if use_celery %}
|
|
10
|
+
{
|
|
11
|
+
name: "{{ project_name }}-worker",
|
|
12
|
+
script: "bash scripts/celery_worker.sh",
|
|
13
|
+
out_file: "./logs/celery.log",
|
|
14
|
+
err_file: "./logs/celery.log",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "{{ project_name }}-beat",
|
|
18
|
+
script: "bash scripts/celery_beat.sh",
|
|
19
|
+
out_file: "./logs/celery_beat.log",
|
|
20
|
+
err_file: "./logs/celery_beat.log",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "{{ project_name }}-flower",
|
|
24
|
+
script: "bash scripts/celery_flower.sh",
|
|
25
|
+
out_file: "./logs/flower.log",
|
|
26
|
+
err_file: "./logs/flower.log",
|
|
27
|
+
},
|
|
28
|
+
{% else %}
|
|
29
|
+
// {
|
|
30
|
+
// name: "{{ project_name }}-worker",
|
|
31
|
+
// script: "bash scripts/celery_worker.sh",
|
|
32
|
+
// out_file: "./logs/celery.log",
|
|
33
|
+
// err_file: "./logs/celery.log",
|
|
34
|
+
// },
|
|
35
|
+
// {
|
|
36
|
+
// name: "{{ project_name }}-beat",
|
|
37
|
+
// script: "bash scripts/celery_beat.sh",
|
|
38
|
+
// out_file: "./logs/celery_beat.log",
|
|
39
|
+
// err_file: "./logs/celery_beat.log",
|
|
40
|
+
// },
|
|
41
|
+
// {
|
|
42
|
+
// name: "{{ project_name }}-flower",
|
|
43
|
+
// script: "bash scripts/celery_flower.sh",
|
|
44
|
+
// out_file: "./logs/flower.log",
|
|
45
|
+
// err_file: "./logs/flower.log",
|
|
46
|
+
// }
|
|
47
|
+
{% endif %}
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
{% endif %}
|
|
@@ -7,12 +7,12 @@ requires-python = ">=3.12"
|
|
|
7
7
|
|
|
8
8
|
dependencies = [
|
|
9
9
|
"alembic>=1.18.4",
|
|
10
|
-
"celery[redis]>=5.6.3",
|
|
11
|
-
"fastapi>=0.136.1",
|
|
10
|
+
{% if use_celery %} "celery[redis]>=5.6.3",
|
|
11
|
+
{% endif %} "fastapi>=0.136.1",
|
|
12
12
|
"fastapi-filter[sqlalchemy]>=2.0.1",
|
|
13
13
|
"fastapi-pagination>=0.15.13",
|
|
14
|
-
"flower>=2.0.1",
|
|
15
|
-
"httpx>=0.28.1",
|
|
14
|
+
{% if use_celery %} "flower>=2.0.1",
|
|
15
|
+
{% endif %} "httpx>=0.28.1",
|
|
16
16
|
"loguru>=0.7.3",
|
|
17
17
|
"pydantic>=2.13.3",
|
|
18
18
|
"pydantic-settings>=2.14.0",
|
|
@@ -180,9 +180,9 @@ exclude = [
|
|
|
180
180
|
# 说明:这些库通常没有官方 type stubs,忽略可以避免 "Cannot find implementation" 报错
|
|
181
181
|
[[tool.mypy.overrides]]
|
|
182
182
|
module = [
|
|
183
|
-
"celery.*",
|
|
183
|
+
{% if use_celery %} "celery.*",
|
|
184
184
|
"flower.*",
|
|
185
|
-
"sqlmodel.*", # SQLModel 的类型推断对 mypy 不友好,建议忽略
|
|
185
|
+
{% endif %} "sqlmodel.*", # SQLModel 的类型推断对 mypy 不友好,建议忽略
|
|
186
186
|
]
|
|
187
187
|
ignore_missing_imports = true
|
|
188
188
|
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/celery_beat.sh.j2
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
1
|
+
{% if use_celery %}#!/usr/bin/env bash
|
|
2
2
|
# 启动 Celery Beat(定时任务调度)
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
@@ -7,3 +7,4 @@ cd "$(dirname "$0")/.."
|
|
|
7
7
|
LOG_LEVEL="${LOG_LEVEL:-INFO}"
|
|
8
8
|
|
|
9
9
|
exec uv run celery -A app.core.celery_app.celery_app beat -l "${LOG_LEVEL}"
|
|
10
|
+
{% endif %}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
1
|
+
{% if use_celery %}#!/usr/bin/env bash
|
|
2
2
|
# 启动 Flower(Celery 任务监控面板)
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
@@ -20,3 +20,4 @@ exec uv run celery -A app.core.celery_app.celery_app flower \
|
|
|
20
20
|
--port="${PORT}" \
|
|
21
21
|
--loglevel="${LOG_LEVEL}" \
|
|
22
22
|
${BASIC_AUTH_ARG}
|
|
23
|
+
{% endif %}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
1
|
+
{% if use_celery %}#!/usr/bin/env bash
|
|
2
2
|
# 启动 Celery Worker
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
@@ -13,3 +13,4 @@ exec uv run celery -A app.core.celery_app.celery_app worker \
|
|
|
13
13
|
-c "${CONCURRENCY}" \
|
|
14
14
|
-l "${LOG_LEVEL}" \
|
|
15
15
|
-n "worker@%h"
|
|
16
|
+
{% endif %}
|
|
@@ -33,6 +33,7 @@ def test_create_smoke_full_flags(tmp_path: Path, monkeypatch) -> None:
|
|
|
33
33
|
"--description", "smoke test",
|
|
34
34
|
"--pm2",
|
|
35
35
|
"--docker",
|
|
36
|
+
"--celery",
|
|
36
37
|
"--no-git",
|
|
37
38
|
],
|
|
38
39
|
)
|
|
@@ -46,7 +47,8 @@ def test_create_smoke_full_flags(tmp_path: Path, monkeypatch) -> None:
|
|
|
46
47
|
"app/main.py",
|
|
47
48
|
"app/api/v1/endpoints/items.py",
|
|
48
49
|
"tests/conftest.py",
|
|
49
|
-
"pm2.config.
|
|
50
|
+
"pm2.config.js",
|
|
51
|
+
"app/core/celery_app.py",
|
|
50
52
|
"Dockerfile",
|
|
51
53
|
):
|
|
52
54
|
assert (root / rel).exists(), f"缺少 {rel}"
|
|
@@ -59,6 +61,26 @@ def test_create_smoke_full_flags(tmp_path: Path, monkeypatch) -> None:
|
|
|
59
61
|
assert "make dev" in result.output
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
def test_create_default_no_celery(tmp_path: Path, monkeypatch) -> None:
|
|
65
|
+
"""--no-celery(默认):不生成 celery 文件,pm2 命令被注释."""
|
|
66
|
+
monkeypatch.chdir(tmp_path)
|
|
67
|
+
result = runner.invoke(
|
|
68
|
+
app,
|
|
69
|
+
[
|
|
70
|
+
"create", "demo-server",
|
|
71
|
+
"--description", "x",
|
|
72
|
+
"--pm2", "--no-docker", "--no-celery", "--no-git",
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
assert result.exit_code == 0, result.output
|
|
76
|
+
|
|
77
|
+
root = tmp_path / "demo-server"
|
|
78
|
+
assert not (root / "app/core/celery_app.py").exists()
|
|
79
|
+
assert not (root / "app/tasks/sample_tasks.py").exists()
|
|
80
|
+
pm2_text = (root / "pm2.config.js").read_text(encoding="utf-8")
|
|
81
|
+
assert "// script: \"bash scripts/celery_worker.sh\"" in pm2_text
|
|
82
|
+
|
|
83
|
+
|
|
62
84
|
def test_create_existing_dir_rejected(tmp_path: Path, monkeypatch) -> None:
|
|
63
85
|
"""目标目录已存在且未指定 --force 时拒绝."""
|
|
64
86
|
monkeypatch.chdir(tmp_path)
|
|
@@ -83,18 +105,19 @@ def test_create_invalid_name_rejected(tmp_path: Path, monkeypatch) -> None:
|
|
|
83
105
|
|
|
84
106
|
|
|
85
107
|
def test_create_interactive_flow(tmp_path: Path, monkeypatch) -> None:
|
|
86
|
-
"""零 flag 交互路径:通过输入补全描述 / pm2 / Dockerfile."""
|
|
108
|
+
"""零 flag 交互路径:通过输入补全描述 / pm2 / Dockerfile / celery."""
|
|
87
109
|
monkeypatch.chdir(tmp_path)
|
|
88
|
-
# 描述输入(回车取默认)、pm2 回答 y、Dockerfile 回答 n
|
|
110
|
+
# 描述输入(回车取默认)、pm2 回答 y、Dockerfile 回答 n、celery 回答 n(默认)
|
|
89
111
|
result = runner.invoke(
|
|
90
112
|
app,
|
|
91
113
|
["create", "demo-server", "--no-git"],
|
|
92
|
-
input="\ny\nn\n",
|
|
114
|
+
input="\ny\nn\nn\n",
|
|
93
115
|
)
|
|
94
116
|
assert result.exit_code == 0, result.output
|
|
95
117
|
root = tmp_path / "demo-server"
|
|
96
|
-
assert (root / "pm2.config.
|
|
118
|
+
assert (root / "pm2.config.js").exists()
|
|
97
119
|
assert not (root / "Dockerfile").exists()
|
|
120
|
+
assert not (root / "app/core/celery_app.py").exists()
|
|
98
121
|
|
|
99
122
|
|
|
100
123
|
def test_create_git_init(tmp_path: Path, monkeypatch) -> None:
|
|
@@ -104,7 +127,7 @@ def test_create_git_init(tmp_path: Path, monkeypatch) -> None:
|
|
|
104
127
|
monkeypatch.chdir(tmp_path)
|
|
105
128
|
result = runner.invoke(
|
|
106
129
|
app,
|
|
107
|
-
["create", "demo-server", "--description", "x", "--no-pm2", "--no-docker"],
|
|
130
|
+
["create", "demo-server", "--description", "x", "--no-pm2", "--no-docker", "--no-celery"],
|
|
108
131
|
)
|
|
109
132
|
assert result.exit_code == 0, result.output
|
|
110
133
|
assert (tmp_path / "demo-server" / ".git").is_dir()
|
|
@@ -15,6 +15,7 @@ BASE_CTX: dict[str, object] = {
|
|
|
15
15
|
"author_name": "tester",
|
|
16
16
|
"use_pm2": False,
|
|
17
17
|
"use_docker": False,
|
|
18
|
+
"use_celery": True,
|
|
18
19
|
"year": 2026,
|
|
19
20
|
"fastapp_cli_version": "0.1.0",
|
|
20
21
|
}
|
|
@@ -70,7 +71,7 @@ def test_render_project_full(tmp_path: Path) -> None:
|
|
|
70
71
|
assert (out / rel).exists(), f"缺少 {rel},实际输出 {len(written)} 个文件"
|
|
71
72
|
|
|
72
73
|
# 条件文件默认不生成
|
|
73
|
-
assert not (out / "pm2.config.
|
|
74
|
+
assert not (out / "pm2.config.js").exists()
|
|
74
75
|
assert not (out / "Dockerfile").exists()
|
|
75
76
|
|
|
76
77
|
# 变量注入
|
|
@@ -90,9 +91,52 @@ def test_render_project_conditional_files(tmp_path: Path) -> None:
|
|
|
90
91
|
out = tmp_path / "proj"
|
|
91
92
|
render_project(out, ctx)
|
|
92
93
|
|
|
93
|
-
pm2 = out / "pm2.config.
|
|
94
|
+
pm2 = out / "pm2.config.js"
|
|
94
95
|
dockerfile = out / "Dockerfile"
|
|
95
96
|
assert pm2.exists()
|
|
96
97
|
assert dockerfile.exists()
|
|
97
|
-
|
|
98
|
+
pm2_text = pm2.read_text(encoding="utf-8")
|
|
99
|
+
assert "demo-server" in pm2_text
|
|
100
|
+
# celery 启用:worker/beat/flower 为生效条目
|
|
101
|
+
assert "celery_worker.sh" in pm2_text
|
|
102
|
+
assert "// name:" not in pm2_text
|
|
98
103
|
assert "python3.12" in dockerfile.read_text(encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_render_project_no_celery(tmp_path: Path) -> None:
|
|
107
|
+
"""use_celery=False:celery 文件不生成,pm2 中对应命令为注释,依赖剔除."""
|
|
108
|
+
from fastapp_cli.render import render_project
|
|
109
|
+
|
|
110
|
+
ctx = dict(BASE_CTX, use_celery=False, use_pm2=True)
|
|
111
|
+
out = tmp_path / "proj"
|
|
112
|
+
render_project(out, ctx)
|
|
113
|
+
|
|
114
|
+
# celery 相关文件不生成
|
|
115
|
+
for rel in (
|
|
116
|
+
"app/core/celery_app.py",
|
|
117
|
+
"app/tasks/sample_tasks.py",
|
|
118
|
+
"scripts/celery_worker.sh",
|
|
119
|
+
"scripts/celery_beat.sh",
|
|
120
|
+
"scripts/celery_flower.sh",
|
|
121
|
+
):
|
|
122
|
+
assert not (out / rel).exists(), f"{rel} 不应生成"
|
|
123
|
+
|
|
124
|
+
# pm2.config.js 中 celery 启动命令被注释
|
|
125
|
+
pm2_text = (out / "pm2.config.js").read_text(encoding="utf-8")
|
|
126
|
+
assert "// name:" in pm2_text
|
|
127
|
+
assert "// script: \"bash scripts/celery_worker.sh\"" in pm2_text
|
|
128
|
+
# server 条目仍生效
|
|
129
|
+
assert 'name: "demo-server-server"' in pm2_text
|
|
130
|
+
|
|
131
|
+
# pyproject 无 celery/flower 依赖
|
|
132
|
+
pyproject = (out / "pyproject.toml").read_text(encoding="utf-8")
|
|
133
|
+
assert "celery" not in pyproject
|
|
134
|
+
assert "flower" not in pyproject
|
|
135
|
+
|
|
136
|
+
# main.py 无 celery 探测
|
|
137
|
+
main_text = (out / "app" / "main.py").read_text(encoding="utf-8")
|
|
138
|
+
assert "celery" not in main_text
|
|
139
|
+
assert "_probe_redis" not in main_text
|
|
140
|
+
|
|
141
|
+
# DEBUG 打印已注入
|
|
142
|
+
assert "settings.DEBUG" in main_text
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
{% if use_pm2 %}
|
|
2
|
-
{
|
|
3
|
-
"apps": [
|
|
4
|
-
{
|
|
5
|
-
"name": "{{ project_name }}-server",
|
|
6
|
-
"script": "bash scripts/start.sh",
|
|
7
|
-
"output": "./logs/server.log",
|
|
8
|
-
"error": "./logs/server-error.log",
|
|
9
|
-
"env": {
|
|
10
|
-
"HOST": "0.0.0.0",
|
|
11
|
-
"PORT": "8000",
|
|
12
|
-
"APP_ENV": "production",
|
|
13
|
-
"WORKERS": "1"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"name": "{{ project_name }}-worker",
|
|
18
|
-
"script": "bash scripts/celery_worker.sh",
|
|
19
|
-
"output": "./logs/celery.log",
|
|
20
|
-
"error": "./logs/celery-error.log",
|
|
21
|
-
"env": {
|
|
22
|
-
"CONCURRENCY": "4",
|
|
23
|
-
"QUEUE": "default",
|
|
24
|
-
"LOG_LEVEL": "INFO"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"name": "{{ project_name }}-beat",
|
|
29
|
-
"script": "bash scripts/celery_beat.sh",
|
|
30
|
-
"log_file": "./logs/celery_beat.log",
|
|
31
|
-
"env": {
|
|
32
|
-
"LOG_LEVEL": "INFO"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"name": "{{ project_name }}-flower",
|
|
37
|
-
"script": "bash scripts/celery_flower.sh",
|
|
38
|
-
"log_file": "./logs/flower.log",
|
|
39
|
-
"env": {
|
|
40
|
-
"FLOWER_HOST": "0.0.0.0",
|
|
41
|
-
"FLOWER_PORT": "5555",
|
|
42
|
-
"LOG_LEVEL": "INFO"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
47
|
-
{% endif %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.pre-commit-config.yaml.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/.python-version.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic/script.py.mako
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/alembic/versions/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/deps.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/__init__.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/api/v1/router.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/context_var.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/database.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/exceptions.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/logging.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/core/middleware.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/base.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/crud/item.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/base.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/models/item.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/common.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/schemas/item.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/services/base.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/tasks/__init__.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/app/utils/__init__.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/scripts/start.sh.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/api/test_items.py.j2
RENAMED
|
File without changes
|
{fastapp_cli-0.1.0 → fastapp_cli-0.3.0}/src/fastapp_cli/templates/project/tests/conftest.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|