"""Abbau Restbestand: stellt am 2026-10-08 beide Paper-Konten ueber den Notweg glatt, Ersatztag 2026-10-09. Ersteller: Pascal Michel Erstellt: 2026-10-07 Doku: LIESMICH.md in diesem Ordner, Abschnitt "Paket AB" Koordinator-Werkzeug, nicht fuers Repo. Am 2026-10-07 um 20:13 beendete ein Druck auf den Netzschalter beide Paper-Runner mitten im Handel, ohne Glattstellen. Pascal am selben Abend: morgen soweit es geht alles verkaufen, Testlaeufe bis Dienstag gestoppt. Plan im Projekt: docs/plans/2026-10-07-paket-ab-abbau-restbestand/. Gestartet von der Windows-Aufgabe "Trading Plattform - Abbau Restbestand" (abbau_restbestand_einrichten.ps1) mit --modus scharf. Ablauf im Modus scharf, jeder Schritt mit seinem Grund: 1. Nur zwischen FRUEHESTENS und SPAETESTENS (Uhr dieses Rechners); sonst nichts, auch keine Datei. Ergebnisdatei mit "flach": true -> nichts mehr, nur eine Zeile im Log. 2. Runner-Units (systemctl show, LoadState und ActiveState): Laeuft eine, ist sie nicht geladen oder ihr Zustand nicht lesbar, wird diese Instanz nicht angefasst (Befund). systemctl is-active meldete auch fuer eine Unit, die es nicht gibt, inactive. Die Zeitgeber sind seit 2026-10-07 aus. 3. Marktuhr von Alpaca (abbau_alpaca_lesen.py): zu oder weniger als MIN_BIS_SCHLUSS_S bis zum Schluss -> nichts, nur eine Zeile im Log. Nicht lesbar -> Befund, nichts angefasst. 4. Bestaende beider Instanzen; nicht lesbar -> Befund, diese Instanz nicht angefasst. 5. Je Instanz mit Position oder offener Order genau ein Aufruf von abbau_glattstellen.sh (scripts/killswitch.py glattstellen; das Bestaetigungswort bleibt in WSL). Keine Schleife: Der naechste Termin der Aufgabe ist die Wiederholung. Wirft der Aufruf, gilt Exit None. 6. Nach einem Eingriff WARTEN_S warten und beide Bestaende neu lesen. 7. Ergebnisdatei abbau.json; "flach": true nur, wenn beide Instanzen nachher ohne Position und ohne offene Order gelesen sind und keine Instanz gesperrt ist. Sonst beobachtet der naechste Termin weiter. 8. Bei Eingriff, Befund oder dem ersten "flach" eine Zeile in Inbox/.md und eine Meldung mit hoher Prioritaet (abendkontrolle_wsl.py --meldung). Die Abendkontrolle ist in der Pause aus; diese Meldung ist das einzige Netz. Modus lesen: nur die lesenden Aufrufe, ohne Fenster und ohne die Sperre durch "flach"; kein Glattstellen, keine Meldung, keine Notiz, keine Datei. Das Ergebnis steht auf stdout. Jeder Aufruf nach draussen geht ueber wsl(); die Angriffsfaelle ersetzen sie. wsl.exe schreibt eigene Fehler in UTF-16LE: wsl() setzt WSL_UTF8=1 und entfernt NUL, sonst scheiterte die Meldung an einem NUL im Argument. Ausgabe und Befunde sind einzeilig: Ein fremder Fehlertext mit Zeilenumbruch setzte sonst eigene Zeilen, bis hin zu einer Ueberschrift, in die Tagesnotiz. Befunde aus Ausnahmen nennen nur den Typ; der Traceback steht nur in abbau.log. Annahme, benannt wie in abendkontrolle.py: Die Uhr dieses Rechners geht nach Berliner Zeit. Aufruf: python abbau_restbestand.py --modus scharf|lesen [--jetzt ISO] [--projekt PFAD] [--vault PFAD] Exit: 0 ohne Befund, 1 mit Befund, 2 Bedienfehler. """ import argparse import json import os import re import subprocess import sys import time import traceback from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from abendkontrolle import KEIN_FENSTER # noqa: E402 from abendkontrolle import WSL # noqa: E402 from abendkontrolle import WSL_PY # noqa: E402 from abendkontrolle import WSL_SKRIPT # noqa: E402 from abendkontrolle import schreibe_notiz # noqa: E402 VAULT = Path(r"D:\Claude") PROJEKT = VAULT / "Code Projekte" / "active - Trading Plattform" ABLAGE_TEIL = Path("data") / "paper" / "abbau" ERGEBNIS_NAME = "abbau.json" LOG_NAME = "abbau.log" # Entscheidung 4 der Anforderung, Berliner Ortszeit, beide Grenzen eingeschlossen. FRUEHESTENS = datetime(2026, 10, 8, 15, 40) SPAETESTENS = datetime(2026, 10, 9, 21, 45) MIN_BIS_SCHLUSS_S = 600 WARTEN_S = 30 SCHARF = "scharf" LESEN = "lesen" MODI = (SCHARF, LESEN) NICHTS = "nichts" STILL = "still" LESEN_ENDE = "lesen_ende" BERICHT = "bericht" INSTANZEN = ("alpaca", "ibkr") UNITS = {"alpaca": "trading-paper.service", "ibkr": "trading-paper-ibkr.service"} # Die Instanzordner in WSL. Nur dieses Skript nennt sie; die WSL-Teile bekommen sie als Argument. ORDNER_WSL = { "alpaca": "/mnt/d/Claude/Code Projekte/active - Trading Plattform", "ibkr": "/mnt/d/Claude/.arbeitsbaeume/trading-ibkr-betrieb", } HIER_WSL = "/mnt/d/Claude/.vault-tools/trading-plattform-pruefung" ALPACA_PY = "/home/claude/venvs/trading/bin/python" ALPACA_LESEN = HIER_WSL + "/abbau_alpaca_lesen.py" IBKR_LESEN = HIER_WSL + "/abbau_ibkr_lesen.sh" GLATTSTELLEN = HIER_WSL + "/abbau_glattstellen.sh" RUNNER_LAEUFT = ("active", "activating", "deactivating", "reloading", "refreshing") RUNNER_STEHT = ("inactive", "failed") RUNNER_TIMEOUT_S = 60 LESEN_TIMEOUT_S = 180 # killswitch.py: Gateway-Zeitlimit 10 s je Anfrage, bis zu drei Sammelversuche, 20 s Positionsfrist. GLATT_TIMEOUT_S = 600 MELDUNG_TIMEOUT_S = 120 ZEILEN_MAX = 12 ZEILE_MAX = 200 # CREATE_NO_WINDOW gibt es nur unter Windows; die Angriffsfaelle laufen auch in WSL. FENSTERLOS = KEIN_FENSTER if sys.platform == "win32" else 0 NUL = chr(0) # Die Ausgabe von scripts/ibkr_pruefen.py lesen, gemessen am 2026-10-07 gegen 22:56 (Anforderung). IBKR_POSITIONEN = re.compile(r"^Positionen: (\d+)\s*$") IBKR_ORDERS = re.compile(r"^Offene Orders: (\d+)\s*$") IBKR_ZEILE = re.compile(r"^ (\S+) (-?\d+)(?:\.0+)?\s*$") IBKR_GELESEN = "ERGEBNIS: gelesen" TITEL = "Abbau Restbestand" KOPF = "Abbau Restbestand (automatisch, Auftrag Pascal 2026-10-07)" GESPERRT_HINWEIS = "Der Killswitch jeder angefassten Instanz steht danach auf gesperrt; Freigeben bleibt Pascal." # Die Angriffsfaelle ersetzen schlafe und starte, damit kein Fall wartet und keiner einen Prozess startet. schlafe = time.sleep starte = subprocess.run def sauber(text: str) -> str: """Text ohne NUL: wsl.exe schreibt eigene Fehler in UTF-16LE, und ein NUL im Argument laesst subprocess scheitern.""" return text.replace(NUL, "") def einzeilig(text: str) -> str: """Ohne NUL und ohne Zeilenumbruch: Ein fremder Text bleibt eine Zeile in Ausgabe, Log, Notiz und Meldung.""" return sauber(text).replace("\r", " ").replace("\n", " ") def wsl(argumente: list[str], timeout: int) -> tuple[int, str, str]: """Ein Aufruf in WSL ohne Shell (--exec): Exit, stdout, stderr. Der einzige Weg nach draussen.""" lauf = starte( [WSL, "-d", "Debian", "-u", "claude", "--exec", *argumente], capture_output=True, timeout=timeout, creationflags=FENSTERLOS, env={**os.environ, "WSL_UTF8": "1"}, ) return ( lauf.returncode, sauber(lauf.stdout.decode("utf-8", errors="replace")), sauber(lauf.stderr.decode("utf-8", errors="replace")), ) def lies_runner(instanz: str) -> str: """LoadState und ActiveState der Runner-Unit (systemctl show); liest nur.""" _, ausgabe, _ = wsl(["systemctl", "show", "--property=LoadState,ActiveState", UNITS[instanz]], RUNNER_TIMEOUT_S) return ausgabe def runner_felder(ausgabe: str) -> tuple[str, str]: """LoadState und ActiveState aus der Ausgabe von systemctl show; was fehlt, ist leer.""" felder = {} for zeile in ausgabe.splitlines(): name, _, wert = zeile.partition("=") felder[name.strip()] = wert.strip() return felder.get("LoadState", ""), felder.get("ActiveState", "") def runner_urteil(laden: str, aktiv: str) -> str | None: """laeuft, steht, oder None (fail-closed). steht nur bei einer geladenen Unit, die inactive oder failed ist.""" if aktiv in RUNNER_LAEUFT: return "laeuft" if laden == "loaded" and aktiv in RUNNER_STEHT: return "steht" return None def lies_alpaca() -> dict: """Die JSON-Zeile von abbau_alpaca_lesen.py; ohne sie steht der Grund in beiden Fehlerfeldern.""" try: code, ausgabe, _ = wsl([ALPACA_PY, ALPACA_LESEN, "--projekt", ORDNER_WSL["alpaca"]], LESEN_TIMEOUT_S) except Exception as ausnahme: code, ausgabe = f"nicht gestartet, {type(ausnahme).__name__}", "" for zeile in reversed(ausgabe.splitlines()): zeile = zeile.strip() if not zeile.startswith("{"): continue try: daten = json.loads(zeile) except ValueError: break if isinstance(daten, dict): return daten break grund = f"abbau_alpaca_lesen.py ohne JSON-Zeile (Exit {code})" return {"uhr_fehler": grund, "bestand_fehler": grund} def uhr_lesbar(uhr) -> bool: """Die Marktuhr in der Form von abbau_alpaca_lesen.py: offen als bool, Sekunden als ganze Zahl.""" return ( isinstance(uhr, dict) and type(uhr.get("offen")) is bool and type(uhr.get("sekunden_bis_schluss")) is int ) def alpaca_bestand(daten: dict) -> dict | None: """Symbol -> Stueck und die Zahl offener Orders aus der JSON-Zeile, oder None, wenn nicht gelesen.""" positionen, orders = daten.get("positionen"), daten.get("orders") if not isinstance(positionen, list) or type(orders) is not int: return None stueck: dict[str, int] = {} for eintrag in positionen: if not isinstance(eintrag, dict) or not isinstance(eintrag.get("symbol"), str): return None if type(eintrag.get("menge")) is not int: return None stueck[eintrag["symbol"]] = stueck.get(eintrag["symbol"], 0) + eintrag["menge"] return {"positionen": stueck, "orders": orders} def ibkr_bestand(code: int, ausgabe: str) -> dict | None: """Positionen und offene Orders aus stdout von scripts/ibkr_pruefen.py lesen, oder None. Gelesen ist der Bestand nur mit Exit 0 oder 1 (1: Befund zur Kontogroesse, die Liste steht trotzdem da), mit der Zeile "ERGEBNIS: gelesen", beiden Zahlenzeilen und genau so vielen Positionszeilen, wie "Positionen: n" sagt. """ zeilen = ausgabe.splitlines() if not any(zeile.startswith(IBKR_GELESEN) for zeile in zeilen): return None if code not in (0, 1): return None anzahl = orders = None gezaehlt = 0 stueck: dict[str, int] = {} im_block = False for zeile in zeilen: treffer = IBKR_POSITIONEN.match(zeile) if treffer: anzahl, im_block = int(treffer.group(1)), True continue treffer = IBKR_ORDERS.match(zeile) if treffer: orders, im_block = int(treffer.group(1)), False continue if im_block: treffer = IBKR_ZEILE.match(zeile) if treffer is None: return None symbol = treffer.group(1).split(".")[0] stueck[symbol] = stueck.get(symbol, 0) + int(treffer.group(2)) gezaehlt += 1 if anzahl is None or orders is None or gezaehlt != anzahl: return None return {"positionen": stueck, "orders": orders} def lies_ibkr() -> tuple[dict | None, int | None]: """Bestand der IBKR-Instanz ueber abbau_ibkr_lesen.sh (ibkr_pruefen.py lesen) und dessen Exit.""" try: code, ausgabe, _ = wsl(["bash", IBKR_LESEN, ORDNER_WSL["ibkr"]], LESEN_TIMEOUT_S) except Exception: return None, None return ibkr_bestand(code, ausgabe), code def hat_bestand(bestand: dict | None) -> bool: """Position ungleich null oder offene Order. Ein nicht gelesener Bestand (None) hat keinen: fail-closed.""" return bestand is not None and (any(menge != 0 for menge in bestand["positionen"].values()) or bestand["orders"] > 0) def ist_flach(bestand: dict | None) -> bool: """Gelesen, keine Position ungleich null und keine offene Order.""" return bestand is not None and not any(menge != 0 for menge in bestand["positionen"].values()) and bestand["orders"] == 0 def flach_markiert(datei: Path) -> bool: """Ob eine fruehere Ergebnisdatei "flach": true traegt; unlesbar oder anders heisst nein.""" try: daten = json.loads(datei.read_text(encoding="utf-8")) except (OSError, ValueError): return False return isinstance(daten, dict) and daten.get("flach") is True def beschreibe(bestand: dict | None) -> str: """Ein Bestand fuer Ausgabe, Notiz und Meldung: Symbol, Richtung, Stueck, offene Orders. Nie ein Betrag.""" if bestand is None: return "nicht gelesen" teile = [ f"{symbol} {'long' if menge > 0 else 'short'} {abs(menge)}" for symbol, menge in sorted(bestand["positionen"].items()) if menge != 0 ] liste = ": " + ", ".join(teile) if teile else "" return f"{len(teile)} Position(en){liste}; offene Orders {bestand['orders']}" def glattstelle(instanz: str, modus: str) -> tuple[int, list[str]]: """Genau ein Aufruf von abbau_glattstellen.sh fuer eine Instanz; nur im Modus scharf. Die Zeilen kommen aus WSL schon gefiltert (Wort, Kontonummern, Betraege); hier werden sie nur gekuerzt. """ if modus != SCHARF: raise RuntimeError("glattstellen nur im Modus scharf") code, ausgabe, fehler = wsl(["bash", GLATTSTELLEN, instanz, ORDNER_WSL[instanz]], GLATT_TIMEOUT_S) zeilen = [zeile.strip()[:ZEILE_MAX] for zeile in (ausgabe + "\n" + fehler).splitlines() if zeile.strip()] return code, zeilen[-ZEILEN_MAX:] def melde(titel: str, text: str, modus: str) -> str: """Eine Meldung mit hoher Prioritaet ueber abendkontrolle_wsl.py --meldung; nur im Modus scharf.""" if modus != SCHARF: return "nicht gesendet: Modus lesen" titel, text = sauber(titel), sauber(text) try: code, ausgabe, _ = wsl([WSL_PY, WSL_SKRIPT, "--meldung", titel, text], MELDUNG_TIMEOUT_S) except Exception as ausnahme: return f"nicht gesendet: {type(ausnahme).__name__}" for zeile in reversed(ausgabe.splitlines()): zeile = zeile.strip() if zeile.startswith("{"): try: return str(json.loads(zeile).get("meldung"))[:200] except (ValueError, AttributeError): break return f"nicht gesendet: keine Antwort (Exit {code})" def logge(ablage: Path, text: str) -> None: """Eine Zeile in abbau.log; ein Fehler dabei haelt den Lauf nicht auf.""" try: ablage.mkdir(parents=True, exist_ok=True) with (ablage / LOG_NAME).open("a", encoding="utf-8", newline="\n") as datei: datei.write(f"{datetime.now().isoformat(timespec='seconds')} {text}\n") except OSError: pass def schreibe_ergebnis(ablage: Path, daten: dict) -> None: """abbau.json ueber eine Nachbardatei und os.replace: nie eine halbe Datei.""" ablage.mkdir(parents=True, exist_ok=True) neu = ablage / (ERGEBNIS_NAME + ".tmp") neu.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n") os.replace(neu, ablage / ERGEBNIS_NAME) class Lauf: """Was ein Lauf gesehen und getan hat; daraus entstehen Ausgabe, Ergebnisdatei, Notiz und Meldung.""" def __init__(self, jetzt: datetime, modus: str, ablage: Path) -> None: self.jetzt = jetzt self.modus = modus self.ablage = ablage self.ausgabe: list[str] = [] self.befunde: list[str] = [] self.gesperrt: set[str] = set() self.markt = "" self.vorher: dict = {} self.nachher: dict = {} self.eingriffe: dict = {} self.spur = "" def sage(self, text: str) -> None: self.ausgabe.append(einzeilig(text)) def befund(self, text: str) -> None: self.befunde.append(einzeilig(text)[:300]) def zusammenfassung(self) -> list[str]: """Je Instanz eine Zeile: vorher, was geschah, nachher.""" zeilen = [] for instanz in INSTANZEN: eingriff = self.eingriffe.get(instanz) tat = "nicht angefasst" if eingriff is None else f"glattstellen Exit {eingriff['exit']}" zeilen.append( f"{instanz}: vorher {beschreibe(self.vorher.get(instanz))}; {tat}; " f"nachher {beschreibe(self.nachher.get(instanz))}" ) return zeilen def fahre(lauf: Lauf) -> str: """Schritte 1 bis 6; gibt NICHTS, STILL, LESEN_ENDE oder BERICHT zurueck.""" scharf = lauf.modus == SCHARF if not scharf: lauf.sage("Modus lesen: kein Glattstellen, keine Meldung, keine Notiz, keine Datei") # 1 Fenster und Ergebnisdatei im_fenster = FRUEHESTENS <= lauf.jetzt <= SPAETESTENS lage = "innerhalb" if im_fenster else "ausserhalb" lauf.sage(f"Fenster {FRUEHESTENS:%Y-%m-%d %H:%M} bis {SPAETESTENS:%Y-%m-%d %H:%M}: {lage}") if scharf and not im_fenster: return NICHTS if flach_markiert(lauf.ablage / ERGEBNIS_NAME): lauf.sage("Ergebnisdatei sagt flach") if scharf: return STILL # 2 Runner-Units: ein laufender, nicht geladener oder unlesbarer Runner sperrt seine Instanz for instanz in INSTANZEN: try: laden, aktiv = runner_felder(lies_runner(instanz)) except Exception as ausnahme: laden, aktiv = "", f"nicht lesbar, {type(ausnahme).__name__}" zustand = f"LoadState={laden or 'leer'}, ActiveState={aktiv or 'leer'}" lauf.sage(f"Runner {instanz} ({UNITS[instanz]}): {zustand}") urteil = runner_urteil(laden, aktiv) if urteil != "steht": lauf.gesperrt.add(instanz) grund = "laeuft" if urteil == "laeuft" else "Zustand nicht lesbar" lauf.befund(f"{instanz}: Runner-Unit {UNITS[instanz]} {grund} ({zustand[:80]}); nicht angefasst") # 3 Marktuhr: zu oder bald zu heisst im Modus scharf still zu Ende alpaca = lies_alpaca() uhr = alpaca.get("uhr") if not uhr_lesbar(uhr): lauf.gesperrt.update(INSTANZEN) lauf.markt = "Marktuhr nicht lesbar" lauf.befund(f"Marktuhr nicht lesbar ({alpaca.get('uhr_fehler') or 'ohne Angabe'}); nichts angefasst") else: if uhr["offen"]: lauf.sage(f"Markt offen, {uhr['sekunden_bis_schluss'] // 60} min bis Schluss") else: lauf.sage("Markt zu") handelbar = uhr["offen"] and uhr["sekunden_bis_schluss"] >= MIN_BIS_SCHLUSS_S if not handelbar: lauf.markt = "Markt zu oder weniger als 10 min bis Schluss" if scharf: return STILL # 4 Bestaende: nicht lesbar heisst Befund; hat_bestand(None) ist falsch, die Instanz bleibt unberuehrt lauf.vorher["alpaca"] = alpaca_bestand(alpaca) if lauf.vorher["alpaca"] is None: lauf.befund(f"alpaca: Bestand nicht lesbar ({alpaca.get('bestand_fehler') or 'ohne Angabe'}); nicht angefasst") lauf.vorher["ibkr"], ibkr_exit = lies_ibkr() if lauf.vorher["ibkr"] is None: lauf.befund(f"ibkr: Bestand nicht lesbar (ibkr_pruefen.py lesen, Exit {ibkr_exit}); nicht angefasst") for instanz in INSTANZEN: lauf.sage(f"Bestand {instanz}: {beschreibe(lauf.vorher[instanz])}") # 5 Je Instanz hoechstens ein Eingriff, nur im Modus scharf; wirft er, gilt Exit None zu_glatt = [instanz for instanz in INSTANZEN if instanz not in lauf.gesperrt and hat_bestand(lauf.vorher[instanz])] if lauf.modus == LESEN: entscheidung = f"keine ({lauf.markt})" if lauf.markt else (", ".join(zu_glatt) or "keine") lauf.sage(f"Glattstellen im Modus scharf: {entscheidung}") return LESEN_ENDE for instanz in zu_glatt: try: ergebnis_code, zeilen = glattstelle(instanz, lauf.modus) except Exception as ausnahme: ergebnis_code, zeilen = None, [f"abgebrochen: {type(ausnahme).__name__}"] lauf.eingriffe[instanz] = {"exit": ergebnis_code, "zeilen": zeilen} if ergebnis_code != 0: lauf.befund(f"{instanz}: glattstellen endete mit Exit {ergebnis_code}") # 6 Nach einem Eingriff warten und beide Bestaende neu lesen if lauf.eingriffe: schlafe(WARTEN_S) lauf.nachher["alpaca"] = alpaca_bestand(lies_alpaca()) lauf.nachher["ibkr"], _ = lies_ibkr() for instanz in INSTANZEN: if lauf.nachher[instanz] is None: lauf.befund(f"{instanz}: Bestand nachher nicht lesbar; Ergebnis offen") else: lauf.nachher = dict(lauf.vorher) return BERICHT def ergebnis_daten(lauf: Lauf, flach: bool) -> dict: """Inhalt von abbau.json: je Instanz vorher, nachher und das Glattstellen; dazu flach und die Befunde.""" return { "zeit": lauf.jetzt.isoformat(timespec="seconds"), "flach": flach, "befunde": lauf.befunde, "instanzen": { instanz: { "vorher": lauf.vorher.get(instanz), "nachher": lauf.nachher.get(instanz), "glattstellen": lauf.eingriffe.get(instanz), } for instanz in INSTANZEN }, } def titel_fuer(lauf: Lauf, flach: bool) -> str: """Rangfolge: flach mit Befund, Befund, flach, Rest offen.""" if flach and lauf.befunde: return f"{TITEL}: flach, mit Befund" if lauf.befunde: return f"{TITEL}: Befund" if flach: return f"{TITEL}: flach" return f"{TITEL}: Rest offen" def notizzeile(lauf: Lauf, flach: bool, gemeldet: str) -> str: """Die Zeile unter "## Agentenprotokoll", mit Unterpunkten. Nur Symbol, Richtung, Stueck, Zahlen.""" if flach and lauf.befunde: urteil = "flach, mit Befund #needs-review" elif lauf.befunde: urteil = "**Befund** #needs-review" elif flach: urteil = "flach" else: urteil = "Rest offen" zeilen = [f"- {lauf.jetzt:%H:%M} Uhr - {KOPF}: {urteil}."] zeilen += [f" - {teil}" for teil in lauf.zusammenfassung()] for instanz, eingriff in lauf.eingriffe.items(): if eingriff["exit"] != 0: zeilen += [f" - {instanz}, killswitch.py: {text}" for text in eingriff["zeilen"][-5:]] zeilen += [f" - Befund: {befund}" for befund in lauf.befunde] if lauf.eingriffe: zeilen.append(f" - {GESPERRT_HINWEIS}") zeilen.append(f" - Meldung ans Telefon: {gemeldet}") return "\n".join(zeilen) def berichte(lauf: Lauf, vault: Path) -> int: """Schritte 7 und 8: Ergebnisdatei und Log; Meldung und Notizzeile bei Eingriff, Befund oder flach.""" # flach nur ohne gesperrte Instanz: Ein laufender oder unlesbarer Runner oder eine unlesbare # Marktuhr kann den Bestand gleich wieder aendern; der naechste Termin beobachtet weiter. flach = not lauf.gesperrt and all(ist_flach(lauf.nachher.get(instanz)) for instanz in INSTANZEN) try: schreibe_ergebnis(lauf.ablage, ergebnis_daten(lauf, flach)) except OSError as ausnahme: lauf.befund(f"Ergebnisdatei nicht geschrieben ({type(ausnahme).__name__})") logge(lauf.ablage, f"scharf: {' | '.join(lauf.ausgabe + lauf.zusammenfassung() + lauf.befunde)} | flach={flach}") if lauf.spur: logge(lauf.ablage, lauf.spur) code = 1 if lauf.befunde else 0 # Still nur ohne Eingriff, ohne Befund und ohne flach. Das erste flach ist die Entwarnung; # Laeufe nach der Marke enden schon in fahre(). if not lauf.eingriffe and not lauf.befunde and not flach: return code titel = titel_fuer(lauf, flach) text = "; ".join(lauf.zusammenfassung() + [f"Befund: {befund}" for befund in lauf.befunde]) gemeldet = melde(titel, text[:1500].replace('"', "'"), lauf.modus) try: schreibe_notiz(vault, lauf.jetzt.date().isoformat(), notizzeile(lauf, flach, gemeldet)) except Exception as ausnahme: code = 1 logge(lauf.ablage, f"Notiz nicht geschrieben: {type(ausnahme).__name__}") return code def main(argv=None) -> int: zerleger = argparse.ArgumentParser(description=__doc__.splitlines()[0], allow_abbrev=False) zerleger.add_argument( "--modus", choices=MODI, required=True, help="lesen: nur lesende Aufrufe, schreibt nichts; scharf: alles, nur ueber die Windows-Aufgabe", ) zerleger.add_argument("--jetzt", help="ISO-Zeitpunkt statt der Uhr (Angriffsfaelle)") zerleger.add_argument("--projekt", default=str(PROJEKT), help="Projektordner unter Windows, fuer die Ablage") zerleger.add_argument("--vault", default=str(VAULT)) argumente = zerleger.parse_args(argv) try: jetzt = datetime.fromisoformat(argumente.jetzt) if argumente.jetzt else datetime.now() except ValueError: zerleger.error("--jetzt ist kein ISO-Zeitpunkt") lauf = Lauf(jetzt, argumente.modus, Path(argumente.projekt) / ABLAGE_TEIL) try: ende = fahre(lauf) except Exception as ausnahme: # Nur der Typ: Der Text einer fremden Ausnahme kann Kontonummer oder Betrag tragen. # Der Traceback geht nur nach abbau.log (data/, nicht veroeffentlicht). lauf.befund(f"abgebrochen: {type(ausnahme).__name__}") lauf.spur = traceback.format_exc() ende = BERICHT if lauf.modus == SCHARF else LESEN_ENDE if ende == NICHTS: return 0 if ende == STILL: logge(lauf.ablage, f"scharf, nichts angefasst: {' | '.join(lauf.ausgabe)}") return 0 if ende == LESEN_ENDE: for zeile in lauf.ausgabe: print(zeile) for befund in lauf.befunde: print(f"BEFUND: {befund}") print(f"ERGEBNIS: gelesen, {'mit' if lauf.befunde else 'ohne'} Befund") return 1 if lauf.befunde else 0 return berichte(lauf, Path(argumente.vault)) if __name__ == "__main__": sys.exit(main())