ebk 0.4.4__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.
Files changed (87) hide show
  1. ebk/__init__.py +35 -0
  2. ebk/ai/__init__.py +23 -0
  3. ebk/ai/knowledge_graph.py +450 -0
  4. ebk/ai/llm_providers/__init__.py +26 -0
  5. ebk/ai/llm_providers/anthropic.py +209 -0
  6. ebk/ai/llm_providers/base.py +295 -0
  7. ebk/ai/llm_providers/gemini.py +285 -0
  8. ebk/ai/llm_providers/ollama.py +294 -0
  9. ebk/ai/metadata_enrichment.py +394 -0
  10. ebk/ai/question_generator.py +328 -0
  11. ebk/ai/reading_companion.py +224 -0
  12. ebk/ai/semantic_search.py +433 -0
  13. ebk/ai/text_extractor.py +393 -0
  14. ebk/calibre_import.py +66 -0
  15. ebk/cli.py +6433 -0
  16. ebk/config.py +230 -0
  17. ebk/db/__init__.py +37 -0
  18. ebk/db/migrations.py +507 -0
  19. ebk/db/models.py +725 -0
  20. ebk/db/session.py +144 -0
  21. ebk/decorators.py +1 -0
  22. ebk/exports/__init__.py +0 -0
  23. ebk/exports/base_exporter.py +218 -0
  24. ebk/exports/echo_export.py +279 -0
  25. ebk/exports/html_library.py +1743 -0
  26. ebk/exports/html_utils.py +87 -0
  27. ebk/exports/hugo.py +59 -0
  28. ebk/exports/jinja_export.py +286 -0
  29. ebk/exports/multi_facet_export.py +159 -0
  30. ebk/exports/opds_export.py +232 -0
  31. ebk/exports/symlink_dag.py +479 -0
  32. ebk/exports/zip.py +25 -0
  33. ebk/extract_metadata.py +341 -0
  34. ebk/ident.py +89 -0
  35. ebk/library_db.py +1440 -0
  36. ebk/opds.py +748 -0
  37. ebk/plugins/__init__.py +42 -0
  38. ebk/plugins/base.py +502 -0
  39. ebk/plugins/hooks.py +442 -0
  40. ebk/plugins/registry.py +499 -0
  41. ebk/repl/__init__.py +9 -0
  42. ebk/repl/find.py +126 -0
  43. ebk/repl/grep.py +173 -0
  44. ebk/repl/shell.py +1677 -0
  45. ebk/repl/text_utils.py +320 -0
  46. ebk/search_parser.py +413 -0
  47. ebk/server.py +3608 -0
  48. ebk/services/__init__.py +28 -0
  49. ebk/services/annotation_extraction.py +351 -0
  50. ebk/services/annotation_service.py +380 -0
  51. ebk/services/export_service.py +577 -0
  52. ebk/services/import_service.py +447 -0
  53. ebk/services/personal_metadata_service.py +347 -0
  54. ebk/services/queue_service.py +253 -0
  55. ebk/services/tag_service.py +281 -0
  56. ebk/services/text_extraction.py +317 -0
  57. ebk/services/view_service.py +12 -0
  58. ebk/similarity/__init__.py +77 -0
  59. ebk/similarity/base.py +154 -0
  60. ebk/similarity/core.py +471 -0
  61. ebk/similarity/extractors.py +168 -0
  62. ebk/similarity/metrics.py +376 -0
  63. ebk/skills/SKILL.md +182 -0
  64. ebk/skills/__init__.py +1 -0
  65. ebk/vfs/__init__.py +101 -0
  66. ebk/vfs/base.py +298 -0
  67. ebk/vfs/library_vfs.py +122 -0
  68. ebk/vfs/nodes/__init__.py +54 -0
  69. ebk/vfs/nodes/authors.py +196 -0
  70. ebk/vfs/nodes/books.py +480 -0
  71. ebk/vfs/nodes/files.py +155 -0
  72. ebk/vfs/nodes/metadata.py +385 -0
  73. ebk/vfs/nodes/root.py +100 -0
  74. ebk/vfs/nodes/similar.py +165 -0
  75. ebk/vfs/nodes/subjects.py +184 -0
  76. ebk/vfs/nodes/tags.py +371 -0
  77. ebk/vfs/resolver.py +228 -0
  78. ebk/vfs_router.py +275 -0
  79. ebk/views/__init__.py +32 -0
  80. ebk/views/dsl.py +668 -0
  81. ebk/views/service.py +619 -0
  82. ebk-0.4.4.dist-info/METADATA +755 -0
  83. ebk-0.4.4.dist-info/RECORD +87 -0
  84. ebk-0.4.4.dist-info/WHEEL +5 -0
  85. ebk-0.4.4.dist-info/entry_points.txt +2 -0
  86. ebk-0.4.4.dist-info/licenses/LICENSE +21 -0
  87. ebk-0.4.4.dist-info/top_level.txt +1 -0
