综合练习
FastAPI 小模块 + pytest 覆盖。
40 分钟
综合实战:课时统计 API
整合本 track 所学:用 FastAPI + asyncio + pathlib + pytest + pyproject 构建「扫描 drafts 并返回统计 JSON」的小服务,作为毕业项目。复习 python-fundamentals;若扩展为 Agent 工具可对接 ai-agents-essentials;继续在 /roadmaps/python 选进阶节点。
学习目标
- 完成可运行的 FastAPI 项目并完成基本测试
- 实践 async 文件扫描、Pydantic 响应模型与错误处理
- 输出 README 与 pyproject,满足可安装、可部署最小标准
需求说明
GET /tracks/{track}/stats:扫描content/课程/{track}/drafts/*.md- 返回每课 slug、字符数、行数、是否含代码块(
```) - 目录不存在时返回 404;支持
?min_chars=500过滤
核心实现 sketch
import asyncio
from pathlib import Path
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
app = FastAPI()
CONTENT_ROOT = Path("content/课程")
class LessonStat(BaseModel):
slug: str
chars: int
lines: int
has_code_block: bool
class TrackStats(BaseModel):
track: str
lessons: list[LessonStat]
async def analyze_file(path: Path) -> LessonStat:
text = await asyncio.to_thread(path.read_text, encoding="utf-8")
return LessonStat(
slug=path.stem,
chars=len(text),
lines=text.count("\n") + (1 if text else 0),
has_code_block="```" in text,
)
@app.get("/tracks/{track}/stats", response_model=TrackStats)
async def track_stats(
track: str,
min_chars: int = Query(default=0, ge=0),
) -> TrackStats:
draft_dir = CONTENT_ROOT / track / "drafts"
if not draft_dir.is_dir():
raise HTTPException(404, f"track not found: {track}")
paths = sorted(draft_dir.glob("*.md"))
stats = await asyncio.gather(*(analyze_file(p) for p in paths))
filtered = [s for s in stats if s.chars >= min_chars]
return TrackStats(track=track, lessons=filtered)
测试要点
def test_track_stats_filters_min_chars(client, tmp_content):
resp = client.get("/tracks/demo/stats?min_chars=100")
assert resp.status_code == 200
data = resp.json()
assert all(l["chars"] >= 100 for l in data["lessons"])
使用 TestClient + fixture 指向 tmp_path 中的假 drafts。
扩展方向
- 加 Redis 缓存统计结果