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
fastapp_cli/__init__.py
ADDED
fastapp_cli/create.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""``fastapp create`` 命令实现.
|
|
2
|
+
|
|
3
|
+
流程:校验 → 交互补全 → 渲染 → git init → 输出下一步指引。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from fastapp_cli import __version__
|
|
15
|
+
from fastapp_cli import prompts
|
|
16
|
+
from fastapp_cli.naming import validate_project_name
|
|
17
|
+
from fastapp_cli.render import render_project
|
|
18
|
+
|
|
19
|
+
FILE_COUNT_EXCLUDE = {".git"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _git_user_name() -> str | None:
|
|
23
|
+
"""读取 git 全局配置中的 user.name,失败返回 None."""
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["git", "config", "user.name"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
check=True,
|
|
30
|
+
)
|
|
31
|
+
name = result.stdout.strip()
|
|
32
|
+
return name or None
|
|
33
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _count_files(out_dir: Path) -> int:
|
|
38
|
+
return sum(
|
|
39
|
+
1 for p in out_dir.rglob("*")
|
|
40
|
+
if p.is_file() and not any(part in FILE_COUNT_EXCLUDE for part in p.parts)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _git_init(project_dir: Path) -> bool:
|
|
45
|
+
"""初始化 git 仓库,失败时告警但不阻塞."""
|
|
46
|
+
try:
|
|
47
|
+
subprocess.run(
|
|
48
|
+
["git", "init", "-q"],
|
|
49
|
+
cwd=project_dir,
|
|
50
|
+
capture_output=True,
|
|
51
|
+
check=True,
|
|
52
|
+
)
|
|
53
|
+
return True
|
|
54
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as exc:
|
|
55
|
+
typer.secho(f"⚠ git init 失败,已跳过:{exc}", fg=typer.colors.YELLOW)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create(
|
|
60
|
+
project_name: str = typer.Argument(
|
|
61
|
+
..., help="项目名:小写字母/数字/连字符,如 my-server(应用包固定位 app)"
|
|
62
|
+
),
|
|
63
|
+
description: str = typer.Option(
|
|
64
|
+
None,
|
|
65
|
+
"--description",
|
|
66
|
+
help="项目描述(未提供时交互提问)",
|
|
67
|
+
),
|
|
68
|
+
pm2: bool = typer.Option(
|
|
69
|
+
None,
|
|
70
|
+
"--pm2/--no-pm2",
|
|
71
|
+
help="是否生成 pm2.config.json(未指定时交互提问)",
|
|
72
|
+
),
|
|
73
|
+
docker: bool = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--docker/--no-docker",
|
|
76
|
+
help="是否生成 Dockerfile(未指定时交互提问)",
|
|
77
|
+
),
|
|
78
|
+
python_version: str = typer.Option(
|
|
79
|
+
"3.12",
|
|
80
|
+
"--python",
|
|
81
|
+
help="生成项目的目标 Python 版本",
|
|
82
|
+
),
|
|
83
|
+
author: str = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--author",
|
|
86
|
+
help="作者署名(默认取 git config user.name)",
|
|
87
|
+
),
|
|
88
|
+
force: bool = typer.Option(
|
|
89
|
+
False,
|
|
90
|
+
"--force",
|
|
91
|
+
help="目标目录已存在时覆盖重建",
|
|
92
|
+
),
|
|
93
|
+
no_git: bool = typer.Option(
|
|
94
|
+
False,
|
|
95
|
+
"--no-git",
|
|
96
|
+
help="跳过 git init",
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""生成一个完整、可直接运行的 FastAPI 项目."""
|
|
100
|
+
# 1. 校验
|
|
101
|
+
try:
|
|
102
|
+
name = validate_project_name(project_name)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
typer.secho(f"✘ {exc}", fg=typer.colors.RED, err=True)
|
|
105
|
+
raise typer.Exit(code=1) from exc
|
|
106
|
+
|
|
107
|
+
project_dir = Path.cwd() / name
|
|
108
|
+
if project_dir.exists():
|
|
109
|
+
if not force:
|
|
110
|
+
typer.secho(
|
|
111
|
+
f"✘ 目标目录已存在:{project_dir}(使用 --force 覆盖重建)",
|
|
112
|
+
fg=typer.colors.RED,
|
|
113
|
+
err=True,
|
|
114
|
+
)
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
typer.secho(f"⚠ 覆盖重建已存在目录:{project_dir}", fg=typer.colors.YELLOW)
|
|
117
|
+
import shutil
|
|
118
|
+
|
|
119
|
+
shutil.rmtree(project_dir)
|
|
120
|
+
|
|
121
|
+
# 2. 交互补全(flag 已提供则跳过)
|
|
122
|
+
desc = prompts.ask_description(description)
|
|
123
|
+
use_pm2 = prompts.ask_use_pm2(pm2)
|
|
124
|
+
use_docker = prompts.ask_use_docker(docker)
|
|
125
|
+
author_name = author or _git_user_name() or "fastapp-cli"
|
|
126
|
+
|
|
127
|
+
# 3. 渲染
|
|
128
|
+
ctx: dict[str, object] = {
|
|
129
|
+
"project_name": name,
|
|
130
|
+
"project_description": desc,
|
|
131
|
+
"python_version": python_version,
|
|
132
|
+
"author_name": author_name,
|
|
133
|
+
"use_pm2": use_pm2,
|
|
134
|
+
"use_docker": use_docker,
|
|
135
|
+
"year": datetime.now(timezone.utc).year,
|
|
136
|
+
"fastapp_cli_version": __version__,
|
|
137
|
+
}
|
|
138
|
+
try:
|
|
139
|
+
render_project(project_dir, ctx)
|
|
140
|
+
except RuntimeError as exc:
|
|
141
|
+
typer.secho(f"✘ 渲染失败:{exc}", fg=typer.colors.RED, err=True)
|
|
142
|
+
raise typer.Exit(code=1) from exc
|
|
143
|
+
|
|
144
|
+
# 4. git init
|
|
145
|
+
if not no_git:
|
|
146
|
+
if _git_init(project_dir):
|
|
147
|
+
typer.secho("✔ 已初始化 git 仓库", fg=typer.colors.GREEN)
|
|
148
|
+
|
|
149
|
+
# 5. 输出指引
|
|
150
|
+
typer.secho(
|
|
151
|
+
f"✔ 生成项目 {name}/({_count_files(project_dir)} 个文件)",
|
|
152
|
+
fg=typer.colors.GREEN,
|
|
153
|
+
)
|
|
154
|
+
typer.echo("下一步:")
|
|
155
|
+
typer.echo(f" cd {name} && uv sync")
|
|
156
|
+
typer.echo(" make dev # 或 uv run uvicorn app.main:app --reload")
|
|
157
|
+
typer.echo(" 访问 /api/v1/docs 查看接口文档")
|
fastapp_cli/main.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""fastapp CLI 入口."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from fastapp_cli import __version__
|
|
8
|
+
from fastapp_cli.create import create
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="fastapp",
|
|
12
|
+
help="FastAPI 项目脚手架:一条命令生成完整可运行的 FastAPI 工程",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
add_completion=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.callback()
|
|
19
|
+
def _root() -> None:
|
|
20
|
+
"""fastapp CLI."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def version() -> None:
|
|
25
|
+
"""显示版本号."""
|
|
26
|
+
typer.echo(f"fastapp-cli {__version__}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
app.command()(create)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
app()
|
fastapp_cli/naming.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""project_name 命名校验.
|
|
2
|
+
|
|
3
|
+
应用包名固定位 ``app``,``project_name`` 仅用于目录名与 pyproject name:
|
|
4
|
+
小写字母 / 数字 / 连字符,且不以连字符开头或结尾。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
PROJECT_NAME_RE = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
|
|
12
|
+
|
|
13
|
+
MAX_LENGTH = 64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_project_name(name: str) -> str:
|
|
17
|
+
"""校验 project_name,非法时抛出 ValueError.
|
|
18
|
+
|
|
19
|
+
规则:
|
|
20
|
+
- 非空,长度 <= 64
|
|
21
|
+
- 仅小写字母 / 数字 / 连字符
|
|
22
|
+
- 以小写字母开头,不以连字符开头或结尾,不含连续连字符
|
|
23
|
+
|
|
24
|
+
:param name: 项目名,如 ``my-server``
|
|
25
|
+
:return: 校验通过的项目名
|
|
26
|
+
:raises ValueError: 名称非法
|
|
27
|
+
"""
|
|
28
|
+
name = (name or "").strip()
|
|
29
|
+
if not name:
|
|
30
|
+
raise ValueError("项目名不能为空")
|
|
31
|
+
if len(name) > MAX_LENGTH:
|
|
32
|
+
raise ValueError(f"项目名过长(最多 {MAX_LENGTH} 字符):{name}")
|
|
33
|
+
if not PROJECT_NAME_RE.match(name):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"项目名非法:{name!r}。"
|
|
36
|
+
"仅允许小写字母、数字与连字符,须以字母开头,"
|
|
37
|
+
"不能以连字符开头/结尾或包含连续连字符,示例:my-server"
|
|
38
|
+
)
|
|
39
|
+
return name
|
fastapp_cli/prompts.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""混合模式交互提问.
|
|
2
|
+
|
|
3
|
+
关键项(描述 / pm2 / Dockerfile)在未通过 flag 提供时交互提问;
|
|
4
|
+
flag 已提供则跳过提问,实现零交互。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
DEFAULT_DESCRIPTION = "A FastAPI project created by fastapp-cli"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ask_description(provided: str | None) -> str:
|
|
15
|
+
"""获取项目描述:flag 已提供则直接使用,否则交互提问."""
|
|
16
|
+
if provided:
|
|
17
|
+
return provided
|
|
18
|
+
return typer.prompt(
|
|
19
|
+
"项目描述",
|
|
20
|
+
default=DEFAULT_DESCRIPTION,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ask_use_pm2(provided: bool | None) -> bool:
|
|
25
|
+
"""是否生成 pm2 配置:未指定时交互提问."""
|
|
26
|
+
if provided is not None:
|
|
27
|
+
return provided
|
|
28
|
+
return typer.confirm("是否包含 pm2 配置?", default=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ask_use_docker(provided: bool | None) -> bool:
|
|
32
|
+
"""是否生成 Dockerfile:未指定时交互提问."""
|
|
33
|
+
if provided is not None:
|
|
34
|
+
return provided
|
|
35
|
+
return typer.confirm("是否包含 Dockerfile?", default=False)
|
fastapp_cli/render.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Jinja2 模板渲染引擎.
|
|
2
|
+
|
|
3
|
+
- 模板随包发布,通过 ``importlib.resources`` 加载,离线可用;
|
|
4
|
+
- ``.j2`` 后缀文件渲染并剥离后缀,其余文件原样拷贝;
|
|
5
|
+
- 渲染完成后自检输出文件中无 ``{{`` / ``{%`` 残留,发现即报错。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import jinja2
|
|
16
|
+
from jinja2 import Environment, FileSystemLoader
|
|
17
|
+
|
|
18
|
+
TEMPLATES_PACKAGE = "fastapp_cli.templates"
|
|
19
|
+
PROJECT_TEMPLATE_DIR = "project"
|
|
20
|
+
|
|
21
|
+
RENDER_SUFFIX = ".j2"
|
|
22
|
+
|
|
23
|
+
JINJA_ENV = Environment(
|
|
24
|
+
loader=FileSystemLoader("/"), # 实际以绝对路径渲染单个文件,此处仅占位
|
|
25
|
+
keep_trailing_newline=True,
|
|
26
|
+
undefined=jinja2.StrictUndefined,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def template_root() -> Path:
|
|
31
|
+
"""返回包内项目模板根目录(发布后随 wheel 分发)."""
|
|
32
|
+
traversable = resources.files(TEMPLATES_PACKAGE) / PROJECT_TEMPLATE_DIR
|
|
33
|
+
# as_file 支持压缩包场景;常规 wheel/开发目录下为真实路径
|
|
34
|
+
with resources.as_file(traversable) as path:
|
|
35
|
+
return Path(path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_string(template_text: str, ctx: dict[str, Any]) -> str:
|
|
39
|
+
"""渲染单段模板文本."""
|
|
40
|
+
return JINJA_ENV.from_string(template_text).render(**ctx)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _iter_template_files(root: Path) -> list[Path]:
|
|
44
|
+
"""递归收集模板文件(跳过目录与 .gitkeep 空占位保留处理)."""
|
|
45
|
+
return sorted(p for p in root.rglob("*") if p.is_file())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def render_project(
|
|
49
|
+
out_dir: Path,
|
|
50
|
+
ctx: dict[str, Any],
|
|
51
|
+
*,
|
|
52
|
+
template_dir: Path | None = None,
|
|
53
|
+
) -> list[Path]:
|
|
54
|
+
"""渲染整个项目模板到目标目录.
|
|
55
|
+
|
|
56
|
+
:param out_dir: 输出目录(须不存在或已清空)
|
|
57
|
+
:param ctx: 模板变量
|
|
58
|
+
:param template_dir: 模板根目录,默认使用包内模板
|
|
59
|
+
:return: 实际写出的文件路径列表(相对 out_dir)
|
|
60
|
+
:raises RuntimeError: 渲染产物中存在 Jinja 残留标记
|
|
61
|
+
"""
|
|
62
|
+
root = template_dir if template_dir is not None else template_root()
|
|
63
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
written: list[Path] = []
|
|
66
|
+
for src in _iter_template_files(root):
|
|
67
|
+
rel = src.relative_to(root)
|
|
68
|
+
dest = out_dir / rel
|
|
69
|
+
|
|
70
|
+
# .j2 后缀剥离:main.py.j2 -> main.py
|
|
71
|
+
if rel.suffix == RENDER_SUFFIX:
|
|
72
|
+
dest = dest.with_suffix("")
|
|
73
|
+
|
|
74
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
if rel.suffix == RENDER_SUFFIX:
|
|
77
|
+
text = src.read_text(encoding="utf-8")
|
|
78
|
+
rendered = render_string(text, ctx)
|
|
79
|
+
# 条件文件:模板整体被 {% if %} 包裹,条件不满足时渲染结果为空 -> 跳过生成
|
|
80
|
+
if not rendered.strip():
|
|
81
|
+
continue
|
|
82
|
+
dest.write_text(rendered, encoding="utf-8")
|
|
83
|
+
_assert_no_jinja_residue(dest)
|
|
84
|
+
# shell 脚本一律保持可执行位
|
|
85
|
+
if dest.suffix == ".sh":
|
|
86
|
+
dest.chmod(dest.stat().st_mode | 0o111)
|
|
87
|
+
else:
|
|
88
|
+
shutil.copyfile(src, dest)
|
|
89
|
+
# 保持脚本可执行位
|
|
90
|
+
if src.stat().st_mode & 0o111:
|
|
91
|
+
dest.chmod(dest.stat().st_mode | 0o111)
|
|
92
|
+
|
|
93
|
+
written.append(dest)
|
|
94
|
+
|
|
95
|
+
_verify_tree(out_dir, len(written))
|
|
96
|
+
return written
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _assert_no_jinja_residue(path: Path) -> None:
|
|
100
|
+
"""自检渲染产物中不允许残留 Jinja 标记."""
|
|
101
|
+
text = path.read_text(encoding="utf-8")
|
|
102
|
+
for marker in ("{{", "{%"):
|
|
103
|
+
if marker in text:
|
|
104
|
+
line_no = next(
|
|
105
|
+
i + 1
|
|
106
|
+
for i, line in enumerate(text.splitlines())
|
|
107
|
+
if marker in line
|
|
108
|
+
)
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
f"渲染残留:{path} 第 {line_no} 行发现未渲染的 Jinja 标记 {marker!r}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _verify_tree(out_dir: Path, expected_count: int) -> None:
|
|
115
|
+
"""整体自检:目标文件数与实际写出数一致,且无 Jinja 残留."""
|
|
116
|
+
out_files = [p for p in out_dir.rglob("*") if p.is_file()]
|
|
117
|
+
if len(out_files) != expected_count:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
f"渲染自检失败:预期写出 {expected_count} 个文件,"
|
|
120
|
+
f"实际 {len(out_files)} 个"
|
|
121
|
+
)
|
|
122
|
+
for path in out_files:
|
|
123
|
+
# 跳过 gitkeep 等空文件;仅扫描文本
|
|
124
|
+
try:
|
|
125
|
+
text = path.read_text(encoding="utf-8")
|
|
126
|
+
except UnicodeDecodeError:
|
|
127
|
+
continue
|
|
128
|
+
for marker in ("{{", "{%"):
|
|
129
|
+
if marker in text:
|
|
130
|
+
raise RuntimeError(f"渲染残留:{path} 发现未渲染的 Jinja 标记 {marker!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""模板包资源定位标记."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 开发环境差异配置(示例)
|
|
2
|
+
# 使用方式:
|
|
3
|
+
# cp .env.development.example .env.development
|
|
4
|
+
# 启动时自动加载:APP_ENV=development bash scripts/start.sh
|
|
5
|
+
|
|
6
|
+
APP_ENV=development
|
|
7
|
+
DEBUG=true
|
|
8
|
+
|
|
9
|
+
# 开发库
|
|
10
|
+
MYSQL_SERVER=localhost
|
|
11
|
+
MYSQL_PORT=3306
|
|
12
|
+
MYSQL_USER=root
|
|
13
|
+
MYSQL_PASSWORD=root
|
|
14
|
+
MYSQL_DB={{ project_name | replace('-', '_') }}_dev
|
|
15
|
+
|
|
16
|
+
# 打印 SQL 方便排查
|
|
17
|
+
SQL_ECHO=true
|
|
18
|
+
|
|
19
|
+
# 本地 Redis
|
|
20
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
21
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ========== App ==========
|
|
2
|
+
APP_NAME={{ project_name }}
|
|
3
|
+
APP_ENV=development
|
|
4
|
+
API_V1_PREFIX=/api/v1
|
|
5
|
+
DEBUG=false
|
|
6
|
+
|
|
7
|
+
# ========== Server ==========
|
|
8
|
+
HOST=0.0.0.0
|
|
9
|
+
PORT=8000
|
|
10
|
+
|
|
11
|
+
# ========== MySQL ==========
|
|
12
|
+
MYSQL_SERVER=localhost
|
|
13
|
+
MYSQL_PORT=3306
|
|
14
|
+
MYSQL_USER=root
|
|
15
|
+
MYSQL_PASSWORD=
|
|
16
|
+
MYSQL_DB={{ project_name | replace('-', '_') }}
|
|
17
|
+
# 是否打印 SQL(开发环境使用)
|
|
18
|
+
SQL_ECHO=false
|
|
19
|
+
|
|
20
|
+
# ========== Celery(redis,缺席时应用启动自动降级 memory://) ==========
|
|
21
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
22
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/1
|
|
23
|
+
CELERY_TASK_DEFAULT_QUEUE=default
|
|
24
|
+
|
|
25
|
+
# ========== Flower ==========
|
|
26
|
+
FLOWER_HOST=0.0.0.0
|
|
27
|
+
FLOWER_PORT=5555
|
|
28
|
+
# 基本认证(可选),格式:user:password,多个用逗号分隔
|
|
29
|
+
# FLOWER_BASIC_AUTH=admin:admin
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 本地默认配置(已 gitignore)。生产配置参考 .env.example / .env.production.example
|
|
2
|
+
# ========== App ==========
|
|
3
|
+
APP_NAME={{ project_name }}
|
|
4
|
+
APP_ENV=development
|
|
5
|
+
API_V1_PREFIX=/api/v1
|
|
6
|
+
DEBUG=true
|
|
7
|
+
|
|
8
|
+
# ========== Server ==========
|
|
9
|
+
HOST=0.0.0.0
|
|
10
|
+
PORT=8000
|
|
11
|
+
|
|
12
|
+
# ========== MySQL(未配置时应用可启动,health 返回 database=down) ==========
|
|
13
|
+
MYSQL_SERVER=localhost
|
|
14
|
+
MYSQL_PORT=3306
|
|
15
|
+
MYSQL_USER=root
|
|
16
|
+
MYSQL_PASSWORD=
|
|
17
|
+
MYSQL_DB={{ project_name | replace('-', '_') }}_dev
|
|
18
|
+
SQL_ECHO=true
|
|
19
|
+
|
|
20
|
+
# ========== Celery(本地无 redis 时启动自动降级 memory://) ==========
|
|
21
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
22
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/1
|
|
23
|
+
CELERY_TASK_DEFAULT_QUEUE=default
|
|
24
|
+
|
|
25
|
+
# ========== Flower ==========
|
|
26
|
+
FLOWER_HOST=0.0.0.0
|
|
27
|
+
FLOWER_PORT=5555
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
### Python template
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# poetry
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
+
#poetry.lock
|
|
104
|
+
|
|
105
|
+
# pdm
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
+
#pdm.lock
|
|
108
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
109
|
+
# in version control.
|
|
110
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
111
|
+
.pdm.toml
|
|
112
|
+
.pdm-python
|
|
113
|
+
.pdm-build/
|
|
114
|
+
|
|
115
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
+
__pypackages__/
|
|
117
|
+
|
|
118
|
+
# Celery stuff
|
|
119
|
+
celerybeat-schedule
|
|
120
|
+
celerybeat.pid
|
|
121
|
+
|
|
122
|
+
# SageMath parsed files
|
|
123
|
+
*.sage.py
|
|
124
|
+
|
|
125
|
+
# Environments
|
|
126
|
+
.env
|
|
127
|
+
.env.development
|
|
128
|
+
.env.production
|
|
129
|
+
.env.test
|
|
130
|
+
.env.local
|
|
131
|
+
!.env.example
|
|
132
|
+
!.env.development.example
|
|
133
|
+
!.env.production.example
|
|
134
|
+
!.env.test.example
|
|
135
|
+
.venv
|
|
136
|
+
env/
|
|
137
|
+
venv/
|
|
138
|
+
ENV/
|
|
139
|
+
env.bak/
|
|
140
|
+
venv.bak/
|
|
141
|
+
|
|
142
|
+
# Spyder project settings
|
|
143
|
+
.spyderproject
|
|
144
|
+
.spyproject
|
|
145
|
+
|
|
146
|
+
# Rope project settings
|
|
147
|
+
.ropeproject
|
|
148
|
+
|
|
149
|
+
# mkdocs documentation
|
|
150
|
+
/site
|
|
151
|
+
|
|
152
|
+
# mypy
|
|
153
|
+
.mypy_cache/
|
|
154
|
+
.dmypy.json
|
|
155
|
+
dmypy.json
|
|
156
|
+
|
|
157
|
+
# Pyre type checker
|
|
158
|
+
.pyre/
|
|
159
|
+
|
|
160
|
+
# pytype static type analyzer
|
|
161
|
+
.pytype/
|
|
162
|
+
|
|
163
|
+
# Cython debug symbols
|
|
164
|
+
cython_debug/
|
|
165
|
+
|
|
166
|
+
# PyCharm
|
|
167
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
168
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
169
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
170
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
171
|
+
.idea/
|
|
172
|
+
.env.dev
|
|
173
|
+
.env.prod
|
|
174
|
+
|
|
175
|
+
# Project specific
|
|
176
|
+
logs/
|
|
177
|
+
.DS_Store
|
|
178
|
+
.ruff_cache/
|