from typing import Annotated, Any from fansifter_common.api.schemas import response_errors from fastapi import APIRouter, Depends, Query from resonance_engine.adapters.db import db from resonance_engine.api import schemas, security from resonance_engine.dsp.exceptions import UnknownDSPClientError from resonance_engine.dsp.models import DSPClient from resonance_engine.fandata import handlers from resonance_engine.fandata.exceptions import FanConnectionNotFoundError from resonance_engine.fandata.models import FanCollectionState, FanConnection from resonance_engine.fandata.types import FanView, FanViewPaginated router = APIRouter( prefix="/fandata", tags=["Fan Data"], dependencies=[Depends(security.authenticate)], ) @router.get( "/stats", response_model=schemas.FanStats, ) def stats(params: Annotated[schemas.GetFanStatsInput, Query()]) -> Any: """Approximate connection/collection counts from planner estimates (no scan).""" with db.autocommit(): dsp_client_id: int | None = None if params.dsp_client_name is not None: client = DSPClient.query.where( DSPClient.name == params.dsp_client_name ).one_or_none() dsp_client_id = client.id if client is not None else -1 conn_q = FanConnection.query coll_q = FanCollectionState.query if dsp_client_id is not None: conn_q = conn_q.where(FanConnection.dsp_client_id == dsp_client_id) coll_q = coll_q.where( FanCollectionState.last_dsp_client_id == dsp_client_id ) conn = conn_q.approx_stats() coll = coll_q.approx_stats() return schemas.FanStats( connections=schemas.FanConnectionStats( total=conn.total, active=conn.active, revoked=conn.revoked ), collection=schemas.FanCollectionStats( total=coll.total, healthy=coll.healthy, with_errors=coll.with_errors ), ) @router.post( "/connections", response_model=schemas.FanConnection, responses=response_errors(UnknownDSPClientError), ) def create_fan_connection(data: schemas.CreateFanConnectionInput) -> Any: return handlers.create_fan_connection( handlers.CreateFanConnectionRequest( email=data.email, dsp_client_name=data.dsp_client_name, token=data.token, ) ) @router.post( "/connections/batch", response_model=schemas.BatchCreateResult, responses=response_errors(UnknownDSPClientError), ) def batch_create_fan_connections(data: schemas.BatchCreateFanConnectionInput) -> Any: count = handlers.batch_create_fan_connections( handlers.BatchCreateFanConnectionRequest( items=[ handlers.CreateFanConnectionRequest( email=item.email, dsp_client_name=item.dsp_client_name, token=item.token, ) for item in data.items ] ) ) return schemas.BatchCreateResult(count=count) @router.get( "/fans", response_model=FanViewPaginated, ) def get_fans(params: Annotated[schemas.GetFansInput, Query()]) -> Any: return handlers.get_fans( handlers.GetFansRequest( dsp_id=params.dsp_id, dsp_client_names=params.dsp_client_names, status=params.status, search=params.search, is_collected=params.is_collected, cursor=params.cursor, limit=params.limit, ) ) @router.post( "/connections/collect", response_model=FanView, responses=response_errors(UnknownDSPClientError, FanConnectionNotFoundError), ) def collect_fan_connection(data: schemas.CollectFanConnectionInput) -> Any: return handlers.collect_fan( handlers.CollectFanRequest( fan_id=data.fan_id, dsp_client_name=data.dsp_client_name, force=data.force, ) )