import logging import gradio as gr from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware from app.config import DEMO_MODE from app.middleware.rate_limit import auth_limiter, account_limiter from app.routes import auth_routes, generate, brand_voice, history, account, payments, bulk from app.legal import pages as legal_pages from app.ui import build_ui logging.basicConfig(level=logging.INFO) logger = logging.getLogger("etsy_optimizer") app = FastAPI(title="Etsy Listing Optimizer API") # --- CORS: explicit allowlist, never wide open -------------------------------- # For a same-origin Gradio+FastAPI deployment on a single HF Space this can # stay empty/self-only; add your own domain here if you build a separate # frontend that calls this API cross-origin. app.add_middleware( CORSMiddleware, allow_origins=[], allow_credentials=True, allow_methods=["GET", "POST", "DELETE"], allow_headers=["Authorization", "Content-Type"], ) # --- Rate limiting ------------------------------------------------------------- app.state.limiter = auth_limiter # slowapi needs exactly one limiter on app.state; # account_limiter shares the same in-memory backend semantics and is applied # via its own decorator on /generate - both raise the same RateLimitExceeded. app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) @app.exception_handler(Exception) async def friendly_error_handler(request: Request, exc: Exception): # Last-resort catch-all: never leak a raw stack trace to the UI. logger.exception("Unhandled error on %s", request.url.path) return JSONResponse(status_code=500, content={"detail": "Something went wrong. Please try again."}) app.include_router(auth_routes.router, tags=["auth"]) app.include_router(generate.router, tags=["generate"]) app.include_router(brand_voice.router, tags=["brand-voice"]) app.include_router(history.router, tags=["history"]) app.include_router(account.router, tags=["account"]) app.include_router(payments.router, tags=["billing"]) app.include_router(bulk.router, tags=["bulk"]) app.include_router(legal_pages.router, tags=["legal"]) @app.get("/health") async def health(): return {"status": "ok", "demo_mode": DEMO_MODE} # Mount the Gradio UI at the root path, on top of the FastAPI routes above. # # NOTE: deliberately NOT named `demo`. Hugging Face's Gradio SDK runtime # scans app_file for a top-level `gr.Blocks`/`gr.Interface` variable named # exactly `demo` and auto-launches it as its own standalone server, even # though we never call .launch() ourselves. That collided with our explicit # uvicorn process (server.py) on the next free port -> "address already in # use: 7861". Renaming it opts this app out of that auto-launch behavior, # since we're intentionally mounting it onto FastAPI instead. gradio_ui = build_ui(app) app = gr.mount_gradio_app(app, gradio_ui, path="/")