#!/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)