ebk 0.3.1__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.

Files changed (61) hide show
  1. ebk/ai/__init__.py +23 -0
  2. ebk/ai/knowledge_graph.py +443 -0
  3. ebk/ai/llm_providers/__init__.py +21 -0
  4. ebk/ai/llm_providers/base.py +230 -0
  5. ebk/ai/llm_providers/ollama.py +362 -0
  6. ebk/ai/metadata_enrichment.py +396 -0
  7. ebk/ai/question_generator.py +328 -0
  8. ebk/ai/reading_companion.py +224 -0
  9. ebk/ai/semantic_search.py +434 -0
  10. ebk/ai/text_extractor.py +394 -0
  11. ebk/cli.py +1097 -9
  12. ebk/db/__init__.py +37 -0
  13. ebk/db/migrations.py +180 -0
  14. ebk/db/models.py +526 -0
  15. ebk/db/session.py +144 -0
  16. ebk/exports/__init__.py +0 -0
  17. ebk/exports/base_exporter.py +218 -0
  18. ebk/exports/html_library.py +1390 -0
  19. ebk/exports/html_utils.py +117 -0
  20. ebk/exports/hugo.py +59 -0
  21. ebk/exports/jinja_export.py +287 -0
  22. ebk/exports/multi_facet_export.py +164 -0
  23. ebk/exports/symlink_dag.py +479 -0
  24. ebk/exports/zip.py +25 -0
  25. ebk/library_db.py +155 -0
  26. ebk/repl/__init__.py +9 -0
  27. ebk/repl/find.py +126 -0
  28. ebk/repl/grep.py +174 -0
  29. ebk/repl/shell.py +1677 -0
  30. ebk/repl/text_utils.py +320 -0
  31. ebk/services/__init__.py +11 -0
  32. ebk/services/import_service.py +442 -0
  33. ebk/services/tag_service.py +282 -0
  34. ebk/services/text_extraction.py +317 -0
  35. ebk/similarity/__init__.py +77 -0
  36. ebk/similarity/base.py +154 -0
  37. ebk/similarity/core.py +445 -0
  38. ebk/similarity/extractors.py +168 -0
  39. ebk/similarity/metrics.py +376 -0
  40. ebk/vfs/__init__.py +101 -0
  41. ebk/vfs/base.py +301 -0
  42. ebk/vfs/library_vfs.py +124 -0
  43. ebk/vfs/nodes/__init__.py +54 -0
  44. ebk/vfs/nodes/authors.py +196 -0
  45. ebk/vfs/nodes/books.py +480 -0
  46. ebk/vfs/nodes/files.py +155 -0
  47. ebk/vfs/nodes/metadata.py +385 -0
  48. ebk/vfs/nodes/root.py +100 -0
  49. ebk/vfs/nodes/similar.py +165 -0
  50. ebk/vfs/nodes/subjects.py +184 -0
  51. ebk/vfs/nodes/tags.py +371 -0
  52. ebk/vfs/resolver.py +228 -0
  53. {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/METADATA +1 -1
  54. ebk-0.3.2.dist-info/RECORD +69 -0
  55. ebk-0.3.2.dist-info/entry_points.txt +2 -0
  56. ebk-0.3.2.dist-info/top_level.txt +1 -0
  57. ebk-0.3.1.dist-info/RECORD +0 -19
  58. ebk-0.3.1.dist-info/entry_points.txt +0 -6
  59. ebk-0.3.1.dist-info/top_level.txt +0 -2
  60. {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/WHEEL +0 -0
  61. {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,218 @@
1
+ """Base exporter class for ebk library exports."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Any
6
+ import json
7
+ import shutil
8
+ import re
9
+ from .html_utils import create_safe_filename, sanitize_for_html
10
+
11
+
12
+ class BaseExporter(ABC):
13
+ """
14
+ Abstract base class for all ebk exporters.
15
+
16
+ Provides common functionality for exporting library data:
17
+ - Loading metadata
18
+ - File operations (copy/symlink)
19
+ - Filename sanitization
20
+ - Directory management
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize the base exporter."""
25
+ self.library_path = None
26
+ self.output_path = None
27
+ self.entries = []
28
+
29
+ def load_metadata(self, library_path: Path) -> List[Dict]:
30
+ """
31
+ Load metadata from the library.
32
+
33
+ Args:
34
+ library_path: Path to the ebk library
35
+
36
+ Returns:
37
+ List of entry dictionaries
38
+
39
+ Raises:
40
+ FileNotFoundError: If metadata.json doesn't exist
41
+ json.JSONDecodeError: If metadata is invalid
42
+ """
43
+ self.library_path = Path(library_path)
44
+ metadata_path = self.library_path / "metadata.json"
45
+
46
+ if not metadata_path.exists():
47
+ raise FileNotFoundError(f"Metadata file not found at {metadata_path}")
48
+
49
+ with open(metadata_path, 'r', encoding='utf-8') as f:
50
+ self.entries = json.load(f)
51
+
52
+ return self.entries
53
+
54
+ def prepare_output_directory(self, output_path: Path, clean: bool = True):
55
+ """
56
+ Prepare the output directory.
57
+
58
+ Args:
59
+ output_path: Path for output
60
+ clean: Whether to clean existing directory
61
+ """
62
+ self.output_path = Path(output_path)
63
+
64
+ if clean and self.output_path.exists():
65
+ shutil.rmtree(self.output_path)
66
+
67
+ self.output_path.mkdir(parents=True, exist_ok=True)
68
+
69
+ def copy_entry_files(self, entry: Dict, source_dir: Path, dest_dir: Path):
70
+ """
71
+ Copy entry files (ebooks and covers) to destination.
72
+
73
+ Args:
74
+ entry: Entry dictionary
75
+ source_dir: Source library directory
76
+ dest_dir: Destination directory
77
+ """
78
+ # Copy ebook files
79
+ for file_path in entry.get('file_paths', []):
80
+ src_file = source_dir / file_path
81
+ if src_file.exists():
82
+ dest_file = dest_dir / Path(file_path).name
83
+ shutil.copy2(src_file, dest_file)
84
+
85
+ # Copy cover image
86
+ cover_path = entry.get('cover_path')
87
+ if cover_path:
88
+ src_cover = source_dir / cover_path
89
+ if src_cover.exists():
90
+ dest_cover = dest_dir / Path(cover_path).name
91
+ shutil.copy2(src_cover, dest_cover)
92
+
93
+ def symlink_entry_files(self, entry: Dict, source_dir: Path, dest_dir: Path):
94
+ """
95
+ Create symlinks to entry files instead of copying.
96
+
97
+ Args:
98
+ entry: Entry dictionary
99
+ source_dir: Source library directory
100
+ dest_dir: Destination directory
101
+ """
102
+ # Symlink ebook files
103
+ for file_path in entry.get('file_paths', []):
104
+ src_file = source_dir / file_path
105
+ if src_file.exists():
106
+ dest_file = dest_dir / Path(file_path).name
107
+ if not dest_file.exists():
108
+ dest_file.symlink_to(src_file.absolute())
109
+
110
+ # Symlink cover image
111
+ cover_path = entry.get('cover_path')
112
+ if cover_path:
113
+ src_cover = source_dir / cover_path
114
+ if src_cover.exists():
115
+ dest_cover = dest_dir / Path(cover_path).name
116
+ if not dest_cover.exists():
117
+ dest_cover.symlink_to(src_cover.absolute())
118
+
119
+ def sanitize_filename(self, name: str, max_length: int = 100) -> str:
120
+ """
121
+ Sanitize filename to be filesystem-safe.
122
+
123
+ Args:
124
+ name: Original filename
125
+ max_length: Maximum length for filename
126
+
127
+ Returns:
128
+ Sanitized filename
129
+ """
130
+ return create_safe_filename(name, max_length=max_length)
131
+
132
+ def get_readable_name(self, entry: Dict) -> str:
133
+ """
134
+ Get a human-readable name for an entry.
135
+
136
+ Args:
137
+ entry: Entry dictionary
138
+
139
+ Returns:
140
+ Readable name combining title and author
141
+ """
142
+ title = entry.get('title', 'Unknown')
143
+ creators = entry.get('creators', [])
144
+
145
+ if creators:
146
+ author = creators[0]
147
+ if len(creators) > 1:
148
+ author += " et al."
149
+ return f"{title} - {author}"
150
+
151
+ return title
152
+
153
+ def write_json(self, data: Any, file_path: Path):
154
+ """
155
+ Write JSON data to file with proper encoding.
156
+
157
+ Args:
158
+ data: Data to serialize
159
+ file_path: Output file path
160
+ """
161
+ with open(file_path, 'w', encoding='utf-8') as f:
162
+ json.dump(data, f, ensure_ascii=False, indent=2)
163
+
164
+ def create_readme(self, output_dir: Path, stats: Dict):
165
+ """
166
+ Create a README file with export information.
167
+
168
+ Args:
169
+ output_dir: Output directory
170
+ stats: Statistics dictionary
171
+ """
172
+ readme_path = output_dir / "README.md"
173
+
174
+ content = f"""# EBK Library Export
175
+
176
+ This directory contains an export of an EBK library.
177
+
178
+ ## Statistics
179
+ - Total entries: {stats.get('total_entries', 0)}
180
+ - Export date: {stats.get('export_date', 'Unknown')}
181
+ - Export type: {stats.get('export_type', 'Unknown')}
182
+
183
+ ## Structure
184
+ {stats.get('structure_description', 'See directory contents for structure.')}
185
+
186
+ ---
187
+ Generated by EBK Library Manager
188
+ """
189
+
190
+ with open(readme_path, 'w', encoding='utf-8') as f:
191
+ f.write(content)
192
+
193
+ @abstractmethod
194
+ def export(self, library_path: Path, output_path: Path, **options):
195
+ """
196
+ Export the library.
197
+
198
+ This method must be implemented by subclasses.
199
+
200
+ Args:
201
+ library_path: Path to source library
202
+ output_path: Path for output
203
+ **options: Additional export options
204
+ """
205
+ pass
206
+
207
+ def validate_export(self) -> bool:
208
+ """
209
+ Validate that the export was successful.
210
+
211
+ Returns:
212
+ True if validation passes
213
+ """
214
+ if not self.output_path or not self.output_path.exists():
215
+ return False
216
+
217
+ # Check if at least some files were created
218
+ return any(self.output_path.iterdir())