mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:27:20 +01:00
Some checks are pending
CI / check (push) Waiting to run
Pope 19 of 267, 235-236. The batch import landed: 174 files from a phone export became 115 new photos in the album, deduplicated on exact bytes twice and stripped on the way in. Every command has photos now, including one that had none at all and so answered nobody who typed it. Two things that had to be fixed before "strip on import" was even possible, and both were live defects rather than new features: media_tags documented that "-all=" zeroes the QuickTime date tags and allowed them by value on that basis. Measured, it does not, so the positive check downstream refused every mp4 the bot was ever offered. "-time:all=" is what zeroes them. And a tag allowlist cannot see a metadata *track*. An iPhone clip already in the album carries mebx samples holding per-frame face bounding boxes and a face id, which survive a complete tag strip because they live in mdat as opaque payload with no tag to refuse. media_tracks drops the track and verifies with ffprobe that only video and audio survived. DOCS-changelog.md crossed the 250-line cap, so the ten most recent releases stay and the rest move to DOCS-changelog-archive.md. Nothing reads the older ones: announce caps a backlog at ten, and since() takes its floor from the version ladder rather than from the file, so a group quiet for twenty releases still gets the ten newest. Verified: a group last told 0.0.3 hears exactly ten. Not restarted. A bump is a publish -- restarting posts these bullets into every watched group, including the real one -- so that is kuhy's to do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt
209 lines
6.8 KiB
Python
209 lines
6.8 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Release versions named after the popes, in order.
|
|
|
|
Release *n* is pope *n*: 0.0.0 is Święty Piotr Apostoł, and the version is a
|
|
pure function of the ordinal rather than a judgement about the size of a
|
|
change. The one fixed point is Jan Paweł II at 2.13.7, which is what picks
|
|
the two constants below -- eight patches per minor, and majors opening at
|
|
ordinals 33 and 153:
|
|
|
|
(264 - 153) = 111 = 13 * 8 + 7 -> 2.13.7
|
|
|
|
Both earlier majors happen to divide evenly by eight as well (32 = 4 * 8,
|
|
120 = 15 * 8), so no bracket needs a fudge factor. The anchors are not
|
|
arbitrary either: 33 is Sylwester I, pope through the Edict of Milan and
|
|
Nicaea, and 153 is Wiktor II, the first pope elected after the 1054 schism.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Final
|
|
from urllib.parse import quote
|
|
|
|
# Ordinals at which a new major version opens. Ascending, first entry 1.
|
|
MAJOR_STARTS: Final = (1, 33, 153)
|
|
PATCHES_PER_MINOR: Final = 8
|
|
|
|
# Which release this build is. Bump by one per release; everything else,
|
|
# including the version string in pyproject.toml, follows from it.
|
|
RELEASE_ORDINAL: Final = 19
|
|
|
|
_POPES_FILE: Final = Path(__file__).with_name("popes.json")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Pope:
|
|
"""One entry of the papal list, as the version ladder needs it."""
|
|
|
|
ordinal: int
|
|
name: str
|
|
# The pl.wikipedia article title, which is name plus any "(papież)"
|
|
# disambiguator -- so it is the URL slug, and name is not.
|
|
title: str
|
|
reign: str
|
|
portrait: str
|
|
latin: str
|
|
|
|
|
|
def _load() -> tuple[Pope, ...]:
|
|
"""Read popes.json once, at import."""
|
|
document: dict[str, Any] = json.loads(_POPES_FILE.read_text(encoding="utf-8"))
|
|
return tuple(
|
|
Pope(
|
|
ordinal=int(entry["ordinal"]),
|
|
name=str(entry["name"]),
|
|
title=str(entry["title"]),
|
|
reign=str(entry["reign"]),
|
|
portrait=str(entry["portrait"]),
|
|
latin=str(entry["latin"]),
|
|
)
|
|
for entry in document["popes"]
|
|
)
|
|
|
|
|
|
POPES: Final = _load()
|
|
|
|
|
|
def version_for(ordinal: int) -> str:
|
|
"""Return the dotted version string for the given papal ordinal.
|
|
|
|
Raises:
|
|
ValueError: if the ordinal is outside the known list.
|
|
"""
|
|
if not 1 <= ordinal <= len(POPES):
|
|
message = f"no pope with ordinal {ordinal}; the list has {len(POPES)}"
|
|
raise ValueError(message)
|
|
major = max(index for index, start in enumerate(MAJOR_STARTS) if ordinal >= start)
|
|
offset = ordinal - MAJOR_STARTS[major]
|
|
return f"{major}.{offset // PATCHES_PER_MINOR}.{offset % PATCHES_PER_MINOR}"
|
|
|
|
|
|
def pope_for(ordinal: int) -> Pope:
|
|
"""Return the pope with the given ordinal.
|
|
|
|
Raises:
|
|
ValueError: if the ordinal is outside the known list.
|
|
"""
|
|
if not 1 <= ordinal <= len(POPES):
|
|
message = f"no pope with ordinal {ordinal}; the list has {len(POPES)}"
|
|
raise ValueError(message)
|
|
return POPES[ordinal - 1]
|
|
|
|
|
|
def ordinal_for(version: str) -> int:
|
|
"""Return the papal ordinal a version string denotes.
|
|
|
|
Raises:
|
|
ValueError: if no release carries that version.
|
|
"""
|
|
for pope in POPES:
|
|
if version_for(pope.ordinal) == version:
|
|
return pope.ordinal
|
|
message = f"no release carries version {version!r}"
|
|
raise ValueError(message)
|
|
|
|
|
|
# Latin ordinals exactly as popes.json spells them, and the number each
|
|
# carries. The vocabulary is closed: these fifteen words are every trailing
|
|
# token in the file's `latin` column apart from four epithets, and a compound
|
|
# reads as a sum -- "Leo Decimus Quartus" is Leo 10+4.
|
|
LATIN_ORDINALS: Final = {
|
|
"Primus": 1,
|
|
"Secundus": 2,
|
|
"Tertius": 3,
|
|
"Quartus": 4,
|
|
"Quintus": 5,
|
|
"Sextus": 6,
|
|
"Septimus": 7,
|
|
"Octavus": 8,
|
|
"Nonus": 9,
|
|
"Decimus": 10,
|
|
"Undecimus": 11,
|
|
"Duodecimus": 12,
|
|
"Duodevicesimus": 18,
|
|
"Undevicesimus": 19,
|
|
"Vicesimus": 20,
|
|
}
|
|
|
|
# The five popes whose Latin trails an epithet or an artifact rather than an
|
|
# ordinal, so stripping cannot find the stem. Enumerated rather than pattern
|
|
# matched: the list is closed at 267 and a rule broad enough to drop "Magnus"
|
|
# would also eat a legitimate second name like "Ioannes Paulus".
|
|
_STEM_OVERRIDES: Final = {
|
|
4: "Clemens", # Clemens Romanus
|
|
45: "Leo", # Leo Magnus
|
|
64: "Gregorius", # Gregorius Magnus
|
|
68: "Adeodatus", # Adeodatus lub Deusdedit -- Polish "or" in a Latin field
|
|
105: "Nicolaus", # Nicolaus Magnus
|
|
}
|
|
|
|
_ROMAN: Final = re.compile(r"^[IVXLC]+$")
|
|
|
|
WIKI_BASE: Final = "https://pl.wikipedia.org/wiki/"
|
|
|
|
|
|
def latin_stem(pope: Pope) -> str:
|
|
"""Return the Latin name with any spelled-out ordinal removed."""
|
|
override = _STEM_OVERRIDES.get(pope.ordinal)
|
|
if override is not None:
|
|
return override
|
|
words = pope.latin.split()
|
|
while words and words[-1] in LATIN_ORDINALS:
|
|
words.pop()
|
|
return " ".join(words)
|
|
|
|
|
|
def roman_numeral(pope: Pope) -> str:
|
|
"""Return the pope's Roman numeral, or "" for the 44 who carry none.
|
|
|
|
Read off the Polish name rather than the Latin, because the Latin is the
|
|
field that omits it: Pius I is stored as plain "Pius". No name holds two
|
|
numeral-shaped tokens, so the first hit is the answer.
|
|
"""
|
|
for token in pope.name.split():
|
|
if _ROMAN.match(token):
|
|
return token
|
|
return ""
|
|
|
|
|
|
def latin_name(pope: Pope) -> str:
|
|
"""Return the release name for a pope: Latin stem plus Roman numeral.
|
|
|
|
This is what a version is called everywhere -- in the group, in
|
|
DOCS-changelog.md, in /healthz and in the release commit subject. Not the
|
|
Polish name, and not the raw `latin` field, which spells the ordinal out
|
|
("Leo Decimus Quartus") where a version wants "Leo XIV".
|
|
"""
|
|
numeral = roman_numeral(pope)
|
|
stem = latin_stem(pope)
|
|
return f"{stem} {numeral}" if numeral else stem
|
|
|
|
|
|
def pope_line(pope: Pope) -> str:
|
|
"""Where a release's pope sits in the list, and when he reigned.
|
|
|
|
Derived from popes.json on both counts -- the ordinal, and the `reign`
|
|
string exactly as the table spells it, which is why a single-year
|
|
pontificate reads "pontyfikat 2025" rather than being padded into a
|
|
range it never had. Typing either by hand is how a changelog ends up
|
|
claiming a pope reigned in the wrong century.
|
|
"""
|
|
return f"papież nr {pope.ordinal} z {len(POPES)}, pontyfikat {pope.reign}"
|
|
|
|
|
|
def article_url(pope: Pope) -> str:
|
|
"""Return the pl.wikipedia URL for a pope.
|
|
|
|
Built from title, not name: half the early popes live at "Ewaryst
|
|
(papież)" and only the title carries the disambiguator.
|
|
"""
|
|
return WIKI_BASE + quote(pope.title.replace(" ", "_"))
|
|
|
|
|
|
__version__: Final = version_for(RELEASE_ORDINAL)
|
|
POPE: Final = pope_for(RELEASE_ORDINAL)
|