import asyncio import logging from typing import Dict, Optional import httpx from fastapi import FastAPI from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse, RedirectResponse logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger("docs") app = FastAPI(title="Unified Marriage API Docs", version="1.0.0") # Переопределяем OpenAPI, чтобы /docs использовал объединённую схему def custom_openapi(): global _cached_openapi import asyncio if _cached_openapi is None or len((_cached_openapi.get("paths") or {})) == 0: _cached_openapi = asyncio.run(build_cache()) return _cached_openapi app.openapi = custom_openapi # Используем ИМЕНА КОНТЕЙНЕРОВ и внутренний порт 8000 MICROSERVICES: Dict[str, str] = { "auth": "http://marriage_auth:8000/openapi.json", "profiles": "http://marriage_profiles:8000/openapi.json", "match": "http://marriage_match:8000/openapi.json", "chat": "http://marriage_chat:8000/openapi.json", "payments": "http://marriage_payments:8000/openapi.json", } _cached_openapi: Optional[dict] = None async def fetch_spec(client: httpx.AsyncClient, name: str, url: str, retries: int = 6, delay: float = 2.0) -> Optional[dict]: for attempt in range(1, retries + 1): try: r = await client.get(url) r.raise_for_status() spec = r.json() log.info("Loaded OpenAPI from %s: %s paths", name, len((spec or {}).get("paths", {}) or {})) return spec except Exception as e: log.warning("Attempt %s/%s failed for %s: %s", attempt, retries, name, e) await asyncio.sleep(delay) log.error("Could not load OpenAPI from %s after %s attempts", name, retries) return None def merge_specs(specs_by_name: Dict[str, Optional[dict]]) -> dict: merged_paths: Dict[str, dict] = {} merged_components = {"schemas": {}} for name, spec in specs_by_name.items(): if not spec: continue for path, methods in (spec.get("paths") or {}).items(): merged_paths[f"/{name}{path}"] = methods merged_components["schemas"].update((spec.get("components") or {}).get("schemas", {}) or {}) openapi_schema = get_openapi( title="Unified Marriage API", version="1.0.0", description="Combined OpenAPI schema for all microservices", routes=[], ) openapi_schema["servers"] = [{"url": "http://localhost:8080"}] openapi_schema["paths"] = merged_paths openapi_schema["components"] = merged_components return openapi_schema async def build_cache() -> dict: async with httpx.AsyncClient(timeout=5.0) as client: tasks = [fetch_spec(client, name, url) for name, url in MICROSERVICES.items()] specs = await asyncio.gather(*tasks) specs_by_name = dict(zip(MICROSERVICES.keys(), specs)) merged = merge_specs(specs_by_name) log.info("Unified OpenAPI built: %s paths", len(merged.get("paths", {}))) return merged async def ensure_cache() -> dict: """Гарантируем, что кэш непустой: если paths == 0 — пересобираем.""" global _cached_openapi if _cached_openapi is None or len((_cached_openapi.get("paths") or {})) == 0: _cached_openapi = await build_cache() return _cached_openapi @app.on_event("startup") async def _warmup_cache(): """Always rebuild cache on startup so Swagger has full data.""" global _cached_openapi try: _cached_openapi = await build_cache() log.info("[startup] Cache built: %s paths", len((_cached_openapi.get("paths") or {}))) except Exception as e: log.error("[startup] Failed to build cache: %s", e) @app.get("/", include_in_schema=False) def root(): return RedirectResponse(url="/docs") @app.get("/_health", include_in_schema=False) def health(): return {"status": "ok", "cached_paths": len((_cached_openapi or {}).get("paths", {}))} @app.get("/debug", include_in_schema=False) async def debug(): out = {} async with httpx.AsyncClient(timeout=5.0) as client: for name, url in MICROSERVICES.items(): try: r = await client.get(url) out[name] = {"status": r.status_code, "len": len(r.text), "starts_with": r.text[:80]} except Exception as e: out[name] = {"error": str(e)} return out @app.get("/refresh", include_in_schema=False) async def refresh(): global _cached_openapi _cached_openapi = await build_cache() return {"refreshed": True, "paths": len(_cached_openapi.get("paths", {}))} @app.get("/openapi.json", include_in_schema=False) async def unified_openapi(): data = await ensure_cache() return JSONResponse(data)