ebookerr-sdk 3.2.0__tar.gz → 3.3.0__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.
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/PKG-INFO +9 -3
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/README.md +8 -2
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/__init__.py +1 -1
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/document.py +139 -17
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/build.py +154 -117
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/document.py +43 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/nav.py +11 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/ncx.py +14 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/text_convert.py +80 -21
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/declarations.py +1 -1
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk.egg-info/PKG-INFO +9 -3
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/pyproject.toml +1 -1
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/LICENSE +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/adoption.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/chapter_naming.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/chapter_number.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/dates.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/epub_hash.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/epub_metadata.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/ids.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/metadata.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/story_url.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/survivor.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/domain/text_encoding.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/availability.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/check.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/disposition.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/identity.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/paths.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/download/routing.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/archive.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/assets.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/builder.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/chapter_numbers.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/chapters.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/errors.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/fonts.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/opf.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/roles.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/safe_xml.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/serialize.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/text.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/titles.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/epub/xhtml.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/generation/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/generation/client.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/generation/output.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/generation/prompt_template.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/generation/support.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/host.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/__main__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/build.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/changelog.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/check.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/cli.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/files.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/imports.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/index.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/pack/scaffold.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/anchor_types.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/anchoring.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/connection.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/link_attempt.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/link_refusal.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/restore_marker.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/scan_ledger.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/providers/urls.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/py.typed +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/readpos.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/registry_index.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/context.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/errors.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/glyphs.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/manifest.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/material_symbols.txt +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/protocols.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/settings.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/summary_template.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/types.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/spi/views.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/testing/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/testing/context.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/testing/views.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/testing/wire.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/__init__.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/external.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/model.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/rules_css.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/rules_html.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/rules_nav.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/rules_opf.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/rules_package.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/validate/validate.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk/wire.py +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk.egg-info/SOURCES.txt +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk.egg-info/dependency_links.txt +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk.egg-info/requires.txt +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/ebookerr_sdk.egg-info/top_level.txt +0 -0
- {ebookerr_sdk-3.2.0 → ebookerr_sdk-3.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ebookerr-sdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: The ebookerr plugin SDK: the plugin contract, the plugin host, a test kit, the pack tool and helper kits.
|
|
5
5
|
Author: ArcCdr
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,10 +41,10 @@ validation, library servers and text generation.
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
pip install "ebookerr-sdk==3.
|
|
44
|
+
pip install "ebookerr-sdk==3.3.*"
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.
|
|
47
|
+
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.3.x speaks SPI 3.3. It needs
|
|
48
48
|
Python 3.11 or later.
|
|
49
49
|
|
|
50
50
|
## Start a plugin
|
|
@@ -66,6 +66,7 @@ or `catalog`. The guide explains what to write next and how to install the zip i
|
|
|
66
66
|
| 2.24 | 3.0 | 3.0.x |
|
|
67
67
|
| 2.24.1 | 3.1 | 3.1.x |
|
|
68
68
|
| 2.24.7 | 3.2 | 3.2.x |
|
|
69
|
+
| 2.24.9 | 3.3 | 3.3.x |
|
|
69
70
|
|
|
70
71
|
ebookerr loads a plugin when its `spi_version` has ebookerr's SPI MAJOR and a MINOR not above
|
|
71
72
|
ebookerr's; otherwise it shows the plugin as incompatible and offers a compatible release.
|
|
@@ -93,6 +94,11 @@ ebookerr's; otherwise it shows the plugin as incompatible and offers a compatibl
|
|
|
93
94
|
|
|
94
95
|
## Release notes
|
|
95
96
|
|
|
97
|
+
### 3.3.0
|
|
98
|
+
|
|
99
|
+
SPI 3.3. The EPUB kit gains helpers for the EPUBs ebookerr writes; later sentences of this
|
|
100
|
+
section name each one. A plugin written for SPI 3.2 needs no change. `build_epub` writes its package through `ebookerr_sdk.epub.builder`: the NCX `dtb:uid` is the OPF identifier and the package carries its creation date, so a built book validates without findings. `EpubDocument.set_title` writes a new title into the package and every table of contents (`NcxDocument.set_doc_title`, `NavDocument.set_doc_title`), so a retitled book's OPF and NCX agree. `retitle_built_epub` gives a `build_epub` output a new title in its package, tables of contents and title page; `convert_stage` and `download_convert_stage` use it, so a converted book's file carries the title the book gets. `ebookerr_sdk.epub.text_convert.is_chapter_heading` names the prose chapter-heading rule. `convert_stage` titles an uploaded document after its own title, else its file name unless the name is a placeholder such as `Document1` or `Untitled`, else a first line that is no chapter heading. `Book.front_matter` holds the text above a document's first heading; `build_epub` puts it on the title page, and `parse_text_to_book` fills it instead of making that text a chapter. `parse_text_to_book` keeps each line of a text that has no blank line at all as its own paragraph. `convert_stage` and `download_convert_stage` pass a `user_facing` conversion error's message through unchanged and answer "The file could not be converted." for any other failure, so no library message or path reaches a user. `EpubDocument.drm_protected()` tells a DRM-protected book (a `rights.xml`, or an `encryption.xml` using anything but font obfuscation) from one whose fonts are only obfuscated.
|
|
101
|
+
|
|
96
102
|
### 3.2.0
|
|
97
103
|
|
|
98
104
|
SPI 3.2. A story can name the multi-part work it is one part of: `StoryPatch.work` and
|
|
@@ -12,10 +12,10 @@ validation, library servers and text generation.
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
pip install "ebookerr-sdk==3.
|
|
15
|
+
pip install "ebookerr-sdk==3.3.*"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.
|
|
18
|
+
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.3.x speaks SPI 3.3. It needs
|
|
19
19
|
Python 3.11 or later.
|
|
20
20
|
|
|
21
21
|
## Start a plugin
|
|
@@ -37,6 +37,7 @@ or `catalog`. The guide explains what to write next and how to install the zip i
|
|
|
37
37
|
| 2.24 | 3.0 | 3.0.x |
|
|
38
38
|
| 2.24.1 | 3.1 | 3.1.x |
|
|
39
39
|
| 2.24.7 | 3.2 | 3.2.x |
|
|
40
|
+
| 2.24.9 | 3.3 | 3.3.x |
|
|
40
41
|
|
|
41
42
|
ebookerr loads a plugin when its `spi_version` has ebookerr's SPI MAJOR and a MINOR not above
|
|
42
43
|
ebookerr's; otherwise it shows the plugin as incompatible and offers a compatible release.
|
|
@@ -64,6 +65,11 @@ ebookerr's; otherwise it shows the plugin as incompatible and offers a compatibl
|
|
|
64
65
|
|
|
65
66
|
## Release notes
|
|
66
67
|
|
|
68
|
+
### 3.3.0
|
|
69
|
+
|
|
70
|
+
SPI 3.3. The EPUB kit gains helpers for the EPUBs ebookerr writes; later sentences of this
|
|
71
|
+
section name each one. A plugin written for SPI 3.2 needs no change. `build_epub` writes its package through `ebookerr_sdk.epub.builder`: the NCX `dtb:uid` is the OPF identifier and the package carries its creation date, so a built book validates without findings. `EpubDocument.set_title` writes a new title into the package and every table of contents (`NcxDocument.set_doc_title`, `NavDocument.set_doc_title`), so a retitled book's OPF and NCX agree. `retitle_built_epub` gives a `build_epub` output a new title in its package, tables of contents and title page; `convert_stage` and `download_convert_stage` use it, so a converted book's file carries the title the book gets. `ebookerr_sdk.epub.text_convert.is_chapter_heading` names the prose chapter-heading rule. `convert_stage` titles an uploaded document after its own title, else its file name unless the name is a placeholder such as `Document1` or `Untitled`, else a first line that is no chapter heading. `Book.front_matter` holds the text above a document's first heading; `build_epub` puts it on the title page, and `parse_text_to_book` fills it instead of making that text a chapter. `parse_text_to_book` keeps each line of a text that has no blank line at all as its own paragraph. `convert_stage` and `download_convert_stage` pass a `user_facing` conversion error's message through unchanged and answer "The file could not be converted." for any other failure, so no library message or path reaches a user. `EpubDocument.drm_protected()` tells a DRM-protected book (a `rights.xml`, or an `encryption.xml` using anything but font obfuscation) from one whose fonts are only obfuscated.
|
|
72
|
+
|
|
67
73
|
### 3.2.0
|
|
68
74
|
|
|
69
75
|
SPI 3.2. A story can name the multi-part work it is one part of: `StoryPatch.work` and
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import hashlib
|
|
11
11
|
import logging
|
|
12
|
+
import re
|
|
12
13
|
from collections.abc import Callable, Mapping
|
|
13
14
|
from contextlib import AbstractContextManager, nullcontext
|
|
14
15
|
from pathlib import Path
|
|
@@ -26,7 +27,9 @@ from ebookerr_sdk.download.check import (
|
|
|
26
27
|
from ebookerr_sdk.download.disposition import parse_content_disposition_filename
|
|
27
28
|
from ebookerr_sdk.download.identity import USER_AGENT as _UA
|
|
28
29
|
from ebookerr_sdk.download.paths import safe_component as _safe_component
|
|
29
|
-
from ebookerr_sdk.epub.build import ConversionResult, DocumentConversionError
|
|
30
|
+
from ebookerr_sdk.epub.build import ConversionResult, DocumentConversionError, retitle_built_epub
|
|
31
|
+
from ebookerr_sdk.epub.errors import EpubError
|
|
32
|
+
from ebookerr_sdk.epub.text_convert import is_chapter_heading
|
|
30
33
|
from ebookerr_sdk.spi import (
|
|
31
34
|
BookPatch,
|
|
32
35
|
BookView,
|
|
@@ -41,6 +44,14 @@ from ebookerr_sdk.spi import (
|
|
|
41
44
|
|
|
42
45
|
_JUNK_AUTHORS = frozenset({"windows user", "user", "administrator", "unknown"})
|
|
43
46
|
_MAX_FIRST_LINE_TITLE = 120
|
|
47
|
+
_COULD_NOT_CONVERT = "The file could not be converted."
|
|
48
|
+
"""The reason a failed conversion gives when the converter named no cause (EXP-297)."""
|
|
49
|
+
_JUNK_FILE_NAME_RE = re.compile(
|
|
50
|
+
r"(?:(?:new )?(?:microsoft word |text )?document|doc|untitled(?: document)?"
|
|
51
|
+
r"|scan|img|image|dsc|pxl)[\s_-]*[\d_]*(?:\s*\(\d+\))?",
|
|
52
|
+
re.IGNORECASE,
|
|
53
|
+
)
|
|
54
|
+
"""A file stem nobody chose: Document1, Untitled 2, New Text Document (3), scan_0023 (EXP-304)."""
|
|
44
55
|
|
|
45
56
|
|
|
46
57
|
def _refusal(host: str, prior: BookView | None) -> str:
|
|
@@ -95,10 +106,58 @@ def _screen_author(raw: str | None, log: logging.Logger) -> str:
|
|
|
95
106
|
return author
|
|
96
107
|
|
|
97
108
|
|
|
109
|
+
def _is_junk_file_name(stem: str) -> bool:
|
|
110
|
+
"""Whether *stem* is a name nobody chose for a document (``EXP-304``).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
stem: A file name without its extension.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True for names like ``Document1``, ``Untitled``, ``New Document``, ``scan_0023``.
|
|
117
|
+
"""
|
|
118
|
+
return bool(_JUNK_FILE_NAME_RE.fullmatch(stem.strip()))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _upload_title(result: ConversionResult, stem: str, log: logging.Logger) -> str:
|
|
122
|
+
"""Return an uploaded document's title (``EXP-304``).
|
|
123
|
+
|
|
124
|
+
Its own title (not the converter's file-name fallback); else the file name the user gave
|
|
125
|
+
it, unless that name names nothing (:func:`_is_junk_file_name`); else its first non-empty
|
|
126
|
+
line when that is no chapter heading and at most ``_MAX_FIRST_LINE_TITLE`` characters; else
|
|
127
|
+
the file name after all. The pull path keeps :func:`_document_title` (``EXP-224``).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
result: What the converter reported.
|
|
131
|
+
stem: The uploaded file's name without its extension.
|
|
132
|
+
log: The plugin's logger (the choice is logged at DEBUG).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The sanitised title.
|
|
136
|
+
"""
|
|
137
|
+
embedded = "" if result.title_from_filename else sanitize(result.title)
|
|
138
|
+
if embedded:
|
|
139
|
+
return embedded
|
|
140
|
+
name = sanitize(stem) or stem
|
|
141
|
+
if not _is_junk_file_name(name):
|
|
142
|
+
log.debug("Title taken from the file name: %r", name)
|
|
143
|
+
return name
|
|
144
|
+
first_line = sanitize(result.first_line or "") or ""
|
|
145
|
+
usable = len(first_line) <= _MAX_FIRST_LINE_TITLE and not is_chapter_heading(first_line)
|
|
146
|
+
if first_line and usable:
|
|
147
|
+
log.debug(
|
|
148
|
+
"Title taken from the first line %r (the file name %r names nothing)", first_line, name
|
|
149
|
+
)
|
|
150
|
+
return first_line
|
|
151
|
+
log.debug("Title fell back to the file name %r (no document title, no usable first line)", name)
|
|
152
|
+
return name
|
|
153
|
+
|
|
154
|
+
|
|
98
155
|
def _document_title(
|
|
99
156
|
result: ConversionResult, fallback: str, log: logging.Logger, *, fallback_label: str
|
|
100
157
|
) -> str:
|
|
101
|
-
"""Return the converted document's title by the ``EXP-224`` precedence.
|
|
158
|
+
"""Return the converted document's title by the ``EXP-224`` pull-path precedence.
|
|
159
|
+
|
|
160
|
+
Used by the pull path only; an upload is titled by :func:`_upload_title`.
|
|
102
161
|
|
|
103
162
|
The document's own title (ignored when the converter says it is only the file name), then
|
|
104
163
|
its first non-empty line when it is at most ``_MAX_FIRST_LINE_TITLE`` characters, then
|
|
@@ -132,6 +191,67 @@ def _document_title(
|
|
|
132
191
|
return title
|
|
133
192
|
|
|
134
193
|
|
|
194
|
+
def _write_chosen_title(epub_path: Path, built: str, title: str, log: logging.Logger) -> None:
|
|
195
|
+
"""Write the stage's chosen *title* into the EPUB when the converter built another (EXP-304).
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
epub_path: The converted EPUB, still in its work folder.
|
|
199
|
+
built: The title the converter built the EPUB with (``ConversionResult.title``).
|
|
200
|
+
title: The title the stage chose for the book.
|
|
201
|
+
log: The plugin's logger.
|
|
202
|
+
"""
|
|
203
|
+
if title == built:
|
|
204
|
+
return
|
|
205
|
+
try:
|
|
206
|
+
retitle_built_epub(epub_path, title)
|
|
207
|
+
except EpubError as exc:
|
|
208
|
+
log.warning("Could not write the title %r into %s: %s", title, epub_path.name, exc)
|
|
209
|
+
return
|
|
210
|
+
log.debug("Retitled the converted EPUB from %r to %r", built, title)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _run_converter(
|
|
214
|
+
convert: Callable[[Path, Path], ConversionResult],
|
|
215
|
+
src: Path,
|
|
216
|
+
epub_path: Path,
|
|
217
|
+
log: logging.Logger,
|
|
218
|
+
log_label: str,
|
|
219
|
+
) -> ConversionResult:
|
|
220
|
+
"""Run the converter; every failure becomes a ``SourcePullError`` with a sentence (EXP-297).
|
|
221
|
+
|
|
222
|
+
A ``DocumentConversionError`` whose class sets ``user_facing = True`` carries the
|
|
223
|
+
converter's own sentence (damaged, password-protected, no text) and is passed through as
|
|
224
|
+
it is. Any other ``DocumentConversionError`` is logged at WARNING with its message, any
|
|
225
|
+
other exception at ERROR with its traceback (a converter defect); both answer
|
|
226
|
+
:data:`_COULD_NOT_CONVERT`. A library's message, an exception class name or a path never
|
|
227
|
+
reaches the reason.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
convert: The converter, ``(source_path, epub_path) -> ConversionResult``.
|
|
231
|
+
src: The document to convert.
|
|
232
|
+
epub_path: Where the EPUB is written.
|
|
233
|
+
log: The plugin's logger.
|
|
234
|
+
log_label: The format's name in log lines (e.g. ``"PDF"``).
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
What the converter reported.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
SourcePullError: The conversion failed; its message is a sentence for the user.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
return convert(src, epub_path)
|
|
244
|
+
except DocumentConversionError as exc:
|
|
245
|
+
if getattr(exc, "user_facing", False) is True:
|
|
246
|
+
log.debug("%s conversion of %s refused: %s", log_label, src.name, exc)
|
|
247
|
+
raise SourcePullError(str(exc)) from exc
|
|
248
|
+
log.warning("%s conversion of %s failed: %s", log_label, src.name, exc)
|
|
249
|
+
raise SourcePullError(_COULD_NOT_CONVERT) from exc
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
log.exception("%s conversion of %s failed unexpectedly", log_label, src.name)
|
|
252
|
+
raise SourcePullError(_COULD_NOT_CONVERT) from exc
|
|
253
|
+
|
|
254
|
+
|
|
135
255
|
def _stage_under_author(work_dir: Path, epub_filename: str, author: str) -> str:
|
|
136
256
|
"""Move ``work_dir/epub_filename`` into its author folder and return the relative path.
|
|
137
257
|
|
|
@@ -178,6 +298,8 @@ def download_convert_stage( # noqa: C901 — download/verify/convert/stage step
|
|
|
178
298
|
then finally the basename's stem. The author is
|
|
179
299
|
screened against a junk-author list (``windows user``, ``user``, ``administrator``,
|
|
180
300
|
``unknown``); if matched (case-insensitive) or empty, it becomes ``"Unknown"``.
|
|
301
|
+
The chosen title is written into the EPUB (package, table of contents, title page) when
|
|
302
|
+
the converter built it with another one.
|
|
181
303
|
|
|
182
304
|
Args:
|
|
183
305
|
url: URL to download
|
|
@@ -200,7 +322,9 @@ def download_convert_stage( # noqa: C901 — download/verify/convert/stage step
|
|
|
200
322
|
URL is the download URL itself — a one-file download is one chapter at one address.
|
|
201
323
|
|
|
202
324
|
Raises:
|
|
203
|
-
SourcePullError: On download, conversion, or staging errors
|
|
325
|
+
SourcePullError: On download, conversion, or staging errors; a conversion failure's
|
|
326
|
+
message is the converter's own sentence, else "The file could not be converted."
|
|
327
|
+
(EXP-297).
|
|
204
328
|
ContentTypeMismatchError: When the server's Content-Type contradicts the claimed format.
|
|
205
329
|
TaskCancelled: If the user cancelled the task at one of the two checkpoints.
|
|
206
330
|
"""
|
|
@@ -346,11 +470,8 @@ def download_convert_stage( # noqa: C901 — download/verify/convert/stage step
|
|
|
346
470
|
file_size = src_path.stat().st_size
|
|
347
471
|
log.debug("Converting %s to EPUB: %s (%d bytes)", log_label, src_path.name, file_size)
|
|
348
472
|
|
|
349
|
-
# Convert to EPUB
|
|
350
|
-
|
|
351
|
-
result = convert(src_path, work_dir / epub_filename)
|
|
352
|
-
except DocumentConversionError as exc:
|
|
353
|
-
raise SourcePullError(f"{log_label} conversion failed: {exc}") from exc
|
|
473
|
+
# Convert to EPUB; a failure is a sentence, never a library message (EXP-297)
|
|
474
|
+
result = _run_converter(convert, src_path, work_dir / epub_filename, log, log_label)
|
|
354
475
|
|
|
355
476
|
# Author, title (EXP-224) and the staged path — the rules convert_stage shares
|
|
356
477
|
author = _screen_author(result.author, log)
|
|
@@ -358,6 +479,7 @@ def download_convert_stage( # noqa: C901 — download/verify/convert/stage step
|
|
|
358
479
|
title = sanitize(Path(cd_name).stem) or epub_stem
|
|
359
480
|
else:
|
|
360
481
|
title = _document_title(result, epub_stem, log, fallback_label="URL path stem")
|
|
482
|
+
_write_chosen_title(work_dir / epub_filename, result.title, title, log)
|
|
361
483
|
output_filename = _stage_under_author(work_dir, epub_filename, author)
|
|
362
484
|
|
|
363
485
|
# Clean up downloaded source
|
|
@@ -431,8 +553,10 @@ def convert_stage(
|
|
|
431
553
|
The converter Sources' ``convert`` (SPI 2.33) calls this for a file the user uploaded. It
|
|
432
554
|
hashes *path* (the same ``content_hash``/``content_length`` custom values the download path
|
|
433
555
|
writes, under *namespace*), converts it into ``work_dir/<file stem>.epub``, and stages that
|
|
434
|
-
under the author folder with the download path's
|
|
435
|
-
title precedence
|
|
556
|
+
under the author folder with the download path's author screening and the ``EXP-304`` upload
|
|
557
|
+
title precedence (``_upload_title``). The chosen title is written into
|
|
558
|
+
the EPUB (package, table of contents, title page) when the converter built it with another
|
|
559
|
+
one. *path* is only read.
|
|
436
560
|
|
|
437
561
|
Args:
|
|
438
562
|
path: The uploaded file; its stem is the title of last resort.
|
|
@@ -449,8 +573,8 @@ def convert_stage(
|
|
|
449
573
|
*work_dir*.
|
|
450
574
|
|
|
451
575
|
Raises:
|
|
452
|
-
SourcePullError: The
|
|
453
|
-
|
|
576
|
+
SourcePullError: The conversion failed; its message is the converter's own sentence,
|
|
577
|
+
else "The file could not be converted." (EXP-297).
|
|
454
578
|
"""
|
|
455
579
|
# SDK host bridges plugin.* loggers into the core as the plugin's lines.
|
|
456
580
|
log = logging.getLogger(f"plugin.{namespace}")
|
|
@@ -459,12 +583,10 @@ def convert_stage(
|
|
|
459
583
|
epub_stem = _safe_component(path.stem)
|
|
460
584
|
epub_filename = f"{epub_stem}.epub"
|
|
461
585
|
log.debug("Converting %s to EPUB: %s (%d bytes)", log_label, path.name, size)
|
|
462
|
-
|
|
463
|
-
result = convert(path, work_dir / epub_filename)
|
|
464
|
-
except DocumentConversionError as exc:
|
|
465
|
-
raise SourcePullError(f"{log_label} conversion failed: {exc}") from exc
|
|
586
|
+
result = _run_converter(convert, path, work_dir / epub_filename, log, log_label)
|
|
466
587
|
author = _screen_author(result.author, log)
|
|
467
|
-
title =
|
|
588
|
+
title = _upload_title(result, epub_stem, log)
|
|
589
|
+
_write_chosen_title(work_dir / epub_filename, result.title, title, log)
|
|
468
590
|
output_filename = _stage_under_author(work_dir, epub_filename, author)
|
|
469
591
|
log.info(
|
|
470
592
|
'Converted %s "%s" (%d chapters) from %s',
|