"""Shared router helpers: media URLs, picture hydration, a basic in-memory rate limiter.""" from __future__ import annotations import time from collections import defaultdict, deque from fastapi import HTTPException, Request, status from sqlalchemy import select from sqlalchemy.orm import Session from ..config import settings from ..models import Picture from ..models.base import SubjectType from ..schemas.dog import PictureOut MEDIA_URL_PREFIX = "/media" def media_url(key: str | None) -> str | None: return f"{MEDIA_URL_PREFIX}/{key}" if key else None def hydrate_picture(pic: Picture) -> PictureOut: out = PictureOut.model_validate(pic) out.url = media_url(pic.file_path) out.thumb_url = media_url(pic.thumb_path) return out def pictures_for(db: Session, subject_type: SubjectType, subject_id: int) -> list[PictureOut]: pics = db.execute( select(Picture) .where(Picture.subject_type == subject_type, Picture.subject_id == subject_id) .order_by(Picture.is_primary.desc(), Picture.id) ).scalars().all() return [hydrate_picture(p) for p in pics] def estimated_breeds_for( db: Session, subject_type: SubjectType, subject_id: int, limit: int = 3 ) -> list[str]: """Top estimated-breed labels for a dog, aggregated across its photos, blending how often each breed appears with how confident the model is (§9.3). Reads the DB-active breed model (no model load) — display data only, never authoritative. """ from ..services.datasets import active_breed_model_in_db, aggregated_breeds return aggregated_breeds(db, subject_type, subject_id, active_breed_model_in_db(db), limit) # ---- Basic fixed-window rate limiter (spec §12, §14) ---- _buckets: dict[str, deque[float]] = defaultdict(deque) def rate_limit(request: Request, key_prefix: str = "report") -> None: limit = settings.rate_limit_reports_per_minute if limit <= 0: return client = request.client.host if request.client else "unknown" key = f"{key_prefix}:{client}" now = time.time() bucket = _buckets[key] while bucket and now - bucket[0] > 60: bucket.popleft() if len(bucket) >= limit: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many reports; please wait a minute and try again.", ) bucket.append(now)