Some checks failed
continuous-integration/drone/push Build is failing
Api docs (SWAGGER, REDOC) available
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
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)
|