Spaces:
Running
Running
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 = {} | |
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 --- | |
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}) | |
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) | |
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) |