ebk/plugins/hooks.py ADDED
@@ -0,0 +1,442 @@
1
+ """
2
+ Hook system for EBK plugins.
3
+
4
+ This module provides a hook system that allows plugins and user code
5
+ to register callbacks for various events in the EBK lifecycle.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from typing import Callable, Any, List, Dict, Optional
11
+ from collections import defaultdict
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class HookRegistry:
17
+ """Registry for managing hook callbacks."""
18
+
19
+ def __init__(self):
20
+ self._hooks: Dict[str, List[Callable]] = defaultdict(list)
21
+ self._async_hooks: Dict[str, List[Callable]] = defaultdict(list)
22
+ self._hook_priorities: Dict[str, Dict[Callable, int]] = defaultdict(dict)
23
+ self._hook_descriptions: Dict[str, str] = {}
24
+
25
+ def register_hook(self,
26
+ event: str,
27
+ callback: Callable,
28
+ priority: int = 0,
29
+ description: Optional[str] = None) -> None:
30
+ """
31
+ Register a hook callback.
32
+
33
+ Args:
34
+ event: Event name to hook into
35
+ callback: Callback function
36
+ priority: Priority (higher runs first)
37
+ description: Optional description of what this hook does
38
+ """
39
+ if asyncio.iscoroutinefunction(callback):
40
+ self._async_hooks[event].append(callback)
41
+ else:
42
+ self._hooks[event].append(callback)
43
+
44
+ self._hook_priorities[event][callback] = priority
45
+
46
+ # Sort by priority
47
+ if event in self._hooks:
48
+ self._hooks[event].sort(
49
+ key=lambda cb: self._hook_priorities[event].get(cb, 0),
50
+ reverse=True
51
+ )
52
+ if event in self._async_hooks:
53
+ self._async_hooks[event].sort(
54
+ key=lambda cb: self._hook_priorities[event].get(cb, 0),
55
+ reverse=True
56
+ )
57
+
58
+ if description:
59
+ hook_id = f"{event}:{callback.__name__}"
60
+ self._hook_descriptions[hook_id] = description
61
+
62
+ logger.debug(f"Registered hook for {event}: {callback.__name__} (priority: {priority})")
63
+
64
+ def unregister_hook(self, event: str, callback: Callable) -> bool:
65
+ """
66
+ Unregister a hook callback.
67
+
68
+ Args:
69
+ event: Event name
70
+ callback: Callback to remove
71
+
72
+ Returns:
73
+ True if callback was removed
74
+ """
75
+ removed = False
76
+
77
+ if callback in self._hooks.get(event, []):
78
+ self._hooks[event].remove(callback)
79
+ removed = True
80
+
81
+ if callback in self._async_hooks.get(event, []):
82
+ self._async_hooks[event].remove(callback)
83
+ removed = True
84
+
85
+ if removed:
86
+ self._hook_priorities[event].pop(callback, None)
87
+ logger.debug(f"Unregistered hook for {event}: {callback.__name__}")
88
+
89
+ return removed
90
+
91
+ def trigger(self, event: str, *args, **kwargs) -> List[Any]:
92
+ """
93
+ Trigger all callbacks for an event (synchronous).
94
+
95
+ Args:
96
+ event: Event name
97
+ *args: Positional arguments for callbacks
98
+ **kwargs: Keyword arguments for callbacks
99
+
100
+ Returns:
101
+ List of results from callbacks
102
+ """
103
+ results = []
104
+
105
+ # Run synchronous hooks
106
+ for callback in self._hooks.get(event, []):
107
+ try:
108
+ result = callback(*args, **kwargs)
109
+ if result is not None:
110
+ results.append(result)
111
+ except Exception as e:
112
+ logger.error(f"Hook {callback.__name__} failed for event {event}: {e}")
113
+
114
+ # Handle async hooks in sync context
115
+ if event in self._async_hooks:
116
+ logger.warning(f"Async hooks registered for {event} but triggered synchronously")
117
+
118
+ return results
119
+
120
+ async def trigger_async(self, event: str, *args, **kwargs) -> List[Any]:
121
+ """
122
+ Trigger all callbacks for an event (asynchronous).
123
+
124
+ Args:
125
+ event: Event name
126
+ *args: Positional arguments for callbacks
127
+ **kwargs: Keyword arguments for callbacks
128
+
129
+ Returns:
130
+ List of results from callbacks
131
+ """
132
+ results = []
133
+
134
+ # Run synchronous hooks
135
+ for callback in self._hooks.get(event, []):
136
+ try:
137
+ result = callback(*args, **kwargs)
138
+ if result is not None:
139
+ results.append(result)
140
+ except Exception as e:
141
+ logger.error(f"Hook {callback.__name__} failed for event {event}: {e}")
142
+
143
+ # Run async hooks
144
+ tasks = []
145
+ for callback in self._async_hooks.get(event, []):
146
+ tasks.append(self._run_async_hook(callback, event, args, kwargs))
147
+
148
+ if tasks:
149
+ async_results = await asyncio.gather(*tasks, return_exceptions=True)
150
+ for result in async_results:
151
+ if isinstance(result, Exception):
152
+ logger.error(f"Async hook failed for event {event}: {result}")
153
+ elif result is not None:
154
+ results.append(result)
155
+
156
+ return results
157
+
158
+ async def _run_async_hook(self, callback: Callable, event: str, args, kwargs) -> Any:
159
+ """
160
+ Run a single async hook with error handling.
161
+
162
+ Args:
163
+ callback: Async callback function
164
+ event: Event name
165
+ args: Positional arguments
166
+ kwargs: Keyword arguments
167
+
168
+ Returns:
169
+ Result from callback or None
170
+ """
171
+ try:
172
+ return await callback(*args, **kwargs)
173
+ except Exception as e:
174
+ logger.error(f"Async hook {callback.__name__} failed for event {event}: {e}")
175
+ raise
176
+
177
+ def trigger_filter(self, event: str, value: Any, *args, **kwargs) -> Any:
178
+ """
179
+ Trigger filter hooks that can modify a value.
180
+
181
+ Each hook receives the current value and can return a modified version.
182
+ If a hook returns None, the value is unchanged.
183
+
184
+ Args:
185
+ event: Event name
186
+ value: Initial value to filter
187
+ *args: Additional positional arguments for callbacks
188
+ **kwargs: Additional keyword arguments for callbacks
189
+
190
+ Returns:
191
+ Final filtered value
192
+ """
193
+ current_value = value
194
+
195
+ for callback in self._hooks.get(event, []):
196
+ try:
197
+ result = callback(current_value, *args, **kwargs)
198
+ if result is not None:
199
+ current_value = result
200
+ except Exception as e:
201
+ logger.error(f"Filter hook {callback.__name__} failed for event {event}: {e}")
202
+
203
+ return current_value
204
+
205
+ async def trigger_filter_async(self, event: str, value: Any, *args, **kwargs) -> Any:
206
+ """
207
+ Trigger async filter hooks that can modify a value.
208
+
209
+ Args:
210
+ event: Event name
211
+ value: Initial value to filter
212
+ *args: Additional positional arguments for callbacks
213
+ **kwargs: Additional keyword arguments for callbacks
214
+
215
+ Returns:
216
+ Final filtered value
217
+ """
218
+ current_value = value
219
+
220
+ # Run synchronous hooks first
221
+ current_value = self.trigger_filter(event, current_value, *args, **kwargs)
222
+
223
+ # Run async hooks
224
+ for callback in self._async_hooks.get(event, []):
225
+ try:
226
+ result = await callback(current_value, *args, **kwargs)
227
+ if result is not None:
228
+ current_value = result
229
+ except Exception as e:
230
+ logger.error(f"Async filter hook {callback.__name__} failed for event {event}: {e}")
231
+
232
+ return current_value
233
+
234
+ def has_hooks(self, event: str) -> bool:
235
+ """
236
+ Check if an event has any hooks registered.
237
+
238
+ Args:
239
+ event: Event name
240
+
241
+ Returns:
242
+ True if hooks are registered
243
+ """
244
+ return bool(self._hooks.get(event) or self._async_hooks.get(event))
245
+
246
+ def list_hooks(self) -> Dict[str, List[str]]:
247
+ """
248
+ List all registered hooks.
249
+
250
+ Returns:
251
+ Dictionary mapping events to callback names
252
+ """
253
+ result = {}
254
+
255
+ for event, callbacks in self._hooks.items():
256
+ if event not in result:
257
+ result[event] = []
258
+ result[event].extend([cb.__name__ for cb in callbacks])
259
+
260
+ for event, callbacks in self._async_hooks.items():
261
+ if event not in result:
262
+ result[event] = []
263
+ result[event].extend([f"{cb.__name__} (async)" for cb in callbacks])
264
+
265
+ return result
266
+
267
+ def clear_hooks(self, event: Optional[str] = None) -> None:
268
+ """
269
+ Clear hooks for an event or all events.
270
+
271
+ Args:
272
+ event: Event name to clear, or None to clear all
273
+ """
274
+ if event:
275
+ self._hooks.pop(event, None)
276
+ self._async_hooks.pop(event, None)
277
+ self._hook_priorities.pop(event, None)
278
+ else:
279
+ self._hooks.clear()
280
+ self._async_hooks.clear()
281
+ self._hook_priorities.clear()
282
+ self._hook_descriptions.clear()
283
+
284
+
285
+ # Global hook registry
286
+ hooks = HookRegistry()
287
+
288
+
289
+ def hook(event: str, priority: int = 0, description: Optional[str] = None):
290
+ """
291
+ Decorator for registering hook callbacks.
292
+
293
+ Usage:
294
+ @hook("entry.added")
295
+ def on_entry_added(entry, library):
296
+ print(f"Entry added: {entry['title']}")
297
+
298
+ @hook("before_export", priority=10)
299
+ async def validate_before_export(entries, format):
300
+ # Async hook
301
+ await validate_entries(entries)
302
+
303
+ Args:
304
+ event: Event name to hook into
305
+ priority: Priority (higher runs first)
306
+ description: Optional description
307
+
308
+ Returns:
309
+ Decorator function
310
+ """
311
+ def decorator(func: Callable) -> Callable:
312
+ hooks.register_hook(event, func, priority, description)
313
+ return func
314
+ return decorator
315
+
316
+
317
+ def trigger_hook(event: str, *args, **kwargs) -> List[Any]:
318
+ """
319
+ Trigger all callbacks for an event.
320
+
321
+ Args:
322
+ event: Event name
323
+ *args: Positional arguments for callbacks
324
+ **kwargs: Keyword arguments for callbacks
325
+
326
+ Returns:
327
+ List of results from callbacks
328
+ """
329
+ return hooks.trigger(event, *args, **kwargs)
330
+
331
+
332
+ async def trigger_hook_async(event: str, *args, **kwargs) -> List[Any]:
333
+ """
334
+ Trigger all callbacks for an event (async).
335
+
336
+ Args:
337
+ event: Event name
338
+ *args: Positional arguments for callbacks
339
+ **kwargs: Keyword arguments for callbacks
340
+
341
+ Returns:
342
+ List of results from callbacks
343
+ """
344
+ return await hooks.trigger_async(event, *args, **kwargs)
345
+
346
+
347
+ def filter_value(event: str, value: Any, *args, **kwargs) -> Any:
348
+ """
349
+ Apply filter hooks to modify a value.
350
+
351
+ Args:
352
+ event: Event name
353
+ value: Initial value
354
+ *args: Additional arguments for callbacks
355
+ **kwargs: Additional keyword arguments for callbacks
356
+
357
+ Returns:
358
+ Filtered value
359
+ """
360
+ return hooks.trigger_filter(event, value, *args, **kwargs)
361
+
362
+
363
+ async def filter_value_async(event: str, value: Any, *args, **kwargs) -> Any:
364
+ """
365
+ Apply async filter hooks to modify a value.
366
+
367
+ Args:
368
+ event: Event name
369
+ value: Initial value
370
+ *args: Additional arguments for callbacks
371
+ **kwargs: Additional keyword arguments for callbacks
372
+
373
+ Returns:
374
+ Filtered value
375
+ """
376
+ return await hooks.trigger_filter_async(event, value, *args, **kwargs)
377
+
378
+
379
+ # Predefined events that EBK will trigger
380
+ EVENTS = {
381
+ # Library events
382
+ 'library.opened': 'Library has been opened',
383
+ 'library.closed': 'Library has been closed',
384
+ 'library.saved': 'Library has been saved',
385
+
386
+ # Entry events
387
+ 'entry.added': 'Entry added to library',
388
+ 'entry.updated': 'Entry updated',
389
+ 'entry.deleted': 'Entry deleted from library',
390
+ 'entry.before_add': 'Before entry is added (can cancel)',
391
+ 'entry.before_update': 'Before entry is updated (can cancel)',
392
+ 'entry.before_delete': 'Before entry is deleted (can cancel)',
393
+
394
+ # Metadata events
395
+ 'metadata.extracted': 'Metadata extracted from source',
396
+ 'metadata.enriched': 'Metadata enriched from external source',
397
+ 'metadata.validated': 'Metadata validated',
398
+
399
+ # Tag events
400
+ 'tags.suggested': 'Tags suggested for entry',
401
+ 'tags.added': 'Tags added to entry',
402
+ 'tags.removed': 'Tags removed from entry',
403
+
404
+ # Import/Export events
405
+ 'import.started': 'Import operation started',
406
+ 'import.progress': 'Import operation progress',
407
+ 'import.completed': 'Import operation completed',
408
+ 'import.failed': 'Import operation failed',
409
+ 'export.started': 'Export operation started',
410
+ 'export.progress': 'Export operation progress',
411
+ 'export.completed': 'Export operation completed',
412
+ 'export.failed': 'Export operation failed',
413
+
414
+ # Search events
415
+ 'search.started': 'Search operation started',
416
+ 'search.completed': 'Search operation completed',
417
+ 'search.results_filtered': 'Search results filtered',
418
+
419
+ # Plugin events
420
+ 'plugin.registered': 'Plugin registered',
421
+ 'plugin.unregistered': 'Plugin unregistered',
422
+ 'plugin.enabled': 'Plugin enabled',
423
+ 'plugin.disabled': 'Plugin disabled',
424
+ 'plugin.configured': 'Plugin configured',
425
+
426
+ # Filter events (value can be modified)
427
+ 'filter.entry_data': 'Filter entry data before save',
428
+ 'filter.search_query': 'Filter search query before execution',
429
+ 'filter.export_entries': 'Filter entries before export',
430
+ 'filter.import_entry': 'Filter entry during import',
431
+ 'filter.suggested_tags': 'Filter suggested tags',
432
+ }
433
+
434
+
435
+ def list_available_events() -> Dict[str, str]:
436
+ """
437
+ List all available events with descriptions.
438
+
439
+ Returns:
440
+ Dictionary mapping event names to descriptions
441
+ """
442
+ return EVENTS.copy()