"""Flask application factory and WSGI entry point.""" from __future__ import annotations import logging from flask import Flask from flask_cors import CORS from app.api.v1 import bp as v1_bp from app.core.config import settings from app.core.install_token import ( ensure_install_token, log_install_token_banner, ) from app.core.logging import configure_logging from app.core.rate_limit import limiter from app.services.bootstrap import ensure_system_groups from app.services.detection_levels import seed_detection_levels from app.services.permissions_seed import seed_all as seed_permissions_and_bindings def _try_bootstrap_at_boot(log: logging.Logger) -> None: """Best-effort: seed system groups + mint an install token if needed. Wrapped in try/except because the DB may not be ready (or schema not migrated yet) at the very first boot — gunicorn must still come up so the operator can run `make migrate` and curl /setup afterwards. """ try: ensure_system_groups() seed_permissions_and_bindings() seed_detection_levels() token = ensure_install_token() if token is not None: log_install_token_banner(token) else: log.info("metamorph.bootstrap.skipped") except Exception as e: log.warning("metamorph.bootstrap.deferred", extra={"error": str(e)}) def create_app() -> Flask: configure_logging(settings.LOG_LEVEL) log = logging.getLogger("metamorph.boot") app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 MB hard cap; per-file limit is 25 MB. CORS( app, origins=settings.cors_origins, supports_credentials=True, max_age=600, ) limiter.init_app(app) app.register_blueprint(v1_bp) log.info( "metamorph.api.boot", extra={ "cors_origins": settings.cors_origins, "log_level": settings.LOG_LEVEL, "evidence_dir": settings.EVIDENCE_DIR, }, ) _try_bootstrap_at_boot(log) return app # WSGI entry point used by gunicorn (`gunicorn app.main:app`). app = create_app()