ebk 0.1.0__py3-none-any.whl → 0.3.2__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.
- ebk/__init__.py +35 -0
- ebk/ai/__init__.py +23 -0
- ebk/ai/knowledge_graph.py +443 -0
- ebk/ai/llm_providers/__init__.py +21 -0
- ebk/ai/llm_providers/base.py +230 -0
- ebk/ai/llm_providers/ollama.py +362 -0
- ebk/ai/metadata_enrichment.py +396 -0
- ebk/ai/question_generator.py +328 -0
- ebk/ai/reading_companion.py +224 -0
- ebk/ai/semantic_search.py +434 -0
- ebk/ai/text_extractor.py +394 -0
- ebk/cli.py +2828 -680
- ebk/config.py +260 -22
- ebk/db/__init__.py +37 -0
- ebk/db/migrations.py +180 -0
- ebk/db/models.py +526 -0
- ebk/db/session.py +144 -0
- ebk/decorators.py +132 -0
- ebk/exports/base_exporter.py +218 -0
- ebk/exports/html_library.py +1390 -0
- ebk/exports/html_utils.py +117 -0
- ebk/exports/hugo.py +7 -3
- ebk/exports/jinja_export.py +287 -0
- ebk/exports/multi_facet_export.py +164 -0
- ebk/exports/symlink_dag.py +479 -0
- ebk/extract_metadata.py +76 -7
- ebk/library_db.py +899 -0
- ebk/plugins/__init__.py +42 -0
- ebk/plugins/base.py +502 -0
- ebk/plugins/hooks.py +444 -0
- ebk/plugins/registry.py +500 -0
- ebk/repl/__init__.py +9 -0
- ebk/repl/find.py +126 -0
- ebk/repl/grep.py +174 -0
- ebk/repl/shell.py +1677 -0
- ebk/repl/text_utils.py +320 -0
- ebk/search_parser.py +413 -0
- ebk/server.py +1633 -0
- ebk/services/__init__.py +11 -0
- ebk/services/import_service.py +442 -0
- ebk/services/tag_service.py +282 -0
- ebk/services/text_extraction.py +317 -0
- ebk/similarity/__init__.py +77 -0
- ebk/similarity/base.py +154 -0
- ebk/similarity/core.py +445 -0
- ebk/similarity/extractors.py +168 -0
- ebk/similarity/metrics.py +376 -0
- ebk/vfs/__init__.py +101 -0
- ebk/vfs/base.py +301 -0
- ebk/vfs/library_vfs.py +124 -0
- ebk/vfs/nodes/__init__.py +54 -0
- ebk/vfs/nodes/authors.py +196 -0
- ebk/vfs/nodes/books.py +480 -0
- ebk/vfs/nodes/files.py +155 -0
- ebk/vfs/nodes/metadata.py +385 -0
- ebk/vfs/nodes/root.py +100 -0
- ebk/vfs/nodes/similar.py +165 -0
- ebk/vfs/nodes/subjects.py +184 -0
- ebk/vfs/nodes/tags.py +371 -0
- ebk/vfs/resolver.py +228 -0
- ebk-0.3.2.dist-info/METADATA +755 -0
- ebk-0.3.2.dist-info/RECORD +69 -0
- {ebk-0.1.0.dist-info → ebk-0.3.2.dist-info}/WHEEL +1 -1
- ebk-0.3.2.dist-info/licenses/LICENSE +21 -0
- ebk/imports/__init__.py +0 -0
- ebk/imports/calibre.py +0 -144
- ebk/imports/ebooks.py +0 -116
- ebk/llm.py +0 -58
- ebk/manager.py +0 -44
- ebk/merge.py +0 -308
- ebk/streamlit/__init__.py +0 -0
- ebk/streamlit/__pycache__/__init__.cpython-310.pyc +0 -0
- ebk/streamlit/__pycache__/display.cpython-310.pyc +0 -0
- ebk/streamlit/__pycache__/filters.cpython-310.pyc +0 -0
- ebk/streamlit/__pycache__/utils.cpython-310.pyc +0 -0
- ebk/streamlit/app.py +0 -185
- ebk/streamlit/display.py +0 -168
- ebk/streamlit/filters.py +0 -151
- ebk/streamlit/utils.py +0 -58
- ebk/utils.py +0 -311
- ebk-0.1.0.dist-info/METADATA +0 -457
- ebk-0.1.0.dist-info/RECORD +0 -29
- {ebk-0.1.0.dist-info → ebk-0.3.2.dist-info}/entry_points.txt +0 -0
- {ebk-0.1.0.dist-info → ebk-0.3.2.dist-info}/top_level.txt +0 -0
ebk/plugins/registry.py
ADDED
|
@@ -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)
|
ebk/repl/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""REPL shell for interactive library navigation.
|
|
2
|
+
|
|
3
|
+
This module provides an interactive shell for navigating and managing
|
|
4
|
+
the ebook library through a virtual filesystem interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ebk.repl.shell import LibraryShell
|
|
8
|
+
|
|
9
|
+
__all__ = ["LibraryShell"]
|
ebk/repl/find.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Find command implementation for REPL shell."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from ebk.library_db import Library
|
|
5
|
+
from ebk.db.models import Book
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FindQuery:
|
|
9
|
+
"""Book finder with metadata filters."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, library: Library):
|
|
12
|
+
"""Initialize find query.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
library: Library instance
|
|
16
|
+
"""
|
|
17
|
+
self.library = library
|
|
18
|
+
|
|
19
|
+
def find(self, filters: Dict[str, Any]) -> List[Book]:
|
|
20
|
+
"""Find books matching filters.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
filters: Dictionary of field:value filters
|
|
24
|
+
Supported fields:
|
|
25
|
+
- title: Book title (partial match)
|
|
26
|
+
- author: Author name (partial match)
|
|
27
|
+
- subject: Subject/tag (partial match)
|
|
28
|
+
- text: Full-text search (FTS5 across title, description, extracted text)
|
|
29
|
+
- language: Language code (exact match)
|
|
30
|
+
- year: Publication year (exact match)
|
|
31
|
+
- publisher: Publisher name (partial match)
|
|
32
|
+
- format: File format (exact match, e.g., pdf, epub)
|
|
33
|
+
- limit: Maximum results (default: 50)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of matching books
|
|
37
|
+
"""
|
|
38
|
+
query = self.library.query()
|
|
39
|
+
|
|
40
|
+
# Apply filters
|
|
41
|
+
if "title" in filters:
|
|
42
|
+
query = query.filter_by_title(filters["title"])
|
|
43
|
+
|
|
44
|
+
if "author" in filters:
|
|
45
|
+
query = query.filter_by_author(filters["author"])
|
|
46
|
+
|
|
47
|
+
if "subject" in filters:
|
|
48
|
+
query = query.filter_by_subject(filters["subject"])
|
|
49
|
+
|
|
50
|
+
if "language" in filters:
|
|
51
|
+
query = query.filter_by_language(filters["language"])
|
|
52
|
+
|
|
53
|
+
if "year" in filters:
|
|
54
|
+
try:
|
|
55
|
+
year = int(filters["year"])
|
|
56
|
+
query = query.filter_by_year(year)
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass # Skip invalid year
|
|
59
|
+
|
|
60
|
+
if "publisher" in filters:
|
|
61
|
+
query = query.filter_by_publisher(filters["publisher"])
|
|
62
|
+
|
|
63
|
+
if "format" in filters:
|
|
64
|
+
query = query.filter_by_format(filters["format"])
|
|
65
|
+
|
|
66
|
+
if "text" in filters:
|
|
67
|
+
query = query.filter_by_text(filters["text"])
|
|
68
|
+
|
|
69
|
+
# Apply limit
|
|
70
|
+
limit = filters.get("limit", 50)
|
|
71
|
+
try:
|
|
72
|
+
# Convert to int if it's a string
|
|
73
|
+
if isinstance(limit, str):
|
|
74
|
+
limit = int(limit)
|
|
75
|
+
if isinstance(limit, int):
|
|
76
|
+
query = query.limit(limit)
|
|
77
|
+
except (ValueError, TypeError):
|
|
78
|
+
# Invalid limit, use default
|
|
79
|
+
query = query.limit(50)
|
|
80
|
+
|
|
81
|
+
# Execute query
|
|
82
|
+
return query.all()
|
|
83
|
+
|
|
84
|
+
def parse_filters(self, args: List[str]) -> Dict[str, Any]:
|
|
85
|
+
"""Parse command-line arguments into filter dictionary.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
args: List of filter arguments in format "field:value"
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary of filters
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If argument format is invalid
|
|
95
|
+
"""
|
|
96
|
+
filters = {}
|
|
97
|
+
|
|
98
|
+
for arg in args:
|
|
99
|
+
if ":" not in arg:
|
|
100
|
+
raise ValueError(f"Invalid filter format: {arg}. Use field:value")
|
|
101
|
+
|
|
102
|
+
field, value = arg.split(":", 1)
|
|
103
|
+
field = field.lower().strip()
|
|
104
|
+
value = value.strip()
|
|
105
|
+
|
|
106
|
+
# Validate field
|
|
107
|
+
valid_fields = {
|
|
108
|
+
"title",
|
|
109
|
+
"author",
|
|
110
|
+
"subject",
|
|
111
|
+
"text",
|
|
112
|
+
"language",
|
|
113
|
+
"year",
|
|
114
|
+
"publisher",
|
|
115
|
+
"format",
|
|
116
|
+
"limit",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if field not in valid_fields:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Unknown field: {field}. Valid fields: {', '.join(sorted(valid_fields))}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
filters[field] = value
|
|
125
|
+
|
|
126
|
+
return filters
|