Trading-Plattform-Pruefung: Abendkontrolle prueft die Sicherung des vorigen Handelstags (Paket E9, Task 726)
abendkontrolle_wsl.py nimmt --sicherung (Stand der Ablage vom Host) und meldet per ntfy, wenn die Sicherung des vorigen Handelstags fehlt, kein Archiv hat, nicht bestaetigt ist oder die Bestaetigung nicht passt; vor erster-tag still, ohne erster-tag ein Befund. abendkontrolle_angriffe.py mit --nur-sicherung (60 Faelle), abendkontrolle_mutanten.py mit --nur und 30 Sicherungs-Mutanten. Gegengeprueft: byte-gleich mit der Plankopie, Lauf gegen echte Archive ohne Abweichung. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
1 parent
c5db92c6de
commit
3e55bb915d
3 files changed
+514
-3
No files matched your search
@@ -5,6 +5,7 @@ Grundlinie; ohne Netz (--ohne-broker/--ohne-kerzen, Kalender und Journal aus Dat
|
||||
Auswertung fuer die Brokerzeile) und ohne Meldung (--ohne-meldung). Faelle an zwei Listenpositionen,
|
||||
wo die Reihenfolge zaehlen kann (AI/Regelwerk/pruefwerkzeuge.md, Abschnitt 3).
|
||||
Aufruf in WSL (Venv) und unter Windows: python abendkontrolle_angriffe.py
|
||||
Nur die Faelle zur Sicherung (Paket E9, Task 726), in WSL: python abendkontrolle_angriffe.py --nur-sicherung
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet; sonst 1 mit der Liste der Abweichungen.
|
||||
"""
|
||||
|
||||
@@ -811,12 +812,269 @@ def ibkr_windows_faelle() -> None:
|
||||
f"ibkr {ibkr} alpaca {alpaca_tage}")
|
||||
|
||||
|
||||
# --- Sicherung des vorigen Handelstags (Paket E9, Task 726), WSL-Teil ------------------------------
|
||||
|
||||
# TAG ist ein Mittwoch, S_VORHER der Dienstag davor, S_DAVOR der Montag. Die Summen sind erfunden.
|
||||
S_VORHER = "2026-09-22"
|
||||
S_DAVOR = "2026-09-21"
|
||||
S_ERSTER = "2026-09-14"
|
||||
S_SUMME = "ab" * 32
|
||||
S_ANDERE = "cd" * 32
|
||||
S_KALENDER = [
|
||||
{"date": S_DAVOR, "open": "09:30", "close": "16:00"},
|
||||
{"date": S_VORHER, "open": "09:30", "close": "16:00"},
|
||||
] + KALENDER_TAG
|
||||
|
||||
|
||||
def s_eintraege(anders: dict | None = None, ohne: tuple = ()) -> dict:
|
||||
"""Beide Instanzen an S_DAVOR und S_VORHER mit Archiv und bestaetigt; anders ersetzt, ohne loescht."""
|
||||
eintraege = {
|
||||
f"{tag}-{instanz}": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}
|
||||
for tag in (S_DAVOR, S_VORHER)
|
||||
for instanz in ("alpaca", "ibkr")
|
||||
}
|
||||
for name in ohne:
|
||||
del eintraege[name]
|
||||
eintraege.update(anders or {})
|
||||
return eintraege
|
||||
|
||||
|
||||
def s_stand(eintraege: dict | None = None, **oben) -> dict:
|
||||
"""Ein Stand wie von abendkontrolle-host.sh; oben ersetzt oder ergaenzt Schluessel der obersten Ebene."""
|
||||
stand = {"erster_tag": S_ERSTER, "eintraege": s_eintraege() if eintraege is None else eintraege}
|
||||
stand.update(oben)
|
||||
return stand
|
||||
|
||||
|
||||
def fahre_sicherung(wurzel: Path, ziel: Path, teil: str, *, schalter: bool = True, tag: str = TAG,
|
||||
meldung: bool = False, env: dict | None = None) -> dict:
|
||||
"""Ein Lauf eines Teils mit --sicherung; ziel ist der Projektordner (alpaca) oder der Betriebsordner (ibkr)."""
|
||||
befehl = [
|
||||
sys.executable, str(HIER / "abendkontrolle_wsl.py"), "--tag", tag,
|
||||
"--kalender", str(wurzel / "kalender.json"), "--journal", str(wurzel / "journal.txt"),
|
||||
"--dienste", str(wurzel / "dienste.json"),
|
||||
]
|
||||
if teil == "ibkr":
|
||||
befehl += ["--teil", "ibkr", "--ibkr-ordner", str(ziel), "--projekt", str(wurzel),
|
||||
"--ibkr-konto", str(wurzel / "konto.json"), "--jetzt", NACH_SCHLUSS]
|
||||
else:
|
||||
befehl += ["--projekt", str(ziel), "--ohne-kerzen", "--ohne-broker"]
|
||||
if schalter:
|
||||
befehl += ["--sicherung", str(wurzel / "sicherung.json")]
|
||||
if not meldung:
|
||||
befehl.append("--ohne-meldung")
|
||||
umgebung = {k: v for k, v in os.environ.items() if k != "ABENDKONTROLLE_AUSWERTUNG"}
|
||||
umgebung.update(env or {})
|
||||
lauf = subprocess.run(befehl, capture_output=True, text=True, env=umgebung, timeout=300)
|
||||
try:
|
||||
ergebnis = json.loads(lauf.stdout.strip().splitlines()[-1])
|
||||
except (IndexError, json.JSONDecodeError):
|
||||
return {"befunde": [f"KEINE JSON-AUSGABE exit {lauf.returncode}: {lauf.stderr[-300:]}"], "exit": lauf.returncode}
|
||||
ergebnis["_exit"] = lauf.returncode
|
||||
return ergebnis
|
||||
|
||||
|
||||
def sicherung_faelle() -> None:
|
||||
def fall(name, stand, *, befund, teil="alpaca", kalender=S_KALENDER, anzahl=None, exit_code=0,
|
||||
schalter=True, tag=TAG, auch=(), nicht=(), fakt=None):
|
||||
"""stand: None heisst, die Datei fehlt; eine Zeichenkette wird roh geschrieben, sonst als JSON."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
ziel = baue_ibkr(wurzel) if teil == "ibkr" else baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(kalender), encoding="utf-8")
|
||||
if isinstance(stand, str):
|
||||
(wurzel / "sicherung.json").write_text(stand, encoding="utf-8")
|
||||
elif stand is not None:
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(stand), encoding="utf-8")
|
||||
erwarte("Sicherung " + name, fahre_sicherung(wurzel, ziel, teil, schalter=schalter, tag=tag),
|
||||
befund=befund, anzahl=anzahl, exit_code=exit_code, auch=auch, nicht=nicht, fakt=fakt)
|
||||
|
||||
a = f"{S_VORHER}-alpaca"
|
||||
i = f"{S_VORHER}-ibkr"
|
||||
offen = {"summe": S_SUMME, "bestaetigt": None, "archiv": True}
|
||||
|
||||
# Grundlinie in beiden Teilen; ohne Schalter bleibt das Werkzeug, wie es war.
|
||||
fall("Grundlinie alpaca", s_stand(), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": a, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("Grundlinie ibkr", s_stand(), teil="ibkr", befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung") == {"name": i, "erster_tag": S_ERSTER, "geprueft": True})
|
||||
fall("ohne Schalter", s_stand(s_eintraege({a: offen})), schalter=False, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
# Umgebung und Form der Datei
|
||||
fall("Datei fehlt", None, befund="Sicherung nicht geprueft (FileNotFoundError", anzahl=1)
|
||||
fall("Datei kein JSON", "<html>", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Datei leer", "", befund="Sicherung nicht geprueft (JSONDecodeError", anzahl=1)
|
||||
fall("Stand null", "null", befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("Stand als Liste", [], befund="Sicherung: Stand ist kein Objekt", anzahl=1)
|
||||
fall("eintraege als Liste", s_stand([]), befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("eintraege fehlt", {"erster_tag": S_ERSTER}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag fehlt", {"eintraege": s_eintraege()}, befund="Sicherung: Stand in unerwarteter Form", anzahl=1)
|
||||
fall("erster_tag als Zahl", s_stand(erster_tag=20260914), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
fall("erster_tag kein Datum", s_stand(erster_tag="morgen"), befund="Sicherung: erster_tag ist kein Datum", anzahl=1)
|
||||
# Fehler, die abendkontrolle-host.sh in den Stand schreibt
|
||||
fall("Host meldet fehler", {"fehler": "Ablage der Sicherung fehlt"},
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung fehlt", anzahl=1)
|
||||
fall("fehler neben vollem Stand", s_stand(fehler="Ablage der Sicherung nicht lesbar"),
|
||||
befund="Sicherung nicht geprueft: Ablage der Sicherung nicht lesbar", anzahl=1)
|
||||
# Erster Sicherungstag (seit der Plankritik Runde 1): null und ein Tag nach TAG sind Befunde, sonst
|
||||
# schwiege die Pruefung auf Dauer. Still bleibt nur ein voriger Handelstag vor erster_tag; TAG selbst
|
||||
# als erster_tag ist erlaubt, und der erste Sicherungstag selbst wird geprueft.
|
||||
fall("erster_tag null", s_stand({}, erster_tag=None),
|
||||
befund="Sicherung nicht geprueft: auf dem Docker-Host fehlt erster-tag der Sicherung", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f
|
||||
and not any("erster-tag" in h for h in e.get("hinweise", [])))
|
||||
fall("erster_tag nach dem geprueften Tag", s_stand({}, erster_tag="2026-09-24"),
|
||||
befund=f"Sicherung nicht geprueft: erster_tag 2026-09-24 liegt nach dem geprueften Tag {TAG}", anzahl=1,
|
||||
fakt=lambda f, e: "sicherung" not in f)
|
||||
fall("voriger Handelstag vor erster_tag", s_stand({}, erster_tag=TAG), befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("geprueft") is False)
|
||||
fall("erster_tag ist der vorige Handelstag", s_stand({}, erster_tag=S_VORHER),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Eintrag des vorigen Handelstags
|
||||
fall("Eintrag fehlt", s_stand(s_eintraege(ohne=(a,))), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag null", s_stand(s_eintraege({a: None})), befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
fall("Eintrag als Liste", s_stand(s_eintraege({a: [S_SUMME, S_SUMME]})),
|
||||
befund=f"Sicherung {a} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# summe
|
||||
for titel, eintrag in (
|
||||
("summe null", {"summe": None, "bestaetigt": S_SUMME}),
|
||||
("summe fehlt", {"bestaetigt": S_SUMME}),
|
||||
("summe leer", {"summe": "", "bestaetigt": S_SUMME}),
|
||||
("summe in Grossbuchstaben", {"summe": S_SUMME.upper(), "bestaetigt": S_SUMME.upper()}),
|
||||
("summe mit 63 Zeichen", {"summe": S_SUMME[:-1], "bestaetigt": S_SUMME[:-1]}),
|
||||
("summe mit 65 Zeichen", {"summe": S_SUMME + "a", "bestaetigt": S_SUMME + "a"}),
|
||||
("summe als Zahl", {"summe": 5, "bestaetigt": S_SUMME}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: keine gueltige Summe", anzahl=1)
|
||||
# bestaetigt
|
||||
for titel, eintrag, text in (
|
||||
("bestaetigt null", {"summe": S_SUMME, "bestaetigt": None}, "nicht bestaetigt"),
|
||||
("bestaetigt fehlt", {"summe": S_SUMME}, "nicht bestaetigt"),
|
||||
("bestaetigt leer", {"summe": S_SUMME, "bestaetigt": ""}, "nicht bestaetigt"),
|
||||
("bestaetigt in Grossbuchstaben", {"summe": S_SUMME, "bestaetigt": S_SUMME.upper()}, "nicht bestaetigt"),
|
||||
("bestaetigt erstes Zeichen anders", {"summe": S_SUMME, "bestaetigt": "0" + S_SUMME[1:]},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("bestaetigt letztes Zeichen anders", {"summe": S_SUMME, "bestaetigt": S_SUMME[:-1] + "0"},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
("Bestaetigung einer anderen Sicherung", {"summe": S_SUMME, "bestaetigt": S_ANDERE},
|
||||
"die Bestaetigung passt nicht zur Summe"),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: {**eintrag, "archiv": True}})), befund=f"Sicherung {a}: {text}", anzahl=1)
|
||||
# archiv (seit der Plankritik Runde 1): Bei gueltiger Summe zaehlt nur true, sonst fehlt das Archiv.
|
||||
for titel, eintrag in (
|
||||
("archiv false", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": False}),
|
||||
("archiv fehlt", {"summe": S_SUMME, "bestaetigt": S_SUMME}),
|
||||
("archiv null", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": None}),
|
||||
("archiv als Text", {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": "true"}),
|
||||
("archiv false, nicht bestaetigt", {"summe": S_SUMME, "bestaetigt": None, "archiv": False}),
|
||||
):
|
||||
fall(titel, s_stand(s_eintraege({a: eintrag})), befund=f"Sicherung {a}: Archiv fehlt auf dem Docker-Host",
|
||||
anzahl=1)
|
||||
# Instanz nach --teil
|
||||
fall("Teil ibkr, nur Alpaca bestaetigt", s_stand(s_eintraege({i: offen})), teil="ibkr",
|
||||
befund=f"Sicherung {i}: nicht bestaetigt", anzahl=1, nicht=("alpaca",))
|
||||
fall("Teil alpaca, nur IBKR bestaetigt", s_stand(s_eintraege({a: offen})),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1, nicht=("ibkr",))
|
||||
fall("Teil ibkr, Eintrag fehlt", s_stand(s_eintraege(ohne=(i,))), teil="ibkr",
|
||||
befund=f"Sicherung {i} fehlt auf dem Docker-Host", anzahl=1)
|
||||
# Reihenfolge: derselbe Schaden am ersten und am letzten Eintrag
|
||||
andere = {name: wert for name, wert in s_eintraege().items() if name != a}
|
||||
fall("Schaden am ersten Eintrag", s_stand({a: offen, **andere}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
fall("Schaden am letzten Eintrag", s_stand({**andere, a: offen}), befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
# Kalender: der juengste Handelstag vor TAG, nicht der Vortag und nicht der letzte Listeneintrag
|
||||
fall("Kalender unsortiert", s_stand(s_eintraege({a: offen})), kalender=list(reversed(S_KALENDER)),
|
||||
befund=f"Sicherung {a}: nicht bestaetigt", anzahl=1)
|
||||
feiertag = [S_KALENDER[0]] + KALENDER_TAG
|
||||
fall("Feiertag am Vortag, der Tag davor bestaetigt", s_stand(s_eintraege({a: offen})), kalender=feiertag,
|
||||
befund=None, anzahl=0, fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{S_DAVOR}-alpaca")
|
||||
fall("Feiertag am Vortag, der Tag davor offen", s_stand(s_eintraege({f"{S_DAVOR}-alpaca": offen})),
|
||||
kalender=feiertag, befund=f"Sicherung {S_DAVOR}-alpaca: nicht bestaetigt", anzahl=1)
|
||||
fall("Kalender ohne Tag davor", s_stand(), kalender=KALENDER_TAG, befund="Sicherung: kein Handelstag in den", anzahl=1)
|
||||
fall("Kalender als Objekt", s_stand(), kalender={}, befund="Sicherung nicht geprueft (ValueError", anzahl=2,
|
||||
auch=("Kalender in unerwarteter Form",))
|
||||
fall("Kalender Eintrag ohne date, erste Position", s_stand(), kalender=[{"open": "09:30"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender Eintrag ohne date, letzte Position", s_stand(), kalender=S_KALENDER + [{"date": 5}],
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=2)
|
||||
fall("Kalender mit Datum in anderer Form", s_stand(), kalender=[{"date": "22.09.2026"}] + S_KALENDER,
|
||||
befund="Sicherung nicht geprueft (ValueError", anzahl=1)
|
||||
# Fenster (seit der Plankritik Runde 1): SICHERUNG_FENSTER_TAGE Kalendertage vor TAG, der Rand gehoert
|
||||
# dazu, TAG selbst nie. Das Werkzeug prueft keinen Wochentag; Rand und Tag davor sind Wochenendtage.
|
||||
rand = "2026-09-13"
|
||||
fall("Kalender voriger Handelstag am Rand des Fensters",
|
||||
s_stand(s_eintraege({f"{rand}-alpaca": {"summe": S_SUMME, "bestaetigt": S_SUMME, "archiv": True}}),
|
||||
erster_tag="2026-09-01"),
|
||||
kalender=[{"date": rand, "open": "09:30", "close": "16:00"}] + KALENDER_TAG, befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == f"{rand}-alpaca")
|
||||
fall("Kalender voriger Handelstag ausserhalb des Fensters", s_stand(),
|
||||
kalender=[{"date": "2026-09-12", "open": "09:30", "close": "16:00"}] + KALENDER_TAG,
|
||||
befund=f"Sicherung: kein Handelstag in den 10 Tagen vor {TAG} im Kalender", anzahl=1)
|
||||
fall("Kalender Tag selbst nie der vorige", s_stand(s_eintraege({f"{TAG}-alpaca": offen})),
|
||||
kalender=KALENDER_TAG + [S_KALENDER[1]], befund=None, anzahl=0,
|
||||
fakt=lambda f, e: f.get("sicherung", {}).get("name") == a)
|
||||
# Zusaetzlich: unbekannte Schluessel und fremde Namen stoeren nicht
|
||||
fall("unbekannte Schluessel und fremde Namen",
|
||||
s_stand(s_eintraege({"notiz": {"summe": None}, f"{S_VORHER}-paper": None}), neu=1), befund=None, anzahl=0)
|
||||
# Bedienfehler: Ein falscher Tag bricht den Teil ab, und die Sicherung meldet sich trotzdem.
|
||||
fall("Tag falsch", s_stand(), tag="2026-13-40", befund="abgebrochen", exit_code=2, anzahl=2,
|
||||
auch=("Sicherung nicht geprueft (ValueError",))
|
||||
|
||||
# Meldeweg: Ein Befund der Sicherung allein loest die Meldung aus (lokaler Probeserver, kein Netz).
|
||||
with tempfile.TemporaryDirectory() as tmp_server:
|
||||
mitschrift = Path(tmp_server) / "mitschrift.jsonl"
|
||||
port = 18767
|
||||
server = subprocess.Popen([sys.executable, str(HIER / "ntfy_probe_server.py"), str(port), str(mitschrift)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
import socket
|
||||
import time
|
||||
for _ in range(50):
|
||||
try:
|
||||
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wurzel = Path(tmp)
|
||||
projekt = baue_projekt(wurzel)
|
||||
(wurzel / "kalender.json").write_text(json.dumps(S_KALENDER), encoding="utf-8")
|
||||
(wurzel / "sicherung.json").write_text(json.dumps(s_stand(s_eintraege({a: offen}))), encoding="utf-8")
|
||||
text = (ECHTES_PROJEKT / "config" / "paper.toml").read_text(encoding="utf-8")
|
||||
anker = 'ntfy_url = "https://ntfy.sh"'
|
||||
if text.count(anker) != 1:
|
||||
raise RuntimeError("Anker ntfy_url fehlt in config/paper.toml")
|
||||
(projekt / "config" / "paper.toml").write_text(
|
||||
text.replace(anker, f'ntfy_url = "http://127.0.0.1:{port}"\nklartext_lokal = true'), encoding="utf-8")
|
||||
(wurzel / "secrets.toml").write_text('[ntfy]\naktiv = true\nthema = "abendkontrolle-angriff"\n',
|
||||
encoding="utf-8")
|
||||
ergebnis = fahre_sicherung(wurzel, projekt, "alpaca", meldung=True,
|
||||
env={"TRADING_GEHEIMNISSE": str(wurzel / "secrets.toml")})
|
||||
zeilen = ([json.loads(z) for z in mitschrift.read_text(encoding="utf-8").split("\n") if z.strip()]
|
||||
if mitschrift.exists() else [])
|
||||
titel = [str(z.get("title") or "") for z in zeilen]
|
||||
befunde = ergebnis.get("befunde", [])
|
||||
gesendet = (ergebnis.get("fakten") or {}).get("meldung")
|
||||
ok = (gesendet == "gesendet" and len(zeilen) == 1 and f"Abendkontrolle {TAG}: 1 Befund(e)" in titel[0]
|
||||
and len(befunde) == 1 and befunde[0].startswith(f"Sicherung {a}: nicht bestaetigt"))
|
||||
zeile = (f"{'OK ' if ok else 'FEHL'} Sicherung Befund geht in die Meldung: "
|
||||
f"befunde={befunde[:2]} titel={titel} meldung={gesendet}")
|
||||
print(zeile)
|
||||
if not ok:
|
||||
abweichungen.append(zeile)
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
windows_faelle()
|
||||
ibkr_windows_faelle()
|
||||
elif "--nur-sicherung" in sys.argv[1:]:
|
||||
sicherung_faelle()
|
||||
else:
|
||||
wsl_faelle()
|
||||
ibkr_wsl_faelle()
|
||||
sicherung_faelle()
|
||||
print(f"ERGEBNIS: {len(abweichungen)} Abweichung(en)")
|
||||
sys.exit(1 if abweichungen else 0)
|
||||
Reference in new issue
Block a user