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.
Files changed (54) hide show
  1. ibook2epub-2.0.0/CHANGELOG.md +130 -0
  2. ibook2epub-2.0.0/CONTRIBUTING.md +98 -0
  3. ibook2epub-2.0.0/LICENSE +21 -0
  4. ibook2epub-2.0.0/MANIFEST.in +16 -0
  5. ibook2epub-2.0.0/PKG-INFO +612 -0
  6. ibook2epub-2.0.0/README.md +578 -0
  7. ibook2epub-2.0.0/epubconvert/__init__.py +3 -0
  8. ibook2epub-2.0.0/epubconvert/__main__.py +8 -0
  9. ibook2epub-2.0.0/epubconvert/app_logger.py +128 -0
  10. ibook2epub-2.0.0/epubconvert/archive.py +519 -0
  11. ibook2epub-2.0.0/epubconvert/cli.py +335 -0
  12. ibook2epub-2.0.0/epubconvert/contained.py +202 -0
  13. ibook2epub-2.0.0/epubconvert/convert.py +721 -0
  14. ibook2epub-2.0.0/epubconvert/defaults.py +52 -0
  15. ibook2epub-2.0.0/epubconvert/display.py +49 -0
  16. ibook2epub-2.0.0/epubconvert/exits.py +64 -0
  17. ibook2epub-2.0.0/epubconvert/inspect_output.py +173 -0
  18. ibook2epub-2.0.0/epubconvert/naming.py +595 -0
  19. ibook2epub-2.0.0/epubconvert/planning.py +838 -0
  20. ibook2epub-2.0.0/epubconvert/run.py +408 -0
  21. ibook2epub-2.0.0/epubconvert/source.py +266 -0
  22. ibook2epub-2.0.0/epubconvert/spec.py +22 -0
  23. ibook2epub-2.0.0/epubconvert/validate.py +599 -0
  24. ibook2epub-2.0.0/ibook2epub.egg-info/PKG-INFO +612 -0
  25. ibook2epub-2.0.0/ibook2epub.egg-info/SOURCES.txt +52 -0
  26. ibook2epub-2.0.0/ibook2epub.egg-info/dependency_links.txt +1 -0
  27. ibook2epub-2.0.0/ibook2epub.egg-info/entry_points.txt +2 -0
  28. ibook2epub-2.0.0/ibook2epub.egg-info/requires.txt +11 -0
  29. ibook2epub-2.0.0/ibook2epub.egg-info/top_level.txt +1 -0
  30. ibook2epub-2.0.0/pyproject.toml +124 -0
  31. ibook2epub-2.0.0/setup.cfg +4 -0
  32. ibook2epub-2.0.0/tests/__init__.py +1 -0
  33. ibook2epub-2.0.0/tests/conftest.py +158 -0
  34. ibook2epub-2.0.0/tests/test_cli.py +298 -0
  35. ibook2epub-2.0.0/tests/test_containment.py +242 -0
  36. ibook2epub-2.0.0/tests/test_convert.py +409 -0
  37. ibook2epub-2.0.0/tests/test_copy_through.py +167 -0
  38. ibook2epub-2.0.0/tests/test_efficiency.py +215 -0
  39. ibook2epub-2.0.0/tests/test_epubcheck.py +173 -0
  40. ibook2epub-2.0.0/tests/test_exit_codes.py +182 -0
  41. ibook2epub-2.0.0/tests/test_experience.py +227 -0
  42. ibook2epub-2.0.0/tests/test_export.py +402 -0
  43. ibook2epub-2.0.0/tests/test_hardening.py +292 -0
  44. ibook2epub-2.0.0/tests/test_integrity.py +337 -0
  45. ibook2epub-2.0.0/tests/test_metadata_naming.py +615 -0
  46. ibook2epub-2.0.0/tests/test_naming.py +374 -0
  47. ibook2epub-2.0.0/tests/test_options.py +279 -0
  48. ibook2epub-2.0.0/tests/test_packaging.py +66 -0
  49. ibook2epub-2.0.0/tests/test_planning.py +249 -0
  50. ibook2epub-2.0.0/tests/test_rules.py +635 -0
  51. ibook2epub-2.0.0/tests/test_shelf.py +200 -0
  52. ibook2epub-2.0.0/tests/test_source.py +163 -0
  53. ibook2epub-2.0.0/tests/test_stable_names.py +351 -0
  54. 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.
@@ -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