kleine Änderungen
This commit is contained in:
parent
0f9c815b86
commit
dc088fa214
47 changed files with 183868 additions and 573 deletions
214
bilder_04/erzeuge_or_architektur.py
Normal file
214
bilder_04/erzeuge_or_architektur.py
Normal 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)
|
||||
Loading…
Reference in a new issue