2025-06-02 18:32:05 +00:00

258 lines
6.9 KiB
Python

from fastapi import FastAPI, Depends, HTTPException, status, Response
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
import uvicorn
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
import logging
# Import application config
from app.config import (
API_TITLE,
API_DESCRIPTION,
API_VERSION,
CORS_ORIGINS,
HOST,
PORT,
DEBUG,
HEALTH_CHECK_PATH,
DETAILED_HEALTH_CHECK_PATH,
HEALTH_CHECK_INCLUDE_DB,
get_settings,
)
# Import database models and config
from app.database.config import get_db, create_tables, SessionLocal
from app.database.models import Todo as TodoModel
# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Create tables if they don't exist
# In production, you should use Alembic migrations instead
create_tables()
# Pydantic models for request and response
class TodoBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
class TodoCreate(TodoBase):
pass
class TodoResponse(TodoBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True # Updated for Pydantic v2 (replaces orm_mode)
# Create the FastAPI app
app = FastAPI(
title=API_TITLE,
description=API_DESCRIPTION,
version=API_VERSION,
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Health endpoints
@app.get(HEALTH_CHECK_PATH, tags=["Health"], status_code=200)
async def health_check():
"""
Simple health check endpoint that always returns healthy.
This is used by container orchestration systems to verify the app is running.
The health check will always return a 200 OK status to indicate the application
is running, even if some components (like the database) might be unavailable.
"""
logger.info(f"Health check requested at {HEALTH_CHECK_PATH}")
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
@app.get(DETAILED_HEALTH_CHECK_PATH, tags=["Health"])
async def detailed_health_check(response: Response):
"""
Detailed health check endpoint that verifies API and database connectivity.
This endpoint provides more detailed information about the state of various
components of the application, including the database connection status.
"""
logger.info(f"Detailed health check requested at {DETAILED_HEALTH_CHECK_PATH}")
# Start with basic info
health_data = {
"status": "healthy", # App is running, so it's healthy from an orchestration perspective
"services": {
"api": "running",
},
"timestamp": datetime.now().isoformat(),
"config": {
"db_check_enabled": HEALTH_CHECK_INCLUDE_DB,
"environment": get_settings().get("APP_ENV"),
},
}
# Check database if enabled
if HEALTH_CHECK_INCLUDE_DB:
db_status = "unknown"
error_message = None
# Create a new session for the health check
db = SessionLocal()
try:
# Test database connection
db.execute("SELECT 1").first()
db_status = "connected"
except Exception as e:
db_status = "disconnected"
error_message = str(e)
# Don't change HTTP status code - we want health check to be 200 OK
# to ensure container orchestration doesn't kill the app
finally:
db.close()
health_data["services"]["database"] = db_status
if error_message:
health_data["services"]["database_error"] = error_message
return health_data
# Root endpoint
@app.get("/", tags=["Root"])
async def root():
"""
Root endpoint that redirects to the API documentation.
"""
return {"message": "Welcome to Todo List API. Visit /docs for documentation."}
# Todo API endpoints
@app.post(
"/todos",
response_model=TodoResponse,
status_code=status.HTTP_201_CREATED,
tags=["Todos"],
)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
"""
Create a new todo item.
"""
db_todo = TodoModel(**todo.model_dump())
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@app.get("/todos", response_model=List[TodoResponse], tags=["Todos"])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""
Get all todo items with pagination.
"""
todos = db.query(TodoModel).offset(skip).limit(limit).all()
return todos
@app.get("/todos/{todo_id}", response_model=TodoResponse, tags=["Todos"])
def read_todo(todo_id: int, db: Session = Depends(get_db)):
"""
Get a specific todo item by ID.
"""
db_todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
@app.put("/todos/{todo_id}", response_model=TodoResponse, tags=["Todos"])
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
"""
Update a todo item.
"""
db_todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
# Update todo item fields
for key, value in todo.model_dump().items():
setattr(db_todo, key, value)
db.commit()
db.refresh(db_todo)
return db_todo
@app.delete(
"/todos/{todo_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
tags=["Todos"],
)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
Delete a todo item.
"""
db_todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
db.delete(db_todo)
db.commit()
return None
# Application startup and shutdown events
@app.on_event("startup")
async def startup_event():
"""
Function that runs when the application starts.
"""
logger.info("Starting Todo List API")
logger.info(f"API Version: {API_VERSION}")
logger.info(f"Environment: {get_settings().get('APP_ENV')}")
logger.info(f"Debug mode: {DEBUG}")
# Log all available routes
routes = []
for route in app.routes:
routes.append(f"{route.path} [{', '.join(route.methods)}]")
logger.info(f"Available routes: {len(routes)}")
for route in sorted(routes):
logger.info(f" {route}")
@app.on_event("shutdown")
async def shutdown_event():
"""
Function that runs when the application shuts down.
"""
logger.info("Shutting down Todo List API")
# For local development
if __name__ == "__main__":
uvicorn.run("main:app", host=HOST, port=PORT, reload=DEBUG, log_level="info")