正在学习
app_legacy.py (1)
app_legacy.py (1)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import sqlite3
from typing import List, Optional
app = FastAPI(title="Tasks Legacy Monolith", version="0.1.0")
DB_PATH = "tasks.db"
class TaskIn(BaseModel):
title: str
description: Optional[str] = ""
completed: bool = False
class Task(TaskIn):
id: int
def _ensure_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
)
conn.commit()
conn.close()
_ensure_db()
@app.get("/health")
def health():
return {"status": "ok", "service": "legacy"}
@app.get("/tasks", response_model=List[Task])
def list_tasks():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
rows = cur.fetchall()
conn.close()
return [
{"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
for r in rows
]
@app.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskIn):
if not payload.title or not payload.title.strip():
raise HTTPException(status_code=400, detail="Title required")
if len(payload.title) > 120:
raise HTTPException(status_code=400, detail="Title too long")
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
(payload.title.strip(), payload.description or "", int(payload.completed)),
)
conn.commit()
task_id = cur.lastrowid
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
row = cur.fetchone()
conn.close()
return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}
@app.put("/tasks/{task_id}", response_model=Task)
def put_task(task_id: int, payload: TaskIn):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
if not cur.fetchone():
conn.close()
raise HTTPException(status_code=404, detail="not found")
cur.execute(
"UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
(payload.title.strip(), payload.description or "", int(payload.completed), task_id),
)
conn.commit()
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id = ?", (task_id,))
row = cur.fetchone()
conn.close()
return {"id": row[0], "title": row[1], "description": row[2], "completed": bool(row[3])}
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit()
conn.close()
Run it:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn
uvicorn app_legacy:app --reload
You now have a baseline. Next, refactor to a layered structure. Create the following files and folders exactly as shown.
app/
init.py
app.py
database.py
models.py
repositories.py
services.py
routes.py
tests/
test_tasks.py
requirements.txt
requirements.txt
fastapi==0.114.0
uvicorn==0.30.5
app/database.py
import sqlite3
from typing import Iterator
DB_PATH = "tasks.db"
def init_db() -> None:
with sqlite3.connect(DB_PATH) as conn:
cur = conn.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, description TEXT, completed INTEGER)"
)
conn.commit()
def get_conn() -> Iterator[sqlite3.Connection]:
return sqlite3.connect(DB_PATH)
app/models.py
from pydantic import BaseModel, Field
from typing import Optional
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=120)
description: Optional[str] = ""
completed: bool = False
class Task(TaskCreate):
id: int
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
app/repositories.py
import sqlite3
from typing import List, Optional, Dict, Any
class TaskRepository:
def __init__(self, conn: sqlite3.Connection):
self.conn = conn
def list(self) -> List[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks ORDER BY id")
return [
{"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
for r in cur.fetchall()
]
def get(self, task_id: int) -> Optional[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("SELECT id, title, description, completed FROM tasks WHERE id=?", (task_id,))
r = cur.fetchone()
if not r:
return None
return {"id": r[0], "title": r[1], "description": r[2], "completed": bool(r[3])}
def create(self, title: str, description: str, completed: bool) -> Dict[str, Any]:
cur = self.conn.cursor()
cur.execute(
"INSERT INTO tasks (title, description, completed) VALUES (?, ?, ?)",
(title, description, int(completed)),
)
self.conn.commit()
return self.get(cur.lastrowid)
def put(self, task_id: int, title: str, description: str, completed: bool) -> Optional[Dict[str, Any]]:
cur = self.conn.cursor()
cur.execute("UPDATE tasks SET title=?, description=?, completed=? WHERE id=?",
(title, description, int(completed), task_id))
self.conn.commit()
return self.get(task_id)
def delete(self, task_id: int) -> None:
cur = self.conn.cursor()
cur.execute("DELETE FROM tasks WHERE id=?", (task_id,))
self.conn.commit()
app/services.py
from typing import Dict, Any, Optional
from .repositories import TaskRepository
from .models import TaskCreate, TaskUpdate
class TaskService:
def __init__(self, repo: TaskRepository):
self.repo = repo
def list_tasks(self):
return self.repo.list()
def create_task(self, payload: TaskCreate):
title = payload.title.strip()
desc = (payload.description or "").strip()
return self.repo.create(title, desc, payload.completed)
def get_task(self, task_id: int) -> Optional[Dict[str, Any]]:
return self.repo.get(task_id)
def put_task(self, task_id: int, payload: TaskCreate):
title = payload.title.strip()
desc = (payload.description or "").strip()
return self.repo.put(task_id, title, desc, payload.completed)
def patch_task(self, task_id: int, payload: TaskUpdate):
current = self.repo.get(task_id)
if not current:
return None
title = current["title"] if payload.title is None else payload.title.strip()
desc = current["description"] if payload.description is None else (payload.description or "").strip()
completed = current["completed"] if payload.completed is None else bool(payload.completed)
return self.repo.put(task_id, title, desc, completed)
def delete_task(self, task_id: int):
self.repo.delete(task_id)
app/routes.py
from fastapi import APIRouter, HTTPException
from typing import List
from .models import Task, TaskCreate, TaskUpdate
from .services import TaskService
router = APIRouter()
def mount_routes(service: TaskService) -> APIRouter:
@router.get("/health")
def health():
return {"status": "ok", "service": "refactored"}
@router.get("/tasks", response_model=List[Task])
def list_tasks():
return service.list_tasks()
@router.post("/tasks", response_model=Task, status_code=201)
def create_task(payload: TaskCreate):
return service.create_task(payload)
@router.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
task = service.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="not found")
return task
@router.put("/tasks/{task_id}", response_model=Task)
def put_task(task_id: int, payload: TaskCreate):
if not service.get_task(task_id):
raise HTTPException(status_code=404, detail="not found")
return service.put_task(task_id, payload)
@router.patch("/tasks/{task_id}", response_model=Task)
def patch_task(task_id: int, payload: TaskUpdate):
updated = service.patch_task(task_id, payload)
if not updated:
raise HTTPException(status_code=404, detail="not found")
return updated
@router.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
if not service.get_task(task_id):
raise HTTPException(status_code=404, detail="not found")
service.delete_task(task_id)
return router
app/app.py
from fastapi import FastAPI
from .database import init_db, get_conn
from .repositories import TaskRepository
from .services import TaskService
from .routes import mount_routes
def create_app() -> FastAPI:
init_db()
app = FastAPI(title="Tasks Refactored", version="1.0.0")
# Simple composition root
conn = get_conn()
repo = TaskRepository(conn)
service = TaskService(repo)
app.include_router(mount_routes(service))
return app
app = create_app()
tests/test_tasks.py
from fastapi.testclient import TestClient
from app.app import app
client = TestClient(app)
def test_health():
r = client.get("/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_create_list_get_put_patch_delete():
# create
r = client.post("/tasks", json={"title": "Refactor", "description": "split layers", "completed": False})
assert r.status_code == 201
tid = r.json()["id"]
# list
r = client.get("/tasks")
assert r.status_code == 200
assert any(t["id"] == tid for t in r.json())
# get
r = client.get(f"/tasks/{tid}")
assert r.status_code == 200
assert r.json()["title"] == "Refactor"
# put
r = client.put(f"/tasks/{tid}", json={"title": "Refactor All", "description": "", "completed": True})
assert r.status_code == 200
assert r.json()["completed"] is True
# patch
r = client.patch(f"/tasks/{tid}", json={"description": "done"})
assert r.status_code == 200
assert r.json()["description"] == "done"
# delete
r = client.delete(f"/tasks/{tid}")
assert r.status_code == 204
# 404 after delete
r = client.get(f"/tasks/{tid}")
assert r.status_code == 404
Run the refactored service and tests:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt pytest
uvicorn app.app:app --reload
练习题
What is the purpose of the _ensure_db() function in the legacy FastAPI application?
A. To create a new task
B. To ensure the database table exists
C. To connect to the database
D. To delete a task
Which endpoint is responsible for returning the health status of the legacy FastAPI application?
A.
/tasksB.
/healthC.
/createD.
/statusWhat are the valid fields in the TaskIn model?
A.
idB.
titleC.
descriptionD.
completedWhich of the following are valid HTTP methods used in the legacy FastAPI application endpoints?
A. GET
B. POST
C. PUT
D. DELETE
The create_task endpoint in the legacy FastAPI application raises an HTTPException if the title is empty or exceeds 120 characters.
The delete_task endpoint in the legacy FastAPI application returns a response body after deleting a task.
The database connection path is defined by the variable ___.
The put_task endpoint updates a task's details using the ___ method.
Explain the purpose of the list_tasks endpoint in the legacy FastAPI application.
What is the role of the init_db() function in the layered structure of the FastAPI application?
Which of the following best describes the purpose of the get_conn() function in the layered structure?
A. To execute SQL queries
B. To initialize the database
C. To provide a database connection iterator
D. To commit changes to the database
What are the key benefits of refactoring the FastAPI application to a layered structure? (Select all that apply)
A. Isolates data access
B. Moves rules to the service layer
C. Keeps I/O at the edges
D. Introduces a minimal startup path
In the refactored layered structure, which component is responsible for handling database connection details and SQL queries?
A. The routes.py file
B. The TaskRepository class in repositories.py
C. The services.py file
D. The app.py file
Which of the following are benefits of moving business rules and validation checks to a service layer in a FastAPI application? (Select all that apply)
A. Makes routes thinner and more focused on HTTP handling
B. Allows for easier testing of core business logic
C. Reduces the need for database connections
D. Enables reuse of business logic across multiple routes
E. Improves performance by reducing network latency
In the refactored structure, the routes.py file should contain all the SQL queries for task operations.
The _____ class in repositories.py should be used to encapsulate all database operations for tasks, following the principle of isolating data access.
Explain how the refactored structure improves maintainability compared to the legacy monolith.
登录后解锁笔记、知识点解析、AI 问答
立即登录