Spaces:
Running
Running
Delete app.py
Browse files
app.py
DELETED
|
@@ -1,125 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
FinWise β FastAPI backend for HuggingFace Spaces
|
| 3 |
-
-------------------------------------------------
|
| 4 |
-
β’ Serves all static HTML/CSS/JS files from the same directory
|
| 5 |
-
β’ /api/quotes?symbols=NVDA,AAPL,MSFT β live quotes via yfinance
|
| 6 |
-
β’ /api/chart?symbol=NVDA&days=8 β daily closes for sparkline
|
| 7 |
-
β’ Full CORS enabled so the browser JS can call these endpoints freely
|
| 8 |
-
β’ Runs on port 7860 (HuggingFace default)
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import os
|
| 12 |
-
import json
|
| 13 |
-
from pathlib import Path
|
| 14 |
-
from typing import List
|
| 15 |
-
|
| 16 |
-
import yfinance as yf
|
| 17 |
-
from fastapi import FastAPI, Query, HTTPException
|
| 18 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 19 |
-
from fastapi.staticfiles import StaticFiles
|
| 20 |
-
from fastapi.responses import FileResponse, JSONResponse
|
| 21 |
-
import uvicorn
|
| 22 |
-
|
| 23 |
-
# ββ App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
-
app = FastAPI(title="FinWise API", docs_url="/api/docs")
|
| 25 |
-
|
| 26 |
-
# Allow all origins (the page is served from the same Space, but be permissive)
|
| 27 |
-
app.add_middleware(
|
| 28 |
-
CORSMiddleware,
|
| 29 |
-
allow_origins=["*"],
|
| 30 |
-
allow_methods=["GET"],
|
| 31 |
-
allow_headers=["*"],
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
# ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
-
def safe_float(val, decimals: int = 2):
|
| 36 |
-
"""Return a rounded float or None if value is NaN / missing."""
|
| 37 |
-
try:
|
| 38 |
-
v = float(val)
|
| 39 |
-
if v != v: # NaN check
|
| 40 |
-
return None
|
| 41 |
-
return round(v, decimals)
|
| 42 |
-
except Exception:
|
| 43 |
-
return None
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# ββ Routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
-
|
| 48 |
-
@app.get("/api/quotes")
|
| 49 |
-
def get_quotes(symbols: str = Query(..., description="Comma-separated tickers, e.g. NVDA,AAPL")):
|
| 50 |
-
"""
|
| 51 |
-
Returns a dict keyed by ticker symbol:
|
| 52 |
-
{ "NVDA": { "price": 875.24, "chg1d": 2.4, "vol": 45000000 }, ... }
|
| 53 |
-
"""
|
| 54 |
-
tickers = [t.strip().upper() for t in symbols.split(",") if t.strip()]
|
| 55 |
-
if not tickers:
|
| 56 |
-
raise HTTPException(status_code=400, detail="No valid symbols provided")
|
| 57 |
-
if len(tickers) > 30:
|
| 58 |
-
raise HTTPException(status_code=400, detail="Max 30 symbols per request")
|
| 59 |
-
|
| 60 |
-
try:
|
| 61 |
-
# yfinance batch download β fast_info is the lightest call
|
| 62 |
-
data = yf.Tickers(" ".join(tickers))
|
| 63 |
-
result = {}
|
| 64 |
-
for sym in tickers:
|
| 65 |
-
try:
|
| 66 |
-
t = data.tickers[sym]
|
| 67 |
-
fi = t.fast_info # lightweight, no extra HTTP round-trip
|
| 68 |
-
result[sym] = {
|
| 69 |
-
"price": safe_float(fi.last_price),
|
| 70 |
-
"chg1d": safe_float(fi.last_price / fi.previous_close * 100 - 100)
|
| 71 |
-
if fi.previous_close else None,
|
| 72 |
-
"vol": int(fi.three_month_average_volume or 0) or None,
|
| 73 |
-
"prev": safe_float(fi.previous_close),
|
| 74 |
-
"high": safe_float(fi.day_high),
|
| 75 |
-
"low": safe_float(fi.day_low),
|
| 76 |
-
}
|
| 77 |
-
except Exception:
|
| 78 |
-
result[sym] = {"price": None, "chg1d": None, "vol": None}
|
| 79 |
-
|
| 80 |
-
return JSONResponse(content=result)
|
| 81 |
-
|
| 82 |
-
except Exception as e:
|
| 83 |
-
raise HTTPException(status_code=502, detail=f"yfinance error: {str(e)}")
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
@app.get("/api/chart")
|
| 87 |
-
def get_chart(symbol: str = Query(...), days: int = Query(default=8, ge=2, le=30)):
|
| 88 |
-
"""
|
| 89 |
-
Returns daily closing prices for the last `days` trading days.
|
| 90 |
-
Used for sparklines.
|
| 91 |
-
{ "symbol": "NVDA", "closes": [820.1, 835.4, ...] }
|
| 92 |
-
"""
|
| 93 |
-
sym = symbol.strip().upper()
|
| 94 |
-
try:
|
| 95 |
-
hist = yf.Ticker(sym).history(period=f"{days}d", interval="1d")
|
| 96 |
-
closes = [safe_float(v) for v in hist["Close"].tolist() if v == v]
|
| 97 |
-
closes = [c for c in closes if c is not None]
|
| 98 |
-
return JSONResponse(content={"symbol": sym, "closes": closes[-7:]})
|
| 99 |
-
except Exception as e:
|
| 100 |
-
raise HTTPException(status_code=502, detail=f"yfinance chart error: {str(e)}")
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
@app.get("/api/health")
|
| 104 |
-
def health():
|
| 105 |
-
return {"status": "ok", "service": "FinWise API"}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ββ Serve static files (HTML/CSS/JS) βββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
-
# All .html files in the same directory are served directly.
|
| 110 |
-
# The root "/" returns index.html.
|
| 111 |
-
BASE_DIR = Path(__file__).parent
|
| 112 |
-
|
| 113 |
-
@app.get("/")
|
| 114 |
-
def root():
|
| 115 |
-
return FileResponse(BASE_DIR / "index.html")
|
| 116 |
-
|
| 117 |
-
# Mount everything else as static β CSS, JS, other HTML pages
|
| 118 |
-
# We do this AFTER the API routes so /api/* is not caught by StaticFiles
|
| 119 |
-
app.mount("/", StaticFiles(directory=str(BASE_DIR), html=True), name="static")
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# ββ Entry point βββββοΏ½οΏ½βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
-
if __name__ == "__main__":
|
| 124 |
-
port = int(os.environ.get("PORT", 7860))
|
| 125 |
-
uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|