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
ebk/repl/find.py ADDED
@@ -0,0 +1,126 @@
1
+ """Find command implementation for REPL shell."""
2
+
3
+ from typing import List, Dict, Any, Optional
4
+ from ebk.library_db import Library
5
+ from ebk.db.models import Book
6
+
7
+
8
+ class FindQuery:
9
+ """Book finder with metadata filters."""
10
+
11
+ def __init__(self, library: Library):
12
+ """Initialize find query.
13
+
14
+ Args:
15
+ library: Library instance
16
+ """
17
+ self.library = library
18
+
19
+ def find(self, filters: Dict[str, Any]) -> List[Book]:
20
+ """Find books matching filters.
21
+
22
+ Args:
23
+ filters: Dictionary of field:value filters
24
+ Supported fields:
25
+ - title: Book title (partial match)
26
+ - author: Author name (partial match)
27
+ - subject: Subject/tag (partial match)
28
+ - text: Full-text search (FTS5 across title, description, extracted text)
29
+ - language: Language code (exact match)
30
+ - year: Publication year (exact match)
31
+ - publisher: Publisher name (partial match)
32
+ - format: File format (exact match, e.g., pdf, epub)
33
+ - limit: Maximum results (default: 50)
34
+
35
+ Returns:
36
+ List of matching books
37
+ """
38
+ query = self.library.query()
39
+
40
+ # Apply filters
41
+ if "title" in filters:
42
+ query = query.filter_by_title(filters["title"])
43
+
44
+ if "author" in filters:
45
+ query = query.filter_by_author(filters["author"])
46
+
47
+ if "subject" in filters:
48
+ query = query.filter_by_subject(filters["subject"])
49
+
50
+ if "language" in filters:
51
+ query = query.filter_by_language(filters["language"])
52
+
53
+ if "year" in filters:
54
+ try:
55
+ year = int(filters["year"])
56
+ query = query.filter_by_year(year)
57
+ except ValueError:
58
+ pass # Skip invalid year
59
+
60
+ if "publisher" in filters:
61
+ query = query.filter_by_publisher(filters["publisher"])
62
+
63
+ if "format" in filters:
64
+ query = query.filter_by_format(filters["format"])
65
+
66
+ if "text" in filters:
67
+ query = query.filter_by_text(filters["text"])
68
+
69
+ # Apply limit
70
+ limit = filters.get("limit", 50)
71
+ try:
72
+ # Convert to int if it's a string
73
+ if isinstance(limit, str):
74
+ limit = int(limit)
75
+ if isinstance(limit, int):
76
+ query = query.limit(limit)
77
+ except (ValueError, TypeError):
78
+ # Invalid limit, use default
79
+ query = query.limit(50)
80
+
81
+ # Execute query
82
+ return query.all()
83
+
84
+ def parse_filters(self, args: List[str]) -> Dict[str, Any]:
85
+ """Parse command-line arguments into filter dictionary.
86
+
87
+ Args:
88
+ args: List of filter arguments in format "field:value"
89
+
90
+ Returns:
91
+ Dictionary of filters
92
+
93
+ Raises:
94
+ ValueError: If argument format is invalid
95
+ """
96
+ filters = {}
97
+
98
+ for arg in args:
99
+ if ":" not in arg:
100
+ raise ValueError(f"Invalid filter format: {arg}. Use field:value")
101
+
102
+ field, value = arg.split(":", 1)
103
+ field = field.lower().strip()
104
+ value = value.strip()
105
+
106
+ # Validate field
107
+ valid_fields = {
108
+ "title",
109
+ "author",
110
+ "subject",
111
+ "text",
112
+ "language",
113
+ "year",
114
+ "publisher",
115
+ "format",
116
+ "limit",
117
+ }
118
+
119
+ if field not in valid_fields:
120
+ raise ValueError(
121
+ f"Unknown field: {field}. Valid fields: {', '.join(sorted(valid_fields))}"
122
+ )
123
+
124
+ filters[field] = value
125
+
126
+ return filters
ebk/repl/grep.py ADDED
@@ -0,0 +1,174 @@
1
+ """grep implementation for REPL shell."""
2
+
3
+ import re
4
+ from typing import List, Tuple, Optional
5
+ from pathlib import Path
6
+
7
+ from ebk.vfs import LibraryVFS, DirectoryNode, FileNode
8
+
9
+
10
+ class GrepMatcher:
11
+ """Unix-like grep functionality for VFS."""
12
+
13
+ def __init__(self, vfs: LibraryVFS):
14
+ """Initialize grep matcher.
15
+
16
+ Args:
17
+ vfs: VFS instance
18
+ """
19
+ self.vfs = vfs
20
+
21
+ def grep(
22
+ self,
23
+ pattern: str,
24
+ paths: List[str],
25
+ recursive: bool = False,
26
+ ignore_case: bool = False,
27
+ line_numbers: bool = False,
28
+ ) -> List[Tuple[str, int, str]]:
29
+ """Search for pattern in files.
30
+
31
+ Args:
32
+ pattern: Regex pattern to search for
33
+ paths: List of paths to search (files or directories)
34
+ recursive: Search directories recursively
35
+ ignore_case: Case-insensitive matching
36
+ line_numbers: Include line numbers in output
37
+
38
+ Returns:
39
+ List of (file_path, line_number, line_content) tuples
40
+ """
41
+ results = []
42
+
43
+ # Compile regex
44
+ flags = re.IGNORECASE if ignore_case else 0
45
+ try:
46
+ regex = re.compile(pattern, flags)
47
+ except re.error as e:
48
+ raise ValueError(f"Invalid regex pattern: {e}")
49
+
50
+ # Process each path
51
+ for path in paths:
52
+ results.extend(
53
+ self._search_path(path, regex, recursive, line_numbers)
54
+ )
55
+
56
+ return results
57
+
58
+ def _search_path(
59
+ self,
60
+ path: str,
61
+ regex: re.Pattern,
62
+ recursive: bool,
63
+ line_numbers: bool,
64
+ ) -> List[Tuple[str, int, str]]:
65
+ """Search a single path.
66
+
67
+ Args:
68
+ path: Path to search
69
+ regex: Compiled regex pattern
70
+ recursive: Search recursively
71
+ line_numbers: Include line numbers
72
+
73
+ Returns:
74
+ List of matches
75
+ """
76
+ results = []
77
+
78
+ # Resolve path
79
+ node = self.vfs.get_node(path)
80
+ if node is None:
81
+ return results
82
+
83
+ # If it's a file, search it
84
+ if isinstance(node, FileNode):
85
+ results.extend(self._search_file(node, regex, line_numbers))
86
+
87
+ # If it's a directory, search children
88
+ elif isinstance(node, DirectoryNode):
89
+ if recursive:
90
+ results.extend(
91
+ self._search_directory_recursive(node, regex, line_numbers)
92
+ )
93
+ else:
94
+ # Just search immediate children that are files
95
+ for child in node.list_children():
96
+ if isinstance(child, FileNode):
97
+ results.extend(
98
+ self._search_file(child, regex, line_numbers)
99
+ )
100
+
101
+ return results
102
+
103
+ def _search_file(
104
+ self,
105
+ file_node: FileNode,
106
+ regex: re.Pattern,
107
+ line_numbers: bool,
108
+ ) -> List[Tuple[str, int, str]]:
109
+ """Search a single file.
110
+
111
+ Args:
112
+ file_node: File node to search
113
+ regex: Compiled regex pattern
114
+ line_numbers: Include line numbers
115
+
116
+ Returns:
117
+ List of matches
118
+ """
119
+ results = []
120
+
121
+ try:
122
+ content = file_node.read_content()
123
+ lines = content.split("\n")
124
+
125
+ file_path = file_node.get_path()
126
+
127
+ for i, line in enumerate(lines, start=1):
128
+ if regex.search(line):
129
+ line_num = i if line_numbers else 0
130
+ results.append((file_path, line_num, line))
131
+
132
+ except Exception:
133
+ # Skip files that can't be read
134
+ pass
135
+
136
+ return results
137
+
138
+ def _search_directory_recursive(
139
+ self,
140
+ dir_node: DirectoryNode,
141
+ regex: re.Pattern,
142
+ line_numbers: bool,
143
+ ) -> List[Tuple[str, int, str]]:
144
+ """Search directory recursively.
145
+
146
+ Args:
147
+ dir_node: Directory node to search
148
+ regex: Compiled regex pattern
149
+ line_numbers: Include line numbers
150
+
151
+ Returns:
152
+ List of matches
153
+ """
154
+ results = []
155
+
156
+ try:
157
+ children = dir_node.list_children()
158
+
159
+ for child in children:
160
+ if isinstance(child, FileNode):
161
+ results.extend(self._search_file(child, regex, line_numbers))
162
+ elif isinstance(child, DirectoryNode):
163
+ # Recurse into subdirectory
164
+ results.extend(
165
+ self._search_directory_recursive(
166
+ child, regex, line_numbers
167
+ )
168
+ )
169
+
170
+ except Exception:
171
+ # Skip directories that can't be read
172
+ pass
173
+
174
+ return results