.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
207 lines
8.0 KiB
Python
207 lines
8.0 KiB
Python
"""Zieht nach Durchgang 1 die Verweise auf Seiten desselben Laufs nach und meldet, was offen bleibt.
|
|
|
|
Ersteller: Pascal Michel
|
|
Erstellt: 2026-09-30
|
|
Doku: docs/code/verweisrunde.md · Version und Änderungen: CHANGELOG.md
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from bookstack.abgleich import berechne_pruefsumme
|
|
from bookstack.api import ApiFehler, BookStackApi
|
|
from bookstack.beschreibung import (
|
|
BeschreibungsFehler,
|
|
beschreibungs_pruefsumme,
|
|
pruefe_beschreibung_html,
|
|
setze_beschreibung,
|
|
zu_beschreibungs_html,
|
|
)
|
|
from bookstack.bilder import Bilduploader
|
|
from bookstack.ergebnis import (
|
|
ARCHIVIERBAR,
|
|
Beschreibungsstand,
|
|
Ergebnis,
|
|
Seitenstand,
|
|
seiten_nutzlast,
|
|
)
|
|
from bookstack.konvertierung import konvertiere
|
|
from bookstack.notiz import schreibe_felder
|
|
from bookstack.verweise import Verweisindex, permalink, schluessel_fuer, trage_ein
|
|
|
|
PLATZHALTER_PRAEFIX = "geplant:"
|
|
|
|
|
|
def _jetzt() -> str:
|
|
return datetime.now().astimezone().isoformat(timespec="seconds")
|
|
|
|
|
|
def _neu_oder_geaendert(vorher: dict[str, str], nachher: dict[str, str]) -> int:
|
|
"""Zahl der Verweisziele, deren URL neu dazukam oder sich geändert hat."""
|
|
return sum(1 for ziel, url in nachher.items() if vorher.get(ziel) != url)
|
|
|
|
|
|
def baue_endindex(
|
|
verweise: Verweisindex, ergebnisse: list[Ergebnis], *, quelle: Path, vault: Path, basis_url: str
|
|
) -> Verweisindex:
|
|
"""verweise.ergaenzt(...) um jede Zeile mit Aktion aus ARCHIVIERBAR und seite.seiten_id
|
|
nicht None: alle schluessel_fuer(pfad) → permalink(basis_url, seiten_id)."""
|
|
weitere: dict[str, str] = {}
|
|
aliase: set[str] = set()
|
|
for ergebnis in ergebnisse:
|
|
seite = ergebnis.seite
|
|
if ergebnis.aktion not in ARCHIVIERBAR or seite is None or seite.seiten_id is None:
|
|
continue
|
|
# Über trage_ein, nicht per Zuweisung: Sonst entschiede die Reihenfolge der Zeilen, ob der
|
|
# Alias einer archivierten Notiz die neue Notiz am alten Pfad verdrängt.
|
|
trage_ein(
|
|
weitere,
|
|
aliase,
|
|
schluessel_fuer(ergebnis.pfad, quelle, vault),
|
|
permalink(basis_url, seite.seiten_id),
|
|
)
|
|
return verweise.ergaenzt(weitere, aliase)
|
|
|
|
|
|
def plane_endindex(
|
|
verweise: Verweisindex, ergebnisse: list[Ergebnis], *, quelle: Path, vault: Path
|
|
) -> Verweisindex:
|
|
"""Probelauf: jede Zeile mit Aktion 'ANLEGEN' und seite → PLATZHALTER_PRAEFIX + pfad.as_posix()."""
|
|
weitere: dict[str, str] = {}
|
|
aliase: set[str] = set()
|
|
for ergebnis in ergebnisse:
|
|
if ergebnis.aktion != "ANLEGEN" or ergebnis.seite is None:
|
|
continue
|
|
trage_ein(
|
|
weitere,
|
|
aliase,
|
|
schluessel_fuer(ergebnis.pfad, quelle, vault),
|
|
PLATZHALTER_PRAEFIX + ergebnis.pfad.as_posix(),
|
|
)
|
|
return verweise.ergaenzt(weitere, aliase)
|
|
|
|
|
|
def _bildtabelle(
|
|
seite: Seitenstand, seiten_id: int, bilduploader: Bilduploader
|
|
) -> tuple[dict[str, str], list[str]]:
|
|
"""(Tabelle für den PUT, fehlende Bildnamen). Lädt nie hoch, liest höchstens die Galerie."""
|
|
if seite.bilder is not None:
|
|
# Tabelle aus Durchgang 1; deckt sie nicht alle Bildnamen ab, steht das unter fehlend.
|
|
tabelle = seite.bilder
|
|
elif not seite.bildnamen:
|
|
return {}, []
|
|
else:
|
|
# Durchgang 1 hat nichts gesendet (ÜBERSPRUNGEN): Die Tabelle kommt lesend aus der Galerie.
|
|
vorhanden = bilduploader.vorhandene(seiten_id)
|
|
tabelle = {name: vorhanden[name] for name in seite.bildnamen if vorhanden.get(name)}
|
|
fehlend = [name for name in dict.fromkeys(seite.bildnamen) if name not in tabelle]
|
|
return tabelle, fehlend
|
|
|
|
|
|
def _ziehe_seite_nach(
|
|
pfad: Path,
|
|
seite: Seitenstand,
|
|
seiten_id: int,
|
|
endindex: Verweisindex,
|
|
api: BookStackApi,
|
|
bilduploader: Bilduploader,
|
|
) -> Ergebnis | None:
|
|
neu = konvertiere(seite.rumpf, verweise=endindex)
|
|
if neu.markdown == seite.markdown:
|
|
return None
|
|
tabelle, fehlend = _bildtabelle(seite, seiten_id, bilduploader)
|
|
# Nie ein PUT mit Platzhaltertext: Fehlt ein Bildname in der Tabelle, gibt es nur die Zeile.
|
|
if fehlend:
|
|
namen = ", ".join(fehlend)
|
|
return Ergebnis(
|
|
pfad,
|
|
"FEHLER",
|
|
f"Verweise nicht nachgezogen: Bild fehlt in der Galerie der Seite ({namen}). "
|
|
"Der nächste Lauf aktualisiert die Seite.",
|
|
)
|
|
markdown = neu.markdown
|
|
if seite.bildnamen:
|
|
markdown = konvertiere(seite.rumpf, verweise=endindex, bilder=tabelle).markdown
|
|
api.anfrage(
|
|
"PUT",
|
|
f"/api/pages/{seiten_id}",
|
|
seiten_nutzlast(seite.titel, seite.ziel, markdown, seite.prioritaet),
|
|
)
|
|
# Gespeichert wird die Summe des bildfreien Markdowns mit den Verweisen dieses PUT.
|
|
pruefsumme = berechne_pruefsumme(
|
|
seite.titel, neu.markdown, seite.ziel.buch_id, seite.ziel.kapitel_id
|
|
)
|
|
schreibe_felder(pfad, {"bookstack_hash": pruefsumme, "bookstack_synced": _jetzt()})
|
|
anzahl = _neu_oder_geaendert(seite.aufgeloeste_verweise, neu.aufgeloeste_verweise)
|
|
return Ergebnis(pfad, "VERLINKT", f"{anzahl} Verweise nachgezogen")
|
|
|
|
|
|
def _ziehe_beschreibung_nach(
|
|
pfad: Path, stand: Beschreibungsstand, endindex: Verweisindex, api: BookStackApi
|
|
) -> Ergebnis | None:
|
|
neu = zu_beschreibungs_html(stand.vorspann, endindex)
|
|
if neu.html == stand.html:
|
|
return None
|
|
pruefe_beschreibung_html(neu.html)
|
|
setze_beschreibung(api, stand.art, stand.ziel_id, neu.html)
|
|
schreibe_felder(
|
|
pfad, {"bookstack_beschreibung_hash": beschreibungs_pruefsumme(stand.ziel_id, neu.html)}
|
|
)
|
|
anzahl = _neu_oder_geaendert(stand.aufgeloeste_verweise, neu.aufgeloeste_verweise)
|
|
return Ergebnis(pfad, "VERLINKT", f"{anzahl} Verweise in der Beschreibung nachgezogen")
|
|
|
|
|
|
def fuehre_verweisrunde(
|
|
ergebnisse: list[Ergebnis],
|
|
endindex: Verweisindex,
|
|
api: BookStackApi,
|
|
bilduploader: Bilduploader,
|
|
) -> list[Ergebnis]:
|
|
"""Zweiter Durchgang, nur mit --anwenden. Liefert neue VERLINKT- und FEHLER-Zeilen."""
|
|
neue: list[Ergebnis] = []
|
|
for ergebnis in ergebnisse:
|
|
seite = ergebnis.seite
|
|
beschreibung = ergebnis.beschreibung
|
|
try:
|
|
if (
|
|
seite is not None
|
|
and seite.seiten_id is not None
|
|
and ergebnis.aktion in ARCHIVIERBAR
|
|
):
|
|
zeile = _ziehe_seite_nach(
|
|
ergebnis.pfad, seite, seite.seiten_id, endindex, api, bilduploader
|
|
)
|
|
elif beschreibung is not None and ergebnis.aktion == "BESCHREIBUNG":
|
|
zeile = _ziehe_beschreibung_nach(ergebnis.pfad, beschreibung, endindex, api)
|
|
else:
|
|
continue
|
|
except (ApiFehler, OSError, BeschreibungsFehler) as fehler:
|
|
zeile = Ergebnis(ergebnis.pfad, "FEHLER", str(fehler))
|
|
if zeile is not None:
|
|
neue.append(zeile)
|
|
return neue
|
|
|
|
|
|
def haenge_offene_an(ergebnisse: list[Ergebnis], endindex: Verweisindex) -> int:
|
|
"""Hängt an jede Zeile mit seite „Verweis nicht auflösbar“ für zwei Gruppen von Zielen an,
|
|
soweit endindex sie nicht auflöst: die offenen Verweise aus Durchgang 1 (seite.offene_verweise)
|
|
und die Ziele aus seite.aufgeloeste_verweise. Rückgabe: Zahl der offenen Verweise, die erst
|
|
der Endindex auflöst; die Ziele aus aufgeloeste_verweise werden gemeldet, nicht gezählt."""
|
|
aufgeloest = 0
|
|
for ergebnis in ergebnisse:
|
|
if ergebnis.seite is None:
|
|
continue
|
|
for ziel in ergebnis.seite.offene_verweise:
|
|
if endindex.aufloesen(ziel) is None:
|
|
ergebnis.text += f"\n Verweis nicht auflösbar: {ziel}"
|
|
else:
|
|
aufgeloest += 1
|
|
# Ein Stamm, der in Durchgang 1 eindeutig war, kann im Endindex mehrdeutig werden;
|
|
# der PUT der Runde entfernt den Link dann, und das darf nicht still passieren.
|
|
for ziel in ergebnis.seite.aufgeloeste_verweise:
|
|
if endindex.aufloesen(ziel) is None:
|
|
ergebnis.text += f"\n Verweis nicht auflösbar: {ziel}"
|
|
return aufgeloest
|