Files
hms-licht-ton/backend/app/routers/equipment.py
T
A0 Implementation Engineer e62ece1c06 feat: T03 backend core – FastAPI + DB models + Equipment API + Redis cache + health + tests
- FastAPI app with CORS, lifespan handlers
- Pydantic Settings config (DB, Redis, CORS, SMTP, JWT, Rentman)
- SQLAlchemy async engine + session (DeclarativeBase)
- 6 DB models: EquipmentCache, RentalRequest, RentalRequestItem, Contact, AdminUser, SyncLog
- Pydantic schemas: EquipmentItem, EquipmentDetail, PaginatedEquipment, ContactCreate, ContactResponse
- Redis cache helper: set/get/delete_pattern, rate limiting, equipment key builders
- Equipment router: list (search/category/sort/pagination), detail, categories – all cached
- Contact router: POST with Pydantic validation + rate limiting (5/min)
- Health router: GET /api/health with DB + Redis status
- 28 pytest tests (all pass, 90% coverage)
- Dockerfile, requirements.txt, pytest.ini, test_report.md
2026-07-09 01:26:45 +02:00

116 lines
3.9 KiB
Python

"""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