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
@@ -0,0 +1,479 @@
1
+ """
2
+ Export library as a navigable directory structure using symlinks to represent tag hierarchies.
3
+
4
+ This module creates a filesystem view of the library where:
5
+ - Tags are represented as directories in a hierarchy
6
+ - Books appear in all relevant tag directories via symlinks
7
+ - The DAG structure of tags is preserved through the directory tree
8
+ """
9
+
10
+ import os
11
+ import json
12
+ import shutil
13
+ from pathlib import Path
14
+ from typing import Dict, List
15
+ import re
16
+ from collections import defaultdict
17
+
18
+
19
+ class SymlinkDAGExporter:
20
+ """Creates a navigable directory structure using symlinks to represent tag hierarchies."""
21
+
22
+ def __init__(self):
23
+ self.tag_separator = "/" # Separator for hierarchical tags
24
+ self.books_dir_name = "_books" # Directory to store actual book files
25
+
26
+ def export(self, lib_dir: str, output_dir: str,
27
+ tag_field: str = "subjects",
28
+ include_files: bool = False, # Changed default to False
29
+ create_index: bool = True,
30
+ flatten: bool = False,
31
+ min_books: int = 0):
32
+ """
33
+ Export library as symlink-based directory structure.
34
+
35
+ Args:
36
+ lib_dir: Path to the ebk library
37
+ output_dir: Output directory for the symlink structure
38
+ tag_field: Field to use for tags (default: "subjects")
39
+ include_files: Whether to copy actual ebook files (default: False)
40
+ create_index: Whether to create index.html files in directories
41
+ flatten: Whether to create direct symlinks to files instead of _books structure
42
+ min_books: Minimum books per tag folder; smaller folders go to _misc (default: 0)
43
+ """
44
+ lib_path = Path(lib_dir)
45
+ output_path = Path(output_dir)
46
+
47
+ # Load metadata
48
+ metadata_file = lib_path / "metadata.json"
49
+ with open(metadata_file, "r") as f:
50
+ entries = json.load(f)
51
+
52
+ # Create output directory
53
+ output_path.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Create books directory for actual files (unless flattening)
56
+ if not flatten:
57
+ books_path = output_path / self.books_dir_name
58
+ books_path.mkdir(exist_ok=True)
59
+
60
+ # Process each entry
61
+ entry_paths = {} # Map entry ID to its path in _books
62
+ tag_entries = defaultdict(list) # Map tag to list of entries
63
+
64
+ for i, entry in enumerate(entries):
65
+ entry_id = entry.get("unique_id", f"entry_{i}")
66
+
67
+ if not flatten:
68
+ # Create entry directory in _books
69
+ entry_dir = books_path / self._sanitize_filename(entry_id)
70
+ entry_dir.mkdir(exist_ok=True)
71
+ entry_paths[entry_id] = entry_dir
72
+
73
+ # Save metadata
74
+ with open(entry_dir / "metadata.json", "w") as f:
75
+ json.dump(entry, f, indent=2)
76
+
77
+ # Handle files - either copy or symlink
78
+ if include_files:
79
+ self._copy_entry_files(entry, lib_path, entry_dir)
80
+ else:
81
+ # Create symlinks to original files
82
+ self._symlink_entry_files(entry, lib_path, entry_dir)
83
+ else:
84
+ # For flatten mode, store original file paths
85
+ entry_paths[entry_id] = entry.get("file_paths", [])
86
+
87
+ # Create a readable symlink name
88
+ title = entry.get("title", "Unknown Title")
89
+ creators = entry.get("creators", [])
90
+ if creators:
91
+ readable_name = f"{self._sanitize_filename(title)} - {self._sanitize_filename(creators[0])}"
92
+ else:
93
+ readable_name = self._sanitize_filename(title)
94
+
95
+ # Store readable name for later use
96
+ entry["_readable_name"] = readable_name
97
+ entry["_entry_id"] = entry_id
98
+
99
+ # Extract tags and build hierarchy
100
+ tags = entry.get(tag_field, [])
101
+ if isinstance(tags, str):
102
+ tags = [tags]
103
+
104
+ for tag in tags:
105
+ # Add to this tag and all parent tags
106
+ tag_parts = tag.split(self.tag_separator)
107
+ for i in range(len(tag_parts)):
108
+ parent_tag = self.tag_separator.join(tag_parts[:i+1])
109
+ tag_entries[parent_tag].append(entry)
110
+
111
+ # Consolidate small tag folders if min_books is set
112
+ if min_books > 0:
113
+ tag_entries = self._consolidate_small_tags(tag_entries, min_books)
114
+
115
+ # Create tag directory structure with symlinks
116
+ self._create_tag_structure(output_path, tag_entries, entry_paths, flatten, lib_path)
117
+
118
+ # Create root index if requested
119
+ if create_index:
120
+ self._create_index_files(output_path, tag_entries, entries)
121
+
122
+ # Create a README
123
+ self._create_readme(output_path, len(entries), len(tag_entries))
124
+
125
+ def _consolidate_small_tags(self, tag_entries: Dict[str, List[Dict]],
126
+ min_books: int) -> Dict[str, List[Dict]]:
127
+ """Consolidate tags with fewer than min_books into a _misc folder."""
128
+ consolidated = defaultdict(list)
129
+ misc_entries = []
130
+
131
+ for tag, entries in tag_entries.items():
132
+ # Get unique entries for this tag
133
+ seen_ids = set()
134
+ unique_entries = []
135
+ for entry in entries:
136
+ entry_id = entry.get("_entry_id", entry.get("unique_id"))
137
+ if entry_id not in seen_ids:
138
+ seen_ids.add(entry_id)
139
+ unique_entries.append(entry)
140
+
141
+ # Check if this tag has enough unique books
142
+ if len(unique_entries) < min_books:
143
+ # Check if it's a leaf tag (no children with enough books)
144
+ tag_prefix = tag + self.tag_separator
145
+ has_large_children = any(
146
+ other_tag.startswith(tag_prefix) and
147
+ len(set(e.get("_entry_id", e.get("unique_id")) for e in tag_entries[other_tag])) >= min_books
148
+ for other_tag in tag_entries.keys()
149
+ )
150
+
151
+ if not has_large_children:
152
+ # Add to misc folder with tag prefix
153
+ for entry in unique_entries:
154
+ misc_entry = entry.copy()
155
+ # Store original tag for display in misc folder
156
+ misc_entry["_original_tag"] = tag
157
+ misc_entries.append(misc_entry)
158
+ else:
159
+ # Keep it as is because it has large children
160
+ consolidated[tag] = entries
161
+ else:
162
+ # Keep tags with enough books
163
+ consolidated[tag] = entries
164
+
165
+ # Add misc entries if any
166
+ if misc_entries:
167
+ consolidated["_misc"] = misc_entries
168
+
169
+ return dict(consolidated)
170
+
171
+ def _sanitize_filename(self, name: str) -> str:
172
+ """Sanitize a string to be safe as a filename."""
173
+ # Replace problematic characters
174
+ name = re.sub(r'[<>:"/\\|?*]', '-', str(name))
175
+ # Remove leading/trailing spaces and dots
176
+ name = name.strip('. ')
177
+ # Limit length (being more conservative)
178
+ if len(name) > 150:
179
+ name = name[:147] + "..."
180
+ return name or "unnamed"
181
+
182
+ def _copy_entry_files(self, entry: Dict, lib_path: Path, entry_dir: Path):
183
+ """Copy ebook and cover files for an entry."""
184
+ # Copy ebook files
185
+ for file_path in entry.get("file_paths", []):
186
+ src_file = lib_path / file_path
187
+ if src_file.exists():
188
+ dest_file = entry_dir / src_file.name
189
+ shutil.copy2(src_file, dest_file)
190
+
191
+ # Copy cover file
192
+ cover_path = entry.get("cover_path")
193
+ if cover_path:
194
+ src_cover = lib_path / cover_path
195
+ if src_cover.exists():
196
+ dest_cover = entry_dir / src_cover.name
197
+ shutil.copy2(src_cover, dest_cover)
198
+
199
+ def _symlink_entry_files(self, entry: Dict, lib_path: Path, entry_dir: Path):
200
+ """Create symlinks to ebook and cover files for an entry."""
201
+ # Symlink ebook files
202
+ for file_path in entry.get("file_paths", []):
203
+ src_file = lib_path / file_path
204
+ if src_file.exists():
205
+ # Get absolute path of source file
206
+ abs_src = src_file.resolve()
207
+ dest_link = entry_dir / src_file.name
208
+
209
+ # Remove existing symlink if it exists
210
+ if dest_link.exists() or dest_link.is_symlink():
211
+ dest_link.unlink()
212
+
213
+ try:
214
+ # Create symlink using absolute path
215
+ dest_link.symlink_to(abs_src)
216
+ except OSError as e:
217
+ print(f"Warning: Could not create symlink for '{file_path}': {e}")
218
+
219
+ # Symlink cover file
220
+ cover_path = entry.get("cover_path")
221
+ if cover_path:
222
+ src_cover = lib_path / cover_path
223
+ if src_cover.exists():
224
+ # Get absolute path of source cover
225
+ abs_cover = src_cover.resolve()
226
+ dest_link = entry_dir / src_cover.name
227
+
228
+ if dest_link.exists() or dest_link.is_symlink():
229
+ dest_link.unlink()
230
+
231
+ try:
232
+ # Create symlink using absolute path
233
+ dest_link.symlink_to(abs_cover)
234
+ except OSError as e:
235
+ print(f"Warning: Could not create symlink for cover '{cover_path}': {e}")
236
+
237
+ def _create_tag_structure(self, output_path: Path,
238
+ tag_entries: Dict[str, List[Dict]],
239
+ entry_paths: Dict[str, Path],
240
+ flatten: bool = False,
241
+ lib_path: Path = None):
242
+ """Create the hierarchical tag directory structure with symlinks."""
243
+ # Sort tags to ensure parents are created before children
244
+ sorted_tags = sorted(tag_entries.keys())
245
+
246
+ for tag in sorted_tags:
247
+ # Create tag directory path
248
+ tag_parts = tag.split(self.tag_separator)
249
+ tag_dir = output_path
250
+ for part in tag_parts:
251
+ tag_dir = tag_dir / self._sanitize_filename(part)
252
+ tag_dir.mkdir(parents=True, exist_ok=True)
253
+
254
+ # Get unique entries for this tag (avoid duplicates)
255
+ seen_ids = set()
256
+ unique_entries = []
257
+ for entry in tag_entries[tag]:
258
+ entry_id = entry["_entry_id"]
259
+ if entry_id not in seen_ids:
260
+ seen_ids.add(entry_id)
261
+ unique_entries.append(entry)
262
+
263
+ # Create symlinks to entries
264
+ for entry in unique_entries:
265
+ entry_id = entry["_entry_id"]
266
+ readable_name = entry["_readable_name"]
267
+
268
+ # For _misc folder, include original tag in the name
269
+ if tag == "_misc" and "_original_tag" in entry:
270
+ original_tag = entry["_original_tag"]
271
+ # Shorten the tag to avoid filesystem limits
272
+ tag_parts = original_tag.split(self.tag_separator)
273
+ if len(tag_parts) > 2:
274
+ # Use only the last two parts of hierarchical tags
275
+ short_tag = self.tag_separator.join(tag_parts[-2:])
276
+ else:
277
+ short_tag = original_tag
278
+
279
+ # Further limit tag length
280
+ if len(short_tag) > 50:
281
+ short_tag = short_tag[:47] + "..."
282
+
283
+ tag_prefix = f"[{short_tag.replace(self.tag_separator, '-')}] "
284
+
285
+ # Ensure the total name isn't too long
286
+ max_name_length = 200 # Safe limit for most filesystems
287
+ if len(tag_prefix + readable_name) > max_name_length:
288
+ # Truncate the readable name to fit
289
+ available_length = max_name_length - len(tag_prefix) - 3
290
+ readable_name = readable_name[:available_length] + "..."
291
+
292
+ if not flatten:
293
+ # Path to actual entry in _books
294
+ target_path = Path(*[".."] * len(tag_parts)) / self.books_dir_name / self._sanitize_filename(entry_id)
295
+ # Create symlink
296
+ symlink_path = tag_dir / readable_name
297
+ else:
298
+ # For flatten mode, create direct symlinks to original files
299
+ file_paths = entry_paths.get(entry_id, [])
300
+ if file_paths:
301
+ # Use the first file path (usually the main ebook file)
302
+ original_file = file_paths[0]
303
+ # Get absolute path to the original file
304
+ abs_file_path = (lib_path / original_file).resolve()
305
+ # Use original filename as symlink name
306
+ symlink_path = tag_dir / Path(original_file).name
307
+ target_path = abs_file_path
308
+ else:
309
+ continue # Skip if no files
310
+
311
+ # Remove existing symlink if it exists
312
+ if symlink_path.exists() or symlink_path.is_symlink():
313
+ symlink_path.unlink()
314
+
315
+ # Create relative symlink
316
+ try:
317
+ symlink_path.symlink_to(target_path)
318
+ except OSError as e:
319
+ # On Windows, creating symlinks might require admin privileges
320
+ print(f"Warning: Could not create symlink for '{readable_name}': {e}")
321
+
322
+ def _create_index_files(self, output_path: Path,
323
+ tag_entries: Dict[str, List[Dict]],
324
+ all_entries: List[Dict]):
325
+ """Create index.html files in each directory for web browsing."""
326
+ # Create root index with tag counts
327
+ root_child_tags = {}
328
+ for tag, entries in tag_entries.items():
329
+ if self.tag_separator not in tag: # Top-level tags only
330
+ unique_count = len(set(e.get("_entry_id", e.get("unique_id"))
331
+ for e in entries))
332
+ root_child_tags[tag] = unique_count
333
+ self._write_index_file(output_path, "Library Root", all_entries, root_child_tags, output_path)
334
+
335
+ # Create index for each tag directory
336
+ for tag, entries in tag_entries.items():
337
+ tag_parts = tag.split(self.tag_separator)
338
+ tag_dir = output_path
339
+ for part in tag_parts:
340
+ tag_dir = tag_dir / self._sanitize_filename(part)
341
+
342
+ # Get child tags with counts
343
+ child_tags = {}
344
+ tag_prefix = tag + self.tag_separator
345
+ for other_tag, other_entries in tag_entries.items():
346
+ if other_tag.startswith(tag_prefix) and other_tag != tag:
347
+ # Check if it's a direct child
348
+ remaining = other_tag[len(tag_prefix):]
349
+ if self.tag_separator not in remaining:
350
+ # Count unique entries for this tag
351
+ unique_count = len(set(e.get("_entry_id", e.get("unique_id"))
352
+ for e in other_entries))
353
+ child_tags[other_tag] = unique_count
354
+
355
+ # Get unique entries
356
+ seen_ids = set()
357
+ unique_entries = []
358
+ for entry in entries:
359
+ entry_id = entry.get("_entry_id", entry.get("unique_id"))
360
+ if entry_id not in seen_ids:
361
+ seen_ids.add(entry_id)
362
+ unique_entries.append(entry)
363
+
364
+ self._write_index_file(tag_dir, tag, unique_entries, child_tags, output_path)
365
+
366
+ def _write_index_file(self, directory: Path, title: str,
367
+ entries: List[Dict], child_tags: Dict[str, int], output_path: Path):
368
+ """Write an index.html file for a directory using Jinja2 template."""
369
+ from jinja2 import Environment, FileSystemLoader
370
+ import json
371
+ import re
372
+
373
+ # Prepare entries for JSON (clean and escape)
374
+ clean_entries = []
375
+ for entry in entries:
376
+ clean_entry = {}
377
+ for key, value in entry.items():
378
+ if isinstance(value, str):
379
+ # Remove problematic HTML from descriptions
380
+ if key == "description":
381
+ # Strip HTML tags from description for JSON
382
+ value = re.sub(r'<[^>]+>', '', value)
383
+ # Limit description length
384
+ if len(value) > 500:
385
+ value = value[:500] + "..."
386
+ clean_entry[key] = value
387
+ elif isinstance(value, list):
388
+ clean_entry[key] = [str(v) for v in value]
389
+ else:
390
+ clean_entry[key] = str(value)
391
+ clean_entries.append(clean_entry)
392
+
393
+ # Convert to JSON for JavaScript
394
+ entries_json = json.dumps(clean_entries, ensure_ascii=True)
395
+
396
+ # Set up Jinja2 environment
397
+ template_dir = Path(__file__).parent / "templates"
398
+ env = Environment(loader=FileSystemLoader(str(template_dir)))
399
+ template = env.get_template("advanced_index.html")
400
+
401
+ # Calculate if we're in a subdirectory (for proper _books path)
402
+ is_subdir = directory != output_path
403
+
404
+ # Render template
405
+ html_content = template.render(
406
+ title=title,
407
+ entries=entries,
408
+ entries_json=entries_json,
409
+ child_tags=child_tags,
410
+ tag_separator=self.tag_separator,
411
+ is_subdir=is_subdir
412
+ )
413
+
414
+ # Write the file
415
+ index_path = directory / "index.html"
416
+ with open(index_path, "w", encoding="utf-8") as f:
417
+ f.write(html_content)
418
+
419
+ def _create_readme(self, output_path: Path, num_entries: int, num_tags: int):
420
+ """Create a README file explaining the structure."""
421
+ readme_content = f"""# EBK Library - Symlink Navigation Structure
422
+
423
+ This directory contains a navigable view of your ebook library organized by tags.
424
+
425
+ ## Statistics
426
+ - Total books: {num_entries}
427
+ - Total tags/categories: {num_tags}
428
+
429
+ ## Structure
430
+
431
+ - **_books/**: Contains the actual ebook files and metadata
432
+ - **Tag directories**: Each tag becomes a directory, with hierarchical tags creating nested directories
433
+ - **Symlinks**: Books appear in multiple tag directories via symbolic links
434
+
435
+ ## Navigation
436
+
437
+ You can navigate this structure using:
438
+ 1. Your file explorer (Finder, Windows Explorer, etc.)
439
+ 2. Command line tools (cd, ls, etc.)
440
+ 3. Web browser (open index.html files)
441
+
442
+ ## Hierarchical Tags
443
+
444
+ Tags like "Programming/Python/Web" create a nested structure:
445
+ ```
446
+ Programming/
447
+ Python/
448
+ Web/
449
+ (books tagged with Programming/Python/Web)
450
+ (books tagged with Programming/Python)
451
+ (books tagged with Programming)
452
+ ```
453
+
454
+ Books appear at each relevant level in the hierarchy.
455
+
456
+ ## Notes
457
+
458
+ - This is a read-only view. Modifying files here won't affect the original library.
459
+ - Symlinks point to files in the _books directory.
460
+ - On Windows, you may need administrator privileges to create symlinks.
461
+
462
+ Generated by EBK - https://github.com/queelius/ebk
463
+ """
464
+
465
+ with open(output_path / "README.md", "w") as f:
466
+ f.write(readme_content)
467
+
468
+
469
+ def export_symlink_dag(lib_dir: str, output_dir: str, **kwargs):
470
+ """
471
+ Convenience function to export library as symlink DAG.
472
+
473
+ Args:
474
+ lib_dir: Path to ebk library
475
+ output_dir: Output directory
476
+ **kwargs: Additional arguments passed to SymlinkDAGExporter.export()
477
+ """
478
+ exporter = SymlinkDAGExporter()
479
+ exporter.export(lib_dir, output_dir, **kwargs)
ebk/exports/zip.py ADDED
@@ -0,0 +1,25 @@
1
+ import os
2
+ import zipfile
3
+ from pathlib import Path
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ def export_zipfile(lib_dir, zip_file):
9
+ """
10
+ Export ebk library to a ZIP archive.
11
+
12
+ Args:
13
+ lib_dir (str): Path to the ebk library directory to export (contains `metadata.json` and ebook-related files)
14
+ zip_file (str): Path to the output ZIP file
15
+ """
16
+ lib_dir = Path(lib_dir)
17
+
18
+ # just want to take the entire directory and zip it
19
+
20
+ with zipfile.ZipFile(zip_file, "w") as z:
21
+ for root, _, files in os.walk(lib_dir):
22
+ for file in files:
23
+ file_path = Path(root) / file
24
+ logging.debug(f"Adding file to zip: {file_path}")
25
+ z.write(file_path, arcname=file_path.relative_to(lib_dir))