Coverage for scripts/maffay.py: 100%

45 statements  

« 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`. 

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 rng = random.Random(seed) # nosec B311 - a bonmot, not a secret 

131 return rng.choice(pool) 

132 

133 

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 ) 

143 

144 

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) 

159 

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 

169 

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 

177 

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

179 return EXIT_OK 

180 

181 

182if __name__ == "__main__": 

183 sys.exit(main())