CoolFace
Apppublic

atulyamann/flexup-api

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
api.py177 linesDownload Raw Back to root
1"""2FlexUp Metrics API3Exposes the stitched.py processing logic as a secure REST API.4"""5 6import os7import io8import uuid9import secrets10import logging11from pathlib import Path12 13from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, status14from fastapi.security import APIKeyHeader15from fastapi.responses import StreamingResponse, FileResponse, Response16from fastapi.staticfiles import StaticFiles17import pandas as pd18 19# In-memory store for processed files {token: bytes}20# Keeps the most recent results (max 20) — older ones evicted as new ones come in21from collections import OrderedDict22_result_store: "OrderedDict[str, bytes]" = OrderedDict()23_MAX_RESULTS = 2024 25from core import run_pipeline  # business logic extracted from stitched.py26 27# ---------------------------------------------------------------------------28# Logging29# ---------------------------------------------------------------------------30logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")31logger = logging.getLogger(__name__)32 33# ---------------------------------------------------------------------------34# API-key auth35# ---------------------------------------------------------------------------36API_KEY_NAME = "X-API-Key"37_api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)38 39def _get_api_key() -> str:40    """Load the expected API key from the environment (never hard-coded)."""41    key = os.environ.get("FLEXUP_API_KEY", "")42    if not key:43        raise RuntimeError("FLEXUP_API_KEY environment variable is not set.")44    return key45 46async def verify_api_key(api_key: str = Depends(_api_key_header)) -> str:47    """Dependency that validates the caller's API key."""48    expected = _get_api_key()49    if not api_key or not secrets.compare_digest(api_key, expected):50        raise HTTPException(51            status_code=status.HTTP_401_UNAUTHORIZED,52            detail="Invalid or missing API key.",53            headers={"WWW-Authenticate": "ApiKey"},54        )55    return api_key56 57# ---------------------------------------------------------------------------58# App59# ---------------------------------------------------------------------------60app = FastAPI(61    title="FlexUp Metrics API",62    description="Upload an Excel workbook and receive the processed site-level metrics.",63    version="1.0.0",64    docs_url="/docs",65    redoc_url="/redoc",66)67 68# Serve the UI69app.mount("/static", StaticFiles(directory="static"), name="static")70 71@app.get("/", include_in_schema=False)72def root():73    return FileResponse("static/index.html")74 75# ---------------------------------------------------------------------------76# Routes77# ---------------------------------------------------------------------------78 79@app.get("/health", tags=["ops"])80def health():81    """Liveness probe — no auth required."""82    return {"status": "ok"}83 84 85@app.post(86    "/process",87    tags=["metrics"],88    summary="Process an Excel workbook and return the metrics file directly.",89    response_description="Excel file with site-level metrics (SiteLevel(2) sheet).",90)91async def process_workbook(92    file: UploadFile = File(..., description="Excel workbook (.xlsx) with the required sheets."),93    _: str = Depends(verify_api_key),94):95    """96    Upload the source Excel workbook and receive the processed metrics workbook.97    Returns an `.xlsx` file as a binary download — best for Python scripts and curl.98 99    For the UI, use `/upload` instead which returns a token.100    """101    output_bytes = await _run_pipeline_for_upload(file)102    return StreamingResponse(103        io.BytesIO(output_bytes),104        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",105        headers={"Content-Disposition": 'attachment; filename="flexup_metrics.xlsx"'},106    )107 108 109@app.post(110    "/upload",111    tags=["metrics"],112    summary="Process an Excel workbook and return a download token.",113)114async def upload_workbook(115    file: UploadFile = File(...),116    _: str = Depends(verify_api_key),117):118    """Same as /process but returns a token instead of the file. Used by the web UI."""119    output_bytes = await _run_pipeline_for_upload(file)120    token = str(uuid.uuid4())121    _result_store[token] = output_bytes122    while len(_result_store) > _MAX_RESULTS:123        _result_store.popitem(last=False)124    return {"token": token}125 126 127async def _run_pipeline_for_upload(file: UploadFile) -> bytes:128    """Validate the upload and run the pipeline. Returns the output bytes."""129    if not file.filename or not file.filename.lower().endswith(".xlsx"):130        raise HTTPException(131            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,132            detail="Only .xlsx files are accepted.",133        )134 135    contents = await file.read()136    if len(contents) == 0:137        raise HTTPException(138            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,139            detail="Uploaded file is empty.",140        )141 142    MAX_UPLOAD_BYTES = 50 * 1024 * 1024143    if len(contents) > MAX_UPLOAD_BYTES:144        raise HTTPException(145            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,146            detail="File exceeds the 50 MB limit.",147        )148 149    logger.info("Received workbook '%s' (%d bytes)", file.filename, len(contents))150 151    try:152        return run_pipeline(io.BytesIO(contents))153    except ValueError as exc:154        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))155    except Exception as exc:156        logger.exception("Pipeline error")157        raise HTTPException(158            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,159            detail=f"Processing failed: {exc}",160        )161 162 163@app.get("/download/{token}", tags=["metrics"])164def download_result(token: str):165    """Retrieve a processed file by its token."""166    data = _result_store.get(token)167    if data is None:168        raise HTTPException(status_code=404, detail="File not found or expired.")169    return StreamingResponse(170        io.BytesIO(data),171        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",172        headers={173            "Content-Disposition": 'attachment; filename="flexup_metrics.xlsx"',174            "Content-Length": str(len(data)),175        },176    )177