epub2text 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {epub2text-0.2.3 → epub2text-0.2.4}/.ruff.toml +1 -1
- {epub2text-0.2.3 → epub2text-0.2.4}/PKG-INFO +2 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/make.py +1 -1
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/__init__.py +2 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/_version.py +3 -3
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/bookmarks.py +9 -5
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/cleaner.py +2 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/cli.py +19 -20
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/models.py +11 -12
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/parser.py +28 -28
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/reader.py +6 -7
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/PKG-INFO +2 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/examples/demo.py +2 -3
- {epub2text-0.2.3 → epub2text-0.2.4}/pyproject.toml +15 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_epub_download.py +4 -2
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_epub_generation.py +1 -1
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_page_extraction_improvements.py +1 -1
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_title_deduplication.py +1 -1
- {epub2text-0.2.3 → epub2text-0.2.4}/.codecrate.toml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.coveragerc +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.github/pytest.ini +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/codecov.yml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/pre-commit.yml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/python-publish.yml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/tests.yml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.gitignore +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.pre-commit-config.yaml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.prettierrc.yml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/.readthedocs.yaml +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/LICENSE +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/README.md +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/api.rst +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/changelog.rst +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/conf.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/index.rst +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/installation.rst +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/make.bat +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/requirements.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/docs/usage.rst +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/__main__.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/formatters.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/py.typed +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/SOURCES.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/dependency_links.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/entry_points.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/requires.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/top_level.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/examples/__init__.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/examples/__main__.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/requirements-test.txt +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/setup.cfg +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/setup.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/__init__.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_cleaner.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_cli_ranges.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_parser_page_list.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_parser_spine_slice.py +0 -0
- {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_reader_bookmarks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epub2text
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A simple tool to extract text from EPUB files.
|
|
5
5
|
Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.14
|
|
39
39
|
Classifier: Topic :: Software Development :: Libraries
|
|
40
|
-
Requires-Python: >=3.
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
License-File: LICENSE
|
|
43
43
|
Requires-Dist: click>=8.0.0
|
|
@@ -136,7 +136,7 @@ def _download_epub(
|
|
|
136
136
|
except Exception as exc:
|
|
137
137
|
if tmp_path:
|
|
138
138
|
Path(tmp_path).unlink(missing_ok=True)
|
|
139
|
-
if isinstance(exc,
|
|
139
|
+
if isinstance(exc, urllib.error.URLError | socket.timeout):
|
|
140
140
|
if isinstance(exc, socket.timeout) or isinstance(
|
|
141
141
|
getattr(exc, "reason", None), socket.timeout
|
|
142
142
|
):
|
|
@@ -152,7 +152,7 @@ def epub2txt(
|
|
|
152
152
|
max_bytes: int = DEFAULT_MAX_DOWNLOAD_BYTES,
|
|
153
153
|
user_agent: str | None = None,
|
|
154
154
|
allowed_schemes: tuple[str, ...] = ("https", "http"),
|
|
155
|
-
) ->
|
|
155
|
+
) -> str | list[str]:
|
|
156
156
|
"""
|
|
157
157
|
Extract text from EPUB file (compatibility function for old epub2txt API).
|
|
158
158
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
31
|
+
__version__ = version = '0.2.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 4)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'ge900e4fef'
|
|
@@ -7,7 +7,7 @@ import tempfile
|
|
|
7
7
|
from dataclasses import asdict, dataclass
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, cast
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -54,7 +54,7 @@ class Bookmark:
|
|
|
54
54
|
class BookmarkManager:
|
|
55
55
|
"""Manages bookmarks for EPUB files."""
|
|
56
56
|
|
|
57
|
-
def __init__(self, bookmark_file:
|
|
57
|
+
def __init__(self, bookmark_file: Path | None = None) -> None:
|
|
58
58
|
"""
|
|
59
59
|
Initialize bookmark manager.
|
|
60
60
|
|
|
@@ -119,11 +119,15 @@ class BookmarkManager:
|
|
|
119
119
|
"""Return the path for the bookmark backup file."""
|
|
120
120
|
return self.bookmark_file.with_suffix(self.bookmark_file.suffix + ".bak")
|
|
121
121
|
|
|
122
|
-
def _read_bookmarks_file(self, path: Path) ->
|
|
122
|
+
def _read_bookmarks_file(self, path: Path) -> dict[str, Any] | None:
|
|
123
123
|
"""Read bookmarks JSON from disk."""
|
|
124
124
|
try:
|
|
125
125
|
with open(path, encoding="utf-8") as f:
|
|
126
|
-
|
|
126
|
+
data = json.load(f)
|
|
127
|
+
if isinstance(data, dict):
|
|
128
|
+
return cast(dict[str, Any], data)
|
|
129
|
+
logger.warning("Unexpected bookmark data structure in %s", path)
|
|
130
|
+
return None
|
|
127
131
|
except (json.JSONDecodeError, OSError) as exc:
|
|
128
132
|
logger.warning("Failed to load bookmarks from %s: %s", path, exc)
|
|
129
133
|
return None
|
|
@@ -144,7 +148,7 @@ class BookmarkManager:
|
|
|
144
148
|
self._bookmarks[key] = asdict(bookmark)
|
|
145
149
|
self._save()
|
|
146
150
|
|
|
147
|
-
def load(self, epub_path: str) ->
|
|
151
|
+
def load(self, epub_path: str) -> Bookmark | None:
|
|
148
152
|
"""
|
|
149
153
|
Load bookmark for a specific EPUB file.
|
|
150
154
|
|
|
@@ -96,7 +96,7 @@ class TextCleaner:
|
|
|
96
96
|
markers = {}
|
|
97
97
|
marker_count = [0]
|
|
98
98
|
|
|
99
|
-
def save_marker(match):
|
|
99
|
+
def save_marker(match: re.Match[str]) -> str:
|
|
100
100
|
placeholder = f"___MARKER_{marker_count[0]}___"
|
|
101
101
|
markers[placeholder] = match.group(0)
|
|
102
102
|
marker_count[0] += 1
|
|
@@ -196,7 +196,7 @@ class TextCleaner:
|
|
|
196
196
|
abbrev_map = {}
|
|
197
197
|
abbrev_counter = [0]
|
|
198
198
|
|
|
199
|
-
def protect_abbrev(match):
|
|
199
|
+
def protect_abbrev(match: re.Match[str]) -> str:
|
|
200
200
|
placeholder = f"__ABBREV_{abbrev_counter[0]}__"
|
|
201
201
|
abbrev_map[placeholder] = match.group(0)
|
|
202
202
|
abbrev_counter[0] += 1
|
|
@@ -7,7 +7,6 @@ import sys
|
|
|
7
7
|
from collections.abc import Iterator
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
10
|
|
|
12
11
|
import click
|
|
13
12
|
from rich.console import Console
|
|
@@ -46,7 +45,7 @@ def spinner(task_description: str) -> Iterator[None]:
|
|
|
46
45
|
yield
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
def parse_range_tokens(range_str: str) -> list[tuple[str,
|
|
48
|
+
def parse_range_tokens(range_str: str) -> list[tuple[str, str | None]]:
|
|
50
49
|
"""
|
|
51
50
|
Parse a range string into ordered tokens.
|
|
52
51
|
|
|
@@ -59,7 +58,7 @@ def parse_range_tokens(range_str: str) -> list[tuple[str, Optional[str]]]:
|
|
|
59
58
|
if not range_str:
|
|
60
59
|
raise ValueError("Range string cannot be empty")
|
|
61
60
|
|
|
62
|
-
tokens: list[tuple[str,
|
|
61
|
+
tokens: list[tuple[str, str | None]] = []
|
|
63
62
|
parts = range_str.split(",")
|
|
64
63
|
|
|
65
64
|
for part in parts:
|
|
@@ -205,7 +204,7 @@ def parse_page_range(range_str: str, pages: list[Page]) -> list[str]:
|
|
|
205
204
|
result: list[str] = []
|
|
206
205
|
seen: set[str] = set()
|
|
207
206
|
|
|
208
|
-
def parse_int(value: str) ->
|
|
207
|
+
def parse_int(value: str) -> int | None:
|
|
209
208
|
try:
|
|
210
209
|
return int(value)
|
|
211
210
|
except ValueError:
|
|
@@ -590,8 +589,8 @@ def list_pages(filepath: Path, page_size: int, use_words: bool) -> None:
|
|
|
590
589
|
)
|
|
591
590
|
def extract_pages_cmd(
|
|
592
591
|
filepath: Path,
|
|
593
|
-
output:
|
|
594
|
-
pages:
|
|
592
|
+
output: Path | None,
|
|
593
|
+
pages: str | None,
|
|
595
594
|
page_size: int,
|
|
596
595
|
use_words: bool,
|
|
597
596
|
raw: bool,
|
|
@@ -786,13 +785,13 @@ def extract_pages_cmd(
|
|
|
786
785
|
)
|
|
787
786
|
def extract(
|
|
788
787
|
filepath: Path,
|
|
789
|
-
output:
|
|
790
|
-
chapters:
|
|
788
|
+
output: Path | None,
|
|
789
|
+
chapters: str | None,
|
|
791
790
|
interactive: bool,
|
|
792
791
|
paragraphs: bool,
|
|
793
792
|
sentences: bool,
|
|
794
793
|
comma: bool,
|
|
795
|
-
max_length:
|
|
794
|
+
max_length: int | None,
|
|
796
795
|
empty_lines: bool,
|
|
797
796
|
separator: str,
|
|
798
797
|
raw: bool,
|
|
@@ -801,7 +800,7 @@ def extract(
|
|
|
801
800
|
no_markers: bool,
|
|
802
801
|
language_model: str,
|
|
803
802
|
offset: int,
|
|
804
|
-
limit:
|
|
803
|
+
limit: int | None,
|
|
805
804
|
line_numbers: bool,
|
|
806
805
|
) -> None:
|
|
807
806
|
"""
|
|
@@ -1012,8 +1011,8 @@ def extract(
|
|
|
1012
1011
|
)
|
|
1013
1012
|
def extract_gutenberg(
|
|
1014
1013
|
filepath: Path,
|
|
1015
|
-
output:
|
|
1016
|
-
chapters:
|
|
1014
|
+
output: Path | None,
|
|
1015
|
+
chapters: str | None,
|
|
1017
1016
|
interactive: bool,
|
|
1018
1017
|
) -> None:
|
|
1019
1018
|
"""
|
|
@@ -1124,9 +1123,9 @@ def extract_gutenberg(
|
|
|
1124
1123
|
# First, normalize the chapter text to help with splitting
|
|
1125
1124
|
chapter_lines = chapter_text.split("\n")
|
|
1126
1125
|
|
|
1127
|
-
current_title = None
|
|
1128
|
-
current_content = []
|
|
1129
|
-
chapters_list = []
|
|
1126
|
+
current_title: str | None = None
|
|
1127
|
+
current_content: list[str] = []
|
|
1128
|
+
chapters_list: list[tuple[str, str]] = []
|
|
1130
1129
|
|
|
1131
1130
|
i = 0
|
|
1132
1131
|
while i < len(chapter_lines):
|
|
@@ -1418,11 +1417,11 @@ def info(filepath: Path, format: str) -> None:
|
|
|
1418
1417
|
)
|
|
1419
1418
|
def read(
|
|
1420
1419
|
filepath: Path,
|
|
1421
|
-
chapter:
|
|
1422
|
-
line:
|
|
1420
|
+
chapter: int | None,
|
|
1421
|
+
line: int | None,
|
|
1423
1422
|
resume: bool,
|
|
1424
|
-
bookmark_file:
|
|
1425
|
-
page_size:
|
|
1423
|
+
bookmark_file: Path | None,
|
|
1424
|
+
page_size: int | None,
|
|
1426
1425
|
no_header: bool,
|
|
1427
1426
|
no_footer: bool,
|
|
1428
1427
|
sentences: bool,
|
|
@@ -1432,7 +1431,7 @@ def read(
|
|
|
1432
1431
|
raw: bool,
|
|
1433
1432
|
keep_footnotes: bool,
|
|
1434
1433
|
keep_page_numbers: bool,
|
|
1435
|
-
width:
|
|
1434
|
+
width: int | None,
|
|
1436
1435
|
) -> None:
|
|
1437
1436
|
"""
|
|
1438
1437
|
Interactively read an EPUB file in the terminal.
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class PageSource(Enum):
|
|
@@ -25,8 +24,8 @@ class Page:
|
|
|
25
24
|
text: str
|
|
26
25
|
char_count: int
|
|
27
26
|
source: PageSource
|
|
28
|
-
chapter_id:
|
|
29
|
-
chapter_title:
|
|
27
|
+
chapter_id: str | None = None # Which chapter this page belongs to
|
|
28
|
+
chapter_title: str | None = None # Title of the chapter
|
|
30
29
|
|
|
31
30
|
def __str__(self) -> str:
|
|
32
31
|
source_str = "print" if self.source == PageSource.EPUB_PAGE_LIST else "syn"
|
|
@@ -41,7 +40,7 @@ class Chapter:
|
|
|
41
40
|
title: str
|
|
42
41
|
text: str
|
|
43
42
|
char_count: int
|
|
44
|
-
parent_id:
|
|
43
|
+
parent_id: str | None = None
|
|
45
44
|
level: int = 0
|
|
46
45
|
|
|
47
46
|
def __str__(self) -> str:
|
|
@@ -52,18 +51,18 @@ class Chapter:
|
|
|
52
51
|
class Metadata:
|
|
53
52
|
"""EPUB metadata."""
|
|
54
53
|
|
|
55
|
-
title:
|
|
54
|
+
title: str | None = None
|
|
56
55
|
authors: list[str] = field(default_factory=list)
|
|
57
|
-
publisher:
|
|
58
|
-
publication_year:
|
|
59
|
-
description:
|
|
56
|
+
publisher: str | None = None
|
|
57
|
+
publication_year: str | None = None
|
|
58
|
+
description: str | None = None
|
|
60
59
|
# Required EPUB3 metadata
|
|
61
|
-
identifier:
|
|
62
|
-
language:
|
|
60
|
+
identifier: str | None = None
|
|
61
|
+
language: str | None = None
|
|
63
62
|
# Optional Dublin Core metadata
|
|
64
63
|
contributors: list[str] = field(default_factory=list)
|
|
65
|
-
rights:
|
|
66
|
-
coverage:
|
|
64
|
+
rights: str | None = None
|
|
65
|
+
coverage: str | None = None
|
|
67
66
|
|
|
68
67
|
def __str__(self) -> str:
|
|
69
68
|
lines = []
|
|
@@ -7,7 +7,7 @@ import logging
|
|
|
7
7
|
import re
|
|
8
8
|
import urllib.parse
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
import ebooklib # type: ignore[import-untyped]
|
|
13
13
|
from bs4 import BeautifulSoup, NavigableString # type: ignore[import-untyped]
|
|
@@ -54,11 +54,11 @@ class EPUBParser:
|
|
|
54
54
|
self.content_texts: dict[str, str] = {}
|
|
55
55
|
self.content_lengths: dict[str, int] = {}
|
|
56
56
|
self.processed_nav_structure: list[dict[str, Any]] = []
|
|
57
|
-
self._metadata:
|
|
58
|
-
self._page_list_nav:
|
|
57
|
+
self._metadata: Metadata | None = None
|
|
58
|
+
self._page_list_nav: tuple[Any, str] | None = None
|
|
59
59
|
self._page_list_nav_checked = False
|
|
60
60
|
self._nav_processed = False
|
|
61
|
-
self._upper_title_cache:
|
|
61
|
+
self._upper_title_cache: set[str] | None = None
|
|
62
62
|
|
|
63
63
|
self._load_epub()
|
|
64
64
|
|
|
@@ -84,7 +84,7 @@ class EPUBParser:
|
|
|
84
84
|
except Exception as e:
|
|
85
85
|
raise ValueError(f"Failed to read EPUB file: {e}") from e
|
|
86
86
|
|
|
87
|
-
def _get_single_metadata(self, field: str) ->
|
|
87
|
+
def _get_single_metadata(self, field: str) -> str | None:
|
|
88
88
|
"""Extract a single metadata value from Dublin Core field."""
|
|
89
89
|
try:
|
|
90
90
|
items = self.book.get_metadata("DC", field)
|
|
@@ -163,7 +163,7 @@ class EPUBParser:
|
|
|
163
163
|
self,
|
|
164
164
|
nav_structure: list[dict[str, Any]],
|
|
165
165
|
chapters: list[Chapter],
|
|
166
|
-
parent_id:
|
|
166
|
+
parent_id: str | None = None,
|
|
167
167
|
level: int = 1,
|
|
168
168
|
include_text: bool = True,
|
|
169
169
|
) -> None:
|
|
@@ -178,7 +178,7 @@ class EPUBParser:
|
|
|
178
178
|
include_text: If True, include full text; if False, only metadata
|
|
179
179
|
"""
|
|
180
180
|
for entry in nav_structure:
|
|
181
|
-
src:
|
|
181
|
+
src: str | None = entry.get("src")
|
|
182
182
|
title: str = entry.get("title", "Untitled")
|
|
183
183
|
children: list[dict[str, Any]] = entry.get("children", [])
|
|
184
184
|
|
|
@@ -438,12 +438,12 @@ class EPUBParser:
|
|
|
438
438
|
|
|
439
439
|
# Handle ordered lists
|
|
440
440
|
for ol in slice_soup.find_all("ol"):
|
|
441
|
-
start_attr = ol.get("start")
|
|
442
|
-
start_num = int(start_attr) if start_attr else 1
|
|
441
|
+
start_attr = ol.get("start")
|
|
442
|
+
start_num = int(start_attr) if start_attr else 1
|
|
443
443
|
for li_idx, li in enumerate(ol.find_all("li", recursive=False)):
|
|
444
444
|
number_text = f"{start_num + li_idx}) "
|
|
445
445
|
if li.string:
|
|
446
|
-
li.string.replace_with(number_text + str(li.string))
|
|
446
|
+
li.string.replace_with(number_text + str(li.string))
|
|
447
447
|
else:
|
|
448
448
|
li.insert(0, NavigableString(number_text))
|
|
449
449
|
|
|
@@ -510,7 +510,7 @@ class EPUBParser:
|
|
|
510
510
|
base_href: str,
|
|
511
511
|
doc_order: dict[str, int],
|
|
512
512
|
doc_order_decoded: dict[str, int],
|
|
513
|
-
) -> tuple[
|
|
513
|
+
) -> tuple[str | None, int | None]:
|
|
514
514
|
"""Find the best matching doc_key for a given base_href."""
|
|
515
515
|
import os
|
|
516
516
|
|
|
@@ -606,7 +606,7 @@ class EPUBParser:
|
|
|
606
606
|
link = li_element.find("a", recursive=False)
|
|
607
607
|
span_text = li_element.find("span", recursive=False)
|
|
608
608
|
title = "Untitled Section"
|
|
609
|
-
src:
|
|
609
|
+
src: str | None = None
|
|
610
610
|
current_entry_node: dict[str, Any] = {"children": []}
|
|
611
611
|
|
|
612
612
|
if link and "href" in link.attrs:
|
|
@@ -673,7 +673,7 @@ class EPUBParser:
|
|
|
673
673
|
)
|
|
674
674
|
tree_structure_list.append(current_entry_node)
|
|
675
675
|
|
|
676
|
-
def _find_position_robust(self, doc_href: str, fragment_id:
|
|
676
|
+
def _find_position_robust(self, doc_href: str, fragment_id: str | None) -> int:
|
|
677
677
|
"""
|
|
678
678
|
Find the position of a fragment ID within a document.
|
|
679
679
|
|
|
@@ -835,7 +835,7 @@ class EPUBParser:
|
|
|
835
835
|
|
|
836
836
|
def extract_chapters(
|
|
837
837
|
self,
|
|
838
|
-
chapter_ids:
|
|
838
|
+
chapter_ids: list[str] | None = None,
|
|
839
839
|
deduplicate_chapter_titles: bool = True,
|
|
840
840
|
skip_toc: bool = False,
|
|
841
841
|
include_chapter_title: bool = False,
|
|
@@ -881,7 +881,7 @@ class EPUBParser:
|
|
|
881
881
|
selected = filtered
|
|
882
882
|
|
|
883
883
|
# Combine text with chapter titles
|
|
884
|
-
parts = []
|
|
884
|
+
parts: list[str] = []
|
|
885
885
|
for i, chapter in enumerate(selected):
|
|
886
886
|
if chapter.text:
|
|
887
887
|
# Remove duplicate title if requested
|
|
@@ -944,7 +944,7 @@ class EPUBParser:
|
|
|
944
944
|
"""
|
|
945
945
|
return bool(self._find_page_list_nav())
|
|
946
946
|
|
|
947
|
-
def _find_page_list_nav(self) ->
|
|
947
|
+
def _find_page_list_nav(self) -> tuple[Any, str] | None:
|
|
948
948
|
"""
|
|
949
949
|
Find the page-list navigation element in the EPUB.
|
|
950
950
|
|
|
@@ -1155,8 +1155,8 @@ class EPUBParser:
|
|
|
1155
1155
|
page_num = 1
|
|
1156
1156
|
current_sentences: list[str] = []
|
|
1157
1157
|
current_size = 0 # Current size in chars or words
|
|
1158
|
-
current_chapter_id:
|
|
1159
|
-
current_chapter_title:
|
|
1158
|
+
current_chapter_id: str | None = None
|
|
1159
|
+
current_chapter_title: str | None = None
|
|
1160
1160
|
|
|
1161
1161
|
def get_size(text: str) -> int:
|
|
1162
1162
|
"""Get size of text in chars or words."""
|
|
@@ -1362,15 +1362,15 @@ class EPUBParser:
|
|
|
1362
1362
|
doc_href: str,
|
|
1363
1363
|
position: int,
|
|
1364
1364
|
chapter_map: list[tuple[str, int, str, str]],
|
|
1365
|
-
) -> tuple[
|
|
1365
|
+
) -> tuple[str | None, str | None]:
|
|
1366
1366
|
"""
|
|
1367
1367
|
Find which chapter a given position belongs to.
|
|
1368
1368
|
|
|
1369
1369
|
Returns:
|
|
1370
1370
|
Tuple of (chapter_id, chapter_title) or (None, None)
|
|
1371
1371
|
"""
|
|
1372
|
-
result_id:
|
|
1373
|
-
result_title:
|
|
1372
|
+
result_id: str | None = None
|
|
1373
|
+
result_title: str | None = None
|
|
1374
1374
|
|
|
1375
1375
|
for ch_doc, ch_pos, ch_id, ch_title in chapter_map:
|
|
1376
1376
|
if ch_doc == doc_href and ch_pos <= position:
|
|
@@ -1381,7 +1381,7 @@ class EPUBParser:
|
|
|
1381
1381
|
|
|
1382
1382
|
return result_id, result_title
|
|
1383
1383
|
|
|
1384
|
-
def _get_spine_index(self, doc_href: str, spine_docs: list[str]) ->
|
|
1384
|
+
def _get_spine_index(self, doc_href: str, spine_docs: list[str]) -> int | None:
|
|
1385
1385
|
"""Get the index of a document in the spine, logging if missing."""
|
|
1386
1386
|
try:
|
|
1387
1387
|
return spine_docs.index(doc_href)
|
|
@@ -1393,8 +1393,8 @@ class EPUBParser:
|
|
|
1393
1393
|
self,
|
|
1394
1394
|
current_doc: str,
|
|
1395
1395
|
current_pos: int,
|
|
1396
|
-
next_doc:
|
|
1397
|
-
next_pos:
|
|
1396
|
+
next_doc: str | None,
|
|
1397
|
+
next_pos: int | None,
|
|
1398
1398
|
spine_docs: list[str],
|
|
1399
1399
|
*,
|
|
1400
1400
|
allow_wraparound: bool,
|
|
@@ -1439,7 +1439,7 @@ class EPUBParser:
|
|
|
1439
1439
|
def _extract_text_between_positions(
|
|
1440
1440
|
self,
|
|
1441
1441
|
current: dict[str, Any],
|
|
1442
|
-
next_entry:
|
|
1442
|
+
next_entry: dict[str, Any] | None,
|
|
1443
1443
|
spine_docs: list[str],
|
|
1444
1444
|
) -> str:
|
|
1445
1445
|
"""
|
|
@@ -1488,7 +1488,7 @@ class EPUBParser:
|
|
|
1488
1488
|
|
|
1489
1489
|
def extract_pages(
|
|
1490
1490
|
self,
|
|
1491
|
-
page_numbers:
|
|
1491
|
+
page_numbers: list[str] | None = None,
|
|
1492
1492
|
deduplicate_chapter_titles: bool = True,
|
|
1493
1493
|
skip_toc: bool = False,
|
|
1494
1494
|
) -> str:
|
|
@@ -1518,8 +1518,8 @@ class EPUBParser:
|
|
|
1518
1518
|
# Note: We don't filter out entire pages here anymore
|
|
1519
1519
|
# Instead, we strip TOC content from pages below
|
|
1520
1520
|
|
|
1521
|
-
parts = []
|
|
1522
|
-
current_chapter:
|
|
1521
|
+
parts: list[str] = []
|
|
1522
|
+
current_chapter: str | None = None
|
|
1523
1523
|
|
|
1524
1524
|
for page in selected:
|
|
1525
1525
|
if not page.text:
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import signal
|
|
4
4
|
import sys
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
import click
|
|
9
8
|
from rich.align import Align
|
|
@@ -71,13 +70,13 @@ class EpubReader:
|
|
|
71
70
|
chapters: list[Chapter],
|
|
72
71
|
title: str,
|
|
73
72
|
epub_path: str,
|
|
74
|
-
page_size:
|
|
73
|
+
page_size: int | None = None,
|
|
75
74
|
show_header: bool = True,
|
|
76
75
|
show_footer: bool = True,
|
|
77
76
|
start_line: int = 0,
|
|
78
|
-
start_chapter:
|
|
79
|
-
bookmark_manager:
|
|
80
|
-
width:
|
|
77
|
+
start_chapter: int | None = None,
|
|
78
|
+
bookmark_manager: BookmarkManager | None = None,
|
|
79
|
+
width: int | None = None,
|
|
81
80
|
) -> None:
|
|
82
81
|
"""
|
|
83
82
|
Initialize the EPUB reader.
|
|
@@ -120,7 +119,7 @@ class EpubReader:
|
|
|
120
119
|
|
|
121
120
|
# State
|
|
122
121
|
self._show_help = False
|
|
123
|
-
self._message:
|
|
122
|
+
self._message: str | None = None
|
|
124
123
|
self._message_style: str = "green"
|
|
125
124
|
self._running = False
|
|
126
125
|
|
|
@@ -402,7 +401,7 @@ class EpubReader:
|
|
|
402
401
|
|
|
403
402
|
return True
|
|
404
403
|
|
|
405
|
-
def _get_effective_width(self) ->
|
|
404
|
+
def _get_effective_width(self) -> int | None:
|
|
406
405
|
"""Get effective content width, capped by terminal width."""
|
|
407
406
|
if self._width is None:
|
|
408
407
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epub2text
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A simple tool to extract text from EPUB files.
|
|
5
5
|
Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.14
|
|
39
39
|
Classifier: Topic :: Software Development :: Libraries
|
|
40
|
-
Requires-Python: >=3.
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
License-File: LICENSE
|
|
43
43
|
Requires-Dist: click>=8.0.0
|
|
@@ -16,9 +16,8 @@ Run with: python -m examples.demo
|
|
|
16
16
|
import tempfile
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
|
-
import pypub
|
|
20
|
+
import pypub
|
|
22
21
|
|
|
23
22
|
from epub2text import (
|
|
24
23
|
Bookmark,
|
|
@@ -43,7 +42,7 @@ from epub2text.formatters import (
|
|
|
43
42
|
# =============================================================================
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def create_sample_epub(output_path:
|
|
45
|
+
def create_sample_epub(output_path: Path | None = None) -> Path:
|
|
47
46
|
"""
|
|
48
47
|
Create a sample EPUB file for demonstration purposes.
|
|
49
48
|
|
|
@@ -10,7 +10,7 @@ include = ["epub2text*"]
|
|
|
10
10
|
name = "epub2text"
|
|
11
11
|
description = "A simple tool to extract text from EPUB files."
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
license = {file = "LICENSE"}
|
|
15
15
|
authors = [
|
|
16
16
|
{name = "Holger Nahrstaedt", email = "nahrstaedt@gmail.com"}
|
|
@@ -67,7 +67,7 @@ commands =
|
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
[tool.mypy]
|
|
70
|
-
python_version = "3.
|
|
70
|
+
python_version = "3.10"
|
|
71
71
|
warn_return_any = true
|
|
72
72
|
warn_unused_configs = true
|
|
73
73
|
disallow_untyped_defs = true
|
|
@@ -80,8 +80,21 @@ warn_no_return = true
|
|
|
80
80
|
warn_unreachable = true
|
|
81
81
|
strict_equality = true
|
|
82
82
|
exclude = [
|
|
83
|
+
"^build/",
|
|
84
|
+
"^epub2text\\.egg-info/",
|
|
83
85
|
]
|
|
84
86
|
|
|
87
|
+
[[tool.mypy.overrides]]
|
|
88
|
+
module = [
|
|
89
|
+
"defusedxml",
|
|
90
|
+
"defusedxml.*",
|
|
91
|
+
"phrasplit",
|
|
92
|
+
"pypub",
|
|
93
|
+
"pypub.*",
|
|
94
|
+
"setuptools",
|
|
95
|
+
]
|
|
96
|
+
ignore_missing_imports = true
|
|
97
|
+
|
|
85
98
|
[tool.git-cliff.changelog]
|
|
86
99
|
# A Tera template to be rendered as the changelog's header.
|
|
87
100
|
# See https://keats.github.io/tera/docs/#introduction
|
|
@@ -11,7 +11,7 @@ from functools import partial
|
|
|
11
11
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
import pypub
|
|
14
|
+
import pypub
|
|
15
15
|
import pytest
|
|
16
16
|
|
|
17
17
|
from epub2text import epub2txt
|
|
@@ -42,7 +42,8 @@ def serve_directory(
|
|
|
42
42
|
thread.start()
|
|
43
43
|
try:
|
|
44
44
|
host, port = server.server_address[:2]
|
|
45
|
-
|
|
45
|
+
host_value = host.decode() if isinstance(host, bytes) else host
|
|
46
|
+
yield f"http://{host_value}:{port}"
|
|
46
47
|
finally:
|
|
47
48
|
# Stop the loop, then close the listening socket to avoid FD/memory growth
|
|
48
49
|
with contextlib.suppress(Exception):
|
|
@@ -68,6 +69,7 @@ def test_epub2txt_downloads_from_url(tmp_path: Path) -> None:
|
|
|
68
69
|
|
|
69
70
|
with serve_directory(tmp_path) as base_url:
|
|
70
71
|
text = epub2txt(f"{base_url}/{epub_path.name}")
|
|
72
|
+
assert isinstance(text, str)
|
|
71
73
|
|
|
72
74
|
assert "downloadable epub" in text.lower()
|
|
73
75
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|