# fablednetmon/core/plugin_manager.py from __future__ import annotations import importlib import logging import sys from pathlib import Path from typing import TYPE_CHECKING import yaml from packaging.version import Version if TYPE_CHECKING: from quart import Quart logger = logging.getLogger(__name__) def load_plugins(app: "Quart") -> None: """Import all enabled plugins and register their blueprints and scheduled tasks. For each plugin listed as enabled in config.yaml under 'plugins': 1. Locate the plugin directory under PLUGIN_DIR 2. Load and validate plugin.yaml (name must match dir, min_app_version check) 3. Merge plugin.yaml config defaults with user overrides in app.config["PLUGINS"] 4. Import the plugin Python package 5. Validate required exports: setup() and get_scheduled_tasks() 6. Call setup(app) 7. Register blueprint at /plugins// if get_blueprint() exists 8. Append get_scheduled_tasks() results to app._task_registry """ import fablednetmon plugin_dir = Path(app.config["PLUGIN_DIR"]) plugins_cfg: dict = app.config["PLUGINS"] # Ensure plugin_dir is on sys.path so plugins are importable by name plugin_dir_str = str(plugin_dir.resolve()) if plugin_dir_str not in sys.path: sys.path.insert(0, plugin_dir_str) for name, cfg in list(plugins_cfg.items()): if not cfg.get("enabled", False): continue plugin_path = plugin_dir / name if not plugin_path.exists(): logger.error("Plugin %r: directory %s not found, skipping", name, plugin_path) continue # Load and validate plugin.yaml yaml_path = plugin_path / "plugin.yaml" if not yaml_path.exists(): logger.error("Plugin %r: plugin.yaml not found, skipping", name) continue try: with yaml_path.open() as f: meta = yaml.safe_load(f) or {} except Exception: logger.exception("Plugin %r: failed to read plugin.yaml, skipping", name) continue if meta.get("name") != name: logger.error( "Plugin %r: plugin.yaml name=%r does not match directory name, skipping", name, meta.get("name"), ) continue min_ver = meta.get("min_app_version") if min_ver: try: if Version(fablednetmon.__version__) < Version(min_ver): logger.error( "Plugin %r: requires fablednetmon>=%s but running %s, skipping", name, min_ver, fablednetmon.__version__, ) continue except Exception: logger.exception("Plugin %r: invalid min_app_version %r, skipping", name, min_ver) continue # Merge plugin.yaml config defaults with user overrides (non-"enabled" keys) defaults = meta.get("config", {}) user_overrides = {k: v for k, v in cfg.items() if k != "enabled"} plugins_cfg[name] = {"enabled": True, **defaults, **user_overrides} # Import the plugin package try: module = importlib.import_module(name) except ImportError: logger.exception("Plugin %r: import failed, skipping", name) continue # Validate required exports if not hasattr(module, "setup"): logger.error("Plugin %r: missing required export 'setup', skipping", name) continue if not hasattr(module, "get_scheduled_tasks"): logger.error( "Plugin %r: missing required export 'get_scheduled_tasks', skipping", name ) continue # Call setup try: module.setup(app) except Exception: logger.exception("Plugin %r: setup() raised, skipping", name) continue # Register blueprint (optional) if hasattr(module, "get_blueprint"): try: bp = module.get_blueprint() app.register_blueprint(bp, url_prefix=f"/plugins/{name}") logger.info("Plugin %r: blueprint registered at /plugins/%s/", name, name) except Exception: logger.exception("Plugin %r: get_blueprint() raised", name) # Register scheduled tasks try: tasks = module.get_scheduled_tasks() app._task_registry.extend(tasks) logger.info("Plugin %r: registered %d scheduled task(s)", name, len(tasks)) except Exception: logger.exception("Plugin %r: get_scheduled_tasks() raised", name) logger.info("Plugin %r loaded (v%s)", name, meta.get("version", "?"))