第 4 周(9/28 一 – 10/4 日)+ 收官(10/5 – 10/7):Web 服务、架构与封装、综合项目¶
本周目标:HTTP/REST/OpenAPI 基础;FastAPI(三个工作日晚上各一块 + 假期深化):参数与 Pydantic、依赖注入、路由拆分、lifespan、异步端点、后台任务、SSE、测试;架构与封装(分层、
Protocol接口、依赖注入、错误边界);安全基础;在 Linux 服务器上运行。10/2 起进入综合项目 Notebook Service(规格见plan/capstone.md)。时长:周一–周三 4h × 3 + 国庆 10/1–10/4 10h × 4 = 52h;收官 10/5–10/7 10h × 3 = 30h。
版本基准(2026-09-05 核实):FastAPI 0.141.x(0.135.0 起内置
fastapi.sse的EventSourceResponse)、Starlette 1.x、uvicorn 随fastapi[standard]安装;详见research/11。对应
learn-agent:M17 → Week 4 的 FastAPI + SSE 接口;M18 → agent 的可测试架构(工具层 / 编排层 / 存储层);M19 → 部署到 H20 服务器。
周一 9/28(4h)|M17a HTTP、REST 与 FastAPI 入门¶
块 1(复习):第 3 周周测错题;聚合器遗留。
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| HTTP 心智模型:请求(方法/路径/查询/头/体)→ 响应(状态码/头/体);GET 幂等、POST 创建、PUT/PATCH 更新、DELETE;常用状态码 200/201/204/400/401/403/404/409/422/500 | A |
| JSON 作为传输格式;OpenAPI(Swagger)= 用 JSON Schema 描述接口;FastAPI 自动生成 /docs、/openapi.json | A |
| fastapi[standard] 安装了什么;fastapi dev app/main.py(开发热重载)vs uvicorn pkg.main:app | A |
| 路径参数(类型自动转换与校验)、查询参数(默认值、Annotated[int, Query(ge=1)])、请求体(Pydantic 模型)、response_model、status_code=201 | A |
| HTTPException(404, detail=...);Pydantic 校验失败自动 422 及其错误体结构 | A |
| 用 TestClient(基于 httpx)写第一个 API 测试 | A |
块 3(练习):exercises/week4/w4_01_fastapi_basics/(app.py 骨架 + test_app.py 已给 10 个测试):GET /health;GET /notes/{id}(404);GET /notes?tag=&limit=(查询校验);POST /notes(201 + response_model=NoteOut,输入 NoteIn 校验标题非空);DELETE /notes/{id}(204);内存字典存储。验收:测试全绿;打开 /docs 手动调一次。
周二 9/29(4h)|M17b 依赖注入、路由拆分、lifespan、设置¶
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| Depends:把"拿数据库连接/当前设置/当前用户"变成可替换的函数;Annotated[Repo, Depends(get_repo)] 写法;yield 依赖做清理 | A |
| APIRouter(prefix="/notes", tags=["notes"]) 拆文件;app.include_router | A |
| lifespan 异步上下文:启动时建连接池/加载配置,关闭时释放(替代已弃用的 on_event);app.state | A |
| 设置:pydantic-settings + @lru_cache def get_settings() 依赖 | A |
| 自定义异常 → @app.exception_handler(NotFoundError) 映射为 HTTP 响应:领域层不 import FastAPI | A |
| 测试:app.dependency_overrides[get_repo] = lambda: InMemoryRepo();TestClient(app) 作为上下文管理器触发 lifespan | A |
练习:w4_02_fastapi_structure/:把周一的单文件拆成 main.py(创建 app、lifespan、异常处理器)、routers/notes.py、deps.py(get_settings/get_repo)、schemas.py、repository.py(Protocol + 内存实现)、errors.py;测试用 dependency_overrides 注入内存仓储并验证 404 由领域异常映射而来。
周三 9/30(4h)|M17c 异步端点、后台任务、SSE、中间件、鉴权¶
块 2(新知识):
| 知识点 | 档 |
| --- | --- |
| async def 端点 vs def 端点:同步端点在线程池里跑,异步端点里绝不能调阻塞函数(否则整个服务卡住);何时选哪个 | A |
| BackgroundTasks:响应后继续做(发通知、写日志);长任务用队列(知道) | A |
| SSE:from fastapi.sse import EventSourceResponse, ServerSentEvent(0.135+);异步生成器逐条 yield;text/event-stream;客户端 httpx 用 stream 读取或 httpx-sse;与 StreamingResponse(任意分块)的区别;WebSocket 知道存在 | A |
| 中间件:请求日志(耗时、状态码、请求 id);CORS 为什么不写 * | B |
| 最小鉴权:Depends(verify_api_key) 读 X-API-Key 头,对比 settings;OAuth2/JWT 知道存在 | B |
| 异步测试:httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test");测 SSE 用 stream 读前 N 条 | A |
练习:w4_03_fastapi_async_sse/:GET /events/countdown?n=5 SSE 每 0.05s 一条 ServerSentEvent(data=json, event="tick"),结束发 event="done";POST /import 用 BackgroundTasks 模拟导入并把进度写入内存状态;GET /slow-sync vs GET /slow-async 各 sleep(0.2) 后返回,测试并发 5 个请求两者耗时对比;请求日志中间件;X-API-Key 保护 /admin/stats;异步测试文件。
收尾:读 plan/capstone.md,画 Notebook Service 的模块图;预习 research/11 第四节项目结构。
周四 10/1(国庆,10h)|M18 架构与封装 + M19 安全与服务器¶
上午 3 块 — M18 架构与封装(A 档):
- 分层:routers(协议层,只做 HTTP ↔ 模型转换)→ services(用例/业务规则)→ repositories(存储,Protocol 接口 + SQLite/内存实现);schemas(Pydantic,传输模型)与 domain(dataclass,领域模型)分开,边界处映射。
- 依赖注入:构造函数传入依赖(NoteService(repo)),测试时传假实现;FastAPI 的 Depends 只是这件事的框架版。
- 错误边界:领域异常层次(NotFound/Conflict/Validation)在服务层抛,在协议层统一映射;不要让 HTTPException 渗入服务层。
- 纯核心 + 命令式外壳:计算不做 I/O,I/O 不做计算;这直接决定可测试性。
- Python 里真正常用的模式:策略 = 传函数;注册表 = 装饰器 + dict;工厂 = @classmethod;适配器 = 包一层实现 Protocol;观察者 = 回调列表;单例 = 模块级实例或 lru_cache。不要为了模式而模式。
- 《Cosmic Python》前 6 章的核心:领域模型、仓储、服务层、工作单元(知道)。
- 代码坏味道清单:函数 > 40 行、参数 > 5 个、布尔参数控制分支、dict 到处传、全局可变状态、try 包住 50 行。
下午前 2 块 — 练习 w4_04_refactor_kata/:给你一个 180 行、把 SQL、校验、格式化、print 全揉在一起的 legacy_notes.py 和一套针对纯函数与 Protocol 的测试 test_refactored.py(先全红)。目标:不改变行为地重构为 domain.py / repository.py / service.py / cli.py,测试全绿,legacy_notes.py 删除。这是 Notebook Service 的预演。
下午第 3 块 + 晚上 2 块 — M19 安全基础与服务器:
| 知识点 | 档 |
| --- | --- |
| 输入校验全靠 Pydantic 与类型;绝不 eval/exec 用户输入;subprocess 不用 shell=True;路径拼接后 resolve() 并检查仍在允许目录内(路径穿越);pickle 只反序列化自己的数据;密钥只在环境变量;CORS 白名单;错误响应不泄漏堆栈;依赖锁定 uv.lock、pip-audit/uv 审计(知道) | A |
| Linux 服务器最小知识(H20):ssh user@host、SSH key、scp/rsync(或 git clone 到服务器);服务器上 curl -LsSf https://astral.sh/uv/install.sh \| sh → uv sync → uv run uvicorn notebook.main:app --host 127.0.0.1 --port 8000;tmux 保持进程;本机 ssh -L 8000:127.0.0.1:8000 user@host 后浏览器开 localhost:8000/docs;systemd --user 与 Docker 知道存在 | B |
| Linux 与 Windows 差异清单:路径分隔符(pathlib 已抹平)、大小写敏感、python3 命令、文件权限、换行符(Git autocrlf) | B |
练习:exercises/week4/w4_05_security_review.md——对照清单审查 kb 与 aggregator,列出 ≥5 处改进并修掉 3 处;把 aggregator 在服务器上跑起来并用 ssh -L 访问 stub server(截图记进日志)。
周五 10/2 – 周日 10/4(国庆,30h)|综合项目 Notebook Service:里程碑 1–3¶
按 plan/capstone.md 的里程碑推进:M1 领域与仓储(10/2)、M2 服务与 API(10/3)、M3 导入/SSE/CLI(10/4)。每天结束:测试全绿、ruff/Pylance 零错误、提交推送、日志。周日晚 周测(exercises/week4/quiz_week4.md)+ AI 评审。
收官 10/5 一 – 10/7 三(30h)¶
| 日 | 内容 |
|---|---|
| 10/5 | 里程碑 4:日志、配置、鉴权、错误边界收口;覆盖率 ≥80%;README;可选部署到 H20 并用 ssh -L 访问 |
| 10/6 | 里程碑 5:AI 评审(模板 4)→ 重构 ≤5 条 → 回归测试;uv build;打 tag v1.0.0;写复盘(做对了什么、卡在哪、下一步) |
| 10/7 | 结业测验(exercises/final/quiz_final.md,60 分钟闭卷:20 题 + 30 分钟限时编码);checklist.md 进阶 A 档三档复评;把"提/否"的条目排进 D+3/D+7/D+30;对照 learn-agent 的计划,标出哪些 Python 前置已经就位 |
本周阅读(详见 research/11、13)¶
| 模块 | 主读 | 补充 |
|---|---|---|
| M17 | FastAPI 官方教程(中文站):Path/Query/Body → Response Model → Handling Errors → Dependencies → Bigger Applications → Testing → SSE;Advanced:Lifespan、Middleware、Background Tasks | research/11 第四节项目结构 |
| M18 | 《Cosmic Python》第 1–6 章(免费在线);《Python 工匠》封装与解耦章 | ArjanCodes 软件设计视频(仅补充) |
| M19 | FastAPI Deployment 概念页;uv 在 Linux 安装文档 | 《Pro Git》服务器章节(可选) |
本周陷阱清单(周测必考)¶
async def 端点里调 time.sleep/同步 SQLite/requests|服务层 import HTTPException|on_event 老写法|dependency_overrides 忘清理|response_model 漏掉导致泄漏内部字段|SSE 生成器里不 await 导致无法取消|CORS * + 凭证|API key 硬编码|路径拼接不检查穿越|服务器上用 0.0.0.0 暴露无鉴权接口|Windows 换行符污染脚本|uv run uvicorn 忘了 --reload 只在开发用