214 lines
8.7 KiB
Python
214 lines
8.7 KiB
Python
#!/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)
|