aboutsummaryrefslogtreecommitdiff
path: root/webmentions_ssg/__init__.py
diff options
context:
space:
mode:
authorDennis Fink2026-08-09 14:15:29 +0200
committerDennis Fink2026-08-09 14:22:00 +0200
commit67eff0a854da010cb6ecd119d84238ec3e119272 (patch)
treec4a85592b957bb01fa62f79329faa6ba0823d9b3 /webmentions_ssg/__init__.py
parentf53c1136184d2afabfb1e5974e527229aa71939a (diff)
downloadwebmentions-ssg-67eff0a854da010cb6ecd119d84238ec3e119272.tar.gz
webmentions-ssg-67eff0a854da010cb6ecd119d84238ec3e119272.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__.py141
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