Spaces:
Running
Running
File size: 4,315 Bytes
526c84c 5e698c6 526c84c 5e698c6 526c84c 40a414f 526c84c 5e698c6 526c84c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
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) |