Coverage for src/paperap/plugins/manager.py: 86%

80 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-20 13:17 -0400

1""" 

2---------------------------------------------------------------------------- 

3 

4 METADATA: 

5 

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 

13 

14---------------------------------------------------------------------------- 

15 

16 LAST MODIFIED: 

17 

18 2025-03-04 By Jess Mann 

19 

20""" 

21 

22from __future__ import annotations 

23 

24import importlib 

25import inspect 

26import logging 

27import pkgutil 

28from pathlib import Path 

29from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self, Set, TypedDict 

30 

31import pydantic 

32 

33from paperap.client import PaperlessClient 

34from paperap.plugins.base import Plugin 

35 

36logger = logging.getLogger(__name__) 

37 

38 

39class PluginConfig(TypedDict): 

40 """ 

41 Configuration settings for a plugin. 

42 """ 

43 

44 enabled_plugins: list[str] 

45 settings: dict[str, Any] 

46 

47 

48class PluginManager(pydantic.BaseModel): 

49 """Manages the discovery, configuration and initialization of plugins.""" 

50 

51 plugins: dict[str, type[Plugin]] = {} 

52 instances: dict[str, Plugin] = {} 

53 config: PluginConfig = { 

54 "enabled_plugins": [], 

55 "settings": {}, 

56 } 

57 client: PaperlessClient 

58 

59 model_config = pydantic.ConfigDict( 

60 arbitrary_types_allowed=True, 

61 validate_default=True, 

62 validate_assignment=True, 

63 ) 

64 

65 @property 

66 def enabled_plugins(self) -> list[str]: 

67 """ 

68 Get the list of enabled plugins. 

69 

70 Returns: 

71 List of enabled plugin names 

72 

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 

77 

78 return list(self.plugins.keys()) 

79 

80 def discover_plugins(self, package_name: str = "paperap.plugins") -> None: 

81 """ 

82 Discover available plugins in the specified package. 

83 

84 Args: 

85 package_name: Dotted path to the package containing plugins. 

86 

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 

93 

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 

100 

101 try: 

102 module = importlib.import_module(module_name) 

103 

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) 

112 

113 def configure(self, config: PluginConfig | None = None, **kwargs) -> None: 

114 """ 

115 Configure the plugin manager with plugin-specific configurations. 

116 

117 Args: 

118 config: dictionary mapping plugin names to their configurations. 

119 

120 """ 

121 if config: 

122 self.config = config 

123 

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()) 

131 

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, {}) 

135 

136 def initialize_plugin(self, plugin_name: str) -> Plugin | None: 

137 """ 

138 Initialize a specific plugin. 

139 

140 Args: 

141 plugin_name: Name of the plugin to initialize. 

142 

143 Returns: 

144 The initialized plugin instance or None if initialization failed. 

145 

146 """ 

147 if plugin_name in self.instances: 

148 return self.instances[plugin_name] 

149 

150 if plugin_name not in self.plugins: 

151 logger.warning("Plugin not found: %s", plugin_name) 

152 return None 

153 

154 plugin_class = self.plugins[plugin_name] 

155 plugin_config = self.get_plugin_config(plugin_name) 

156 

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 

167 

168 def initialize_all_plugins(self) -> dict[str, Plugin]: 

169 """ 

170 Initialize all discovered plugins. 

171 

172 Returns: 

173 Dictionary mapping plugin names to their initialized instances. 

174 

175 """ 

176 # Get enabled plugins from config 

177 enabled_plugins = self.enabled_plugins 

178 

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 

185 

186 return initialized