Skip to content

第一个完整项目——内存版待办事项 ​

这一章要把哪些知识串起来 ​

一个任务包含编号、标题、描述和完成状态。我们实现创建、列表、详情、完整替换、局部更新、删除六种操作。

这里使用 Python 字典存储数据,目的是先把接口流程学清楚。服务重启后数据消失,多个进程也不共享这份字典;第 15 章讲 MySQL 基础,第 26~38 章完成持久化版本。

接口约定 ​

方法路径输入成功结果
POST/todos创建模型201,返回任务
GET/todos筛选和分页查询参数200,返回列表
GET/todos/{todo_id}路径编号200,返回任务
PUT/todos/{todo_id}完整任务内容200,返回替换后任务
PATCH/todos/{todo_id}部分字段200,返回更新后任务
DELETE/todos/{todo_id}路径编号204,无正文

完整示例:todo_memory.py ​

python
from itertools import count
from typing import Annotated

from fastapi import FastAPI, HTTPException, Query, Response
from pydantic import BaseModel, ConfigDict, Field, field_validator

app = FastAPI(title="内存版待办事项")


class TodoCreate(BaseModel):
    model_config = ConfigDict(extra="forbid")

    title: str = Field(min_length=1, max_length=100)
    description: str | None = Field(default=None, max_length=500)
    done: bool = False

    @field_validator("title")
    @classmethod
    def clean_title(cls, value: str) -> str:
        value = value.strip()
        if not value:
            raise ValueError("标题不能只有空格")
        return value


class TodoPatch(BaseModel):
    model_config = ConfigDict(extra="forbid")

    title: str | None = Field(default=None, min_length=1, max_length=100)
    description: str | None = Field(default=None, max_length=500)
    done: bool | None = None

    @field_validator("title")
    @classmethod
    def clean_title(cls, value: str | None) -> str:
        if value is None or not value.strip():
            raise ValueError("标题不能为 null 或只有空格")
        return value.strip()

    @field_validator("done")
    @classmethod
    def check_done(cls, value: bool | None) -> bool:
        if value is None:
            raise ValueError("完成状态不能为 null")
        return value


class TodoPublic(TodoCreate):
    id: int


todos: dict[int, TodoPublic] = {}
id_counter = count(1)


def find_todo(todo_id: int) -> TodoPublic:
    todo = todos.get(todo_id)
    if todo is None:
        raise HTTPException(status_code=404, detail="任务不存在")
    return todo


@app.post("/todos", response_model=TodoPublic, status_code=201)
async def create_todo(data: TodoCreate):
    todo_id = next(id_counter)
    todo = TodoPublic(id=todo_id, **data.model_dump())
    todos[todo_id] = todo
    return todo


@app.get("/todos", response_model=list[TodoPublic])
async def list_todos(
    done: bool | None = None,
    skip: Annotated[int, Query(ge=0)] = 0,
    limit: Annotated[int, Query(ge=1, le=100)] = 10,
):
    items = sorted(todos.values(), key=lambda todo: todo.id)
    if done is not None:
        items = [todo for todo in items if todo.done == done]
    return items[skip : skip + limit]


@app.get("/todos/{todo_id}", response_model=TodoPublic)
async def read_todo(todo_id: int):
    return find_todo(todo_id)


@app.put("/todos/{todo_id}", response_model=TodoPublic)
async def replace_todo(todo_id: int, data: TodoCreate):
    find_todo(todo_id)
    todo = TodoPublic(id=todo_id, **data.model_dump())
    todos[todo_id] = todo
    return todo


@app.patch("/todos/{todo_id}", response_model=TodoPublic)
async def update_todo(todo_id: int, data: TodoPatch):
    old_todo = find_todo(todo_id)
    changes = data.model_dump(exclude_unset=True)
    merged = {**old_todo.model_dump(), **changes}
    updated = TodoPublic.model_validate(merged)
    todos[todo_id] = updated
    return updated


@app.delete("/todos/{todo_id}", status_code=204)
async def delete_todo(todo_id: int):
    find_todo(todo_id)
    del todos[todo_id]
    return Response(status_code=204)

启动:

powershell
python -m uvicorn todo_memory:app --reload

为什么 PATCH 模型这样写 ​

PATCH 的核心是分清三种意图:

请求意图本示例行为
{}没有指定变更保持原数据
{"done": true}标记完成仅修改完成状态
{"description": null}清空描述描述变成 None
{"title": null}将标题设为空拒绝,返回 422
{"done": null}将完成状态设为空拒绝,返回 422

PATCH 模型给字段设置默认值以允许省略。字段校验器默认不会因为一个字段被省略就运行;客户端显式提供值时才进行相应检查。这个例子因此允许省略 title,同时拒绝显式 null。

exclude_unset=True 保留“传过哪些字段”的信息;用 exclude_none=True 会错误地丢掉“清空描述”这种明确意图。官方局部更新说明

为什么合并后再校验 ​

TodoPatch 检查的是修改请求;TodoPublic.model_validate(merged) 检查的是修改完成后的整个任务。

避免认为 model_copy(update=...) 一定会重新校验更新内容。需要保证合并结果仍然合法时,可以像这里一样重新构建或校验模型。Pydantic 模型复制说明

手动验证顺序 ​

  1. POST 创建 {"title": "学习请求参数", "description": "完成第 5 章"}。
  2. 记录响应中的 id,不要假设永远为 1。
  3. GET 读取该任务。
  4. PATCH 提交 {"done": true}。
  5. GET /todos?done=true,确认列表包含它。
  6. PATCH 提交 {"description": null},确认描述被清空。
  7. 提交 {"title": " "},应得到 422。
  8. DELETE 删除任务,响应应是 204 且没有正文。
  9. 再次 GET,该任务应返回 404。

本章练习 ​

为列表添加 keyword 搜索,搜索标题;再添加 priority,只允许 low、normal、high。

过关标准:不看原代码,也能写出创建、详情、PATCH 和删除流程,并解释模型和状态码。