diff --git a/.gitignore b/.gitignore index 2727892..9a944a2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ instance/ .env .env.* !.env.example +!*.env.example.jinja .envrc # Runtime data diff --git a/copier-template/.codex/skills/{{ project_slug | lower | replace('_', '-') }}-project/SKILL.md.jinja b/copier-template/.codex/skills/{{ project_slug | lower | replace('_', '-') }}-project/SKILL.md.jinja index e5ac7b8..bb644a1 100644 --- a/copier-template/.codex/skills/{{ project_slug | lower | replace('_', '-') }}-project/SKILL.md.jinja +++ b/copier-template/.codex/skills/{{ project_slug | lower | replace('_', '-') }}-project/SKILL.md.jinja @@ -27,7 +27,8 @@ description: "{{ project_name }} 业务项目 skill。用于当前由 iTi-Flask ## 项目结构 -- `main.py`:导入 `config`,注册模块,创建 FastAPI app。 +- `main.py`:ASGI 入口,导出 `app`。 +- `app/app_factory.py`:导入 `config`,注册模块,创建 FastAPI app。 - `config.py`:项目本地配置映射。 - `app/modules/`:业务模块。 - `app/models/`:项目 SQLAlchemy 模型。 @@ -35,6 +36,12 @@ description: "{{ project_name }} 业务项目 skill。用于当前由 iTi-Flask - `tests/`:pytest 路由和行为测试。 - `app.sh`:Linux/macOS/Git Bash 命令入口。 - `app.cmd`:Windows CMD 命令入口。 +- `Dockerfile`:容器镜像构建入口。 +- `docker-compose.yml`:本地 Compose 部署入口。 +- `docker-compose.with-db.yml`:本地 MySQL 叠加部署入口。 +- `.env.example`:本地和 Compose 环境变量样例。 +- `.vscode/launch.json`:VSCode FastAPI 调试配置。 +- `.dockerignore`:Docker 构建排除规则。 - `pyproject.toml`:包信息和依赖。 - `.copier-answers.yml`:Copier 模板更新锚点。 @@ -68,6 +75,12 @@ description: "{{ project_name }} 业务项目 skill。用于当前由 iTi-Flask - 同步模板骨架:`./app.sh template-update` - 运行测试:`./app.sh test` - 本地启动:`./app.sh serve 8000` +- 构建 Docker 镜像:`./app.sh docker-build` +- 启动 Docker Compose:`./app.sh docker-up` +- 启动 Docker Compose 和 MySQL:`./app.sh docker-up-db` +- 停止 Docker Compose:`./app.sh docker-down` +- 停止 Docker Compose 和 MySQL:`./app.sh docker-down-db` +- 查看应用容器日志:`./app.sh docker-logs` - 创建 migration:`./app.sh migration "alice add order table"` - 执行 migration:`./app.sh migrate` - 查看 Alembic heads:`./app.sh heads` diff --git a/copier-template/.dockerignore b/copier-template/.dockerignore new file mode 100644 index 0000000..2ed3874 --- /dev/null +++ b/copier-template/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.venv +__pycache__ +*.py[cod] +.pytest_cache +.mypy_cache +.ruff_cache +*.egg-info +build +dist +runtime +logs +.env +.env.* +!.env.example +*.db +*.sqlite +*.sqlite3 diff --git a/copier-template/.env.example.jinja b/copier-template/.env.example.jinja new file mode 100644 index 0000000..f585825 --- /dev/null +++ b/copier-template/.env.example.jinja @@ -0,0 +1,13 @@ +APP_ENV=prod +APP_PORT=8000 + +MYSQL_HOST=host.docker.internal +MYSQL_PORT=3306 +MYSQL_ROOT_PASSWORD=root-password +MYSQL_DATABASE=app +MYSQL_USER=app +MYSQL_PASSWORD=change-me + +SECRET_KEY=change-me +JWT_SECRET_KEY=change-me +LOG_FILE_ENABLED=true diff --git a/copier-template/.vscode/launch.json.jinja b/copier-template/.vscode/launch.json.jinja new file mode 100644 index 0000000..f5b48d5 --- /dev/null +++ b/copier-template/.vscode/launch.json.jinja @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "{{ project_name }}: FastAPI", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": [ + "main:app", + "--reload", + "--host", + "127.0.0.1", + "--port", + "8000" + ], + "cwd": "${workspaceFolder}", + "env": { + "APP_ENV": "dev", + "APP_ENV_DIR": "${workspaceFolder}" + }, + "jinja": true, + "justMyCode": true + } + ] +} diff --git a/copier-template/Dockerfile.jinja b/copier-template/Dockerfile.jinja new file mode 100644 index 0000000..3d134de --- /dev/null +++ b/copier-template/Dockerfile.jinja @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl git \ + && rm -rf /var/lib/apt/lists/* + +COPY . . +RUN pip install --no-cache-dir uv \ + && if [ -f uv.lock ]; then \ + uv sync --frozen --no-dev; \ + else \ + uv sync --no-dev; \ + fi + +EXPOSE 8000 + +CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/copier-template/README.md.jinja b/copier-template/README.md.jinja index d385575..6727bf3 100644 --- a/copier-template/README.md.jinja +++ b/copier-template/README.md.jinja @@ -21,6 +21,7 @@ AI 修改本项目时优先读: ## 初始化 ```bash +cp .env.example .env ./app.sh init ``` @@ -59,6 +60,26 @@ app.cmd init-system APP_ENV=prod ./app.sh migrate ``` +## Docker + +```bash +cp .env.example .env +./app.sh docker-up +./app.sh docker-logs +./app.sh docker-down +``` + +`docker-compose.yml` 默认只启动应用,数据库使用外部 MySQL。 +需要本地 MySQL 时使用: + +```bash +./app.sh docker-up-db +./app.sh docker-down-db +``` + +应用容器启动时会先执行 migration{% if include_system %} 和系统 seed{% endif %}。 +本地运行数据写入 `runtime/`,MySQL 数据写入 Compose volume。 + ## 同步更新 同步框架包: @@ -79,10 +100,15 @@ APP_ENV=prod ./app.sh migrate ./app.sh template-update ``` -模板更新会改 `main.py`、`config.py`、`app.sh`、`app.cmd`、示例模块、测试和项目 skill 等模板拥有的文件。 +模板更新会改 `main.py`、`app/app_factory.py`、`config.py`、`Dockerfile`、`docker-compose.yml`、`docker-compose.with-db.yml`、`.dockerignore`、`.env.example`、`.vscode/launch.json`、`app.sh`、`app.cmd`、示例模块、测试和项目 skill 等模板拥有的文件。 该命令跟随模板仓库 `HEAD`。 执行前先提交或暂存当前项目改动,执行后检查 diff。 +## VSCode 调试 + +模板内置 `.vscode/launch.json`。 +选择 `{{ project_name }}: FastAPI` 即可用 `uvicorn main:app --reload` 调试。 + ## 数据库迁移 ```bash diff --git a/copier-template/app.cmd.jinja b/copier-template/app.cmd.jinja index 4d36a45..05bf3de 100644 --- a/copier-template/app.cmd.jinja +++ b/copier-template/app.cmd.jinja @@ -36,6 +36,12 @@ if "%COMMAND%"=="template-update" ( ) if "%COMMAND%"=="test" goto test if "%COMMAND%"=="serve" goto serve +if "%COMMAND%"=="docker-build" goto docker_build +if "%COMMAND%"=="docker-up" goto docker_up +if "%COMMAND%"=="docker-up-db" goto docker_up_db +if "%COMMAND%"=="docker-down" goto docker_down +if "%COMMAND%"=="docker-down-db" goto docker_down_db +if "%COMMAND%"=="docker-logs" goto docker_logs if "%COMMAND%"=="migrate" goto migrate if "%COMMAND%"=="migration" goto migration if "%COMMAND%"=="heads" goto heads @@ -63,6 +69,12 @@ echo template-check 检查 Copier 模板是否有更新 echo template-update 按 Copier 模板更新项目骨架 echo test 运行测试:uv run --extra dev pytest -q echo serve [环境] [端口] 本地启动,默认 dev / 8000 +echo docker-build 构建 Docker 镜像 +echo docker-up 启动 Docker Compose 应用 +echo docker-up-db 启动 Docker Compose 应用和 MySQL +echo docker-down 停止 Docker Compose 应用 +echo docker-down-db 停止 Docker Compose 应用和 MySQL +echo docker-logs 查看应用容器日志 echo migrate 执行 Alembic upgrade head echo migration ^<说明^> 生成 migration,说明建议以作者名开头 echo heads 查看 Alembic heads @@ -75,6 +87,7 @@ echo. echo 示例: echo app.cmd install echo app.cmd serve +echo app.cmd docker-up echo app.cmd serve test 8000 echo set APP_ENV=prod ^&^& app.cmd migrate echo app.cmd migration "alice add order table" @@ -100,7 +113,6 @@ goto end :serve set "ENV_NAME=%APP_ENV%" -if not defined ENV_NAME set "ENV_NAME=%ITI_ENV%" if not defined ENV_NAME set "ENV_NAME=dev" set "PORT=8000" set "ARG1=%~1" @@ -127,10 +139,33 @@ if /I "%ARG1%"=="dev" ( ) if "%PORT%"=="" set "PORT=8000" set "APP_ENV=%ENV_NAME%" -set "ITI_ENV=%ENV_NAME%" uv run uvicorn main:app --reload --port "%PORT%" goto end +:docker_build +docker compose build +goto end + +:docker_up +docker compose up -d --build +goto end + +:docker_up_db +docker compose -f docker-compose.yml -f docker-compose.with-db.yml up -d --build +goto end + +:docker_down +docker compose down +goto end + +:docker_down_db +docker compose -f docker-compose.yml -f docker-compose.with-db.yml down +goto end + +:docker_logs +docker compose logs -f app +goto end + :migrate uv run alembic -c migrations/alembic.ini upgrade head goto end @@ -157,7 +192,8 @@ uv run iti-system migrations sync --target migrations/versions goto end :system_seed -uv run iti-system seed system main:app +set "PYTHONPATH=." +uv run iti-system seed system app:create_app goto end :init_system @@ -165,7 +201,8 @@ uv run iti-system migrations sync --target migrations/versions if errorlevel 1 goto end uv run alembic -c migrations/alembic.ini upgrade head if errorlevel 1 goto end -uv run iti-system seed system main:app +set "PYTHONPATH=." +uv run iti-system seed system app:create_app goto end {% endif %}:init @@ -175,7 +212,8 @@ if errorlevel 1 goto end if errorlevel 1 goto end {% endif %}uv run alembic -c migrations/alembic.ini upgrade head if errorlevel 1 goto end -{% if include_system %}uv run iti-system seed system main:app +{% if include_system %}set "PYTHONPATH=." +uv run iti-system seed system app:create_app {% endif %}goto end :end diff --git a/copier-template/app.sh.jinja b/copier-template/app.sh.jinja index 99d1c7c..033c38c 100644 --- a/copier-template/app.sh.jinja +++ b/copier-template/app.sh.jinja @@ -29,6 +29,12 @@ show_help() { template-update 按 Copier 模板更新项目骨架 test 运行测试:uv run --extra dev pytest -q serve [环境] [端口] 本地启动,默认 dev / 8000 + docker-build 构建 Docker 镜像 + docker-up 启动 Docker Compose 应用 + docker-up-db 启动 Docker Compose 应用和 MySQL + docker-down 停止 Docker Compose 应用 + docker-down-db 停止 Docker Compose 应用和 MySQL + docker-logs 查看应用容器日志 migrate 执行 Alembic upgrade head migration <说明> 生成 migration,说明建议以作者名开头 heads 查看 Alembic heads @@ -41,6 +47,7 @@ show_help() { 示例: ./app.sh install ./app.sh serve + ./app.sh docker-up ./app.sh serve test 8000 APP_ENV=prod ./app.sh migrate ./app.sh migration "alice add order table" @@ -72,7 +79,7 @@ case "$command" in uv run --extra dev pytest -q ;; serve) - env_name=${APP_ENV:-${ITI_ENV:-dev}} + env_name=${APP_ENV:-dev} port=8000 if [ $# -gt 0 ]; then case "$1" in @@ -92,7 +99,25 @@ case "$command" in if [ "$env_name" = default ]; then env_name=dev fi - APP_ENV="$env_name" ITI_ENV="$env_name" uv run uvicorn main:app --reload --port "$port" + APP_ENV="$env_name" uv run uvicorn main:app --reload --port "$port" + ;; + docker-build) + docker compose build + ;; + docker-up) + docker compose up -d --build + ;; + docker-up-db) + docker compose -f docker-compose.yml -f docker-compose.with-db.yml up -d --build + ;; + docker-down) + docker compose down + ;; + docker-down-db) + docker compose -f docker-compose.yml -f docker-compose.with-db.yml down + ;; + docker-logs) + docker compose logs -f app ;; migrate) uv run alembic -c migrations/alembic.ini upgrade head @@ -115,18 +140,18 @@ case "$command" in uv run iti-system migrations sync --target migrations/versions ;; system-seed) - uv run iti-system seed system main:app + PYTHONPATH=. uv run iti-system seed system app:create_app ;; init-system) uv run iti-system migrations sync --target migrations/versions uv run alembic -c migrations/alembic.ini upgrade head - uv run iti-system seed system main:app + PYTHONPATH=. uv run iti-system seed system app:create_app ;; {% endif %} init) uv sync --extra dev {% if include_system %} uv run iti-system migrations sync --target migrations/versions {% endif %} uv run alembic -c migrations/alembic.ini upgrade head -{% if include_system %} uv run iti-system seed system main:app +{% if include_system %} PYTHONPATH=. uv run iti-system seed system app:create_app {% endif %} ;; *) echo "未知命令:$command" >&2 diff --git a/copier-template/app/__init__.py.jinja b/copier-template/app/__init__.py.jinja index 71ea3d8..2540cfd 100644 --- a/copier-template/app/__init__.py.jinja +++ b/copier-template/app/__init__.py.jinja @@ -1 +1,3 @@ -"""{{ project_name }} package.""" +from .app_factory import create_app + +__all__ = ["create_app"] diff --git a/copier-template/app/app_factory.py.jinja b/copier-template/app/app_factory.py.jinja new file mode 100644 index 0000000..868b63c --- /dev/null +++ b/copier-template/app/app_factory.py.jinja @@ -0,0 +1,27 @@ +from __future__ import annotations + +import os + +from iti import create_app as create_framework_app +{% if include_system %} +from iti_system import create_system_module +{% endif %} +from config import config +from app.modules.example.module import ExampleModule + + +def create_app(config_name: str | None = None, config_overrides: dict | None = None): + config_name = config_name or os.getenv("APP_ENV", "dev") + modules = [ExampleModule()] +{% if include_system %} + modules.append(create_system_module()) +{% endif %} + app = create_framework_app( + config_name=config_name, + config_mapping=config, + modules=modules, + ) + if config_overrides: + for key, value in config_overrides.items(): + setattr(app.state.config, key.lower(), value) + return app diff --git a/copier-template/config.py.jinja b/copier-template/config.py.jinja index 2f7f7bd..b6618e2 100644 --- a/copier-template/config.py.jinja +++ b/copier-template/config.py.jinja @@ -1,55 +1,45 @@ -import os +from __future__ import annotations from pathlib import Path from iti.config import ( - BaseConfig, DevConfig as BaseDevConfig, + TestConfig as BaseTestConfig, ProdConfig as BaseProdConfig, ) BASE_DIR = Path(__file__).resolve().parent +APP_NAME = "{{ project_name }}" + + +def runtime_path(name: str) -> str: + return str(BASE_DIR / "runtime" / name) + + +def apply_project_config(config) -> None: + config.app_name = APP_NAME + config.base_dir = BASE_DIR + config.file_storage["LOCAL"]["base_path"] = runtime_path("uploads") + config.log_dir = runtime_path("logs") class DevConfig(BaseDevConfig): def __init__(self) -> None: super().__init__() - self.app_name = "{{ project_name }}" - self.base_dir = BASE_DIR - self.file_storage["LOCAL"]["base_path"] = str(BASE_DIR / "runtime" / "uploads") - self.log_dir = str(BASE_DIR / "runtime" / "logs") + apply_project_config(self) -class TestConfig(BaseConfig): +class TestConfig(BaseTestConfig): def __init__(self) -> None: - super().__init__( - app_name="{{ project_name }}", - app_env="test", - testing=True, - database_url=os.getenv( - "DATABASE_URL", - f"mysql+pymysql://{os.getenv('MYSQL_USER', 'root')}:" - f"{os.getenv('MYSQL_PASSWORD', 'password')}@" - f"{os.getenv('MYSQL_HOST', '127.0.0.1')}:" - f"{os.getenv('MYSQL_PORT', '3306')}/" - f"{os.getenv('MYSQL_DATABASE', 'app_test')}?charset=utf8mb4", - ), - base_dir=BASE_DIR, - ratelimit_enabled=False, - log_file_enabled=False, - ) - self.app_name = "{{ project_name }}" - self.base_dir = BASE_DIR - self.log_dir = str(BASE_DIR / "runtime" / "logs") + super().__init__() + apply_project_config(self) class ProdConfig(BaseProdConfig): def __init__(self) -> None: super().__init__() - self.app_name = "{{ project_name }}" - self.base_dir = BASE_DIR - self.log_dir = str(BASE_DIR / "runtime" / "logs") + apply_project_config(self) config = { diff --git a/copier-template/docker-compose.with-db.yml.jinja b/copier-template/docker-compose.with-db.yml.jinja new file mode 100644 index 0000000..9b5ba56 --- /dev/null +++ b/copier-template/docker-compose.with-db.yml.jinja @@ -0,0 +1,31 @@ +services: + app: + environment: + MYSQL_HOST: db + MYSQL_PORT: 3306 + depends_on: + db: + condition: service_healthy + + db: + image: mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root-password} + MYSQL_DATABASE: ${MYSQL_DATABASE:-app} + MYSQL_USER: ${MYSQL_USER:-app} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change-me} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + ports: + - "${MYSQL_PORT:-3306}:3306" + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + mysql-data: diff --git a/copier-template/docker-compose.yml.jinja b/copier-template/docker-compose.yml.jinja new file mode 100644 index 0000000..338092f --- /dev/null +++ b/copier-template/docker-compose.yml.jinja @@ -0,0 +1,26 @@ +services: + app: + build: + context: . + image: {{ project_slug | replace('_', '-') }}:local + environment: + APP_ENV: ${APP_ENV:-prod} + SECRET_KEY: ${SECRET_KEY:-change-me} + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-me} + MYSQL_HOST: ${MYSQL_HOST:-host.docker.internal} + MYSQL_PORT: ${MYSQL_PORT:-3306} + MYSQL_DATABASE: ${MYSQL_DATABASE:-app} + MYSQL_USER: ${MYSQL_USER:-app} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change-me} + LOG_FILE_ENABLED: ${LOG_FILE_ENABLED:-true} + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "${APP_PORT:-8000}:8000" + volumes: + - ./runtime:/app/runtime + command: > + sh -c "{% if include_system %}uv run iti-system migrations sync --target migrations/versions && + {% endif %}uv run alembic -c migrations/alembic.ini upgrade head && + {% if include_system %}PYTHONPATH=. uv run iti-system seed system app:create_app && + {% endif %}uv run uvicorn main:app --host 0.0.0.0 --port 8000" diff --git a/copier-template/main.py.jinja b/copier-template/main.py.jinja index dddeb78..4d256fe 100644 --- a/copier-template/main.py.jinja +++ b/copier-template/main.py.jinja @@ -1,22 +1,4 @@ -import os +from app import create_app -from iti import create_app -{% if include_system %}from iti_system import create_system_module -{% endif -%} -from config import config -from app.modules.example.module import ExampleModule - - -modules = [ - ExampleModule(), -{% if include_system %} create_system_module(), -{% endif -%} -] - - -app = create_app( - config_name=os.getenv("APP_ENV", os.getenv("ITI_ENV", "dev")), - config_mapping=config, - modules=modules, -) +app = create_app() diff --git a/copier-template/migrations/env.py.jinja b/copier-template/migrations/env.py.jinja index 3dabc06..1fdb3ad 100644 --- a/copier-template/migrations/env.py.jinja +++ b/copier-template/migrations/env.py.jinja @@ -30,7 +30,7 @@ target_metadata = Base.metadata def get_url() -> str: - env_name = os.getenv("APP_ENV", os.getenv("ITI_ENV", "dev")) + env_name = os.getenv("APP_ENV", "dev") config_cls = app_config.get(env_name, app_config["default"]) return os.getenv("DATABASE_URL") or config_cls().database_url diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0517e6d..a4a9fa5 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -6,8 +6,8 @@ ## 环境选择 ```bash -ITI_ENV=dev uv run uvicorn main:app --reload -ITI_ENV=prod uv run uvicorn main:app +APP_ENV=dev uv run uvicorn main:app --reload +APP_ENV=prod uv run uvicorn main:app ``` 也可以显式传入: @@ -47,10 +47,10 @@ MYSQL_DATABASE=iti_dev 框架会从当前目录加载第一个存在的文件: 1. `.env.local` -2. `.env.` +2. `.env.` 3. `.env` -`ITI_ENV_DIR` 可指定查找目录。 +`APP_ENV_DIR` 可指定查找目录。 ## 常用字段 diff --git a/docs/COPIER_TEMPLATE.md b/docs/COPIER_TEMPLATE.md index 67d2e3e..d992127 100644 --- a/docs/COPIER_TEMPLATE.md +++ b/docs/COPIER_TEMPLATE.md @@ -25,6 +25,7 @@ copier-template/.codex/skills/{{ project_slug | lower | replace('_', '-') }}-pro ```bash ./iti.sh make-app ../my-business-app my-business-app cd ../my-business-app +cp .env.example .env ./app.sh init ./app.sh serve 8000 ``` @@ -56,9 +57,16 @@ app.cmd serve 8000 ## 生成内容 - `main.py` +- `app/app_factory.py` - `config.py` - `pyproject.toml` - `migrations/` +- `Dockerfile` +- `docker-compose.yml` +- `docker-compose.with-db.yml` +- `.dockerignore` +- `.vscode/launch.json` +- `.env.example` - 示例 FastAPI 模块 - 示例 SQLAlchemy 模型 - 示例测试 @@ -94,6 +102,11 @@ app.cmd init-system ./app.sh install ./app.sh test ./app.sh serve 8000 +./app.sh docker-up +./app.sh docker-up-db +./app.sh docker-logs +./app.sh docker-down +./app.sh docker-down-db ./app.sh migration "alice add order table" ./app.sh migrate ``` @@ -138,5 +151,23 @@ app.cmd init-system 模板更新前,业务项目工作区必须干净。 执行后检查 diff,再运行测试。 -模板拥有的文件包括 `main.py`、`config.py`、`app.sh`、`app.cmd`、`pyproject.toml`、`migrations/`、示例模块、示例测试、README 和项目 skill。 +模板拥有的文件包括 `main.py`、`app/app_factory.py`、`config.py`、`Dockerfile`、`docker-compose.yml`、`docker-compose.with-db.yml`、`.dockerignore`、`.env.example`、`.vscode/launch.json`、`app.sh`、`app.cmd`、`pyproject.toml`、`migrations/`、示例模块、示例测试、README 和项目 skill。 业务项目自己的模块、模型、API 文档和业务 README 由业务项目维护。 + +## Docker + +模板生成: + +- `Dockerfile`:基于 `python:3.11-slim`,使用 `uv sync --frozen --no-dev` 安装运行依赖。 +- `docker-compose.yml`:启动应用,默认连接外部 MySQL。 +- `docker-compose.with-db.yml`:叠加启动 MySQL 8.4。 +- `.env.example`:Compose 和本地运行共用的环境变量样例。 +- `.dockerignore`:排除虚拟环境、运行数据和本地密钥。 + +应用容器启动时会执行业务 migration。 +带 `iti-system` 的项目还会先同步 system migration,并执行 system seed。 + +## VSCode + +模板生成 `.vscode/launch.json`。 +默认调试配置以 `uvicorn main:app --reload` 启动,使用 `APP_ENV=dev`。 diff --git a/docs/SEEDS.md b/docs/SEEDS.md index 48251d1..085bebc 100644 --- a/docs/SEEDS.md +++ b/docs/SEEDS.md @@ -16,7 +16,7 @@ 系统包提供: ```bash -uv run iti-system seed system main:app +PYTHONPATH=. uv run iti-system seed system app:create_app ``` 它会写入默认角色、管理员、系统菜单、字典和配置。 diff --git a/docs/TESTING_DEPLOYMENT.md b/docs/TESTING_DEPLOYMENT.md index 42f74ad..9105c6b 100644 --- a/docs/TESTING_DEPLOYMENT.md +++ b/docs/TESTING_DEPLOYMENT.md @@ -33,7 +33,7 @@ CREATE DATABASE my_business_app_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unico export DATABASE_URL='mysql+pymysql://root:password@127.0.0.1:3306/iti_test?charset=utf8mb4' uv run alembic upgrade head uv run iti-system migrations sync --target migrations/versions -uv run iti-system seed system main:app +PYTHONPATH=. uv run iti-system seed system app:create_app uv run uvicorn main:app --reload ``` @@ -48,7 +48,23 @@ curl http://127.0.0.1:8000/ready ## Docker Compose 健康检查 -服务容器可使用: +模板生成项目已经包含 Docker Compose: + +```bash +cp .env.example .env +./app.sh docker-up +./app.sh docker-logs +./app.sh docker-down +``` + +如需本地 MySQL: + +```bash +./app.sh docker-up-db +./app.sh docker-down-db +``` + +服务容器使用: ```yaml healthcheck: diff --git a/iti/app.py b/iti/app.py index 67e1827..f61b5b0 100644 --- a/iti/app.py +++ b/iti/app.py @@ -36,7 +36,7 @@ from sqlalchemy.exc import SQLAlchemyError from iti.auth.permissions import StaticPermissionProvider from iti.audit import init_audit from iti.cache import CacheManager -from iti.config import BaseConfig, get_config +from iti.config import BaseConfig, get_config, get_env_name from iti.db import configure_db from iti.exceptions import ItiError from iti.health import router as health_router @@ -282,7 +282,7 @@ def _resolve_config( if config_mapping is None: return get_config(config_name) if isinstance(config_mapping, Mapping): - env_name = config_name or "dev" + env_name = config_name or get_env_name() value = config_mapping.get(env_name, config_mapping.get("default")) if value is None: return get_config(config_name) diff --git a/iti/config.py b/iti/config.py index 207d9fb..8111fec 100644 --- a/iti/config.py +++ b/iti/config.py @@ -11,9 +11,13 @@ from dotenv import load_dotenv BASE_DIR = Path(os.getenv("ITI_BASE_DIR", Path.cwd())).resolve() +def get_env_name(default: str = "dev") -> str: + return os.getenv("APP_ENV") or default + + def load_env_file(env_dir: str | os.PathLike | None = None) -> bool: - search_dir = Path(env_dir or os.getenv("ITI_ENV_DIR") or Path.cwd()).resolve() - env_name = os.getenv("APP_ENV", os.getenv("ITI_ENV", "dev")) + search_dir = Path(env_dir or os.getenv("APP_ENV_DIR") or Path.cwd()).resolve() + env_name = get_env_name() for name in (".env.local", f".env.{env_name}", ".env"): path = search_dir / name if path.exists(): @@ -176,6 +180,6 @@ config = { def get_config(env_name: str | None = None) -> BaseConfig: - env_name = env_name or os.getenv("APP_ENV", os.getenv("ITI_ENV", "dev")) + env_name = env_name or get_env_name() config_cls = config.get(env_name, config["default"]) return config_cls()