decomposer: generate deliverable files for Implement the task REST API service with FastAPI, including standard CRUD endpoints, HTTP status semantics, request and response models, and structured error handling.; Add a PostgreSQL persistence layer for the task service using SQLAlchemy models, session management, database initialization or migrations, and durable task storage integrated with the existing API contract.; Define and integrate Pydantic request and response schemas for task fields, status values, identifiers, timestamps, and validation errors across the existing FastAPI service and persistence contract.; Add automated pytest coverage for the task API, including CRUD behavior, request and response validation failures, HTTP status semantics, and persistence interactions using isolated test data.; Containerize the FastAPI task service and PostgreSQL persistence service with Docker Compose, including environment-based configuration, health checks, networking, startup dependencies, and persistent database storage.; Create project documentation and operational guidance covering local setup, Docker Compose configuration, environment variables, API endpoints, validation and error responses, testing commands, persistence behavior, and relevant best-practice notes.

This commit is contained in:
2026-08-19 11:37:10 +00:00
parent 878957c05a
commit 324ddc19db
18 changed files with 550 additions and 5 deletions

110
app/main.py Normal file
View File

@@ -0,0 +1,110 @@
from contextlib import asynccontextmanager
from uuid import UUID
from fastapi import Depends, FastAPI, Query, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from .config import settings
from .db import get_db, init_db
from .models import TaskStatus
from .repository import create_task, delete_task, get_task, list_tasks, update_task
from .schemas import ErrorResponse, TaskCreate, TaskRead, TaskUpdate
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
yield
app = FastAPI(title=settings.app_name, version="1.0.0", lifespan=lifespan)
def error_response(code: str, message: str, details: list | None = None) -> JSONResponse:
return JSONResponse(status_code=404 if code == "TASK_NOT_FOUND" else 500, content={"error": {"code": code, "message": message, "details": details or []}})
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
details = [{"loc": list(error.get("loc", [])), "msg": error.get("msg", "Invalid value"), "type": error.get("type")} for error in exc.errors()]
return JSONResponse(status_code=422, content={"error": {"code": "VALIDATION_ERROR", "message": "Request validation failed", "details": details}})
@app.get("/health")
def health(db: Session = Depends(get_db)):
try:
db.execute(text("SELECT 1"))
return {"status": "ok"}
except SQLAlchemyError:
return JSONResponse(status_code=503, content={"error": {"code": "DATABASE_UNAVAILABLE", "message": "Database is unavailable", "details": []}})
@app.post("/tasks", response_model=TaskRead, status_code=status.HTTP_201_CREATED, responses={422: {"model": ErrorResponse}})
def create(data: TaskCreate, db: Session = Depends(get_db)):
try:
return create_task(db, data)
except SQLAlchemyError:
db.rollback()
return error_response("DATABASE_ERROR", "Unable to create task")
@app.get("/tasks", response_model=list[TaskRead])
def list_all(status_filter: TaskStatus | None = Query(default=None, alias="status"), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=100)):
# dependency is intentionally declared below to keep query parameters obvious in OpenAPI
return []
@app.get("/tasks", response_model=list[TaskRead], include_in_schema=False)
def _list_shadow(db: Session = Depends(get_db)):
return []
# Replace the parameter-only route above with the database-backed implementation.
app.routes.pop(-2)
@app.get("/tasks", response_model=list[TaskRead])
def list_tasks_endpoint(status_filter: TaskStatus | None = Query(default=None, alias="status"), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=100), db: Session = Depends(get_db)):
return list_tasks(db, status_filter, skip, limit)
def require_task(task_id: UUID, db: Session) :
task = get_task(db, task_id)
if task is None:
raise ValueError("TASK_NOT_FOUND")
return task
@app.get("/tasks/{task_id}", response_model=TaskRead, responses={404: {"model": ErrorResponse}})
def get_one(task_id: UUID, db: Session = Depends(get_db)):
task = get_task(db, task_id)
if task is None:
return error_response("TASK_NOT_FOUND", "Task not found")
return task
@app.patch("/tasks/{task_id}", response_model=TaskRead, responses={404: {"model": ErrorResponse}})
def patch(task_id: UUID, data: TaskUpdate, db: Session = Depends(get_db)):
task = get_task(db, task_id)
if task is None:
return error_response("TASK_NOT_FOUND", "Task not found")
try:
return update_task(db, task, data)
except SQLAlchemyError:
db.rollback()
return error_response("DATABASE_ERROR", "Unable to update task")
@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT, responses={404: {"model": ErrorResponse}})
def remove(task_id: UUID, db: Session = Depends(get_db)):
task = get_task(db, task_id)
if task is None:
return error_response("TASK_NOT_FOUND", "Task not found")
try:
delete_task(db, task)
return None
except SQLAlchemyError:
db.rollback()
return error_response("DATABASE_ERROR", "Unable to delete task")