正在学习

Claude Context File

app_legacy.py

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

# In another terminal:
pytest -q

You have preserved behavior while isolating concerns. Routes are slim, the service expresses rules and fan-in logic, and repositories encapsulate SQL. This makes tests fast and future changes safer.

Clarification Table: Before vs. After

| Aspect | Legacy Monolith | Refactored Structure | Benefit |
| --- | --- | --- | --- |
| Database access | Inline sqlite 3 calls in routes | TaskRepository hides SQL | Easier to swap storage and test |
| Business rules | Scattered in endpoints | TaskService centralizes logic | Consistent validation and reuse |
| Routing | Fat endpoints | Thin handlers in routes.py | Readable HTTP layer |
| Initialization | Global state and helpers | create_app( ) composition | Deterministic startup |
| Testability | Hard to isolate logic | pytest hits stable boundaries | Safer incremental changes |

You transformed a working-but-fragile monolith into a small, layered application that is easier to reason about and extend. The refactor changed structure, not behavior, and introduced seams—a repository and a service—where Claude Code can help you iterate confidently. From here you can add caching, switch SQLite to Postgres, or plug in background jobs without touching route code. In the next section you will apply the same patterns to synchronize edits across multiple modules, letting Claude update routes, services, and tests in one guided pass while preserving contracts.

Chapter 11 – Security, Risk, and Governance in Claude Workflows

练习题

In the legacy FastAPI application, which decorator is used to define the health check endpoint?

A. @app.post("/health")
B. @app.put("/health")
C. @app.get("/health")
D. @app.delete("/health")

Which class in the legacy application defines the structure for creating a new task, including its title, description, and completion status?

A. Task
B. BaseModel
C. TaskIn
D. TaskUpdate

What are the valid HTTP status codes returned by the endpoints in the legacy FastAPI application?

A. 200 OK
B. 201 Created
C. 204 No Content
D. 404 Not Found
E. 500 Internal Server Error

The legacy application uses SQLite as its database, and the database file is named 'tasks.db'.

In the legacy application, the function responsible for ensuring the database table exists is named ___.

Explain the purpose of the Task model in the legacy application.

Which command is used to run the legacy FastAPI application using uvicorn?

A. uvicorn app_legacy:app --port 8000
B. uvicorn app_legacy:app --reload
C. uvicorn app:app_legacy --reload
D. uvicorn run app_legacy:app

The legacy application's create_task endpoint validates that the task title is not empty and does not exceed 120 characters.

In the refactored layered structure, the function that initializes the database is named ___.

Describe the role of the TaskRepository class in the refactored layered structure.

In the refactored layered structure, which class is responsible for handling business logic related to tasks?

A. TaskRepository
B. TaskService
C. TaskRoute
D. TaskModel

Which of the following are best practices for error handling in FastAPI applications according to the provided knowledge points?

A. Use print statements for debugging
B. Handle errors using FastAPI’s HTTPException
C. Ignore errors and let the application crash
D. Use try-except blocks without raising HTTPException
E. Provide meaningful error messages in HTTPException

In the refactored layered structure, the TaskRepository class should directly interact with the database to perform CRUD operations on tasks.

In FastAPI, to validate the length of the task title, you should use the ___ attribute in the TaskCreate model.

Explain the importance of isolating data access in the refactored layered structure.

登录后解锁笔记、知识点解析、AI 问答

立即登录