diff options
| author | Dennis Fink | 2026-08-09 14:15:29 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-09 14:15:29 +0200 |
| commit | e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9 (patch) | |
| tree | 234f63980aba8fe3c8d03e7bc8959598bce7e790 /webmentions_ssg/__init__.py | |
| parent | f53c1136184d2afabfb1e5974e527229aa71939a (diff) | |
| download | webmentions-ssg-e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9.tar.gz webmentions-ssg-e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9.zip | |
Implement received Webmention handling
Add the Flask application setup, database models and migrations,
authentication, and configuration for development and testing.
Implement asynchronous Webmention verification with Huey, including HTML
and plain-text source validation, retries, status tracking, and size
limits.
Add status, login, and paginated received-Webmention views together with
comprehensive tests for forms, views, and receiver tasks.
Diffstat (limited to 'webmentions_ssg/__init__.py')
| -rw-r--r-- | webmentions_ssg/__init__.py | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/webmentions_ssg/__init__.py b/webmentions_ssg/__init__.py index 7f93695..f66e478 100644 --- a/webmentions_ssg/__init__.py +++ b/webmentions_ssg/__init__.py @@ -2,4 +2,145 @@ # # 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 |
