import os from collections.abc import Mapping from pathlib import Path from typing import Any from flask import Flask from config import CONFIGS from .extensions import csrf, db, login_manager, migrate from .core.security import configure_security_headers def create_app( config_name: str | None = None, config_overrides: Mapping[str, Any] | None = None, ) -> Flask: """Create and configure an isolated Steady application instance.""" app = Flask(__name__, instance_relative_config=True) selected_config = config_name or os.environ.get("STEADY_ENV", "development") try: app.config.from_object(CONFIGS[selected_config]) except KeyError as exc: valid_names = ", ".join(sorted(CONFIGS)) raise ValueError( f"Unknown configuration {selected_config!r}; choose {valid_names}." ) from exc if config_overrides: app.config.from_mapping(config_overrides) Path(app.instance_path).mkdir(parents=True, exist_ok=True) _validate_config(app) _initialize_extensions(app) _register_blueprints(app) configure_security_headers(app) return app def _validate_config(app: Flask) -> None: if not app.config.get("SECRET_KEY"): raise RuntimeError("SECRET_KEY must be set outside development and testing.") def _initialize_extensions(app: Flask) -> None: db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) csrf.init_app(app) login_manager.login_view = "auth.login" login_manager.login_message_category = "info" def _register_blueprints(app: Flask) -> None: # Imports stay local so feature modules do not create circular imports. from .auth import bp as auth_blueprint from .core import bp as core_blueprint from .settings import bp as settings_blueprint from .tasks import bp as tasks_blueprint app.register_blueprint(core_blueprint) app.register_blueprint(auth_blueprint, url_prefix="/auth") app.register_blueprint(tasks_blueprint, url_prefix="/tasks") app.register_blueprint(settings_blueprint, url_prefix="/settings") if app.config["FEATURE_ADMIN"]: from .admin import bp as admin_blueprint app.register_blueprint(admin_blueprint, url_prefix="/admin")