""" SEC Filings endpoints: search, document listing, and exhibit extraction. """ import logging from datetime import datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.schemas.filing import ( ExhibitContentResponse, FilingDocumentInfo, FilingDocumentListResponse, FilingSearchResponse, FilingSummary, ) from app.services.sec_filings_service import sec_filings_service router = APIRouter() logger = logging.getLogger("app.api.v1.filings") @router.get("/search/{ticker}", response_model=FilingSearchResponse) async def search_filings( ticker: str, form_type: Optional[str] = Query( None, description="Comma-separated form types (e.g. '8-K,6-K'). Default: all supported.", ), start_date: Optional[str] = Query(None, description="Start date YYYY-MM-DD"), end_date: Optional[str] = Query(None, description="End date YYYY-MM-DD"), limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), force_refresh: bool = Query(False, description="Force re-indexing from SEC"), db: AsyncSession = Depends(get_db), ): """Search SEC filings for a ticker. Auto-indexes from SEC if not in DB.""" form_types_set = None if form_type: form_types_set = {ft.strip().upper() for ft in form_type.split(",") if ft.strip()} unsupported = form_types_set - sec_filings_service.SUPPORTED_FORM_TYPES if unsupported: raise HTTPException( status_code=400, detail=f"Unsupported form types: {unsupported}. Supported: {sec_filings_service.SUPPORTED_FORM_TYPES}", ) start_dt = None end_dt = None try: if start_date: start_dt = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc) if end_date: end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace( hour=23, minute=59, second=59, tzinfo=timezone.utc ) except ValueError: raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD") if force_refresh: try: await sec_filings_service.index_filings( db, ticker, form_types=form_types_set, force_refresh=True ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Force refresh indexing failed for {ticker}: {e}") raise HTTPException(status_code=502, detail=f"SEC indexing failed: {e}") try: filings, total_count = await sec_filings_service.search_filings( db, ticker, form_types=form_types_set, start_date=start_dt, end_date=end_dt, limit=limit, offset=offset, ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Filing search failed for {ticker}: {e}") raise HTTPException(status_code=502, detail=f"Filing search failed: {e}") summaries = [] for f in filings: docs_count = len(f.documents_json) if f.documents_json else None summaries.append( FilingSummary( accession_number=f.accession_number, form_type=f.form_type, filing_date=f.filing_date.strftime("%Y-%m-%d"), primary_document=f.primary_document, filing_description=f.filing_description, documents_count=docs_count, ) ) return FilingSearchResponse( ticker=ticker.upper(), filings=summaries, total_count=total_count, metadata={ "limit": limit, "offset": offset, "form_types": list(form_types_set) if form_types_set else None, }, ) @router.get("/documents/{accession_number}", response_model=FilingDocumentListResponse) async def get_filing_documents( accession_number: str, db: AsyncSession = Depends(get_db), ): """List all documents within a SEC filing.""" try: documents = await sec_filings_service.get_filing_documents(db, accession_number) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Document listing failed for {accession_number}: {e}") raise HTTPException(status_code=502, detail=f"Document listing failed: {e}") doc_infos = [FilingDocumentInfo(**d) for d in documents] return FilingDocumentListResponse( accession_number=accession_number, documents=doc_infos, metadata={"total_documents": len(doc_infos)}, ) @router.get("/exhibit/{accession_number}", response_model=ExhibitContentResponse) async def get_exhibit_content( accession_number: str, exhibit_type: str = Query("EX-99.1", description="Exhibit type (e.g. EX-99.1)"), db: AsyncSession = Depends(get_db), ): """Extract exhibit content (e.g., press release EX-99.1) from a filing.""" try: result = await sec_filings_service.get_exhibit_content( db, accession_number, exhibit_type ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Exhibit extraction failed for {accession_number}/{exhibit_type}: {e}") raise HTTPException(status_code=502, detail=f"Exhibit extraction failed: {e}") return ExhibitContentResponse( accession_number=accession_number, exhibit_type=exhibit_type, content=result["content"], content_type=result.get("content_type"), filename=result.get("filename"), url=result.get("url"), )