Spaces:
Running
Running
| import logging | |
| import os | |
| from contextlib import asynccontextmanager | |
| from typing import AsyncGenerator | |
| from fastapi import FastAPI, Query, Request, Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from slowapi import _rate_limit_exceeded_handler | |
| from slowapi.errors import RateLimitExceeded | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from app.core.config import settings | |
| from app.core.rate_limit import limiter | |
| from app.core.worker_logging import ( | |
| LIVE_LOG_WHITELIST, | |
| read_log_tail, | |
| resolve_log_path, | |
| setup_app_logging, | |
| setup_structured_logger, | |
| ) | |
| from app.db.mongodb import mongodb | |
| from app.routers import analyze, games | |
| from app.services.nlp_service import get_nlp_service | |
| from app.services.steam_service import steam_service | |
| # Konfiguracja logowania | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" | |
| ) | |
| class SecurityHeadersMiddleware(BaseHTTPMiddleware): | |
| async def dispatch(self, request: Request, call_next): | |
| response: Response = await call_next(request) | |
| response.headers["X-Content-Type-Options"] = "nosniff" | |
| response.headers["X-Frame-Options"] = "SAMEORIGIN" | |
| response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" | |
| return response | |
| async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: | |
| """ | |
| Zarządza cyklem życia aplikacji. | |
| Nawiązuje połączenie z MongoDB przy starcie | |
| i zamyka je przy wyłączeniu. | |
| """ | |
| if not settings.mongodb_url: | |
| raise RuntimeError( | |
| "MONGODB_URL is not set. Please configure it in .env or environment variables." | |
| ) | |
| await mongodb.connect() | |
| setup_structured_logger("live") | |
| setup_app_logging() | |
| yield | |
| await steam_service.close() | |
| await mongodb.disconnect() | |
| app = FastAPI( | |
| title="SentimentStream API", | |
| description="API do analizy sentymentu recenzji gier Steam w czasie rzeczywistym", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| # Rate limiter | |
| app.state.limiter = limiter | |
| app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[arg-type] | |
| # Konfiguracja CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=settings.cors_origins_list, | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "OPTIONS"], | |
| allow_headers=["Content-Type", "Accept"], | |
| ) | |
| # Security headers | |
| app.add_middleware(SecurityHeadersMiddleware) | |
| # Rejestracja routerów | |
| app.include_router(analyze.router, prefix="/api", tags=["analyze"]) | |
| app.include_router(games.router, prefix="/api", tags=["games"]) | |
| async def get_logs( | |
| request: Request, | |
| lines: int = Query(default=100, ge=1, le=1000), | |
| level: str | None = Query(default=None), | |
| event: str | None = Query(default=None), | |
| file: str = Query(default="live"), | |
| ): | |
| """Token-protected endpoint to read structured log tail.""" | |
| auth = request.headers.get("Authorization", "") | |
| expected = settings.worker_trigger_token | |
| if expected: | |
| if not auth.startswith("Bearer ") or auth[7:] != expected: | |
| return JSONResponse(status_code=401, content={"detail": "Unauthorized"}) | |
| log_path = resolve_log_path(file, LIVE_LOG_WHITELIST) | |
| if log_path is None: | |
| return JSONResponse( | |
| status_code=400, | |
| content={"detail": f"Unknown log file: '{file}'. Valid: {list(LIVE_LOG_WHITELIST.keys())}"}, | |
| ) | |
| entries = read_log_tail(log_path, lines=lines, level=level, event=event) | |
| return {"entries": entries, "count": len(entries)} | |
| async def health_check() -> dict: | |
| """Endpoint sprawdzający stan aplikacji z rzeczywistą weryfikacją zależności.""" | |
| mongo_ok = False | |
| if mongodb.client is not None: | |
| try: | |
| await mongodb.client.admin.command("ping") | |
| mongo_ok = True | |
| except Exception: | |
| pass | |
| nlp_svc = get_nlp_service() | |
| model_ok = hasattr(nlp_svc, "classifier") and nlp_svc.classifier is not None | |
| overall = "healthy" if (mongo_ok and model_ok) else "degraded" | |
| return { | |
| "status": overall, | |
| "mongodb": "connected" if mongo_ok else "disconnected", | |
| "model": "loaded" if model_ok else "not_loaded", | |
| } | |
| # Obsługa plików statycznych (Frontend) - tylko jeśli istnieją (np. w Dockerze) | |
| # Ścieżka w kontenerze Docker będzie: /app/frontend/dist | |
| # Lokalnie zazwyczaj nie istnieje (bo używamy vite dev server), więc pomijamy | |
| static_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend", "dist") | |
| if settings.app_mode != "api" and os.path.exists(static_dir): | |
| app.mount("/assets", StaticFiles(directory=os.path.join(static_dir, "assets")), name="assets") | |
| # Catch-all dla SPA (React Router) | |
| async def serve_spa(full_path: str): | |
| if full_path.startswith("api"): | |
| return {"error": "API route not found"} | |
| file_path = os.path.join(static_dir, full_path) | |
| if os.path.exists(file_path) and os.path.isfile(file_path): | |
| return FileResponse(file_path) | |
| return FileResponse(os.path.join(static_dir, "index.html")) | |