diff --git a/.claude/skills/license-check/SKILL.md b/.claude/skills/license-check/SKILL.md new file mode 100644 index 0000000..c5621b7 --- /dev/null +++ b/.claude/skills/license-check/SKILL.md @@ -0,0 +1,94 @@ +--- +name: license-check +description: "Prüft und aktualisiert THIRD_PARTY_LICENSES.txt bei Dependency-Änderungen. Verwende diesen Skill IMMER zusammen mit dem version-bump Skill wenn der Benutzer einen Git-Commit erstellen möchte, 'commit' erwähnt, oder nach /commit fragt. Der Skill erkennt automatisch ob sich Dependencies in pyproject.toml geändert haben und aktualisiert die Lizenzdatei entsprechend." +--- + +# License Check Skill + +Dieser Skill stellt sicher, dass die Datei `THIRD_PARTY_LICENSES.txt` immer synchron mit den tatsächlichen Dependencies in `pyproject.toml` bleibt. Er wird automatisch als Teil des Commit-Workflows ausgeführt, parallel zum version-bump Skill. + +## Warum das wichtig ist + +DocuMentor listet alle verwendeten Drittanbieter-Bibliotheken mit Lizenzinformationen in `THIRD_PARTY_LICENSES.txt` auf. Wenn Dependencies hinzugefügt oder entfernt werden, muss diese Datei aktualisiert werden — sonst sind die Lizenzangaben unvollständig oder veraltet, was rechtliche Konsequenzen haben kann. + +## Ablauf + +### Schritt 1: Prüfskript ausführen + +Führe das gebündelte Prüfskript aus: + +```bash +uv run python .claude/skills/license-check/scripts/check_licenses.py +``` + +Das Skript gibt JSON aus mit: +- `missing`: Dependencies in pyproject.toml die in THIRD_PARTY_LICENSES.txt fehlen +- `removed`: Einträge in THIRD_PARTY_LICENSES.txt die nicht mehr in pyproject.toml stehen +- `info`: Automatisch ermittelte Lizenz-Metadaten für fehlende Pakete + +### Schritt 2: Ergebnis auswerten + +- Wenn `missing` und `removed` beide leer sind: **Keine Aktion nötig.** Fahre direkt mit dem Commit fort. +- Wenn es Änderungen gibt: Zeige dem Benutzer eine Zusammenfassung und frage ob die Lizenzdatei aktualisiert werden soll. + +Beispiel-Zusammenfassung: +``` +Lizenzdatei-Prüfung: + + lxml (BSD License) — neu hinzuzufügen + - some-old-lib — aus Lizenzdatei zu entfernen +``` + +### Schritt 3: THIRD_PARTY_LICENSES.txt aktualisieren + +#### Neue Dependencies hinzufügen + +Füge neue Einträge in die passende Sektion ein (Python-Abhängigkeiten oder Eingebettete Bibliotheken). Verwende das bestehende Format: + +``` +N. PaketName + Version: >=X.Y.Z + Lizenz: Lizenzname + Webseite: https://... + GitHub: https://github.com/... + Beschreibung: Kurzbeschreibung auf Englisch + Copyright: Copyright (c) Jahr Autor +``` + +Dabei gilt: +- Die Nummerierung fortlaufend innerhalb der Sektion +- Dev-Dependencies bekommen den Suffix `(Development)` im Namen +- Transitive Dependencies bekommen den Suffix `(via HauptPaket)` im Namen +- Die Lizenzinfos aus dem `info`-Feld des Skripts verwenden +- Wenn das Skript keine Info liefert (Paket nicht installiert), recherchiere via Web + +#### Sortierung + +Die Reihenfolge der Einträge folgt der logischen Gruppierung: +1. Haupt-Dependencies (Runtime) +2. Transitive Dependencies direkt nach ihrem Eltern-Paket (z.B. ConnectorX nach Polars) +3. Dev-Dependencies am Ende der Python-Sektion + +#### Entfernte Dependencies löschen + +Entferne den kompletten Block (Name, Version, Lizenz, etc.) des entfernten Pakets und nummeriere die verbleibenden Einträge neu. + +#### Neue Lizenztypen + +Wenn eine neue Dependency eine Lizenz verwendet, die noch nicht im Abschnitt "Lizenztexte" am Ende der Datei aufgeführt ist, füge den Lizenztext dort hinzu. Gängige Lizenztexte (MIT, Apache 2.0, BSD-3-Clause) sind bereits vorhanden. + +### Schritt 4: KNOWN_ALIASES aktualisieren + +Wenn neue Dependencies hinzugefügt werden, prüfe ob das `KNOWN_ALIASES`-Dict in `.claude/skills/license-check/scripts/check_licenses.py` aktualisiert werden muss: + +- Wenn der Paketname in pyproject.toml anders ist als in THIRD_PARTY_LICENSES.txt (z.B. durch Suffixe wie "(Development)" oder "(via X)") +- Wenn eine Dependency transitive Dependencies hat, die separat gelistet werden + +### Schritt 5: Commit fortsetzen + +Füge die geänderte `THIRD_PARTY_LICENSES.txt` (und ggf. `check_licenses.py`) zum Staging-Bereich hinzu und fahre mit dem normalen Commit-Workflow fort. + +## Wichtige Hinweise + +- Das Skript prüft nur **Python-Abhängigkeiten** und **eingebettete Bibliotheken** — externe Tools (Saxon, FOP, diff-pdf) werden ignoriert +- Die Lizenzdatei enthält auch den Abschnitt "Stand: Monat Jahr" am Ende — diesen bei Änderungen auf den aktuellen Monat aktualisieren +- `uv.lock` nicht manuell ändern — wird durch `uv sync` automatisch aktualisiert diff --git a/.claude/skills/license-check/scripts/check_licenses.py b/.claude/skills/license-check/scripts/check_licenses.py new file mode 100644 index 0000000..8ed48de --- /dev/null +++ b/.claude/skills/license-check/scripts/check_licenses.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Vergleicht die Dependencies aus pyproject.toml mit den Einträgen in THIRD_PARTY_LICENSES.txt. + +Gibt eine JSON-Ausgabe mit: +- missing: Dependencies die in pyproject.toml stehen aber nicht in THIRD_PARTY_LICENSES.txt +- removed: Einträge in THIRD_PARTY_LICENSES.txt die nicht mehr in pyproject.toml stehen +- version_changed: Dependencies deren Mindestversion sich geändert hat +- info: Metadaten zu fehlenden Paketen (Lizenz, Homepage, etc.) +""" + +import json +import re +import sys +import tomllib +from importlib.metadata import PackageNotFoundError, metadata +from pathlib import Path + + +def parse_pyproject(pyproject_path: Path) -> dict[str, str]: + """Parst pyproject.toml und extrahiert Dependencies mit Mindestversionen.""" + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + + deps: dict[str, str] = {} + + # dependencies-Sektion + for dep_str in data.get("project", {}).get("dependencies", []): + m = re.match(r"([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:>=([0-9.]+))?", dep_str) + if m: + deps[m.group(1).lower()] = m.group(2) or "" + + # dependency-groups dev + for dep_str in data.get("dependency-groups", {}).get("dev", []): + if isinstance(dep_str, str): + m = re.match(r"([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:>=([0-9.]+))?", dep_str) + if m: + deps[m.group(1).lower()] = m.group(2) or "" + + return deps + + +def parse_licenses_file(licenses_path: Path) -> tuple[dict[str, str], dict[str, str]]: + """Parst THIRD_PARTY_LICENSES.txt und extrahiert Paketnamen nach Sektion. + + Returns: + tuple[dict, dict]: (python_deps, embedded_libs) — jeweils lowercase key -> original name + """ + content = licenses_path.read_text(encoding="utf-8") + python_deps: dict[str, str] = {} + embedded_libs: dict[str, str] = {} + current_section = None + current_target = None + + for line in content.splitlines(): + if "Python-Abhängigkeiten" in line: + current_section = "python" + current_target = python_deps + continue + if "Eingebettete Bibliotheken" in line: + current_section = "embedded" + current_target = embedded_libs + continue + if "Externe Tools" in line or "Lizenztexte" in line: + current_section = None + current_target = None + continue + if current_target is None: + continue + + # Nummerierter Eintrag: "1. PaketName" oder "1. PaketName (via X)" + entry_match = re.match(r"\s*\d+\.\s+(.+?)(?:\s+\(.*\))?\s*$", line) + if entry_match: + name = entry_match.group(1).strip() + current_target[name.lower()] = name + continue + + # Version-Zeile: " Version: >=X.Y.Z" + version_match = re.match(r"\s+Version:\s*>=?([\d.]+)", line) + if version_match and current_target: + last_key = list(current_target.keys())[-1] + current_target[last_key] = current_target[last_key] + "|" + version_match.group(1) + + return python_deps, embedded_libs + + +# Mapping: pyproject-Name -> zugehörige Einträge in THIRD_PARTY_LICENSES.txt +# Deckt transitive Dependencies und Aliase mit Suffixen ab. +KNOWN_ALIASES = { + "pyside6": ["pyside6"], + "pydantic-settings": ["pydantic-settings", "pydantic"], # pydantic ist transitive Dep + "pydantic-yaml": ["pydantic-yaml"], + "polars": ["polars", "connectorx (via polars)", "pyarrow (via polars)"], + "connectorx": ["connectorx (via polars)"], + "psutil": ["psutil"], + "lxml": ["lxml"], # BSD-3-Clause, XML/XSLT-Parsing + "ruff": ["ruff (development)"], + "pyinstaller": ["pyinstaller (development)"], + "pillow": ["pillow (development)"], +} + + +def get_package_info(pkg_name: str) -> dict: + """Holt Paket-Metadaten via importlib.metadata.""" + info = {"name": pkg_name, "installed": False} + try: + m = metadata(pkg_name) + info["installed"] = True + info["version"] = m.get("Version", "") + info["summary"] = m.get("Summary", "") + + # Lizenz ermitteln + license_expr = m.get("License-Expression") or "" + if not license_expr: + classifiers = [c for c in (m.get_all("Classifier") or []) if "License" in c] + if classifiers: + license_expr = classifiers[0].split(" :: ")[-1] + else: + lic_text = m.get("License") or "" + if "MIT" in lic_text: + license_expr = "MIT License" + elif "BSD" in lic_text: + license_expr = "BSD License" + elif "Apache" in lic_text: + license_expr = "Apache License 2.0" + elif "LGPL" in lic_text or "GPL" in lic_text: + license_expr = lic_text[:80] + else: + license_expr = lic_text[:80] if lic_text else "Unbekannt" + info["license"] = license_expr + + # Homepage/GitHub + urls = m.get_all("Project-URL") or [] + for url_entry in urls: + if "," in url_entry: + label, url = url_entry.split(",", 1) + label = label.strip().lower() + url = url.strip() + if "homepage" in label or "home-page" in label: + info["homepage"] = url + elif "repository" in label or "github" in label or "source" in label: + info["github"] = url + if "homepage" not in info: + homepage = m.get("Home-page") + if homepage: + info["homepage"] = homepage + + # Author/Copyright + author = m.get("Author") or m.get("Author-email") or "" + info["author"] = author + + except PackageNotFoundError: + pass + + return info + + +def normalize_name(name: str) -> str: + """Normalisiert Paketnamen für Vergleich.""" + return re.sub(r"[-_.]+", "-", name).lower().strip() + + +def main(): + project_root = Path(__file__).resolve().parents[4] # .claude/skills/license-check/scripts -> root + pyproject_path = project_root / "pyproject.toml" + licenses_path = project_root / "THIRD_PARTY_LICENSES.txt" + + if not pyproject_path.exists(): + print(json.dumps({"error": f"pyproject.toml nicht gefunden: {pyproject_path}"})) + sys.exit(1) + + if not licenses_path.exists(): + print(json.dumps({"error": f"THIRD_PARTY_LICENSES.txt nicht gefunden: {licenses_path}"})) + sys.exit(1) + + pyproject_deps = parse_pyproject(pyproject_path) + python_entries, embedded_entries = parse_licenses_file(licenses_path) + + # Normalisiere Python-License-Entry-Keys + normalized_license_names = {} + for key in python_entries: + clean = re.sub(r"\s*\(.*?\)", "", key).strip() + normalized_license_names[normalize_name(clean)] = key + + result = { + "pyproject_deps": {k: v for k, v in sorted(pyproject_deps.items())}, + "python_license_entries": list(python_entries.keys()), + "embedded_license_entries": list(embedded_entries.keys()), + "missing": [], + "removed": [], + "info": {}, + } + + # Finde fehlende Dependencies + covered_in_licenses = set() + for dep_name in pyproject_deps: + norm = normalize_name(dep_name) + if norm in normalized_license_names: + covered_in_licenses.add(norm) + elif dep_name in KNOWN_ALIASES: + found = False + for alias in KNOWN_ALIASES[dep_name]: + alias_norm = normalize_name(re.sub(r"\s*\(.*?\)", "", alias)) + if alias_norm in normalized_license_names: + found = True + covered_in_licenses.add(alias_norm) + if not found: + result["missing"].append(dep_name) + result["info"][dep_name] = get_package_info(dep_name) + else: + result["missing"].append(dep_name) + result["info"][dep_name] = get_package_info(dep_name) + + # Finde entfernte Einträge (nur Python-Abhängigkeiten, NICHT eingebettete) + for norm_name, orig_key in normalized_license_names.items(): + if norm_name not in covered_in_licenses: + # Prüfe ob es ein "via"-Eintrag ist + if "(via" in orig_key: + parent = re.search(r"\(via\s+(\w+)\)", orig_key) + if parent and normalize_name(parent.group(1)) in {normalize_name(d) for d in pyproject_deps}: + continue + # Prüfe ob es über KNOWN_ALIASES abgedeckt ist + is_alias = False + for dep, aliases in KNOWN_ALIASES.items(): + if dep in pyproject_deps: + for alias in aliases: + if normalize_name(re.sub(r"\s*\(.*?\)", "", alias)) == norm_name: + is_alias = True + break + if is_alias: + break + if not is_alias: + result["removed"].append(orig_key) + + print(json.dumps(result, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/DocuMentor.wxs b/DocuMentor.wxs index 7276358..1a3785a 100644 --- a/DocuMentor.wxs +++ b/DocuMentor.wxs @@ -4,7 +4,7 @@ =6.0.2 + Lizenz: BSD-3-Clause License + Webseite: https://lxml.de/ + GitHub: https://github.com/lxml/lxml + Beschreibung: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API + Copyright: Copyright (c) 2004 Infrae. All rights reserved. + +10. Ruff (Development) Version: >=0.14.11 Lizenz: MIT License GitHub: https://github.com/astral-sh/ruff Beschreibung: An extremely fast Python linter and code formatter Copyright: Copyright (c) 2022 Charlie Marsh -10. PyInstaller (Development) +11. PyInstaller (Development) Version: >=6.0.0 Lizenz: GPL-2.0 mit Bootloader-Ausnahme Webseite: https://pyinstaller.org @@ -82,7 +90,7 @@ Python-Abhängigkeiten Beschreibung: Bundles Python applications into stand-alone executables Copyright: Copyright (c) 2010-2025 PyInstaller Development Team -11. Pillow (Development) +12. Pillow (Development) Version: >=10.0.0 Lizenz: HPND License (Historical Permission Notice and Disclaimer) Webseite: https://python-pillow.org @@ -245,5 +253,5 @@ HINWEISE ================================================================================ Stand: März 2026 -Erstellt für: DocuMentor v1.1.0 +Erstellt für: DocuMentor v1.1.1 ================================================================================ diff --git a/installer.iss b/installer.iss index c6f4d75..a760963 100644 --- a/installer.iss +++ b/installer.iss @@ -10,7 +10,7 @@ ; Build-Befehl: iscc installer.iss #define MyAppName "DocuMentor" -#define MyAppVersion "1.1.0" +#define MyAppVersion "1.1.1" #define MyAppPublisher "Ihr Name/Organisation" #define MyAppURL "https://github.com/yourusername/xsl-validator" #define MyAppExeName "DocuMentor.exe" diff --git a/pyproject.toml b/pyproject.toml index c6b3279..2b975fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "DocuMentor" -version = "1.1.0" +version = "1.1.1" description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung" readme = "README.md" license = {text = "MIT"} diff --git a/src/ui/AboutDialog.py b/src/ui/AboutDialog.py index 7bfe3f7..fc3b3e6 100644 --- a/src/ui/AboutDialog.py +++ b/src/ui/AboutDialog.py @@ -116,7 +116,7 @@ class AboutDialog(QDialog): header.resizeSection(0, 170) # Name header.resizeSection(1, 180) # Lizenz header.resizeSection(2, 80) # Installiert - header.resizeSection(5, 140) # Kategorie + header.resizeSection(5, 200) # Kategorie header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Webseite header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) # Copyright diff --git a/uv.lock b/uv.lock index ceedbd9..0b392cc 100644 --- a/uv.lock +++ b/uv.lock @@ -34,7 +34,7 @@ wheels = [ [[package]] name = "documentor" -version = "1.0.0" +version = "1.1.0" source = { virtual = "." } dependencies = [ { name = "connectorx" },