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