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)