""" Overlay API — headline ingestion only. Scope was reduced: wikimedia / youtube / google_trends / finra / feature_build were removed. Only the Yahoo RSS headline collector remains. """ from datetime import datetime, timedelta, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from app.core.database import get_db from app.models.overlay_feature import OverlayJobLog from app.models.overlay_raw_event import OverlayHeadlineEvent router = APIRouter() class HeadlineItem(BaseModel): title: str publisher: Optional[str] = None published_at: datetime article_guid: str class HeadlinesResponse(BaseModel): symbol: str headlines: List[HeadlineItem] headline_count_6h: int headline_count_24h: int publisher_breadth_24h: int class JobLogEntry(BaseModel): id: str job_type: str status: str started_at: datetime completed_at: Optional[datetime] = None records_processed: int = 0 error_message: Optional[str] = None class JobLogResponse(BaseModel): logs: List[JobLogEntry] total_count: int def _utc(dt): if isinstance(dt, datetime) and dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt @router.get( "/admin/job-log", response_model=JobLogResponse, summary="Overlay job log", tags=["overlay-admin"], ) async def get_job_log( limit: int = Query(50, ge=1, le=500), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(OverlayJobLog).order_by(desc(OverlayJobLog.started_at)).limit(limit) ) logs = result.scalars().all() entries = [ JobLogEntry( id=str(log.id), job_type=log.job_type, status=log.status, started_at=log.started_at, completed_at=log.completed_at, records_processed=log.records_processed or 0, error_message=log.error_message, ) for log in logs ] return JobLogResponse(logs=entries, total_count=len(entries)) @router.get( "/{symbol}/headlines", response_model=HeadlinesResponse, summary="Recent headlines for a symbol", ) async def get_headlines( symbol: str, hours: int = Query(24, ge=1, le=168), db: AsyncSession = Depends(get_db), ): symbol = symbol.upper() cutoff = datetime.now(timezone.utc) - timedelta(hours=hours) cutoff_6h = datetime.now(timezone.utc) - timedelta(hours=6) result = await db.execute( select(OverlayHeadlineEvent) .where(OverlayHeadlineEvent.published_at >= cutoff) .order_by(desc(OverlayHeadlineEvent.published_at)) .limit(500) ) all_events = result.scalars().all() sym_events = [e for e in all_events if symbol in (e.matched_symbols or [])] if not sym_events: raise HTTPException(status_code=404, detail=f"No headlines for {symbol} in last {hours}h") headlines = [ HeadlineItem( title=e.title, publisher=e.publisher, published_at=e.published_at, article_guid=e.article_guid, ) for e in sym_events ] publishers = {e.publisher for e in sym_events if e.publisher} count_6h = sum(1 for e in sym_events if _utc(e.published_at) >= cutoff_6h) return HeadlinesResponse( symbol=symbol, headlines=headlines, headline_count_6h=count_6h, headline_count_24h=len(sym_events), publisher_breadth_24h=len(publishers), )