"""Routes for calendar events, screening, ownership, and estimates.""" import functools import logging from collections.abc import Awaitable, Callable from typing import ParamSpec, TypeVar from fastapi import APIRouter, HTTPException, Path, Query from models import SYMBOL_PATTERN, ApiResponse import calendar_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1") P = ParamSpec("P") R = TypeVar("R") def _validate_symbol(symbol: str) -> str: if not SYMBOL_PATTERN.match(symbol): raise HTTPException(status_code=400, detail="Invalid symbol format") return symbol.upper() def _safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: @functools.wraps(fn) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: try: return await fn(*args, **kwargs) except HTTPException: raise except Exception: logger.exception("Upstream data error") raise HTTPException( status_code=502, detail="Data provider error. Check server logs.", ) return wrapper # type: ignore[return-value] # --- Calendar Events --- @router.get("/calendar/earnings", response_model=ApiResponse) @_safe async def earnings_calendar( start_date: str | None = Query(default=None, description="YYYY-MM-DD"), end_date: str | None = Query(default=None, description="YYYY-MM-DD"), ): """Get upcoming earnings announcements.""" data = await calendar_service.get_earnings_calendar(start_date, end_date) return ApiResponse(data=data) @router.get("/calendar/dividends", response_model=ApiResponse) @_safe async def dividend_calendar( start_date: str | None = Query(default=None, description="YYYY-MM-DD"), end_date: str | None = Query(default=None, description="YYYY-MM-DD"), ): """Get upcoming dividend dates.""" data = await calendar_service.get_dividend_calendar(start_date, end_date) return ApiResponse(data=data) @router.get("/calendar/ipo", response_model=ApiResponse) @_safe async def ipo_calendar( start_date: str | None = Query(default=None, description="YYYY-MM-DD"), end_date: str | None = Query(default=None, description="YYYY-MM-DD"), ): """Get upcoming IPOs.""" data = await calendar_service.get_ipo_calendar(start_date, end_date) return ApiResponse(data=data) @router.get("/calendar/splits", response_model=ApiResponse) @_safe async def splits_calendar( start_date: str | None = Query(default=None, description="YYYY-MM-DD"), end_date: str | None = Query(default=None, description="YYYY-MM-DD"), ): """Get upcoming stock splits.""" data = await calendar_service.get_splits_calendar(start_date, end_date) return ApiResponse(data=data) # --- Analyst Estimates --- @router.get("/stock/{symbol}/estimates", response_model=ApiResponse) @_safe async def stock_estimates(symbol: str = Path(..., min_length=1, max_length=20)): """Get analyst consensus estimates.""" symbol = _validate_symbol(symbol) data = await calendar_service.get_analyst_estimates(symbol) return ApiResponse(data=data) @router.get("/stock/{symbol}/share-statistics", response_model=ApiResponse) @_safe async def stock_share_stats(symbol: str = Path(..., min_length=1, max_length=20)): """Get share statistics: float, outstanding, short interest.""" symbol = _validate_symbol(symbol) data = await calendar_service.get_share_statistics(symbol) return ApiResponse(data=data) # --- Ownership (SEC, free) --- @router.get("/stock/{symbol}/sec-insider", response_model=ApiResponse) @_safe async def stock_sec_insider(symbol: str = Path(..., min_length=1, max_length=20)): """Get insider trading data from SEC (Form 4).""" symbol = _validate_symbol(symbol) data = await calendar_service.get_insider_trading(symbol) return ApiResponse(data=data) @router.get("/stock/{symbol}/institutional", response_model=ApiResponse) @_safe async def stock_institutional(symbol: str = Path(..., min_length=1, max_length=20)): """Get institutional holders from SEC 13F filings.""" symbol = _validate_symbol(symbol) data = await calendar_service.get_institutional_holders(symbol) return ApiResponse(data=data) # --- Screener --- @router.get("/screener", response_model=ApiResponse) @_safe async def stock_screener(): """Screen stocks using available filters.""" data = await calendar_service.screen_stocks() return ApiResponse(data=data)