"""Application factory (Chapter 02 / Chapter 04).""" from __future__ import annotations from flask import Flask, jsonify, redirect, request, url_for from flask_login import current_user from sqlalchemy import text from app.budget import validate_budget_config from app.config import get_config from app.extensions import cache, csrf, db, limiter, login_manager, migrate from app.logging_setup import configure_logging from app.utils.assets import register_asset_helper def create_app(config_name: str | None = None) -> Flask: """Build and configure the Flask application instance.""" app = Flask(__name__) app.config.from_object(get_config(config_name)) validate_budget_config(app) # Chapter 03: fail loudly on out-of-budget config configure_logging(app) # Chapter 02 factor 11 / Chapter 12 if not app.config.get("SECRET_KEY") and not app.testing: raise RuntimeError("SECRET_KEY must be set via environment variable") db.init_app(app) cache.init_app(app) csrf.init_app(app) login_manager.init_app(app) migrate.init_app(app, db) limiter.init_app(app) # Chapter 12: rate limiting register_blueprints(app) register_cli(app) register_asset_helper(app) @login_manager.unauthorized_handler def handle_unauthorized(): """Chapter 11 envelope compliance: JSON 401 for /api/... paths instead of Flask-Login's default redirect (which would hand back login HTML to a fetch()/HTMX JSON caller). Full-page routes still redirect to the login form as normal. """ if request.path.startswith("/api/"): return jsonify(error={"code": "unauthorized", "message": "Authentication required."}), 401 return redirect(url_for("auth.login", next=request.path)) @app.get("/healthz") def healthz(): """Liveness/readiness check (Chapter 12) — DB reachable, no long-running work.""" db.session.execute(text("SELECT 1")) return {"data": {"status": "ok"}, "meta": {}} @app.get("/") def index(): """Root URL: authenticated -> Overview, otherwise -> the login page.""" if current_user.is_authenticated: return redirect(url_for("overview.overview")) return redirect(url_for("auth.login")) return app def register_blueprints(app: Flask) -> None: """Register one blueprint per dashboard section + support area (Ch. 04).""" from app.blueprints.api import bp as api_bp from app.blueprints.auth import bp as auth_bp from app.blueprints.overview import bp as overview_bp from app.blueprints.security import bp as security_bp from app.blueprints.seo import bp as seo_bp from app.blueprints.uploads import bp as uploads_bp app.register_blueprint(overview_bp) app.register_blueprint(seo_bp) app.register_blueprint(security_bp) app.register_blueprint(uploads_bp) app.register_blueprint(api_bp) app.register_blueprint(auth_bp) def register_cli(app: Flask) -> None: """Attach `flask ` admin processes (factor 12).""" from app.cli import register_commands register_commands(app)