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
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
|