Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/abbau_restbestand.py
T
Pascal MichelandClaude Opus 5 3784322a76 Automatischer Push: 8 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abbau_alpaca_lesen.py
- .vault-tools/trading-plattform-pruefung/abbau_glattstellen.sh
- .vault-tools/trading-plattform-pruefung/abbau_ibkr_lesen.sh
- .vault-tools/trading-plattform-pruefung/abbau_restbestand.py
- .vault-tools/trading-plattform-pruefung/abbau_restbestand_angriffe.py
- .vault-tools/trading-plattform-pruefung/abbau_restbestand_einrichten.ps1
- AI/Regelwerk/windows-shell.md

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 04:00:01 +02:00

576 lines
25 KiB
Python

"""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/<tag>.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())