Coverage for scripts/maffay.py: 100%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 06:18 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 06:18 +0000
1#!/usr/bin/env python3
2"""Return a bonmot from a random Peter Maffay song — behind `/rhiza:maffay`.
4The point of a bundled script rather than prose is the same as everywhere else in
5this plugin: a model asked to "pick a random song" does not pick randomly. It
6gravitates to the two or three best-known titles, so the command would feel broken
7by the third run. `random.choice` over a curated list actually is uniform, and
8`--seed` makes a run reproducible for tests and for a demo.
10**What is quoted, and what is not.** Each entry holds the song's *title line* — for
11Maffay that is nearly always the hook itself ("Über sieben Brücken musst du gehn",
12"Ich wollte nie erwachsen sein") — with the song and year attributed. Lyric bodies
13are deliberately **not** reproduced: a title is not a protectable work, a verse is,
14and shipping verses inside a plugin is redistribution. The ``apply`` line beside each
15entry is this repo's own gloss, not part of the song, and is labelled as such in the
16output so the two are never confused.
18Adding an entry is a one-line edit to ``BONMOTS``; keep the same shape and only add
19songs whose title and year you can actually confirm.
21Usage:
22 uv run --python 3.12 --no-project python \
23 scripts/maffay.py [--theme WORD] [--seed N] [--list] [--json]
25Exit codes:
26 0 a bonmot was returned (or the catalogue was listed)
27 1 --theme matched nothing in the catalogue
28"""
30from __future__ import annotations
32import argparse
33import json
34import random
35import sys
36from typing import Any
38EXIT_OK = 0
39EXIT_NO_MATCH = 1
41# line: the song's title line — the hook, and the only quoted text.
42# apply: this repo's gloss, not Maffay's words. Kept short and shipping-related.
43BONMOTS: tuple[dict[str, Any], ...] = (
44 {
45 "line": "Über sieben Brücken musst du gehn",
46 "song": "Über sieben Brücken musst du gehn",
47 "year": 1980,
48 "themes": ("geduld", "weg", "release"),
49 "apply": "Seven detours before the fix lands. Ship it anyway.",
50 },
51 {
52 "line": "Und es war Sommer",
53 "song": "Und es war Sommer",
54 "year": 1976,
55 "themes": ("nostalgie", "sommer"),
56 "apply": "The commit you remember fondly is the one you no longer have to maintain.",
57 },
58 {
59 "line": "So bist du",
60 "song": "So bist du",
61 "year": 1979,
62 "themes": ("annahme", "liebe"),
63 "apply": "Take the codebase as it is first; refactor it second.",
64 },
65 {
66 "line": "Ich wollte nie erwachsen sein",
67 "song": "Nessaja",
68 "year": 1983,
69 "themes": ("neugier", "spiel", "nessaja"),
70 "apply": "Curiosity is the feature; seniority is the side effect.",
71 },
72 {
73 "line": "Du",
74 "song": "Du",
75 "year": 1970,
76 "themes": ("liebe", "anfang"),
77 "apply": "One word, a whole career. Small diffs travel furthest.",
78 },
79 {
80 "line": "Josie",
81 "song": "Josie",
82 "year": 1979,
83 "themes": ("aufbruch", "abschied"),
84 "apply": "Some branches you say goodbye to instead of merging.",
85 },
86 {
87 "line": "Eiszeit",
88 "song": "Eiszeit",
89 "year": 1982,
90 "themes": ("kälte", "warnung"),
91 "apply": "A repo with no tests is an ice age with good intentions.",
92 },
93 {
94 "line": "Tabaluga",
95 "song": "Tabaluga",
96 "year": 1983,
97 "themes": ("mut", "nessaja", "spiel"),
98 "apply": "A small dragon, a long journey — that is every migration.",
99 },
100)
103def themes() -> list[str]:
104 """Return every theme keyword in the catalogue, sorted and deduplicated."""
105 return sorted({theme for entry in BONMOTS for theme in entry["themes"]})
108def candidates(theme: str | None) -> list[dict[str, Any]]:
109 """Return the entries matching *theme* — all of them when *theme* is None.
111 Matching is a case-insensitive substring test against the theme keywords and the
112 song title, so ``--theme sommer`` and ``--theme Brücken`` both land.
113 """
114 if theme is None:
115 return list(BONMOTS)
116 needle = theme.strip().casefold()
117 return [
118 entry
119 for entry in BONMOTS
120 if any(needle in t.casefold() for t in entry["themes"])
121 or needle in entry["song"].casefold()
122 ]
125def pick(theme: str | None = None, seed: int | None = None) -> dict[str, Any] | None:
126 """Return one random matching entry, or None when *theme* matches nothing."""
127 pool = candidates(theme)
128 if not pool:
129 return None
130 rng = random.Random(seed) # nosec B311 - a bonmot, not a secret
131 return rng.choice(pool)
134def render(entry: dict[str, Any]) -> str:
135 """Format *entry* for the terminal, keeping the gloss visibly separate."""
136 return "\n".join(
137 (
138 f"🎸 „{entry['line']}“",
139 f" — Peter Maffay, „{entry['song']}“ ({entry['year']})",
140 f" Für uns: {entry['apply']}",
141 )
142 )
145def main(argv: list[str] | None = None) -> int:
146 """Entry point: print one bonmot (or the catalogue) and return an exit code."""
147 parser = argparse.ArgumentParser(
148 description="Return a bonmot from a random Peter Maffay song.",
149 )
150 parser.add_argument("--theme", help="Only consider songs matching this keyword or title.")
151 parser.add_argument("--seed", type=int, help="Seed the choice, for a reproducible run.")
152 parser.add_argument(
153 "--list", dest="list_all", action="store_true", help="Print the whole catalogue instead."
154 )
155 parser.add_argument(
156 "--json", dest="json_output", action="store_true", help="Emit the result as JSON."
157 )
158 args = parser.parse_args(argv)
160 if args.list_all:
161 pool = candidates(args.theme)
162 if args.json_output:
163 print(json.dumps({"themes": themes(), "bonmots": pool}, ensure_ascii=False, indent=2))
164 else:
165 for item in pool:
166 print(render(item), end="\n\n")
167 print(f"{len(pool)} songs · themes: {', '.join(themes())}")
168 return EXIT_OK
170 entry = pick(args.theme, args.seed)
171 if entry is None:
172 print(
173 f"error: no song matches --theme {args.theme!r}. Known themes: {', '.join(themes())}",
174 file=sys.stderr,
175 )
176 return EXIT_NO_MATCH
178 print(json.dumps(entry, ensure_ascii=False) if args.json_output else render(entry))
179 return EXIT_OK
182if __name__ == "__main__":
183 sys.exit(main())