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/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
@@ -0,0 +1,11 @@
1
+ """
2
+ Services for ebk business logic.
3
+ """
4
+
5
+ from .text_extraction import TextExtractionService
6
+ from .import_service import ImportService
7
+
8
+ __all__ = [
9
+ 'TextExtractionService',
10
+ 'ImportService'
11
+ ]