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

FastAPI — 类型就是契约

无需收集全部数据的 JSON Lines 导出

在 TT Lab 中继续学习

目标

把惰性生成、行边界、公开字段和输出上限,与 HTTP 流连接起来。

为什么重要

管理员一下载所有订单,服务器内存就猛增。原因是 export 函数在生成 JSON 之前,先把所有行都收集进了列表。后来改成了按行输出,可是正文里的换行破坏了真正的行边界,内部成本也原样发了出去。流式传输不是改一下返回类型就行,而是要一并确定惰性求值和表示契约。

步骤

  1. 在 /root/work/fa-jsonl-export-lab/service.py 中,validate_row(row) 在它是 dict,id 是排除 bool 的正 int,name 是非空 str 时,返回 row。其余都是 ValueError。允许有额外的内部字段。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/fa-jsonl-export-lab
test -e /root/work/fa-jsonl-export-lab/service.py || cp /opt/fixtures/ten_labs/fa-jsonl-export-lab/service.py /root/work/fa-jsonl-export-lab/service.py
cd /root/work/fa-jsonl-export-lab
  1. 在 /root/work/fa-jsonl-export-lab/service.py 中,project(row) 在 validate_row 之后,返回只含 id 和 name 的新 dict。原件的内部字段保持原样。

  2. 在 /root/work/fa-jsonl-export-lab/service.py 中,encode_line(row) 是一个 str:把 project 的结果用 ensure_ascii=False、separators=(',',':')、sort_keys=True 编码为 JSON,并在末尾附加一个 ' '。name 中的换行必须是 JSON 转义。

  3. 在 /root/work/fa-jsonl-export-lab/service.py 中,validate_max(value) 只原样返回排除 bool 的 1–1000 的 int,其余都是 ValueError。

  4. 在 /root/work/fa-jsonl-export-lab/service.py 中,take_rows(rows, maximum) 是一个 iterator,它借助 islice 等方式,最多只惰性地返回 maximum 条。调用时验证 maximum,每执行一次 next,只消耗一次输入。

  5. 在 /root/work/fa-jsonl-export-lab/service.py 中,json_lines(rows, maximum=100) 对从 take_rows 得到的每一行,yield encode_line。不要返回全部拼接好的字符串或列表。

  6. 在 /root/work/fa-jsonl-export-lab/service.py 中,decode_lines(text) 对 splitlines 中每一个非空的行,先 json.loads,再 validate_row,然后作为列表返回。空字符串是 [],空的中间行是 ValueError。

  7. 在 /root/work/fa-jsonl-export-lab/service.py 中,create_app(rows) 在 GET /export 中,把 json_lines(rows, 100) 作为 application/x-ndjson 的 StreamingResponse 返回。rows 是可以再次遍历的列表。必须没有内部字段,并保留每一行的内容和顺序。

参考

验证行契约

在 /root/work/fa-jsonl-export-lab/service.py 中,validate_row(row) 在它是 dict,id 是排除 bool 的正 int,name 是非空 str 时,返回 row。其余都是 ValueError。允许有额外的内部字段。

先做一次准备。已有的文件不会被覆盖。

mkdir -p /root/work/fa-jsonl-export-lab
test -e /root/work/fa-jsonl-export-lab/service.py || cp /opt/fixtures/ten_labs/fa-jsonl-export-lab/service.py /root/work/fa-jsonl-export-lab/service.py
cd /root/work/fa-jsonl-export-lab

区分 bool 和数字,并把空名称当作错误处理。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/01-contract.sh 确认。

只构造公开的行

在 /root/work/fa-jsonl-export-lab/service.py 中,project(row) 在 validate_row 之后,返回只含 id 和 name 的新 dict。原件的内部字段保持原样。

导出路径也必须应用与普通 API 相同的公开字段策略。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/02-contract.sh 确认。

保持行边界地编码

在 /root/work/fa-jsonl-export-lab/service.py 中,encode_line(row) 是一个 str:把 project 的结果用 ensure_ascii=False、separators=(',',':')、sort_keys=True 编码为 JSON,并在末尾附加一个 ' '。name 中的换行必须是 JSON 转义。

如果用字符串拼接来生成 JSON,格式会在引号和换行处被破坏。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/03-contract.sh 确认。

验证输出数量上限

在 /root/work/fa-jsonl-export-lab/service.py 中,validate_max(value) 只原样返回排除 bool 的 1–1000 的 int,其余都是 ValueError。

要求调用者给出上限,以免意外地把无限输入一直读到底。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/04-contract.sh 确认。

只消耗需要的行

在 /root/work/fa-jsonl-export-lab/service.py 中,take_rows(rows, maximum) 是一个 iterator,它借助 islice 等方式,最多只惰性地返回 maximum 条。调用时验证 maximum,每执行一次 next,只消耗一次输入。

一旦转成 list(rows),就无法处理无限输入和大容量输入。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/05-contract.sh 确认。

惰性序列化行

在 /root/work/fa-jsonl-export-lab/service.py 中,json_lines(rows, maximum=100) 对从 take_rows 得到的每一行,yield encode_line。不要返回全部拼接好的字符串或列表。

让对象的选择和表示的转换,各自都保持为惰性的步骤。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/06-contract.sh 确认。

重新验证下载得到的行

在 /root/work/fa-jsonl-export-lab/service.py 中,decode_lines(text) 对 splitlines 中每一个非空的行,先 json.loads,再 validate_row,然后作为列表返回。空字符串是 [],空的中间行是 ValueError。

区分空文件与格式损坏的空记录。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/07-contract.sh 确认。

完成 HTTP 下载

在 /root/work/fa-jsonl-export-lab/service.py 中,create_app(rows) 在 GET /export 中,把 json_lines(rows, 100) 作为 application/x-ndjson 的 StreamingResponse 返回。rows 是可以再次遍历的列表。必须没有内部字段,并保留每一行的内容和顺序。

不能只把 Content-Type 写成流式,还要配合生成器测试,确认内部并没有把全部内容收集起来。

保存后用 bash /opt/lab/checks/fa-jsonl-export-lab/08-contract.sh 确认。