Spaces:
Build error
Build error
| """ | |
| BankBot FastAPI β production entry point. | |
| Phase 7: structured logging, metrics, security headers, rate limiting. | |
| """ | |
| import json | |
| import os | |
| import time | |
| from collections import defaultdict | |
| from fastapi import FastAPI, Request, Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from app.database.database import engine, Base | |
| import app.database.models # noqa: F401 | |
| # βββ Routers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from app.ai.router import router as ai_router | |
| from app.websocket.router import router as ws_router | |
| from app.auth.router import router as auth_router | |
| from app.dashboard.router import router as dashboard_router | |
| from app.notifications.router import router as notifications_router | |
| from app.transactions.router import router as transactions_router | |
| # βββ Observability ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from app.middleware.logging import RequestLoggingMiddleware, metrics, api_logger | |
| # βββ App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI( | |
| title="BankBot AI API", | |
| description="Production-grade AI-powered financial platform", | |
| version="2.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| # βββ CORS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _raw = os.environ.get("BACKEND_CORS_ORIGINS", '["http://localhost:3000","http://localhost:7860"]') | |
| try: | |
| allowed_origins = json.loads(_raw) | |
| except Exception: | |
| allowed_origins = ["http://localhost:3000", "http://localhost:7860"] | |
| # In HF Spaces, the Space URL is dynamic β allow all *.hf.space origins | |
| # by using allow_origin_regex as a fallback | |
| HF_SPACE_PATTERN = r"https://.*\.hf\.space" | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=allowed_origins, | |
| allow_origin_regex=HF_SPACE_PATTERN, | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], | |
| allow_headers=["Authorization", "Content-Type", "X-Request-ID"], | |
| expose_headers=["X-Request-ID", "X-Process-Time"], | |
| ) | |
| # βββ Request logging ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.add_middleware(RequestLoggingMiddleware) | |
| # βββ Security headers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def security_headers(request: Request, call_next): | |
| response: Response = await call_next(request) | |
| response.headers["X-Content-Type-Options"] = "nosniff" | |
| response.headers["X-Frame-Options"] = "DENY" | |
| response.headers["X-XSS-Protection"] = "1; mode=block" | |
| response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" | |
| response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" | |
| return response | |
| # βββ Process-time header ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def process_time_header(request: Request, call_next): | |
| t = time.time() | |
| response = await call_next(request) | |
| response.headers["X-Process-Time"] = f"{(time.time()-t)*1000:.1f}ms" | |
| return response | |
| # βββ Rate limiter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _rate_store: dict = defaultdict(list) | |
| RATE_LIMIT = 120 | |
| RATE_WINDOW = 60 | |
| async def rate_limit(request: Request, call_next): | |
| skip = request.url.path in ("/health", "/") or \ | |
| "websocket" in request.headers.get("upgrade", "").lower() | |
| if skip: | |
| return await call_next(request) | |
| ip = request.client.host if request.client else "unknown" | |
| now = time.time() | |
| _rate_store[ip] = [t for t in _rate_store[ip] if t > now - RATE_WINDOW] | |
| if len(_rate_store[ip]) >= RATE_LIMIT: | |
| metrics.record_error(request.url.path, 429, "rate_limited") | |
| return JSONResponse( | |
| status_code=429, | |
| content={"detail": "Too many requests. Please slow down."}, | |
| headers={"Retry-After": str(RATE_WINDOW)}, | |
| ) | |
| _rate_store[ip].append(now) | |
| return await call_next(request) | |
| # βββ Startup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def startup(): | |
| api_logger.info("BankBot API starting", extra={"version": "2.0.0"}) | |
| Base.metadata.create_all(bind=engine) | |
| api_logger.info("Database tables ready") | |
| # Log active backends | |
| from app.ai.ollama_integration import OPENAI_API_KEY, GROQ_API_KEY, has_active_ai_backend | |
| from app.middleware.cache import cache | |
| from app.database.database import SQLALCHEMY_DATABASE_URL | |
| ai_backend = "openai" if OPENAI_API_KEY else ("groq" if GROQ_API_KEY else "ollama") | |
| api_logger.info("Startup diagnostics", extra={ | |
| "ai_backend": ai_backend, | |
| "ai_available": has_active_ai_backend(), | |
| "db_type": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", | |
| "cache_type": "redis" if cache.use_redis else "memory", | |
| }) | |
| # βββ Routers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.include_router(auth_router) | |
| app.include_router(ai_router) | |
| app.include_router(ws_router) | |
| app.include_router(dashboard_router) | |
| app.include_router(notifications_router) | |
| app.include_router(transactions_router) | |
| # βββ Core endpoints βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def root(): | |
| return {"message": "BankBot AI API v2.0", "status": "operational", "docs": "/docs"} | |
| def health(): | |
| from app.middleware.cache import cache | |
| from app.database.database import SQLALCHEMY_DATABASE_URL | |
| return { | |
| "status": "healthy", | |
| "timestamp": time.time(), | |
| "db": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", | |
| "cache": "redis" if cache.use_redis else "memory", | |
| "uptime_s": round(time.time() - metrics.start_time, 0), | |
| } | |
| def api_status(): | |
| from app.ai.ollama_integration import has_active_ai_backend, OPENAI_API_KEY, GROQ_API_KEY | |
| from app.middleware.cache import cache | |
| from app.database.database import SQLALCHEMY_DATABASE_URL | |
| ai = "openai" if OPENAI_API_KEY else ("groq" if GROQ_API_KEY else "ollama") | |
| return { | |
| "ai_backend": ai, | |
| "ai_available": has_active_ai_backend(), | |
| "db_type": "sqlite" if "sqlite" in SQLALCHEMY_DATABASE_URL else "postgresql", | |
| "cache_type": "redis" if cache.use_redis else "memory", | |
| "version": "2.0.0", | |
| } | |
| def get_metrics(): | |
| """ | |
| Live observability dashboard β request counts, AI latency, | |
| cache hit ratio, WebSocket stats, recent errors. | |
| """ | |
| return metrics.summary() | |