# SPDX-FileCopyrightText: 2026 Dennis Fink # # SPDX-License-Identifier: BSD-3-Clause import logging import logging.handlers import os from pathlib import Path import click import coloredlogs import sqlalchemy as sa from flask import Flask from flask_bootstrap import Bootstrap5 from flask_migrate import Migrate, upgrade from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import DeclarativeBase from webmentions_ssg.config import DefaultConfig, DevelopmentConfig, ProductionConfig from .tasks import Huey APP_NAME = "webmentions-ssg" VERSION = "0.0.1" class Base(DeclarativeBase): pass BOOTSTRAP = Bootstrap5() DATABASE = SQLAlchemy(model_class=Base) MIGRATE = Migrate() HUEY = Huey() def create_app(config_class: type[DefaultConfig] | None = None) -> Flask: """Creates and configures the Flask application.""" development = os.environ.get( "WEBMENTIONS_SSG_DEV", "", ).lower() in { "1", "true", "yes", "on", } if config_class is None: config_class = DevelopmentConfig if development else ProductionConfig app = Flask( __name__, instance_path=str(Path("testing").absolute()) if development else None, ) app.config.from_object(config_class) if not app.testing: app.config.from_pyfile(app.config["CONFIG_FILE"], silent=True) app.config.from_prefixed_env(prefix="FLASK") app.config.from_prefixed_env(prefix="WEBMENTIONS_SSG") default_logging_formatter = logging.Formatter(app.config["LOG_FORMAT"]) log_file_handler = logging.handlers.RotatingFileHandler( app.config["LOG_FILE"], maxBytes=app.config["LOGFILE_MAX_BYTES"], backupCount=app.config["LOGFILE_BACKUP_COUNT"], ) log_file_handler.setLevel(app.config["LOG_LEVEL"]) log_file_handler.setFormatter(default_logging_formatter) logging.basicConfig(level=logging.DEBUG, handlers=[logging.NullHandler()]) app.logger.handlers.clear() app.logger.setLevel(app.config["LOG_LEVEL"]) app.logger.addHandler(log_file_handler) werkzeug_logger = logging.getLogger("werkzeug") werkzeug_logger.setLevel(app.config["LOG_LEVEL"]) werkzeug_logger.addHandler(log_file_handler) coloredlogs.install( level=app.config["LOG_LEVEL"], logger=app.logger, fmt=app.config["LOG_FORMAT"], ) coloredlogs.install( level=app.config["LOG_LEVEL"], logger=werkzeug_logger, fmt=app.config["LOG_FORMAT"], ) DATABASE.init_app(app) MIGRATE.init_app(app, db=DATABASE) from .auth import AUTH AUTH.init_app(app) BOOTSTRAP.init_app(app) HUEY.init_app(app) @app.context_processor def inject_version(): return dict(VERSION=VERSION) from .views import root_page app.register_blueprint(root_page) @app.cli.command("create-db") def create_db() -> None: """Create or update the database to the latest migration.""" upgrade() @app.cli.command("create-user") @click.argument("username") @click.password_option( confirmation_prompt=True, ) def create_user( username: str, password: str, ) -> None: """Create a user.""" from .models import User if ( DATABASE.session.scalar(sa.select(User).where(User.username == username)) is not None ): raise click.ClickException(f"User {username!r} already exists.") user = User( username=username, ) user.set_password(password) DATABASE.session.add(user) DATABASE.session.commit() click.echo(f"Created user {username!r}.") return app