File size: 2,396 Bytes
de1e3fc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | """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)
|