"""FastAPI entrypoint for Prism v2.""" from __future__ import annotations from contextlib import asynccontextmanager from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI, HTTPException, Query, status from fastapi.middleware.cors import CORSMiddleware from app.db import watchlist from app.schemas import FinancialsResponse, HistoryPoint, MarketIndex, SearchResult, TickerOverview, ValuationResponse, WatchlistResponse from app.services import data_service load_dotenv() @asynccontextmanager async def lifespan(_: FastAPI): watchlist.init_db(DB_PATH) yield app = FastAPI(title="Prism v2 API", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001", "http://127.0.0.1:3001", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) DB_PATH = Path(__file__).resolve().parents[1] / "data" / "prism.db" @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/api/search", response_model=list[SearchResult]) def search(q: str = Query(default="", min_length=0)) -> list[dict]: return data_service.search_tickers(q) @app.get("/api/market/indices", response_model=list[MarketIndex]) def market_indices() -> list[dict]: return data_service.get_market_indices() @app.get("/api/tickers/{symbol}/overview", response_model=TickerOverview) def ticker_overview(symbol: str) -> dict: overview = data_service.get_ticker_overview(symbol) if overview is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ticker data unavailable") return overview @app.get("/api/tickers/{symbol}/history", response_model=list[HistoryPoint]) def ticker_history(symbol: str, period: str = Query(default="1y", pattern="^(1m|3m|6m|1y|5y)$")) -> list[dict]: return data_service.get_price_history(symbol, period=period) @app.get("/api/tickers/{symbol}/financials", response_model=FinancialsResponse) def ticker_financials(symbol: str, period: str = Query(default="annual", pattern="^(annual|quarterly)$")) -> dict: return data_service.get_financials(symbol, period=period) @app.get("/api/tickers/{symbol}/valuation", response_model=ValuationResponse) def ticker_valuation(symbol: str) -> dict: return data_service.get_valuation(symbol) @app.get("/api/watchlist", response_model=WatchlistResponse) def get_watchlist() -> dict: items = [] for row in watchlist.list_symbols(DB_PATH): info = data_service.get_company_info(row["symbol"]) items.append({**row, "quote": data_service.build_quote(info, row["symbol"])}) return {"items": items, "limit": watchlist.WATCHLIST_LIMIT} @app.post("/api/watchlist/{symbol}", response_model=WatchlistResponse, status_code=status.HTTP_201_CREATED) def add_watchlist_symbol(symbol: str) -> dict: try: watchlist.add_symbol(symbol, DB_PATH) except watchlist.WatchlistFullError as exc: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return get_watchlist() @app.delete("/api/watchlist/{symbol}", response_model=WatchlistResponse) def delete_watchlist_symbol(symbol: str) -> dict: try: watchlist.remove_symbol(symbol, DB_PATH) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return get_watchlist()