Coverage for plugin/scripts/maffay.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-15 14:46 +0000

1#!/usr/bin/env python3 

2"""Return a bonmot from a random Peter Maffay song — behind `/rhiza:maffay`. 

3 

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. 

9 

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. 

17 

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. 

20 

21Usage: 

22 uv run --python 3.12 --no-project python \ 

23 scripts/maffay.py [--theme WORD] [--seed N] [--list] [--json] 

24 

25Exit codes: 

26 0 a bonmot was returned (or the catalogue was listed) 

27 1 --theme matched nothing in the catalogue 

28""" 

29 

30from __future__ import annotations 

31 

32import argparse 

33import json 

34import random 

35import sys 

36from typing import Any 

37 

38EXIT_OK = 0 

39EXIT_NO_MATCH = 1 

40 

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) 

101 

102 

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"]}) 

106 

107 

108def candidates(theme: str | None) -> list[dict[str, Any]]: 

109 """Return the entries matching *theme* — all of them when *theme* is None. 

110 

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 ] 

123 

124 

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 # noqa/nosec: picking a Peter Maffay lyric, not a key. `--seed` exists so the tests 

131 # can pin the choice, which a cryptographic generator would make impossible. 

132 rng = random.Random(seed) # noqa: S311 # nosec B311 

133 return rng.choice(pool) 

134 

135 

136def render(entry: dict[str, Any]) -> str: 

137 """Format *entry* for the terminal, keeping the gloss visibly separate.""" 

138 return "\n".join( 

139 ( 

140 f"🎸 „{entry['line']}", 

141 f" — Peter Maffay, „{entry['song']}“ ({entry['year']})", 

142 f" Für uns: {entry['apply']}", 

143 ) 

144 ) 

145 

146 

147def main(argv: list[str] | None = None) -> int: 

148 """Entry point: print one bonmot (or the catalogue) and return an exit code.""" 

149 parser = argparse.ArgumentParser( 

150 description="Return a bonmot from a random Peter Maffay song.", 

151 ) 

152 parser.add_argument("--theme", help="Only consider songs matching this keyword or title.") 

153 parser.add_argument("--seed", type=int, help="Seed the choice, for a reproducible run.") 

154 parser.add_argument( 

155 "--list", dest="list_all", action="store_true", help="Print the whole catalogue instead." 

156 ) 

157 parser.add_argument( 

158 "--json", dest="json_output", action="store_true", help="Emit the result as JSON." 

159 ) 

160 args = parser.parse_args(argv) 

161 

162 if args.list_all: 

163 pool = candidates(args.theme) 

164 if args.json_output: 

165 print(json.dumps({"themes": themes(), "bonmots": pool}, ensure_ascii=False, indent=2)) 

166 else: 

167 for item in pool: 

168 print(render(item), end="\n\n") 

169 print(f"{len(pool)} songs · themes: {', '.join(themes())}") 

170 return EXIT_OK 

171 

172 entry = pick(args.theme, args.seed) 

173 if entry is None: 

174 print( 

175 f"error: no song matches --theme {args.theme!r}. Known themes: {', '.join(themes())}", 

176 file=sys.stderr, 

177 ) 

178 return EXIT_NO_MATCH 

179 

180 print(json.dumps(entry, ensure_ascii=False) if args.json_output else render(entry)) 

181 return EXIT_OK 

182 

183 

184if __name__ == "__main__": 

185 sys.exit(main())