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