"""Equipment API endpoints with caching and pagination.""" import math from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.cache import ( equipment_categories_key, equipment_detail_key, equipment_list_key, get_cached, set_cache, ) from app.database import get_db from app.models.equipment import EquipmentCache from app.schemas.equipment import EquipmentDetail, EquipmentItem, PaginatedEquipment router = APIRouter(prefix="/equipment", tags=["equipment"]) @router.get("", response_model=PaginatedEquipment) async def list_equipment( search: str | None = Query(None, description="Search in name"), category: str | None = Query(None, description="Filter by category"), sort: str | None = Query("name_asc", description="Sort order: name_asc, name_desc"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(20, ge=1, le=100, description="Items per page"), db: AsyncSession = Depends(get_db), ) -> PaginatedEquipment: """Return a paginated, filterable, sortable list of equipment.""" cache_key = equipment_list_key(page, category, sort, search) cached = await get_cached(cache_key) if cached is not None: return PaginatedEquipment(**cached) query = select(EquipmentCache) count_query = select(func.count(EquipmentCache.id)) if search: query = query.where(EquipmentCache.name.ilike(f"%{search}%")) count_query = count_query.where(EquipmentCache.name.ilike(f"%{search}%")) if category: query = query.where(EquipmentCache.category == category) count_query = count_query.where(EquipmentCache.category == category) if sort == "name_desc": query = query.order_by(EquipmentCache.name.desc()) else: query = query.order_by(EquipmentCache.name.asc()) total_result = await db.execute(count_query) total = total_result.scalar() or 0 offset = (page - 1) * page_size query = query.offset(offset).limit(page_size) result = await db.execute(query) items = [EquipmentItem.model_validate(row) for row in result.scalars().all()] total_pages = math.ceil(total / page_size) if page_size > 0 else 0 response = PaginatedEquipment( items=items, total=total, page=page, page_size=page_size, total_pages=total_pages, ) await set_cache(cache_key, response.model_dump(mode="json"), ttl=3600) return response @router.get("/categories", response_model=list[str]) async def list_categories( db: AsyncSession = Depends(get_db), ) -> list[str]: """Return a list of all distinct equipment categories.""" cache_key = equipment_categories_key() cached = await get_cached(cache_key) if cached is not None: return cached query = select(EquipmentCache.category).distinct().where(EquipmentCache.category.isnot(None)).order_by(EquipmentCache.category.asc()) result = await db.execute(query) categories = [row[0] for row in result.all() if row[0]] await set_cache(cache_key, categories, ttl=3600) return categories @router.get("/{equipment_id}", response_model=EquipmentDetail) async def get_equipment( equipment_id: int, db: AsyncSession = Depends(get_db), ) -> EquipmentDetail: """Return a single equipment detail by ID.""" cache_key = equipment_detail_key(equipment_id) cached = await get_cached(cache_key) if cached is not None: return EquipmentDetail(**cached) result = await db.execute(select(EquipmentCache).where(EquipmentCache.id == equipment_id)) item = result.scalar_one_or_none() if item is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found", ) detail = EquipmentDetail.model_validate(item) await set_cache(cache_key, detail.model_dump(mode="json"), ttl=3600) return detail