From 67d8c6bf302bfae7054c091c24a67ada64333171 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 7 May 2026 08:25:34 +0800 Subject: [PATCH] feat: add FastAPI example project with CRUD API - Add FastAPI application with health check and item CRUD endpoints - Add Pydantic models for request/response validation - Add pytest test suite with 5 passing tests - Add project documentation and run script Co-Authored-By: Claude Opus 4.7 --- .gitignore | 1 + README.md | 40 +++++++++++++++ app/__init__.py | 1 + app/main.py | 35 +++++++++++++ app/models.py | 13 +++++ docs/plans/2026-05-07-fastapi-example-plan.md | 38 ++++++++++++++ requirements.txt | 4 ++ run.sh | 3 ++ tests/__init__.py | 1 + tests/test_api.py | 49 +++++++++++++++++++ 10 files changed, 185 insertions(+) create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 docs/plans/2026-05-07-fastapi-example-plan.md create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 tests/__init__.py create mode 100644 tests/test_api.py diff --git a/.gitignore b/.gitignore index 7627b94..109d78b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ __pycache__/ *.egg-info/ .eggs/ *.egg +.venv/ # 环境变量 .env diff --git a/README.md b/README.md new file mode 100644 index 0000000..660ca01 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# FastAPI Example + +一个最小化的 FastAPI REST API 示例项目。 + +## 快速开始 + +```bash +# 创建虚拟环境并安装依赖 +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +# 启动服务 +./run.sh + +# 或直接运行 +uvicorn app.main:app --reload +``` + +## API 端点 + +| 方法 | 路径 | 描述 | +|------|------|------| +| GET | /health | 健康检查 | +| GET | /items | 列出所有项目 | +| POST | /items | 创建新项目 | +| GET | /items/{id} | 获取单个项目 | + +## 测试 + +```bash +source .venv/bin/activate +pytest tests/ -v +``` + +## API 文档 + +启动服务后访问: +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..bd737e1 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# app package \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..686b2ed --- /dev/null +++ b/app/main.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, HTTPException +from typing import Dict +from app.models import Item, ItemCreate + +app = FastAPI(title="FastAPI Example") + +# 内存存储 +_items: Dict[int, Item] = {} +_item_id_counter = 0 + + +@app.get("/health") +async def health_check(): + return {"status": "ok"} + + +@app.post("/items", response_model=Item) +async def create_item(item: ItemCreate): + global _item_id_counter + _item_id_counter += 1 + new_item = Item(id=_item_id_counter, **item.model_dump()) + _items[_item_id_counter] = new_item + return new_item + + +@app.get("/items", response_model=list[Item]) +async def list_items(): + return list(_items.values()) + + +@app.get("/items/{item_id}", response_model=Item) +async def get_item(item_id: int): + if item_id not in _items: + raise HTTPException(status_code=404, detail="Item not found") + return _items[item_id] \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..d86d21f --- /dev/null +++ b/app/models.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel +from typing import Optional + + +class Item(BaseModel): + id: Optional[int] = None + name: str + description: Optional[str] = None + + +class ItemCreate(BaseModel): + name: str + description: Optional[str] = None \ No newline at end of file diff --git a/docs/plans/2026-05-07-fastapi-example-plan.md b/docs/plans/2026-05-07-fastapi-example-plan.md new file mode 100644 index 0000000..b42d064 --- /dev/null +++ b/docs/plans/2026-05-07-fastapi-example-plan.md @@ -0,0 +1,38 @@ +# FastAPI 示例项目实现计划 + +日期:2026-05-07 + +## 目标 +搭建一个最小可运行的 FastAPI REST API 示例项目。 + +## 任务清单 + +### 批次 1:项目基础 +| # | 任务 | 文件 | 验证 | 预计时间 | +|---|------|------|------|----------| +| 1 | 创建 requirements.txt | requirements.txt | 文件存在 | 1min | +| 2 | 创建项目目录结构 | app/, tests/ | 目录存在 | 1min | +| 3 | 编写数据模型 | app/models.py | 导入成功 | 2min | + +### 批次 2:核心实现(TDD) +| # | 任务 | 文件 | 验证 | 预计时间 | +|---|------|------|------|----------| +| 4 | 写失败测试:健康检查 | tests/test_api.py | pytest FAIL | 2min | +| 5 | 实现健康检查端点 | app/main.py | pytest PASS | 2min | +| 6 | 写失败测试:CRUD | tests/test_api.py | pytest FAIL | 3min | +| 7 | 实现 CRUD 端点 | app/routes.py | pytest PASS | 5min | + +### 批次 3:收尾 +| # | 任务 | 文件 | 验证 | 预计时间 | +|---|------|------|------|----------| +| 8 | 添加启动脚本 | run.sh | 脚本可执行 | 1min | +| 9 | 更新文档 | README.md | 文件存在 | 2min | + +## 验证命令 +- 单测:`pytest tests/ -v` +- 启动:`uvicorn app.main:app --reload` + +## 完成标准 +- [ ] 所有测试通过 +- [ ] API 可启动并响应 +- [ ] 代码已提交 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..35f4a6c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.100.0 +uvicorn[standard]>=0.23.0 +pytest>=7.0.0 +httpx>=0.24.0 \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..0112e8e --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source .venv/bin/activate +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4308e6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# tests package \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..433508f --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,49 @@ +from fastapi.testclient import TestClient + + +def test_health_check(): + """测试健康检查端点""" + from app.main import app + client = TestClient(app) + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_create_item(): + """测试创建项目""" + from app.main import app + client = TestClient(app) + response = client.post("/items", json={"name": "Test Item", "description": "A test"}) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Test Item" + assert data["id"] == 1 + + +def test_list_items(): + """测试列出项目""" + from app.main import app + client = TestClient(app) + client.post("/items", json={"name": "Item 1"}) + response = client.get("/items") + assert response.status_code == 200 + assert len(response.json()) >= 1 + + +def test_get_item(): + """测试获取单个项目""" + from app.main import app + client = TestClient(app) + created = client.post("/items", json={"name": "Item 2"}).json() + response = client.get(f"/items/{created['id']}") + assert response.status_code == 200 + assert response.json()["name"] == "Item 2" + + +def test_get_item_not_found(): + """测试获取不存在的项目""" + from app.main import app + client = TestClient(app) + response = client.get("/items/99999") + assert response.status_code == 404 \ No newline at end of file