summaryrefslogtreecommitdiff
path: root/backend/app/main.py
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 12:46:13 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 12:46:13 -0700
commit1482422f2f5b236cdcdff4429ae06bb55dca4083 (patch)
tree4653cb4986a8a138f84dbec934effb0d011751d3 /backend/app/main.py
Add stack start and stop scripts
Diffstat (limited to 'backend/app/main.py')
-rw-r--r--backend/app/main.py94
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()