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 <noreply@anthropic.com>
This commit is contained in:
parent
23e94ada3b
commit
67d8c6bf30
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,6 +17,7 @@ __pycache__/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
.eggs/
|
.eggs/
|
||||||
*.egg
|
*.egg
|
||||||
|
.venv/
|
||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
.env
|
.env
|
||||||
|
|||||||
40
README.md
Normal file
40
README.md
Normal file
@ -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
|
||||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# app package
|
||||||
35
app/main.py
Normal file
35
app/main.py
Normal file
@ -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]
|
||||||
13
app/models.py
Normal file
13
app/models.py
Normal file
@ -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
|
||||||
38
docs/plans/2026-05-07-fastapi-example-plan.md
Normal file
38
docs/plans/2026-05-07-fastapi-example-plan.md
Normal file
@ -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 可启动并响应
|
||||||
|
- [ ] 代码已提交
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
fastapi>=0.100.0
|
||||||
|
uvicorn[standard]>=0.23.0
|
||||||
|
pytest>=7.0.0
|
||||||
|
httpx>=0.24.0
|
||||||
3
run.sh
Executable file
3
run.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# tests package
|
||||||
49
tests/test_api.py
Normal file
49
tests/test_api.py
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user