feat: add tackle catalog and recommendation analytics
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from sqlalchemy import select
|
||||
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, Spot, Waterbody
|
||||
from ..schemas import BaitOut, FishOut, WaterbodyOut
|
||||
from ..models import Bait, CatchReport, Fish, ModerationStatus, Rig, Spot, TackleItem, Waterbody
|
||||
from ..schemas import BaitOut, FishOut, PaginatedTackleItemOut, RigOut, TackleItemOut, WaterbodyOut
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -23,6 +26,68 @@ def baits(db: Db, limit: int = Query(200, ge=1, le=500), offset: int = Query(0,
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user