refactor: extract public data router
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from ..config import settings
|
||||
from ..dependencies import Db
|
||||
from ..models import (
|
||||
CatchReport,
|
||||
CommunityImportRun,
|
||||
DataSource,
|
||||
ExternalObservation,
|
||||
Fish,
|
||||
OfficialRecordImport,
|
||||
SourceType,
|
||||
Waterbody,
|
||||
)
|
||||
from ..schemas import (
|
||||
ImportRunPublicOut,
|
||||
OfficialRecordOut,
|
||||
PaginatedOfficialRecordOut,
|
||||
PublicObservationOut,
|
||||
SourceStatusOut,
|
||||
)
|
||||
from ..time_utils import aware
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/v1/community-observations", response_model=list[PublicObservationOut])
|
||||
def community_observations(
|
||||
db: Db,
|
||||
limit: int = Query(12, ge=1, le=50),
|
||||
offset: int = Query(0, ge=0),
|
||||
waterbody: str | None = None,
|
||||
fish: str | None = None,
|
||||
) -> list[PublicObservationOut]:
|
||||
query = select(ExternalObservation).join(ExternalObservation.source).options(
|
||||
joinedload(ExternalObservation.source)
|
||||
).where(
|
||||
ExternalObservation.catch_report_id.is_(None),
|
||||
ExternalObservation.status != "rejected",
|
||||
DataSource.enabled.is_(True),
|
||||
)
|
||||
if waterbody:
|
||||
query = query.join(ExternalObservation.waterbody).where(Waterbody.slug == waterbody)
|
||||
if fish:
|
||||
query = query.join(ExternalObservation.fish).where(Fish.slug == fish)
|
||||
items = list(db.scalars(
|
||||
query.order_by(ExternalObservation.last_seen_at.desc(), ExternalObservation.id.desc())
|
||||
.offset(offset).limit(limit)
|
||||
))
|
||||
result: list[PublicObservationOut] = []
|
||||
for item in items:
|
||||
missing = []
|
||||
if item.x is None or item.y is None:
|
||||
missing.append("координаты")
|
||||
if item.weight_g is None:
|
||||
missing.append("вес")
|
||||
result.append(PublicObservationOut(
|
||||
id=item.id,
|
||||
source_system=item.source_system,
|
||||
source_name=item.source.name,
|
||||
source_url=item.source_url,
|
||||
fish_name=item.fish_name,
|
||||
waterbody_name=item.waterbody_name,
|
||||
x=item.x,
|
||||
y=item.y,
|
||||
weight_g=item.weight_g,
|
||||
last_seen_at=item.last_seen_at,
|
||||
missing_fields=missing,
|
||||
quality="incomplete" if missing else "unverified",
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/api/v1/source-status", response_model=list[SourceStatusOut])
|
||||
def source_status(db: Db) -> list[SourceStatusOut]:
|
||||
now = datetime.now(timezone.utc)
|
||||
result = []
|
||||
for source in db.scalars(select(DataSource).order_by(DataSource.name)):
|
||||
runs = list(db.scalars(
|
||||
select(CommunityImportRun)
|
||||
.where(CommunityImportRun.source_system == source.key)
|
||||
.order_by(CommunityImportRun.started_at.desc()).limit(20)
|
||||
))
|
||||
latest = runs[0] if runs else None
|
||||
success = next((run for run in runs if run.status == "success"), None)
|
||||
if not source.enabled:
|
||||
state = "disabled"
|
||||
elif latest is None:
|
||||
state = "waiting"
|
||||
elif latest.status == "failed":
|
||||
state = "source_changed" if "CommunityParseError" in (latest.error_summary or "") else "temporarily_limited"
|
||||
elif aware(latest.started_at) < now - timedelta(seconds=settings.community_import_interval_seconds * 2):
|
||||
state = "stale"
|
||||
else:
|
||||
state = "healthy"
|
||||
result.append(SourceStatusOut(
|
||||
source_system=source.key,
|
||||
name=source.name,
|
||||
status=state,
|
||||
last_started_at=latest.started_at if latest else None,
|
||||
last_success_at=success.started_at if success else None,
|
||||
observations=db.scalar(
|
||||
select(func.count()).select_from(ExternalObservation)
|
||||
.where(ExternalObservation.source_system == source.key)
|
||||
) or 0,
|
||||
))
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/api/v1/records", response_model=PaginatedOfficialRecordOut)
|
||||
def records(
|
||||
db: Db,
|
||||
fish: str | None = None,
|
||||
waterbody: str | None = None,
|
||||
category: str | None = None,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> PaginatedOfficialRecordOut:
|
||||
query = select(CatchReport).options(
|
||||
joinedload(CatchReport.fish),
|
||||
joinedload(CatchReport.waterbody),
|
||||
joinedload(CatchReport.bait),
|
||||
).where(CatchReport.source_type == SourceType.official_record)
|
||||
if fish:
|
||||
query = query.join(CatchReport.fish).where(Fish.slug == fish)
|
||||
if waterbody:
|
||||
query = query.join(CatchReport.waterbody).where(Waterbody.slug == waterbody)
|
||||
if category:
|
||||
query = query.where(CatchReport.raw_payload["category"].as_string() == category)
|
||||
total = db.scalar(
|
||||
query.with_only_columns(func.count(CatchReport.id), maintain_column_froms=True).order_by(None)
|
||||
) or 0
|
||||
items = list(db.scalars(
|
||||
query.order_by(CatchReport.caught_at.desc(), CatchReport.weight_g.desc(), CatchReport.id.desc())
|
||||
.offset(offset).limit(limit)
|
||||
))
|
||||
return PaginatedOfficialRecordOut(
|
||||
items=[OfficialRecordOut(
|
||||
id=item.id,
|
||||
fish=item.fish.name_ru,
|
||||
weight_g=item.weight_g,
|
||||
waterbody=item.waterbody.name_ru,
|
||||
bait=item.bait.name if item.bait else None,
|
||||
player_name=item.player_name,
|
||||
record_date=item.caught_at,
|
||||
category=(item.raw_payload or {}).get("category"),
|
||||
region=(item.raw_payload or {}).get("region"),
|
||||
source_url=item.source_url,
|
||||
) for item in items],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/imports", response_model=list[ImportRunPublicOut])
|
||||
def imports(
|
||||
db: Db,
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> list[OfficialRecordImport]:
|
||||
return list(db.scalars(
|
||||
select(OfficialRecordImport)
|
||||
.order_by(OfficialRecordImport.started_at.desc(), OfficialRecordImport.id.desc())
|
||||
.offset(offset).limit(limit)
|
||||
))
|
||||
Reference in New Issue
Block a user