from __future__ import annotations from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.database import get_db from app.models import Ingredient from app.schemas.ingredient import IngredientCreate, IngredientRead, IngredientUpdate from app.security import require_admin public_router = APIRouter(prefix="/api/ingredients", tags=["ingredients"]) admin_router = APIRouter( prefix="/api/admin/ingredients", tags=["ingredients-admin"], dependencies=[Depends(require_admin)], ) @public_router.get("", response_model=List[IngredientRead]) def list_ingredients( q: Optional[str] = Query(default=None), limit: int = Query(default=100, le=500), db: Session = Depends(get_db), ) -> List[Ingredient]: query = db.query(Ingredient) if q: like = f"%{q.lower()}%" query = query.filter( or_( Ingredient.name_lower.ilike(like), Ingredient.aliases.any(q), ) ) return query.order_by(Ingredient.name).limit(limit).all() @public_router.get("/{ingredient_id}", response_model=IngredientRead) def get_ingredient(ingredient_id: UUID, db: Session = Depends(get_db)) -> Ingredient: row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first() if row is None: raise HTTPException(status_code=404, detail="ingredient not found") return row @admin_router.post("", response_model=IngredientRead, status_code=status.HTTP_201_CREATED) def create_ingredient(payload: IngredientCreate, db: Session = Depends(get_db)) -> Ingredient: row = Ingredient( name=payload.name, name_lower=payload.name.lower(), aliases=payload.aliases, aisle=payload.aisle, unit=payload.unit, typical_price=payload.typical_price, ) db.add(row) try: db.commit() except IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="ingredient name already exists") db.refresh(row) return row @admin_router.patch("/{ingredient_id}", response_model=IngredientRead) def update_ingredient( ingredient_id: UUID, payload: IngredientUpdate, db: Session = Depends(get_db), ) -> Ingredient: row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first() if row is None: raise HTTPException(status_code=404, detail="ingredient not found") data = payload.model_dump(exclude_unset=True) if "name" in data: row.name = data["name"] row.name_lower = data["name"].lower() for field in ("aliases", "aisle", "unit", "typical_price"): if field in data: setattr(row, field, data[field]) try: db.commit() except IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="ingredient name conflict") db.refresh(row) return row @admin_router.delete( "/{ingredient_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, ) def delete_ingredient(ingredient_id: UUID, db: Session = Depends(get_db)): row = db.query(Ingredient).filter(Ingredient.id == ingredient_id).first() if row is None: raise HTTPException(status_code=404, detail="ingredient not found") db.delete(row) db.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)