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.
- 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 +1097 -9
- 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/exports/__init__.py +0 -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 +59 -0
- ebk/exports/jinja_export.py +287 -0
- ebk/exports/multi_facet_export.py +164 -0
- ebk/exports/symlink_dag.py +479 -0
- ebk/exports/zip.py +25 -0
- ebk/library_db.py +155 -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/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.1.dist-info → ebk-0.3.2.dist-info}/METADATA +1 -1
- ebk-0.3.2.dist-info/RECORD +69 -0
- ebk-0.3.2.dist-info/entry_points.txt +2 -0
- ebk-0.3.2.dist-info/top_level.txt +1 -0
- ebk-0.3.1.dist-info/RECORD +0 -19
- ebk-0.3.1.dist-info/entry_points.txt +0 -6
- ebk-0.3.1.dist-info/top_level.txt +0 -2
- {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/WHEEL +0 -0
- {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())
|