TT Lab
开始
学习 学习路径 课程

FastAPI — 类型就是契约

使用 ETag 防止更新被覆盖

在 TT Lab 中继续学习

目标

当两个用户修改同一份草稿时,用 SQL 条件更新和 HTTP 响应来防止更新丢失。

为什么重要

一次正常请求的成功,并不能保证边界值和故障恢复。本实验先把每个函数的契约以小规模实现或测试,再把它们连接到真实运行中。评分不是只看代码是否存在或报告里的措辞,而是检查结果、异常和存储状态。请保留前面步骤的代码,继续进行下一步。

步骤

  1. 在 /root/work/fa-conditional-lab/service.py 中,让 init_db(path) 在 SQLite 文件中创建 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)。重复调用也不会清除已有数据。第一步的准备用下面的命令完成。
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
  1. 在 /root/work/fa-conditional-lab/service.py 中,让 create_note(path, note_id, title) 以版本 1 INSERT 一条备忘并提交。相同的 id 以 sqlite3.IntegrityError 拒绝,并且不覆盖已有的标题。
  2. 在 /root/work/fa-conditional-lab/service.py 中,让 read_note(path, note_id) 返回 {id, title, version} 字典或 None。不要把 id 用字符串拼接进 SQL,而要使用绑定。
  3. 在 /root/work/fa-conditional-lab/service.py 中,让 etag(version) 把整数版本返回为包含双引号的字符串。例如:版本 3 对应的字符串是 '"v3"'。
  4. 在 /root/work/fa-conditional-lab/service.py 中,让 parse_match(header) 只接受双引号内为 v 加一个正的十进制整数的标签,并返回版本。None、weak 标签、通配符、列表、v0 等都是 ValueError。例如:'"v12"' → 12。
  5. 在 /root/work/fa-conditional-lab/service.py 中,让 update_note(path, note_id, title, version) 只有在 id 和 version 都一致时才更新标题,并把版本加 1。有一行被修改时返回 True,没有时返回 False。即使是并发请求,赢家也必须只有一个。
  6. 在 /root/work/fa-conditional-lab/service.py 中,让 put_note(path, note_id, title, header) 对不存在的 id 返回 404,缺少条件返回 428,不支持的条件或版本不一致返回 412,成功返回 204。拒绝时不要修改 DB。
  7. 在 /root/work/fa-conditional-lab/service.py 中,让 create_app(path) 提供 GET /notes/{note_id} 和 PUT /notes/{note_id}。GET 返回备忘 JSON 和 ETag,PUT 接收 {title: 1–100 个字符的字符串} 和 If-Match,并按上述结果码返回空响应。空标题为 422,GET 不存在的备忘为 404。

参考

创建带有版本的存储

在 /root/work/fa-conditional-lab/service.py 中,让 init_db(path) 在 SQLite 文件中创建 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)。重复调用也不会清除已有数据。第一步的准备用下面的命令完成。

mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab

使用 CREATE TABLE IF NOT EXISTS,并用 with sqlite3.connect(path) 确定提交的边界。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/01-contract.sh 直接复现。保存文件后请重新运行。

固定首次保存的版本

在 /root/work/fa-conditional-lab/service.py 中,让 create_note(path, note_id, title) 以版本 1 INSERT 一条备忘并提交。相同的 id 以 sqlite3.IntegrityError 拒绝,并且不覆盖已有的标题。

INSERT OR REPLACE 会把重复创建变成更新。这一步的契约是创建新备忘。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/02-contract.sh 直接复现。保存文件后请重新运行。

区分不存在的备忘和存在的备忘

在 /root/work/fa-conditional-lab/service.py 中,让 read_note(path, note_id) 返回 {id, title, version} 字典或 None。不要把 id 用字符串拼接进 SQL,而要使用绑定。

先检查 fetchone 是否为 None。不要直接返回元组。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/03-contract.sh 直接复现。保存文件后请重新运行。

生成强 ETag

在 /root/work/fa-conditional-lab/service.py 中,让 etag(version) 把整数版本返回为包含双引号的字符串。例如:版本 3 对应的字符串是 '"v3"'。

HTTP 标签中的双引号不是说明用的记号,而是实际的头部值的一部分。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/04-contract.sh 直接复现。保存文件后请重新运行。

只接受支持的条件语法

在 /root/work/fa-conditional-lab/service.py 中,让 parse_match(header) 只接受双引号内为 v 加一个正的十进制整数的标签,并返回版本。None、weak 标签、通配符、列表、v0 等都是 ValueError。例如:'"v12"' → 12。

使用 re.fullmatch,标签后面多出来的字符串就不会被默许。本实验并不是实现完整的 HTTP 标签语法。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/05-contract.sh 直接复现。保存文件后请重新运行。

把比较和写入合并成一条 SQL

在 /root/work/fa-conditional-lab/service.py 中,让 update_note(path, note_id, title, version) 只有在 id 和 version 都一致时才更新标题,并把版本加 1。有一行被修改时返回 True,没有时返回 False。即使是并发请求,赢家也必须只有一个。

查看 UPDATE ... WHERE id=? AND version=? 的 rowcount。先 SELECT 再无条件 UPDATE,无法阻止竞争。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/06-contract.sh 直接复现。保存文件后请重新运行。

把失败归类为 HTTP 结果

在 /root/work/fa-conditional-lab/service.py 中,让 put_note(path, note_id, title, header) 对不存在的 id 返回 404,缺少条件返回 428,不支持的条件或版本不一致返回 412,成功返回 204。拒绝时不要修改 DB。

先判断资源是否存在,再解析条件。不要通过重新查询把失败变成成功。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/07-contract.sh 直接复现。保存文件后请重新运行。

用 HTTP 重现保存过期页面

在 /root/work/fa-conditional-lab/service.py 中,让 create_app(path) 提供 GET /notes/{note_id} 和 PUT /notes/{note_id}。GET 返回备忘 JSON 和 ETag,PUT 接收 {title: 1–100 个字符的字符串} 和 If-Match,并按上述结果码返回空响应。空标题为 422,GET 不存在的备忘为 404。

用 Header 接收 If-Match,并用 TestClient 先 GET,再把同一个标签发送两次试试。只有第一次修改生效,新的 GET 必须是 v2。

评分可以用 bash /opt/lab/checks/fa-conditional-lab/08-contract.sh 直接复现。保存文件后请重新运行。