diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-17 12:46:13 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-17 12:46:13 -0700 |
| commit | 1482422f2f5b236cdcdff4429ae06bb55dca4083 (patch) | |
| tree | 4653cb4986a8a138f84dbec934effb0d011751d3 /backend/app/main.py | |
Add stack start and stop scripts
Diffstat (limited to 'backend/app/main.py')
| -rw-r--r-- | backend/app/main.py | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..fc98a5e --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,94 @@ +"""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 HistoryPoint, MarketIndex, SearchResult, TickerOverview, 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/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() |
