ibook2epub 2.0.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.
- ibook2epub-2.0.0/CHANGELOG.md +130 -0
- ibook2epub-2.0.0/CONTRIBUTING.md +98 -0
- ibook2epub-2.0.0/LICENSE +21 -0
- ibook2epub-2.0.0/MANIFEST.in +16 -0
- ibook2epub-2.0.0/PKG-INFO +612 -0
- ibook2epub-2.0.0/README.md +578 -0
- ibook2epub-2.0.0/epubconvert/__init__.py +3 -0
- ibook2epub-2.0.0/epubconvert/__main__.py +8 -0
- ibook2epub-2.0.0/epubconvert/app_logger.py +128 -0
- ibook2epub-2.0.0/epubconvert/archive.py +519 -0
- ibook2epub-2.0.0/epubconvert/cli.py +335 -0
- ibook2epub-2.0.0/epubconvert/contained.py +202 -0
- ibook2epub-2.0.0/epubconvert/convert.py +721 -0
- ibook2epub-2.0.0/epubconvert/defaults.py +52 -0
- ibook2epub-2.0.0/epubconvert/display.py +49 -0
- ibook2epub-2.0.0/epubconvert/exits.py +64 -0
- ibook2epub-2.0.0/epubconvert/inspect_output.py +173 -0
- ibook2epub-2.0.0/epubconvert/naming.py +595 -0
- ibook2epub-2.0.0/epubconvert/planning.py +838 -0
- ibook2epub-2.0.0/epubconvert/run.py +408 -0
- ibook2epub-2.0.0/epubconvert/source.py +266 -0
- ibook2epub-2.0.0/epubconvert/spec.py +22 -0
- ibook2epub-2.0.0/epubconvert/validate.py +599 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/PKG-INFO +612 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/SOURCES.txt +52 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/dependency_links.txt +1 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/entry_points.txt +2 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/requires.txt +11 -0
- ibook2epub-2.0.0/ibook2epub.egg-info/top_level.txt +1 -0
- ibook2epub-2.0.0/pyproject.toml +124 -0
- ibook2epub-2.0.0/setup.cfg +4 -0
- ibook2epub-2.0.0/tests/__init__.py +1 -0
- ibook2epub-2.0.0/tests/conftest.py +158 -0
- ibook2epub-2.0.0/tests/test_cli.py +298 -0
- ibook2epub-2.0.0/tests/test_containment.py +242 -0
- ibook2epub-2.0.0/tests/test_convert.py +409 -0
- ibook2epub-2.0.0/tests/test_copy_through.py +167 -0
- ibook2epub-2.0.0/tests/test_efficiency.py +215 -0
- ibook2epub-2.0.0/tests/test_epubcheck.py +173 -0
- ibook2epub-2.0.0/tests/test_exit_codes.py +182 -0
- ibook2epub-2.0.0/tests/test_experience.py +227 -0
- ibook2epub-2.0.0/tests/test_export.py +402 -0
- ibook2epub-2.0.0/tests/test_hardening.py +292 -0
- ibook2epub-2.0.0/tests/test_integrity.py +337 -0
- ibook2epub-2.0.0/tests/test_metadata_naming.py +615 -0
- ibook2epub-2.0.0/tests/test_naming.py +374 -0
- ibook2epub-2.0.0/tests/test_options.py +279 -0
- ibook2epub-2.0.0/tests/test_packaging.py +66 -0
- ibook2epub-2.0.0/tests/test_planning.py +249 -0
- ibook2epub-2.0.0/tests/test_rules.py +635 -0
- ibook2epub-2.0.0/tests/test_shelf.py +200 -0
- ibook2epub-2.0.0/tests/test_source.py +163 -0
- ibook2epub-2.0.0/tests/test_stable_names.py +351 -0
- ibook2epub-2.0.0/tests/test_validate.py +869 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
The exit codes changed incompatibly. Nothing else did, and the rest of this
|
|
11
|
+
release is additions.
|
|
12
|
+
|
|
13
|
+
### Breaking
|
|
14
|
+
|
|
15
|
+
- **Every distinct failure now has its own exit code.** Five unrelated
|
|
16
|
+
conditions used to exit `2`: a mistyped flag, a source directory that is not
|
|
17
|
+
there, a missing optional extra, a missing external tool, and `--verify`
|
|
18
|
+
pointed at nothing. A scheduled run could not tell a misconfiguration it
|
|
19
|
+
should alert on from a transient state it should retry. `2` still means a bad
|
|
20
|
+
command line, because every tool means that by it; the rest moved to `3`
|
|
21
|
+
through `7`, with `130` for Ctrl-C. `epubconvert/exits.py` is the single
|
|
22
|
+
source for the table in the README, so the two cannot drift apart.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `--name-by author-title` names books from their own `dc:title` and
|
|
27
|
+
`dc:creator` instead of the package directory. Apple names a package after
|
|
28
|
+
the title, so an exported shelf sorted by title and no amount of flags would
|
|
29
|
+
make it sort by author. The publisher's sort name is preferred, read from
|
|
30
|
+
either the EPUB2 `opf:file-as` attribute or the EPUB3 `<meta refines="#id"
|
|
31
|
+
property="file-as">` element — Apple's library is overwhelmingly the latter,
|
|
32
|
+
and reading only the attribute would have left 301 of 2,793 books filed under
|
|
33
|
+
their author's first name. Where a publisher gives the author and the
|
|
34
|
+
illustrator the same `id`, the first sort name wins, which is the author's.
|
|
35
|
+
`dc:creator` is used verbatim when no sort name exists, never rearranged.
|
|
36
|
+
Composes with `--portable-names`, which decides how a name is cleaned rather
|
|
37
|
+
than where it comes from.
|
|
38
|
+
- Up to two contributors are named in full; beyond that the list collapses to
|
|
39
|
+
`Peralta, Samuel et al.`, which is what other library tools produce.
|
|
40
|
+
Publishers put a book's entire contributor list in one metadata field joined
|
|
41
|
+
with ` & `, and a twelve-author name is not a usable filename even when it
|
|
42
|
+
fits inside the byte limit.
|
|
43
|
+
- Neither half of a name can squeeze the other out. Clamping used to trim the
|
|
44
|
+
end of the composed `Author - Title`, which is the title: an anthology came
|
|
45
|
+
out as `... & Wecks, Erik - The Time Travel.epub`, keeping fourteen
|
|
46
|
+
contributors and losing "Chronicles". A book whose `dc:title` is its whole
|
|
47
|
+
jacket blurb keeps its author prefix and has the title trimmed instead,
|
|
48
|
+
because a shelf that stops sorting by author defeats the point of the policy.
|
|
49
|
+
- An author made only of characters that sanitising removes (`..`, `?`) is
|
|
50
|
+
treated as absent rather than leaving a separator with nothing in front of
|
|
51
|
+
it.
|
|
52
|
+
- A trimmed title backs off to the last whole word, so a shortened name reads
|
|
53
|
+
as deliberate rather than damaged: `...watch television` rather than
|
|
54
|
+
`...watch television a`.
|
|
55
|
+
- Stable collision names. Under `--on-collision suffix`, a book that has to
|
|
56
|
+
share a name is marked with a digest of its own `dc:identifier` rather than
|
|
57
|
+
its position in the colliding group. Adding a book that sorts earlier no
|
|
58
|
+
longer renames every later member. Books whose identifier is missing, a
|
|
59
|
+
placeholder, or shared with another book keep the positional suffix and the
|
|
60
|
+
run says so.
|
|
61
|
+
- Orphan reporting. An archive that no book in the library claims is now named
|
|
62
|
+
by `--list`, carries `"source": null` in the JSON, and is counted in the run
|
|
63
|
+
summary. Nothing is deleted; the gap was that nothing would say either.
|
|
64
|
+
- Copy-through for books that need no conversion. Already-valid `.epub` files
|
|
65
|
+
and `.pdf` files are copied verbatim through the same
|
|
66
|
+
temporary-then-replace path everything else uses, so a library holding both
|
|
67
|
+
Apple's package folders and books that arrived already zipped exports whole.
|
|
68
|
+
`--no-copy-through` turns it off.
|
|
69
|
+
- The run reports how many books were named from a package document that
|
|
70
|
+
declared no creator, and how many kept their folder name because the document
|
|
71
|
+
declared no title. A shelf that comes out half-named says so, rather than
|
|
72
|
+
leaving it to be noticed afterwards.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- `dc:identifier` was read as the first such element in document order, where
|
|
77
|
+
the spec names the canonical one through the `unique-identifier` IDREF on
|
|
78
|
+
`<package>`. Publishers commonly list a retail ASIN or ISBN first, so the
|
|
79
|
+
wrong value was returned for 798 of 2,805 books in a real library. Nothing
|
|
80
|
+
consumed the field yet, so nothing had broken.
|
|
81
|
+
- Structural checks in the source walk ran only where `st_flags` exists, so a
|
|
82
|
+
package that could not be checked for undownloaded files skipped the rest of
|
|
83
|
+
its checks on Linux.
|
|
84
|
+
- The two write paths in `_decide` had drifted into being byte-identical and
|
|
85
|
+
are now one.
|
|
86
|
+
- Roughly forty correctness, security, performance and readability findings
|
|
87
|
+
from two full review passes, applied by rule at every call site of the rule
|
|
88
|
+
rather than at the one site that surfaced them.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- `assign_names` returns an `Assignment` record rather than a bare triple, so a
|
|
93
|
+
book that loses a collision can say which file holds the name and what its
|
|
94
|
+
own identifier is.
|
|
95
|
+
- The PyPI publish workflow was hardened and pinned.
|
|
96
|
+
- Installation instructions cover the published distribution rather than only
|
|
97
|
+
an editable checkout, and say what the `portable` extra actually buys: it is
|
|
98
|
+
needed for `--portable-names romanize` and for nothing else.
|
|
99
|
+
- CodeTour walkthroughs were added for the conversion and planning paths.
|
|
100
|
+
|
|
101
|
+
## [1.2.1] - 2026-08-24
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- Issues raised by review of the Tier 3 work.
|
|
106
|
+
- Findings from an automated review pass.
|
|
107
|
+
|
|
108
|
+
## [1.2.0] - 2026-08-23
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- `-p` / `--portable-names` with `strip` and `romanize` modes, and
|
|
113
|
+
identity-based deduplication so two spellings of one book export once.
|
|
114
|
+
- Validation, source inspection and `--list`.
|
|
115
|
+
- Reproducible archives, and a clean exit on interrupt.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- `click` replaced with `argparse`, removing the last runtime dependency.
|
|
120
|
+
- Python 3.10 is the minimum; development moved to 3.14.
|
|
121
|
+
- Relicensed to MIT.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- Filename-length and output-overlap bugs.
|
|
126
|
+
- Nested content that looked like Apple bookkeeping was being dropped.
|
|
127
|
+
|
|
128
|
+
[2.0.0]: https://github.com/raeq/ibook2epub/compare/v1.2.1...v2.0.0
|
|
129
|
+
[1.2.1]: https://github.com/raeq/ibook2epub/compare/v1.2.0...v1.2.1
|
|
130
|
+
[1.2.0]: https://github.com/raeq/ibook2epub/releases/tag/v1.2.0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Running the checks
|
|
4
|
+
|
|
5
|
+
CI runs five steps. Run all five before pushing; the first four are fast.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
ruff check epubconvert tests
|
|
9
|
+
ruff format --check epubconvert tests
|
|
10
|
+
mypy
|
|
11
|
+
pylint epubconvert tests
|
|
12
|
+
pytest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`pylint` has no score threshold. It exits non-zero on any message, which is
|
|
16
|
+
deliberate: it used to run with `--fail-under=8.0` against a tree scoring
|
|
17
|
+
10.00, so a regression to 8.1 merged green.
|
|
18
|
+
|
|
19
|
+
## Two invariants worth knowing before you change anything
|
|
20
|
+
|
|
21
|
+
### The output directory is the only record of completed work
|
|
22
|
+
|
|
23
|
+
There is no state file. Whether a book has been converted is decided by
|
|
24
|
+
globbing `*.epub` in the output directory and recomputing each name's identity.
|
|
25
|
+
Everything follows from that:
|
|
26
|
+
|
|
27
|
+
- A name the glob cannot match means the book is re-converted on every run,
|
|
28
|
+
for ever. A title of `?` once produced a file called `epub`.
|
|
29
|
+
- Anything that lands in the output directory is recorded as finished and no
|
|
30
|
+
rerun retries it. A truncated archive is permanent.
|
|
31
|
+
|
|
32
|
+
`archive.assert_is_a_book` is the choke point. It runs before every
|
|
33
|
+
`partial.replace()` and refuses an archive that lacks `META-INF/container.xml`
|
|
34
|
+
or holds no members. Every silent-success defect this project has had ended
|
|
35
|
+
there: an unreadable subdirectory that contributed nothing, a package deleted
|
|
36
|
+
between planning and writing, a symlinked directory holding somebody else's
|
|
37
|
+
files. In each case the run wrote a valid zip, reported an export, and recorded
|
|
38
|
+
a book that was wrong or missing. `--validate` is the thorough check and it is
|
|
39
|
+
off by default, so the cheap unconditional assertion is what actually holds the
|
|
40
|
+
line.
|
|
41
|
+
|
|
42
|
+
If you add a path that writes into the output directory, it goes through that
|
|
43
|
+
function.
|
|
44
|
+
|
|
45
|
+
### A name from a book is input, not fact
|
|
46
|
+
|
|
47
|
+
`container.xml` names the package document. The package document names every
|
|
48
|
+
manifest item. The package directory names its own entries. All of those are
|
|
49
|
+
chosen by whoever produced or sideloaded the book, and all of them get joined
|
|
50
|
+
onto a real directory.
|
|
51
|
+
|
|
52
|
+
`epubconvert/contained.py` holds the rule, and a test asserts nothing
|
|
53
|
+
reimplements it. This is not a style preference. The rule was written three
|
|
54
|
+
times: a traversal through a manifest href was fixed at the one call site that
|
|
55
|
+
had it, the archive writer later grew a weaker version of its own, and the
|
|
56
|
+
readers of `container.xml`, the package document and `encryption.xml` grew
|
|
57
|
+
none — so a book could still point its own package document at any file the
|
|
58
|
+
user could read, months after the "same" bug was closed.
|
|
59
|
+
|
|
60
|
+
Names shown to a user go through `epubconvert/display.py`, which escapes
|
|
61
|
+
control characters. A package name carrying `ESC[2K` can otherwise erase the
|
|
62
|
+
line reporting it.
|
|
63
|
+
|
|
64
|
+
### Exit codes are a contract
|
|
65
|
+
|
|
66
|
+
`epubconvert/exits.py` holds every code with its meaning, and the README table
|
|
67
|
+
is generated from `MEANINGS`, so the two cannot drift. A new failure mode gets
|
|
68
|
+
a new code rather than reusing a near-enough one: five conditions once shared
|
|
69
|
+
`2`, and a scheduled run could not tell a typo from a missing dependency.
|
|
70
|
+
|
|
71
|
+
Environment checks — does this directory exist, is that tool installed — go in
|
|
72
|
+
`run.main`, not in `parse_args`. `parser.error` always exits 2, so validating
|
|
73
|
+
the environment there is what collapsed them in the first place.
|
|
74
|
+
|
|
75
|
+
## Measure before tuning
|
|
76
|
+
|
|
77
|
+
Two of this project's performance assumptions were wrong, and both had been
|
|
78
|
+
sitting in the code unquestioned:
|
|
79
|
+
|
|
80
|
+
- `COMPRESS_LEVEL = 9` had never been applied. `ZipFile(compresslevel=)` is
|
|
81
|
+
consulted only when `open()` builds its own `ZipInfo`, and this code hands it
|
|
82
|
+
a prebuilt one, so every member had always deflated at zlib's default.
|
|
83
|
+
Applying the constant would have cost 3.2× the CPU for 0.6% less size.
|
|
84
|
+
- `--validate` was assumed to double the read work, because it re-inflates
|
|
85
|
+
every member. It costs about 7%: the archive is still in the page cache, and
|
|
86
|
+
inflating is far cheaper than deflating.
|
|
87
|
+
|
|
88
|
+
Neither was discoverable by reading. If you change a tuning constant, a worker
|
|
89
|
+
count, or a compression choice, record the measurement in a comment next to it
|
|
90
|
+
and say what you measured on. The constants in `archive.py` and
|
|
91
|
+
`convert.default_workers` carry theirs.
|
|
92
|
+
|
|
93
|
+
## Style
|
|
94
|
+
|
|
95
|
+
Docstrings explain *why*, and often cite the defect that motivated a decision.
|
|
96
|
+
That is intentional and worth keeping. When you move code, re-read the
|
|
97
|
+
docstrings that moved with it — four of them were left pointing at functions
|
|
98
|
+
that had relocated.
|
ibook2epub-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2026 Richard Quinn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# setuptools auto-includes test*.py by a legacy rule, which picks up
|
|
2
|
+
# tests/test_*.py and silently leaves out conftest.py and __init__.py. Every
|
|
3
|
+
# fixture lives in conftest, so a packager running the suite from the sdist
|
|
4
|
+
# got collection errors rather than tests.
|
|
5
|
+
recursive-include tests *.py
|
|
6
|
+
|
|
7
|
+
include CONTRIBUTING.md
|
|
8
|
+
include CHANGELOG.md
|
|
9
|
+
|
|
10
|
+
# Review artefacts and editor tooling are working files, not part of the
|
|
11
|
+
# distribution. Named explicitly so a new one does not start shipping.
|
|
12
|
+
exclude hai-sdlc-review-*.md
|
|
13
|
+
exclude uv.lock
|
|
14
|
+
prune .tours
|
|
15
|
+
prune .claude
|
|
16
|
+
prune .github
|