Files
doc_build/lib/create_examples.py
T

198 lines
6.2 KiB
Python

"""
create_examples.py - Liest Stuecklisten-Daten aus Excel-Dateien im Examples-Ordner.
Verwendung:
python create_examples.py [--example-dir <pfad>]
"""
from __future__ import annotations
import argparse
import logging
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
log = logging.getLogger(__name__)
@dataclass
class StkItem:
position: str
teilenummer: str
menge: Any
def _cell_str(value: Any) -> str:
return str(value).strip() if value is not None else ""
def _find_col_index(header_row: tuple[Any, ...], candidates: list[str]) -> int | None:
normalized = [_cell_str(v).lower() for v in header_row]
for candidate in candidates:
candidate_norm = candidate.lower()
if candidate_norm in normalized:
return normalized.index(candidate_norm)
return None
def _extract_stk_liste_ist(xlsx_path: Path) -> list[StkItem]:
import openpyxl
wb = openpyxl.load_workbook(str(xlsx_path), read_only=True, data_only=True)
if "Stk.-Liste IST" not in wb.sheetnames:
wb.close()
raise ValueError("Reiter 'Stk.-Liste IST' nicht gefunden")
ws = wb["Stk.-Liste IST"]
header_idx: tuple[int | None, int | None, int | None] | None = None
items: list[StkItem] = []
for row in ws.iter_rows(values_only=True):
if header_idx is None:
pos_idx = _find_col_index(list(row), ["position", "pos", "pos."])
part_idx = _find_col_index(
list(row),
["teilenummer", "teilnummer", "article", "artikelnummer"],
)
qty_idx = _find_col_index(list(row), ["menge", "anzahl", "qty", "quantity"])
if pos_idx is not None and part_idx is not None and qty_idx is not None:
header_idx = (pos_idx, part_idx, qty_idx)
continue
pos_idx, part_idx, qty_idx = header_idx
assert pos_idx is not None and part_idx is not None and qty_idx is not None
position = _cell_str(row[pos_idx]) if pos_idx < len(row) else ""
teilenummer = _cell_str(row[part_idx]) if part_idx < len(row) else ""
menge = row[qty_idx] if qty_idx < len(row) else None
# Leere Zeilen ueberspringen
if not position and not teilenummer and menge in ("", None):
continue
if not position or not teilenummer:
continue
items.append(StkItem(position=position, teilenummer=teilenummer, menge=menge))
wb.close()
return items
def _process_single_folder(folder: Path, example_dir: Path, result: dict, log) -> None:
xlsx_files = sorted(folder.glob("*.xlsx"))
if not xlsx_files:
return
xlsx_path = xlsx_files[0]
rel_folder = str(folder.relative_to(example_dir))
try:
items = _extract_stk_liste_ist(xlsx_path)
except ValueError as exc:
log.warning("%s/%s uebersprungen: %s", rel_folder, xlsx_path.name, exc)
return
result[rel_folder] = [
{
"position": item.position,
"teilenummer": item.teilenummer,
"menge": item.menge,
}
for item in items
]
def process_all(example_dir: Path) -> dict[str, list[dict[str, Any]]]:
"""
Durchsucht rekursiv alle Unterverzeichnisse in example_dir.
Pro Unterverzeichnis wird die erste gefundene .xlsx-Datei gelesen und aus
dem Reiter "Stk.-Liste IST" werden Position, Teilenummer, Menge extrahiert.
"""
if not example_dir.exists():
log.error("Beispiel-Verzeichnis existiert nicht: %s", example_dir)
sys.exit(1)
subdirs = sorted([p for p in example_dir.rglob("*") if p.is_dir()])
if not subdirs:
log.error("Keine Unterverzeichnisse in %s gefunden", example_dir)
sys.exit(1)
log.info("Gefunden: %d Unterverzeichnisse in %s", len(subdirs), example_dir)
result: dict[str, list[dict[str, Any]]] = {}
for folder in subdirs:
log.info("Verarbeite %s", folder)
_process_single_folder(folder, example_dir, result, log)
log.info("Fertig. Gelesene Verzeichnisse mit Daten: %d", len(result))
return result
def main() -> None:
parser = argparse.ArgumentParser(
description="Liest Position/Teilenummer/Menge aus Example-Exceldateien"
)
parser.add_argument(
"--example-dir",
type=Path,
default=None,
help=(
"Unterordnername in PV_EXAMPLES oder direkter Verzeichnispfad "
"(default: alle Unterordner in PV_EXAMPLES)"
),
)
parser.add_argument("--verbose", "-v", action="store_true",
help="Ausfuehrliche Ausgabe")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)-7s %(message)s",
)
import os
pv_examples = os.environ.get("PV_EXAMPLES")
if not pv_examples:
log.error("PV_EXAMPLES ist nicht gesetzt")
sys.exit(1)
examples_root = Path(pv_examples).resolve()
if not examples_root.exists() or not examples_root.is_dir():
log.error("PV_EXAMPLES ist kein gueltiges Verzeichnis: %s", examples_root)
sys.exit(1)
if args.example_dir is None:
log.info("Beispiel-Verzeichnis (alle Unterordner): %s", examples_root)
data = process_all(examples_root)
log.info("Objekt erzeugt mit %d Schluesseln", len(data))
return
# --example-dir: entweder direkter Verzeichnispfad oder Unterordnername in PV_EXAMPLES
candidate = args.example_dir
if candidate.is_absolute():
target_dir = candidate.resolve()
else:
as_path = candidate.resolve()
if as_path.exists() and as_path.is_dir():
target_dir = as_path
else:
target_dir = (examples_root / candidate).resolve()
if not target_dir.exists() or not target_dir.is_dir():
log.error(
"--example-dir muss ein gueltiger Verzeichnispfad oder Unterordner in PV_EXAMPLES sein: %s",
args.example_dir,
)
sys.exit(1)
log.info("Beispiel-Verzeichnis (einzeln): %s", target_dir)
result: dict[str, list[dict[str, Any]]] = {}
_process_single_folder(target_dir, target_dir.parent, result, log)
log.info("Objekt erzeugt mit %d Schluesseln", len(result))
if __name__ == "__main__":
main()