Files
moneyfy/backend/scripts/vendor_simple_icons.py
T
moneyfyandClaude Opus 5 8edb69fe6a feat(logos): Provider-Kette, lokaler Cache und Markenfarben
- simple-icons als kompakter Index im Repository (3.459 Marken, 2 MB gzip),
  erzeugt von scripts/vendor_simple_icons.py bzw. `make vendor-icons`
- logo.dev und Brandfetch als optionale Adapter, ohne Schlüssel übersprungen
- Favicon-Fallback und generierter Buchstaben-Avatar als Garantie
- Bei eindeutigem Offline-Treffer unterbleiben Anfragen nach außen komplett
- Cache im Dateisystem nach SHA-256, Auslieferung nur über /api/logos/{id}
  mit immutable-Header und ETag
- Markenfarbe aus SVG-Fills bzw. per k-Means (k=4) über 64x64 Pixel, dazu eine
  aufgehellte Variante mit mindestens 4,5:1 Kontrast auf dunklem Grund
- Kandidatensuche mit Vorauswahl, Auswahl, Upload und Zurücksetzen
- Bildtyp wird nur noch am Inhalt bestimmt, nicht an der gemeldeten Kopfzeile
- 60 neue Tests, insgesamt 208 grün

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
2026-09-09 13:54:21 +02:00

115 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Erzeugt den lokalen simple-icons-Index aus dem npm-Paket.
Das npm-Paket enthält rund 3.500 einzelne SVG-Dateien (etwa 15 MB). Für den
Betrieb genügen Slug, Titel, Markenfarbe, Pfaddaten und Aliasnamen daraus wird
eine einzelne gzip-komprimierte JSON-Datei von etwa 2 MB.
Aufruf: python scripts/vendor_simple_icons.py [--version 16] [--keep-tmp]
"""
import argparse
import gzip
import json
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path
TARGET = Path(__file__).resolve().parent.parent / "app" / "assets" / "simple_icons.json.gz"
PATH_PATTERN = re.compile(r'<path\s+d="([^"]+)"')
def download(version: str, workdir: Path) -> Path:
"""Lädt das npm-Paket herunter und entpackt es."""
result = subprocess.run(
["npm", "pack", f"simple-icons@{version}"],
cwd=workdir,
capture_output=True,
text=True,
check=True,
)
archive = workdir / result.stdout.strip().splitlines()[-1]
with tarfile.open(archive) as tar:
tar.extractall(workdir, filter="data")
return workdir / "package"
def collect_aliases(entry: dict) -> list[str]:
"""Sammelt alternative Schreibweisen aus den Metadaten."""
names: list[str] = []
aliases = entry.get("aliases") or {}
for key in ("aka", "alt", "old", "dup"):
value = aliases.get(key)
if isinstance(value, list):
names.extend(item if isinstance(item, str) else item.get("title", "") for item in value)
elif isinstance(value, dict):
names.extend(str(item) for item in value.values())
return [name for name in names if name]
def build(package: Path) -> list[dict]:
"""Baut den kompakten Index aus Metadaten und SVG-Pfaden."""
metadata = json.loads((package / "data" / "simple-icons.json").read_text(encoding="utf-8"))
icons: list[dict] = []
for entry in metadata:
svg_file = package / "icons" / f"{entry['slug']}.svg"
if not svg_file.exists():
print(f" übersprungen (keine SVG-Datei): {entry['slug']}", file=sys.stderr)
continue
match = PATH_PATTERN.search(svg_file.read_text(encoding="utf-8"))
if not match:
print(f" übersprungen (kein Pfad): {entry['slug']}", file=sys.stderr)
continue
icon = {
"s": entry["slug"],
"t": entry["title"],
"h": entry["hex"],
"p": match.group(1),
}
aliases = collect_aliases(entry)
if aliases:
icon["a"] = aliases
icons.append(icon)
return icons
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", default="16", help="npm-Version von simple-icons.")
parser.add_argument("--keep-tmp", action="store_true", help="Arbeitsverzeichnis behalten.")
args = parser.parse_args()
workdir = Path(tempfile.mkdtemp(prefix="simple-icons-"))
try:
print(f"Lade simple-icons@{args.version} …")
package = download(args.version, workdir)
version = json.loads((package / "package.json").read_text())["version"]
icons = build(package)
payload = {"version": version, "icons": icons}
raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
TARGET.parent.mkdir(parents=True, exist_ok=True)
TARGET.write_bytes(gzip.compress(raw, 9))
print(
f"{len(icons)} Icons aus simple-icons {version} geschrieben nach "
f"{TARGET.relative_to(TARGET.parent.parent.parent)} "
f"({TARGET.stat().st_size / 1_000_000:.2f} MB)."
)
return 0
finally:
if not args.keep_tmp:
shutil.rmtree(workdir, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())