Files
catena/examples/python/shelve_a_library.py
T

111 lines
4.2 KiB
Python
Raw Normal View History

"""What a book says about itself, checked against what a register says back.
A catalogue's subjects and titles come from wherever its files came from. Two
things can correct that without a model: the cataloguing block the book prints
in its own front matter, and the register that assigned its ISBN. Neither is
trustworthy alone — a scan that starts mid-frontmatter yields a running head
where the title should be, and an ISBN a catalogue has been carrying wrongly
returns a confidently wrong book. Together they are worth acting on.
python examples/python/shelve_a_library.py converted/*.md
With no arguments it reads a book from Project Gutenberg, which prints no
cataloguing block at all — the honest common case, and worth seeing.
"""
import pathlib
import re
import sys
import urllib.request
import catena
# The closed list this library is browsed by. `Heading = term, term` gives the
# words that file a book under it; a heading on its own matches its own name.
SHELF = """
Philosophy = philosophy, philosophical
Ancient Philosophy = ancient philosophy, plato, aristotle
Political Science = political science, politics, government
Theology = theology, christianity, religion
Church History = church history, ecclesiastical history
Byzantine History = byzantium, byzantine, constantinople
History = history, historiography
Literature = literature, fiction, poetry
Psychology = psychology
Ethics
Logic
"""
GUTENBERG = "https://www.gutenberg.org/cache/epub/5827/pg5827.txt"
def sample(into: pathlib.Path) -> str:
"""Locke's *Essay*, which Gutenberg strips the front matter from."""
book = into / "locke.txt"
if not book.exists():
into.mkdir(parents=True, exist_ok=True)
print(f"fetching {GUTENBERG}", file=sys.stderr)
with urllib.request.urlopen(GUTENBERG, timeout=120) as reply: # noqa: S310
book.write_bytes(reply.read())
return book.read_text(encoding="utf-8", errors="replace")
def same(a: str, b: str) -> bool:
"""Whether two printings of one title are the same title.
Compared on letters and digits alone. A register and a title page differ
over case, over a curly apostrophe against a straight one, and over
whether the subtitle is part of the title — none of which is a
disagreement, and all of which a plain string comparison calls one.
"""
fold = lambda s: re.sub(r"[^a-z0-9]+", "", (s or "").lower())
a, b = fold(a), fold(b)
return bool(a) and bool(b) and (a in b or b in a)
def look(text: str, name: str) -> None:
printed = catena.read_colophon(text, shelf=SHELF)
title = printed.get("title")
isbns = printed.get("isbns") or []
print(f"\n{name}")
print(f" printed title {title or '— (no title page in these pages)'}")
if printed.get("authors"):
print(f" printed authors {', '.join(printed['authors'])}")
if printed.get("shelved"):
print(f" shelved under {', '.join(printed['shelved'])}")
for raw in printed.get("unshelved", []):
# Not a failure: a heading the shelf has no place for is how you find
# out the shelf is missing one.
print(f" no shelf for {raw}")
if not printed.get("subjects"):
print(" subjects — (no cataloguing block printed)")
if not isbns:
print(" register — (nothing to look up; one source only)")
return
# The only call here that opens a socket, and it blocks.
found = catena.fetch_identifier(isbns[0], polite="you@example.org")
if not found or not found.get("found"):
print(f" register {isbns[0]} is unknown to every register asked")
return
print(f" register says {found['title']} / {', '.join(found.get('authors') or [])}")
agrees = bool(title) and same(title, found["title"])
print(f" verdict {'corroborated' if agrees else 'two sources disagree — ask a person'}")
def main() -> int:
paths = [pathlib.Path(p) for p in sys.argv[1:]]
if not paths:
look(sample(pathlib.Path("/tmp/catena-example")), "Locke, from Gutenberg")
return 0
for path in paths:
look(path.read_text(encoding="utf-8", errors="replace"), path.name)
return 0
if __name__ == "__main__":
raise SystemExit(main())