ebk 0.1.0__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 +0 -0
- ebk/cli.py +879 -0
- ebk/config.py +35 -0
- ebk/exports/__init__.py +0 -0
- ebk/exports/hugo.py +55 -0
- ebk/exports/zip.py +25 -0
- ebk/extract_metadata.py +273 -0
- ebk/ident.py +96 -0
- ebk/imports/__init__.py +0 -0
- ebk/imports/calibre.py +144 -0
- ebk/imports/ebooks.py +116 -0
- ebk/llm.py +58 -0
- ebk/manager.py +44 -0
- ebk/merge.py +308 -0
- 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 +185 -0
- ebk/streamlit/display.py +168 -0
- ebk/streamlit/filters.py +151 -0
- ebk/streamlit/utils.py +58 -0
- ebk/utils.py +311 -0
- ebk-0.1.0.dist-info/METADATA +457 -0
- ebk-0.1.0.dist-info/RECORD +29 -0
- ebk-0.1.0.dist-info/WHEEL +5 -0
- ebk-0.1.0.dist-info/entry_points.txt +2 -0
- ebk-0.1.0.dist-info/top_level.txt +1 -0
ebk/cli.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import networkx as nx
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.logging import RichHandler
|
|
14
|
+
from rich.progress import Progress
|
|
15
|
+
from rich.prompt import Confirm
|
|
16
|
+
from rich.traceback import install
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich import print_json as print_json_as_table
|
|
19
|
+
from rich.json import JSON
|
|
20
|
+
|
|
21
|
+
from .exports.hugo import export_hugo
|
|
22
|
+
from .exports.zip import export_zipfile
|
|
23
|
+
from .imports import ebooks, calibre
|
|
24
|
+
from .merge import merge_libraries
|
|
25
|
+
from .utils import enumerate_ebooks, load_library, get_unique_filename, search_regex, search_jmes, get_library_statistics, get_index_by_unique_id, print_json_as_table
|
|
26
|
+
from .ident import add_unique_id
|
|
27
|
+
from .llm import query_llm
|
|
28
|
+
|
|
29
|
+
# Initialize Rich Traceback for better error messages
|
|
30
|
+
install(show_locals=True)
|
|
31
|
+
|
|
32
|
+
# Initialize Rich Console
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
# Configure logging to use Rich's RichHandler
|
|
36
|
+
logging.basicConfig(
|
|
37
|
+
level=logging.INFO, # Set to INFO by default, DEBUG if verbose
|
|
38
|
+
format="%(message)s",
|
|
39
|
+
datefmt="[%X]",
|
|
40
|
+
handlers=[RichHandler(rich_tracebacks=True)]
|
|
41
|
+
)
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
app = typer.Typer()
|
|
45
|
+
|
|
46
|
+
@app.callback()
|
|
47
|
+
def main(
|
|
48
|
+
ctx: typer.Context,
|
|
49
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose mode"),
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
ebk - A lightweight tool for managing eBook metadata.
|
|
53
|
+
"""
|
|
54
|
+
if verbose:
|
|
55
|
+
logger.setLevel(logging.DEBUG)
|
|
56
|
+
console.print("[bold green]Verbose mode enabled.[/bold green]")
|
|
57
|
+
|
|
58
|
+
@app.command()
|
|
59
|
+
def import_zip(
|
|
60
|
+
zip_file: str = typer.Argument(..., help="Path to the Zip file containing the ebk library"),
|
|
61
|
+
output_dir: str = typer.Option(None, "--output-dir", "-o", help="Output directory for the ebk library (default: <zip_file>_ebk)"),
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Import an ebk library from a Zip file.
|
|
65
|
+
"""
|
|
66
|
+
output_dir = output_dir or f"{zip_file.rstrip('.zip')}"
|
|
67
|
+
with Progress(console=console) as progress:
|
|
68
|
+
task = progress.add_task("[cyan]Importing Zip file...", total=None)
|
|
69
|
+
try:
|
|
70
|
+
if Path(output_dir).exists():
|
|
71
|
+
output_dir = get_unique_filename(output_dir)
|
|
72
|
+
with progress:
|
|
73
|
+
shutil.unpack_archive(zip_file, output_dir)
|
|
74
|
+
progress.update(task, description="[green]Zip file imported successfully!")
|
|
75
|
+
logger.info(f"Zip file imported to {output_dir}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
progress.update(task, description="[red]Failed to import Zip file.")
|
|
78
|
+
logger.error(f"Error importing Zip file: {e}")
|
|
79
|
+
raise typer.Exit(code=1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def import_calibre(
|
|
84
|
+
calibre_dir: str = typer.Argument(..., help="Path to the Calibre library directory"),
|
|
85
|
+
output_dir: str = typer.Option(None, "--output-dir", "-o", help="Output directory for the ebk library (default: <calibre_dir>_ebk)")
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Import a Calibre library.
|
|
89
|
+
"""
|
|
90
|
+
output_dir = output_dir or f"{calibre_dir.rstrip('/')}-ebk"
|
|
91
|
+
with Progress(console=console) as progress:
|
|
92
|
+
task = progress.add_task("[cyan]Importing Calibre library...", total=None)
|
|
93
|
+
try:
|
|
94
|
+
calibre.import_calibre(calibre_dir, output_dir)
|
|
95
|
+
progress.update(task, description="[green]Calibre library imported successfully!")
|
|
96
|
+
logger.info(f"Calibre library imported to {output_dir}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
progress.update(task, description="[red]Failed to import Calibre library.")
|
|
99
|
+
logger.error(f"Error importing Calibre library: {e}")
|
|
100
|
+
raise typer.Exit(code=1)
|
|
101
|
+
|
|
102
|
+
@app.command()
|
|
103
|
+
def import_ebooks(
|
|
104
|
+
ebooks_dir: str = typer.Argument(..., help="Path to the directory containing ebook files"),
|
|
105
|
+
output_dir: str = typer.Option(None, "--output-dir", "-o", help="Output directory for the ebk library (default: <ebooks_dir>_ebk)"),
|
|
106
|
+
ebook_formats: List[str] = typer.Option(
|
|
107
|
+
["pdf", "epub", "mobi", "azw3", "txt", "markdown", "html", "docx", "rtf", "djvu", "fb2", "cbz", "cbr"],
|
|
108
|
+
"--ebook-formats", "-f",
|
|
109
|
+
help="List of ebook formats to import"
|
|
110
|
+
)
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Recursively import a directory of ebooks. The metadata will be inferred from the file.
|
|
114
|
+
"""
|
|
115
|
+
output_dir = output_dir or f"{ebooks_dir.rstrip('/')}-ebk"
|
|
116
|
+
with Progress(console=console) as progress:
|
|
117
|
+
progress.add_task("[cyan]Importing raw ebooks...", total=None)
|
|
118
|
+
try:
|
|
119
|
+
ebooks.import_ebooks(ebooks_dir, output_dir, ebook_formats)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error importing raw ebooks: {e}")
|
|
122
|
+
raise typer.Exit(code=1)
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def export(
|
|
126
|
+
format: str = typer.Argument(..., help="Export format (e.g., 'hugo', 'zip')"),
|
|
127
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to export (contains `metadata.json` and ebook-related files)"),
|
|
128
|
+
destination: Optional[str] = typer.Argument(
|
|
129
|
+
None,
|
|
130
|
+
help="Destination path (Hugo site directory or Zip file path). If not provided for 'zip' format, defaults to '<lib_dir>.zip' or '<lib_dir> (j).zip' to avoid overwriting."
|
|
131
|
+
)
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
Export the ebk library to the specified format.
|
|
135
|
+
"""
|
|
136
|
+
format = format.lower()
|
|
137
|
+
lib_path = Path(lib_dir)
|
|
138
|
+
|
|
139
|
+
if not lib_path.exists() or not lib_path.is_dir():
|
|
140
|
+
console.print(f"[red]Library directory '{lib_dir}' does not exist or is not a directory.[/red]")
|
|
141
|
+
raise typer.Exit(code=1)
|
|
142
|
+
|
|
143
|
+
if format == "zip":
|
|
144
|
+
# Determine the destination filename
|
|
145
|
+
if destination:
|
|
146
|
+
dest_path = Path(destination)
|
|
147
|
+
if dest_path.exists():
|
|
148
|
+
console.print(f"[yellow]Destination '{destination}' already exists. Finding an available filename...[/yellow]")
|
|
149
|
+
dest_str = get_unique_filename(destination)
|
|
150
|
+
dest_path = Path(dest_str)
|
|
151
|
+
console.print(f"[green]Using '{dest_path.name}' as the destination.[/green]")
|
|
152
|
+
else:
|
|
153
|
+
dest_str = get_unique_filename(lib_dir + ".zip")
|
|
154
|
+
dest_path = Path(dest_str)
|
|
155
|
+
console.print(f"[bold]No destination provided[/bold]. Using default [bold green]{dest_path.name}.[/bold green]")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
with Progress(console=console) as progress:
|
|
159
|
+
task = progress.add_task("[cyan]Exporting to Zip...", total=None)
|
|
160
|
+
try:
|
|
161
|
+
export_zipfile(str(lib_path), str(dest_path))
|
|
162
|
+
console.print(f"[bold green]Exported library to '{dest_path}'.[/bold green]")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
progress.update(task, description="[red]Failed to export to Zip.")
|
|
165
|
+
logger.error(f"Error exporting to Zip: {e}")
|
|
166
|
+
console.print(f"[bold red]Failed to export to Zip: {e}[/bold red]")
|
|
167
|
+
raise typer.Exit(code=1)
|
|
168
|
+
|
|
169
|
+
elif format == "hugo":
|
|
170
|
+
if not destination:
|
|
171
|
+
console.print(f"[red]Destination directory is required for 'hugo' export format.[/red]")
|
|
172
|
+
raise typer.Exit(code=1)
|
|
173
|
+
|
|
174
|
+
dest_path = Path(destination)
|
|
175
|
+
if not dest_path.exists():
|
|
176
|
+
try:
|
|
177
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
console.print(f"[green]Created destination directory '{destination}'.[/green]")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
console.print(f"[red]Failed to create destination directory '{destination}': {e}[/red]")
|
|
181
|
+
raise typer.Exit(code=1)
|
|
182
|
+
elif not dest_path.is_dir():
|
|
183
|
+
console.print(f"[red]Destination '{destination}' exists and is not a directory.[/red]")
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
|
|
186
|
+
with Progress(console=console) as progress:
|
|
187
|
+
task = progress.add_task("[cyan]Exporting to Hugo...", total=None)
|
|
188
|
+
try:
|
|
189
|
+
export_hugo(str(lib_path), str(dest_path))
|
|
190
|
+
progress.update(task, description="[green]Exported to Hugo successfully!")
|
|
191
|
+
logger.info(f"Library exported to Hugo at {dest_path}")
|
|
192
|
+
console.print(f"[bold green]Exported library to Hugo directory '{dest_path}'.[/bold green]")
|
|
193
|
+
except Exception as e:
|
|
194
|
+
progress.update(task, description="[red]Failed to export to Hugo.")
|
|
195
|
+
logger.error(f"Error exporting to Hugo: {e}")
|
|
196
|
+
console.print(f"[bold red]Failed to export to Hugo: {e}[/bold red]")
|
|
197
|
+
raise typer.Exit(code=1)
|
|
198
|
+
|
|
199
|
+
else:
|
|
200
|
+
console.print(f"[red]Unsupported export format: '{format}'. Supported formats are 'zip' and 'hugo'.[/red]")
|
|
201
|
+
raise typer.Exit(code=1)
|
|
202
|
+
|
|
203
|
+
@app.command()
|
|
204
|
+
def show_index(
|
|
205
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to display"),
|
|
206
|
+
indices: list[int] = typer.Argument(..., help="Index of the entry to display"),
|
|
207
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON")
|
|
208
|
+
):
|
|
209
|
+
"""
|
|
210
|
+
Display the index of the ebk library.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
lib_dir (str): Path to the ebk library directory to display
|
|
214
|
+
index (int): Index of the entry to display
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
typer.Exit: If the library directory is invalid or the index is out of range
|
|
219
|
+
"""
|
|
220
|
+
metadata_list = load_library(lib_dir)
|
|
221
|
+
if not metadata_list:
|
|
222
|
+
console.print("[red]Failed to load library.[/red]")
|
|
223
|
+
raise typer.Exit(code=1)
|
|
224
|
+
|
|
225
|
+
total_books = len(metadata_list)
|
|
226
|
+
for index in indices:
|
|
227
|
+
if index < 0 or index >= total_books:
|
|
228
|
+
console.print(f"[red]Index {index} is out of range (0-{total_books - 1}).[/red]")
|
|
229
|
+
raise typer.Exit(code=1)
|
|
230
|
+
|
|
231
|
+
for index in indices:
|
|
232
|
+
entry = metadata_list[index]
|
|
233
|
+
if output_json:
|
|
234
|
+
console.print_json(json.dumps(entry, indent=2))
|
|
235
|
+
else:
|
|
236
|
+
# Create a table
|
|
237
|
+
table = Table(title="ebk Ebook Entry", show_lines=True)
|
|
238
|
+
|
|
239
|
+
# Add column headers dynamically based on JSON keys
|
|
240
|
+
columns = entry.keys() # Assuming all objects have the same structure
|
|
241
|
+
for column in columns:
|
|
242
|
+
table.add_column(column, justify="center", style="bold cyan")
|
|
243
|
+
|
|
244
|
+
# Add rows dynamically
|
|
245
|
+
for item in entry:
|
|
246
|
+
table.add_row(*(str(entry[col]) for col in columns))
|
|
247
|
+
|
|
248
|
+
# Print the table
|
|
249
|
+
console.print(table)
|
|
250
|
+
|
|
251
|
+
@app.command()
|
|
252
|
+
def about():
|
|
253
|
+
"""
|
|
254
|
+
Display information about ebk.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
console.print("[bold green]Welcome to ebk![/bold green]\n")
|
|
258
|
+
console.print("A lightweight and efficient tool for managing eBook metadata.\n")
|
|
259
|
+
|
|
260
|
+
console.print("[bold]Usage:[/bold]")
|
|
261
|
+
console.print(" - Run [bold]ebk --help[/bold] for general help.")
|
|
262
|
+
console.print(" - Use [bold]ebk <command> --help[/bold] for detailed command-specific help.\n")
|
|
263
|
+
|
|
264
|
+
console.print("[bold]More Information:[/bold]")
|
|
265
|
+
console.print(" 📖 GitHub: [link=https://github.com/queelius/ebk]github.com/queelius/ebk[/link]")
|
|
266
|
+
console.print(" 🌐 Website: [link=https://metafunctor.com]metafunctor.com[/link]")
|
|
267
|
+
console.print(" 📧 Contact: [link=mailto:lex@metafunctor.com]lex@metafunctor.com[/link]\n")
|
|
268
|
+
|
|
269
|
+
console.print("Developed by [bold]Alex Towell[/bold]. Enjoy using ebk! 🚀")
|
|
270
|
+
|
|
271
|
+
@app.command()
|
|
272
|
+
def merge(
|
|
273
|
+
operation: str = typer.Argument(..., help="Set-theoretic operation to apply (union, intersect, diff, symdiff)"),
|
|
274
|
+
output_dir: str = typer.Argument(..., help="Output directory for the merged ebk library"),
|
|
275
|
+
libs: List[str] = typer.Argument(..., help="Paths to the source ebk library directories", min=2)
|
|
276
|
+
):
|
|
277
|
+
"""
|
|
278
|
+
Merge multiple ebk libraries using set-theoretic operations.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
operation (str): Set-theoretic operation to apply (union, intersect, diff, symdiff)
|
|
282
|
+
output_dir (str): Output directory for the merged ebk library
|
|
283
|
+
libs (List[str]): Paths to the source ebk library directories
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
typer.Exit: If the library directory is invalid or the index is out of range
|
|
287
|
+
|
|
288
|
+
Output:
|
|
289
|
+
Merges the specified libraries using the set-theoretic operation and saves the result in the output directory.
|
|
290
|
+
"""
|
|
291
|
+
with Progress(console=console) as progress:
|
|
292
|
+
task = progress.add_task(f"[cyan]Merging libraries with operation '{operation}'...", total=None)
|
|
293
|
+
try:
|
|
294
|
+
merge_libraries(libs, output_dir, operation)
|
|
295
|
+
progress.update(task, description=f"[green]Libraries merged into {output_dir}")
|
|
296
|
+
console.print(f"[bold green]Libraries merged with operation '{operation}' into {output_dir}[/bold green]")
|
|
297
|
+
except Exception as e:
|
|
298
|
+
progress.update(task, description="[red]Failed to merge libraries.")
|
|
299
|
+
logger.error(f"Error merging libraries: {e}")
|
|
300
|
+
console.print(f"[bold red]Failed to merge libraries: {e}[/bold red]")
|
|
301
|
+
raise typer.Exit(code=1)
|
|
302
|
+
|
|
303
|
+
@app.command()
|
|
304
|
+
def stats(
|
|
305
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to get stats"),
|
|
306
|
+
keywords: List[str] = typer.Option(
|
|
307
|
+
["python", "data", "machine learning"],
|
|
308
|
+
"--keywords",
|
|
309
|
+
"-k",
|
|
310
|
+
help="Keywords to search for in titles"
|
|
311
|
+
)
|
|
312
|
+
):
|
|
313
|
+
"""
|
|
314
|
+
Get statistics about the ebk library.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
lib_dir (str): Path to the ebk library directory to get stats
|
|
318
|
+
keywords (List[str]): Keywords to search for in titles
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
typer.Exit: If the library directory is invalid
|
|
322
|
+
|
|
323
|
+
Output:
|
|
324
|
+
Prints the statistics about the library.
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
stats = get_library_statistics(lib_dir, keywords)
|
|
328
|
+
console.print_json(json.dumps(stats, indent=2))
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"Error generating statistics: {e}")
|
|
331
|
+
console.print(f"[bold red]Failed to generate statistics: {e}[/bold red]")
|
|
332
|
+
raise typer.Exit(code=1)
|
|
333
|
+
|
|
334
|
+
@app.command()
|
|
335
|
+
def list_indices(
|
|
336
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to list"),
|
|
337
|
+
indices: List[int] = typer.Argument(..., help="Indices of entries to list"),
|
|
338
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
339
|
+
detailed: bool = typer.Option(False, "--detailed", "-d", help="Show detailed information")):
|
|
340
|
+
"""
|
|
341
|
+
List the entries in the ebk library directory by index.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
lib_dir (str): Path to the ebk library directory to list
|
|
345
|
+
indices (List[int]): Indices of entries to list
|
|
346
|
+
output_json (bool): Output as JSON
|
|
347
|
+
detailed (bool): Show detailed information
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
typer.Exit: If the library directory is invalid or the index is out of range
|
|
351
|
+
|
|
352
|
+
Output:
|
|
353
|
+
Prints the list of entries in the library directory.
|
|
354
|
+
"""
|
|
355
|
+
lib_path = Path(lib_dir)
|
|
356
|
+
if not lib_path.exists():
|
|
357
|
+
console.print(f"[bold red]Error:[/bold red] The library directory '{lib_dir}' does not exist.")
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
if not lib_path.is_dir():
|
|
361
|
+
console.print(f"[bold red]Error:[/bold red] The path '{lib_dir}' is not a directory.")
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
metadata_list = load_library(lib_dir)
|
|
366
|
+
if output_json:
|
|
367
|
+
console.print_json(json.dumps(metadata_list, indent=2))
|
|
368
|
+
else:
|
|
369
|
+
enumerate_ebooks(metadata_list, lib_path, indices, detailed)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Error listing ebooks: {e}")
|
|
372
|
+
console.print(f"[bold red]Failed to list ebooks: {e}[/bold red]")
|
|
373
|
+
raise typer.Exit(code=1)
|
|
374
|
+
|
|
375
|
+
@app.command()
|
|
376
|
+
def list(
|
|
377
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to list"),
|
|
378
|
+
output_json: bool = typer.Option(False, "--json", "-j", help="Output as JSON")):
|
|
379
|
+
"""
|
|
380
|
+
List the entries in the ebk library directory.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
lib_dir (str): Path to the ebk library directory to list
|
|
384
|
+
output_json (bool): Output as JSON
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
typer.Exit: If the library directory is invalid
|
|
388
|
+
|
|
389
|
+
Output:
|
|
390
|
+
Prints the list of entries in the library directory.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
lib_path = Path(lib_dir)
|
|
394
|
+
|
|
395
|
+
if not lib_path.exists():
|
|
396
|
+
console.print(f"[bold red]Error:[/bold red] The library directory '{lib_dir}' does not exist.")
|
|
397
|
+
sys.exit(1)
|
|
398
|
+
|
|
399
|
+
if not lib_path.is_dir():
|
|
400
|
+
console.print(f"[bold red]Error:[/bold red] The path '{lib_dir}' is not a directory.")
|
|
401
|
+
sys.exit(1)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
metadata_list = load_library(lib_dir)
|
|
405
|
+
if output_json:
|
|
406
|
+
console.print_json(json.dumps(metadata_list, indent=2))
|
|
407
|
+
else:
|
|
408
|
+
enumerate_ebooks(metadata_list, lib_path)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.error(f"Error listing ebooks: {e}")
|
|
411
|
+
console.print(f"[bold red]Failed to list ebooks: {e}[/bold red]")
|
|
412
|
+
raise typer.Exit(code=1)
|
|
413
|
+
|
|
414
|
+
@app.command()
|
|
415
|
+
def add(
|
|
416
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
417
|
+
json_file: str = typer.Option(None, "--json", help="JSON file containing entry info to add"),
|
|
418
|
+
title: str = typer.Option(None, "--title", help="Title of the entry to add"),
|
|
419
|
+
creators: List[str] = typer.Option(None, "--creators", help="Creators of the entry to add"),
|
|
420
|
+
ebooks: List[str] = typer.Option(None, "--ebooks", help="Paths to the ebook files to add"),
|
|
421
|
+
cover: str = typer.Option(None, "--cover", help="Path to the cover image to add")
|
|
422
|
+
):
|
|
423
|
+
"""
|
|
424
|
+
Add entries to the ebk library.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
428
|
+
json_file (str): Path to a JSON file containing entry info to add
|
|
429
|
+
title (str): Title of the entry to add
|
|
430
|
+
creators (List[str]): Creators of the entry to add
|
|
431
|
+
ebooks (List[str]): Paths to the ebook files to add
|
|
432
|
+
cover (str): Path to the cover image to add
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
typer.Exit: If the library directory is invalid or the entry is invalid
|
|
436
|
+
|
|
437
|
+
Output:
|
|
438
|
+
Adds the specified entry to the library and updates the metadata file in-place.
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
metadata_list = load_library(lib_dir)
|
|
442
|
+
if not metadata_list:
|
|
443
|
+
console.print("[red]Failed to load library.[/red]")
|
|
444
|
+
raise typer.Exit(code=1)
|
|
445
|
+
console.print(f"Loaded [bold]{len(metadata_list)}[/bold] entries from [green]{lib_dir}[/green]")
|
|
446
|
+
|
|
447
|
+
if json_file:
|
|
448
|
+
with open(json_file, "r") as f:
|
|
449
|
+
new_entries = json.load(f)
|
|
450
|
+
for entry in new_entries:
|
|
451
|
+
add_unique_id(entry)
|
|
452
|
+
metadata_list.append(entry)
|
|
453
|
+
console.print(f"[green]Added {len(new_entries)} entries from {json_file}[/green]")
|
|
454
|
+
else:
|
|
455
|
+
if not title or not creators:
|
|
456
|
+
console.print("[red]Title and creators are required when not using a JSON file.[/red]")
|
|
457
|
+
raise typer.Exit(code=1)
|
|
458
|
+
new_entry = {
|
|
459
|
+
"title": title,
|
|
460
|
+
"creators": creators,
|
|
461
|
+
"file_paths": ebooks or [],
|
|
462
|
+
"cover_path": cover,
|
|
463
|
+
}
|
|
464
|
+
add_unique_id(new_entry)
|
|
465
|
+
metadata_list.append(new_entry)
|
|
466
|
+
console.print(f"Adding new entry: [bold]{new_entry['title']}[/bold]")
|
|
467
|
+
|
|
468
|
+
# Save updated metadata
|
|
469
|
+
with open(Path(lib_dir) / "metadata.json", "w") as f:
|
|
470
|
+
json.dump(metadata_list, f, indent=2)
|
|
471
|
+
|
|
472
|
+
# Use Rich's Progress to copy files
|
|
473
|
+
with Progress(console=console) as progress:
|
|
474
|
+
if ebooks:
|
|
475
|
+
task = progress.add_task("[cyan]Copying ebook files...", total=len(ebooks))
|
|
476
|
+
for ebook in ebooks:
|
|
477
|
+
shutil.copy(ebook, lib_dir)
|
|
478
|
+
progress.advance(task)
|
|
479
|
+
logger.debug(f"Copied ebook file: {ebook}")
|
|
480
|
+
if cover:
|
|
481
|
+
task = progress.add_task("[cyan]Copying cover image...", total=1)
|
|
482
|
+
shutil.copy(cover, lib_dir)
|
|
483
|
+
progress.advance(task)
|
|
484
|
+
logger.debug(f"Copied cover image: {cover}")
|
|
485
|
+
|
|
486
|
+
console.print(f"[bold green]Added new entry: {new_entry['title']}[/bold green]")
|
|
487
|
+
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error(f"Error adding entry: {e}")
|
|
490
|
+
console.print(f"[bold red]Failed to add entry: {e}[/bold red]")
|
|
491
|
+
raise typer.Exit(code=1)
|
|
492
|
+
|
|
493
|
+
@app.command()
|
|
494
|
+
def remove(
|
|
495
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
496
|
+
regex: str = typer.Argument(..., help="Regex search expression to remove entries"),
|
|
497
|
+
force: bool = typer.Option(False, "--force", help="Force removal without confirmation"),
|
|
498
|
+
apply_to: List[str] = typer.Option(
|
|
499
|
+
["title"],
|
|
500
|
+
"--apply-to",
|
|
501
|
+
help="Apply the removal to ebooks, covers, or all files",
|
|
502
|
+
show_default=True
|
|
503
|
+
)
|
|
504
|
+
):
|
|
505
|
+
"""
|
|
506
|
+
Remove entries from the ebk library.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
510
|
+
regex (str): Regex search expression to remove entries
|
|
511
|
+
force (bool): Force removal without confirmation
|
|
512
|
+
apply_to (List[str]): Apply the removal to ebooks, covers, or all files
|
|
513
|
+
|
|
514
|
+
Raises:
|
|
515
|
+
typer.Exit: If the library directory is invalid or the index is out of range
|
|
516
|
+
|
|
517
|
+
Output:
|
|
518
|
+
Removed entries from the library directory and associated files in-place.
|
|
519
|
+
"""
|
|
520
|
+
try:
|
|
521
|
+
metadata_list = load_library(lib_dir)
|
|
522
|
+
if not metadata_list:
|
|
523
|
+
console.print("[red]Failed to load library.[/red]")
|
|
524
|
+
raise typer.Exit(code=1)
|
|
525
|
+
console.print(f"Loaded [bold]{len(metadata_list)}[/bold] entries from [green]{lib_dir}[/green]")
|
|
526
|
+
|
|
527
|
+
rem_list = []
|
|
528
|
+
if "title" in apply_to:
|
|
529
|
+
rem_list += [entry for entry in metadata_list if re.search(regex, entry.get("title", ""))]
|
|
530
|
+
if "creators" in apply_to:
|
|
531
|
+
rem_list += [entry for entry in metadata_list if any(re.search(regex, creator) for creator in entry.get("creators", []))]
|
|
532
|
+
if "identifiers" in apply_to:
|
|
533
|
+
rem_list += [entry for entry in metadata_list if any(re.search(regex, identifier) for identifier in entry.get("identifiers", {}).values())]
|
|
534
|
+
|
|
535
|
+
# Remove duplicates based on unique_id
|
|
536
|
+
rem_list = list({entry['unique_id']: entry for entry in rem_list}.values())
|
|
537
|
+
|
|
538
|
+
if not rem_list:
|
|
539
|
+
console.print("[yellow]No matching entries found for removal.[/yellow]")
|
|
540
|
+
raise typer.Exit()
|
|
541
|
+
|
|
542
|
+
for entry in rem_list:
|
|
543
|
+
if not force:
|
|
544
|
+
console.print(f"Remove entry: [bold]{entry.get('title', 'No Title')}[/bold]")
|
|
545
|
+
confirm = Confirm.ask("Confirm removal?")
|
|
546
|
+
if not confirm:
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
metadata_list.remove(entry)
|
|
550
|
+
console.print(f"[green]Removed entry: {entry.get('title', 'No Title')}[/green]")
|
|
551
|
+
logger.debug(f"Removed entry: {entry}")
|
|
552
|
+
|
|
553
|
+
with open(Path(lib_dir) / "metadata.json", "w") as f:
|
|
554
|
+
json.dump(metadata_list, f, indent=2)
|
|
555
|
+
|
|
556
|
+
console.print(f"[bold green]Removed {len(rem_list)} entries from {lib_dir}[/bold green]")
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
logger.error(f"Error removing entries: {e}")
|
|
560
|
+
console.print(f"[bold red]Failed to remove entries: {e}[/bold red]")
|
|
561
|
+
raise typer.Exit(code=1)
|
|
562
|
+
|
|
563
|
+
@app.command()
|
|
564
|
+
def remove_id(lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
565
|
+
unique_id: str = typer.Argument(..., help="Unique ID of the entry to remove")):
|
|
566
|
+
"""
|
|
567
|
+
Remove an entry from the ebk library by unique ID.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
571
|
+
unique_id (str): Unique ID of the entry to remove
|
|
572
|
+
"""
|
|
573
|
+
id = get_index_by_unique_id(lib_dir, unique_id)
|
|
574
|
+
remove_index(lib_dir, [id])
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@app.command()
|
|
578
|
+
def update_index(
|
|
579
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
580
|
+
index: int = typer.Argument(..., help="Index of the entry to update"),
|
|
581
|
+
json_file: str = typer.Option(None, "--json", help="JSON file containing updated entry info"),
|
|
582
|
+
title: str = typer.Option(None, "--title", help="New title for the entry"),
|
|
583
|
+
creators: List[str] = typer.Option(None, "--creators", help="New creators for the entry"),
|
|
584
|
+
ebooks: List[str] = typer.Option(None, "--ebooks", help="Paths to the new ebook files"),
|
|
585
|
+
cover: str = typer.Option(None, "--cover", help="Path to the new cover image")
|
|
586
|
+
):
|
|
587
|
+
"""
|
|
588
|
+
Update an entry in the ebk library by index.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
592
|
+
index (int): Index of the entry to update
|
|
593
|
+
json_file (str): Path to a JSON file containing updated entry info
|
|
594
|
+
title (str): New title for the entry
|
|
595
|
+
creators (List[str]): New creators for the entry
|
|
596
|
+
ebooks (List[str]): Paths to the new ebook files
|
|
597
|
+
cover (str): Path to the new cover image
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
metadata_list = load_library(lib_dir)
|
|
602
|
+
if not metadata_list:
|
|
603
|
+
console.print("[red]Failed to load library.[/red]")
|
|
604
|
+
raise typer.Exit(code=1)
|
|
605
|
+
console.print(f"Loaded [bold]{len(metadata_list)}[/bold] entries from [green]{lib_dir}[/green]")
|
|
606
|
+
|
|
607
|
+
if json_file:
|
|
608
|
+
with open(json_file, "r") as f:
|
|
609
|
+
updated_entry = json.load(f)
|
|
610
|
+
else:
|
|
611
|
+
updated_entry = metadata_list[index]
|
|
612
|
+
if title:
|
|
613
|
+
updated_entry["title"] = title
|
|
614
|
+
if creators:
|
|
615
|
+
updated_entry["creators"] = creators
|
|
616
|
+
if ebooks:
|
|
617
|
+
updated_entry["file_paths"] = ebooks
|
|
618
|
+
if cover:
|
|
619
|
+
updated_entry["cover_path"] = cover
|
|
620
|
+
|
|
621
|
+
metadata_list[index] = updated_entry
|
|
622
|
+
with open(Path(lib_dir) / "metadata.json", "w") as f:
|
|
623
|
+
json.dump(metadata_list, f, indent=2)
|
|
624
|
+
|
|
625
|
+
console.print(f"[bold green]Updated entry at index {index} in {lib_dir}[/bold green]")
|
|
626
|
+
except Exception as e:
|
|
627
|
+
logger.error(f"Error updating entry by index: {e}")
|
|
628
|
+
console.print(f"[bold red]Failed to update entry by index: {e}[/bold red]")
|
|
629
|
+
raise typer.Exit(code=1)
|
|
630
|
+
|
|
631
|
+
@app.command()
|
|
632
|
+
def update_id(
|
|
633
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
634
|
+
unique_id: str = typer.Argument(..., help="Unique ID of the entry to update"),
|
|
635
|
+
json_file: str = typer.Option(None, "--json", help="JSON file containing updated entry info"),
|
|
636
|
+
title: str = typer.Option(None, "--title", help="New title for the entry"),
|
|
637
|
+
creators: List[str] = typer.Option(None, "--creators", help="New creators for the entry"),
|
|
638
|
+
ebooks: List[str] = typer.Option(None, "--ebooks", help="Paths to the new ebook files"),
|
|
639
|
+
cover: str = typer.Option(None, "--cover", help="Path to the new cover image")
|
|
640
|
+
):
|
|
641
|
+
"""
|
|
642
|
+
Update an entry in the ebk library by unique id.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
646
|
+
id: str: Unique ID of the entry to update
|
|
647
|
+
json_file (str): Path to a JSON file containing updated entry info
|
|
648
|
+
title (str): New title for the entry
|
|
649
|
+
creators (List[str]): New creators for the entry
|
|
650
|
+
ebooks (List[str]): Paths to the new ebook files
|
|
651
|
+
cover (str): Path to the new cover image
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
id = lambda entry: entry.get("unique_id")
|
|
655
|
+
index = get_index_by_unique_id(lib_dir, id)
|
|
656
|
+
if index == -1:
|
|
657
|
+
console.print(f"[red]Entry with unique ID [bold]{unique_id}[/bold] not found.[/red]")
|
|
658
|
+
raise typer.Exit(code=1)
|
|
659
|
+
|
|
660
|
+
update_index(lib_dir, index, json_file, title, creators, ebooks, cover)
|
|
661
|
+
|
|
662
|
+
@app.command()
|
|
663
|
+
def remove_index(
|
|
664
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to modify"),
|
|
665
|
+
indices: List[int] = typer.Argument(..., help="Indices of entries to remove")
|
|
666
|
+
):
|
|
667
|
+
"""
|
|
668
|
+
Remove entries from the ebk library by index.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
lib_dir (str): Path to the ebk library directory to modify
|
|
672
|
+
indices (List[int]): Indices of entries to remove
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
typer.Exit: If the library directory is invalid or the index is out of range
|
|
676
|
+
|
|
677
|
+
Output:
|
|
678
|
+
Removes the specified entries from the library and updates the metadata file in-place.
|
|
679
|
+
"""
|
|
680
|
+
try:
|
|
681
|
+
metadata_list = load_library(lib_dir)
|
|
682
|
+
if not metadata_list:
|
|
683
|
+
console.print("[red]Failed to load library.[/red]")
|
|
684
|
+
raise typer.Exit(code=1)
|
|
685
|
+
console.print(f"Loaded [bold]{len(metadata_list)}[/bold] entries from [green]{lib_dir}[/green]")
|
|
686
|
+
|
|
687
|
+
indices = sorted(indices, reverse=True)
|
|
688
|
+
with Progress(console=console) as progress:
|
|
689
|
+
task = progress.add_task("[cyan]Removing entries...", total=len(indices))
|
|
690
|
+
removed_count = 0
|
|
691
|
+
for i in indices:
|
|
692
|
+
if 0 <= i < len(metadata_list):
|
|
693
|
+
del metadata_list[i]
|
|
694
|
+
progress.advance(task)
|
|
695
|
+
logger.debug(f"Removed entry at index {i}")
|
|
696
|
+
removed_count += 1
|
|
697
|
+
else:
|
|
698
|
+
console.print(f"[yellow]Index {i} is out of range.[/yellow]")
|
|
699
|
+
|
|
700
|
+
with open(Path(lib_dir) / "metadata.json", "w") as f:
|
|
701
|
+
json.dump(metadata_list, f, indent=2)
|
|
702
|
+
|
|
703
|
+
console.print(f"[bold green]Removed {removed_count} entries from {lib_dir}[/bold green]")
|
|
704
|
+
|
|
705
|
+
except Exception as e:
|
|
706
|
+
logger.error(f"Error removing entries by index: {e}")
|
|
707
|
+
console.print(f"[bold red]Failed to remove entries: {e}[/bold red]")
|
|
708
|
+
raise typer.Exit(code=1)
|
|
709
|
+
|
|
710
|
+
@app.command()
|
|
711
|
+
def dash(
|
|
712
|
+
port: int = typer.Option(8501, "--port", help="Port to run the Streamlit app (default: 8501)")
|
|
713
|
+
):
|
|
714
|
+
"""
|
|
715
|
+
Launch the Streamlit dashboard.
|
|
716
|
+
"""
|
|
717
|
+
try:
|
|
718
|
+
app_path = Path(__file__).parent / 'streamlit' / 'app.py'
|
|
719
|
+
|
|
720
|
+
if not app_path.exists():
|
|
721
|
+
console.print(f"[bold red]Streamlit app not found at {app_path}[/bold red]")
|
|
722
|
+
raise typer.Exit(code=1)
|
|
723
|
+
|
|
724
|
+
subprocess.run(
|
|
725
|
+
['streamlit', 'run', str(app_path), "--server.port", str(port)],
|
|
726
|
+
check=True
|
|
727
|
+
)
|
|
728
|
+
logger.info(f"Streamlit dashboard launched on port {port}")
|
|
729
|
+
except FileNotFoundError:
|
|
730
|
+
console.print("[bold red]Error:[/bold red] Streamlit is not installed. Please install it with `pip install streamlit`.")
|
|
731
|
+
raise typer.Exit(code=1)
|
|
732
|
+
except subprocess.CalledProcessError as e:
|
|
733
|
+
logger.error(f"Error launching Streamlit dashboard: {e}")
|
|
734
|
+
console.print(f"[bold red]Failed to launch Streamlit dashboard: {e}[/bold red]")
|
|
735
|
+
raise typer.Exit(code=e.returncode)
|
|
736
|
+
except Exception as e:
|
|
737
|
+
logger.error(f"Unexpected error launching Streamlit dashboard: {e}")
|
|
738
|
+
console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
|
|
739
|
+
raise typer.Exit(code=1)
|
|
740
|
+
|
|
741
|
+
@app.command()
|
|
742
|
+
def regex(
|
|
743
|
+
query: str = typer.Argument(..., help="Regex search expression."),
|
|
744
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to search"),
|
|
745
|
+
json_out: bool = typer.Option(False, "--json", "-j", help="Output search results as JSON"),
|
|
746
|
+
fields: List[str] = typer.Option(["title"], "--fields", "-f", help="Fields to search in (default: title)")):
|
|
747
|
+
"""
|
|
748
|
+
Search entries in an ebk library using a regex expression on specified fields.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
query (str): Regex search expression
|
|
752
|
+
lib_dir (str): Path to the ebk library directory to search
|
|
753
|
+
json_out (bool): Output search results as JSON
|
|
754
|
+
fields (List[str]): Fields to search in (default: title)
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Search results as a table or JSON
|
|
758
|
+
"""
|
|
759
|
+
try:
|
|
760
|
+
results = search_regex(lib_dir, query, fields)
|
|
761
|
+
if json_out:
|
|
762
|
+
console.print_json(json.dumps(results, indent=2))
|
|
763
|
+
else:
|
|
764
|
+
enumerate_ebooks(results, Path(lib_dir))
|
|
765
|
+
except Exception as e:
|
|
766
|
+
logger.error(f"Error searching library with regex: {e}")
|
|
767
|
+
console.print(f"[bold red]Failed to search library with regex: {e}[/bold red]")
|
|
768
|
+
raise typer.Exit(code=1)
|
|
769
|
+
|
|
770
|
+
@app.command()
|
|
771
|
+
def jmespath(
|
|
772
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to query"),
|
|
773
|
+
query: str = typer.Argument(..., help="JMESPath query string to search in the library"),
|
|
774
|
+
json_out: bool = typer.Option(False, "--json", "-j", help="Output search results as JSON")):
|
|
775
|
+
"""
|
|
776
|
+
Query the ebk library using JMESPath.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
lib_dir (str): Path to the ebk library directory to query
|
|
780
|
+
query (str): JMESPath query string to search in the library
|
|
781
|
+
output_json (bool): Output search results as JSON
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
JMEPSath query results, either pretty printed or as JSON.
|
|
785
|
+
"""
|
|
786
|
+
try:
|
|
787
|
+
results = search_jmes(lib_dir, query)
|
|
788
|
+
if json_out:
|
|
789
|
+
console.print_json(json.dumps(results, indent=2))
|
|
790
|
+
else:
|
|
791
|
+
print_json_as_table(results)
|
|
792
|
+
except Exception as e:
|
|
793
|
+
logger.error(f"Error querying library with JMESPath: {e}")
|
|
794
|
+
console.print(f"[bold red]Failed to query library with JMESPath: {e}[/bold red]")
|
|
795
|
+
raise typer.Exit(code=1)
|
|
796
|
+
|
|
797
|
+
@app.command()
|
|
798
|
+
def llm(
|
|
799
|
+
lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to query"),
|
|
800
|
+
query: str = typer.Argument(..., help="Query string to search in the library")
|
|
801
|
+
):
|
|
802
|
+
"""
|
|
803
|
+
Query the ebk library using the LLM (Large Language Model) endpoint.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
lib_dir (str): Path to the ebk library directory to query
|
|
807
|
+
query (str): Natural language query to interact with the library
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
LLM query results
|
|
811
|
+
"""
|
|
812
|
+
try:
|
|
813
|
+
query_llm(lib_dir, query)
|
|
814
|
+
except Exception as e:
|
|
815
|
+
logger.error(f"Error querying library with LLM: {e}")
|
|
816
|
+
console.print(f"[bold red]Failed to query library with LLM: {e}[/bold red]")
|
|
817
|
+
raise typer.Exit(code=1)
|
|
818
|
+
|
|
819
|
+
@app.command()
|
|
820
|
+
def visualize(lib_dir: str = typer.Argument(..., help="Path to the ebk library directory to generate a complex network"),
|
|
821
|
+
output_file: str = typer.Option(None, "--output-file", "-o", help="Output file for the graph visualization"),
|
|
822
|
+
pretty_stats: bool = typer.Option(True, "--stats", "-s", help="Pretty print complex network statistics"),
|
|
823
|
+
json_stats: bool = typer.Option(False, "--json-stats", "-j", help="Output complex network statistics as JSON")):
|
|
824
|
+
|
|
825
|
+
"""
|
|
826
|
+
Generate a complex network visualization from the ebk library.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
lib_dir (str): Path to the ebk library directory to generate a complex network
|
|
830
|
+
output_file (str): Output file for the graph visualization
|
|
831
|
+
pretty_stats (bool): Pretty print complex network statistics
|
|
832
|
+
json_stats (bool): Output complex network statistics as JSON
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
Complex network visualization and statistics
|
|
836
|
+
"""
|
|
837
|
+
|
|
838
|
+
if output_file and not output_file.endswith(('.html', '.png', '.json')):
|
|
839
|
+
logging.error("Output file must be either an HTML file, PNG file, or JSON file.")
|
|
840
|
+
sys.exit(1)
|
|
841
|
+
|
|
842
|
+
if not os.path.isdir(lib_dir):
|
|
843
|
+
logging.error(f"The specified library directory '{lib_dir}' does not exist or is not a directory.")
|
|
844
|
+
sys.exit(1)
|
|
845
|
+
|
|
846
|
+
metadata_list = load_library(lib_dir)
|
|
847
|
+
if not metadata_list:
|
|
848
|
+
logging.error(f"No metadata found in the library directory '{lib_dir}'.")
|
|
849
|
+
sys.exit(1)
|
|
850
|
+
|
|
851
|
+
net = visualize.generate_complex_network(metadata_list)
|
|
852
|
+
|
|
853
|
+
if output_file:
|
|
854
|
+
if output_file.endswith('.html'):
|
|
855
|
+
# Interactive visualization with pyvis
|
|
856
|
+
visualize.as_pyvis(net, output_file)
|
|
857
|
+
elif output_file.endswith('.json'):
|
|
858
|
+
net_json = nx.node_link_data(net) # Convert to node-link format
|
|
859
|
+
console.print(JSON(json.dumps(net_json, indent=2)))
|
|
860
|
+
elif output_file.endswith('.png'):
|
|
861
|
+
visualize.as_png(net, output_file)
|
|
862
|
+
|
|
863
|
+
if pretty_stats:
|
|
864
|
+
console.print(nx.info(net))
|
|
865
|
+
# console.print(f"[bold green]Complex network generated successfully![/bold green]")
|
|
866
|
+
# console.print(f"Nodes: {net.number_of_nodes()}")
|
|
867
|
+
# console.print(f"Edges: {net.number_of_edges()}")
|
|
868
|
+
# console.print(f"Average Degree: {np.mean([d for n, d in net.degree()])}")
|
|
869
|
+
# console.print(f"Average Clustering Coefficient: {nx.average_clustering(net)}")
|
|
870
|
+
# console.print(f"Transitivity: {nx.transitivity(net)}")
|
|
871
|
+
# console.print(f"Average Shortest Path Length: {nx.average_shortest_path_length(net)}")
|
|
872
|
+
# console.print(f"Global Clustering Coefficient: {nx.transitivity(net)}")
|
|
873
|
+
# console.print(f"Global Efficiency: {nx.global_efficiency(net)}")
|
|
874
|
+
# console.print(f"Modularity: {community.modularity(community.best_partition(net), net)}")
|
|
875
|
+
if json_stats:
|
|
876
|
+
console.print_json(json.dumps(nx.info(net), indent=2))
|
|
877
|
+
|
|
878
|
+
if __name__ == "__main__":
|
|
879
|
+
app()
|