Coverage for src/paperap/plugins/manager.py: 86%
80 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-18 12:26 -0400
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-18 12:26 -0400
1"""
2----------------------------------------------------------------------------
4 METADATA:
6 File: manager.py
7 Project: paperap
8 Created: 2025-03-04
9 Version: 0.0.7
10 Author: Jess Mann
11 Email: jess@jmann.me
12 Copyright (c) 2025 Jess Mann
14----------------------------------------------------------------------------
16 LAST MODIFIED:
18 2025-03-04 By Jess Mann
20"""
22from __future__ import annotations
24import importlib
25import inspect
26import logging
27import pkgutil
28from pathlib import Path
29from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self, Set, TypedDict
31import pydantic
33from paperap.client import PaperlessClient
34from paperap.plugins.base import Plugin
36logger = logging.getLogger(__name__)
39class PluginConfig(TypedDict):
40 """
41 Configuration settings for a plugin.
42 """
44 enabled_plugins: list[str]
45 settings: dict[str, Any]
48class PluginManager(pydantic.BaseModel):
49 """Manages the discovery, configuration and initialization of plugins."""
51 plugins: dict[str, type[Plugin]] = {}
52 instances: dict[str, Plugin] = {}
53 config: PluginConfig = {
54 "enabled_plugins": [],
55 "settings": {},
56 }
57 client: PaperlessClient
59 model_config = pydantic.ConfigDict(
60 arbitrary_types_allowed=True,
61 validate_default=True,
62 validate_assignment=True,
63 )
65 @property
66 def enabled_plugins(self) -> list[str]:
67 """
68 Get the list of enabled plugins.
70 Returns:
71 List of enabled plugin names
73 """
74 # TODO: There's a bug here... disabling every plugin will then enable every plugin
75 if enabled := self.config.get("enabled_plugins"):
76 return enabled
78 return list(self.plugins.keys())
80 def discover_plugins(self, package_name: str = "paperap.plugins") -> None:
81 """
82 Discover available plugins in the specified package.
84 Args:
85 package_name: Dotted path to the package containing plugins.
87 """
88 try:
89 package = importlib.import_module(package_name)
90 except ImportError:
91 logger.warning("Could not import plugin package: %s", package_name)
92 return
94 # Find all modules in the package
95 for _, module_name, is_pkg in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
96 if is_pkg:
97 # Recursively discover plugins in subpackages
98 self.discover_plugins(module_name)
99 continue
101 try:
102 module = importlib.import_module(module_name)
104 # Find plugin classes in the module
105 for _name, obj in inspect.getmembers(module, inspect.isclass):
106 if issubclass(obj, Plugin) and obj is not Plugin and obj.__module__ == module_name:
107 plugin_name = obj.__name__
108 self.plugins[plugin_name] = obj
109 logger.debug("Discovered plugin: %s", plugin_name)
110 except Exception as e:
111 logger.error("Error loading plugin module %s: %s", module_name, e)
113 def configure(self, config: PluginConfig | None = None, **kwargs) -> None:
114 """
115 Configure the plugin manager with plugin-specific configurations.
117 Args:
118 config: dictionary mapping plugin names to their configurations.
120 """
121 if config:
122 self.config = config
124 if kwargs:
125 if enabled_plugins := kwargs.pop("enabled_plugins", None):
126 self.config["enabled_plugins"] = enabled_plugins
127 if settings := kwargs.pop("settings", None):
128 self.config["settings"] = settings
129 if kwargs:
130 logger.warning("Unexpected configuration keys: %s", kwargs.keys())
132 def get_plugin_config(self, plugin_name: str) -> dict[str, Any]:
133 """Get the configuration for a specific plugin."""
134 return self.config["settings"].get(plugin_name, {})
136 def initialize_plugin(self, plugin_name: str) -> Plugin | None:
137 """
138 Initialize a specific plugin.
140 Args:
141 plugin_name: Name of the plugin to initialize.
143 Returns:
144 The initialized plugin instance or None if initialization failed.
146 """
147 if plugin_name in self.instances:
148 return self.instances[plugin_name]
150 if plugin_name not in self.plugins:
151 logger.warning("Plugin not found: %s", plugin_name)
152 return None
154 plugin_class = self.plugins[plugin_name]
155 plugin_config = self.get_plugin_config(plugin_name)
157 try:
158 # Initialize the plugin with plugin-specific config
159 plugin_instance = plugin_class(manager=self, **plugin_config)
160 self.instances[plugin_name] = plugin_instance
161 logger.info("Initialized plugin: %s", plugin_name)
162 return plugin_instance
163 except Exception as e:
164 # Do not allow plugins to interrupt the normal program flow.
165 logger.error("Failed to initialize plugin %s: %s", plugin_name, e)
166 return None
168 def initialize_all_plugins(self) -> dict[str, Plugin]:
169 """
170 Initialize all discovered plugins.
172 Returns:
173 Dictionary mapping plugin names to their initialized instances.
175 """
176 # Get enabled plugins from config
177 enabled_plugins = self.enabled_plugins
179 # Initialize plugins
180 initialized = {}
181 for plugin_name in enabled_plugins:
182 instance = self.initialize_plugin(plugin_name)
183 if instance:
184 initialized[plugin_name] = instance
186 return initialized