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.
- ebk/__init__.py +35 -0
- ebk/ai/__init__.py +23 -0
- ebk/ai/knowledge_graph.py +450 -0
- ebk/ai/llm_providers/__init__.py +26 -0
- ebk/ai/llm_providers/anthropic.py +209 -0
- ebk/ai/llm_providers/base.py +295 -0
- ebk/ai/llm_providers/gemini.py +285 -0
- ebk/ai/llm_providers/ollama.py +294 -0
- ebk/ai/metadata_enrichment.py +394 -0
- ebk/ai/question_generator.py +328 -0
- ebk/ai/reading_companion.py +224 -0
- ebk/ai/semantic_search.py +433 -0
- ebk/ai/text_extractor.py +393 -0
- ebk/calibre_import.py +66 -0
- ebk/cli.py +6433 -0
- ebk/config.py +230 -0
- ebk/db/__init__.py +37 -0
- ebk/db/migrations.py +507 -0
- ebk/db/models.py +725 -0
- ebk/db/session.py +144 -0
- ebk/decorators.py +1 -0
- ebk/exports/__init__.py +0 -0
- ebk/exports/base_exporter.py +218 -0
- ebk/exports/echo_export.py +279 -0
- ebk/exports/html_library.py +1743 -0
- ebk/exports/html_utils.py +87 -0
- ebk/exports/hugo.py +59 -0
- ebk/exports/jinja_export.py +286 -0
- ebk/exports/multi_facet_export.py +159 -0
- ebk/exports/opds_export.py +232 -0
- ebk/exports/symlink_dag.py +479 -0
- ebk/exports/zip.py +25 -0
- ebk/extract_metadata.py +341 -0
- ebk/ident.py +89 -0
- ebk/library_db.py +1440 -0
- ebk/opds.py +748 -0
- ebk/plugins/__init__.py +42 -0
- ebk/plugins/base.py +502 -0
- ebk/plugins/hooks.py +442 -0
- ebk/plugins/registry.py +499 -0
- ebk/repl/__init__.py +9 -0
- ebk/repl/find.py +126 -0
- ebk/repl/grep.py +173 -0
- ebk/repl/shell.py +1677 -0
- ebk/repl/text_utils.py +320 -0
- ebk/search_parser.py +413 -0
- ebk/server.py +3608 -0
- ebk/services/__init__.py +28 -0
- ebk/services/annotation_extraction.py +351 -0
- ebk/services/annotation_service.py +380 -0
- ebk/services/export_service.py +577 -0
- ebk/services/import_service.py +447 -0
- ebk/services/personal_metadata_service.py +347 -0
- ebk/services/queue_service.py +253 -0
- ebk/services/tag_service.py +281 -0
- ebk/services/text_extraction.py +317 -0
- ebk/services/view_service.py +12 -0
- ebk/similarity/__init__.py +77 -0
- ebk/similarity/base.py +154 -0
- ebk/similarity/core.py +471 -0
- ebk/similarity/extractors.py +168 -0
- ebk/similarity/metrics.py +376 -0
- ebk/skills/SKILL.md +182 -0
- ebk/skills/__init__.py +1 -0
- ebk/vfs/__init__.py +101 -0
- ebk/vfs/base.py +298 -0
- ebk/vfs/library_vfs.py +122 -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/vfs_router.py +275 -0
- ebk/views/__init__.py +32 -0
- ebk/views/dsl.py +668 -0
- ebk/views/service.py +619 -0
- ebk-0.4.4.dist-info/METADATA +755 -0
- ebk-0.4.4.dist-info/RECORD +87 -0
- ebk-0.4.4.dist-info/WHEEL +5 -0
- ebk-0.4.4.dist-info/entry_points.txt +2 -0
- ebk-0.4.4.dist-info/licenses/LICENSE +21 -0
- ebk-0.4.4.dist-info/top_level.txt +1 -0
ebk/repl/text_utils.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Text processing utilities for REPL shell.
|
|
2
|
+
|
|
3
|
+
Implements Unix-like text utilities: head, tail, wc, sort, uniq.
|
|
4
|
+
All utilities support reading from stdin or file content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TextUtils:
|
|
11
|
+
"""Collection of text processing utilities."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def head(content: str, lines: int = 10) -> str:
|
|
15
|
+
"""Return first N lines of content.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
content: Input text
|
|
19
|
+
lines: Number of lines to return (default: 10)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
First N lines joined with newlines
|
|
23
|
+
"""
|
|
24
|
+
if not content:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
text_lines = content.split("\n")
|
|
28
|
+
return "\n".join(text_lines[:lines])
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def tail(content: str, lines: int = 10) -> str:
|
|
32
|
+
"""Return last N lines of content.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
content: Input text
|
|
36
|
+
lines: Number of lines to return (default: 10)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Last N lines joined with newlines
|
|
40
|
+
"""
|
|
41
|
+
if not content:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
text_lines = content.split("\n")
|
|
45
|
+
return "\n".join(text_lines[-lines:])
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def wc(content: str, lines_only: bool = False, words_only: bool = False,
|
|
49
|
+
chars_only: bool = False) -> str:
|
|
50
|
+
"""Count lines, words, and characters.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
content: Input text
|
|
54
|
+
lines_only: Only count lines (-l flag)
|
|
55
|
+
words_only: Only count words (-w flag)
|
|
56
|
+
chars_only: Only count characters (-c flag)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Formatted count string
|
|
60
|
+
"""
|
|
61
|
+
if not content:
|
|
62
|
+
return "0"
|
|
63
|
+
|
|
64
|
+
line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
|
|
65
|
+
word_count = len(content.split())
|
|
66
|
+
char_count = len(content)
|
|
67
|
+
|
|
68
|
+
if lines_only:
|
|
69
|
+
return str(line_count)
|
|
70
|
+
elif words_only:
|
|
71
|
+
return str(word_count)
|
|
72
|
+
elif chars_only:
|
|
73
|
+
return str(char_count)
|
|
74
|
+
else:
|
|
75
|
+
# Show all three
|
|
76
|
+
return f"{line_count:>8} {word_count:>8} {char_count:>8}"
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def sort_lines(content: str, reverse: bool = False) -> str:
|
|
80
|
+
"""Sort lines alphabetically.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
content: Input text
|
|
84
|
+
reverse: Sort in reverse order (default: False)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Sorted lines joined with newlines
|
|
88
|
+
"""
|
|
89
|
+
if not content:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
lines = content.split("\n")
|
|
93
|
+
# Filter out empty last line if present
|
|
94
|
+
if lines and lines[-1] == "":
|
|
95
|
+
lines = lines[:-1]
|
|
96
|
+
sorted_lines = sorted(lines, reverse=reverse)
|
|
97
|
+
return "\n".join(sorted_lines) + "\n"
|
|
98
|
+
else:
|
|
99
|
+
sorted_lines = sorted(lines, reverse=reverse)
|
|
100
|
+
return "\n".join(sorted_lines)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def uniq(content: str, count: bool = False) -> str:
|
|
104
|
+
"""Remove duplicate adjacent lines.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
content: Input text
|
|
108
|
+
count: Prefix lines with occurrence count (-c flag)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Unique lines, optionally with counts
|
|
112
|
+
"""
|
|
113
|
+
if not content:
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
lines = content.split("\n")
|
|
117
|
+
if not lines:
|
|
118
|
+
return ""
|
|
119
|
+
|
|
120
|
+
unique_lines = []
|
|
121
|
+
current_line = None
|
|
122
|
+
current_count = 0
|
|
123
|
+
|
|
124
|
+
for line in lines:
|
|
125
|
+
if line == current_line:
|
|
126
|
+
current_count += 1
|
|
127
|
+
else:
|
|
128
|
+
# Save previous line if exists
|
|
129
|
+
if current_line is not None:
|
|
130
|
+
if count:
|
|
131
|
+
unique_lines.append(f"{current_count:>7} {current_line}")
|
|
132
|
+
else:
|
|
133
|
+
unique_lines.append(current_line)
|
|
134
|
+
|
|
135
|
+
# Start new line
|
|
136
|
+
current_line = line
|
|
137
|
+
current_count = 1
|
|
138
|
+
|
|
139
|
+
# Don't forget the last line
|
|
140
|
+
if current_line is not None:
|
|
141
|
+
if count:
|
|
142
|
+
unique_lines.append(f"{current_count:>7} {current_line}")
|
|
143
|
+
else:
|
|
144
|
+
unique_lines.append(current_line)
|
|
145
|
+
|
|
146
|
+
return "\n".join(unique_lines)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def parse_head_args(args: List[str]) -> tuple[int, Optional[str]]:
|
|
150
|
+
"""Parse head command arguments.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
args: Command arguments
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Tuple of (line_count, filename)
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If arguments are invalid
|
|
160
|
+
"""
|
|
161
|
+
lines = 10
|
|
162
|
+
filename = None
|
|
163
|
+
|
|
164
|
+
i = 0
|
|
165
|
+
while i < len(args):
|
|
166
|
+
arg = args[i]
|
|
167
|
+
|
|
168
|
+
if arg == "-n":
|
|
169
|
+
# Next arg is line count
|
|
170
|
+
if i + 1 >= len(args):
|
|
171
|
+
raise ValueError("head: -n requires an argument")
|
|
172
|
+
try:
|
|
173
|
+
lines = int(args[i + 1])
|
|
174
|
+
i += 2
|
|
175
|
+
except ValueError:
|
|
176
|
+
raise ValueError(f"head: invalid line count: {args[i + 1]}")
|
|
177
|
+
elif arg.startswith("-") and arg[1:].isdigit():
|
|
178
|
+
# Short form: -10
|
|
179
|
+
try:
|
|
180
|
+
lines = int(arg[1:])
|
|
181
|
+
i += 1
|
|
182
|
+
except ValueError:
|
|
183
|
+
raise ValueError(f"head: invalid line count: {arg}")
|
|
184
|
+
elif not arg.startswith("-"):
|
|
185
|
+
# Filename
|
|
186
|
+
filename = arg
|
|
187
|
+
i += 1
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(f"head: unknown option: {arg}")
|
|
190
|
+
|
|
191
|
+
return lines, filename
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_tail_args(args: List[str]) -> tuple[int, Optional[str]]:
|
|
195
|
+
"""Parse tail command arguments.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
args: Command arguments
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Tuple of (line_count, filename)
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If arguments are invalid
|
|
205
|
+
"""
|
|
206
|
+
lines = 10
|
|
207
|
+
filename = None
|
|
208
|
+
|
|
209
|
+
i = 0
|
|
210
|
+
while i < len(args):
|
|
211
|
+
arg = args[i]
|
|
212
|
+
|
|
213
|
+
if arg == "-n":
|
|
214
|
+
# Next arg is line count
|
|
215
|
+
if i + 1 >= len(args):
|
|
216
|
+
raise ValueError("tail: -n requires an argument")
|
|
217
|
+
try:
|
|
218
|
+
lines = int(args[i + 1])
|
|
219
|
+
i += 2
|
|
220
|
+
except ValueError:
|
|
221
|
+
raise ValueError(f"tail: invalid line count: {args[i + 1]}")
|
|
222
|
+
elif arg.startswith("-") and arg[1:].isdigit():
|
|
223
|
+
# Short form: -10
|
|
224
|
+
try:
|
|
225
|
+
lines = int(arg[1:])
|
|
226
|
+
i += 1
|
|
227
|
+
except ValueError:
|
|
228
|
+
raise ValueError(f"tail: invalid line count: {arg}")
|
|
229
|
+
elif not arg.startswith("-"):
|
|
230
|
+
# Filename
|
|
231
|
+
filename = arg
|
|
232
|
+
i += 1
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(f"tail: unknown option: {arg}")
|
|
235
|
+
|
|
236
|
+
return lines, filename
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def parse_wc_args(args: List[str]) -> tuple[bool, bool, bool, Optional[str]]:
|
|
240
|
+
"""Parse wc command arguments.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
args: Command arguments
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Tuple of (lines_only, words_only, chars_only, filename)
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
ValueError: If arguments are invalid
|
|
250
|
+
"""
|
|
251
|
+
lines_only = False
|
|
252
|
+
words_only = False
|
|
253
|
+
chars_only = False
|
|
254
|
+
filename = None
|
|
255
|
+
|
|
256
|
+
for arg in args:
|
|
257
|
+
if arg == "-l":
|
|
258
|
+
lines_only = True
|
|
259
|
+
elif arg == "-w":
|
|
260
|
+
words_only = True
|
|
261
|
+
elif arg == "-c":
|
|
262
|
+
chars_only = True
|
|
263
|
+
elif not arg.startswith("-"):
|
|
264
|
+
filename = arg
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(f"wc: unknown option: {arg}")
|
|
267
|
+
|
|
268
|
+
return lines_only, words_only, chars_only, filename
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def parse_sort_args(args: List[str]) -> tuple[bool, Optional[str]]:
|
|
272
|
+
"""Parse sort command arguments.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
args: Command arguments
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Tuple of (reverse, filename)
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
ValueError: If arguments are invalid
|
|
282
|
+
"""
|
|
283
|
+
reverse = False
|
|
284
|
+
filename = None
|
|
285
|
+
|
|
286
|
+
for arg in args:
|
|
287
|
+
if arg == "-r":
|
|
288
|
+
reverse = True
|
|
289
|
+
elif not arg.startswith("-"):
|
|
290
|
+
filename = arg
|
|
291
|
+
else:
|
|
292
|
+
raise ValueError(f"sort: unknown option: {arg}")
|
|
293
|
+
|
|
294
|
+
return reverse, filename
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def parse_uniq_args(args: List[str]) -> tuple[bool, Optional[str]]:
|
|
298
|
+
"""Parse uniq command arguments.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
args: Command arguments
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Tuple of (count, filename)
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
ValueError: If arguments are invalid
|
|
308
|
+
"""
|
|
309
|
+
count = False
|
|
310
|
+
filename = None
|
|
311
|
+
|
|
312
|
+
for arg in args:
|
|
313
|
+
if arg == "-c":
|
|
314
|
+
count = True
|
|
315
|
+
elif not arg.startswith("-"):
|
|
316
|
+
filename = arg
|
|
317
|
+
else:
|
|
318
|
+
raise ValueError(f"uniq: unknown option: {arg}")
|
|
319
|
+
|
|
320
|
+
return count, filename
|