bindings: what a closed subject scheme says about a heading

book-pipeline held its own copy of the scheme operations bibclean already
had. The scheme itself — which headings a collection uses, their aliases,
their parents — stays with the collection, because that is the most
collection-specific thing there is. What a scheme *means* is the same
question everywhere, and this answers it: `canonical_subject`,
`compose_subjects`, `needs_subject`.

Checked against the Python over the library's 228 distinct tags and all
5,385 books: identical on every one.
This commit is contained in:
funkpower
2026-09-20 15:15:48 -04:00
parent 7a08ed953d
commit b28e272506
5 changed files with 70 additions and 0 deletions
+3
View File
@@ -81,6 +81,9 @@ except ValueError as why:
| `find_citations` | [`citefind`](../citefind/README.md) |
| `looks_like_footnote_citations` | whether a body chunk carries notes worth reading |
| `suggest_author` | [`bibclean`](../bibclean/README.md)'s author recovery |
| `canonical_subject` | what a closed subject scheme calls a heading |
| `compose_subjects` | the subjects a book should carry under one |
| `needs_subject` | whether it carries a heading the scheme knows |
| `citation_prompt` | the question `citefind --prompt` writes |
| `merge_citation_reply` | what `citefind --merge` does with the answer |
@@ -13,14 +13,17 @@ lines of JSON.
from ._catena import (
__version__,
cue_windows,
canonical_subject,
chunk_markdown,
citation_prompt,
compose_subjects,
count_tokens,
find_citations,
fuse_hits,
identify,
looks_like_footnote_citations,
merge_citation_reply,
needs_subject,
pack_cues,
parse_identifier,
parse_subtitles,
@@ -34,14 +37,17 @@ from ._catena import (
__all__ = [
"__version__",
"cue_windows",
"canonical_subject",
"chunk_markdown",
"citation_prompt",
"compose_subjects",
"count_tokens",
"find_citations",
"fuse_hits",
"identify",
"looks_like_footnote_citations",
"merge_citation_reply",
"needs_subject",
"pack_cues",
"parse_identifier",
"parse_subtitles",
@@ -39,6 +39,17 @@ def fuse_hits(
def identify(name: str, data: bytes) -> dict[str, Any]: ...
def find_citations(text: str) -> list[dict[str, Any]]: ...
def looks_like_footnote_citations(text: str) -> bool: ...
def canonical_subject(
scheme: Mapping[str, Any], name: str
) -> str | None: ...
def compose_subjects(
scheme: Mapping[str, Any],
subjects: Iterable[str],
title: str | None = ...,
) -> list[str]: ...
def needs_subject(
scheme: Mapping[str, Any], existing: Iterable[str]
) -> bool: ...
def suggest_author(
title: str, front_matter: str | None = ...
) -> dict[str, Any] | None: ...
+24
View File
@@ -215,6 +215,30 @@ pub fn parse_identifier(text: &str) -> Option<idnorm::Identifier> {
idnorm::parse(text)
}
/// What a closed subject scheme says about a set of headings.
///
/// The scheme is supplied rather than built in — which subjects a collection
/// uses is the most collection-specific thing there is. These take it as the
/// record `bibclean` reads from a file.
pub fn canonical_subject(scheme: &bibclean::scheme::Scheme, name: &str) -> Option<String> {
scheme.canonical(name).map(str::to_string)
}
/// The subjects a book should carry: the chosen ones, their parents, and
/// nothing the title rules out.
pub fn compose_subjects(
scheme: &bibclean::scheme::Scheme,
subjects: &[String],
title: Option<&str>,
) -> Vec<String> {
scheme.compose(subjects, title)
}
/// Whether a book has no heading the scheme recognises.
pub fn needs_subject(scheme: &bibclean::scheme::Scheme, existing: &[String]) -> bool {
scheme.needs_subject(existing)
}
/// The best author a record's own title and front matter can offer.
///
/// `None` when neither says anything that reads as a person's name, which is
+26
View File
@@ -220,6 +220,29 @@ fn identify<'py>(py: Python<'py>, name: &str, data: &[u8]) -> PyResult<Bound<'py
to_py(py, &crate::identify(name, data))
}
/// The subject a heading means under a scheme, or `None`.
#[pyfunction]
fn canonical_subject(scheme: &Bound<'_, PyAny>, name: &str) -> PyResult<Option<String>> {
Ok(crate::canonical_subject(&from_py(scheme)?, name))
}
/// The subjects a book should carry under a scheme.
#[pyfunction]
#[pyo3(signature = (scheme, subjects, title = None))]
fn compose_subjects(
scheme: &Bound<'_, PyAny>,
subjects: Vec<String>,
title: Option<&str>,
) -> PyResult<Vec<String>> {
Ok(crate::compose_subjects(&from_py(scheme)?, &subjects, title))
}
/// Whether a book carries no heading the scheme recognises.
#[pyfunction]
fn needs_subject(scheme: &Bound<'_, PyAny>, existing: Vec<String>) -> PyResult<bool> {
Ok(crate::needs_subject(&from_py(scheme)?, &existing))
}
/// The best author a record's own title and front matter can offer.
///
/// `None` when neither says anything that reads as a person's name.
@@ -314,6 +337,9 @@ fn _catena(module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_function(wrap_pyfunction!(find_citations, module)?)?;
module.add_function(wrap_pyfunction!(looks_like_footnote_citations, module)?)?;
module.add_function(wrap_pyfunction!(suggest_author, module)?)?;
module.add_function(wrap_pyfunction!(needs_subject, module)?)?;
module.add_function(wrap_pyfunction!(compose_subjects, module)?)?;
module.add_function(wrap_pyfunction!(canonical_subject, module)?)?;
module.add_function(wrap_pyfunction!(citation_prompt, module)?)?;
module.add_function(wrap_pyfunction!(merge_citation_reply, module)?)?;
module.add_function(wrap_pyfunction!(series_of, module)?)?;