"""Der Plugin-Vertrag. Ein Plugin ist eine Klasse, die von BasePlugin erbt, und ein Modulattribut `plugin` mit einer Instanz davon. Der Kern ruft beim Start `on_load(kontext)` auf, beim Herunterfahren `on_unload()`. Fachlogik gehört ausschließlich in Plugins — der Kern kennt keine Plugin-Inhalte. """ from __future__ import annotations import sys from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, ClassVar from fastapi import APIRouter from sqlalchemy import Connection if TYPE_CHECKING: from fastapi.templating import Jinja2Templates from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from redaktionskern.config import Settings from redaktionskern.plugin_loader import PluginRegistry @dataclass(frozen=True) class NavEntry: """Ein Eintrag in der Hauptnavigation.""" label: str url: str @dataclass(frozen=True) class Migration: """Eine Schema-Migration des Plugins.""" version: str up: Callable[[Connection], None] class PluginContext: """Alles, was der Kern einem Plugin beim Laden übergibt.""" def __init__( self, engine: "Engine", session_factory: "sessionmaker", settings: "Settings", templates: "Jinja2Templates", registry: "PluginRegistry | None" = None, ) -> None: self.engine = engine self.session_factory = session_factory self.settings = settings self.templates = templates #: Registry aller geladenen Plugins — damit auch Hintergrund-Jobs #: eines Plugins die öffentliche API anderer Plugins erreichen #: (in Anfragen geht das üblicherweise über request.app.state.registry). self.registry = registry class BasePlugin: """Basis-Klasse für alle Plugins (der Plugin-Vertrag). Pflicht: die Klassenattribute `name`, `title`, `description`. Optional überschreibbar: migrations(), navigation(), templates_dir(), on_load(), on_unload(). """ name: ClassVar[str] title: ClassVar[str] = "" description: ClassVar[str] = "" version: ClassVar[str] = "0.1.0" def __init__(self) -> None: self.router = APIRouter() self.context: PluginContext | None = None self.loaded = False def migrations(self) -> list[Migration]: """Eigene Schema-Migrationen des Plugins (leer = keine).""" return [] def navigation(self) -> list[NavEntry]: """Einträge für die Hauptnavigation (leer = keiner).""" return [] def templates_dir(self) -> Path | None: """Template-Ordner des Plugins (/templates), falls vorhanden.""" modul = sys.modules.get(type(self).__module__) datei = getattr(modul, "__file__", None) if modul else None if datei is None: return None verzeichnis = Path(datei).parent / "templates" return verzeichnis if verzeichnis.is_dir() else None def on_load(self, context: PluginContext) -> None: """Lifecycle-Hook: wird beim Start des Kerns aufgerufen.""" self.context = context self.loaded = True def on_unload(self) -> None: """Lifecycle-Hook: wird beim Herunterfahren aufgerufen.""" self.loaded = False