PawTrace / backend /app /api /datasets.py
Elliott Duke
HomingPet: lost-dog reunification (FastAPI + React) with Render deploy
de1e3fc
Raw
History Blame Contribute Delete
8.21 kB
"""Admin dataset management endpoints (auth-protected, under /admin).
Covers list/view/purge, image→dataset lookup, embed-all, and match-all. The dataset *load* job
endpoints live in api/jobs.py (background job + polling for the dashboard's load form).
"""
from __future__ import annotations
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import Dataset, Picture, User
from ..models.base import SubjectType
from ..schemas.dataset import (
DatasetDetailOut,
DatasetOut,
EmbedAllResult,
ImageDatasetOut,
MatchDatasetResult,
PurgeResult,
)
from ..security import require_admin
from ..services import datasets as ds
router = APIRouter(prefix="/admin", tags=["admin-datasets"])
def _get_dataset(db: Session, dataset_id: int) -> Dataset:
dataset = db.get(Dataset, dataset_id)
if not dataset:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Dataset not found")
return dataset
@router.get("/datasets", response_model=list[DatasetOut])
def list_datasets(
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> list[DatasetOut]:
datasets = db.execute(select(Dataset).order_by(Dataset.creation_time.desc())).scalars().all()
out: list[DatasetOut] = []
for d in datasets:
item = DatasetOut.model_validate(d)
item.case_count = ds.case_count(db, d)
out.append(item)
return out
@router.get("/datasets/{dataset_id}", response_model=DatasetDetailOut)
def get_dataset(
dataset_id: int,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> DatasetDetailOut:
dataset = _get_dataset(db, dataset_id)
stats = ds.dataset_stats(db, dataset)
base = DatasetOut.model_validate(dataset)
base.case_count = stats["case_count"]
return DatasetDetailOut(**base.model_dump(), stats=stats)
@router.get("/test-match")
def test_match(
kind: str,
dog_id: int,
top_k: int = 10,
apply_breed_gate: bool = False,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> dict:
"""Score one dog against ALL dogs in the opposite category by embedding similarity (testing)."""
if kind not in ("known", "unknown"):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "kind must be 'known' or 'unknown'")
subject_type = SubjectType.known if kind == "known" else SubjectType.unknown
query = ds.dog_profile(db, subject_type, dog_id)
if query is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Dog not found")
top_k = max(1, min(top_k, 50))
from ..services.matching import rank_dog_against_opposite
ranked, considered, query_embedded = rank_dog_against_opposite(
db, subject_type=subject_type, subject_id=dog_id, top_k=top_k,
apply_breed_gate=apply_breed_gate,
)
results = []
for cand_type, cid, score in ranked:
prof = ds.dog_profile(db, cand_type, cid)
if prof:
results.append({**prof, "score": score})
from ..ml import get_embedder
e = get_embedder()
return {
"query": query,
"results": results,
"model": f"{e.name}/{e.version}",
"candidate_count": considered,
"query_embedded": query_embedded,
}
@router.get("/dog/{kind}/{dog_id}")
def dog_detail(
kind: str,
dog_id: int,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> dict:
"""A dog's profile plus ALL of its photos (full images) — for the profile viewer."""
if kind not in ("known", "unknown"):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "kind must be 'known' or 'unknown'")
subject_type = SubjectType.known if kind == "known" else SubjectType.unknown
profile = ds.dog_profile(db, subject_type, dog_id)
if profile is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Dog not found")
from .helpers import pictures_for
photos = [p.model_dump() for p in pictures_for(db, subject_type, dog_id)]
return {"profile": profile, "photos": photos}
@router.get("/breeds")
def list_breeds(
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> dict:
"""Predicted breed labels (HF softmax, active model) available to filter by."""
return {"model": ds.active_breed_model_in_db(db), "breeds": ds.list_breeds(db)}
@router.get("/dogs")
def list_all_dogs(
kind: str = "all",
limit: int = 30,
offset: int = 0,
breed: str | None = None,
breed_k: int = 10,
sort: str = "newest",
zip: str | None = None,
added_from: date | None = None,
added_to: date | None = None,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> dict:
"""Browse every dog profile, filterable by predicted breed / ZIP / added-date and sortable.
``breed_k`` = 1 matches the top predicted breed; 5–10 matches within the top-K. ``sort`` is
``newest`` (default) or ``oldest``. ``zip`` matches a dog's ZIP by prefix; ``added_from`` /
``added_to`` bound the added date (inclusive).
"""
kind = kind if kind in ("all", "known", "unknown") else "all"
limit = max(1, min(limit, 100))
offset = max(0, offset)
breed_k = max(1, min(breed_k, 50))
sort = sort if sort in ("newest", "oldest") else "newest"
return ds.list_all_dogs(
db, kind, limit, offset, breed=breed or None, breed_k=breed_k,
sort=sort, zip_prefix=(zip or None), added_from=added_from, added_to=added_to,
)
@router.get("/datasets/{dataset_id}/dogs")
def list_dataset_dogs(
dataset_id: int,
limit: int = 25,
offset: int = 0,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> dict:
dataset = _get_dataset(db, dataset_id)
limit = max(1, min(limit, 100))
return ds.list_dogs(db, dataset, limit, offset)
@router.delete("/datasets/{dataset_id}", response_model=PurgeResult)
def delete_dataset(
dataset_id: int,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> PurgeResult:
dataset = _get_dataset(db, dataset_id)
deleted = ds.purge_dataset(db, dataset)
return PurgeResult(dataset_id=dataset_id, deleted=deleted)
@router.get("/image/{image_id}/dataset", response_model=ImageDatasetOut)
def image_dataset(
image_id: int,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> ImageDatasetOut:
pic = db.get(Picture, image_id)
if not pic:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Image not found")
from ..models import KnownDog, UnknownDog
if pic.subject_type == SubjectType.known:
dog = db.get(KnownDog, pic.subject_id)
else:
dog = db.get(UnknownDog, pic.subject_id)
dataset_id = getattr(dog, "dataset_id", None) if dog else None
dataset_name = None
if dataset_id:
d = db.get(Dataset, dataset_id)
dataset_name = d.name if d else None
return ImageDatasetOut(
image_id=image_id,
subject_type=pic.subject_type.value,
subject_id=pic.subject_id,
dataset_id=dataset_id,
dataset_name=dataset_name,
)
@router.post("/datasets/{dataset_id}/embed-all", response_model=EmbedAllResult)
def embed_all(
dataset_id: int,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> EmbedAllResult:
dataset = _get_dataset(db, dataset_id)
result = ds.embed_all(db, dataset)
return EmbedAllResult(dataset_id=dataset_id, **result)
@router.post("/datasets/{dataset_id}/match", response_model=MatchDatasetResult)
def match_dataset(
dataset_id: int,
candidate_dataset_id: int | None = None,
_: User = Depends(require_admin),
db: Session = Depends(get_db),
) -> MatchDatasetResult:
dataset = _get_dataset(db, dataset_id)
result = ds.match_dataset(db, dataset, candidate_dataset_id=candidate_dataset_id)
return MatchDatasetResult(
dataset_id=dataset_id,
cases_processed=result["cases_processed"],
matches_created=result["matches_created"],
candidate_dataset_id=result["candidate_dataset_id"],
)