fastcheck/fastcheck_api/app/main.py
2026-04-16 12:05:29 -04:00

135 lines
4.0 KiB
Python

from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, JSONResponse
from fastcheck_api.app.api.v1.routers import auth, checks, reports, usage
from fastcheck_api.app.core.logging import configure_logging
from fastcheck_api.app.core.mongodb import lifespan_db
from fastcheck_api.app.services.legacy_service import legacy_service
from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service
from fastcheck_api.app.middleware.request_logging import RequestLoggingMiddleware
from fastcheck_api.app.middleware.tenant_context import TenantContextMiddleware
from fastcheck_api.app.core.config import settings
logger = logging.getLogger("fastcheck")
tags_metadata = [
{"name": "auth", "description": "Authentication and session management."},
{"name": "checks", "description": "Create checks (evaluations) and retrieve job/results."},
{"name": "reports", "description": "Fetch saved reports from existing persistence."},
{"name": "usage", "description": "Usage and credit balance information."},
]
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_logging()
async for _ in lifespan_db():
if settings.rabbitmq_enabled:
try:
await rabbitmq_service.connect()
except Exception:
logger.exception("RabbitMQ connection failed; continuing without async processing")
try:
yield
finally:
try:
await rabbitmq_service.close()
except Exception:
logger.exception("RabbitMQ shutdown failed")
await legacy_service.close()
app = FastAPI(
title="FastCheck API",
version="0.1.0",
description="FastCheck API for check evaluation and report retrieval.",
openapi_url="/openapi.json",
docs_url="/internal/docs",
redoc_url="/internal/redoc",
openapi_tags=tags_metadata,
lifespan=lifespan,
)
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(TenantContextMiddleware)
@app.get(
"/",
summary="API entrypoint",
description="Convenience endpoint with links to the OpenAPI schema and internal Swagger/ReDoc.",
)
async def root():
return {
"openapi": "/openapi.json",
"docs": "/docs",
"swagger_ui": "/internal/docs",
"redoc": "/internal/redoc",
"health": "/health",
}
@app.get(
"/docs",
summary="Customer API docs",
description="Customer-facing API reference with generated code examples. The OpenAPI schema remains the source of truth.",
include_in_schema=False,
)
async def customer_docs():
return HTMLResponse(
"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FastCheck API Docs</title>
<style>
html, body { height: 100%; margin: 0; }
</style>
</head>
<body>
<rapi-doc
spec-url="/openapi.json"
render-style="read"
show-header="false"
allow-try="true"
allow-authentication="true"
allow-server-selection="true"
show-components="true"
show-info="true"
show-side-nav="true"
theme="dark"
primary-color="#4f46e5"
></rapi-doc>
<script src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</body>
</html>
""".strip()
)
@app.get("/health")
async def health():
return {"status": "ok"}
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.exception("Unhandled error", extra={"path": request.url.path})
return JSONResponse(status_code=500, content={"status": "error", "message": "Something went wrong!"})
api_v1 = APIRouter(prefix=settings.normalized_api_prefix)
api_v1.include_router(auth.router)
api_v1.include_router(checks.router)
api_v1.include_router(reports.router)
api_v1.include_router(usage.router)
app.include_router(api_v1)