#!/usr/bin/env python3 # Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Regenerate ``signal_bot/popes.json`` from the Polish Wikipedia pope list. Run this by hand when a new pope is elected; the output is committed, so the bot never touches the network to learn its own version name. The ``Lp.`` column of *Poczet papieży* is the authority for the ordinal. It is the reason this is scraped rather than recalled: antipopes, Benedykt IX's three separate reigns and the Stefan II question all shift the count, and being wrong by one silently moves Jan Paweł II off 2.13.7. """ from __future__ import annotations import json import re import sys from pathlib import Path from typing import Final import httpx API: Final = ( "https://pl.wikipedia.org/w/api.php?action=parse" "&page=Poczet%20papie%C5%BCy&prop=wikitext&format=json&formatversion=2" ) # Wikimedia's policy requires a descriptive agent with a contact URL; the # generic client default is rate-limited hard and may be blocked outright. USER_AGENT: Final = "signal-bot popes generator (https://github.com/kuhyx/signal-bot)" # One row per pope: "|." (later rows add a second, per-era numbering that # is deliberately ignored), then the linked name, then the portrait file. _ROW: Final = re.compile(r"\|(\d+)\.(?: */ *'''\d+\.''')?\s*\n\|\[?\[?([^\]|\n]+)") _PORTRAIT: Final = re.compile(r"\[\[Plik:([^\]|]+)") # A year is a wiki link whose whole target is digits. Matching bare numbers # instead catches "100px" from an image and the "31" of "31 stycznia". _YEARS: Final = re.compile(r"\[\[(\d{1,4})\]\]") # The list spells the first entry "Piotr"; kuhy asked for his full title. FIRST_POPE: Final = "Święty Piotr Apostoł" EXPECTED_TOTAL: Final = 267 JOHN_PAUL_II: Final = 264 def _clean(name: str) -> str: """Strip the wiki disambiguation suffix from a linked page title.""" return re.sub(r"\s*\(papież\)", "", name).strip() def _reign(row: str) -> str: """Return a row's reign years as ``start-end``, or ``start-`` if reigning. The reign is the row's last cell -- but a row that ends a wikitable runs on into the prose after ``|}``, which is how Jan Pawel II first came out as "100" (an image width) and Leon XIV as "2005-069" (a footnote). """ cell = row.split("\n|}", maxsplit=1)[0].rsplit("\n|", 1)[-1] years = _YEARS.findall(cell) if not years: return "" if len(years) == 1: return f"{years[0]}-" if cell.rstrip().endswith(("-", "\u2013")) else years[0] return f"{years[0]}-{years[-1]}" def parse(wikitext: str) -> list[dict[str, object]]: """Return one entry per pope, ordered by the list's own Lp. column.""" popes: dict[int, dict[str, object]] = {} for row in re.split(r"\n\|-", wikitext): match = _ROW.match(row.lstrip("\n")) portrait = _PORTRAIT.search(row) if match is None or portrait is None: continue ordinal = int(match.group(1)) popes.setdefault( ordinal, { "ordinal": ordinal, "name": _clean(match.group(2)), "reign": _reign(row), "portrait": portrait.group(1).strip(), }, ) return [popes[n] for n in sorted(popes)] def _rejection(popes: list[dict[str, object]]) -> str: """Return why this list must not be written, or an empty string.""" if [p["ordinal"] for p in popes] != list(range(1, len(popes) + 1)): return "ordinals are not contiguous from 1" if len(popes) != EXPECTED_TOTAL: return f"got {len(popes)} popes, expected {EXPECTED_TOTAL}" if popes[JOHN_PAUL_II - 1]["name"] != "Jan Paweł II": return f"#{JOHN_PAUL_II} is not Jan Paweł II" return "" def main() -> int: """Write popes.json, refusing to write anything that fails its own checks.""" response = httpx.get(API, headers={"User-Agent": USER_AGENT}, timeout=60.0) response.raise_for_status() popes = parse(str(response.json()["parse"]["wikitext"])) if popes: popes[0]["name"] = FIRST_POPE rejection = _rejection(popes) if rejection: sys.stderr.write(f"refusing to write: {rejection}\n") return 1 out = Path(__file__).resolve().parent.parent / "signal_bot" / "popes.json" document = {"source": API, "popes": popes} out.write_text(json.dumps(document, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") sys.stdout.write(f"wrote {len(popes)} popes to {out}\n") return 0 if __name__ == "__main__": raise SystemExit(main())