Files

100 lines
4.8 KiB
Python

from uuid import UUID
from fastapi import APIRouter, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.orm import selectinload
from ..dependencies import Db
from ..models import Bait, CatchReport, Fish, ModerationStatus, Rig, Spot, TackleItem, Waterbody
from ..schemas import BaitOut, FishOut, PaginatedTackleItemOut, RigOut, TackleItemOut, WaterbodyOut
router = APIRouter()
@router.get("/api/v1/fishes", response_model=list[FishOut])
def fishes(db: Db, limit: int = Query(200, ge=1, le=500), offset: int = Query(0, ge=0)) -> list[Fish]:
return list(db.scalars(select(Fish).order_by(Fish.name_ru, Fish.id).offset(offset).limit(limit)))
@router.get("/api/v1/waterbodies", response_model=list[WaterbodyOut])
def waterbodies(db: Db, limit: int = Query(200, ge=1, le=500), offset: int = Query(0, ge=0)) -> list[Waterbody]:
return list(db.scalars(select(Waterbody).order_by(Waterbody.name_ru, Waterbody.id).offset(offset).limit(limit)))
@router.get("/api/v1/baits", response_model=list[BaitOut])
def baits(db: Db, limit: int = Query(200, ge=1, le=500), offset: int = Query(0, ge=0)) -> list[Bait]:
return list(db.scalars(select(Bait).order_by(Bait.name, Bait.id).offset(offset).limit(limit)))
def _item_missing_fields(item: TackleItem) -> list[str]:
return [field for field, value in (
("subcategory", item.subcategory), ("brand", item.brand),
("family", item.family), ("unlock_level", item.unlock_level),
("source_url", item.source_url), ("source_checked_at", item.source_checked_at),
) if value is None]
@router.get("/api/v1/tackle/items", response_model=PaginatedTackleItemOut)
def tackle_items(
db: Db,
category: str | None = Query(None, pattern="^(bait|lure|rod|reel|line|hook|rig|float|sinker|other)$"),
brand: str | None = None,
family: str | None = None,
unlock_level: int | None = Query(None, ge=0),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
) -> PaginatedTackleItemOut:
query = select(TackleItem)
if category:
query = query.where(TackleItem.category == category)
if brand:
query = query.where(TackleItem.brand == brand)
if family:
query = query.where(TackleItem.family == family)
if unlock_level is not None:
query = query.where(TackleItem.unlock_level == unlock_level)
total = db.scalar(query.with_only_columns(func.count(TackleItem.id), maintain_column_froms=True).order_by(None)) or 0
items = list(db.scalars(query.order_by(TackleItem.name, TackleItem.id).offset(offset).limit(limit)))
return PaginatedTackleItemOut(
items=[TackleItemOut.model_validate(item).model_copy(update={"missing_fields": _item_missing_fields(item)}) for item in items],
total=total, limit=limit, offset=offset,
)
@router.get("/api/v1/tackle/items/{item_id}", response_model=TackleItemOut)
def tackle_item(item_id: UUID, db: Db) -> TackleItemOut:
item = db.get(TackleItem, item_id)
if item is None:
raise HTTPException(status_code=404, detail="tackle item not found")
return TackleItemOut.model_validate(item).model_copy(update={"missing_fields": _item_missing_fields(item)})
@router.get("/api/v1/tackle/rigs/{rig_id}", response_model=RigOut)
def rig_detail(rig_id: UUID, db: Db) -> RigOut:
rig = db.scalar(select(Rig).options(selectinload(Rig.components)).where(Rig.id == rig_id))
if rig is None:
raise HTTPException(status_code=404, detail="rig not found")
missing = [field for field, value in (
("source_url", rig.source_url), ("source_checked_at", rig.source_checked_at),
) if value is None]
return RigOut(
id=rig.id, name=rig.name, source_system=rig.source_system,
source_external_id=rig.source_external_id, source_url=rig.source_url,
source_checked_at=rig.source_checked_at, missing_fields=missing,
components=[{
"id": component.id, "role": component.role, "position": component.position,
"raw_value": component.raw_value, "tackle_item_id": component.tackle_item_id,
} for component in sorted(rig.components, key=lambda value: value.position)],
)
@router.get("/api/v1/public-spot-pages")
def public_spot_pages(db: Db, limit: int = Query(500, ge=1, le=500), offset: int = Query(0, ge=0)) -> list[str]:
rows = db.execute(select(Waterbody.slug, Spot.x, Spot.y, Fish.slug)
.select_from(CatchReport).join(Spot, CatchReport.spot_id == Spot.id)
.join(Waterbody, Spot.waterbody_id == Waterbody.id).join(Fish, CatchReport.fish_id == Fish.id)
.where(CatchReport.moderation_status == ModerationStatus.approved, CatchReport.deleted_at.is_(None))
.distinct().order_by(Waterbody.slug, Spot.x, Spot.y, Fish.slug).offset(offset).limit(limit))
return [path for water, x, y, fish in rows for path in
(f"/spots/{water}-{x}x{y}", f"/waterbodies/{water}/{fish}")]