from __future__ import annotations from fastapi import FastAPI from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from .config import settings from .automation import router as automation_router from .registry import SERVICES, load_service_app, service_enabled from .security import ApiKeyMiddleware, BodyLimitMiddleware, RateLimitMiddleware, RequestContextMiddleware, require_configured_security VERSION = "1.0.0-enterprise" def create_app() -> FastAPI: settings.ensure_runtime_dirs() app = FastAPI( title=settings.app_name, version=VERSION, docs_url="/docs" if settings.allow_public_docs or not settings.api_key_required else None, redoc_url="/redoc" if settings.allow_public_docs or not settings.api_key_required else None, ) app.add_middleware(RequestContextMiddleware) app.add_middleware(BodyLimitMiddleware, settings=settings) app.add_middleware(RateLimitMiddleware, settings=settings) app.add_middleware(ApiKeyMiddleware, settings=settings) app.include_router(automation_router) studio_dir = settings.base_dir / "maester_enterprise" / "studio" if studio_dir.exists(): app.mount("/studio", StaticFiles(directory=studio_dir, html=True), name="studio") mounted: list[dict[str, str]] = [] failed: list[dict[str, str]] = [] if settings.mount_services: for service in SERVICES: if not service_enabled(service): continue try: service_app = load_service_app(settings.services_dir, service) app.mount(service.path, service_app) mounted.append({"key": service.key, "path": service.path, "title": service.title}) except Exception as exc: failed.append({"key": service.key, "error": str(exc)}) @app.get("/api/meta") def metadata() -> dict: return { "name": settings.app_name, "version": VERSION, "environment": settings.environment, "cpu_safe_mode": settings.cpu_safe_mode, "security": { "api_key_required": settings.api_key_required, "docs_public": settings.allow_public_docs, }, "services": mounted, "failed_services": failed, } @app.get("/", response_class=RedirectResponse) def root() -> RedirectResponse: return RedirectResponse(url="/studio/") @app.get("/version") def version() -> dict[str, str]: return {"version": VERSION} @app.get("/health") def health() -> dict: return {"status": "ok", "services_mounted": len(mounted), "services_failed": len(failed)} @app.get("/ready") def ready() -> JSONResponse: try: require_configured_security(settings) except Exception as exc: return JSONResponse({"status": "not_ready", "detail": str(exc)}, status_code=503) status_code = 200 if not failed else 207 return JSONResponse({"status": "ready", "mounted": mounted, "failed": failed}, status_code=status_code) @app.get("/services") def services() -> dict: catalog = [] mounted_by_key = {item["key"]: item for item in mounted} failed_by_key = {item["key"]: item for item in failed} for service in SERVICES: catalog.append( { "key": service.key, "title": service.title, "path": service.path, "description": service.description, "enabled": service_enabled(service), "mounted": service.key in mounted_by_key, "error": failed_by_key.get(service.key, {}).get("error"), } ) return {"services": catalog} return app app = create_app()