- semantic versioning - auto update on build - git hash zusätzlich - Anzeige per cli und im Fenster
242 lines
7.0 KiB
Python
242 lines
7.0 KiB
Python
"""CLI entry point for the Attendance PDF Generator.
|
||
|
||
Usage
|
||
-----
|
||
python generate.py generate --file Anwesenheit_2026.xlsx --sheet Feb
|
||
python generate.py validate --file Anwesenheit_2026.xlsx --sheet Feb
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import click
|
||
from PIL import Image
|
||
|
||
# Add project root to sys.path so 'src' is importable
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
||
from src.config_loader import load_config
|
||
from src.excel_reader import extract_attendance, extract_persons, extract_year_from_filename
|
||
from src.pdf_filler import fill_pdf
|
||
from src.pdf_render import render_pdf_pages
|
||
from src.validators import show_preview_and_confirm, validate
|
||
|
||
|
||
@click.group()
|
||
@click.pass_context
|
||
def cli(ctx: click.Context) -> None:
|
||
"""Anwesenheit PDF Generator — Excel → PDF"""
|
||
try:
|
||
config = load_config()
|
||
version = config.get("version", "unknown")
|
||
click.echo(f"Anwesenheit PDF Generator v{version}")
|
||
except Exception as exc:
|
||
click.echo(f"Fehler beim Laden der Konfiguration: {exc}", err=True)
|
||
|
||
|
||
@cli.command("validate")
|
||
@click.option(
|
||
"--file",
|
||
"excel_file",
|
||
required=True,
|
||
type=click.Path(exists=False, file_okay=True, dir_okay=False),
|
||
help="Pfad zur Excel-Datei (z. B. Anwesenheit_2026.xlsx)",
|
||
)
|
||
@click.option(
|
||
"--month",
|
||
required=True,
|
||
help="Name des Monats-Sheets (z. B. Feb, Mrz, Jan)",
|
||
)
|
||
def validate_cmd(excel_file: str, month: str) -> None:
|
||
"""Nur Validierung und Vorschau – keine PDFs werden erzeugt."""
|
||
config = load_config()
|
||
errors = validate(excel_file, month)
|
||
|
||
try:
|
||
year = extract_year_from_filename(excel_file, config)
|
||
except Exception:
|
||
year = 0
|
||
|
||
try:
|
||
persons = extract_persons(excel_file, month, config)
|
||
person_count = len(persons)
|
||
except Exception:
|
||
person_count = 0
|
||
|
||
show_preview_and_confirm(excel_file, month, year, person_count, config, errors)
|
||
|
||
if errors:
|
||
click.echo("\nValidierung fehlgeschlagen. Keine PDFs erzeugt.")
|
||
sys.exit(1)
|
||
else:
|
||
click.echo("\nValidierung erfolgreich.")
|
||
|
||
|
||
@cli.command("generate")
|
||
@click.option(
|
||
"--file",
|
||
"excel_file",
|
||
required=True,
|
||
type=click.Path(exists=False, file_okay=True, dir_okay=False),
|
||
help="Pfad zur Excel-Datei (z. B. Anwesenheit_2026.xlsx)",
|
||
)
|
||
@click.option(
|
||
"--month",
|
||
required=True,
|
||
help="Name des Monats-Sheets (z. B. Feb, Mrz, Jan)",
|
||
)
|
||
@click.option(
|
||
"--out",
|
||
"output_dir",
|
||
required=False,
|
||
default=None,
|
||
type=click.Path(file_okay=False, dir_okay=True),
|
||
help="Ausgabeverzeichnis (Standard: output/ aus config.json)",
|
||
)
|
||
def generate_cmd(excel_file: str, month: str, output_dir: str | None) -> None:
|
||
"""Liest Excel-Daten und erzeugt eine PDF-Datei pro Teilnehmer."""
|
||
config = load_config()
|
||
|
||
# --- Validation ----------------------------------------------------------
|
||
errors = validate(excel_file, month)
|
||
|
||
try:
|
||
year = extract_year_from_filename(excel_file, config)
|
||
except Exception as exc:
|
||
click.echo(f"Fehler bei Jahresextraktion: {exc}", err=True)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
persons = extract_persons(excel_file, month, config)
|
||
except Exception as exc:
|
||
click.echo(f"Fehler beim Lesen der Teilnehmer: {exc}", err=True)
|
||
sys.exit(1)
|
||
|
||
# --- Preview + confirmation ----------------------------------------------
|
||
confirmed = show_preview_and_confirm(
|
||
excel_file, month, year, len(persons), config, errors
|
||
)
|
||
if not confirmed:
|
||
click.echo("Abgebrochen. Keine PDFs erzeugt.")
|
||
sys.exit(0)
|
||
|
||
if errors:
|
||
click.echo("Generierung abgebrochen – es gibt Validierungsfehler.")
|
||
sys.exit(1)
|
||
|
||
# --- Resolve paths -------------------------------------------------------
|
||
project_root = Path(__file__).parent
|
||
template_path = (project_root / config["template_pdf"]).resolve()
|
||
out_dir = Path(output_dir).resolve() if output_dir else (project_root / config["output_dir"]).resolve()
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# --- Generate PDFs -------------------------------------------------------
|
||
success_count = 0
|
||
fail_count = 0
|
||
|
||
for person in persons:
|
||
try:
|
||
attendance = extract_attendance(excel_file, month, person, year, config)
|
||
output_filename = f"{person.filename_safe_name}.pdf"
|
||
output_path = out_dir / output_filename
|
||
fill_pdf(attendance, template_path, output_path, config)
|
||
click.echo(f" [OK] {output_filename}")
|
||
success_count += 1
|
||
except Exception as exc:
|
||
click.echo(f" [FEHLER] {person.filename_safe_name}: {exc}", err=True)
|
||
fail_count += 1
|
||
|
||
click.echo()
|
||
click.echo(f"Fertig: {success_count} PDF(s) erzeugt, {fail_count} Fehler.")
|
||
click.echo(f"Ausgabeverzeichnis: {out_dir}")
|
||
|
||
if fail_count > 0:
|
||
sys.exit(1)
|
||
|
||
|
||
@cli.command("render-image")
|
||
@click.option(
|
||
"--pdf",
|
||
"pdf_file",
|
||
required=True,
|
||
type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
||
help="Pfad zur PDF-Datei, die als Bild gerendert werden soll.",
|
||
)
|
||
@click.option(
|
||
"--out",
|
||
"output_path",
|
||
required=False,
|
||
default=None,
|
||
type=click.Path(file_okay=True, dir_okay=True),
|
||
help="Ausgabepfad (Datei für Einzelseite oder Ordner für --all-pages).",
|
||
)
|
||
@click.option(
|
||
"--dpi",
|
||
required=False,
|
||
default=150,
|
||
show_default=True,
|
||
type=int,
|
||
help="Render-Auflösung in DPI.",
|
||
)
|
||
@click.option(
|
||
"--page",
|
||
required=False,
|
||
default=1,
|
||
show_default=True,
|
||
type=int,
|
||
help="1-basierter Seitenindex für Einzelseiten-Render.",
|
||
)
|
||
@click.option(
|
||
"--all-pages",
|
||
is_flag=True,
|
||
default=False,
|
||
help="Alle Seiten rendern (ignoriert --page).",
|
||
)
|
||
def render_image_cmd(
|
||
pdf_file: str,
|
||
output_path: str | None,
|
||
dpi: int,
|
||
page: int,
|
||
all_pages: bool,
|
||
) -> None:
|
||
"""Rendert eine PDF direkt als PNG-Bild(er) über die Regression-Renderfunktion."""
|
||
pdf_path = Path(pdf_file).resolve()
|
||
images = render_pdf_pages(pdf_path, dpi=dpi)
|
||
|
||
if not images:
|
||
click.echo("Keine Seiten in der PDF gefunden.", err=True)
|
||
sys.exit(1)
|
||
|
||
if all_pages:
|
||
if output_path:
|
||
out_dir = Path(output_path).resolve()
|
||
else:
|
||
out_dir = pdf_path.parent / f"{pdf_path.stem}_images"
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
for idx, img in enumerate(images, start=1):
|
||
out_file = out_dir / f"{pdf_path.stem}_seite_{idx:02d}.png"
|
||
Image.fromarray(img).save(out_file)
|
||
click.echo(f"[OK] {out_file}")
|
||
return
|
||
|
||
if page < 1 or page > len(images):
|
||
click.echo(
|
||
f"Ungültige Seite: {page}. PDF hat {len(images)} Seite(n).",
|
||
err=True,
|
||
)
|
||
sys.exit(1)
|
||
|
||
if output_path:
|
||
out_file = Path(output_path).resolve()
|
||
else:
|
||
out_file = pdf_path.parent / f"{pdf_path.stem}_seite_{page:02d}.png"
|
||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
Image.fromarray(images[page - 1]).save(out_file)
|
||
click.echo(f"[OK] {out_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
cli()
|