Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen
Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur, was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen, das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor. Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale Bezeichnung ersetzt; die Regeln selbst sind unverändert. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
commit
c064c729ed
82 files changed
+13428
No files matched your search
@@ -0,0 +1,156 @@
|
||||
"""Löst Regal-, Buch- und Kapitelnamen in IDs auf und legt Fehlendes auf Wunsch an."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from bookstack.api import BookStackApi
|
||||
|
||||
|
||||
class StrukturFehler(Exception):
|
||||
"""Ein benanntes Regal, Buch oder Kapitel existiert nicht (und darf nicht angelegt werden)."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ziel:
|
||||
buch_id: int
|
||||
kapitel_id: int | None = None
|
||||
angelegt: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _passt(eintrag: dict, name: str) -> bool:
|
||||
return str(eintrag.get("name", "")).strip().casefold() == name.strip().casefold()
|
||||
|
||||
|
||||
class Strukturaufloeser:
|
||||
def __init__(
|
||||
self,
|
||||
api: BookStackApi,
|
||||
*,
|
||||
anlegen_erlaubt: bool = False,
|
||||
nur_planen: bool = False,
|
||||
) -> None:
|
||||
self._api = api
|
||||
self._anlegen_erlaubt = anlegen_erlaubt
|
||||
# Im Probelauf darf nichts entstehen. Fehlendes wird nur benannt.
|
||||
self._nur_planen = nur_planen
|
||||
self._speicher: dict[str, list[dict]] = {}
|
||||
|
||||
def _sammlung(self, name: str) -> list[dict]:
|
||||
if name not in self._speicher:
|
||||
self._speicher[name] = self._api.liste(f"/api/{name}")
|
||||
return self._speicher[name]
|
||||
|
||||
def _verwerfe(self, name: str) -> None:
|
||||
self._speicher.pop(name, None)
|
||||
|
||||
def _finde(self, sammlung: str, name: str, **filter_felder) -> dict | None:
|
||||
for eintrag in self._sammlung(sammlung):
|
||||
if _passt(eintrag, name) and all(
|
||||
eintrag.get(feld) == wert for feld, wert in filter_felder.items()
|
||||
):
|
||||
return eintrag
|
||||
return None
|
||||
|
||||
def _fehlt(self, art: str, name: str) -> StrukturFehler:
|
||||
return StrukturFehler(
|
||||
f"{art} „{name}“ existiert in BookStack nicht. "
|
||||
"Namen im Frontmatter prüfen oder mit --struktur-anlegen erneut starten."
|
||||
)
|
||||
|
||||
def _vermerke(self, art: str, name: str) -> str:
|
||||
"""Beschreibt eine Neuanlage — im Probelauf als Absicht, sonst als Tatsache."""
|
||||
zusatz = " (wird angelegt)" if self._nur_planen else ""
|
||||
return f'{art} "{name.strip()}"{zusatz}'
|
||||
|
||||
def _haenge_an_regal(self, regal_name: str, buch_id: int, angelegt: list[str]) -> None:
|
||||
regal = self._finde("shelves", regal_name)
|
||||
if regal is None:
|
||||
if not self._anlegen_erlaubt:
|
||||
raise self._fehlt("Regal", regal_name)
|
||||
angelegt.append(self._vermerke("Regal", regal_name))
|
||||
if self._nur_planen:
|
||||
return
|
||||
self._api.anfrage(
|
||||
"POST", "/api/shelves", {"name": regal_name.strip(), "books": [buch_id]}
|
||||
)
|
||||
self._verwerfe("shelves")
|
||||
return
|
||||
|
||||
vollstaendig = self._api.anfrage("GET", f"/api/shelves/{regal['id']}")
|
||||
# Lesen liefert Buchobjekte, Schreiben erwartet reine IDs. Beide Formen dulden.
|
||||
vorhandene = [
|
||||
int(eintrag["id"] if isinstance(eintrag, dict) else eintrag)
|
||||
for eintrag in vollstaendig.get("books", [])
|
||||
]
|
||||
if buch_id in vorhandene or self._nur_planen:
|
||||
return
|
||||
self._api.anfrage(
|
||||
"PUT", f"/api/shelves/{regal['id']}", {"books": vorhandene + [buch_id]}
|
||||
)
|
||||
|
||||
def aufloesen(self, regal: str | None, buch: str, kapitel: str | None) -> Ziel:
|
||||
if not buch or not buch.strip():
|
||||
raise StrukturFehler("Feld „bookstack_book“ fehlt oder ist leer.")
|
||||
|
||||
angelegt: list[str] = []
|
||||
gefunden = self._finde("books", buch)
|
||||
if gefunden is None:
|
||||
if not self._anlegen_erlaubt:
|
||||
raise self._fehlt("Buch", buch)
|
||||
angelegt.append(self._vermerke("Buch", buch))
|
||||
if self._nur_planen:
|
||||
gefunden = {"id": 0}
|
||||
else:
|
||||
gefunden = self._api.anfrage("POST", "/api/books", {"name": buch.strip()})
|
||||
self._verwerfe("books")
|
||||
buch_id = int(gefunden["id"])
|
||||
|
||||
if regal and regal.strip():
|
||||
self._haenge_an_regal(regal, buch_id, angelegt)
|
||||
|
||||
kapitel_id: int | None = None
|
||||
if kapitel and kapitel.strip():
|
||||
treffer = self._finde("chapters", kapitel, book_id=buch_id)
|
||||
if treffer is None:
|
||||
if not self._anlegen_erlaubt:
|
||||
raise self._fehlt("Kapitel", kapitel)
|
||||
angelegt.append(self._vermerke("Kapitel", kapitel))
|
||||
if self._nur_planen:
|
||||
treffer = {"id": 0}
|
||||
else:
|
||||
treffer = self._api.anfrage(
|
||||
"POST", "/api/chapters", {"book_id": buch_id, "name": kapitel.strip()}
|
||||
)
|
||||
self._verwerfe("chapters")
|
||||
kapitel_id = int(treffer["id"])
|
||||
|
||||
return Ziel(buch_id=buch_id, kapitel_id=kapitel_id, angelegt=angelegt)
|
||||
|
||||
def baum(self) -> str:
|
||||
"""Menschenlesbarer Überblick über die vorhandene Struktur."""
|
||||
buecher = {int(eintrag["id"]): eintrag for eintrag in self._sammlung("books")}
|
||||
kapitel: dict[int, list[str]] = {}
|
||||
for eintrag in self._sammlung("chapters"):
|
||||
kapitel.setdefault(int(eintrag.get("book_id", 0)), []).append(str(eintrag["name"]))
|
||||
|
||||
zeilen: list[str] = []
|
||||
zugeordnet: set[int] = set()
|
||||
for regal in self._sammlung("shelves"):
|
||||
zeilen.append(f"Regal: {regal['name']}")
|
||||
vollstaendig = self._api.anfrage("GET", f"/api/shelves/{regal['id']}")
|
||||
for eintrag in vollstaendig.get("books", []):
|
||||
buch_id = int(eintrag["id"])
|
||||
zugeordnet.add(buch_id)
|
||||
zeilen.append(f" Buch: {buecher.get(buch_id, eintrag)['name']}")
|
||||
zeilen.extend(f" Kapitel: {name}" for name in kapitel.get(buch_id, []))
|
||||
|
||||
frei = [wert for schluessel, wert in buecher.items() if schluessel not in zugeordnet]
|
||||
if frei:
|
||||
zeilen.append("Ohne Regal:")
|
||||
for eintrag in frei:
|
||||
zeilen.append(f" Buch: {eintrag['name']}")
|
||||
zeilen.extend(
|
||||
f" Kapitel: {name}" for name in kapitel.get(int(eintrag["id"]), [])
|
||||
)
|
||||
return "\n".join(zeilen)
|
||||
Reference in new issue
Block a user