"""Baut die schlanke Kopie eines Projektbaums fuer den Byte-Vergleich -- Ausschluesse nur als Pfade vom Wurzelordner. Ersteller: Pascal Michel Erstellt: 2026-09-17 Doku: .vault-tools/plan-kopie/LIESMICH.md Aufruf: python kopie_bauen.py --ohne [ ...] Warum Pfade und keine Namen: Ein Ausschluss von "jedem Ordner namens data" hat am 2026-09-17 auch das Paket trading/data getroffen. Nach dem Kopieren vergleicht das Skript deshalb die Python-Pakete (Ordner mit __init__.py) beider Baeume und meldet jedes, das in der Kopie fehlt und nicht unter einem ausgeschlossenen Pfad liegt. Das Ziel darf nicht existieren; Verknuepfungen und Junctions werden nicht verfolgt, sondern uebersprungen. """ import argparse import os import shutil import sys from pathlib import Path def _relativ(pfad: Path, wurzel: Path) -> str: return pfad.relative_to(wurzel).as_posix() def _pakete(wurzel: Path, ohne: set[str]) -> set[str]: gefunden = set() for ordner, unterordner, dateien in os.walk(wurzel): rel = _relativ(Path(ordner), wurzel) unterordner[:] = [ name for name in unterordner if (f"{rel}/{name}" if rel != "." else name) not in ohne and not Path(ordner, name).is_symlink() and not Path(ordner, name).is_junction() ] if "__init__.py" in dateien: gefunden.add(rel) return gefunden def main() -> int: parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("projekt", type=Path) parser.add_argument("ziel", type=Path) parser.add_argument("--ohne", nargs="*", default=[], help="relative Pfade vom Wurzelordner, mit /") args = parser.parse_args() projekt = args.projekt.resolve() ziel = args.ziel.resolve() if not projekt.is_dir(): print(f"BEFUND Projekt {projekt} ist kein Ordner") return 2 if ziel.exists(): print(f"BEFUND Ziel {ziel} existiert schon -- erst ansehen, dann selbst loeschen") return 2 ohne = {eintrag.replace("\\", "/").strip("/") for eintrag in args.ohne} befunde = [] for eintrag in sorted(ohne): if "/" not in eintrag and not (projekt / eintrag).exists(): befunde.append(f"Ausschluss '{eintrag}' gibt es im Wurzelordner nicht -- ein Name statt eines Pfads?") elif not (projekt / eintrag).exists(): befunde.append(f"Ausschluss '{eintrag}' gibt es nicht") uebersprungen = [] def ignoriere(ordner: str, namen: list[str]) -> list[str]: rel = _relativ(Path(ordner), projekt) raus = [] for name in namen: pfad = Path(ordner, name) relpfad = name if rel == "." else f"{rel}/{name}" if relpfad in ohne or name == "__pycache__": raus.append(name) elif pfad.is_symlink() or pfad.is_junction(): uebersprungen.append(relpfad) raus.append(name) return raus shutil.copytree(projekt, ziel, ignore=ignoriere) fehlend = sorted(_pakete(projekt, ohne) - _pakete(ziel, set())) for paket in fehlend: befunde.append(f"Paket '{paket}' fehlt in der Kopie") dateien = sum(len(d) for _, _, d in os.walk(ziel)) print(f"{dateien} Dateien kopiert, {len(ohne)} Pfade ausgeschlossen, " f"{len(uebersprungen)} Verknuepfungen uebersprungen, {len(_pakete(ziel, set()))} Pakete in der Kopie.") for zeile in uebersprungen: print(f"HINWEIS Verknuepfung uebersprungen: {zeile}") for zeile in befunde: print(f"BEFUND {zeile}") print(f"{len(befunde)} Befund(e).") return 1 if befunde else 0 if __name__ == "__main__": sys.exit(main())