mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-11 18:43:49 +01:00
The tally becomes a message history. Ordinal 9 lands on 0.1.0 by arithmetic rather than by choice, which is a pleasant coincidence for a release this size. What changes for the group, in the four bullets they will read: photos and videos now count (a caption-less photo was worth zero, so everyone's numbers jumped), people who said nothing appear at 0, nine new commands, and the bot remembers history from 30 August 2026. What changes underneath: an append-only per-message log holding metadata and never a body; days that are calendar days in Europe/Warsaw rather than "since the last post"; records, crown streaks and weekday averages; weekly and monthly summaries; and reports that survive an outage instead of being lost. Exercised in testbot before this bump, per DOCS-changelog.md: all nine commands ran clean, and kuhy confirmed the rendering on a real phone -- the one check that could not be done from here, since Signal's proportional font is why the columns are separated by a dash rather than padded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
110 lines
3.4 KiB
Python
110 lines
3.4 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
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Final
|
|
|
|
# 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 = 9
|
|
|
|
_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)
|
|
|
|
|
|
__version__: Final = version_for(RELEASE_ORDINAL)
|
|
POPE: Final = pope_for(RELEASE_ORDINAL)
|