ebookerr-sdk 3.0.0__tar.gz → 3.2.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/LICENSE +21 -0
- ebookerr_sdk-3.2.0/PKG-INFO +152 -0
- ebookerr_sdk-3.2.0/README.md +123 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/__init__.py +2 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/adoption.py +53 -7
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/chapter_naming.py +2 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/chapter_number.py +68 -67
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/ids.py +1 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/story_url.py +3 -3
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/__init__.py +2 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/check.py +1 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/document.py +1 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/paths.py +44 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/__init__.py +2 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/chapter_numbers.py +3 -3
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/opf.py +9 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/host.py +32 -4
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/__init__.py +8 -5
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/check.py +67 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/cli.py +34 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/imports.py +33 -9
- ebookerr_sdk-3.2.0/ebookerr_sdk/pack/scaffold.py +261 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/readpos.py +1 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/registry_index.py +8 -3
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/__init__.py +13 -2
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/context.py +53 -9
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/declarations.py +11 -8
- ebookerr_sdk-3.2.0/ebookerr_sdk/spi/glyphs.py +42 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/manifest.py +57 -5
- ebookerr_sdk-3.2.0/ebookerr_sdk/spi/material_symbols.txt +4262 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/types.py +14 -3
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/testing/context.py +15 -3
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/testing/wire.py +27 -5
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/__init__.py +1 -1
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/wire.py +5 -0
- ebookerr_sdk-3.2.0/ebookerr_sdk.egg-info/PKG-INFO +152 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk.egg-info/SOURCES.txt +4 -0
- ebookerr_sdk-3.2.0/pyproject.toml +45 -0
- ebookerr_sdk-3.0.0/PKG-INFO +0 -13
- ebookerr_sdk-3.0.0/README.md +0 -65
- ebookerr_sdk-3.0.0/ebookerr_sdk.egg-info/PKG-INFO +0 -13
- ebookerr_sdk-3.0.0/pyproject.toml +0 -27
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/__init__.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/dates.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/epub_hash.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/epub_metadata.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/metadata.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/survivor.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/domain/text_encoding.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/availability.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/disposition.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/identity.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/download/routing.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/archive.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/assets.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/build.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/builder.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/chapters.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/document.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/errors.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/fonts.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/nav.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/ncx.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/roles.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/safe_xml.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/serialize.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/text.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/text_convert.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/titles.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/epub/xhtml.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/generation/__init__.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/generation/client.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/generation/output.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/generation/prompt_template.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/generation/support.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/__main__.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/build.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/changelog.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/files.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/pack/index.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/__init__.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/anchor_types.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/anchoring.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/connection.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/link_attempt.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/link_refusal.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/restore_marker.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/scan_ledger.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/providers/urls.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/py.typed +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/errors.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/protocols.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/settings.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/summary_template.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/spi/views.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/testing/__init__.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/testing/views.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/external.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/model.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/rules_css.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/rules_html.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/rules_nav.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/rules_opf.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/rules_package.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk/validate/validate.py +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk.egg-info/dependency_links.txt +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk.egg-info/requires.txt +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/ebookerr_sdk.egg-info/top_level.txt +0 -0
- {ebookerr_sdk-3.0.0 → ebookerr_sdk-3.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ArcCdr
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ebookerr-sdk
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: The ebookerr plugin SDK: the plugin contract, the plugin host, a test kit, the pack tool and helper kits.
|
|
5
|
+
Author: ArcCdr
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/ArcCdr/lit-ebookerr/blob/main/docs/PLUGIN_DEV.md
|
|
8
|
+
Project-URL: API reference, https://arccdr.github.io/lit-ebookerr/
|
|
9
|
+
Project-URL: Source, https://github.com/ArcCdr/lit-ebookerr/tree/main/sdk
|
|
10
|
+
Project-URL: Plugins, https://github.com/ArcCdr/ebookerr-plugins
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: defusedxml>=0.7
|
|
21
|
+
Requires-Dist: tinycss2>=1.3
|
|
22
|
+
Requires-Dist: requests>=2.31
|
|
23
|
+
Requires-Dist: packaging>=24
|
|
24
|
+
Provides-Extra: text
|
|
25
|
+
Requires-Dist: charset-normalizer>=3.3; extra == "text"
|
|
26
|
+
Provides-Extra: generation
|
|
27
|
+
Requires-Dist: pillow>=10; extra == "generation"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# ebookerr-sdk
|
|
31
|
+
|
|
32
|
+
The toolkit for writing [ebookerr](https://github.com/ArcCdr/lit-ebookerr) plugins: the plugin
|
|
33
|
+
contract (`ebookerr_sdk.spi`), the host that runs a plugin (`ebookerr_sdk.host`), a test kit
|
|
34
|
+
(`ebookerr_sdk.testing`), the `pack` command, and helper kits for EPUB files, downloads,
|
|
35
|
+
validation, library servers and text generation.
|
|
36
|
+
|
|
37
|
+
- Guide: <https://github.com/ArcCdr/lit-ebookerr/blob/main/docs/PLUGIN_DEV.md>
|
|
38
|
+
- API reference: <https://arccdr.github.io/lit-ebookerr/>
|
|
39
|
+
- Plugins: <https://github.com/ArcCdr/ebookerr-plugins>
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "ebookerr-sdk==3.2.*"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.2.x speaks SPI 3.2. It needs
|
|
48
|
+
Python 3.11 or later.
|
|
49
|
+
|
|
50
|
+
## Start a plugin
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m ebookerr_sdk.pack new hello_book --type book
|
|
54
|
+
pytest hello_book/tests
|
|
55
|
+
python -m ebookerr_sdk.pack check hello_book
|
|
56
|
+
python -m ebookerr_sdk.pack build hello_book --out dist
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`new` writes a plugin that passes `check` and its own test; `--type` is `source`, `epub`, `book`
|
|
60
|
+
or `catalog`. The guide explains what to write next and how to install the zip in ebookerr.
|
|
61
|
+
|
|
62
|
+
## Compatibility
|
|
63
|
+
|
|
64
|
+
| ebookerr | SPI | ebookerr-sdk |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| 2.24 | 3.0 | 3.0.x |
|
|
67
|
+
| 2.24.1 | 3.1 | 3.1.x |
|
|
68
|
+
| 2.24.7 | 3.2 | 3.2.x |
|
|
69
|
+
|
|
70
|
+
ebookerr loads a plugin when its `spi_version` has ebookerr's SPI MAJOR and a MINOR not above
|
|
71
|
+
ebookerr's; otherwise it shows the plugin as incompatible and offers a compatible release.
|
|
72
|
+
|
|
73
|
+
## Versioning
|
|
74
|
+
|
|
75
|
+
1. A MINOR release only adds: an optional manifest key whose default keeps today's behaviour, or
|
|
76
|
+
a new protocol, role, operation, context method, event or trigger scope that you opt into.
|
|
77
|
+
2. Anything else — removing or renaming, changing a default or a meaning, making a key required,
|
|
78
|
+
refusing a manifest that was valid — waits for the next MAJOR release.
|
|
79
|
+
3. ebookerr ignores manifest keys it does not know, with a warning; your plugin should ignore
|
|
80
|
+
request keys it does not know. `python -m ebookerr_sdk.pack check` refuses unknown manifest
|
|
81
|
+
keys and settings-field types, so a typo is caught before you release.
|
|
82
|
+
4. Before it is removed, a public item is deprecated: named in these release notes and logged
|
|
83
|
+
once per plugin as a warning when used, it keeps working for at least two MINOR releases and
|
|
84
|
+
six months.
|
|
85
|
+
5. An ebookerr release supports one SPI MAJOR, and moves to a new MAJOR only once every official
|
|
86
|
+
plugin has a compatible release.
|
|
87
|
+
6. A PATCH release of the SDK fixes defects only; it never changes the public API.
|
|
88
|
+
7. The public API is `manifest.toml`, `ebookerr_sdk.spi`, `ebookerr_sdk.host`,
|
|
89
|
+
`ebookerr_sdk.testing`, `python -m ebookerr_sdk.pack` and the kits `ebookerr_sdk.epub`,
|
|
90
|
+
`.validate`, `.download`, `.generation`, `.providers` and `.domain`. `ebookerr_sdk.wire`,
|
|
91
|
+
`.readpos`, `.registry_index` and every `_`-prefixed name are internal and may change in any
|
|
92
|
+
release.
|
|
93
|
+
|
|
94
|
+
## Release notes
|
|
95
|
+
|
|
96
|
+
### 3.2.0
|
|
97
|
+
|
|
98
|
+
SPI 3.2. A story can name the multi-part work it is one part of: `StoryPatch.work` and
|
|
99
|
+
`StoryPatch.work_url`. `series` and `series_url` mean a series the site itself declares —
|
|
100
|
+
separate works in a declared order — never the work a chapter belongs to. `BookView.work_url`
|
|
101
|
+
carries the work a book is a chapter of. `ebookerr_sdk.domain.adoption` gains `same_series` and
|
|
102
|
+
`same_work`; `AdoptionSide.work_url` and `Evidence.same_work` are new, and signal E5 is "same
|
|
103
|
+
series or same work". `EBOOKERR_META_KEYS` gains `work_url`. A plugin written for SPI 3.1 needs
|
|
104
|
+
no change.
|
|
105
|
+
|
|
106
|
+
### 3.1.0
|
|
107
|
+
|
|
108
|
+
SPI 3.1. A `[[ui_triggers]]` entry's `icon` is optional and defaults to the plugin's `icon`;
|
|
109
|
+
declare one only when a plugin has more than one action. Every glyph name must be one the
|
|
110
|
+
self-hosted Material Symbols font renders (`ebookerr_sdk/spi/material_symbols.txt`): `pack check`
|
|
111
|
+
reports an unknown name, two actions of one scope showing the same glyph, and a trigger without
|
|
112
|
+
`icon` in a manifest that still declares `spi_version = "3.0"`. `pack check` now reports a declared
|
|
113
|
+
requirement that is not installed where it runs instead of guessing its module names. A plugin
|
|
114
|
+
written for SPI 3.0 needs no change.
|
|
115
|
+
|
|
116
|
+
### 3.0.0
|
|
117
|
+
|
|
118
|
+
SPI 3.0. A plugin written for SPI 2.x declares `spi_version = "3.0"` and applies these changes:
|
|
119
|
+
|
|
120
|
+
- `spi_version` is required.
|
|
121
|
+
- Roles are declared only as `[roles.<name>]` tables. The top-level keys `exclusive_group`,
|
|
122
|
+
`provider`, `delete_mode`, `reader_url_template`, `handles_merge_proposals`, `catch_all` and
|
|
123
|
+
`extract_url_patterns` are gone (`provider`, `delete_mode` and `reader_url_template` remain keys
|
|
124
|
+
of `[roles.library_server]`).
|
|
125
|
+
- `network`, `auth_methods`, `headless`, `headed` and `default_enabled` are no longer manifest
|
|
126
|
+
keys. A plugin runs on events when it declares `events`, and runs by hand when it declares a
|
|
127
|
+
`book_selection_action` or `cover_invalidate` trigger; `PluginManifest.headed` is derived from
|
|
128
|
+
its triggers.
|
|
129
|
+
- Settings-field types are `string`, `textarea`, `int`, `float`, `bool`, `url`, `url_list`,
|
|
130
|
+
`threshold_list`, `record_list` and `command`: a secret is `secret = true` on a `string`, a
|
|
131
|
+
drop-down is `options` with `options_strict = true` on a `string`; the types `text`, `secret`
|
|
132
|
+
and `select` are gone. `SettingsGroup.collapsed` is gone.
|
|
133
|
+
- `UiFilter` and `UiTrigger.filters` are gone.
|
|
134
|
+
- `PluginContext.ask_yes_no` returns `True`, `False`, or `None` when the prompt was abandoned or
|
|
135
|
+
timed out; `PluginContext.ask_yes_no_answer` and `PluginContext.queue` are gone.
|
|
136
|
+
- `UpdateCheckingSource.check_for_update` always receives `ctx`; its `cancel_event` parameter is
|
|
137
|
+
gone.
|
|
138
|
+
- `BookView.erotica_tags` is gone; use `BookView.tags`.
|
|
139
|
+
- `ProviderAnchor`, `ProviderBookmark` and `ReadPositionAnchoring` are imported from
|
|
140
|
+
`ebookerr_sdk.providers` only.
|
|
141
|
+
- New: `PluginContext.credentials(url)` and `SiteCredential` — the stored sign-in for a site the
|
|
142
|
+
plugin may use: a host in its `auth_sites` (`"*"` for any site it processes) or the host of a
|
|
143
|
+
`credential = true` URL setting.
|
|
144
|
+
- New: `python -m ebookerr_sdk.pack new` starts a plugin; `pack check` refuses unknown manifest
|
|
145
|
+
keys, settings-field types and trigger scopes.
|
|
146
|
+
- `ebookerr_sdk.spi` is split into modules (`errors`, `settings`, `types`, `views`,
|
|
147
|
+
`declarations`, `context`, `protocols`); every name still imports from `ebookerr_sdk.spi`.
|
|
148
|
+
- `ebookerr_sdk.wire`, `ebookerr_sdk.readpos` and `ebookerr_sdk.registry_index` are internal.
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
`ebookerr-sdk` is released under the MIT licence. Each plugin carries its own licence.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ebookerr-sdk
|
|
2
|
+
|
|
3
|
+
The toolkit for writing [ebookerr](https://github.com/ArcCdr/lit-ebookerr) plugins: the plugin
|
|
4
|
+
contract (`ebookerr_sdk.spi`), the host that runs a plugin (`ebookerr_sdk.host`), a test kit
|
|
5
|
+
(`ebookerr_sdk.testing`), the `pack` command, and helper kits for EPUB files, downloads,
|
|
6
|
+
validation, library servers and text generation.
|
|
7
|
+
|
|
8
|
+
- Guide: <https://github.com/ArcCdr/lit-ebookerr/blob/main/docs/PLUGIN_DEV.md>
|
|
9
|
+
- API reference: <https://arccdr.github.io/lit-ebookerr/>
|
|
10
|
+
- Plugins: <https://github.com/ArcCdr/ebookerr-plugins>
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install "ebookerr-sdk==3.2.*"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The SDK's MAJOR.MINOR is the SPI version it speaks: `ebookerr-sdk` 3.2.x speaks SPI 3.2. It needs
|
|
19
|
+
Python 3.11 or later.
|
|
20
|
+
|
|
21
|
+
## Start a plugin
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m ebookerr_sdk.pack new hello_book --type book
|
|
25
|
+
pytest hello_book/tests
|
|
26
|
+
python -m ebookerr_sdk.pack check hello_book
|
|
27
|
+
python -m ebookerr_sdk.pack build hello_book --out dist
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`new` writes a plugin that passes `check` and its own test; `--type` is `source`, `epub`, `book`
|
|
31
|
+
or `catalog`. The guide explains what to write next and how to install the zip in ebookerr.
|
|
32
|
+
|
|
33
|
+
## Compatibility
|
|
34
|
+
|
|
35
|
+
| ebookerr | SPI | ebookerr-sdk |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| 2.24 | 3.0 | 3.0.x |
|
|
38
|
+
| 2.24.1 | 3.1 | 3.1.x |
|
|
39
|
+
| 2.24.7 | 3.2 | 3.2.x |
|
|
40
|
+
|
|
41
|
+
ebookerr loads a plugin when its `spi_version` has ebookerr's SPI MAJOR and a MINOR not above
|
|
42
|
+
ebookerr's; otherwise it shows the plugin as incompatible and offers a compatible release.
|
|
43
|
+
|
|
44
|
+
## Versioning
|
|
45
|
+
|
|
46
|
+
1. A MINOR release only adds: an optional manifest key whose default keeps today's behaviour, or
|
|
47
|
+
a new protocol, role, operation, context method, event or trigger scope that you opt into.
|
|
48
|
+
2. Anything else — removing or renaming, changing a default or a meaning, making a key required,
|
|
49
|
+
refusing a manifest that was valid — waits for the next MAJOR release.
|
|
50
|
+
3. ebookerr ignores manifest keys it does not know, with a warning; your plugin should ignore
|
|
51
|
+
request keys it does not know. `python -m ebookerr_sdk.pack check` refuses unknown manifest
|
|
52
|
+
keys and settings-field types, so a typo is caught before you release.
|
|
53
|
+
4. Before it is removed, a public item is deprecated: named in these release notes and logged
|
|
54
|
+
once per plugin as a warning when used, it keeps working for at least two MINOR releases and
|
|
55
|
+
six months.
|
|
56
|
+
5. An ebookerr release supports one SPI MAJOR, and moves to a new MAJOR only once every official
|
|
57
|
+
plugin has a compatible release.
|
|
58
|
+
6. A PATCH release of the SDK fixes defects only; it never changes the public API.
|
|
59
|
+
7. The public API is `manifest.toml`, `ebookerr_sdk.spi`, `ebookerr_sdk.host`,
|
|
60
|
+
`ebookerr_sdk.testing`, `python -m ebookerr_sdk.pack` and the kits `ebookerr_sdk.epub`,
|
|
61
|
+
`.validate`, `.download`, `.generation`, `.providers` and `.domain`. `ebookerr_sdk.wire`,
|
|
62
|
+
`.readpos`, `.registry_index` and every `_`-prefixed name are internal and may change in any
|
|
63
|
+
release.
|
|
64
|
+
|
|
65
|
+
## Release notes
|
|
66
|
+
|
|
67
|
+
### 3.2.0
|
|
68
|
+
|
|
69
|
+
SPI 3.2. A story can name the multi-part work it is one part of: `StoryPatch.work` and
|
|
70
|
+
`StoryPatch.work_url`. `series` and `series_url` mean a series the site itself declares —
|
|
71
|
+
separate works in a declared order — never the work a chapter belongs to. `BookView.work_url`
|
|
72
|
+
carries the work a book is a chapter of. `ebookerr_sdk.domain.adoption` gains `same_series` and
|
|
73
|
+
`same_work`; `AdoptionSide.work_url` and `Evidence.same_work` are new, and signal E5 is "same
|
|
74
|
+
series or same work". `EBOOKERR_META_KEYS` gains `work_url`. A plugin written for SPI 3.1 needs
|
|
75
|
+
no change.
|
|
76
|
+
|
|
77
|
+
### 3.1.0
|
|
78
|
+
|
|
79
|
+
SPI 3.1. A `[[ui_triggers]]` entry's `icon` is optional and defaults to the plugin's `icon`;
|
|
80
|
+
declare one only when a plugin has more than one action. Every glyph name must be one the
|
|
81
|
+
self-hosted Material Symbols font renders (`ebookerr_sdk/spi/material_symbols.txt`): `pack check`
|
|
82
|
+
reports an unknown name, two actions of one scope showing the same glyph, and a trigger without
|
|
83
|
+
`icon` in a manifest that still declares `spi_version = "3.0"`. `pack check` now reports a declared
|
|
84
|
+
requirement that is not installed where it runs instead of guessing its module names. A plugin
|
|
85
|
+
written for SPI 3.0 needs no change.
|
|
86
|
+
|
|
87
|
+
### 3.0.0
|
|
88
|
+
|
|
89
|
+
SPI 3.0. A plugin written for SPI 2.x declares `spi_version = "3.0"` and applies these changes:
|
|
90
|
+
|
|
91
|
+
- `spi_version` is required.
|
|
92
|
+
- Roles are declared only as `[roles.<name>]` tables. The top-level keys `exclusive_group`,
|
|
93
|
+
`provider`, `delete_mode`, `reader_url_template`, `handles_merge_proposals`, `catch_all` and
|
|
94
|
+
`extract_url_patterns` are gone (`provider`, `delete_mode` and `reader_url_template` remain keys
|
|
95
|
+
of `[roles.library_server]`).
|
|
96
|
+
- `network`, `auth_methods`, `headless`, `headed` and `default_enabled` are no longer manifest
|
|
97
|
+
keys. A plugin runs on events when it declares `events`, and runs by hand when it declares a
|
|
98
|
+
`book_selection_action` or `cover_invalidate` trigger; `PluginManifest.headed` is derived from
|
|
99
|
+
its triggers.
|
|
100
|
+
- Settings-field types are `string`, `textarea`, `int`, `float`, `bool`, `url`, `url_list`,
|
|
101
|
+
`threshold_list`, `record_list` and `command`: a secret is `secret = true` on a `string`, a
|
|
102
|
+
drop-down is `options` with `options_strict = true` on a `string`; the types `text`, `secret`
|
|
103
|
+
and `select` are gone. `SettingsGroup.collapsed` is gone.
|
|
104
|
+
- `UiFilter` and `UiTrigger.filters` are gone.
|
|
105
|
+
- `PluginContext.ask_yes_no` returns `True`, `False`, or `None` when the prompt was abandoned or
|
|
106
|
+
timed out; `PluginContext.ask_yes_no_answer` and `PluginContext.queue` are gone.
|
|
107
|
+
- `UpdateCheckingSource.check_for_update` always receives `ctx`; its `cancel_event` parameter is
|
|
108
|
+
gone.
|
|
109
|
+
- `BookView.erotica_tags` is gone; use `BookView.tags`.
|
|
110
|
+
- `ProviderAnchor`, `ProviderBookmark` and `ReadPositionAnchoring` are imported from
|
|
111
|
+
`ebookerr_sdk.providers` only.
|
|
112
|
+
- New: `PluginContext.credentials(url)` and `SiteCredential` — the stored sign-in for a site the
|
|
113
|
+
plugin may use: a host in its `auth_sites` (`"*"` for any site it processes) or the host of a
|
|
114
|
+
`credential = true` URL setting.
|
|
115
|
+
- New: `python -m ebookerr_sdk.pack new` starts a plugin; `pack check` refuses unknown manifest
|
|
116
|
+
keys, settings-field types and trigger scopes.
|
|
117
|
+
- `ebookerr_sdk.spi` is split into modules (`errors`, `settings`, `types`, `views`,
|
|
118
|
+
`declarations`, `context`, `protocols`); every name still imports from `ebookerr_sdk.spi`.
|
|
119
|
+
- `ebookerr_sdk.wire`, `ebookerr_sdk.readpos` and `ebookerr_sdk.registry_index` are internal.
|
|
120
|
+
|
|
121
|
+
## Licence
|
|
122
|
+
|
|
123
|
+
`ebookerr-sdk` is released under the MIT licence. Each plugin carries its own licence.
|
|
@@ -19,7 +19,7 @@ ruff D, pdoc, mutmut and the traceability gate all cover the SDK; it has its own
|
|
|
19
19
|
``write_read_state``, ``convert``) and the progress, dialog, view, circuit, defer and state
|
|
20
20
|
frames.
|
|
21
21
|
- ``testing/`` — Test doubles for ebookerr plugins (``PMG-D26``).
|
|
22
|
-
- ``pack/`` —
|
|
22
|
+
- ``pack/`` — Start, build, check and index ebookerr plugin packages (PMG-FR-72, PMG-FR-75).
|
|
23
23
|
- ``epub/`` — the custom EPUB library: ``archive``, ``opf``, ``ncx``, ``xhtml``, ``document``,
|
|
24
24
|
``chapters``/``chapter_table()``, ``serialize``, ``roles``, ``titles``, ``fonts``, ``assets``,
|
|
25
25
|
``builder``, ``nav`` — every EPUB plugin's shared tooling.
|
|
@@ -44,4 +44,4 @@ ruff D, pdoc, mutmut and the traceability gate all cover the SDK; it has its own
|
|
|
44
44
|
``pack/``, so a fetched index and a built one are validated by exactly the same rules.
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
-
__version__ = "3.
|
|
47
|
+
__version__ = "3.2.0"
|
|
@@ -4,6 +4,8 @@ This module provides pure functions for normalising chapter identifiers
|
|
|
4
4
|
so they can be compared across different formatting conventions.
|
|
5
5
|
Because these functions are pure (no database, no external dependencies),
|
|
6
6
|
the adoption heuristic is fully unit-testable.
|
|
7
|
+
|
|
8
|
+
It also provides ``same_series``/``same_work`` (the two halves of signal E5).
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
@@ -15,6 +17,7 @@ from dataclasses import dataclass
|
|
|
15
17
|
from urllib.parse import urlsplit
|
|
16
18
|
|
|
17
19
|
from ebookerr_sdk.domain.chapter_number import extract_chapter_info
|
|
20
|
+
from ebookerr_sdk.domain.ids import normalise_url
|
|
18
21
|
from ebookerr_sdk.domain.text_encoding import unicode_identity
|
|
19
22
|
|
|
20
23
|
|
|
@@ -103,7 +106,7 @@ class AdoptionSide:
|
|
|
103
106
|
|
|
104
107
|
Attributes:
|
|
105
108
|
book_id: The book's id.
|
|
106
|
-
title: The book title as stored, e.g. ``"
|
|
109
|
+
title: The book title as stored, e.g. ``"Sample Alpha Pt. 05"``.
|
|
107
110
|
author: The author as stored; ``""`` when unknown.
|
|
108
111
|
site: The site as stored; ``""`` when unknown.
|
|
109
112
|
series: The series as stored; ``""`` when unknown.
|
|
@@ -112,6 +115,7 @@ class AdoptionSide:
|
|
|
112
115
|
numbers: Raw F8 chapter-number tokens. For an orphan this holds exactly the one
|
|
113
116
|
number parsed from its title; for a target, every number in its EPUB table of
|
|
114
117
|
contents, in table order.
|
|
118
|
+
work_url: The work this book is a chapter of; ``""`` when none (SPI 3.2).
|
|
115
119
|
"""
|
|
116
120
|
|
|
117
121
|
book_id: str
|
|
@@ -122,6 +126,7 @@ class AdoptionSide:
|
|
|
122
126
|
series_url: str
|
|
123
127
|
story_url: str
|
|
124
128
|
numbers: tuple[str, ...]
|
|
129
|
+
work_url: str = ""
|
|
125
130
|
|
|
126
131
|
|
|
127
132
|
@dataclass(frozen=True, slots=True)
|
|
@@ -153,6 +158,8 @@ class Evidence:
|
|
|
153
158
|
orphan_number: First chapter number from orphan, or empty string.
|
|
154
159
|
target_numbers: All chapter numbers from target, in table order.
|
|
155
160
|
stem: Normalized book title stem (title with chapter numbers removed).
|
|
161
|
+
same_work: The orphan is a chapter of the target's story or of its work (E5, optional;
|
|
162
|
+
SPI 3.2).
|
|
156
163
|
"""
|
|
157
164
|
|
|
158
165
|
author_match: bool
|
|
@@ -165,13 +172,14 @@ class Evidence:
|
|
|
165
172
|
orphan_number: str
|
|
166
173
|
target_numbers: tuple[str, ...]
|
|
167
174
|
stem: str
|
|
175
|
+
same_work: bool = False
|
|
168
176
|
|
|
169
177
|
@property
|
|
170
178
|
def score(self) -> int:
|
|
171
179
|
"""The optional-signal score: E4 +1, E5 +2, E6 +2, E7 +1 (0..6)."""
|
|
172
180
|
return (
|
|
173
181
|
(1 if self.same_site else 0)
|
|
174
|
-
+ (2 if self.same_series else 0)
|
|
182
|
+
+ (2 if self.same_series or self.same_work else 0)
|
|
175
183
|
+ (2 if self.url_affinity else 0)
|
|
176
184
|
+ (1 if self.adjacent else 0)
|
|
177
185
|
)
|
|
@@ -194,6 +202,42 @@ def _common_prefix_len(a: str, b: str) -> int:
|
|
|
194
202
|
return sum(1 for _ in itertools.takewhile(lambda p: p[0] == p[1], zip(a, b, strict=False)))
|
|
195
203
|
|
|
196
204
|
|
|
205
|
+
def same_series(a_series: str, a_series_url: str, b_series: str, b_series_url: str) -> bool:
|
|
206
|
+
"""Signal E5's series half: one non-empty normalised series name or series URL (``DEC-57``).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
a_series: The first book's series.
|
|
210
|
+
a_series_url: The first book's series URL.
|
|
211
|
+
b_series: The second book's series.
|
|
212
|
+
b_series_url: The second book's series URL.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
``True`` when the normalised names are equal and non-empty, or the URLs are.
|
|
216
|
+
"""
|
|
217
|
+
return (normalize_key(a_series) == normalize_key(b_series) != "") or (
|
|
218
|
+
a_series_url == b_series_url != ""
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def same_work(orphan_work_url: str, *, target_story_url: str, target_work_url: str) -> bool:
|
|
223
|
+
"""Signal E5's work half: the orphan is a chapter of the target's story or work (``DEC-57``).
|
|
224
|
+
|
|
225
|
+
URLs compare in their normalised form (``normalise_url``); an empty URL never matches.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
orphan_work_url: The orphan's work URL.
|
|
229
|
+
target_story_url: The target's own story URL.
|
|
230
|
+
target_work_url: The target's work URL.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
``True`` when the orphan's work is the target's story or the target's work.
|
|
234
|
+
"""
|
|
235
|
+
if not orphan_work_url:
|
|
236
|
+
return False
|
|
237
|
+
key = normalise_url(orphan_work_url)
|
|
238
|
+
return any(url and normalise_url(url) == key for url in (target_story_url, target_work_url))
|
|
239
|
+
|
|
240
|
+
|
|
197
241
|
def score_pair(orphan: AdoptionSide, target: AdoptionSide) -> Evidence:
|
|
198
242
|
"""Score one candidate adoption pair against the seven DEC-57 signals.
|
|
199
243
|
|
|
@@ -228,9 +272,10 @@ def score_pair(orphan: AdoptionSide, target: AdoptionSide) -> Evidence:
|
|
|
228
272
|
# 7. Same site: normalized site must be non-empty and equal
|
|
229
273
|
same_site = normalize_key(orphan.site) == normalize_key(target.site) != ""
|
|
230
274
|
|
|
231
|
-
# 8.
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
# 8. E5: same series, or the orphan is a chapter of the target's story or work (SPI 3.2)
|
|
276
|
+
series_match = same_series(orphan.series, orphan.series_url, target.series, target.series_url)
|
|
277
|
+
work_match = same_work(
|
|
278
|
+
orphan.work_url, target_story_url=target.story_url, target_work_url=target.work_url
|
|
234
279
|
)
|
|
235
280
|
|
|
236
281
|
# 9. URL affinity: common prefix of slug keys >= 8 characters
|
|
@@ -251,12 +296,13 @@ def score_pair(orphan: AdoptionSide, target: AdoptionSide) -> Evidence:
|
|
|
251
296
|
stem_match=stem_match,
|
|
252
297
|
numbers_disjoint=numbers_disjoint,
|
|
253
298
|
same_site=same_site,
|
|
254
|
-
same_series=
|
|
299
|
+
same_series=series_match,
|
|
255
300
|
url_affinity=url_affinity,
|
|
256
301
|
adjacent=adjacent,
|
|
257
302
|
orphan_number=orphan_number,
|
|
258
303
|
target_numbers=tuple(target.numbers),
|
|
259
304
|
stem=orphan_stem,
|
|
305
|
+
same_work=work_match,
|
|
260
306
|
)
|
|
261
307
|
|
|
262
308
|
|
|
@@ -275,7 +321,7 @@ def evidence_json(evidence: Evidence) -> str:
|
|
|
275
321
|
"E2": evidence.stem_match,
|
|
276
322
|
"E3": evidence.numbers_disjoint,
|
|
277
323
|
"E4": evidence.same_site,
|
|
278
|
-
"E5": evidence.same_series,
|
|
324
|
+
"E5": evidence.same_series or evidence.same_work,
|
|
279
325
|
"E6": evidence.url_affinity,
|
|
280
326
|
"E7": evidence.adjacent,
|
|
281
327
|
"orphan_number": evidence.orphan_number,
|
|
@@ -52,8 +52,8 @@ def slugify(text: str, *, max_length: int = 80) -> str:
|
|
|
52
52
|
Returns empty string for empty input or all-nonalnum input.
|
|
53
53
|
|
|
54
54
|
Examples:
|
|
55
|
-
>>> slugify("
|
|
56
|
-
'
|
|
55
|
+
>>> slugify("Sample Delta")
|
|
56
|
+
'sample_delta'
|
|
57
57
|
>>> slugify(" A -- B!! ")
|
|
58
58
|
'a_b'
|
|
59
59
|
>>> slugify("")
|
|
@@ -12,10 +12,10 @@ priority order:
|
|
|
12
12
|
``a``, ``Pt.02``->``b``), e.g. ``Ch. 03 Pt. 02`` -> ``03b``.
|
|
13
13
|
* With no ``Ch``, a trailing ``Part`` / ``Pt.`` / ``Session`` number is the chapter
|
|
14
14
|
(``Pt. 01`` -> ``01``). A ``Part``/``Book``/volume token *before* a ``Ch`` stays
|
|
15
|
-
in the book name (``
|
|
15
|
+
in the book name (``Sample Nettle Pt. 03 Ch. 05`` -> book ``Sample Nettle Pt. 03``).
|
|
16
16
|
* A bare trailing number or range is a chapter only when each part is <= 3 digits
|
|
17
17
|
(``Earth 22`` -> ``22``, ``Daughter 1-4`` -> ``1-4`` but ``Room 1203`` -> none);
|
|
18
|
-
the number may abut the book name directly (``
|
|
18
|
+
the number may abut the book name directly (``Gamma31`` -> ``31``), but a longer run
|
|
19
19
|
of digits is never split; a number that lives only in the URL slug is ignored.
|
|
20
20
|
* The number is preserved exactly as written (leading zeros kept, ranges keep
|
|
21
21
|
their hyphen), with spaces stripped (``Ch. 023 - 025`` -> ``023-025``).
|
|
@@ -54,7 +54,7 @@ _CH_PT_RE = re.compile(r"(?<![A-Za-z])(?:chapter|ch)\.?\s*(\d+)\s+(?:part|pt)\.?
|
|
|
54
54
|
_CH_RE = re.compile(rf"(?<![A-Za-z])(?:chapter|ch|c)\.?\s*({_ATOM})", re.I)
|
|
55
55
|
# A part/session marker anywhere (only consulted when there is no chapter marker).
|
|
56
56
|
_PT_RE = re.compile(rf"(?<![A-Za-z])(?:part|pt|session)\.?\s*({_ATOM})", re.I)
|
|
57
|
-
# A bare trailing number or range; a letter may abut it ("
|
|
57
|
+
# A bare trailing number or range; a letter may abut it ("Gamma31"), another digit may not.
|
|
58
58
|
# The range alternative comes first so "1-4" is taken whole instead of just its "4" — a
|
|
59
59
|
# merged anthology's own title is fed back through this function on the next merge.
|
|
60
60
|
_BARE_RE = re.compile(r"(?<!\d)(\d{1,3}\s*-\s*\d{1,3}|\d+)\s*$")
|
|
@@ -258,73 +258,74 @@ def extract_chapter_info(
|
|
|
258
258
|
Examples:
|
|
259
259
|
The table below is the acceptance oracle for the module-level rules —
|
|
260
260
|
every row is exercised verbatim by
|
|
261
|
-
``tests/unit/test_chapter_number.py::TestExampleTable`` (
|
|
262
|
-
|
|
261
|
+
``tests/unit/test_chapter_number.py::TestExampleTable`` (invented titles
|
|
262
|
+
that keep the numbering patterns of real chapter titles; the source URL is
|
|
263
|
+
not shown as it is not consulted):
|
|
263
264
|
|
|
264
265
|
| Raw title | Book name | Number |
|
|
265
266
|
| ---------------------------------------- | ------------------------------- | ------- |
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
|
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
|
276
|
-
|
|
|
277
|
-
|
|
|
278
|
-
|
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
|
|
|
288
|
-
|
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
| (The)
|
|
296
|
-
|
|
|
297
|
-
|
|
|
298
|
-
|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
307
|
-
|
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
|
|
|
311
|
-
|
|
|
312
|
-
|
|
|
313
|
-
|
|
|
314
|
-
|
|
|
315
|
-
|
|
|
316
|
-
|
|
|
317
|
-
|
|
|
318
|
-
|
|
|
319
|
-
|
|
|
320
|
-
|
|
|
321
|
-
|
|
|
322
|
-
|
|
|
323
|
-
|
|
|
324
|
-
|
|
|
325
|
-
|
|
|
326
|
-
|
|
|
327
|
-
|
|
|
267
|
+
| Sample Alpha Part 5 | Sample Alpha | 5 |
|
|
268
|
+
| Sample Alpha Pt. 01 | Sample Alpha | 01 |
|
|
269
|
+
| Sample Acorn Book II Ch. 052 | Sample Acorn Book II | 052 |
|
|
270
|
+
| Sample Badger (Part 10) | Sample Badger | 10 |
|
|
271
|
+
| Sample Cinder, Really?! Ch.45 | Sample Cinder, Really?! | 45 |
|
|
272
|
+
| Sample Dahlia Pt. 02 - A New Post | Sample Dahlia | 02 |
|
|
273
|
+
| Sample Egret Pt. 07a | Sample Egret | 07a |
|
|
274
|
+
| Sample Falcon Pt. 09 - The End... | Sample Falcon | 09 |
|
|
275
|
+
| Sample Epsilon - Pt. 01 | Sample Epsilon | 01 |
|
|
276
|
+
| Sample Garnet's Hollow | Sample Garnet's Hollow | |
|
|
277
|
+
| Sample Harbor's Edge | Sample Harbor's Edge | |
|
|
278
|
+
| Sample Indigo (2/5) | Sample Indigo | 2 |
|
|
279
|
+
| Sample Jasper Challenger 22 | Sample Jasper Challenger | 22 |
|
|
280
|
+
| Sample Kestrel C33 | Sample Kestrel | 33 |
|
|
281
|
+
| Sample Larch (Ch. 20) | Sample Larch | 20 |
|
|
282
|
+
| Sample Maple – Dallas | Sample Maple – Dallas | |
|
|
283
|
+
| Sample Nimbus: Expansion Ch 11 | Sample Nimbus: Expansion | 11 |
|
|
284
|
+
| Sample Opal Ch. 03 | Sample Opal | 03 |
|
|
285
|
+
| Sample Opal Ch. 03 Pt. 02 | Sample Opal | 03b |
|
|
286
|
+
| Sample Opal Ch. 05 Pt. 01 | Sample Opal | 05a |
|
|
287
|
+
| Sample Opal Ch. 05 Pt. 02 | Sample Opal | 05b |
|
|
288
|
+
| Sample Pebble (Ch. 02) | Sample Pebble | 02 |
|
|
289
|
+
| Sample Quartz Pt 5 | Sample Quartz | 5 |
|
|
290
|
+
| Sample Raven, Ch 20 | Sample Raven | 20 |
|
|
291
|
+
| Sample Heath | Sample Heath | |
|
|
292
|
+
| Sample Grove | Sample Grove | |
|
|
293
|
+
| Sample Sable - Book Two (Ch. 46) | Sample Sable - Book Two | 46 |
|
|
294
|
+
| Sample Thistle (750 Word Challenge) | Sample Thistle | |
|
|
295
|
+
| Sample Umber - Ch. 03 | Sample Umber | 03 |
|
|
296
|
+
| (The) Sample Violet | (The) Sample Violet | |
|
|
297
|
+
| Sample Willow - The Bus | Sample Willow - The Bus | |
|
|
298
|
+
| Sample Yarrow Ch. 02: Arrival | Sample Yarrow | 02 |
|
|
299
|
+
| Sample Zephyr | Sample Zephyr | |
|
|
300
|
+
| Sample Alder | Sample Alder | |
|
|
301
|
+
| Sample Bramble | Sample Bramble | |
|
|
302
|
+
| Sample Coral - Session 1 | Sample Coral | 1 |
|
|
303
|
+
| Sample Dove | Sample Dove | |
|
|
304
|
+
| Sample Elm | Sample Elm | |
|
|
305
|
+
| Sample Flint - Ch. 04 Amsterdam | Sample Flint | 04 |
|
|
306
|
+
| Sample Gale & Sons (Pt. 3 of 3) | Sample Gale & Sons | 3 |
|
|
307
|
+
| Sample Iris Bk. 06 Ch. 09 | Sample Iris Bk. 06 | 09 |
|
|
308
|
+
| Sample Jade - The Visitor Ch. 441-450 | Sample Jade - The Visitor | 441-450 |
|
|
309
|
+
| Sample Kelp B2 Ch. 08 | Sample Kelp B2 | 08 |
|
|
310
|
+
| Sample Lark | Sample Lark | |
|
|
311
|
+
| Sample Moss - Ch. 3 Sunday | Sample Moss | 3 |
|
|
312
|
+
| Sample Nettle Pt. 03 Ch. 05 | Sample Nettle Pt. 03 | 05 |
|
|
313
|
+
| Sample Onyx Ch. 32-34 | Sample Onyx | 32-34 |
|
|
314
|
+
| Sample Pine Ch. 02: Ada & Bea | Sample Pine | 02 |
|
|
315
|
+
| Sample Quill 03 | Sample Quill | 03 |
|
|
316
|
+
| Gamma31 | Gamma | 31 |
|
|
317
|
+
| Gamma 30 | Gamma | 30 |
|
|
318
|
+
| Sample Slate-Grey Pt. 01 | Sample Slate-Grey | 01 |
|
|
319
|
+
| Sample Tansy Pt. 02 Ch. 51-52 | Sample Tansy Pt. 02 | 51-52 |
|
|
320
|
+
| Sample Vale Room 1203 | Sample Vale Room 1203 | |
|
|
321
|
+
| Sample Wren Pt. 02 Ch. 07 | Sample Wren Pt. 02 | 07 |
|
|
322
|
+
| Sample Aster Ch. 641-650 | Sample Aster | 641-650 |
|
|
323
|
+
| Sample Briar Pt. 07 Ch. 16 | Sample Briar Pt. 07 | 16 |
|
|
324
|
+
| Sample Clover - Ch. 023 - 025 | Sample Clover | 023-025 |
|
|
325
|
+
| Sample Drift 03: Field Trip Ch. 05 | Sample Drift 03: Field Trip | 05 |
|
|
326
|
+
| Sample Frost 03.041 | Sample Frost 03 | 041 |
|
|
327
|
+
| Sample Fern 1-4 | Sample Fern | 1-4 |
|
|
328
|
+
| Sample Alpha 3 - 5 | Sample Alpha | 3-5 |
|
|
328
329
|
"""
|
|
329
330
|
_ = chapter_url
|
|
330
331
|
title = (chapter_title or "").strip()
|