ebk 0.1.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ebk might be problematic. Click here for more details.

@@ -0,0 +1,500 @@
1
+ """
2
+ Plugin registry and discovery system for EBK.
3
+
4
+ This module handles plugin registration, discovery, and management.
5
+ """
6
+
7
+ import importlib
8
+ import importlib.metadata
9
+ import inspect
10
+ import logging
11
+ import pkgutil
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Type, Any, Callable
14
+ from concurrent.futures import ThreadPoolExecutor, as_completed
15
+
16
+ from .base import Plugin
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class PluginRegistry:
22
+ """Central registry for all EBK plugins."""
23
+
24
+ def __init__(self):
25
+ self._plugins: Dict[str, List[Plugin]] = {}
26
+ self._plugin_classes: Dict[str, Type[Plugin]] = {}
27
+ self._plugin_instances: Dict[str, Plugin] = {}
28
+ self._config: Dict[str, Dict[str, Any]] = {}
29
+ self._enabled: Dict[str, bool] = {}
30
+
31
+ def discover_plugins(self,
32
+ search_paths: Optional[List[Path]] = None,
33
+ entry_point_group: str = "ebk.plugins") -> None:
34
+ """
35
+ Discover plugins from various sources.
36
+
37
+ Args:
38
+ search_paths: Additional paths to search for plugins
39
+ entry_point_group: Entry point group name for installed plugins
40
+ """
41
+ # 1. Discover from entry points (installed packages)
42
+ self._discover_entry_points(entry_point_group)
43
+
44
+ # 2. Discover from local plugins directory
45
+ self._discover_local_plugins()
46
+
47
+ # 3. Discover from additional search paths
48
+ if search_paths:
49
+ for path in search_paths:
50
+ self._discover_path_plugins(path)
51
+
52
+ # 4. Discover from environment variable
53
+ self._discover_env_plugins()
54
+
55
+ logger.info(f"Discovered {len(self._plugin_instances)} plugins")
56
+
57
+ def _discover_entry_points(self, group: str) -> None:
58
+ """
59
+ Discover plugins via setuptools entry points.
60
+
61
+ Args:
62
+ group: Entry point group name
63
+ """
64
+ try:
65
+ # Get entry points for the group
66
+ if hasattr(importlib.metadata, 'entry_points'):
67
+ # Python 3.10+
68
+ eps = importlib.metadata.entry_points()
69
+ if hasattr(eps, 'select'):
70
+ # Python 3.10+
71
+ entry_points = eps.select(group=group)
72
+ else:
73
+ # Python 3.9
74
+ entry_points = eps.get(group, [])
75
+ else:
76
+ # Fallback for older versions
77
+ entry_points = []
78
+
79
+ for ep in entry_points:
80
+ try:
81
+ plugin_class = ep.load()
82
+ if self._is_valid_plugin_class(plugin_class):
83
+ self._register_class(plugin_class)
84
+ logger.info(f"Loaded plugin from entry point: {ep.name}")
85
+ except Exception as e:
86
+ logger.error(f"Failed to load plugin {ep.name}: {e}")
87
+
88
+ except Exception as e:
89
+ logger.warning(f"Could not discover entry point plugins: {e}")
90
+
91
+ def _discover_local_plugins(self) -> None:
92
+ """Discover plugins in the local plugins directory."""
93
+ plugins_dir = Path(__file__).parent
94
+ self._discover_path_plugins(plugins_dir)
95
+
96
+ def _discover_path_plugins(self, path: Path) -> None:
97
+ """
98
+ Discover plugins in a specific directory.
99
+
100
+ Args:
101
+ path: Directory to search for plugins
102
+ """
103
+ if not path.exists() or not path.is_dir():
104
+ return
105
+
106
+ # Skip __pycache__ and other special directories
107
+ if path.name.startswith('__'):
108
+ return
109
+
110
+ # Look for Python modules
111
+ for module_info in pkgutil.iter_modules([str(path)]):
112
+ if module_info.name.startswith('_'):
113
+ continue
114
+
115
+ try:
116
+ # Import the module
117
+ if path.parent in Path(__file__).parents:
118
+ # It's within the ebk package
119
+ module_path = f"ebk.plugins.{module_info.name}"
120
+ else:
121
+ # It's an external path
122
+ module_path = module_info.name
123
+
124
+ module = importlib.import_module(module_path)
125
+
126
+ # Find plugin classes in the module
127
+ for name, obj in inspect.getmembers(module):
128
+ if self._is_valid_plugin_class(obj):
129
+ self._register_class(obj)
130
+ logger.info(f"Loaded plugin class: {name} from {module_path}")
131
+
132
+ except Exception as e:
133
+ logger.error(f"Failed to load plugin module {module_info.name}: {e}")
134
+
135
+ def _discover_env_plugins(self) -> None:
136
+ """Discover plugins from environment variable."""
137
+ import os
138
+ plugin_paths = os.environ.get('EBK_PLUGIN_PATH', '')
139
+
140
+ if plugin_paths:
141
+ for path_str in plugin_paths.split(':'):
142
+ path = Path(path_str).expanduser()
143
+ if path.exists():
144
+ self._discover_path_plugins(path)
145
+
146
+ def _is_valid_plugin_class(self, obj: Any) -> bool:
147
+ """
148
+ Check if an object is a valid plugin class.
149
+
150
+ Args:
151
+ obj: Object to check
152
+
153
+ Returns:
154
+ True if it's a valid plugin class
155
+ """
156
+ return (
157
+ inspect.isclass(obj) and
158
+ issubclass(obj, Plugin) and
159
+ obj is not Plugin and
160
+ not inspect.isabstract(obj) and
161
+ obj.__module__ != 'ebk.plugins.base' # Skip base classes
162
+ )
163
+
164
+ def _register_class(self, plugin_class: Type[Plugin]) -> None:
165
+ """
166
+ Register a plugin class.
167
+
168
+ Args:
169
+ plugin_class: Plugin class to register
170
+ """
171
+ try:
172
+ # Create an instance to get the name
173
+ instance = plugin_class()
174
+ name = instance.name
175
+
176
+ if name in self._plugin_classes:
177
+ logger.warning(f"Plugin {name} already registered, skipping")
178
+ return
179
+
180
+ self._plugin_classes[name] = plugin_class
181
+
182
+ # Determine plugin type from base class
183
+ plugin_type = self._get_plugin_type(plugin_class)
184
+ if plugin_type:
185
+ if plugin_type not in self._plugins:
186
+ self._plugins[plugin_type] = []
187
+
188
+ # Store the class for lazy instantiation
189
+ self._plugins[plugin_type].append(instance)
190
+ self._plugin_instances[name] = instance
191
+
192
+ logger.debug(f"Registered plugin: {name} (type: {plugin_type})")
193
+
194
+ except Exception as e:
195
+ logger.error(f"Failed to register plugin class {plugin_class.__name__}: {e}")
196
+
197
+ def _get_plugin_type(self, plugin_class: Type[Plugin]) -> Optional[str]:
198
+ """
199
+ Determine the plugin type from its base class.
200
+
201
+ Args:
202
+ plugin_class: Plugin class
203
+
204
+ Returns:
205
+ Plugin type name or None
206
+ """
207
+ from . import base
208
+
209
+ # Map base classes to type names
210
+ type_map = {
211
+ base.MetadataExtractor: 'metadata_extractor',
212
+ base.TagSuggester: 'tag_suggester',
213
+ base.ContentAnalyzer: 'content_analyzer',
214
+ base.SimilarityFinder: 'similarity_finder',
215
+ base.Deduplicator: 'deduplicator',
216
+ base.Validator: 'validator',
217
+ base.Exporter: 'exporter'
218
+ }
219
+
220
+ for base_class, type_name in type_map.items():
221
+ if issubclass(plugin_class, base_class):
222
+ return type_name
223
+
224
+ return None
225
+
226
+ def register(self, plugin: Plugin) -> None:
227
+ """
228
+ Register a plugin instance.
229
+
230
+ Args:
231
+ plugin: Plugin instance to register
232
+ """
233
+ name = plugin.name
234
+
235
+ if name in self._plugin_instances:
236
+ logger.warning(f"Plugin {name} already registered, replacing")
237
+
238
+ self._plugin_instances[name] = plugin
239
+
240
+ # Determine plugin type
241
+ plugin_type = self._get_plugin_type(type(plugin))
242
+ if plugin_type:
243
+ if plugin_type not in self._plugins:
244
+ self._plugins[plugin_type] = []
245
+
246
+ # Remove old instance if exists
247
+ self._plugins[plugin_type] = [
248
+ p for p in self._plugins[plugin_type] if p.name != name
249
+ ]
250
+ self._plugins[plugin_type].append(plugin)
251
+
252
+ logger.info(f"Registered plugin instance: {name}")
253
+
254
+ def unregister(self, name: str) -> bool:
255
+ """
256
+ Unregister a plugin.
257
+
258
+ Args:
259
+ name: Plugin name
260
+
261
+ Returns:
262
+ True if plugin was unregistered
263
+ """
264
+ if name not in self._plugin_instances:
265
+ return False
266
+
267
+ plugin = self._plugin_instances[name]
268
+ del self._plugin_instances[name]
269
+
270
+ # Remove from type list
271
+ for plugin_list in self._plugins.values():
272
+ plugin_list[:] = [p for p in plugin_list if p.name != name]
273
+
274
+ # Cleanup
275
+ try:
276
+ plugin.cleanup()
277
+ except Exception as e:
278
+ logger.error(f"Error during plugin cleanup: {e}")
279
+
280
+ logger.info(f"Unregistered plugin: {name}")
281
+ return True
282
+
283
+ def get_plugins(self, plugin_type: str) -> List[Plugin]:
284
+ """
285
+ Get all plugins of a specific type.
286
+
287
+ Args:
288
+ plugin_type: Type of plugins to get
289
+
290
+ Returns:
291
+ List of plugin instances
292
+ """
293
+ plugins = self._plugins.get(plugin_type, [])
294
+
295
+ # Filter by enabled status
296
+ return [p for p in plugins if self._enabled.get(p.name, True)]
297
+
298
+ def get_plugin(self, name: str) -> Optional[Plugin]:
299
+ """
300
+ Get a specific plugin by name.
301
+
302
+ Args:
303
+ name: Plugin name
304
+
305
+ Returns:
306
+ Plugin instance or None
307
+ """
308
+ plugin = self._plugin_instances.get(name)
309
+
310
+ # Check if enabled
311
+ if plugin and not self._enabled.get(name, True):
312
+ return None
313
+
314
+ return plugin
315
+
316
+ def configure_plugin(self, name: str, config: Dict[str, Any]) -> bool:
317
+ """
318
+ Configure a plugin.
319
+
320
+ Args:
321
+ name: Plugin name
322
+ config: Configuration dictionary
323
+
324
+ Returns:
325
+ True if configuration was successful
326
+ """
327
+ plugin = self._plugin_instances.get(name)
328
+ if not plugin:
329
+ logger.error(f"Plugin {name} not found")
330
+ return False
331
+
332
+ try:
333
+ self._config[name] = config
334
+ plugin.initialize(config)
335
+
336
+ if not plugin.validate_config():
337
+ logger.error(f"Invalid configuration for plugin {name}")
338
+ return False
339
+
340
+ logger.info(f"Configured plugin: {name}")
341
+ return True
342
+
343
+ except Exception as e:
344
+ logger.error(f"Failed to configure plugin {name}: {e}")
345
+ return False
346
+
347
+ def enable_plugin(self, name: str) -> bool:
348
+ """
349
+ Enable a plugin.
350
+
351
+ Args:
352
+ name: Plugin name
353
+
354
+ Returns:
355
+ True if plugin was enabled
356
+ """
357
+ if name not in self._plugin_instances:
358
+ logger.error(f"Plugin {name} not found")
359
+ return False
360
+
361
+ self._enabled[name] = True
362
+ logger.info(f"Enabled plugin: {name}")
363
+ return True
364
+
365
+ def disable_plugin(self, name: str) -> bool:
366
+ """
367
+ Disable a plugin.
368
+
369
+ Args:
370
+ name: Plugin name
371
+
372
+ Returns:
373
+ True if plugin was disabled
374
+ """
375
+ if name not in self._plugin_instances:
376
+ logger.error(f"Plugin {name} not found")
377
+ return False
378
+
379
+ self._enabled[name] = False
380
+ logger.info(f"Disabled plugin: {name}")
381
+ return True
382
+
383
+ def list_plugins(self) -> Dict[str, List[str]]:
384
+ """
385
+ List all registered plugins by type.
386
+
387
+ Returns:
388
+ Dictionary mapping plugin types to plugin names
389
+ """
390
+ result = {}
391
+ for plugin_type, plugins in self._plugins.items():
392
+ result[plugin_type] = [p.name for p in plugins]
393
+ return result
394
+
395
+ def get_plugin_info(self, name: str) -> Optional[Dict[str, Any]]:
396
+ """
397
+ Get information about a plugin.
398
+
399
+ Args:
400
+ name: Plugin name
401
+
402
+ Returns:
403
+ Plugin information dictionary
404
+ """
405
+ plugin = self._plugin_instances.get(name)
406
+ if not plugin:
407
+ return None
408
+
409
+ return {
410
+ 'name': plugin.name,
411
+ 'version': plugin.version,
412
+ 'description': plugin.description,
413
+ 'author': plugin.author,
414
+ 'type': self._get_plugin_type(type(plugin)),
415
+ 'enabled': self._enabled.get(name, True),
416
+ 'configured': name in self._config,
417
+ 'requires': plugin.requires
418
+ }
419
+
420
+ def cleanup(self) -> None:
421
+ """Cleanup all plugins."""
422
+ for plugin in self._plugin_instances.values():
423
+ try:
424
+ plugin.cleanup()
425
+ except Exception as e:
426
+ logger.error(f"Error cleaning up plugin {plugin.name}: {e}")
427
+
428
+ self._plugins.clear()
429
+ self._plugin_instances.clear()
430
+ self._plugin_classes.clear()
431
+ self._config.clear()
432
+ self._enabled.clear()
433
+
434
+
435
+ # Global plugin registry instance
436
+ plugin_registry = PluginRegistry()
437
+
438
+
439
+ def register_plugin(plugin_or_class):
440
+ """
441
+ Decorator or function to register a plugin.
442
+
443
+ Can be used as:
444
+ - @register_plugin on a class
445
+ - register_plugin(plugin_instance)
446
+
447
+ Args:
448
+ plugin_or_class: Plugin class or instance
449
+
450
+ Returns:
451
+ The plugin class (for decorator usage)
452
+ """
453
+ if inspect.isclass(plugin_or_class):
454
+ # Used as decorator on a class
455
+ plugin_registry._register_class(plugin_or_class)
456
+ return plugin_or_class
457
+ else:
458
+ # Used as function with instance
459
+ plugin_registry.register(plugin_or_class)
460
+ return plugin_or_class
461
+
462
+
463
+ def get_plugins(plugin_type: str) -> List[Plugin]:
464
+ """
465
+ Get all plugins of a specific type.
466
+
467
+ Args:
468
+ plugin_type: Type of plugins to get
469
+
470
+ Returns:
471
+ List of plugin instances
472
+ """
473
+ return plugin_registry.get_plugins(plugin_type)
474
+
475
+
476
+ def get_plugin(name: str) -> Optional[Plugin]:
477
+ """
478
+ Get a specific plugin by name.
479
+
480
+ Args:
481
+ name: Plugin name
482
+
483
+ Returns:
484
+ Plugin instance or None
485
+ """
486
+ return plugin_registry.get_plugin(name)
487
+
488
+
489
+ def configure_plugin(name: str, config: Dict[str, Any]) -> bool:
490
+ """
491
+ Configure a plugin.
492
+
493
+ Args:
494
+ name: Plugin name
495
+ config: Configuration dictionary
496
+
497
+ Returns:
498
+ True if configuration was successful
499
+ """
500
+ return plugin_registry.configure_plugin(name, config)