kleine Änderungen

This commit is contained in:
dschlueter 2026-09-09 07:37:07 +02:00
commit dc088fa214
47 changed files with 183868 additions and 573 deletions

View file

@ -0,0 +1,214 @@
#!/usr/bin/env python3
# erzeuge_or_architektur.py
"""
Erzeugt das Schichtenbild der OR-Plattform im Kapitel Praxisfallen:
bilder_04/kap15_or_plattform.svg
ERSETZT die bisherige, von Hand gezeichnete Fassung. Der Anlass war ein
Widerspruch: Die Tabelle im Abschnitt nennt SECHS Schichten, das gezeichnete
Bild daneben zeigte nur FUENF. Die sechste - "Betrieb" mit Monitoring,
Audit-Trail und Alarmen - fehlte; "Audit-Trail" kam nur noch als Beschriftung
eines Pfeils vor. Genau so entstehen Bilder, die dem Text daneben
widersprechen, und genau deshalb bekommt dieses hier einen Generator.
Die Schichten werden NICHT in diesem Skript aufgezaehlt, sondern aus der
Tabelle des Kapitels gelesen. Damit gibt es nur eine Quelle: Wer eine Zeile in
50_Praxis.md ergaenzt, ergaenzt das Bild mit. Fehlt die Tabelle oder aendert
sich ihr Aufbau, bricht der Generator ab, statt ein halbes Bild zu schreiben.
Dargestellt wird die Kette als das, was sie ist: Die ersten fuenf Schichten
sind ein Datenweg von den Quellsystemen bis zur Ausfuehrung. "Betrieb" ist
keine Station auf diesem Weg, sondern liegt neben allen - Monitoring und
Audit-Trail betreffen jede Schicht. Als sechster Kasten in der Reihe waere sie
falsch dargestellt; deshalb steht sie als Spange daneben. Dass sie beim
Handzeichnen ganz verschwand, hat vermutlich denselben Grund.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_or_architektur.py
Benoetigt: matplotlib
"""
from __future__ import annotations
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from stil_04 import FARBEN, speichere # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch # noqa: E402
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
KAPITEL = os.path.join(BASIS, "Operations_Research_mit_Python_Version_04",
"50_Praxis.md")
UEBERSCHRIFT = "## Architektur einer produktionsreifen OR-Plattform"
# Was zwischen den Schichten fliesst. Steht nicht in der Tabelle - die
# beschreibt die Schichten, nicht die Uebergaenge. Fuenf Stationen in der
# Kette ergeben VIER Pfeile; lies_schichten() rechnet das nach.
UEBERGAENGE = [
"Events · REST · Polling",
"geprüfter Snapshot (JSON)",
"Ergebnis + Kostenzerlegung",
"Freigabe",
]
# Die Schicht, die neben der Kette steht statt in ihr.
QUERSCHNITT = "Betrieb"
def saeubere(zelle: str) -> str:
"""Markdown-Auszeichnung und Indexmarken aus einer Tabellenzelle nehmen."""
zelle = re.sub(r"\{idx:[^}]*\}", "", zelle)
zelle = zelle.replace("**", "").replace("`", "")
return zelle.strip()
def lies_schichten() -> list[tuple[str, str, str]]:
"""Die Schichtentabelle des Kapitels als Liste (Schicht, Aufgabe, Werkzeuge).
Einzige Quelle fuer den Bildinhalt - siehe Modulkommentar."""
with open(KAPITEL, encoding="utf-8") as datei:
text = datei.read()
if UEBERSCHRIFT not in text:
raise SystemExit(f"OR-Architektur: Überschrift '{UEBERSCHRIFT}' nicht "
f"in {os.path.basename(KAPITEL)} gefunden.")
rest = text.split(UEBERSCHRIFT, 1)[1]
zeilen = []
gefunden = False
for zeile in rest.splitlines():
if zeile.startswith("| Schicht |"):
gefunden = True
continue
if not gefunden:
continue
if zeile.startswith("| ---"):
continue
if not zeile.startswith("|"):
break
felder = [saeubere(f) for f in zeile.strip().strip("|").split("|")]
if len(felder) != 3:
raise SystemExit(f"OR-Architektur: Tabellenzeile mit "
f"{len(felder)} statt 3 Spalten: {zeile!r}")
zeilen.append(tuple(felder))
if not zeilen:
raise SystemExit("OR-Architektur: Die Schichtentabelle wurde nicht "
"gefunden. Hat sich der Abschnitt geändert?")
if zeilen[-1][0] != QUERSCHNITT:
raise SystemExit(
f"OR-Architektur: Die letzte Tabellenzeile heißt "
f"'{zeilen[-1][0]}', erwartet wurde '{QUERSCHNITT}'. Diese "
f"Schicht wird als Spange neben der Kette gezeichnet, nicht in "
f"ihr - die Zuordnung muss stimmen.")
# Die Kette sind alle Zeilen ausser der Querschnittsschicht; zwischen n
# Stationen liegen n-1 Pfeile.
if len(zeilen) - 2 != len(UEBERGAENGE):
raise SystemExit(
f"OR-Architektur: {len(zeilen) - 1} Stationen in der Kette "
f"brauchen {len(zeilen) - 2} beschriftete Übergänge, es sind "
f"aber {len(UEBERGAENGE)}. Beim Ergänzen einer Schicht gehört "
f"ein Übergang dazu.")
return zeilen
def umbreche(text: str, breite: int) -> str:
"""Einfacher Wortumbruch - textwrap.fill ohne den Import."""
zeilen, aktuell = [], ""
for wort in text.split():
if aktuell and len(aktuell) + 1 + len(wort) > breite:
zeilen.append(aktuell)
aktuell = wort
else:
aktuell = f"{aktuell} {wort}".strip()
if aktuell:
zeilen.append(aktuell)
return "\n".join(zeilen)
def zeichne(schichten) -> None:
kette = schichten[:-1]
quer = schichten[-1]
figur, achse = plt.subplots(figsize=(8.8, 6.6))
achse.set_axis_off()
kastenbreite, kastenhoehe = 6.4, 1.24
abstand = 1.88
links = 0.0
for stufe, (name, aufgabe, werkzeuge) in enumerate(kette):
unten = -stufe * abstand
achse.add_patch(FancyBboxPatch(
(links, unten), kastenbreite, kastenhoehe,
boxstyle="round,pad=0.02,rounding_size=0.10",
facecolor=FARBEN["flaeche"], edgecolor=FARBEN["haupt"],
linewidth=1.6, zorder=3))
achse.text(links + 0.18, unten + kastenhoehe - 0.26, name,
fontsize=10.5, color=FARBEN["haupt"], fontweight="bold",
va="center", zorder=4)
achse.text(links + 0.18, unten + kastenhoehe - 0.52,
umbreche(aufgabe, 66), fontsize=8.5,
color=FARBEN["text"], va="top", zorder=4)
achse.text(links + 0.18, unten + 0.17,
umbreche(werkzeuge, 70), fontsize=8,
color=FARBEN["gedaempft"], va="bottom", style="italic",
zorder=4)
if stufe < len(kette) - 1:
pfeilspitze = unten - abstand + kastenhoehe
achse.add_patch(FancyArrowPatch(
(links + kastenbreite / 2, unten),
(links + kastenbreite / 2, pfeilspitze),
arrowstyle="-|>", mutation_scale=16,
color=FARBEN["zweit"], linewidth=1.6, zorder=2))
achse.text(links + kastenbreite / 2 + 0.16,
(unten + pfeilspitze) / 2, UEBERGAENGE[stufe],
fontsize=8, color=FARBEN["zweit"], va="center",
zorder=4)
# Die Querschnittsschicht als Spange neben der ganzen Kette.
unten_ganz = -(len(kette) - 1) * abstand
spange_links = links + kastenbreite + 0.45
achse.add_patch(FancyBboxPatch(
(spange_links, unten_ganz), 0.92,
(len(kette) - 1) * abstand + kastenhoehe,
boxstyle="round,pad=0.02,rounding_size=0.10",
facecolor=FARBEN["warnung"], alpha=0.13,
edgecolor=FARBEN["warnung"], linewidth=1.6, zorder=3))
achse.text(spange_links + 0.46,
unten_ganz + ((len(kette) - 1) * abstand + kastenhoehe) / 2,
f"{quer[0]}{quer[1]}\n{quer[2]}",
fontsize=9, color=FARBEN["warnung"], fontweight="bold",
ha="center", va="center", rotation=90, zorder=4)
achse.text(links, kastenhoehe + 0.42,
"Der Optimierer ist kein Skript, sondern ein Dienst",
fontsize=11.5, color=FARBEN["text"], fontweight="bold")
achse.text(links, -(len(kette) - 1) * abstand - 0.42,
f"{quer[0]}“ ist keine Station auf dem Weg — Monitoring und "
f"Audit-Trail betreffen jede Schicht.\nDeshalb steht sie "
f"neben der Kette, nicht in ihr.",
fontsize=8.5, color=FARBEN["gedaempft"], va="top")
achse.set_xlim(-0.3, spange_links + 1.5)
achse.set_ylim(-(len(kette) - 1) * abstand - 1.3, kastenhoehe + 0.85)
achse.set_aspect("equal")
speichere(figur, "kap15_or_plattform")
if __name__ == "__main__":
schichten = lies_schichten()
for name, aufgabe, werkzeuge in schichten:
markierung = " (Spange)" if name == QUERSCHNITT else ""
print(f" {name:<22}{markierung}")
print(f" {len(schichten)} Schichten aus der Tabelle in "
f"{os.path.basename(KAPITEL)} gelesen — "
f"{len(schichten) - 1} in der Kette, 1 quer.")
zeichne(schichten)