"""Inventarisiert das Fortna-Rohmaterial und ordnet jede Datei einem Zielkapitel zu. Scannt das Fortna-Quellverzeichnis (Standard: PV_FORTNA_SOURCE oder --source-dir) rekursiv, erkennt fuer jede PDF/DOCX-Datei die Sprache (aus dem Sprachordner) und das Ziel-Kapitel unter chapters/ (anhand einer praefix-basierten Mapping-Tabelle, abgeleitet aus doc/kapitelstruktur.md). Ergebnis wird als results/fortna_inventory.json geschrieben und tabellarisch auf stdout ausgegeben. Reines Lese-/Analyseskript - schreibt nichts nach chapters/. Verwendung: python fortna_inventory.py python fortna_inventory.py --source-dir "c:/10-Develop/projekte/Fortna" python fortna_inventory.py --lang de # nur deutsche Dateien im Report """ from __future__ import annotations import argparse import json import logging import os import re import sys from collections import Counter from dataclasses import dataclass, asdict from pathlib import Path log = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Sprach-Erkennung aus Ordnernamen # --------------------------------------------------------------------------- # Reihenfolge wichtig: spezifische Muster zuerst. Ein Segment des relativen # Pfades wird gegen diese Muster geprueft. LANG_PATTERNS: list[tuple[re.Pattern[str], str]] = [ (re.compile(r"_DE(_|$)|_DE_original|DE_f[uü]r|01_DE\b", re.I), "de"), (re.compile(r"_EN(_|$)|_EN_original|EN_for|1_EN_original", re.I), "en"), (re.compile(r"_CZ(_|$)|02_CZ|03_CZ|CZ_pro", re.I), "cz"), ] # Sprach-Suffixe im Dateinamen (Fallback, wenn Ordner nichts hergibt) FILENAME_LANG = [ (re.compile(r"[_\-\[ ]DE[_\-\.\] ]|_DE\b", re.I), "de"), (re.compile(r"[_\-\[ ]EN[_\-\.\] ]|_EN\b", re.I), "en"), (re.compile(r"[_\-\[ ]CZ[_\-\.\] ]|_CZ\b", re.I), "cz"), ] def detect_lang(rel_path: Path) -> str: """Bestimmt die Sprache einer Datei anhand Pfadsegmenten und Dateiname. Fallback auf 'de', wenn nichts eindeutig erkennbar (z.B. sprachneutrale Datenblaetter oder gemischte DE-EN-Dokumente). """ # Zuerst Ordnersegmente pruefen (aussagekraeftiger als Dateiname) for part in rel_path.parts[:-1]: for pattern, lang in LANG_PATTERNS: if pattern.search(part): return lang # Dann Dateiname name = rel_path.name for pattern, lang in FILENAME_LANG: if pattern.search(name): return lang return "de" # --------------------------------------------------------------------------- # Kapitel-Mapping: Fortna-Quellpfad-Praefix -> (Ziel-Kapitel, Aktion) # --------------------------------------------------------------------------- # Aktion: "convert" = Volltext+Bilder nach RST, "list" = nur Verzeichnisliste. # Regeln werden der Reihe nach geprueft; erste passende gewinnt (praefix-Match # gegen den mit '/' normalisierten relativen Pfad, case-insensitive). @dataclass class MapRule: pattern: re.Pattern[str] chapter: str action: str # "convert" | "list" MAPPING: list[MapRule] = [ # --- Volltext-Konvertierung: die 5 Hauptanleitungen (nur DE_original) --- MapRule(re.compile(r"02_INSTRUCTIONS.*/01_BETRIEBSANLEITUNG", re.I), "04_betrieb/betriebsanleitung", "convert"), MapRule(re.compile(r"02_INSTRUCTIONS.*/02_WARTUNGSANLEITUNG", re.I), "05_wartung/wartungsanleitung", "convert"), MapRule(re.compile(r"02_INSTRUCTIONS.*/03_FUNKTION_STEUERUNG_ILS", re.I), "06_steuerung/ils", "convert"), MapRule(re.compile(r"02_INSTRUCTIONS.*/04_FUNKTION_STEUERUNG_OMNIFLO", re.I), "06_steuerung/omniflo", "convert"), MapRule(re.compile(r"02_INSTRUCTIONS.*/05_FUNKTION_STEUERUNG_LOADING_LOOP", re.I), "06_steuerung/loading_loop", "convert"), # Fremdsprach-Varianten der Anleitungen (EN: USER_MANUAL / MAINTENANCE / # FUNKTION_CONTROL, CZ: NAVOD / RIZENI). In diesem Durchlauf (nur DE) werden # sie NICHT konvertiert (action="list"), aber sie sollen nicht als UNMAPPED # erscheinen. Zuordnung nach Anleitungstyp anhand der fuehrenden Nummer. MapRule(re.compile(r"02_INSTRUCTIONS.*/01_", re.I), "04_betrieb/betriebsanleitung", "list"), MapRule(re.compile(r"02_INSTRUCTIONS.*/02_", re.I), "05_wartung/wartungsanleitung", "list"), MapRule(re.compile(r"02_INSTRUCTIONS.*/03_", re.I), "06_steuerung/ils", "list"), MapRule(re.compile(r"02_INSTRUCTIONS.*/04_", re.I), "06_steuerung/omniflo", "list"), MapRule(re.compile(r"02_INSTRUCTIONS.*/05_", re.I), "06_steuerung/loading_loop", "list"), # --- Verzeichnislisten: alles uebrige nach Kapitel --- # 00 Inhaltsverzeichnis / Notes MapRule(re.compile(r"^00_CONTENTS", re.I), "00_inhaltsverzeichnis", "list"), # 01 Zertifikate MapRule(re.compile(r"^01_CERTIFICATES.*/01_SCH[OÖ]NENBERGER", re.I), "01_zertifikate/konformitaetserklaerung", "list"), MapRule(re.compile(r"^01_CERTIFICATES.*/(02_)?CONFIG", re.I), "01_zertifikate/konfigurationsbeschreibung", "list"), MapRule(re.compile(r"^01_CERTIFICATES", re.I), "01_zertifikate/konformitaetserklaerung", "list"), # 03 Ersatzteile (Fortna 03_SPARE_PARTS) MapRule(re.compile(r"^03_SPARE_PARTS", re.I), "07_ersatzteile", "list"), # 04 Technical Doc -> 09 / 10 MapRule(re.compile(r"^04_TECHNICAL_DOC.*/01_LAYOUT.*/01_MECHANICAL", re.I), "09_technische_unterlagen/layouts/mechanisch", "list"), MapRule(re.compile(r"^04_TECHNICAL_DOC.*/01_LAYOUT.*/02_ELECTRICAL", re.I), "09_technische_unterlagen/layouts/elektrisch", "list"), MapRule(re.compile(r"^04_TECHNICAL_DOC.*/02_AREA_STOP", re.I), "09_technische_unterlagen/bereichsstopp", "list"), MapRule(re.compile(r"^04_TECHNICAL_DOC.*/03_EMERGENCY_STOP", re.I), "09_technische_unterlagen/notstopp", "list"), MapRule(re.compile(r"^04_TECHNICAL_DOC.*/04_DIAGRAMS", re.I), "09_technische_unterlagen/schaltplaene", "list"), MapRule(re.compile(r"^04_TECHNICAL_DOC.*/05_REPORTS", re.I), "10_anhang/pruefberichte", "list"), # 05 Supplier Doc -> 08 Lieferantendoku (nach Gewerk) MapRule(re.compile(r"^05_SUPPLIER_DOC.*/01_MiTek", re.I), "08_lieferantendoku/stahlbau", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC.*/02_ABS", re.I), "08_lieferantendoku/sicherheitseinrichtungen", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC.*/(03_DTS|04_Leantechnik)", re.I), "08_lieferantendoku/antriebe", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC.*/05_Electrics.*/01_Instructions.*/01_MOTORS", re.I), "08_lieferantendoku/antriebe", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC.*/05_Electrics", re.I), "08_lieferantendoku/elektrik", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC.*/(06_|07_)?Pneumatic", re.I), "08_lieferantendoku/pneumatik", "list"), MapRule(re.compile(r"^05_SUPPLIER_DOC", re.I), "08_lieferantendoku", "list"), ] def classify(rel_path: Path) -> tuple[str | None, str | None]: """Ordnet einen relativen Pfad Kapitel + Aktion zu. Returns (chapter, action) oder (None, None), wenn keine Regel greift. """ norm = rel_path.as_posix() for rule in MAPPING: if rule.pattern.search(norm): return rule.chapter, rule.action return None, None # --------------------------------------------------------------------------- # Inventar-Aufbau # --------------------------------------------------------------------------- @dataclass class InvItem: rel_path: str filename: str ext: str size_bytes: int lang: str fortna_top: str # oberster Fortna-Ordner chapter: str | None # Ziel-Kapitel unter chapters/ action: str | None # "convert" | "list" | None def resolve_source(arg_dir: str | None) -> Path: """Bestimmt das Fortna-Quellverzeichnis.""" if arg_dir: src = Path(arg_dir).expanduser().resolve() elif os.environ.get("PV_FORTNA_SOURCE"): src = Path(os.environ["PV_FORTNA_SOURCE"]).expanduser().resolve() else: # Sinnvoller Default fuer diese Umgebung src = Path("c:/10-Develop/projekte/Fortna").resolve() return src def build_inventory(source: Path) -> list[InvItem]: """Scannt das Quellverzeichnis und baut die Inventarliste.""" items: list[InvItem] = [] exts = {".pdf", ".docx"} for path in sorted(source.rglob("*")): if not path.is_file(): continue if path.suffix.lower() not in exts: continue rel = path.relative_to(source) chapter, action = classify(rel) items.append(InvItem( rel_path=rel.as_posix(), filename=path.name, ext=path.suffix.lower(), size_bytes=path.stat().st_size, lang=detect_lang(rel), fortna_top=rel.parts[0] if rel.parts else "", chapter=chapter, action=action, )) return items def human_size(n: int) -> str: """Formatiert Bytes menschenlesbar (KB/MB).""" if n >= 1024 * 1024: return f"{n / (1024 * 1024):.1f}M" if n >= 1024: return f"{n / 1024:.0f}K" return f"{n}B" def print_summary(items: list[InvItem], lang_filter: str | None) -> None: """Gibt eine tabellarische Zusammenfassung auf stdout aus.""" shown = [i for i in items if lang_filter is None or i.lang == lang_filter] # Uebersicht nach Kapitel + Aktion by_chapter: Counter = Counter() for it in shown: key = f"{it.chapter or '(UNMAPPED)'} [{it.action or '-'}]" by_chapter[key] += 1 print("\n=== Dateien je Ziel-Kapitel ===") for key in sorted(by_chapter): print(f" {by_chapter[key]:4d} {key}") # Sprachverteilung by_lang: Counter = Counter(i.lang for i in items) print("\n=== Sprachverteilung (alle) ===") for lang in sorted(by_lang): print(f" {by_lang[lang]:4d} {lang}") # Nicht zugeordnete Dateien explizit auflisten unmapped = [i for i in shown if i.chapter is None] if unmapped: print(f"\n=== NICHT ZUGEORDNET ({len(unmapped)}) ===") for it in unmapped: print(f" {it.rel_path}") # Konvertierungs-Kandidaten (Volltext) explizit auflisten conv = [i for i in items if i.action == "convert"] print(f"\n=== Volltext-Konvertierung ({len(conv)} Kandidaten, alle Sprachen) ===") for it in sorted(conv, key=lambda x: (x.chapter or "", x.lang)): print(f" [{it.lang}] {it.chapter:32s} <- {it.rel_path} ({human_size(it.size_bytes)})") print(f"\nGesamt: {len(items)} Dateien, davon {len(shown)} in Filter '{lang_filter or 'alle'}'.") def main() -> None: parser = argparse.ArgumentParser( description="Inventarisiert Fortna-Rohmaterial und ordnet Zielkapitel zu." ) parser.add_argument("--source-dir", default=None, help="Fortna-Quellverzeichnis (default: PV_FORTNA_SOURCE " "oder c:/10-Develop/projekte/Fortna)") parser.add_argument("--lang", default=None, choices=["de", "en", "cz"], help="Report auf eine Sprache filtern (JSON enthaelt immer alles)") parser.add_argument("--output", default=None, help="Ausgabe-JSON (default: results/fortna_inventory.json)") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() logging.basicConfig( level=logging.DEBUG if args.verbose else logging.INFO, format="%(levelname)-7s %(message)s", ) source = resolve_source(args.source_dir) if not source.exists() or not source.is_dir(): log.error("Quellverzeichnis nicht gefunden: %s", source) sys.exit(1) log.info("Scanne: %s", source) items = build_inventory(source) log.info("Gefunden: %d Dateien (.pdf/.docx)", len(items)) # JSON schreiben project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent)) results_dir = project_root / "results" results_dir.mkdir(exist_ok=True) out_path = Path(args.output) if args.output else results_dir / "fortna_inventory.json" if not out_path.is_absolute(): out_path = results_dir / out_path payload = { "source": str(source), "count": len(items), "items": [asdict(i) for i in items], } with open(out_path, "w", encoding="utf-8") as f: json.dump(payload, f, ensure_ascii=False, indent=2) log.info("Inventar geschrieben: %s", out_path) print_summary(items, args.lang) if __name__ == "__main__": main()