mgbam's picture
Update app.py
526c84c verified
raw
history blame
4.32 kB
import httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, ValidationError
from contextlib import asynccontextmanager
# --- Configuration ---
# Centralized configuration makes the app easier to modify.
COINGECKO_API_URL = "https://api.coingecko.com/api/v3/simple/price"
TARGET_COINS = ["bitcoin", "ethereum", "dogecoin"]
TARGET_CURRENCY = "usd"
# --- Pydantic Models for Data Validation ---
# This ensures the API response from CoinGecko matches our expectations.
class PriceData(BaseModel):
usd: float
# The full response is a dictionary mapping coin names (str) to their PriceData.
# Example: {"bitcoin": {"usd": 65000.0}}
ApiResponse = dict[str, PriceData]
# --- Application State and Lifespan Management ---
# Using a lifespan event is the modern way to manage resources like HTTP clients.
# This client will be created on startup and closed gracefully on shutdown.
app_state = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# On startup: create a single, reusable httpx client.
app_state["http_client"] = httpx.AsyncClient()
yield
# On shutdown: close the client.
await app_state["http_client"].aclose()
# --- FastAPI App Initialization ---
app = FastAPI(lifespan=lifespan)
templates = Jinja2Templates(directory="templates")
# --- API Endpoints ---
@app.get("/", response_class=HTMLResponse)
async def serve_home_page(request: Request):
"""Serves the main HTML page which will then trigger HTMX calls."""
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/api/prices", response_class=HTMLResponse)
async def get_prices_ui():
"""
This endpoint is called by HTMX.
It fetches crypto data and returns an HTML FRAGMENT to be swapped into the page.
"""
try:
client: httpx.AsyncClient = app_state["http_client"]
# Build the request parameters dynamically
params = {
"ids": ",".join(TARGET_COINS),
"vs_currencies": TARGET_CURRENCY
}
response = await client.get(COINGECKO_API_URL, params=params)
response.raise_for_status() # Raise an exception for 4xx/5xx errors
# Validate the received data against our Pydantic model
prices = ApiResponse(**response.json())
# This is a simple but effective way to render an HTML fragment.
# For larger fragments, you would use another Jinja2 template.
html_fragment = ""
for coin, data in prices.items():
html_fragment += f"<div><strong>{coin.capitalize()}:</strong> ${data.usd:,.2f}</div>"
return HTMLResponse(content=html_fragment)
except httpx.RequestError as e:
# Handle network-related errors
return HTMLResponse(content=f"<div class='error'>Network error: Could not connect to CoinGecko.</div>", status_code=503)
except ValidationError as e:
# Handle cases where CoinGecko's API response changes unexpectedly
return HTMLResponse(content=f"<div class='error'>Invalid API response from data source.</div>", status_code=502)
except Exception as e:
# Generic catch-all for other unexpected errors
return HTMLResponse(content=f"<div class='error'>An unexpected error occurred.</div>", status_code=500)
@app.get("/api/forecast/{coin_id}")
async def get_forecast(coin_id: str):
"""
Placeholder for a real forecasting model.
A real implementation would involve loading a pre-trained model,
fetching historical data, and running a prediction.
"""
if coin_id not in TARGET_COINS:
raise HTTPException(status_code=404, detail=f"Forecast not available for '{coin_id}'")
# In a real app, this would be the output of your ML model.
mock_forecast = {
"coin_id": coin_id,
"prediction_in_24h": "up",
"confidence": 0.78,
"detail": "This is a mock forecast. A real implementation requires a data science model."
}
return mock_forecast
# --- Main execution (for development) ---
if __name__ == "__main__":
# To run: uvicorn main:app --reload --port 7860
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)