67 lines
3.0 KiB
Python
67 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session, joinedload, selectinload
|
|
|
|
from ..dependencies import Db
|
|
from ..models import CatchReport, Fish, ModerationStatus, Spot, Waterbody
|
|
from ..schemas import TackleCombinationOut
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/api/v1/analytics/tackle", response_model=list[TackleCombinationOut])
|
|
def tackle_combinations(
|
|
db: Db,
|
|
waterbody: str | None = None,
|
|
fish: str | None = None,
|
|
method: str | None = None,
|
|
hours: int = Query(72, ge=24, le=168),
|
|
min_samples: int = Query(3, ge=1, le=100),
|
|
min_players: int = Query(2, ge=1, le=100),
|
|
) -> list[TackleCombinationOut]:
|
|
now = datetime.now(timezone.utc)
|
|
query = select(CatchReport).options(
|
|
joinedload(CatchReport.fish), joinedload(CatchReport.waterbody),
|
|
selectinload(CatchReport.tackle_components),
|
|
).where(
|
|
CatchReport.moderation_status == ModerationStatus.approved,
|
|
CatchReport.deleted_at.is_(None),
|
|
CatchReport.reported_at >= now - timedelta(hours=hours),
|
|
)
|
|
if waterbody:
|
|
query = query.join(Waterbody, CatchReport.waterbody_id == Waterbody.id).where(Waterbody.slug == waterbody)
|
|
if fish:
|
|
query = query.join(Fish, CatchReport.fish_id == Fish.id).where(Fish.slug == fish)
|
|
if method:
|
|
query = query.where(CatchReport.fishing_method == method)
|
|
|
|
groups: dict[tuple[str, str], list[CatchReport]] = defaultdict(list)
|
|
for report in db.scalars(query):
|
|
for component in report.tackle_components:
|
|
if component.raw_value.strip():
|
|
groups[(component.role, component.raw_value.strip())].append(report)
|
|
|
|
result = []
|
|
for (role, value), reports in groups.items():
|
|
unique_reports = {report.id: report for report in reports}
|
|
players = {report.player_name.strip().casefold() for report in unique_reports.values() if report.player_name and report.player_name.strip()}
|
|
catches = len(unique_reports)
|
|
unique_players = len(players)
|
|
last_seen = max(report.reported_at for report in unique_reports.values())
|
|
enough = catches >= min_samples and unique_players >= min_players
|
|
result.append(TackleCombinationOut(
|
|
role=role, value=value, catches=catches, unique_players=unique_players,
|
|
last_seen_at=last_seen, status="recommendation" if enough else "insufficient_data",
|
|
explanation=(
|
|
"Достаточно независимых наблюдений для рекомендации."
|
|
if enough else
|
|
f"Данных мало: нужно минимум {min_samples} наблюдения и {min_players} независимых игрока."
|
|
),
|
|
))
|
|
return sorted(result, key=lambda item: (item.status != "recommendation", -item.catches, -item.unique_players, item.role, item.value))
|