Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60f4b7dcef | |||
| 40b778b41b | |||
| 6fcf706d96 | |||
| 8b29214abd | |||
| ac654a6f7c | |||
| cedd9bfa0f | |||
| 62d0af9fe3 | |||
| b30bb0ed2d | |||
| 5ecad6ce89 | |||
| 2daa77e85d |
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"enabledPlugins": {
|
|
||||||
"feature-dev@claude-plugins-official": true
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(uv version)",
|
|
||||||
"Bash(uv run ruff check)",
|
|
||||||
"Bash(uv run ruff check *)",
|
|
||||||
"Bash(uv tree)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
---
|
|
||||||
name: dep-update-check
|
|
||||||
description: Prüft manuell, ob Python-Version und Projekt-Dependencies aktualisiert werden können, und testet die Kompatibilität. Verwende diesen Skill AUSSCHLIESSLICH wenn der Benutzer explizit danach fragt – z.B. '/dep-update-check', 'Prüfe Dependencies', 'Sind Updates verfügbar?', 'Kann ich Python updaten?'. Niemals automatisch auslösen.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dependency & Python Update Check
|
|
||||||
|
|
||||||
Dieser Skill prüft systematisch, welche Updates für das DocuMentor-Projekt verfügbar sind und ob sie kompatibel miteinander sind. Verwendete Werkzeuge: `uv`, `pyproject.toml`.
|
|
||||||
|
|
||||||
## Ablauf
|
|
||||||
|
|
||||||
Führe alle Schritte der Reihe nach aus und erstelle am Ende einen übersichtlichen Report.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Schritt 1: Python-Versionscheck
|
|
||||||
|
|
||||||
Lies die Python-Constraint aus `pyproject.toml` (Feld `requires-python`).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep "requires-python" pyproject.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
Prüfe dann, welche stabilen Python-Versionen im erlaubten Bereich verfügbar sind:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv python list
|
|
||||||
```
|
|
||||||
|
|
||||||
**Stabile Versionen** sind jene ohne Suffix wie `b1`, `rc1`, `a1` oder `+freethreaded`. Filtere Beta- und Release-Candidate-Versionen heraus.
|
|
||||||
|
|
||||||
Vergleiche:
|
|
||||||
- Aktuell verwendete Version: `uv run python --version`
|
|
||||||
- Neueste stabile verfügbare Version innerhalb der Constraints
|
|
||||||
|
|
||||||
Falls eine neuere stabile Version verfügbar ist, notiere dies für den Report.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Schritt 2: Veraltete Dependencies ermitteln
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv tree --outdated
|
|
||||||
```
|
|
||||||
|
|
||||||
Extrahiere daraus die **direkten** Projektabhängigkeiten (aus `pyproject.toml`, Abschnitte `[project] dependencies` und `[dependency-groups]`) und trenne sie von transitiven Abhängigkeiten.
|
|
||||||
|
|
||||||
Erstelle eine strukturierte Liste:
|
|
||||||
- Direkte Dependencies: Name, aktuelle Version, neueste Version
|
|
||||||
- Transitive Dependencies mit Updates (nur zur Information)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Schritt 3: Kompatibilitätstest
|
|
||||||
|
|
||||||
Sichere zunächst die aktuelle Lock-Datei:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp uv.lock uv.lock.backup
|
|
||||||
```
|
|
||||||
|
|
||||||
Versuche dann, alle Dependencies auf die neuesten kompatiblen Versionen aufzulösen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv lock --upgrade 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
**Interpretation:**
|
|
||||||
- `Resolved X packages` ohne Fehler → alle Updates sind kompatibel
|
|
||||||
- Fehlermeldungen über Versionskonflikte → notiere welche Pakete sich gegenseitig blockieren
|
|
||||||
- Prüfe ob `uv.lock` verändert wurde: `diff uv.lock.backup uv.lock | head -50`
|
|
||||||
|
|
||||||
Stelle die Lock-Datei wieder her (wir wollen die Umgebung nicht tatsächlich ändern):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mv uv.lock.backup uv.lock
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Schritt 4: Report erstellen
|
|
||||||
|
|
||||||
Gib den Report im folgenden Format aus:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🐍 Python-Version
|
|
||||||
|
|
||||||
| | Version |
|
|
||||||
|---|---|
|
|
||||||
| Aktuell in Verwendung | z.B. 3.13.11 |
|
|
||||||
| Neueste stabile im erlaubten Bereich | z.B. 3.13.13 |
|
|
||||||
| Update empfohlen? | Ja / Nein |
|
|
||||||
|
|
||||||
Falls ein Python-Update verfügbar ist, zeige den Befehl:
|
|
||||||
```bash
|
|
||||||
uv python install 3.X.Y
|
|
||||||
# Dann in pyproject.toml requires-python anpassen falls nötig
|
|
||||||
# Danach: uv sync
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📦 Direkte Dependencies
|
|
||||||
|
|
||||||
Tabelle mit Spalten: Paket | Aktuell | Verfügbar | Status
|
|
||||||
|
|
||||||
Status-Symbole:
|
|
||||||
- ✅ Aktuell
|
|
||||||
- ⬆️ Update verfügbar
|
|
||||||
- ⚠️ Update verfügbar, aber Kompatibilitätsproblem
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔗 Transitive Dependencies (Auswahl)
|
|
||||||
|
|
||||||
Nur falls es relevante Updates gibt, kurze Liste.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔄 Kompatibilitäts-Ergebnis
|
|
||||||
|
|
||||||
Klares Fazit:
|
|
||||||
- Können alle direkten Dependencies gleichzeitig aktualisiert werden? Ja/Nein
|
|
||||||
- Falls Nein: Welche Konflikte bestehen und warum?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📋 Empfohlene Aktion
|
|
||||||
|
|
||||||
Falls Updates verfügbar und kompatibel:
|
|
||||||
```bash
|
|
||||||
uv sync --upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
Falls nur einzelne Pakete aktualisiert werden sollen:
|
|
||||||
```bash
|
|
||||||
uv add paketname>=neue.version
|
|
||||||
```
|
|
||||||
|
|
||||||
Falls Python aktualisiert werden soll (nur wenn innerhalb der Constraints):
|
|
||||||
```bash
|
|
||||||
uv python install 3.X.Y
|
|
||||||
uv sync
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hinweise
|
|
||||||
|
|
||||||
- **Nie `uv sync --upgrade` automatisch ausführen** – nur im Report vorschlagen, der Benutzer entscheidet.
|
|
||||||
- Beta/RC-Python-Versionen werden nicht empfohlen (erkennbar an Suffixen wie `b1`, `rc1`).
|
|
||||||
- `pyarrow` und andere native Pakete können bei Python-Upgrades besondere Anforderungen haben – darauf hinweisen falls relevant.
|
|
||||||
- Wenn der `uv lock --upgrade`-Test fehlschlägt, die Lock-Datei **immer** aus dem Backup wiederherstellen.
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
name: version-bump
|
|
||||||
description: "Versionsverwaltung für DocuMentor-Commits. Verwende diesen Skill IMMER wenn der Benutzer einen Git-Commit erstellen möchte, 'commit' erwähnt, oder nach /commit fragt. Der Skill fragt vor dem Commit, ob die Programmversion aktualisiert werden soll, und aktualisiert alle versionsbezogenen Dateien einheitlich."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Version Bump Skill
|
|
||||||
|
|
||||||
Dieser Skill stellt sicher, dass bei jedem Commit die Programmversion bewusst behandelt wird. Bevor der eigentliche Commit erstellt wird, wird der Benutzer gefragt, ob und wie die Version angepasst werden soll.
|
|
||||||
|
|
||||||
## Warum das wichtig ist
|
|
||||||
|
|
||||||
DocuMentor speichert die Version an mehreren Stellen gleichzeitig (pyproject.toml, Installer-Dateien, Lizenz-Footer). Wenn diese aus dem Takt geraten, entstehen inkonsistente Builds. Dieser Skill verhindert das, indem er alle Stellen auf einmal aktualisiert.
|
|
||||||
|
|
||||||
## Ablauf
|
|
||||||
|
|
||||||
### Schritt 1: Benutzer fragen
|
|
||||||
|
|
||||||
Bevor du den Commit erstellst, frage den Benutzer mit dem AskUserQuestion-Tool:
|
|
||||||
|
|
||||||
**Frage:** "Soll die Programmversion aktualisiert werden?"
|
|
||||||
|
|
||||||
Optionen:
|
|
||||||
- **Patch (X.Y.Z+1)** — Bugfix, kleine Änderung
|
|
||||||
- **Minor (X.Y+1.0)** — Neues Feature, Erweiterung
|
|
||||||
- **Major (X+1.0.0)** — Breaking Change, großer Meilenstein
|
|
||||||
- **Nein, Version beibehalten** — Keine Versionsänderung
|
|
||||||
|
|
||||||
Zeige dabei die aktuelle Version aus `pyproject.toml` in der Frage an.
|
|
||||||
|
|
||||||
### Schritt 2: Version aktualisieren (falls gewünscht)
|
|
||||||
|
|
||||||
Wenn der Benutzer eine Versionserhöhung wählt:
|
|
||||||
|
|
||||||
1. **`pyproject.toml`** — über `uv version --bump` aktualisieren (niemals direkt bearbeiten):
|
|
||||||
```bash
|
|
||||||
uv version --bump patch # für Patch
|
|
||||||
uv version --bump minor # für Minor
|
|
||||||
uv version --bump major # für Major
|
|
||||||
```
|
|
||||||
Nach dem Befehl die neue Version aus `pyproject.toml` auslesen — sie ist die Single Source of Truth.
|
|
||||||
|
|
||||||
2. **`DocuMentor.wxs`** — WiX Installer (z. B. Zeile mit `Version=`):
|
|
||||||
```xml
|
|
||||||
Version="X.Y.Z"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **`installer.iss`** — Inno Setup (z. B. Zeile mit `#define MyAppVersion`):
|
|
||||||
```
|
|
||||||
#define MyAppVersion "X.Y.Z"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **`THIRD_PARTY_LICENSES.txt`** — Lizenz-Footer (letzte Zeile):
|
|
||||||
```
|
|
||||||
Erstellt für: DocuMentor vX.Y.Z
|
|
||||||
```
|
|
||||||
|
|
||||||
Führe zuerst `uv version --bump` aus, lese danach die neue Version aus `pyproject.toml`, und aktualisiere dann die übrigen drei Dateien auf diesen Wert.
|
|
||||||
|
|
||||||
### Schritt 3: Commit erstellen
|
|
||||||
|
|
||||||
Nachdem die Versionsdateien aktualisiert wurden (oder der Benutzer "Nein" gewählt hat), erstelle den Commit ganz normal nach den üblichen Commit-Konventionen. Falls die Version geändert wurde, füge die geänderten Versionsdateien zum Commit hinzu.
|
|
||||||
|
|
||||||
## Wichtige Hinweise
|
|
||||||
|
|
||||||
- `pyproject.toml` **niemals direkt bearbeiten** — immer `uv version --bump` verwenden
|
|
||||||
- Die Version in `pyproject.toml` ist die Single Source of Truth; nach dem Bump dort auslesen
|
|
||||||
- `create_version_info.py` liest automatisch aus `pyproject.toml` — diese Datei muss nicht manuell angepasst werden
|
|
||||||
- `uv.lock` wird durch `uv sync` automatisch aktualisiert — nicht manuell ändern
|
|
||||||
- Wenn der Benutzer "Nein" wählt, einfach normal mit dem Commit fortfahren
|
|
||||||
-15
@@ -6,20 +6,5 @@ dist/
|
|||||||
wheels/
|
wheels/
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
*.spec.bak
|
|
||||||
*.manifest
|
|
||||||
*.log
|
|
||||||
version_info.txt
|
|
||||||
|
|
||||||
# Generierte Icons (optional - entfernen falls Icons versioniert werden sollen)
|
|
||||||
# resources/icon.ico
|
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
# WiX Installer Build-Artefakte
|
|
||||||
ProductFiles.wxs
|
|
||||||
*.msi
|
|
||||||
*.wixpdb
|
|
||||||
.wix/
|
|
||||||
|
|||||||
@@ -1,404 +0,0 @@
|
|||||||
# DocuMentor Build-Anleitung
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dependencies installieren (inkl. Pillow für Icon-Generierung und PyInstaller)
|
|
||||||
uv sync --all-groups
|
|
||||||
```
|
|
||||||
|
|
||||||
Für MSI-Installer zusätzlich:
|
|
||||||
- **WiX Toolset v6**: `dotnet tool install --global wix --version 6.*`
|
|
||||||
> **Hinweis**: WiX v7+ erfordert die Akzeptanz der OSMF-EULA (`wix eula accept`). WiX v6 vermeidet diese Einschränkung.
|
|
||||||
|
|
||||||
Für Setup.exe zusätzlich:
|
|
||||||
- **Inno Setup**: https://jrsoftware.org/isdl.php
|
|
||||||
|
|
||||||
## Schnellstart: ZIP-Distribution erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Automatischer Build (empfohlen)
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Dies erstellt automatisch:
|
|
||||||
1. **Icon** (falls nicht vorhanden): `resources/icon.ico`
|
|
||||||
2. **Versionsinformationen**: `version_info.txt`
|
|
||||||
3. **Executable**: `dist/DocuMentor/DocuMentor.exe` (mit Icon und Versionsinformationen)
|
|
||||||
4. **ZIP-Archiv**: `dist/DocuMentor-YYYYMMDD-Windows.zip`
|
|
||||||
|
|
||||||
### Manuelle Build-Schritte
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Cleanup
|
|
||||||
rm -rf build/ dist/
|
|
||||||
|
|
||||||
# 2. PyInstaller ausführen
|
|
||||||
uv run pyinstaller --clean DocuMentor.spec
|
|
||||||
|
|
||||||
# 3. Testen
|
|
||||||
# Auf Windows: dist/DocuMentor/DocuMentor.exe
|
|
||||||
# Mit Wine: wine dist/DocuMentor/DocuMentor.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
## MSI-Installer erstellen (WiX Toolset)
|
|
||||||
|
|
||||||
### Schritt 1: PyInstaller Build erstellen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 2: ProductFiles.wxs generieren
|
|
||||||
|
|
||||||
**WICHTIG**: WiX v6 hat das `heat` Tool entfernt. Stattdessen wird ein Python-Skript verwendet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generiert automatisch ProductFiles.wxs mit allen Dateien aus dist/DocuMentor
|
|
||||||
uv run python generate_wix_files.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 3: MSI kompilieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schritt 4: MSI testen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installation (als Administrator)
|
|
||||||
msiexec /i DocuMentor.msi
|
|
||||||
|
|
||||||
# Silent Installation für Deployment
|
|
||||||
msiexec /i DocuMentor.msi /quiet /qn /norestart
|
|
||||||
|
|
||||||
# Deinstallation
|
|
||||||
msiexec /x DocuMentor.msi
|
|
||||||
```
|
|
||||||
|
|
||||||
### MSI-Vorteile
|
|
||||||
|
|
||||||
- Windows-Standard (`.msi` Format)
|
|
||||||
- Gruppen-Richtlinien-Deployment (GPO) für Enterprise
|
|
||||||
- Silent Installation (`msiexec /quiet`)
|
|
||||||
- Windows Installer Transaktionen und Rollback
|
|
||||||
- Patch-Unterstützung (.msp für Updates)
|
|
||||||
- Versionsupgrades automatisch verwaltet
|
|
||||||
|
|
||||||
### MSI-Build automatisieren
|
|
||||||
|
|
||||||
Alternativ zu den manuellen Schritten kann ein `build_msi.py` Skript verwendet werden:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python build_msi.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Dieses Skript führt `generate_wix_files.py` und `wix build` automatisch nacheinander aus.
|
|
||||||
|
|
||||||
## Setup.exe erstellen (Inno Setup)
|
|
||||||
|
|
||||||
1. **GUID generieren** für `installer.iss` (nur beim ersten Mal!):
|
|
||||||
```bash
|
|
||||||
uv run python generate_guid.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Kopiere die generierte GUID und füge sie in `installer.iss` bei `AppId` ein (Zeile ~22).
|
|
||||||
|
|
||||||
**WICHTIG**: Die GUID nur EINMAL generieren! Bei Updates dieselbe GUID verwenden.
|
|
||||||
|
|
||||||
2. **Windows-Build erstellen**:
|
|
||||||
```bash
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Installer kompilieren**:
|
|
||||||
```bash
|
|
||||||
iscc installer.iss
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Ergebnis**:
|
|
||||||
- `dist/installer/DocuMentor-Setup-0.1.0.exe` (mit Icon und Versionsinformationen)
|
|
||||||
|
|
||||||
## Konfiguration anpassen
|
|
||||||
|
|
||||||
### Icon anpassen
|
|
||||||
|
|
||||||
**Option 1: Standard-Icon generieren**
|
|
||||||
```bash
|
|
||||||
uv run python create_icon.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2: Eigenes Icon aus PNG erstellen**
|
|
||||||
```bash
|
|
||||||
uv run python create_icon.py ihr-logo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 3: Manuell ICO-Datei platzieren**
|
|
||||||
1. Icon erstellen oder downloaden (`.ico` Format mit mehreren Größen)
|
|
||||||
2. Als `resources/icon.ico` speichern
|
|
||||||
3. Beim nächsten Build wird es automatisch verwendet
|
|
||||||
|
|
||||||
Das Icon wird automatisch verwendet für:
|
|
||||||
- Windows-Executable (DocuMentor.exe)
|
|
||||||
- Inno Setup Installer
|
|
||||||
- Desktop-Verknüpfungen
|
|
||||||
|
|
||||||
**Anforderungen:**
|
|
||||||
- Multi-Size ICO (16x16 bis 256x256 Pixel)
|
|
||||||
- Das `create_icon.py` Skript erstellt alle Größen automatisch
|
|
||||||
|
|
||||||
### Versionsinformationen
|
|
||||||
|
|
||||||
Versionsinformationen werden automatisch aus `pyproject.toml` generiert:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python create_version_info.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Version ändern:**
|
|
||||||
|
|
||||||
In `pyproject.toml`:
|
|
||||||
```toml
|
|
||||||
version = "0.2.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Auch aktualisieren in:
|
|
||||||
- `installer.iss` (Zeile 13: `#define MyAppVersion`)
|
|
||||||
|
|
||||||
Dann Versionsinformationen neu generieren:
|
|
||||||
```bash
|
|
||||||
uv run python create_version_info.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Was enthalten die Versionsinformationen:**
|
|
||||||
- Dateiversion und Produktversion
|
|
||||||
- Beschreibung und Copyright
|
|
||||||
- Anwendungsname
|
|
||||||
- Wird in Windows Explorer angezeigt (Rechtsklick → Eigenschaften → Details)
|
|
||||||
|
|
||||||
### Build-Größe reduzieren
|
|
||||||
|
|
||||||
In `DocuMentor.spec` Module ausschließen:
|
|
||||||
```python
|
|
||||||
excludes=[
|
|
||||||
'tkinter',
|
|
||||||
'matplotlib',
|
|
||||||
'test',
|
|
||||||
'unittest',
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
### One-File Build (alles in einer .exe)
|
|
||||||
|
|
||||||
In `DocuMentor.spec` ändern:
|
|
||||||
```python
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries, # Uncomment
|
|
||||||
a.zipfiles, # Uncomment
|
|
||||||
a.datas, # Uncomment
|
|
||||||
[], # Comment out
|
|
||||||
exclude_binaries=False, # Ändern
|
|
||||||
# ...
|
|
||||||
name='DocuMentor',
|
|
||||||
onefile=True, # Hinzufügen
|
|
||||||
)
|
|
||||||
|
|
||||||
# COLLECT auskommentieren oder entfernen
|
|
||||||
```
|
|
||||||
|
|
||||||
**Achtung**: One-File ist langsamer beim Start (3-5 Sekunden).
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Lokales Testing (Linux/WSL)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build erstellen
|
|
||||||
uv run python build_windows.py
|
|
||||||
|
|
||||||
# Mit Wine testen
|
|
||||||
wine dist/DocuMentor/DocuMentor.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing auf Windows
|
|
||||||
|
|
||||||
1. ZIP-Datei auf Windows-System kopieren
|
|
||||||
2. Entpacken
|
|
||||||
3. `DocuMentor.exe` starten
|
|
||||||
4. Features testen:
|
|
||||||
- [ ] Programmstart
|
|
||||||
- [ ] Einstellungsdialog öffnet beim ersten Start
|
|
||||||
- [ ] Projekt öffnen/erstellen
|
|
||||||
- [ ] Tree-Navigation
|
|
||||||
- [ ] XSL/XML-Dateien hinzufügen
|
|
||||||
- [ ] PDF-Generierung (mit konfigurierten Tools)
|
|
||||||
- [ ] PDF-Vergleich
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "Module not found" beim Start
|
|
||||||
|
|
||||||
**Lösung A** — Einfache Python-Module: Hidden imports in `DocuMentor.spec` ergänzen:
|
|
||||||
```python
|
|
||||||
hiddenimports=[
|
|
||||||
'missing_module',
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lösung B** — Packages mit nativen Extensions (Rust/C, z.B. `connectorx`):
|
|
||||||
`hiddenimports` allein reicht nicht, da PyInstaller die `__init__.py` ins PYZ-Archiv packt, aber die `.pyd`-Extension separat im Dateisystem erwartet. Stattdessen `collect_all` verwenden:
|
|
||||||
```python
|
|
||||||
from PyInstaller.utils.hooks import collect_all
|
|
||||||
cx_datas, cx_binaries, cx_hiddenimports = collect_all('problematic_package')
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
# ...
|
|
||||||
binaries=cx_binaries,
|
|
||||||
datas=cx_datas,
|
|
||||||
hiddenimports=cx_hiddenimports,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Antivirus blockiert die .exe
|
|
||||||
|
|
||||||
**Ursache**: Unsigned executables werden oft als verdächtig eingestuft.
|
|
||||||
|
|
||||||
**Lösungen**:
|
|
||||||
1. Code-Signing-Zertifikat kaufen und verwenden
|
|
||||||
2. Bei Microsoft SmartScreen einreichen
|
|
||||||
3. Exception in Antivirus eintragen (für Tests)
|
|
||||||
|
|
||||||
### Executable ist zu groß (>200 MB)
|
|
||||||
|
|
||||||
**Lösungen**:
|
|
||||||
1. UPX-Kompression ist bereits aktiv
|
|
||||||
2. Ungenutzte Module excluden (siehe oben)
|
|
||||||
3. Virtual Environment aufräumen: `uv sync --no-dev`
|
|
||||||
|
|
||||||
### UI-Dateien nicht gefunden
|
|
||||||
|
|
||||||
**Problem**: `.ui` Dateien werden nicht gefunden.
|
|
||||||
|
|
||||||
**Lösung**: In `DocuMentor.spec` prüfen:
|
|
||||||
```python
|
|
||||||
datas=ui_files, # Muss gesetzt sein
|
|
||||||
```
|
|
||||||
|
|
||||||
Im Code müssen Ressource-Pfade PyInstaller-kompatibel aufgelöst werden:
|
|
||||||
```python
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
res_path = Path(sys._MEIPASS) / "res" / "data.sql"
|
|
||||||
else:
|
|
||||||
res_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automatisierung
|
|
||||||
|
|
||||||
### GitHub Actions (CI/CD)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Build Windows Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v1
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: uv run python build_windows.py
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: dist/*.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lokales Release-Skript
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# release.sh - Erstellt vollständiges Release
|
|
||||||
|
|
||||||
VERSION="0.1.0"
|
|
||||||
|
|
||||||
echo "Building DocuMentor v$VERSION..."
|
|
||||||
|
|
||||||
# 1. Build
|
|
||||||
uv run python build_windows.py
|
|
||||||
|
|
||||||
# 2. Installer (falls Inno Setup installiert)
|
|
||||||
if command -v iscc &> /dev/null; then
|
|
||||||
iscc installer.iss
|
|
||||||
echo "✓ Installer erstellt"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Release v$VERSION fertig!"
|
|
||||||
echo " • ZIP: dist/DocuMentor-*-Windows.zip"
|
|
||||||
echo " • Setup: dist/installer/DocuMentor-Setup-$VERSION.exe"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Distributions-Formate im Vergleich
|
|
||||||
|
|
||||||
| | ZIP | Setup.exe (Inno Setup) | MSI (WiX) |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Portabel** | Ja | Nein | Nein |
|
|
||||||
| **Installation nötig** | Nein | Ja | Ja |
|
|
||||||
| **Deinstallation** | Nein | Ja | Ja |
|
|
||||||
| **Start-Menü** | Nein | Ja | Ja |
|
|
||||||
| **GPO-Deployment** | Nein | Nein | Ja |
|
|
||||||
| **Silent Install** | — | Ja | Ja |
|
|
||||||
| **Rollback** | — | Nein | Ja |
|
|
||||||
| **Patch-Support** | — | Nein | Ja (.msp) |
|
|
||||||
|
|
||||||
## Externe Abhängigkeiten
|
|
||||||
|
|
||||||
DocuMentor benötigt diese Tools (NICHT im Installer enthalten):
|
|
||||||
|
|
||||||
1. **Java Runtime Environment (JRE) 11+** — Für Saxon und Apache FOP (https://adoptium.net/)
|
|
||||||
2. **Apache FOP** — XSL-FO zu PDF Konvertierung (https://xmlgraphics.apache.org/fop/)
|
|
||||||
3. **Saxon XSLT Prozessor** — XSLT 2.0/3.0 Transformationen (https://www.saxonica.com/)
|
|
||||||
4. **diff-pdf** — PDF-Vergleich (https://vslavik.github.io/diff-pdf/)
|
|
||||||
|
|
||||||
Nach der Installation müssen die Pfade zu diesen Tools in den DocuMentor-Einstellungen konfiguriert werden.
|
|
||||||
|
|
||||||
## Dokumentation für Endbenutzer
|
|
||||||
|
|
||||||
Die `dist/DocuMentor/README.txt` wird automatisch erstellt und enthält:
|
|
||||||
- Installationsanweisungen
|
|
||||||
- Liste der externen Abhängigkeiten
|
|
||||||
- Konfigurationshinweise
|
|
||||||
|
|
||||||
## Versionierung
|
|
||||||
|
|
||||||
Version in folgenden Dateien aktualisieren:
|
|
||||||
1. `pyproject.toml` — `version = "X.Y.Z"`
|
|
||||||
2. `installer.iss` — `#define MyAppVersion "X.Y.Z"`
|
|
||||||
|
|
||||||
Dann Versionsinformationen neu generieren:
|
|
||||||
```bash
|
|
||||||
uv run python create_version_info.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lizenz und Rechtliches
|
|
||||||
|
|
||||||
- PySide6 (LGPL): Dynamische Verlinkung ist OK
|
|
||||||
- Polars (MIT): Unproblematisch
|
|
||||||
- Pydantic (MIT): Unproblematisch
|
|
||||||
|
|
||||||
Externe Tools (Saxon, FOP) haben eigene Lizenzen und müssen separat installiert werden.
|
|
||||||
@@ -1,356 +1,169 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Spreche mit mir auf Deutsch! (Communicate with me in German!)
|
Spreche mit mir auf Deutsch! (Communicate with me in German!)
|
||||||
|
|
||||||
## Projektübersicht
|
## Projektübersicht
|
||||||
|
|
||||||
DocuMentor (ehemals xsl-validator) ist eine PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit XML-Dateien. Sie bietet eine GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung von PDF-Generierungsprojekten mit PostgreSQL-Datenbankintegration.
|
DocuMentor (ehemals xsl-validator) ist eine PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit XML-Dateien. Sie bietet eine GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung von PDF-Generierungsprojekten mit PostgreSQL-Datenbankintegration.
|
||||||
|
|
||||||
## Anvisiertes Nutzungsszenario
|
## PySide6-GUI
|
||||||
Der primäre Einsatz ist die kontinuierliche Weiterentwicklung von PDF-Dokumenten in Flexnow (Software zur Prüfungsverwaltung). Dabei handelt es sich beispielsweise um amtliche Urkunden, Zeugnisse und Bescheide.
|
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden
|
||||||
|
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten
|
||||||
Die Basis bilden etwa 100 XSL-Dateien. Die meisten sind mittels `<xsl:import/>` bzw. `<xsl:include/>` miteinander verknüpft (ähnlich der Klassen-Vererbung). Daher können sich Änderungen in einer XSL-Datei auf (unerwartet) viele andere auswirken. Um diese Auswirkungen im Auge zu behalten, wird DocuMentor entwickelt.
|
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt
|
||||||
|
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden
|
||||||
**Typischer Workflow:**
|
|
||||||
1. Entwickler führt benötigte Änderungen an den XSL-Dateien durch
|
## Entwicklungskommandos
|
||||||
2. Entwickler startet die Transformation im DocuMentor und begutachtet die generierte PDF-Diff
|
|
||||||
3. Prüfung: Wurden die richtigen PDF-Dateien geändert?
|
### Paketverwaltung
|
||||||
4. Prüfung: Hat die Änderung der XSL-Dateien die erhoffte Änderung in den PDF-Dateien ergeben?
|
Dieses Projekt verwendet den `uv` Paketmanager (nicht pip oder poetry):
|
||||||
|
```bash
|
||||||
Diese Schritte können sich mehrfach wiederholen.
|
uv sync # Abhängigkeiten installieren
|
||||||
|
uv run python src/main.py # Anwendung starten
|
||||||
Da der DocuMentor permanent im Hintergrund läuft, ist ein sparsamer Umgang mit RAM wichtig:
|
uv run python test_hash_implementation.py # Hash-Tests ausführen
|
||||||
- Worker-Pools nach Verwendung herunterfahren
|
```
|
||||||
- Große Datenstrukturen frühzeitig freigeben
|
|
||||||
- Polars DataFrames statt Pandas (geringerer RAM-Verbrauch)
|
### Linting
|
||||||
- Lazy Loading wo möglich
|
```bash
|
||||||
|
uv run ruff check # Code-Style prüfen (Zeilenlänge: 120)
|
||||||
## Entwicklungskommandos
|
uv run ruff format # Code formatieren
|
||||||
|
```
|
||||||
### Paketverwaltung
|
|
||||||
Dieses Projekt verwendet den `uv` Paketmanager (nicht pip oder poetry):
|
## Architektur
|
||||||
```bash
|
|
||||||
uv sync # Abhängigkeiten installieren
|
### Konfigurationssystem (src/conf.py)
|
||||||
uv run python src/main.py # Anwendung starten
|
|
||||||
```
|
Die Anwendung verwendet ein zentralisiertes Konfigurationsmodell mit Pydantic:
|
||||||
|
|
||||||
### Linting
|
- **AppSettings**: Globales Singleton (`app_settings`), das die gesamte Anwendungskonfiguration speichert
|
||||||
```bash
|
- Wird an plattformspezifischen Orten gespeichert:
|
||||||
uv run ruff check # Code-Style prüfen (Zeilenlänge: 120)
|
- Linux: `~/.config/DocuMentor/config.json`
|
||||||
uv run ruff format # Code formatieren
|
- Windows: `%APPDATA%\DocuMentor\config.json`
|
||||||
```
|
- macOS: `~/Library/Application Support/DocuMentor/config.json`
|
||||||
|
- Enthält Listen von Tools: `java_vms`, `saxon_jars`, `apache_fops`, `diff_pdfs`, `xsl_dirs`, `postgresql_dbs`
|
||||||
### Tests
|
|
||||||
Dieses Projekt verwendet KEINE pytest/unittest-Frameworks. Tests sind standalone Python-Skripte:
|
- **ProjectData**: Projektspezifische Einstellungen, die in `project.yaml` im jeweiligen Projektverzeichnis gespeichert werden
|
||||||
```bash
|
- Enthält hierarchische Baumstruktur von Transformationsknoten
|
||||||
uv run python test_hash_implementation.py # Hash-Tests
|
- Verwendet `TreeNode` und `XslFile` zur Organisation
|
||||||
uv run python test_xml_hash_duplicate_detection.py # Duplikatserkennung
|
- Jede `XmlFile` hat eine optionale `hashsum` (blake2b) zur Änderungsverfolgung
|
||||||
```
|
|
||||||
|
### Wichtige Datenmodelle
|
||||||
### Commit
|
|
||||||
Jedes mal bei Commit diese Skills nutzen:
|
1. **Tool-Konfigurationsmodelle** (JavaVm, SaxonJar, ApacheFop, DiffPdf, XslDir, PostgreSqlDb):
|
||||||
|
- Jedes hat eine `id` und `version`
|
||||||
- /license-check
|
- Speichert Pfade zu Binärdateien/Verzeichnissen
|
||||||
- /version-bump
|
|
||||||
|
2. **Project-Modell**:
|
||||||
## Code-Style-Richtlinien
|
- Referenziert Tool-Konfigurationen über ID
|
||||||
|
- Verlinkt zu einem Projektverzeichnis mit `project.yaml`
|
||||||
### Import-Organisation
|
- Hat Hilfsmethoden wie `getXsl()`, `getJavaVm()` um IDs in Namen/Versionen aufzulösen
|
||||||
|
|
||||||
Reihenfolge (keine Leerzeilen zwischen Gruppen):
|
3. **Baumstruktur** (TreeNode → XslFile → XmlFile):
|
||||||
```python
|
- Hierarchische Organisation von Transformations-Workflows
|
||||||
# 1. Standard Library
|
- `TreeNode`: Organisationseinheit mit `xslt_params` und Kindknoten/-dateien
|
||||||
import os
|
- `XslFile`: XSL-Stylesheet mit zugehörigen XML-Dateien und XSLT-Parametern
|
||||||
import sys
|
- `XmlFile`: XML-Eingabedatei mit optionalem blake2b-Hash
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
### UI-Architektur (src/ui/)
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
Die Anwendung folgt einem spezifischen PySide6-Muster:
|
||||||
# 2. Drittanbieter
|
|
||||||
from PySide6.QtCore import Qt, QThread, Signal
|
1. **UI-Definitionsdateien** (`*_ui.py`): Automatisch generiert aus UI-Designer-Dateien
|
||||||
from PySide6.QtWidgets import QDialog, QMainWindow
|
- Diese Dateien definieren die UI-Struktur als Klassen (z.B. `Ui_MainWindow`)
|
||||||
from pydantic import BaseModel, Field
|
- Sollten NICHT manuell bearbeitet werden
|
||||||
|
|
||||||
# 3. Lokale Imports (IMMER absolute Imports, KEINE relativen .imports)
|
2. **Implementierungsdateien** (ohne `_ui` Suffix): Tatsächliche Dialog-/Fenster-Implementierungen
|
||||||
from conf import app_settings, TreeNode, XslFile
|
- Importieren und verwenden die entsprechende `*_ui.py` Datei
|
||||||
from ui.MainWindow import MainWindow
|
- Enthalten Business-Logik und Signal/Slot-Verbindungen
|
||||||
```
|
- Beispiel: `MainWindow.py` verwendet `Ui_MainWindow` aus `MainWinddow_ui.py`
|
||||||
|
|
||||||
- `TYPE_CHECKING` für zirkuläre Import-Vermeidung nutzen
|
Beim Erstellen neuer Dialoge:
|
||||||
- Keine relativen Imports (`.` oder `..`)
|
- Immer zuerst eine entsprechende UI-Datei erstellen
|
||||||
|
- Die UI-Datei wird automatisch als `.py`-Datei von einer VS Code Extension generiert
|
||||||
### Type Annotations
|
- Die generierte UI-Klasse in der Implementierungsdatei importieren und verwenden
|
||||||
|
|
||||||
Moderne Union-Syntax verwenden:
|
### Hauptfenster (src/ui/MainWindow.py)
|
||||||
```python
|
|
||||||
# RICHTIG
|
Zentrale Schaltstelle der Anwendung mit mehreren wichtigen Verantwortlichkeiten:
|
||||||
def transform(xml_path: Path, params: dict[str, str]) -> tuple[bool, str]:
|
|
||||||
result: str | None = None
|
1. **Projektverwaltung**:
|
||||||
files: list[Path] = []
|
- Öffnet und verwaltet PDF-Transformationsprojekte
|
||||||
|
- Lädt/speichert `ProjectData` aus `project.yaml` Dateien
|
||||||
# FALSCH
|
|
||||||
def transform(xml_path, params): # Keine Annotations
|
2. **Tree Widget**: Zeigt hierarchische Struktur von Transformationsknoten an
|
||||||
result: Optional[str] = None # Alte Union-Syntax
|
- Kontextmenüs zum Hinzufügen/Bearbeiten/Löschen von Knoten, XSL-Dateien und XML-Dateien
|
||||||
files: List[Path] = [] # Großgeschriebene Types
|
- Drag-and-Drop-Unterstützung für XML-Dateien
|
||||||
```
|
|
||||||
|
3. **PDF-Vergleichsansicht**:
|
||||||
### Naming Conventions
|
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
|
||||||
|
- Alpha-Blending für visuellen Vergleich
|
||||||
```python
|
- Zoom- und Pan-Funktionalität
|
||||||
# Klassen: PascalCase
|
|
||||||
class SaxonWorkerPool:
|
4. **Asynchrone Operationen**:
|
||||||
|
- `XmlHashCalculatorThread`: Hintergrund-blake2b-Hash-Berechnung für XML-Dateien
|
||||||
# Funktionen/Methoden: snake_case
|
- `DatabaseTestThread` (in PostgreSqlConfigDialog): Asynchrones Testen von Datenbankverbindungen
|
||||||
def transform_saxon(xml_file: Path) -> bool:
|
|
||||||
|
### Hash-Berechnungssystem
|
||||||
# Private Methoden: _snake_case mit Unterstrich
|
|
||||||
def _create_tree_item(self, node: TreeNode):
|
Die Anwendung verwendet blake2b-Hashing zur Verfolgung von XML-Dateiänderungen:
|
||||||
|
|
||||||
# Konstanten: UPPER_CASE
|
- **Automatisch**: Hashes werden berechnet, wenn Projekte geladen werden (nur für Dateien ohne existierenden Hash)
|
||||||
SAXON_WORKER_JAVA = """..."""
|
- **Asynchron**: Hintergrund-Thread (`XmlHashCalculatorThread`) um die UI reaktionsfähig zu halten
|
||||||
```
|
- **Format**: `blake2b:<64-Zeichen-Hexdigest>`
|
||||||
|
- **Speicherung**: Persistiert in `project.yaml` innerhalb jedes `XmlFile`-Objekts
|
||||||
### Formatierung
|
- **Details**: Siehe `docs/blake2b_hash_implementation.md`
|
||||||
- **Zeilenlänge:** 120 Zeichen (via Ruff konfiguriert)
|
|
||||||
- **Strings:** Bevorzugt Double-Quotes `"..."`, aber konsistent im File
|
### Theme-System
|
||||||
- **Trailing Commas:** Bei mehrzeiligen Strukturen verwenden
|
|
||||||
|
Die Anwendung unterstützt mehrere Qt-Themes:
|
||||||
### Error Handling
|
- Theme-Auswahlmenü wird dynamisch aus `QStyleFactory.keys()` befüllt
|
||||||
|
- Theme-Präferenz wird in `AppSettings.theme` gespeichert
|
||||||
IMMER Logging statt `print()` verwenden:
|
- Dark-Theme-Unterstützung via `qdarktheme` Paket (aktuell in main.py auskommentiert)
|
||||||
```python
|
|
||||||
import logging
|
### Datenbankintegration
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
PostgreSQL-Integration mit Polars und ConnectorX:
|
||||||
def transform(xml_path: Path) -> tuple[bool, str]:
|
- Konfiguration wird im `PostgreSqlDb`-Modell mit SSL-Modus-Unterstützung gespeichert
|
||||||
try:
|
- SQL-Abfragen werden via `_execute_sql_query()` im MainWindow ausgeführt
|
||||||
logger.info(f"Transformation gestartet: {xml_path}")
|
- Ergebnisse werden in Polars DataFrames geladen
|
||||||
result = do_transform(xml_path)
|
|
||||||
return True, "Erfolg"
|
## Wichtige Konventionen
|
||||||
except FileNotFoundError as e:
|
|
||||||
error_msg = f"XML-Datei nicht gefunden: {xml_path}"
|
### Deutsche Sprache
|
||||||
logger.error(error_msg)
|
Die Codebasis verwendet Deutsch für:
|
||||||
return False, error_msg
|
- UI-Texte und Labels
|
||||||
except Exception as e:
|
- Kommentare und Dokumentation
|
||||||
error_msg = f"Fehler bei Transformation: {str(e)}"
|
- Variablennamen wo kontextuell passend
|
||||||
logger.exception(error_msg) # Mit Stack Trace
|
- Log-Meldungen
|
||||||
return False, error_msg
|
|
||||||
```
|
### Pfadbehandlung
|
||||||
|
- Immer `pathlib.Path`-Objekte verwenden, keine Strings
|
||||||
- `logger.debug()` für Debugging-Infos
|
- `expanduser()` und `expandvars()` für Benutzer-/Umgebungspfade verwenden
|
||||||
- `logger.info()` für normale Operationen
|
- Projektrelative Pfade werden als relativ gespeichert, zur Laufzeit gegen `project_dir` aufgelöst
|
||||||
- `logger.warning()` für Warnungen
|
|
||||||
- `logger.error()` für Fehler ohne Stack Trace
|
### ID-basierte Lookups
|
||||||
- `logger.exception()` für Fehler MIT Stack Trace
|
Konfigurationsentitäten (Tools, Datenbanken) werden in Projekten über ID referenziert. Die Hilfsmethoden des `Project`-Modells (`getXsl()`, `getJavaVm()`, etc.) verwenden, um IDs in Anzeigewerte aufzulösen.
|
||||||
- Fehlermeldungen auf Deutsch
|
|
||||||
|
### Einstellungspersistenz
|
||||||
### Docstrings
|
- Globale Einstellungen: `app_settings.save()` nach Änderungen aufrufen
|
||||||
|
- Projekteinstellungen: `project_data.writeSettings(project_dir)` nach Änderungen aufrufen
|
||||||
Google-Style auf Deutsch:
|
|
||||||
```python
|
## Arbeiten mit der Codebasis
|
||||||
def transform_xml_to_pdf(xml_path: Path, xsl_path: Path, output_dir: Path) -> tuple[bool, str]:
|
|
||||||
"""
|
### Neue Tool-Konfigurationen hinzufügen
|
||||||
Transformiert eine XML-Datei mit XSL zu PDF.
|
1. Modell zu `conf.py` hinzufügen (ähnlich wie `JavaVm`, `SaxonJar`)
|
||||||
|
2. Listenfeld zu `AppSettings` hinzufügen
|
||||||
Args:
|
3. Konfigurationsdialog in `src/ui/` erstellen (UI-Datei + Implementierung)
|
||||||
xml_path: Pfad zur XML-Eingabedatei
|
4. Zu `AppSettings.py` Tabs hinzufügen
|
||||||
xsl_path: Pfad zum XSL-Stylesheet
|
5. `Project`-Modell aktualisieren, falls das Tool projektspezifisch sein soll
|
||||||
output_dir: Zielverzeichnis für PDF-Ausgabe
|
|
||||||
|
### Neue Baumoperationen hinzufügen
|
||||||
Returns:
|
1. Aktion zum Kontextmenü in `_create_context_menu_for_type()` hinzufügen
|
||||||
tuple[bool, str]: (Erfolg, Fehlermeldung oder Info-Text)
|
2. Handler-Methode implementieren nach Namensschema `_action_tree_node()`, `_action_xsl_file()`, etc.
|
||||||
|
3. Baum nach Änderungen mit `_load_nodes_to_tree()` aktualisieren
|
||||||
Raises:
|
4. `self.project_data.writeSettings(self.project.project_dir)` aufrufen um Änderungen zu persistieren
|
||||||
FileNotFoundError: Wenn XML- oder XSL-Datei nicht existiert
|
|
||||||
"""
|
### Projektstruktur modifizieren
|
||||||
```
|
Das `ProjectData`-Modell ist die Quelle der Wahrheit. Alle Änderungen an der Baumstruktur müssen:
|
||||||
|
1. Die `project_data.nodes` Liste modifizieren
|
||||||
### Pfadbehandlung
|
2. `project_data.writeSettings()` aufrufen um zu persistieren
|
||||||
- Immer `pathlib.Path`-Objekte verwenden, keine Strings
|
3. Baum mit `_load_nodes_to_tree()` neu laden um Änderungen in der UI zu reflektieren
|
||||||
- `expanduser()` und `expandvars()` für Benutzer-/Umgebungspfade verwenden
|
|
||||||
- Projektrelative Pfade werden als relativ gespeichert, zur Laufzeit gegen `project_dir` aufgelöst
|
|
||||||
|
|
||||||
## Architektur
|
|
||||||
|
|
||||||
### Konfigurationssystem (src/conf.py)
|
|
||||||
|
|
||||||
Die Anwendung verwendet ein zentralisiertes Konfigurationsmodell mit Pydantic:
|
|
||||||
|
|
||||||
- **AppSettings**: Globales Singleton (`app_settings`), das die gesamte Anwendungskonfiguration speichert
|
|
||||||
- Wird an plattformspezifischen Orten gespeichert:
|
|
||||||
- Linux: `~/.config/DocuMentor/config.json`
|
|
||||||
- Windows: `%APPDATA%\DocuMentor\config.json`
|
|
||||||
- macOS: `~/Library/Application Support/DocuMentor/config.json`
|
|
||||||
- Enthält Listen von Tools: `java_vms`, `saxon_jars`, `apache_fops`, `diff_pdfs`, `xsl_dirs`, `postgresql_dbs`
|
|
||||||
|
|
||||||
- **ProjectData**: Projektspezifische Einstellungen, die in `project.yaml` im jeweiligen Projektverzeichnis gespeichert werden
|
|
||||||
- Enthält hierarchische Baumstruktur von Transformationsknoten
|
|
||||||
- Verwendet `TreeNode` und `XslFile` zur Organisation
|
|
||||||
- Jede `XmlFile` hat eine optionale `hashsum` (blake2b) zur Änderungsverfolgung
|
|
||||||
|
|
||||||
### Wichtige Datenmodelle
|
|
||||||
|
|
||||||
1. **Tool-Konfigurationsmodelle** (JavaVm, SaxonJar, ApacheFop, DiffPdf, XslDir, PostgreSqlDb):
|
|
||||||
- Jedes hat eine `id` und `version`
|
|
||||||
- Speichert Pfade zu Binärdateien/Verzeichnissen
|
|
||||||
|
|
||||||
2. **Project-Modell**:
|
|
||||||
- Referenziert Tool-Konfigurationen über ID
|
|
||||||
- Verlinkt zu einem Projektverzeichnis mit `project.yaml`
|
|
||||||
- Hat Hilfsmethoden wie `getXsl()`, `getJavaVm()` um IDs in Namen/Versionen aufzulösen
|
|
||||||
|
|
||||||
3. **Baumstruktur** (TreeNode → XslFile → XmlFile):
|
|
||||||
- Hierarchische Organisation von Transformations-Workflows
|
|
||||||
- `TreeNode`: Organisationseinheit mit `xslt_params` und Kindknoten/-dateien
|
|
||||||
- `XslFile`: XSL-Stylesheet mit zugehörigen XML-Dateien und XSLT-Parametern
|
|
||||||
- `XmlFile`: XML-Eingabedatei mit optionalem blake2b-Hash
|
|
||||||
|
|
||||||
### UI-Architektur (src/ui/)
|
|
||||||
|
|
||||||
Die Anwendung folgt einem spezifischen PySide6-Muster:
|
|
||||||
|
|
||||||
1. **UI-Definitionsdateien** (`*_ui.py`): Automatisch generiert aus UI-Designer-Dateien
|
|
||||||
- Diese Dateien definieren die UI-Struktur als Klassen (z.B. `Ui_MainWindow`)
|
|
||||||
- Sollten NICHT manuell bearbeitet werden
|
|
||||||
|
|
||||||
2. **Implementierungsdateien** (ohne `_ui` Suffix): Tatsächliche Dialog-/Fenster-Implementierungen
|
|
||||||
- Importieren und verwenden die entsprechende `*_ui.py` Datei
|
|
||||||
- Enthalten Business-Logik und Signal/Slot-Verbindungen
|
|
||||||
- Beispiel: `MainWindow.py` verwendet `Ui_MainWindow` aus `MainWinddow_ui.py`
|
|
||||||
|
|
||||||
Beim Erstellen neuer Dialoge:
|
|
||||||
- Immer zuerst eine entsprechende UI-Datei erstellen
|
|
||||||
- Die UI-Datei wird automatisch als `.py`-Datei von einer VS Code Extension generiert
|
|
||||||
- Die generierte UI-Klasse in der Implementierungsdatei importieren und verwenden
|
|
||||||
|
|
||||||
**UI-Import-Pattern:**
|
|
||||||
```python
|
|
||||||
from PySide6.QtWidgets import QDialog
|
|
||||||
from ui.JavaVmConfigDialog_ui import Ui_JavaVmConfigDialog
|
|
||||||
|
|
||||||
class JavaVmConfigDialog(QDialog):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.ui = Ui_JavaVmConfigDialog()
|
|
||||||
self.ui.setupUi(self)
|
|
||||||
# Signale NACH setupUi() verbinden
|
|
||||||
self.ui.browseButton.clicked.connect(self._browse_file)
|
|
||||||
```
|
|
||||||
|
|
||||||
- UI-Klassen NIEMALS direkt erben, nur als `self.ui` Member
|
|
||||||
- Alle Widgets über `self.ui.widgetName` zugreifen
|
|
||||||
- Signal-Verbindungen immer NACH `setupUi()` aufrufen
|
|
||||||
|
|
||||||
### Hauptfenster (src/ui/MainWindow.py)
|
|
||||||
|
|
||||||
Zentrale Schaltstelle der Anwendung mit mehreren wichtigen Verantwortlichkeiten:
|
|
||||||
|
|
||||||
1. **Projektverwaltung**:
|
|
||||||
- Öffnet und verwaltet PDF-Transformationsprojekte
|
|
||||||
- Lädt/speichert `ProjectData` aus `project.yaml` Dateien
|
|
||||||
|
|
||||||
2. **Tree Widget**: Zeigt hierarchische Struktur von Transformationsknoten an
|
|
||||||
- Kontextmenüs zum Hinzufügen/Bearbeiten/Löschen von Knoten, XSL-Dateien und XML-Dateien
|
|
||||||
- Drag-and-Drop-Unterstützung für XML-Dateien
|
|
||||||
|
|
||||||
3. **PDF-Vergleichsansicht**:
|
|
||||||
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
|
|
||||||
- Alpha-Blending für visuellen Vergleich
|
|
||||||
- Zoom- und Pan-Funktionalität
|
|
||||||
|
|
||||||
4. **Asynchrone Operationen**:
|
|
||||||
- `XmlHashCalculatorThread`: Hintergrund-blake2b-Hash-Berechnung für XML-Dateien
|
|
||||||
- `DatabaseTestThread` (in PostgreSqlConfigDialog): Asynchrones Testen von Datenbankverbindungen
|
|
||||||
|
|
||||||
### XSL-Abhängigkeitsgraph (src/ui/XslDependencyDialog.py)
|
|
||||||
|
|
||||||
Interaktiver Dialog zur Visualisierung von `<xsl:import/>`- und `<xsl:include/>`-Abhängigkeiten zwischen XSL-Dateien:
|
|
||||||
- Sidebar mit Suchfilter zur Navigation
|
|
||||||
- Abhängigkeitsgraph-Darstellung via vis.js
|
|
||||||
- Parsing der XSL-Dateien mit lxml
|
|
||||||
|
|
||||||
### Hash-Berechnungssystem
|
|
||||||
|
|
||||||
Die Anwendung verwendet blake2b-Hashing zur Verfolgung von XML-Dateiänderungen:
|
|
||||||
|
|
||||||
- **Automatisch**: Hashes werden berechnet, wenn Projekte geladen werden (nur für Dateien ohne existierenden Hash)
|
|
||||||
- **Asynchron**: Hintergrund-Thread (`XmlHashCalculatorThread`) um die UI reaktionsfähig zu halten
|
|
||||||
- **Format**: `blake2b:<64-Zeichen-Hexdigest>`
|
|
||||||
- **Speicherung**: Persistiert in `project.yaml` innerhalb jedes `XmlFile`-Objekts
|
|
||||||
- **Details**: Siehe `docs/blake2b_hash_implementation.md`
|
|
||||||
|
|
||||||
### Theme-System
|
|
||||||
|
|
||||||
Die Anwendung unterstützt mehrere Qt-Themes:
|
|
||||||
- Theme-Auswahlmenü wird dynamisch aus `QStyleFactory.keys()` befüllt
|
|
||||||
- Theme-Präferenz wird in `AppSettings.theme` gespeichert
|
|
||||||
|
|
||||||
### Datenbankintegration
|
|
||||||
|
|
||||||
PostgreSQL-Integration mit Polars und ConnectorX:
|
|
||||||
- Konfiguration wird im `PostgreSqlDb`-Modell mit SSL-Modus-Unterstützung gespeichert
|
|
||||||
- SQL-Abfragen werden asynchron via `DatabaseQueryThread` im `DatabaseMixin` ausgeführt
|
|
||||||
- Ergebnisse werden in Polars DataFrames geladen
|
|
||||||
|
|
||||||
### Thread-basierte Operationen
|
|
||||||
|
|
||||||
```python
|
|
||||||
from PySide6.QtCore import QThread, Signal
|
|
||||||
|
|
||||||
class HashCalculatorThread(QThread):
|
|
||||||
progress = Signal(int)
|
|
||||||
finished = Signal(dict)
|
|
||||||
|
|
||||||
def __init__(self, files: list[Path]):
|
|
||||||
super().__init__()
|
|
||||||
self.files = files
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
for i, file_path in enumerate(self.files):
|
|
||||||
hash_value = calculate_hash(file_path)
|
|
||||||
self.progress.emit(i + 1)
|
|
||||||
self.finished.emit(results)
|
|
||||||
|
|
||||||
# Verwendung
|
|
||||||
thread = HashCalculatorThread(xml_files)
|
|
||||||
thread.progress.connect(self._on_progress)
|
|
||||||
thread.finished.connect(self._on_finished)
|
|
||||||
thread.start() # NICHT run() direkt aufrufen!
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wichtige Konventionen
|
|
||||||
|
|
||||||
### Deutsche Sprache
|
|
||||||
Die Codebasis verwendet Deutsch für:
|
|
||||||
- UI-Texte und Labels
|
|
||||||
- Kommentare und Dokumentation
|
|
||||||
- Variablennamen wo kontextuell passend
|
|
||||||
- Log-Meldungen
|
|
||||||
|
|
||||||
### ID-basierte Lookups
|
|
||||||
Konfigurationsentitäten (Tools, Datenbanken) werden in Projekten über ID referenziert. Die Hilfsmethoden des `Project`-Modells (`getXsl()`, `getJavaVm()`, etc.) verwenden, um IDs in Anzeigewerte aufzulösen.
|
|
||||||
|
|
||||||
### Einstellungspersistenz
|
|
||||||
- Globale Einstellungen: `app_settings.save()` nach Änderungen aufrufen
|
|
||||||
- Projekteinstellungen: `project_data.writeSettings(project_dir)` nach Änderungen aufrufen
|
|
||||||
|
|
||||||
## Arbeiten mit der Codebasis
|
|
||||||
|
|
||||||
### Neue Tool-Konfigurationen hinzufügen
|
|
||||||
1. Modell zu `conf.py` hinzufügen (ähnlich wie `JavaVm`, `SaxonJar`)
|
|
||||||
2. Listenfeld zu `AppSettings` hinzufügen
|
|
||||||
3. Konfigurationsdialog in `src/ui/` erstellen (UI-Datei + Implementierung)
|
|
||||||
4. Zu `AppSettings.py` Tabs hinzufügen
|
|
||||||
5. `Project`-Modell aktualisieren, falls das Tool projektspezifisch sein soll
|
|
||||||
|
|
||||||
### Neue Baumoperationen hinzufügen
|
|
||||||
1. Aktion zum Kontextmenü in `_create_context_menu_for_type()` hinzufügen
|
|
||||||
2. Handler-Methode implementieren nach Namensschema `_action_tree_node()`, `_action_xsl_file()`, etc.
|
|
||||||
3. Baum nach Änderungen mit `_load_nodes_to_tree()` aktualisieren
|
|
||||||
4. `self.project_data.writeSettings(self.project.project_dir)` aufrufen um Änderungen zu persistieren
|
|
||||||
|
|
||||||
### Projektstruktur modifizieren
|
|
||||||
Das `ProjectData`-Modell ist die Quelle der Wahrheit. Alle Änderungen an der Baumstruktur müssen:
|
|
||||||
1. Die `project_data.nodes` Liste modifizieren
|
|
||||||
2. `project_data.writeSettings()` aufrufen um zu persistieren
|
|
||||||
3. Baum mit `_load_nodes_to_tree()` neu laden um Änderungen in der UI zu reflektieren
|
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
PyInstaller Konfiguration für DocuMentor
|
|
||||||
Erstellt eine eigenständige Windows-Executable ohne Python-Installation
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from PyInstaller.utils.hooks import collect_all
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
# Projektpfad
|
|
||||||
project_root = Path(SPECPATH)
|
|
||||||
src_path = project_root / 'src'
|
|
||||||
|
|
||||||
# connectorx komplett sammeln (Python-Code, native .pyd und Metadaten)
|
|
||||||
# PyInstaller erkennt connectorx nicht automatisch, da es zur Laufzeit
|
|
||||||
# von polars per importlib.import_module() geladen wird
|
|
||||||
cx_datas, cx_binaries, cx_hiddenimports = collect_all('connectorx')
|
|
||||||
|
|
||||||
# Alle UI-Dateien sammeln
|
|
||||||
ui_files = []
|
|
||||||
for ui_file in (src_path / 'ui').glob('*.ui'):
|
|
||||||
ui_files.append((str(ui_file), 'ui'))
|
|
||||||
|
|
||||||
# Ressource-Dateien (SQL, CSV) einbinden
|
|
||||||
res_files = []
|
|
||||||
for res_file in (src_path / 'res').glob('*'):
|
|
||||||
res_files.append((str(res_file), 'res'))
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
[str(src_path / 'main.py')],
|
|
||||||
pathex=[str(src_path)],
|
|
||||||
binaries=cx_binaries,
|
|
||||||
datas=ui_files + res_files + cx_datas + [
|
|
||||||
(str(project_root / 'pyproject.toml'), '.'),
|
|
||||||
(str(project_root / 'THIRD_PARTY_LICENSES.txt'), '.'),
|
|
||||||
(str(project_root / 'resources' / 'icon.ico'), 'resources'),
|
|
||||||
],
|
|
||||||
hiddenimports=[
|
|
||||||
'PySide6.QtCore',
|
|
||||||
'PySide6.QtGui',
|
|
||||||
'PySide6.QtWidgets',
|
|
||||||
'pydantic',
|
|
||||||
'pydantic_settings',
|
|
||||||
'pydantic_yaml',
|
|
||||||
'polars',
|
|
||||||
'pyarrow',
|
|
||||||
] + cx_hiddenimports,
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
win_no_prefer_redirects=False,
|
|
||||||
win_private_assemblies=False,
|
|
||||||
cipher=block_cipher,
|
|
||||||
noarchive=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|
||||||
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
[],
|
|
||||||
exclude_binaries=True,
|
|
||||||
name='DocuMentor',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
console=False, # Keine Konsole anzeigen (GUI-Anwendung)
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
icon=str(project_root / 'resources' / 'icon.ico') if (project_root / 'resources' / 'icon.ico').exists() else None,
|
|
||||||
version=str(project_root / 'version_info.txt') if (project_root / 'version_info.txt').exists() else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
coll = COLLECT(
|
|
||||||
exe,
|
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
name='DocuMentor',
|
|
||||||
)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
|
||||||
|
|
||||||
<!-- Paket-Definition (ersetzt Product in v4) -->
|
|
||||||
<Package
|
|
||||||
Name="DocuMentor"
|
|
||||||
Version="1.6.4"
|
|
||||||
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
|
||||||
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
|
||||||
Language="1031"
|
|
||||||
Compressed="yes"
|
|
||||||
InstallerVersion="500">
|
|
||||||
|
|
||||||
<MajorUpgrade
|
|
||||||
DowngradeErrorMessage="Eine neuere Version ist bereits installiert."
|
|
||||||
AllowSameVersionUpgrades="yes" />
|
|
||||||
|
|
||||||
<!-- Media Template -->
|
|
||||||
<MediaTemplate EmbedCab="yes" />
|
|
||||||
|
|
||||||
<!-- Feature-Definition -->
|
|
||||||
<Feature Id="ProductFeature" Title="DocuMentor" Level="1">
|
|
||||||
<ComponentGroupRef Id="ProductComponents" />
|
|
||||||
<ComponentRef Id="ApplicationShortcut" />
|
|
||||||
<ComponentRef Id="DesktopShortcut" />
|
|
||||||
</Feature>
|
|
||||||
|
|
||||||
<!-- Minimal UI (Standard Windows Installer Dialog) -->
|
|
||||||
|
|
||||||
<!-- Icon -->
|
|
||||||
<Icon Id="icon.ico" SourceFile="resources\icon.ico"/>
|
|
||||||
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
|
|
||||||
<Property Id="ARPHELPLINK" Value="https://github.com/IhrRepo/DocuMentor" />
|
|
||||||
</Package>
|
|
||||||
|
|
||||||
<!-- Fragment: Verzeichnisstruktur -->
|
|
||||||
<Fragment>
|
|
||||||
<StandardDirectory Id="ProgramFilesFolder">
|
|
||||||
<Directory Id="INSTALLFOLDER" Name="DocuMentor" />
|
|
||||||
</StandardDirectory>
|
|
||||||
|
|
||||||
<StandardDirectory Id="ProgramMenuFolder">
|
|
||||||
<Directory Id="ApplicationProgramsFolder" Name="DocuMentor"/>
|
|
||||||
</StandardDirectory>
|
|
||||||
|
|
||||||
<StandardDirectory Id="DesktopFolder" />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<!-- Fragment: Shortcuts -->
|
|
||||||
<Fragment>
|
|
||||||
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="A498B66C-726D-44AA-95F4-CB4FBDCEF26E">
|
|
||||||
<Shortcut
|
|
||||||
Id="ApplicationStartMenuShortcut"
|
|
||||||
Name="DocuMentor"
|
|
||||||
Description="XSL-Transformations-Verwaltung"
|
|
||||||
Target="[INSTALLFOLDER]DocuMentor.exe"
|
|
||||||
WorkingDirectory="INSTALLFOLDER"
|
|
||||||
Icon="icon.ico" />
|
|
||||||
<RemoveFolder Id="CleanUpShortCut" On="uninstall"/>
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCU"
|
|
||||||
Key="Software\DocuMentor"
|
|
||||||
Name="installed"
|
|
||||||
Type="integer"
|
|
||||||
Value="1"
|
|
||||||
KeyPath="yes"/>
|
|
||||||
</Component>
|
|
||||||
|
|
||||||
<Component Id="DesktopShortcut" Directory="DesktopFolder" Guid="B498B66C-726D-44AA-95F4-CB4FBDCEF26E">
|
|
||||||
<Shortcut
|
|
||||||
Id="DesktopShortcutId"
|
|
||||||
Name="DocuMentor"
|
|
||||||
Target="[INSTALLFOLDER]DocuMentor.exe"
|
|
||||||
WorkingDirectory="INSTALLFOLDER"
|
|
||||||
Icon="icon.ico" />
|
|
||||||
<RegistryValue
|
|
||||||
Root="HKCU"
|
|
||||||
Key="Software\DocuMentor"
|
|
||||||
Name="desktopShortcut"
|
|
||||||
Type="integer"
|
|
||||||
Value="1"
|
|
||||||
KeyPath="yes"/>
|
|
||||||
</Component>
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
</Wix>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 [Ihr Name / Your Name]
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
-260
@@ -1,260 +0,0 @@
|
|||||||
# Lizenzübersicht für DocuMentor
|
|
||||||
|
|
||||||
## Verwendete Bibliotheken und ihre Lizenzen
|
|
||||||
|
|
||||||
### Python-Abhängigkeiten (aus pyproject.toml)
|
|
||||||
|
|
||||||
| Bibliothek | Version | Lizenz | Einschränkungen |
|
|
||||||
|------------|---------|--------|-----------------|
|
|
||||||
| **PySide6** | ≥6.9.1 | **LGPL-3.0 ODER GPL-2.0 ODER GPL-3.0** | ⚠️ Copyleft-Lizenz, siehe unten |
|
|
||||||
| Pydantic | ≥2.9.1 | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| Pydantic-Settings | ≥2.9.1 | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| Pydantic-YAML | ≥1.5.1 | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| Polars | ≥1.31.0 | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| ConnectorX | (via Polars) | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| PyArrow | (via Polars) | Apache 2.0 | ✅ Keine Einschränkungen |
|
|
||||||
| pyqtdarktheme | ≥2.1.0 | MIT | ✅ Keine Einschränkungen |
|
|
||||||
| Ruff | ≥0.14.8 | MIT | ✅ Keine Einschränkungen (nur Dev) |
|
|
||||||
|
|
||||||
### Externe Tools (nicht in Python integriert)
|
|
||||||
|
|
||||||
| Tool | Lizenz | Verwendung | Einschränkungen |
|
|
||||||
|------|--------|------------|-----------------|
|
|
||||||
| **Saxon-HE** | Mozilla Public License 2.0 (MPL-2.0) | XSLT-Transformationen | ⚠️ Weak Copyleft |
|
|
||||||
| **Apache FOP** | Apache License 2.0 | PDF-Generierung | ✅ Keine Einschränkungen |
|
|
||||||
| diff-pdf | GPL-2.0 (vermutlich) | PDF-Vergleich | ⚠️ Nur externes Tool |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Kritische Lizenz: PySide6 (LGPL-3.0)
|
|
||||||
|
|
||||||
### Was bedeutet LGPL-3.0 für DocuMentor?
|
|
||||||
|
|
||||||
**LGPL (Lesser GNU Public License)** ist eine "schwächere" Version der GPL und wurde speziell für Bibliotheken entwickelt.
|
|
||||||
|
|
||||||
#### ✅ Was ist ERLAUBT:
|
|
||||||
- **Kommerzielle Nutzung** - Du kannst DocuMentor verkaufen
|
|
||||||
- **Proprietärer Code** - Dein Code muss NICHT Open Source sein
|
|
||||||
- **Private Nutzung** - Keine Verpflichtungen
|
|
||||||
- **Dynamisches Linking** - In Python automatisch gegeben (via pip/import)
|
|
||||||
|
|
||||||
#### ⚠️ Was ist ERFORDERLICH:
|
|
||||||
1. **LGPL-Lizenztext beilegen** - Du musst die LGPL-Lizenz mit verteilen
|
|
||||||
2. **Copyright-Hinweise** - Erwähne PySide6 und Qt in deiner Software
|
|
||||||
3. **Nutzern Bibliotheks-Austausch ermöglichen** - Bei Python automatisch erfüllt, da:
|
|
||||||
- Nutzer können `pip install pyside6==andere-version` ausführen
|
|
||||||
- Python-Module sind dynamisch geladen
|
|
||||||
4. **Änderungen an PySide6 veröffentlichen** - Falls du PySide6 selbst änderst (sehr unwahrscheinlich)
|
|
||||||
|
|
||||||
#### ❌ Was ist NICHT erforderlich:
|
|
||||||
- **Dein eigener Code** muss NICHT unter LGPL stehen
|
|
||||||
- **Dein Source Code** muss NICHT veröffentlicht werden
|
|
||||||
- **Deine Änderungen** an DocuMentor müssen NICHT Open Source sein
|
|
||||||
|
|
||||||
### Praktische Umsetzung für Python-Anwendungen
|
|
||||||
|
|
||||||
Da Python-Pakete über pip installiert werden und dynamisch importiert werden, sind die LGPL-Anforderungen bereits erfüllt:
|
|
||||||
- ✅ Dynamisches Linking durch `import PySide6`
|
|
||||||
- ✅ Nutzer können andere PySide6-Versionen installieren
|
|
||||||
- ✅ Keine statische Kompilierung in Binary
|
|
||||||
|
|
||||||
**Fazit:** Du kannst DocuMentor unter praktisch jeder Lizenz veröffentlichen, auch proprietär.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Empfohlene Lizenzen für DocuMentor
|
|
||||||
|
|
||||||
### Option 1: MIT License (EMPFOHLEN) ⭐
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ Einfachste und permissivste Lizenz
|
|
||||||
- ✅ Kompatibel mit LGPL und allen anderen verwendeten Lizenzen
|
|
||||||
- ✅ Erlaubt kommerzielle Nutzung ohne Einschränkungen
|
|
||||||
- ✅ Kurz und leicht verständlich
|
|
||||||
- ✅ Sehr verbreitet in der Open-Source-Community
|
|
||||||
- ✅ Gleiche Lizenz wie die meisten Dependencies (Pydantic, Polars, etc.)
|
|
||||||
|
|
||||||
**Nachteile:**
|
|
||||||
- Kein Patent-Schutz
|
|
||||||
- Keine Copyleft-Schutz (andere können proprietäre Forks erstellen)
|
|
||||||
|
|
||||||
**Wann verwenden:**
|
|
||||||
Wenn du maximale Freiheit für Nutzer möchtest und Open Source fördern willst, ohne strenge Bedingungen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Option 2: Apache License 2.0
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ Kompatibel mit allen Dependencies
|
|
||||||
- ✅ Expliziter Patent-Schutz
|
|
||||||
- ✅ Erlaubt kommerzielle Nutzung
|
|
||||||
- ✅ Professioneller für größere Projekte
|
|
||||||
- ✅ Gleiche Lizenz wie Apache FOP
|
|
||||||
|
|
||||||
**Nachteile:**
|
|
||||||
- Etwas komplexer als MIT
|
|
||||||
- Erfordert NOTICE-Datei für Änderungen
|
|
||||||
|
|
||||||
**Wann verwenden:**
|
|
||||||
Wenn du Patent-Schutz möchtest und ein professionelleres Image für Enterprise-Nutzer brauchst.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Option 3: GPL-3.0 (Copyleft)
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ Kompatibel mit PySide6 (gleiche Lizenz)
|
|
||||||
- ✅ Starker Copyleft-Schutz - Alle Derivate müssen Open Source sein
|
|
||||||
- ✅ Schützt vor proprietären Forks
|
|
||||||
- ✅ Für reine Open-Source-Projekte ideal
|
|
||||||
|
|
||||||
**Nachteile:**
|
|
||||||
- ❌ Strenge Copyleft-Anforderungen
|
|
||||||
- ❌ Nutzer können DocuMentor nicht in proprietäre Software integrieren
|
|
||||||
- ❌ Weniger flexibel für kommerzielle Nutzung
|
|
||||||
|
|
||||||
**Wann verwenden:**
|
|
||||||
Wenn du sicherstellen willst, dass alle Modifikationen Open Source bleiben.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Option 4: LGPL-3.0
|
|
||||||
|
|
||||||
**Vorteile:**
|
|
||||||
- ✅ Gleiche Lizenz wie PySide6 (konsistent)
|
|
||||||
- ✅ Schwächerer Copyleft als GPL
|
|
||||||
- ✅ Erlaubt Verwendung in proprietärer Software
|
|
||||||
|
|
||||||
**Nachteile:**
|
|
||||||
- Komplexere Anforderungen als MIT/Apache
|
|
||||||
- Weniger verbreitet für Anwendungen (eher für Bibliotheken)
|
|
||||||
|
|
||||||
**Wann verwenden:**
|
|
||||||
Wenn du einen Kompromiss zwischen GPL und MIT möchtest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lizenz-Kompatibilitätsmatrix
|
|
||||||
|
|
||||||
```
|
|
||||||
DocuMentor Lizenz → Kompatibilität mit Dependencies
|
|
||||||
|
|
||||||
MIT ✅ Kompatibel mit allen
|
|
||||||
Apache 2.0 ✅ Kompatibel mit allen
|
|
||||||
GPL-3.0 ✅ Kompatibel mit allen
|
|
||||||
LGPL-3.0 ✅ Kompatibel mit allen
|
|
||||||
Proprietär ✅ Kompatibel mit allen (LGPL-Bedingungen beachten)
|
|
||||||
```
|
|
||||||
|
|
||||||
Alle genannten Lizenzen sind mit den verwendeten Bibliotheken kompatibel!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Konkrete Empfehlung
|
|
||||||
|
|
||||||
### Für DocuMentor: **MIT License** ⭐
|
|
||||||
|
|
||||||
**Begründung:**
|
|
||||||
1. ✅ Die meisten Dependencies (Pydantic, Polars, pyqtdarktheme) sind MIT-lizenziert
|
|
||||||
2. ✅ LGPL-3.0 (PySide6) erlaubt die Verwendung in MIT-lizenzierter Software
|
|
||||||
3. ✅ Maximale Freiheit für Nutzer und Entwickler
|
|
||||||
4. ✅ Einfach und unkompliziert
|
|
||||||
5. ✅ Fördert Adoption und Beiträge
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nächste Schritte
|
|
||||||
|
|
||||||
### 1. LICENSE-Datei erstellen
|
|
||||||
|
|
||||||
Erstelle eine `LICENSE` Datei im Root-Verzeichnis mit dem MIT-Lizenztext:
|
|
||||||
|
|
||||||
```
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 [Dein Name]
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. pyproject.toml aktualisieren
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[project]
|
|
||||||
name = "DocuMentor"
|
|
||||||
version = "0.1.0"
|
|
||||||
license = {text = "MIT"} # Füge diese Zeile hinzu
|
|
||||||
# ... rest bleibt gleich
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Copyright-Hinweise hinzufügen
|
|
||||||
|
|
||||||
Füge in jede Quellcode-Datei einen Header ein:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Copyright (c) 2025 [Dein Name]
|
|
||||||
# Licensed under the MIT License
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. THIRD_PARTY_LICENSES.txt erstellen
|
|
||||||
|
|
||||||
Erstelle eine Datei, die alle verwendeten Bibliotheken und ihre Lizenzen auflistet:
|
|
||||||
|
|
||||||
```
|
|
||||||
DocuMentor verwendet folgende Open-Source-Bibliotheken:
|
|
||||||
|
|
||||||
1. PySide6 - LGPL-3.0 OR GPL-2.0 OR GPL-3.0
|
|
||||||
https://www.qt.io/qt-for-python
|
|
||||||
|
|
||||||
2. Pydantic - MIT License
|
|
||||||
https://github.com/pydantic/pydantic
|
|
||||||
|
|
||||||
3. Polars - MIT License
|
|
||||||
https://github.com/pola-rs/polars
|
|
||||||
|
|
||||||
... (alle weiteren Bibliotheken)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Externe Tools (Saxon, Apache FOP)
|
|
||||||
|
|
||||||
**Wichtig:** Saxon-HE und Apache FOP sind **externe Programme**, die nicht in DocuMentor eingebettet sind.
|
|
||||||
|
|
||||||
- **Saxon-HE**: MPL-2.0 - Du darfst es verwenden, musst aber nicht deine Software unter MPL lizenzieren
|
|
||||||
- **Apache FOP**: Apache 2.0 - Kompatibel mit MIT
|
|
||||||
|
|
||||||
Da diese Tools nur **aufgerufen** werden (nicht eingebettet), hast du keine zusätzlichen Verpflichtungen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quellen
|
|
||||||
|
|
||||||
- [PySide6 License](https://doc.qt.io/qtforpython-6/licenses.html)
|
|
||||||
- [Pydantic MIT License](https://github.com/pydantic/pydantic/blob/main/LICENSE)
|
|
||||||
- [Polars License](https://github.com/pola-rs/polars)
|
|
||||||
- [PyQtDarkTheme License](https://github.com/5yutan5/PyQtDarkTheme)
|
|
||||||
- [Apache FOP License](https://xmlgraphics.apache.org/fop/license.html)
|
|
||||||
- [Saxon License](https://github.com/Saxonica/Saxon-HE)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Stand:** Januar 2025
|
|
||||||
**Autor:** Lizenzanalyse für DocuMentor
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# DocuMentor
|
|
||||||
|
|
||||||
**Professionelle XSL-Transformations-Verwaltung und PDF-Generierung**
|
|
||||||
|
|
||||||
DocuMentor (ehemals xsl-validator) ist eine leistungsstarke PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit automatischer PDF-Generierung. Die Anwendung bietet eine intuitive GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung komplexer PDF-Generierungsprojekte mit PostgreSQL-Datenbankintegration.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### 🌳 Hierarchische Projektverwaltung
|
|
||||||
- Organisieren Sie Ihre XSL-Transformationen in einer übersichtlichen Baumstruktur
|
|
||||||
- Flexible Workflow-Definitionen mit verschachtelten Knoten
|
|
||||||
- Projektspezifische Konfiguration mit `project.yaml`
|
|
||||||
|
|
||||||
### ⚡ Asynchrone Batch-Verarbeitung
|
|
||||||
- Verarbeiten Sie große Mengen von XML-Dateien im Hintergrund
|
|
||||||
- Fortschrittsanzeige für lange Transformationen
|
|
||||||
- 4x schnellere XSLT-Transformationen durch Worker-Pool-Architektur
|
|
||||||
|
|
||||||
### 🔍 Intelligente Duplikatserkennung
|
|
||||||
- Automatische Hash-basierte Erkennung von identischen XML-Dateien (Blake2b)
|
|
||||||
- Verhindert Redundanzen und spart Speicherplatz
|
|
||||||
- Asynchrone Hash-Berechnung im Hintergrund
|
|
||||||
|
|
||||||
### 📄 PDF-Vergleichsansicht
|
|
||||||
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
|
|
||||||
- Alpha-Blending für visuellen Vergleich
|
|
||||||
- Zoom- und Pan-Funktionalität
|
|
||||||
|
|
||||||
### 🗄️ PostgreSQL-Integration
|
|
||||||
- Nahtlose Datenbankanbindung mit Polars und ConnectorX
|
|
||||||
- Performante SQL-Abfragen und Datenverarbeitung
|
|
||||||
- SSL-Modus-Unterstützung
|
|
||||||
|
|
||||||
### 🛠️ Konfigurierbare Toolchains
|
|
||||||
- Flexible Verwaltung von Saxon, Apache FOP und diff-pdf
|
|
||||||
- Versionierung von Tools
|
|
||||||
- Plattformübergreifende Unterstützung (Linux, Windows, macOS)
|
|
||||||
|
|
||||||
### 🎨 Modernes UI
|
|
||||||
- Dark-Theme-Unterstützung via `qdarktheme`
|
|
||||||
- Drag-and-Drop für XML-Dateien
|
|
||||||
- Responsive und intuitive Benutzeroberfläche
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Voraussetzungen
|
|
||||||
|
|
||||||
- Python 3.13 oder höher
|
|
||||||
- [uv](https://github.com/astral-sh/uv) Paketmanager
|
|
||||||
|
|
||||||
### Abhängigkeiten installieren
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Mit uv (empfohlen)
|
|
||||||
uv sync
|
|
||||||
|
|
||||||
# Oder mit pip
|
|
||||||
pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Externe Tools (optional)
|
|
||||||
|
|
||||||
Für die volle Funktionalität benötigen Sie:
|
|
||||||
|
|
||||||
- **Saxon-HE**: XSLT 3.0 Prozessor ([Download](https://www.saxonica.com/download/))
|
|
||||||
- **Apache FOP**: PDF-Generierung aus XSL-FO ([Download](https://xmlgraphics.apache.org/fop/download.html))
|
|
||||||
- **diff-pdf**: PDF-Vergleich ([GitHub](https://github.com/vslavik/diff-pdf))
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
### Anwendung starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python src/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Projekt erstellen
|
|
||||||
|
|
||||||
1. Legen Sie ein neues Projekt an
|
|
||||||
2. Konfigurieren Sie Ihre Tools (Saxon, Apache FOP) in den Einstellungen
|
|
||||||
3. Organisieren Sie XSL-Stylesheets und XML-Dateien in der Baumstruktur
|
|
||||||
4. Führen Sie Transformationen aus
|
|
||||||
|
|
||||||
### Konfiguration
|
|
||||||
|
|
||||||
Die Anwendung speichert Konfigurationsdateien an folgenden Orten:
|
|
||||||
|
|
||||||
- **Linux**: `~/.config/DocuMentor/config.json`
|
|
||||||
- **Windows**: `%APPDATA%\DocuMentor\config.json`
|
|
||||||
- **macOS**: `~/Library/Application Support/DocuMentor/config.json`
|
|
||||||
|
|
||||||
Projektdaten werden in `project.yaml` im jeweiligen Projektverzeichnis gespeichert.
|
|
||||||
|
|
||||||
## Entwicklung
|
|
||||||
|
|
||||||
### Code-Qualität
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Code-Style prüfen
|
|
||||||
uv run ruff check
|
|
||||||
|
|
||||||
# Code formatieren
|
|
||||||
uv run ruff format
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Hash-Implementierung testen
|
|
||||||
uv run python test_hash_implementation.py
|
|
||||||
|
|
||||||
# Duplikatserkennung testen
|
|
||||||
uv run python test_xml_hash_duplicate_detection.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Architektur
|
|
||||||
|
|
||||||
- **PySide6**: Native Qt-basierte GUI
|
|
||||||
- **Pydantic**: Typsichere Konfigurationsverwaltung
|
|
||||||
- **Polars**: Lightning-fast DataFrame-Verarbeitung
|
|
||||||
- **Blake2b**: Kryptographische Hash-Funktion für Integritätsprüfung
|
|
||||||
|
|
||||||
Siehe [CLAUDE.md](CLAUDE.md) für detaillierte Entwicklerdokumentation.
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
DocuMentor ist unter der [MIT License](LICENSE) lizenziert.
|
|
||||||
|
|
||||||
### Third-Party-Lizenzen
|
|
||||||
|
|
||||||
Diese Software verwendet folgende Open-Source-Bibliotheken:
|
|
||||||
|
|
||||||
- **PySide6** - LGPL-3.0 OR GPL-2.0 OR GPL-3.0
|
|
||||||
- **Pydantic** - MIT License
|
|
||||||
- **Polars** - MIT License
|
|
||||||
- **pyqtdarktheme** - MIT License
|
|
||||||
- Weitere siehe [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)
|
|
||||||
|
|
||||||
Externe Tools (separat zu installieren):
|
|
||||||
- **Saxon-HE** - Mozilla Public License 2.0
|
|
||||||
- **Apache FOP** - Apache License 2.0
|
|
||||||
|
|
||||||
Vollständige Lizenzanalyse: [LICENSES.md](LICENSES.md)
|
|
||||||
|
|
||||||
## Danksagungen
|
|
||||||
|
|
||||||
Vielen Dank an alle Entwickler der verwendeten Open-Source-Bibliotheken!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**DocuMentor** - Professionelle XSL-Transformations-Verwaltung für anspruchsvolle Projekte
|
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
================================================================================
|
|
||||||
THIRD PARTY LICENSES
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
DocuMentor verwendet die folgenden Open-Source-Bibliotheken und Tools.
|
|
||||||
Vielen Dank an alle Entwickler und Maintainer dieser Projekte!
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Python-Abhängigkeiten
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
1. PySide6
|
|
||||||
Version: >=6.10.1
|
|
||||||
Lizenz: LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
||||||
Webseite: https://www.qt.io/qt-for-python
|
|
||||||
GitHub: https://github.com/qt/pyside-pyside-setup
|
|
||||||
Beschreibung: Qt for Python - Official Python bindings for Qt
|
|
||||||
Copyright: Copyright (C) The Qt Company Ltd.
|
|
||||||
|
|
||||||
2. Pydantic
|
|
||||||
Version: >=2.12.0
|
|
||||||
Lizenz: MIT License
|
|
||||||
Webseite: https://pydantic.dev
|
|
||||||
GitHub: https://github.com/pydantic/pydantic
|
|
||||||
Beschreibung: Data validation using Python type hints
|
|
||||||
Copyright: Copyright (c) 2017 to present Pydantic Services Inc.
|
|
||||||
|
|
||||||
3. Pydantic-Settings
|
|
||||||
Version: >=2.12.0
|
|
||||||
Lizenz: MIT License
|
|
||||||
GitHub: https://github.com/pydantic/pydantic-settings
|
|
||||||
Beschreibung: Settings management using Pydantic
|
|
||||||
Copyright: Copyright (c) 2023 Pydantic Services Inc.
|
|
||||||
|
|
||||||
4. Pydantic-YAML
|
|
||||||
Version: >=1.6.0
|
|
||||||
Lizenz: MIT License
|
|
||||||
GitHub: https://github.com/NowanIlfideme/pydantic-yaml
|
|
||||||
Beschreibung: YAML support for Pydantic models
|
|
||||||
Copyright: Copyright (c) 2020 Anatoly Makarevich
|
|
||||||
|
|
||||||
5. Polars
|
|
||||||
Version: >=1.37.0
|
|
||||||
Lizenz: MIT License
|
|
||||||
Webseite: https://pola.rs
|
|
||||||
GitHub: https://github.com/pola-rs/polars
|
|
||||||
Beschreibung: Lightning-fast DataFrame library
|
|
||||||
Copyright: Copyright (c) 2025 Ritchie Vink
|
|
||||||
|
|
||||||
6. ConnectorX (via Polars)
|
|
||||||
Lizenz: MIT License
|
|
||||||
GitHub: https://github.com/sfu-db/connector-x
|
|
||||||
Beschreibung: Fast database connector for DataFrames
|
|
||||||
Copyright: Copyright (c) 2021 SFU Database Group
|
|
||||||
|
|
||||||
7. PyArrow (via Polars)
|
|
||||||
Lizenz: Apache License 2.0
|
|
||||||
Webseite: https://arrow.apache.org/docs/python/
|
|
||||||
GitHub: https://github.com/apache/arrow
|
|
||||||
Beschreibung: Python library for Apache Arrow
|
|
||||||
Copyright: Copyright (c) 2016-2025 The Apache Software Foundation
|
|
||||||
|
|
||||||
8. psutil
|
|
||||||
Version: >=6.1.1
|
|
||||||
Lizenz: BSD-3-Clause License
|
|
||||||
GitHub: https://github.com/giampaolo/psutil
|
|
||||||
Beschreibung: Cross-platform lib for process and system monitoring
|
|
||||||
Copyright: Copyright (c) 2009 Giampaolo Rodola
|
|
||||||
|
|
||||||
9. lxml
|
|
||||||
Version: >=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
|
|
||||||
|
|
||||||
11. PyInstaller (Development)
|
|
||||||
Version: >=6.0.0
|
|
||||||
Lizenz: GPL-2.0 mit Bootloader-Ausnahme
|
|
||||||
Webseite: https://pyinstaller.org
|
|
||||||
GitHub: https://github.com/pyinstaller/pyinstaller
|
|
||||||
Beschreibung: Bundles Python applications into stand-alone executables
|
|
||||||
Copyright: Copyright (c) 2010-2025 PyInstaller Development Team
|
|
||||||
|
|
||||||
12. Pillow (Development)
|
|
||||||
Version: >=10.0.0
|
|
||||||
Lizenz: HPND License (Historical Permission Notice and Disclaimer)
|
|
||||||
Webseite: https://python-pillow.org
|
|
||||||
GitHub: https://github.com/python-pillow/Pillow
|
|
||||||
Beschreibung: Python Imaging Library (Fork)
|
|
||||||
Copyright: Copyright (c) 2010-2025 Jeffrey A. Clark and contributors
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Eingebettete Bibliotheken
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Diese Bibliotheken sind direkt im Quellcode von DocuMentor enthalten.
|
|
||||||
|
|
||||||
1. vis-network (vis.js)
|
|
||||||
Version: 9.1.9
|
|
||||||
Lizenz: Apache License 2.0 ODER MIT License (Dual-Lizenz)
|
|
||||||
Webseite: https://visjs.github.io/vis-network/
|
|
||||||
GitHub: https://github.com/visjs/vis-network
|
|
||||||
Beschreibung: A dynamic, browser-based network visualization library
|
|
||||||
Copyright: Copyright (c) 2011-2017 Almende B.V, http://almende.com
|
|
||||||
Copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
|
|
||||||
Datei: src/res/vis-network.min.js
|
|
||||||
Hinweis: Wird inline in QWebEngineView für den XSL-Abhängigkeitsgraph verwendet
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Externe Tools (nicht eingebettet)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Diese Tools werden als externe Programme aufgerufen und sind nicht
|
|
||||||
Bestandteil der DocuMentor-Distribution. Sie müssen separat installiert werden.
|
|
||||||
|
|
||||||
1. Saxon-HE (Home Edition)
|
|
||||||
Lizenz: Mozilla Public License 2.0 (MPL-2.0)
|
|
||||||
Webseite: https://www.saxonica.com/
|
|
||||||
GitHub: https://github.com/Saxonica/Saxon-HE
|
|
||||||
Beschreibung: XSLT 3.0 and XQuery 3.1 processor
|
|
||||||
Copyright: Copyright (c) Saxonica Limited
|
|
||||||
Hinweis: Für XSLT-Transformationen verwendet
|
|
||||||
|
|
||||||
2. Apache FOP (Formatting Objects Processor)
|
|
||||||
Lizenz: Apache License 2.0
|
|
||||||
Webseite: https://xmlgraphics.apache.org/fop/
|
|
||||||
Beschreibung: XSL-FO to PDF converter
|
|
||||||
Copyright: Copyright (c) 1999-2025 The Apache Software Foundation
|
|
||||||
Hinweis: Für PDF-Generierung verwendet
|
|
||||||
|
|
||||||
3. diff-pdf
|
|
||||||
Lizenz: GPL-2.0 (vermutlich)
|
|
||||||
GitHub: https://github.com/vslavik/diff-pdf
|
|
||||||
Beschreibung: Tool for visually comparing PDF files
|
|
||||||
Hinweis: Optional für PDF-Vergleich verwendet
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Lizenztexte
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
MIT License
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
Apache License 2.0
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
BSD-3-Clause License (psutil)
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
3. Neither the name of the copyright holder nor the names of its contributors
|
|
||||||
may be used to endorse or promote products derived from this software
|
|
||||||
without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
||||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
||||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
||||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
||||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
||||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
||||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
||||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
LGPL-3.0 / GPL-2.0 / GPL-3.0 (PySide6)
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
PySide6 ist unter LGPL-3.0, GPL-2.0 oder GPL-3.0 lizenziert.
|
|
||||||
Die vollständigen Lizenztexte finden Sie unter:
|
|
||||||
|
|
||||||
LGPL-3.0: https://www.gnu.org/licenses/lgpl-3.0.html
|
|
||||||
GPL-2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
||||||
GPL-3.0: https://www.gnu.org/licenses/gpl-3.0.html
|
|
||||||
|
|
||||||
Weitere Informationen: https://doc.qt.io/qtforpython-6/licenses.html
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
Mozilla Public License 2.0 (Saxon-HE)
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Der vollständige Lizenztext der Mozilla Public License 2.0 ist verfügbar unter:
|
|
||||||
https://www.mozilla.org/en-US/MPL/2.0/
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
HINWEISE
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
1. PySide6 (LGPL-3.0):
|
|
||||||
DocuMentor verwendet PySide6 über dynamisches Linking (Python import).
|
|
||||||
Nutzer können die PySide6-Version über pip austauschen.
|
|
||||||
Der Quellcode von DocuMentor muss nicht unter LGPL veröffentlicht werden.
|
|
||||||
|
|
||||||
2. Externe Tools:
|
|
||||||
Saxon-HE, Apache FOP und diff-pdf sind separate Programme, die von
|
|
||||||
DocuMentor aufgerufen werden, aber nicht in die Distribution eingebettet sind.
|
|
||||||
Nutzer müssen diese Tools selbst installieren.
|
|
||||||
|
|
||||||
3. Aktualisierungen:
|
|
||||||
Bitte überprüfen Sie regelmäßig die Lizenzen der verwendeten Bibliotheken,
|
|
||||||
da sich diese ändern können.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
Stand: April 2026
|
|
||||||
Erstellt für: DocuMentor v1.6.4
|
|
||||||
================================================================================
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"""
|
|
||||||
WiX MSI Build-Skript für DocuMentor (WiX v6)
|
|
||||||
|
|
||||||
Erstellt einen MSI-Installer aus dem PyInstaller Build.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(project_root: Path) -> str:
|
|
||||||
"""Liest die Versionsnummer aus pyproject.toml."""
|
|
||||||
pyproject_path = project_root / "pyproject.toml"
|
|
||||||
with pyproject_path.open("rb") as f:
|
|
||||||
data = tomllib.load(f)
|
|
||||||
return data["project"]["version"]
|
|
||||||
|
|
||||||
|
|
||||||
def build_msi():
|
|
||||||
"""Erstellt MSI-Installer mit WiX v6."""
|
|
||||||
project_root = Path(__file__).parent
|
|
||||||
dist_dir = project_root / "dist" / "DocuMentor"
|
|
||||||
|
|
||||||
if not dist_dir.exists():
|
|
||||||
print("FEHLER: PyInstaller Build nicht gefunden!")
|
|
||||||
print("Bitte zuerst ausführen: uv run python build_windows.py")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
version = get_version(project_root)
|
|
||||||
msi_output = project_root / "dist" / f"DocuMentor-{version}.msi"
|
|
||||||
|
|
||||||
print(f"DocuMentor Build gefunden: {dist_dir}")
|
|
||||||
print(f"Version: {version}")
|
|
||||||
print(f"MSI-Ausgabe: {msi_output}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Schritt 1: ProductFiles.wxs generieren (ersetzt WiX heat)
|
|
||||||
print("Schritt 1/2: Generiere ProductFiles.wxs...")
|
|
||||||
result = subprocess.run(["uv", "run", "python", "generate_wix_files.py"], check=False)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print("\nFEHLER: ProductFiles.wxs Generierung fehlgeschlagen!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Schritt 2: MSI kompilieren mit WiX v6
|
|
||||||
print("Schritt 2/2: Kompiliere MSI-Installer...")
|
|
||||||
result = subprocess.run(
|
|
||||||
["wix", "build", "DocuMentor.wxs", "ProductFiles.wxs", "-o", str(msi_output)],
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print("\nFEHLER: MSI-Kompilierung fehlgeschlagen!")
|
|
||||||
print("Stelle sicher, dass WiX v6 installiert ist:")
|
|
||||||
print(" dotnet tool install --global wix --version 6.*")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"[OK] MSI erfolgreich erstellt: {msi_output}")
|
|
||||||
print()
|
|
||||||
print("Installation testen mit:")
|
|
||||||
print(f" msiexec /i \"{msi_output}\"")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
build_msi()
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Build-Skript für Windows-Distribution von DocuMentor
|
|
||||||
|
|
||||||
Erstellt:
|
|
||||||
1. Eigenständige Executable mit PyInstaller
|
|
||||||
2. Optional: ZIP-Archiv für portable Distribution
|
|
||||||
"""
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(project_root: Path) -> str:
|
|
||||||
"""Liest die Versionsnummer aus pyproject.toml."""
|
|
||||||
with (project_root / "pyproject.toml").open("rb") as f:
|
|
||||||
return tomllib.load(f)["project"]["version"]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
project_root = Path(__file__).parent
|
|
||||||
dist_dir = project_root / "dist"
|
|
||||||
build_dir = project_root / "build"
|
|
||||||
resources_dir = project_root / "resources"
|
|
||||||
icon_path = resources_dir / "icon.ico"
|
|
||||||
version_info_path = project_root / "version_info.txt"
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("DocuMentor Windows Build")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# 1. Icon und Versionsinformationen generieren
|
|
||||||
print("\n[1/6] Icon und Versionsinformationen generieren...")
|
|
||||||
|
|
||||||
# Icon erstellen falls nicht vorhanden
|
|
||||||
if not icon_path.exists():
|
|
||||||
print(" Erstelle Standard-Icon...")
|
|
||||||
try:
|
|
||||||
subprocess.run([sys.executable, "create_icon.py"], check=True, cwd=project_root)
|
|
||||||
print(" ✓ Icon erstellt")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f" ✗ Icon-Erstellung fehlgeschlagen: {e}")
|
|
||||||
print(" ⚠ Fahre ohne Icon fort...")
|
|
||||||
else:
|
|
||||||
print(f" ✓ Icon vorhanden: {icon_path.name}")
|
|
||||||
|
|
||||||
# Versionsinformationen erstellen
|
|
||||||
print(" Erstelle Versionsinformationen...")
|
|
||||||
try:
|
|
||||||
subprocess.run([sys.executable, "create_version_info.py"], check=True, cwd=project_root)
|
|
||||||
print(" ✓ Versionsinformationen erstellt")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f" ✗ Versionsinformationen-Erstellung fehlgeschlagen: {e}")
|
|
||||||
print(" ⚠ Fahre ohne Versionsinformationen fort...")
|
|
||||||
|
|
||||||
# 2. Cleanup alter Builds
|
|
||||||
print("\n[2/6] Cleanup alter Builds...")
|
|
||||||
if dist_dir.exists():
|
|
||||||
shutil.rmtree(dist_dir)
|
|
||||||
print(" ✓ dist/ gelöscht")
|
|
||||||
if build_dir.exists():
|
|
||||||
shutil.rmtree(build_dir)
|
|
||||||
print(" ✓ build/ gelöscht")
|
|
||||||
|
|
||||||
# 3. PyInstaller ausführen
|
|
||||||
print("\n[3/6] PyInstaller Build starten...")
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["pyinstaller", "--clean", "DocuMentor.spec"],
|
|
||||||
check=True,
|
|
||||||
cwd=project_root
|
|
||||||
)
|
|
||||||
print(" ✓ Build erfolgreich")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f" ✗ Build fehlgeschlagen: {e}")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# 4. README für Distribution erstellen
|
|
||||||
print("\n[4/6] README erstellen...")
|
|
||||||
readme_content = """DocuMentor - XSL-Transformations-Verwaltung
|
|
||||||
============================================
|
|
||||||
|
|
||||||
Installation:
|
|
||||||
1. Entpacken Sie dieses Archiv in ein Verzeichnis Ihrer Wahl
|
|
||||||
2. Führen Sie DocuMentor.exe aus
|
|
||||||
|
|
||||||
Externe Abhängigkeiten (separat zu installieren):
|
|
||||||
- Java Runtime Environment (JRE) oder JDK
|
|
||||||
- Apache FOP (für PDF-Generierung)
|
|
||||||
- Saxon XSLT-Prozessor (JAR-Datei)
|
|
||||||
- diff-pdf (für PDF-Vergleiche)
|
|
||||||
|
|
||||||
Beim ersten Start werden Sie aufgefordert, die Pfade zu diesen
|
|
||||||
Tools in den Programmeinstellungen zu konfigurieren.
|
|
||||||
|
|
||||||
Konfiguration und Logs:
|
|
||||||
- Windows: %APPDATA%\\DocuMentor\\
|
|
||||||
- Konfiguration: config.json
|
|
||||||
- Logs: logs\\
|
|
||||||
|
|
||||||
Support:
|
|
||||||
Bei Fragen oder Problemen erstellen Sie bitte ein Issue auf GitHub.
|
|
||||||
"""
|
|
||||||
|
|
||||||
readme_path = dist_dir / "DocuMentor" / "README.txt"
|
|
||||||
readme_path.write_text(readme_content, encoding='utf-8')
|
|
||||||
print(" ✓ README.txt erstellt")
|
|
||||||
|
|
||||||
# 5. Icon ins dist-Verzeichnis kopieren (für Installer)
|
|
||||||
print("\n[5/6] Icon für Installer vorbereiten...")
|
|
||||||
if icon_path.exists():
|
|
||||||
dist_icon = dist_dir / "DocuMentor" / "icon.ico"
|
|
||||||
shutil.copy2(icon_path, dist_icon)
|
|
||||||
print(" ✓ Icon kopiert")
|
|
||||||
else:
|
|
||||||
print(" ⚠ Kein Icon vorhanden")
|
|
||||||
|
|
||||||
# 6. ZIP-Archiv erstellen
|
|
||||||
print("\n[6/6] ZIP-Archiv erstellen...")
|
|
||||||
version = get_version(project_root)
|
|
||||||
zip_name = f"DocuMentor-{version}"
|
|
||||||
zip_path = dist_dir / zip_name
|
|
||||||
|
|
||||||
shutil.make_archive(
|
|
||||||
str(zip_path),
|
|
||||||
'zip',
|
|
||||||
dist_dir,
|
|
||||||
'DocuMentor'
|
|
||||||
)
|
|
||||||
print(f" ✓ {zip_name}.zip erstellt")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Build abgeschlossen!")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"\nErgebnisse:")
|
|
||||||
print(f" • Executable: dist/DocuMentor/DocuMentor.exe")
|
|
||||||
print(f" • ZIP-Archiv: dist/{zip_name}.zip")
|
|
||||||
print("\nNächste Schritte:")
|
|
||||||
print(" 1. Testen Sie DocuMentor.exe auf einem Windows-System")
|
|
||||||
print(" 2. Optional: Erstellen Sie einen Installer mit Inno Setup")
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
-164
@@ -1,164 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Icon-Generator für DocuMentor
|
|
||||||
|
|
||||||
Erstellt ein Icon aus einem PNG oder generiert ein Standard-Icon.
|
|
||||||
Unterstützt Windows (.ico) und verschiedene Größen.
|
|
||||||
|
|
||||||
Verwendung:
|
|
||||||
python create_icon.py # Generiert Standard-Icon
|
|
||||||
python create_icon.py source.png # Konvertiert PNG zu ICO
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
except ImportError:
|
|
||||||
print("Fehler: Pillow ist nicht installiert.")
|
|
||||||
print("Installation: uv pip install pillow")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def create_default_icon(output_path: Path):
|
|
||||||
"""Erstellt ein Standard-Icon mit DocuMentor-Branding."""
|
|
||||||
sizes = [256, 128, 64, 48, 32, 16]
|
|
||||||
images = []
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
# Neues Bild erstellen mit Farbverlauf
|
|
||||||
img = Image.new('RGB', (size, size), color='white')
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
# Hintergrund: Blau-Verlauf (vereinfacht als solides Blau)
|
|
||||||
bg_color = (41, 128, 185) # Professionelles Blau
|
|
||||||
draw.rectangle([0, 0, size, size], fill=bg_color)
|
|
||||||
|
|
||||||
# Dokument-Symbol (vereinfachte Darstellung)
|
|
||||||
margin = size // 8
|
|
||||||
doc_left = margin
|
|
||||||
doc_top = margin
|
|
||||||
doc_right = size - margin
|
|
||||||
doc_bottom = size - margin
|
|
||||||
|
|
||||||
# Weißes Dokument
|
|
||||||
draw.rectangle(
|
|
||||||
[doc_left, doc_top, doc_right, doc_bottom],
|
|
||||||
fill='white',
|
|
||||||
outline=(52, 73, 94),
|
|
||||||
width=max(1, size // 64)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ecke umgeknickt (rechts oben)
|
|
||||||
fold_size = size // 6
|
|
||||||
points = [
|
|
||||||
(doc_right - fold_size, doc_top),
|
|
||||||
(doc_right, doc_top + fold_size),
|
|
||||||
(doc_right - fold_size, doc_top + fold_size),
|
|
||||||
]
|
|
||||||
draw.polygon(points, fill=(220, 220, 220), outline=(52, 73, 94))
|
|
||||||
|
|
||||||
# Text-Linien im Dokument (nur bei größeren Icons)
|
|
||||||
if size >= 32:
|
|
||||||
line_margin = doc_left + size // 12
|
|
||||||
line_width = doc_right - doc_left - size // 6
|
|
||||||
line_count = min(3, size // 32)
|
|
||||||
line_spacing = (doc_bottom - doc_top - fold_size) // (line_count + 2)
|
|
||||||
|
|
||||||
for i in range(line_count):
|
|
||||||
y = doc_top + fold_size + line_spacing * (i + 1)
|
|
||||||
draw.rectangle(
|
|
||||||
[line_margin, y, line_margin + line_width, y + max(1, size // 128)],
|
|
||||||
fill=(52, 73, 94)
|
|
||||||
)
|
|
||||||
|
|
||||||
# "M" für Mentor (nur bei großen Icons)
|
|
||||||
if size >= 64:
|
|
||||||
try:
|
|
||||||
# Versuche System-Font zu verwenden
|
|
||||||
font_size = size // 4
|
|
||||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
|
|
||||||
except:
|
|
||||||
# Fallback auf Default-Font
|
|
||||||
font = ImageFont.load_default()
|
|
||||||
|
|
||||||
text = "M"
|
|
||||||
# Zentrieren (grobe Schätzung)
|
|
||||||
bbox = draw.textbbox((0, 0), text, font=font)
|
|
||||||
text_width = bbox[2] - bbox[0]
|
|
||||||
text_height = bbox[3] - bbox[1]
|
|
||||||
text_x = (size - text_width) // 2
|
|
||||||
text_y = doc_bottom - text_height - margin // 2
|
|
||||||
|
|
||||||
draw.text((text_x, text_y), text, fill=bg_color, font=font)
|
|
||||||
|
|
||||||
images.append(img)
|
|
||||||
|
|
||||||
# Als ICO speichern
|
|
||||||
images[0].save(
|
|
||||||
output_path,
|
|
||||||
format='ICO',
|
|
||||||
sizes=[(img.width, img.height) for img in images],
|
|
||||||
append_images=images[1:]
|
|
||||||
)
|
|
||||||
print(f"✓ Standard-Icon erstellt: {output_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def convert_png_to_ico(source_path: Path, output_path: Path):
|
|
||||||
"""Konvertiert ein PNG-Bild zu einem Multi-Size ICO."""
|
|
||||||
try:
|
|
||||||
img = Image.open(source_path)
|
|
||||||
|
|
||||||
# Zu RGBA konvertieren falls nötig
|
|
||||||
if img.mode != 'RGBA':
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
|
|
||||||
# Verschiedene Größen erstellen
|
|
||||||
sizes = [256, 128, 64, 48, 32, 16]
|
|
||||||
images = []
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
resized = img.resize((size, size), Image.Resampling.LANCZOS)
|
|
||||||
images.append(resized)
|
|
||||||
|
|
||||||
# Als ICO speichern
|
|
||||||
images[0].save(
|
|
||||||
output_path,
|
|
||||||
format='ICO',
|
|
||||||
sizes=[(img.width, img.height) for img in images],
|
|
||||||
append_images=images[1:]
|
|
||||||
)
|
|
||||||
print(f"✓ Icon erstellt aus {source_path.name}: {output_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Fehler beim Konvertieren: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
project_root = Path(__file__).parent
|
|
||||||
resources_dir = project_root / "resources"
|
|
||||||
resources_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
output_ico = resources_dir / "icon.ico"
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
# PNG zu ICO konvertieren
|
|
||||||
source_path = Path(sys.argv[1])
|
|
||||||
if not source_path.exists():
|
|
||||||
print(f"Fehler: Datei nicht gefunden: {source_path}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
convert_png_to_ico(source_path, output_ico)
|
|
||||||
else:
|
|
||||||
# Standard-Icon generieren
|
|
||||||
print("Erstelle Standard-Icon...")
|
|
||||||
create_default_icon(output_ico)
|
|
||||||
|
|
||||||
print(f"\nIcon gespeichert: {output_ico}")
|
|
||||||
print("Das Icon wird automatisch von PyInstaller und Inno Setup verwendet.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Generiert Windows-Versionsinformationen für PyInstaller
|
|
||||||
|
|
||||||
Liest Version aus pyproject.toml und erstellt version_info.txt
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def parse_version(version_str: str) -> tuple[int, int, int, int]:
|
|
||||||
"""Parst Version-String (z.B. '0.1.0') zu Tuple (0, 1, 0, 0)."""
|
|
||||||
parts = version_str.split('.')
|
|
||||||
major = int(parts[0]) if len(parts) > 0 else 0
|
|
||||||
minor = int(parts[1]) if len(parts) > 1 else 0
|
|
||||||
patch = int(parts[2]) if len(parts) > 2 else 0
|
|
||||||
build = 0 # Könnte aus Git-Commit-Count generiert werden
|
|
||||||
return (major, minor, patch, build)
|
|
||||||
|
|
||||||
|
|
||||||
def create_version_info(project_root: Path):
|
|
||||||
"""Erstellt version_info.txt für PyInstaller."""
|
|
||||||
|
|
||||||
# pyproject.toml lesen
|
|
||||||
pyproject_path = project_root / "pyproject.toml"
|
|
||||||
with open(pyproject_path, 'rb') as f:
|
|
||||||
pyproject = tomllib.load(f)
|
|
||||||
|
|
||||||
project = pyproject['project']
|
|
||||||
version = project['version']
|
|
||||||
name = project['name']
|
|
||||||
description = project['description']
|
|
||||||
|
|
||||||
# Version parsen
|
|
||||||
file_version = parse_version(version)
|
|
||||||
product_version = file_version
|
|
||||||
|
|
||||||
# Jahr für Copyright
|
|
||||||
year = datetime.now().year
|
|
||||||
|
|
||||||
# version_info.txt Content
|
|
||||||
version_info_content = f"""# UTF-8
|
|
||||||
#
|
|
||||||
# Generiert automatisch von create_version_info.py
|
|
||||||
# NICHT manuell bearbeiten!
|
|
||||||
#
|
|
||||||
|
|
||||||
VSVersionInfo(
|
|
||||||
ffi=FixedFileInfo(
|
|
||||||
# filevers und prodvers als Tuple: (1, 0, 0, 0)
|
|
||||||
filevers={file_version},
|
|
||||||
prodvers={product_version},
|
|
||||||
# Maske für gültige Bits in filevers und prodvers
|
|
||||||
mask=0x3f,
|
|
||||||
# Flags - kann VS_FF_DEBUG, VS_FF_PRERELEASE, etc. enthalten
|
|
||||||
flags=0x0,
|
|
||||||
# Betriebssystem - VOS_NT_WINDOWS32
|
|
||||||
OS=0x40004,
|
|
||||||
# Dateityp - VFT_APP (Anwendung)
|
|
||||||
fileType=0x1,
|
|
||||||
# Subtyp (nicht verwendet für VFT_APP)
|
|
||||||
subtype=0x0,
|
|
||||||
# Datumsstempel
|
|
||||||
date=(0, 0)
|
|
||||||
),
|
|
||||||
kids=[
|
|
||||||
StringFileInfo(
|
|
||||||
[
|
|
||||||
StringTable(
|
|
||||||
'040904B0', # Deutsch (0x0409 = Englisch, 0x0407 = Deutsch), Unicode
|
|
||||||
[StringStruct('CompanyName', 'Vitali Graf / Software- und Datenbankentwicklung'),
|
|
||||||
StringStruct('FileDescription', '{description}'),
|
|
||||||
StringStruct('FileVersion', '{version}'),
|
|
||||||
StringStruct('InternalName', '{name}'),
|
|
||||||
StringStruct('LegalCopyright', '© {year} Vitali Graf. Alle Rechte vorbehalten.'),
|
|
||||||
StringStruct('OriginalFilename', '{name}.exe'),
|
|
||||||
StringStruct('ProductName', '{name}'),
|
|
||||||
StringStruct('ProductVersion', '{version}')])
|
|
||||||
]),
|
|
||||||
VarFileInfo([VarStruct('Translation', [1033, 1200])]) # Englisch, Unicode
|
|
||||||
]
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# version_info.txt schreiben
|
|
||||||
version_info_path = project_root / "version_info.txt"
|
|
||||||
version_info_path.write_text(version_info_content, encoding='utf-8')
|
|
||||||
|
|
||||||
print("✓ version_info.txt erstellt")
|
|
||||||
print(f" Version: {version}")
|
|
||||||
print(f" Datei: {version_info_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
project_root = Path(__file__).parent
|
|
||||||
create_version_info(project_root)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Transformation ohne Force — Entscheidungslogik
|
|
||||||
|
|
||||||
Die zentrale Methode ist `is_up_to_date()` in transform.py:155-180. Sie basiert auf Modifikationszeiten (mtime), nicht auf Hashes.
|
|
||||||
|
|
||||||
## Ablauf
|
|
||||||
Die Prüfung erfolgt an zwei Stellen in der Pipeline:
|
|
||||||
1. Vor Saxon-Transformation (`transform_saxon()`) — transform.py:192-194
|
|
||||||
2. Vor PDF-Build (`build_pdf()`) — transform.py:322-324
|
|
||||||
|
|
||||||
Beide rufen `is_up_to_date()` auf, die prüft:
|
|
||||||
- Existiert die New-PDF?
|
|
||||||
- Ist die XML-Datei neuer als die New-PDF?
|
|
||||||
- Ist die XSL-Datei neuer als die New-PDF?
|
|
||||||
|
|
||||||
Wenn die New-PDF existiert und älter als alle Inputs ist → Transformation wird übersprungen mit `(True, "Übersprungen (aktuell)")`.
|
|
||||||
|
|
||||||
## Mermaid-Diagramm
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["Transformation gestartet<br/>(force=False)"] --> B{"force == True?"}
|
|
||||||
B -- Ja --> EXEC["Transformation ausführen"]
|
|
||||||
B -- Nein --> C{"New-PDF existiert?"}
|
|
||||||
C -- Nein --> EXEC
|
|
||||||
C -- Ja --> D["mtime der New-PDF ermitteln"]
|
|
||||||
D --> E{"XML-Datei neuer<br/>als New-PDF?"}
|
|
||||||
E -- Ja --> EXEC
|
|
||||||
E -- Nein --> F{"XSL-Datei neuer<br/>als New-PDF?"}
|
|
||||||
F -- Ja --> EXEC
|
|
||||||
F -- Nein --> SKIP["Übersprungen (aktuell)<br/>return (True, 'Übersprungen')"]
|
|
||||||
|
|
||||||
EXEC --> S1["Schritt 1: Saxon<br/>XML → FO"]
|
|
||||||
S1 --> S1OK{"Saxon erfolgreich?"}
|
|
||||||
S1OK -- Nein --> FAIL["Pipeline abgebrochen"]
|
|
||||||
S1OK -- Ja --> S2CHECK{"force == True?<br/>(erneute Prüfung<br/>für build_pdf)"}
|
|
||||||
S2CHECK -- Ja --> S2["Schritt 2: FOP<br/>FO → PDF"]
|
|
||||||
S2CHECK -- Nein --> S2UP{"is_up_to_date()?"}
|
|
||||||
S2UP -- Ja --> S2SKIP["PDF-Build übersprungen"]
|
|
||||||
S2UP -- Nein --> S2
|
|
||||||
S2 --> S3["Schritt 3: diff-pdf<br/>PDF-Vergleich"]
|
|
||||||
S3 --> DONE["Pipeline abgeschlossen"]
|
|
||||||
|
|
||||||
style SKIP fill:#4CAF50,color:#fff
|
|
||||||
style EXEC fill:#2196F3,color:#fff
|
|
||||||
style FAIL fill:#f44336,color:#fff
|
|
||||||
style DONE fill:#4CAF50,color:#fff
|
|
||||||
style S2SKIP fill:#4CAF50,color:#fff
|
|
||||||
```
|
|
||||||
## Wichtige Details
|
|
||||||
- Keine Hash-basierte Prüfung: Die Skip-Logik nutzt ausschließlich `mtime`-Vergleiche, nicht die blake2b-Hashes (die werden nur für Änderungsverfolgung in der UI verwendet).
|
|
||||||
- Doppelte Prüfung: `is_up_to_date()` wird sowohl vor Saxon als auch vor FOP aufgerufen — theoretisch könnte Saxon ausgeführt, aber der PDF-Build übersprungen werden.
|
|
||||||
- Skip = Erfolg: Ein übersprungener Schritt gilt als erfolgreich `(True, ...)`, die Pipeline läuft weiter.
|
|
||||||
- Force-Aufruf: Über das Kontextmenü gibt es explizite Force-Methoden wie `_transform_all_xml_files_force()` in transformation.py.
|
|
||||||
|
|
||||||
|
|
||||||
## Erweiterung
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
LOAD["Projekt geladen"] --> BUILD["XSL-Abhängigkeitsgraph aufbauen<br/>dict[Path, set[Path]]"]
|
|
||||||
BUILD --> CACHE["Im Speicher halten"]
|
|
||||||
|
|
||||||
TRANSFORM["is_up_to_date() aufgerufen"] --> CHECK{"Graph-Eintrag<br/>vorhanden?"}
|
|
||||||
CHECK -- Nein --> PARSE["XSL parsen, Imports auflösen,<br/>Eintrag erstellen"]
|
|
||||||
PARSE --> MTIME
|
|
||||||
CHECK -- Ja --> STALE{"mtime der XSL<br/>geändert seit letztem Parse?"}
|
|
||||||
STALE -- Ja --> PARSE
|
|
||||||
STALE -- Nein --> MTIME["mtime aller Abhängigkeiten<br/>gegen New-PDF prüfen"]
|
|
||||||
MTIME --> RESULT["Ergebnis"]
|
|
||||||
```
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# Icon und Versionsinformationen für Windows-Build
|
|
||||||
|
|
||||||
## Übersicht
|
|
||||||
|
|
||||||
DocuMentor unterstützt professionelle Windows-Builds mit:
|
|
||||||
- **Anwendungs-Icon** in allen benötigten Größen
|
|
||||||
- **Windows-Versionsinformationen** (Datei-Eigenschaften)
|
|
||||||
- Automatische Integration in Build-Prozess
|
|
||||||
|
|
||||||
## Icon-System
|
|
||||||
|
|
||||||
### Automatische Icon-Generierung
|
|
||||||
|
|
||||||
Das Build-Skript generiert automatisch ein Standard-Icon, falls keins vorhanden ist:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Falls `resources/icon.ico` nicht existiert, wird automatisch ein Standard-Icon mit DocuMentor-Branding erstellt.
|
|
||||||
|
|
||||||
### Manuelles Icon erstellen
|
|
||||||
|
|
||||||
#### Option 1: Standard-Icon
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python create_icon.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Erstellt ein einfaches Icon mit:
|
|
||||||
- Blauem Hintergrund (professionelles Blau: #2980B9)
|
|
||||||
- Weißem Dokument-Symbol
|
|
||||||
- "M" für Mentor (bei großen Icons)
|
|
||||||
- Umgeknickter Ecke
|
|
||||||
- Mehreren Größen (16×16 bis 256×256)
|
|
||||||
|
|
||||||
#### Option 2: Aus eigenem PNG
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python create_icon.py mein-logo.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Konvertiert ein PNG-Bild zu einem Multi-Size Windows-Icon:
|
|
||||||
- Unterstützt Transparenz
|
|
||||||
- Erstellt alle benötigten Größen
|
|
||||||
- Optimiert für verschiedene Bildschirmauflösungen
|
|
||||||
|
|
||||||
**PNG-Anforderungen:**
|
|
||||||
- Idealerweise 256×256 Pixel oder größer
|
|
||||||
- Quadratisches Format
|
|
||||||
- PNG oder JPEG Format
|
|
||||||
- Transparenter Hintergrund empfohlen
|
|
||||||
|
|
||||||
### Icon-Größen
|
|
||||||
|
|
||||||
Das ICO-Format enthält folgende Auflösungen:
|
|
||||||
|
|
||||||
| Größe | Verwendung |
|
|
||||||
|---------|--------------------------------------|
|
|
||||||
| 256×256 | Windows 7+, Taskleiste, große Icons |
|
|
||||||
| 128×128 | Windows 7+, große Icons |
|
|
||||||
| 64×64 | Hohe DPI-Displays |
|
|
||||||
| 48×48 | Standard Desktop-Icon |
|
|
||||||
| 32×32 | Explorer Details-Ansicht |
|
|
||||||
| 16×16 | Kleinstes Icon, Titelleiste |
|
|
||||||
|
|
||||||
### Wo wird das Icon verwendet?
|
|
||||||
|
|
||||||
- **DocuMentor.exe** - Anwendungs-Icon
|
|
||||||
- **Setup.exe** - Installer-Icon (Inno Setup)
|
|
||||||
- **Desktop-Verknüpfung** - Erstellt beim Installieren
|
|
||||||
- **Start-Menü** - Windows-Programmgruppe
|
|
||||||
- **Taskleiste** - Beim Ausführen
|
|
||||||
- **Deinstallations-Programm** - System-Einstellungen
|
|
||||||
|
|
||||||
## Versionsinformationen
|
|
||||||
|
|
||||||
### Automatische Generierung
|
|
||||||
|
|
||||||
Versionsinformationen werden automatisch vom Build-Skript generiert:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manuelle Generierung
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python create_version_info.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inhalt der Versionsinformationen
|
|
||||||
|
|
||||||
Die `version_info.txt` enthält:
|
|
||||||
|
|
||||||
```
|
|
||||||
FileVersion: 0.1.0.0
|
|
||||||
ProductVersion: 0.1.0.0
|
|
||||||
CompanyName: Ihr Name/Organisation
|
|
||||||
FileDescription: Professionelle XSL-Transformations-Verwaltung und PDF-Generierung
|
|
||||||
InternalName: DocuMentor
|
|
||||||
LegalCopyright: © 2026 Ihr Name. Alle Rechte vorbehalten.
|
|
||||||
OriginalFilename: DocuMentor.exe
|
|
||||||
ProductName: DocuMentor
|
|
||||||
```
|
|
||||||
|
|
||||||
### Version aus pyproject.toml
|
|
||||||
|
|
||||||
Die Version wird automatisch aus `pyproject.toml` gelesen:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[project]
|
|
||||||
name = "DocuMentor"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Version ändern
|
|
||||||
|
|
||||||
**Schritt 1:** Version in `pyproject.toml` ändern:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
version = "0.2.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Schritt 2:** Version in `installer.iss` ändern (Zeile 13):
|
|
||||||
|
|
||||||
```iss
|
|
||||||
#define MyAppVersion "0.2.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Schritt 3:** Build neu ausführen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python build_windows.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Versionsinformationen werden automatisch neu generiert.
|
|
||||||
|
|
||||||
### Versionsnummern-Schema
|
|
||||||
|
|
||||||
DocuMentor verwendet [Semantic Versioning](https://semver.org/lang/de/):
|
|
||||||
|
|
||||||
```
|
|
||||||
MAJOR.MINOR.PATCH
|
|
||||||
|
|
||||||
0.1.0 → Erste Beta-Version
|
|
||||||
1.0.0 → Erste stabile Version
|
|
||||||
1.1.0 → Neue Features
|
|
||||||
1.1.1 → Bugfixes
|
|
||||||
2.0.0 → Breaking Changes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows-Eigenschaften anzeigen
|
|
||||||
|
|
||||||
Nach dem Build können Sie die Versionsinformationen in Windows anzeigen:
|
|
||||||
|
|
||||||
1. Rechtsklick auf `DocuMentor.exe`
|
|
||||||
2. **Eigenschaften** auswählen
|
|
||||||
3. Tab **Details** öffnen
|
|
||||||
|
|
||||||
Dort sehen Sie:
|
|
||||||
- Dateiversion
|
|
||||||
- Produktversion
|
|
||||||
- Beschreibung
|
|
||||||
- Copyright
|
|
||||||
- Produktname
|
|
||||||
- Original-Dateiname
|
|
||||||
|
|
||||||
## Integration in Build-Prozess
|
|
||||||
|
|
||||||
### DocuMentor.spec
|
|
||||||
|
|
||||||
```python
|
|
||||||
exe = EXE(
|
|
||||||
# ...
|
|
||||||
icon=str(project_root / 'resources' / 'icon.ico') if (project_root / 'resources' / 'icon.ico').exists() else None,
|
|
||||||
version=str(project_root / 'version_info.txt') if (project_root / 'version_info.txt').exists() else None,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Automatische Erkennung:**
|
|
||||||
- Icon wird verwendet, falls vorhanden
|
|
||||||
- Versionsinformationen werden verwendet, falls vorhanden
|
|
||||||
- Build funktioniert auch ohne Icon/Version (mit Warnung)
|
|
||||||
|
|
||||||
### installer.iss
|
|
||||||
|
|
||||||
```iss
|
|
||||||
SetupIconFile=dist\DocuMentor\icon.ico
|
|
||||||
UninstallDisplayIcon={app}\DocuMentor.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
Das Icon wird automatisch vom `build_windows.py` nach `dist/DocuMentor/` kopiert.
|
|
||||||
|
|
||||||
## Anpassungen
|
|
||||||
|
|
||||||
### Company Name / Copyright
|
|
||||||
|
|
||||||
In `create_version_info.py` (Zeile ~65):
|
|
||||||
|
|
||||||
```python
|
|
||||||
StringStruct('CompanyName', 'Ihr Name/Organisation'),
|
|
||||||
StringStruct('LegalCopyright', '© {year} Ihr Name. Alle Rechte vorbehalten.'),
|
|
||||||
```
|
|
||||||
|
|
||||||
Ändern Sie "Ihr Name/Organisation" auf Ihren tatsächlichen Namen oder Firmennamen.
|
|
||||||
|
|
||||||
### Icon-Design
|
|
||||||
|
|
||||||
Falls Sie das Standard-Icon anpassen möchten, bearbeiten Sie `create_icon.py`:
|
|
||||||
|
|
||||||
**Farben ändern** (Zeile ~31):
|
|
||||||
```python
|
|
||||||
bg_color = (41, 128, 185) # Blau - ändern Sie RGB-Werte
|
|
||||||
```
|
|
||||||
|
|
||||||
**Symbol ändern:**
|
|
||||||
- Bearbeiten Sie die `create_default_icon()` Funktion
|
|
||||||
- Oder erstellen Sie Ihr eigenes Icon in einem Grafikprogramm
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Icon-Design
|
|
||||||
|
|
||||||
1. **Einfach und klar**: Funktioniert auch bei 16×16 Pixel
|
|
||||||
2. **Hoher Kontrast**: Gut lesbar auf hellem und dunklem Hintergrund
|
|
||||||
3. **Professionell**: Passend zum Business-Kontext
|
|
||||||
4. **Wiedererkennbar**: Symbolisiert die Anwendung
|
|
||||||
|
|
||||||
### Versionierung
|
|
||||||
|
|
||||||
1. **Semantische Versionierung**: MAJOR.MINOR.PATCH
|
|
||||||
2. **Vor jedem Release aktualisieren**
|
|
||||||
3. **Git-Tags verwenden**: `git tag v0.1.0`
|
|
||||||
4. **GUID beibehalten**: Nie die Inno Setup GUID ändern bei Updates!
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Icon wird nicht angezeigt
|
|
||||||
|
|
||||||
**Problem**: DocuMentor.exe zeigt kein Icon
|
|
||||||
|
|
||||||
**Lösungen:**
|
|
||||||
1. Prüfen ob `resources/icon.ico` existiert
|
|
||||||
2. Build neu ausführen: `uv run python build_windows.py`
|
|
||||||
3. Windows Icon-Cache löschen und neu starten
|
|
||||||
|
|
||||||
### Versionsinformationen fehlen
|
|
||||||
|
|
||||||
**Problem**: Eigenschaften → Details zeigt keine Informationen
|
|
||||||
|
|
||||||
**Lösungen:**
|
|
||||||
1. Prüfen ob `version_info.txt` existiert
|
|
||||||
2. `uv run python create_version_info.py` ausführen
|
|
||||||
3. Build neu ausführen
|
|
||||||
|
|
||||||
### Pillow-Fehler beim Icon-Erstellen
|
|
||||||
|
|
||||||
**Problem**: `ImportError: No module named 'PIL'`
|
|
||||||
|
|
||||||
**Lösung:**
|
|
||||||
```bash
|
|
||||||
uv sync --all-groups
|
|
||||||
```
|
|
||||||
|
|
||||||
Dies installiert Pillow automatisch.
|
|
||||||
|
|
||||||
## Weiterführende Informationen
|
|
||||||
|
|
||||||
- **PyInstaller Icon-Dokumentation**: https://pyinstaller.org/en/stable/usage.html#icons
|
|
||||||
- **Windows ICO Format**: https://en.wikipedia.org/wiki/ICO_(file_format)
|
|
||||||
- **Semantic Versioning**: https://semver.org/lang/de/
|
|
||||||
- **Pillow Dokumentation**: https://pillow.readthedocs.io/
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
GUID-Generator für Inno Setup
|
|
||||||
|
|
||||||
Generiert eine eindeutige GUID für die AppId in installer.iss
|
|
||||||
"""
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# GUID generieren
|
|
||||||
guid = uuid.uuid4()
|
|
||||||
guid_str = f"{{{{{str(guid).upper()}}}}}"
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("GUID für Inno Setup AppId")
|
|
||||||
print("=" * 60)
|
|
||||||
print()
|
|
||||||
print("Generierte GUID:")
|
|
||||||
print(f" {guid_str}")
|
|
||||||
print()
|
|
||||||
print("Anleitung:")
|
|
||||||
print("1. Kopieren Sie die GUID oben")
|
|
||||||
print("2. Öffnen Sie installer.iss")
|
|
||||||
print("3. Suchen Sie nach 'AppId={{' (Zeile ~22)")
|
|
||||||
print("4. Ersetzen Sie die Beispiel-GUID mit Ihrer neuen GUID")
|
|
||||||
print()
|
|
||||||
print("Beispiel:")
|
|
||||||
print(f" AppId={guid_str}")
|
|
||||||
print()
|
|
||||||
print("WICHTIG:")
|
|
||||||
print("- Die GUID sollte nur EINMAL beim ersten Setup generiert werden")
|
|
||||||
print("- Ändern Sie die GUID NICHT bei Updates, sonst wird die App")
|
|
||||||
print(" als separate Anwendung installiert!")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
"""
|
|
||||||
WiX ProductFiles.wxs Generator
|
|
||||||
|
|
||||||
Generiert automatisch eine WXS-Datei mit allen Dateien aus dist/DocuMentor.
|
|
||||||
Ersetzt die veraltete 'wix heat' Funktionalität für WiX v6.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
from pathlib import Path
|
|
||||||
from xml.etree import ElementTree as ET
|
|
||||||
|
|
||||||
|
|
||||||
def generate_guid() -> str:
|
|
||||||
"""Generiert eine neue GUID."""
|
|
||||||
return str(uuid.uuid4()).upper()
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_id(name: str) -> str:
|
|
||||||
"""
|
|
||||||
Macht einen String WiX-konform für IDs.
|
|
||||||
|
|
||||||
WiX erlaubt nur: A-Z, a-z, 0-9, _, .
|
|
||||||
Darf nicht mit Zahl beginnen.
|
|
||||||
"""
|
|
||||||
# Ersetze illegale Zeichen
|
|
||||||
sanitized = name.replace("-", "_").replace(" ", "_").replace(".", "_")
|
|
||||||
|
|
||||||
# Entferne alle anderen nicht erlaubten Zeichen
|
|
||||||
allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"
|
|
||||||
sanitized = "".join(c if c in allowed else "_" for c in sanitized)
|
|
||||||
|
|
||||||
# Darf nicht mit Zahl beginnen
|
|
||||||
if sanitized and sanitized[0].isdigit():
|
|
||||||
sanitized = f"_{sanitized}"
|
|
||||||
|
|
||||||
return sanitized
|
|
||||||
|
|
||||||
|
|
||||||
def create_wix_fragment(dist_dir: Path) -> ET.Element:
|
|
||||||
"""
|
|
||||||
Erstellt ein WiX Fragment mit allen Dateien aus dem dist Verzeichnis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dist_dir: Pfad zum dist/DocuMentor Verzeichnis
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ET.Element: Wix Root-Element mit Fragment
|
|
||||||
"""
|
|
||||||
# Namespace (WiX v4+)
|
|
||||||
ns = "http://wixtoolset.org/schemas/v4/wxs"
|
|
||||||
ET.register_namespace("", ns)
|
|
||||||
|
|
||||||
# Root Element
|
|
||||||
wix = ET.Element(f"{{{ns}}}Wix")
|
|
||||||
fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
|
|
||||||
component_group = ET.SubElement(
|
|
||||||
fragment, f"{{{ns}}}ComponentGroup", {"Id": "ProductComponents", "Directory": "INSTALLFOLDER"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sammle alle Dateien
|
|
||||||
all_files = sorted(dist_dir.rglob("*"))
|
|
||||||
files_only = [f for f in all_files if f.is_file()]
|
|
||||||
|
|
||||||
print(f"Gefunden: {len(files_only)} Dateien")
|
|
||||||
|
|
||||||
# Gruppiere nach Verzeichnis
|
|
||||||
dirs_dict: dict[Path, list[Path]] = {}
|
|
||||||
for file in files_only:
|
|
||||||
rel_dir = file.parent.relative_to(dist_dir)
|
|
||||||
if rel_dir not in dirs_dict:
|
|
||||||
dirs_dict[rel_dir] = []
|
|
||||||
dirs_dict[rel_dir].append(file)
|
|
||||||
|
|
||||||
# Erstelle Directory Fragments
|
|
||||||
dir_fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
|
|
||||||
|
|
||||||
# INSTALLFOLDER ist bereits in DocuMentor.wxs definiert
|
|
||||||
directory_ref = ET.SubElement(dir_fragment, f"{{{ns}}}DirectoryRef", {"Id": "INSTALLFOLDER"})
|
|
||||||
|
|
||||||
# Erstelle Verzeichnisstruktur
|
|
||||||
created_dirs = {"INSTALLFOLDER": directory_ref}
|
|
||||||
|
|
||||||
for rel_dir in sorted(dirs_dict.keys()):
|
|
||||||
if rel_dir == Path("."):
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = rel_dir.parts
|
|
||||||
parent_id = "INSTALLFOLDER"
|
|
||||||
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
current_path = Path(*parts[: i + 1])
|
|
||||||
dir_id = f"Dir_{sanitize_id(current_path.as_posix().replace('/', '_'))}"
|
|
||||||
|
|
||||||
if dir_id not in created_dirs:
|
|
||||||
parent_elem = created_dirs[parent_id]
|
|
||||||
new_dir = ET.SubElement(parent_elem, f"{{{ns}}}Directory", {"Id": dir_id, "Name": part})
|
|
||||||
created_dirs[dir_id] = new_dir
|
|
||||||
parent_id = dir_id
|
|
||||||
else:
|
|
||||||
parent_id = dir_id
|
|
||||||
|
|
||||||
# Füge Komponenten hinzu
|
|
||||||
component_counter = 0
|
|
||||||
|
|
||||||
for rel_dir, files in sorted(dirs_dict.items()):
|
|
||||||
if rel_dir == Path("."):
|
|
||||||
dir_id = "INSTALLFOLDER"
|
|
||||||
else:
|
|
||||||
dir_id = f"Dir_{sanitize_id(rel_dir.as_posix().replace('/', '_'))}"
|
|
||||||
|
|
||||||
# Erstelle eine Komponente pro Verzeichnis
|
|
||||||
component_id = f"Component_{sanitize_id(dir_id)}"
|
|
||||||
component_counter += 1
|
|
||||||
|
|
||||||
component = ET.SubElement(
|
|
||||||
component_group, f"{{{ns}}}Component", {"Id": component_id, "Directory": dir_id, "Guid": generate_guid()}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Füge alle Dateien des Verzeichnisses hinzu
|
|
||||||
for idx, file in enumerate(files):
|
|
||||||
file_id = f"File_{sanitize_id(component_id)}_{idx}"
|
|
||||||
# Absoluter Pfad für WiX
|
|
||||||
source_path = str(file).replace("/", "\\")
|
|
||||||
|
|
||||||
file_attribs = {"Id": file_id, "Source": source_path, "Name": file.name}
|
|
||||||
|
|
||||||
# Erste Datei ist KeyPath
|
|
||||||
if idx == 0:
|
|
||||||
file_attribs["KeyPath"] = "yes"
|
|
||||||
|
|
||||||
ET.SubElement(component, f"{{{ns}}}File", file_attribs)
|
|
||||||
|
|
||||||
print(f"Erstellt: {component_counter} Komponenten")
|
|
||||||
|
|
||||||
return wix
|
|
||||||
|
|
||||||
|
|
||||||
def format_xml(element: ET.Element, level: int = 0) -> None:
|
|
||||||
"""Formatiert XML mit Einrückungen (in-place)."""
|
|
||||||
indent = " "
|
|
||||||
i = f"\n{indent * level}"
|
|
||||||
|
|
||||||
if len(element):
|
|
||||||
if not element.text or not element.text.strip():
|
|
||||||
element.text = i + indent
|
|
||||||
if not element.tail or not element.tail.strip():
|
|
||||||
element.tail = i
|
|
||||||
last_child = None
|
|
||||||
for child in element:
|
|
||||||
format_xml(child, level + 1)
|
|
||||||
last_child = child
|
|
||||||
if last_child is not None and (not last_child.tail or not last_child.tail.strip()):
|
|
||||||
last_child.tail = i
|
|
||||||
else:
|
|
||||||
if level and (not element.tail or not element.tail.strip()):
|
|
||||||
element.tail = i
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Hauptfunktion."""
|
|
||||||
dist_dir = Path("dist/DocuMentor")
|
|
||||||
output_file = Path("ProductFiles.wxs")
|
|
||||||
|
|
||||||
if not dist_dir.exists():
|
|
||||||
print(f"FEHLER: {dist_dir} existiert nicht!")
|
|
||||||
print("Führe zuerst 'uv run pyinstaller DocuMentor.spec' aus.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Generiere ProductFiles.wxs aus {dist_dir}...")
|
|
||||||
|
|
||||||
wix_root = create_wix_fragment(dist_dir)
|
|
||||||
format_xml(wix_root)
|
|
||||||
|
|
||||||
# Schreibe XML
|
|
||||||
tree = ET.ElementTree(wix_root)
|
|
||||||
tree.write(output_file, encoding="utf-8", xml_declaration=True)
|
|
||||||
|
|
||||||
print(f"[OK] {output_file} erfolgreich erstellt!")
|
|
||||||
print(f"\nNaechste Schritte:")
|
|
||||||
print(f"1. wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
; Inno Setup Konfiguration für DocuMentor
|
|
||||||
; Erstellt eine professionelle Setup.exe für Windows
|
|
||||||
;
|
|
||||||
; Installation von Inno Setup: https://jrsoftware.org/isdl.php
|
|
||||||
;
|
|
||||||
; WICHTIG: Vor dem ersten Build GUID generieren!
|
|
||||||
; python -c "import uuid; print(f'{{{{' + str(uuid.uuid4()).upper() + '}}}}')"
|
|
||||||
; Ergebnis in AppId unten einfügen
|
|
||||||
;
|
|
||||||
; Build-Befehl: iscc installer.iss
|
|
||||||
|
|
||||||
#define MyAppName "DocuMentor"
|
|
||||||
#define MyAppVersion "1.6.4"
|
|
||||||
#define MyAppPublisher "Ihr Name/Organisation"
|
|
||||||
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
|
||||||
#define MyAppExeName "DocuMentor.exe"
|
|
||||||
|
|
||||||
[Setup]
|
|
||||||
; Basis-Informationen
|
|
||||||
; WICHTIG: Ersetzen Sie die GUID mit einer eigenen generierten GUID!
|
|
||||||
; AppId={{BEISPIEL-GUID-HIER-EINFÜGEN}}
|
|
||||||
AppId={{A1B2C3D4-E5F6-4789-ABCD-EF0123456789}}
|
|
||||||
AppName={#MyAppName}
|
|
||||||
AppVersion={#MyAppVersion}
|
|
||||||
AppPublisher={#MyAppPublisher}
|
|
||||||
AppPublisherURL={#MyAppURL}
|
|
||||||
AppSupportURL={#MyAppURL}
|
|
||||||
AppUpdatesURL={#MyAppURL}
|
|
||||||
|
|
||||||
; Installation
|
|
||||||
DefaultDirName={autopf}\{#MyAppName}
|
|
||||||
DefaultGroupName={#MyAppName}
|
|
||||||
AllowNoIcons=yes
|
|
||||||
|
|
||||||
; Output
|
|
||||||
OutputDir=dist\installer
|
|
||||||
OutputBaseFilename=DocuMentor-Setup-{#MyAppVersion}
|
|
||||||
SetupIconFile=dist\DocuMentor\icon.ico
|
|
||||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
|
||||||
|
|
||||||
; Kompression
|
|
||||||
Compression=lzma
|
|
||||||
SolidCompression=yes
|
|
||||||
|
|
||||||
; Moderne UI
|
|
||||||
WizardStyle=modern
|
|
||||||
|
|
||||||
; Rechte (normal für User-Installation, admin für System-weite Installation)
|
|
||||||
PrivilegesRequired=lowest
|
|
||||||
PrivilegesRequiredOverridesAllowed=dialog
|
|
||||||
|
|
||||||
[Languages]
|
|
||||||
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
|
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
|
||||||
|
|
||||||
[Tasks]
|
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
|
||||||
|
|
||||||
[Files]
|
|
||||||
; Alle Dateien aus dem PyInstaller-Build
|
|
||||||
Source: "dist\DocuMentor\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
||||||
|
|
||||||
[Icons]
|
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
|
||||||
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
|
||||||
|
|
||||||
[Run]
|
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
|
||||||
|
|
||||||
[Messages]
|
|
||||||
; Deutsche Anpassungen
|
|
||||||
german.WelcomeLabel2=Dies wird [name/ver] auf Ihrem Computer installieren.%n%nBitte stellen Sie sicher, dass folgende externe Tools installiert sind:%n• Java Runtime Environment (JRE)%n• Apache FOP%n• Saxon XSLT-Prozessor%n• diff-pdf
|
|
||||||
+9
-14
@@ -1,18 +1,15 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "DocuMentor"
|
name = "DocuMentor"
|
||||||
version = "1.6.4"
|
version = "0.1.0"
|
||||||
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
requires-python = ">=3.13"
|
||||||
requires-python = ">=3.13,<3.15"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic-settings>=2.12.0",
|
"pyqtdarktheme>=2.1.0",
|
||||||
"pyside6>=6.10.1",
|
"pydantic-settings>=2.9.1",
|
||||||
"polars[connectorx,pyarrow]>=1.37.0",
|
"pyside6>=6.9.1",
|
||||||
"connectorx>=0.4.0",
|
"polars[connectorx,pyarrow]>=1.31.0",
|
||||||
"pydantic-yaml>=1.6.0",
|
"pydantic-yaml>=1.5.1",
|
||||||
"psutil>=6.1.1",
|
|
||||||
"lxml>=6.0.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@@ -27,7 +24,5 @@ extend-exclude = ["*_ui.py"]
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff>=0.14.11",
|
"ruff>=0.14.8",
|
||||||
"pyinstaller>=6.0.0",
|
|
||||||
"pillow>=10.0.0",
|
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 678 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 819 KiB |
@@ -1,64 +0,0 @@
|
|||||||
# Resources für DocuMentor
|
|
||||||
|
|
||||||
Dieses Verzeichnis enthält Ressourcen für den Windows-Build.
|
|
||||||
|
|
||||||
## Icon (icon.ico)
|
|
||||||
|
|
||||||
Das Icon wird verwendet für:
|
|
||||||
- Windows-Executable (DocuMentor.exe)
|
|
||||||
- Inno Setup Installer
|
|
||||||
- Desktop-Verknüpfungen
|
|
||||||
- Start-Menü-Einträge
|
|
||||||
|
|
||||||
### Icon erstellen
|
|
||||||
|
|
||||||
#### Automatisch (Standard-Icon):
|
|
||||||
```bash
|
|
||||||
python create_icon.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Dies erstellt ein einfaches Standard-Icon mit DocuMentor-Branding.
|
|
||||||
|
|
||||||
#### Aus eigenem PNG-Bild:
|
|
||||||
```bash
|
|
||||||
python create_icon.py mein-icon.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Ihr PNG sollte idealerweise:
|
|
||||||
- Mindestens 256x256 Pixel groß sein
|
|
||||||
- Quadratisch sein
|
|
||||||
- Transparenten Hintergrund haben (optional)
|
|
||||||
|
|
||||||
### Icon-Anforderungen
|
|
||||||
|
|
||||||
Das `.ico`-Dateiformat enthält mehrere Auflösungen:
|
|
||||||
- 256x256 (Windows 7+, Taskleiste)
|
|
||||||
- 128x128
|
|
||||||
- 64x64
|
|
||||||
- 48x48 (Standard Desktop-Icon)
|
|
||||||
- 32x32 (Explorer Details)
|
|
||||||
- 16x16 (kleines Icon)
|
|
||||||
|
|
||||||
Das `create_icon.py` Skript erstellt automatisch alle diese Größen.
|
|
||||||
|
|
||||||
## Icon manuell ersetzen
|
|
||||||
|
|
||||||
1. Eigenes Icon als `resources/icon.ico` speichern
|
|
||||||
2. Oder mit einem Online-Tool PNG→ICO konvertieren
|
|
||||||
3. Build-Skript verwendet automatisch die vorhandene Datei
|
|
||||||
|
|
||||||
## Design-Richtlinien
|
|
||||||
|
|
||||||
Falls Sie ein eigenes Icon erstellen:
|
|
||||||
- **Einfach und klar**: Funktioniert auch in kleinen Größen (16x16)
|
|
||||||
- **Professionell**: Passend zum Business-Kontext
|
|
||||||
- **Wiedererkennbar**: DocuMentor steht für Dokumenten-Management
|
|
||||||
- **Kontrast**: Gut sichtbar auf hellem und dunklem Hintergrund
|
|
||||||
|
|
||||||
## Weitere Ressourcen
|
|
||||||
|
|
||||||
In diesem Verzeichnis können später weitere Ressourcen abgelegt werden:
|
|
||||||
- Splash-Screen-Bilder
|
|
||||||
- Toolbar-Icons
|
|
||||||
- Dokumentations-Bilder
|
|
||||||
- etc.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
Erstelle ein professionelles Icon für eine Desktop-Anwendung namens "DocuMentor".
|
|
||||||
|
|
||||||
Die Anwendung wird verwendet für:
|
|
||||||
- Verwaltung von XSL-Transformationen
|
|
||||||
- Umwandlung von XML-Dokumenten zu PDF-Dateien
|
|
||||||
- Vergleich und Validierung von PDF-Dokumenten
|
|
||||||
|
|
||||||
Design-Anforderungen:
|
|
||||||
|
|
||||||
1. Stil: Minimalistisch, modern, professionell, business-orientiert
|
|
||||||
|
|
||||||
2. Farben:
|
|
||||||
- Hauptfarbe: Blau (#2980B9 oder ähnlich)
|
|
||||||
- Akzentfarbe: Weiß oder helles Grau
|
|
||||||
- Maximal 2-3 Farben insgesamt
|
|
||||||
|
|
||||||
3. Elemente (wähle eine Kombination):
|
|
||||||
- Dokument-Symbol (Papier/Seite)
|
|
||||||
- Transformation/Workflow-Element (Pfeil, Zahnrad)
|
|
||||||
- Optional: Stilisierter Buchstabe "M" oder "D"
|
|
||||||
|
|
||||||
4. Technische Anforderungen:
|
|
||||||
- Quadratisches Format
|
|
||||||
- Einfache, klare Linien
|
|
||||||
- Hoher Kontrast
|
|
||||||
- Muss auch bei 16x16 Pixel noch erkennbar sein
|
|
||||||
- Flat Design (keine 3D-Effekte)
|
|
||||||
- Keine Farbverläufe
|
|
||||||
|
|
||||||
5. Hintergrund:
|
|
||||||
- Transparent ODER
|
|
||||||
- Einfarbig (blau oder weiß)
|
|
||||||
|
|
||||||
6. Referenz-Stil:
|
|
||||||
- Ähnlich wie Microsoft Office Icons
|
|
||||||
- Ähnlich wie Visual Studio Code Icons
|
|
||||||
- Moderne SaaS-Anwendungs-Icons
|
|
||||||
|
|
||||||
Bitte erstelle ein Icon, das:
|
|
||||||
- Professionell und vertrauenswürdig wirkt
|
|
||||||
- Für technische Anwender geeignet ist
|
|
||||||
- Gut in einer Windows-Taskleiste aussieht
|
|
||||||
- Auch als Desktop-Verknüpfung funktioniert
|
|
||||||
|
|
||||||
Format: PNG oder SVG, mindestens 512x512 Pixel
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
@@ -1,150 +0,0 @@
|
|||||||
# Icon-Prompt für Bild-Generierungs-KI
|
|
||||||
|
|
||||||
## DocuMentor Logo/Icon
|
|
||||||
|
|
||||||
### Deutsche Version (für deutsche KI-Tools)
|
|
||||||
|
|
||||||
```
|
|
||||||
Erstelle ein professionelles, minimalistisches SVG-Icon für eine Business-Software namens "DocuMentor".
|
|
||||||
|
|
||||||
Anwendungsbeschreibung:
|
|
||||||
DocuMentor ist eine Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen. Die Software wird von technischen Redakteuren und Entwicklern verwendet, um XML-Dokumente in PDF-Dateien zu transformieren und diese zu vergleichen.
|
|
||||||
|
|
||||||
Design-Anforderungen:
|
|
||||||
- Stil: Professionell, modern, technisch, business-orientiert
|
|
||||||
- Farben: Blaue Töne (z.B. #2980B9, #3498DB) kombiniert mit neutralen Grautönen oder Weiß
|
|
||||||
- Elemente: Kombination aus Dokumenten-Symbol und Transformations-/Workflow-Elementen
|
|
||||||
- Einfachheit: Muss auch in sehr kleinen Größen (16x16 Pixel) erkennbar sein
|
|
||||||
- Klare Linien und hoher Kontrast
|
|
||||||
|
|
||||||
Symbolik-Vorschläge:
|
|
||||||
- Ein Dokument mit Transformations-Pfeilen
|
|
||||||
- Gestapelte/verschachtelte Dokumente (XML → XSLT → PDF)
|
|
||||||
- Stilisiertes "D" oder "M" für DocuMentor
|
|
||||||
- Workflow-Diagramm mit Dokumenten-Symbolen
|
|
||||||
- Dokument mit Zahnrad (Verarbeitung/Transformation)
|
|
||||||
|
|
||||||
Format: Vektorgrafik (SVG), quadratisch (1:1 Verhältnis), 512x512 Pixel oder größer
|
|
||||||
Hintergrund: Transparent oder einfarbig (blau/weiß)
|
|
||||||
|
|
||||||
Stil-Referenzen: Microsoft Office Icons, Adobe Creative Cloud Icons, moderne SaaS-Anwendungen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Englische Version (für internationale KI-Tools wie DALL-E, Midjourney, Stable Diffusion)
|
|
||||||
|
|
||||||
```
|
|
||||||
Create a professional, minimalist SVG icon for business software called "DocuMentor".
|
|
||||||
|
|
||||||
Application Description:
|
|
||||||
DocuMentor is a desktop application for managing and validating XSL transformations. The software is used by technical writers and developers to transform XML documents into PDF files and compare them.
|
|
||||||
|
|
||||||
Design Requirements:
|
|
||||||
- Style: Professional, modern, technical, business-oriented
|
|
||||||
- Colors: Blue tones (e.g., #2980B9, #3498DB) combined with neutral grays or white
|
|
||||||
- Elements: Combination of document symbol and transformation/workflow elements
|
|
||||||
- Simplicity: Must be recognizable even at very small sizes (16x16 pixels)
|
|
||||||
- Clean lines and high contrast
|
|
||||||
|
|
||||||
Symbolism Suggestions:
|
|
||||||
- A document with transformation arrows
|
|
||||||
- Stacked/nested documents (XML → XSLT → PDF)
|
|
||||||
- Stylized "D" or "M" for DocuMentor
|
|
||||||
- Workflow diagram with document symbols
|
|
||||||
- Document with gear icon (processing/transformation)
|
|
||||||
|
|
||||||
Format: Vector graphic (SVG), square (1:1 ratio), 512x512 pixels or larger
|
|
||||||
Background: Transparent or solid color (blue/white)
|
|
||||||
|
|
||||||
Style References: Microsoft Office icons, Adobe Creative Cloud icons, modern SaaS applications
|
|
||||||
|
|
||||||
Additional Instructions:
|
|
||||||
- Flat design, not 3D
|
|
||||||
- No gradients or complex shadows
|
|
||||||
- Maximum 3 colors
|
|
||||||
- Geometric shapes preferred
|
|
||||||
- Professional and trustworthy appearance
|
|
||||||
```
|
|
||||||
|
|
||||||
### Alternativer Prompt (detaillierter für KIs wie ChatGPT mit DALL-E)
|
|
||||||
|
|
||||||
```
|
|
||||||
Design a minimalist icon for "DocuMentor" - a professional XML/XSL transformation management software.
|
|
||||||
|
|
||||||
Concept: A clean, modern icon that combines:
|
|
||||||
1. A document/page symbol (representing XML/PDF files)
|
|
||||||
2. An element suggesting transformation or workflow (arrows, gears, or connecting lines)
|
|
||||||
3. Professional color scheme: Primary blue (#2980B9) with white/gray accents
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
- Vector style, flat design
|
|
||||||
- Must work well at 16x16, 48x48, and 256x256 pixels
|
|
||||||
- High contrast for visibility
|
|
||||||
- No text, icon only
|
|
||||||
- Square format (512x512px minimum)
|
|
||||||
- Transparent background preferred
|
|
||||||
|
|
||||||
Style inspiration: Think Microsoft Office 365 icons, VS Code icons, or modern productivity app icons - clean, professional, instantly recognizable.
|
|
||||||
|
|
||||||
Technical constraints:
|
|
||||||
- Simple enough to work as a favicon
|
|
||||||
- Clear silhouette when shown in monochrome
|
|
||||||
- Distinctive enough to stand out in a taskbar or dock
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prompt für spezifische Konzepte
|
|
||||||
|
|
||||||
### Konzept 1: Dokument mit Transformation
|
|
||||||
|
|
||||||
```
|
|
||||||
A minimalist icon showing a document page with a curved arrow pointing to another document, symbolizing transformation. Blue (#2980B9) and white color scheme. Flat design, professional, suitable for business software. SVG style, 512x512px, transparent background.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Konzept 2: Gestapelte Dokumente
|
|
||||||
|
|
||||||
```
|
|
||||||
An icon with three overlapping document sheets in a cascading arrangement, representing XML to XSL to PDF transformation workflow. Modern flat design, blue gradient (#3498DB to #2980B9), white accents. Professional business software icon. 512x512px SVG format.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Konzept 3: Dokument + Zahnrad
|
|
||||||
|
|
||||||
```
|
|
||||||
A clean icon combining a document page with a small gear/cog symbol in the corner, representing document processing. Minimalist design, blue (#2980B9) on white background. Professional style like Microsoft Office icons. 512x512px, vector art, high contrast.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Konzept 4: Stilisiertes "M"
|
|
||||||
|
|
||||||
```
|
|
||||||
A stylized letter "M" for "Mentor" integrated with document/page elements. Modern, geometric, professional. Blue (#2980B9) color scheme. Suitable for small sizes. Flat design, vector style, 512x512px, transparent background.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
1. Wähle einen der Prompts oben
|
|
||||||
2. Füge ihn in eine Bild-Generierungs-KI ein:
|
|
||||||
- **DALL-E 3** (ChatGPT Plus): Englischer Prompt empfohlen
|
|
||||||
- **Midjourney**: Englischer Prompt, evtl. kürzer
|
|
||||||
- **Adobe Firefly**: Deutscher oder englischer Prompt
|
|
||||||
- **Stable Diffusion**: Englischer Prompt mit detaillierten Tags
|
|
||||||
- **Leonardo.ai**: Englischer Prompt
|
|
||||||
|
|
||||||
3. Lade das generierte Bild herunter (idealerweise als PNG)
|
|
||||||
|
|
||||||
4. Konvertiere zu ICO:
|
|
||||||
```bash
|
|
||||||
uv run python create_icon.py generiertes-icon.png
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tipps für beste Ergebnisse
|
|
||||||
|
|
||||||
- **Iteriere**: Generiere mehrere Varianten
|
|
||||||
- **Einfachheit**: Betone "minimalist", "simple", "clean"
|
|
||||||
- **Größe**: Teste das Icon in verschiedenen Größen
|
|
||||||
- **Kontrast**: Achte auf gute Sichtbarkeit auf hellem und dunklem Hintergrund
|
|
||||||
- **Professionalität**: Vermeide zu verspielte oder kindliche Designs
|
|
||||||
|
|
||||||
## Nachbearbeitung
|
|
||||||
|
|
||||||
Falls die KI kein perfektes SVG erstellt:
|
|
||||||
1. PNG exportieren (hohe Auflösung, mind. 512x512px)
|
|
||||||
2. Mit Inkscape oder Adobe Illustrator zu SVG konvertieren
|
|
||||||
3. Oder direkt als PNG verwenden und mit `create_icon.py` konvertieren
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 605 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
+25
-85
@@ -73,72 +73,6 @@ class SSLMode(str, Enum):
|
|||||||
VERIFY_FULL = "verify-full"
|
VERIFY_FULL = "verify-full"
|
||||||
|
|
||||||
|
|
||||||
class XsltVersion(str, Enum):
|
|
||||||
"""XSLT-Version für Saxon-Transformationen."""
|
|
||||||
|
|
||||||
XSLT_1_0 = "1.0" # JAXP API (nur XSLT 1.0)
|
|
||||||
XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0)
|
|
||||||
|
|
||||||
|
|
||||||
class GraphLayout(str, Enum):
|
|
||||||
"""vis.js Physics-Solver / Layout-Modus."""
|
|
||||||
|
|
||||||
BARNES_HUT = "barnesHut"
|
|
||||||
FORCE_ATLAS2 = "forceAtlas2Based"
|
|
||||||
REPULSION = "repulsion"
|
|
||||||
HIERARCHICAL = "hierarchical"
|
|
||||||
|
|
||||||
|
|
||||||
class HierarchicalDirection(str, Enum):
|
|
||||||
"""Richtung für hierarchisches Layout."""
|
|
||||||
|
|
||||||
UD = "UD"
|
|
||||||
DU = "DU"
|
|
||||||
LR = "LR"
|
|
||||||
RL = "RL"
|
|
||||||
|
|
||||||
|
|
||||||
class HierarchicalSortMethod(str, Enum):
|
|
||||||
"""Sortiermethode für hierarchisches Layout."""
|
|
||||||
|
|
||||||
HUBSIZE = "hubsize"
|
|
||||||
DIRECTED = "directed"
|
|
||||||
|
|
||||||
|
|
||||||
class GraphLayoutSettings(BaseModel):
|
|
||||||
"""Persistierte vis.js Layout-Einstellungen für den XSL-Abhängigkeitsgraph."""
|
|
||||||
|
|
||||||
layout: GraphLayout = GraphLayout.BARNES_HUT
|
|
||||||
|
|
||||||
# barnesHut
|
|
||||||
bh_gravitational_constant: int = -3000
|
|
||||||
bh_central_gravity: float = 0.3
|
|
||||||
bh_spring_length: int = 150
|
|
||||||
bh_spring_constant: float = 0.04
|
|
||||||
bh_damping: float = 0.09
|
|
||||||
|
|
||||||
# forceAtlas2Based
|
|
||||||
fa_gravitational_constant: int = -50
|
|
||||||
fa_central_gravity: float = 0.01
|
|
||||||
fa_spring_length: int = 100
|
|
||||||
fa_spring_constant: float = 0.08
|
|
||||||
fa_damping: float = 0.4
|
|
||||||
|
|
||||||
# repulsion
|
|
||||||
re_node_distance: int = 120
|
|
||||||
re_central_gravity: float = 0.0
|
|
||||||
re_spring_length: int = 200
|
|
||||||
re_spring_constant: float = 0.05
|
|
||||||
re_damping: float = 0.09
|
|
||||||
|
|
||||||
# hierarchical
|
|
||||||
hi_direction: HierarchicalDirection = HierarchicalDirection.UD
|
|
||||||
hi_sort_method: HierarchicalSortMethod = HierarchicalSortMethod.HUBSIZE
|
|
||||||
hi_level_separation: int = 150
|
|
||||||
hi_node_spacing: int = 100
|
|
||||||
hi_tree_spacing: int = 200
|
|
||||||
|
|
||||||
|
|
||||||
class PostgreSqlDb(BaseModel):
|
class PostgreSqlDb(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
@@ -148,7 +82,6 @@ class PostgreSqlDb(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
ssl_mode: SSLMode = SSLMode.PREFER
|
ssl_mode: SSLMode = SSLMode.PREFER
|
||||||
timeout: int = 10
|
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel):
|
class Project(BaseModel):
|
||||||
@@ -162,31 +95,42 @@ class Project(BaseModel):
|
|||||||
xsl_dir_id: int = Field(..., description="ID des XSL-Verzeichnisses", gt=0)
|
xsl_dir_id: int = Field(..., description="ID des XSL-Verzeichnisses", gt=0)
|
||||||
postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0)
|
postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0)
|
||||||
fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis")
|
fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis")
|
||||||
xslt_params: dict[str, str] = Field(default_factory=dict, description="Projektweite XSLT-Parameter")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _lookup(collection, item_id: int, attr: str) -> str:
|
|
||||||
"""Sucht einen Wert in einer Konfigurationsliste anhand der ID."""
|
|
||||||
value = [getattr(x, attr) for x in collection if x.id == item_id]
|
|
||||||
return value[0] if value else ""
|
|
||||||
|
|
||||||
def getXsl(self) -> str:
|
def getXsl(self) -> str:
|
||||||
return self._lookup(app_settings.xsl_dirs, self.xsl_dir_id, "name")
|
global app_settings
|
||||||
|
value = [x.name for x in app_settings.xsl_dirs if x.id == self.xsl_dir_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
def getJavaVm(self) -> str:
|
def getJavaVm(self) -> str:
|
||||||
return self._lookup(app_settings.java_vms, self.java_vm_id, "version")
|
global app_settings
|
||||||
|
value = [x.version for x in app_settings.java_vms if x.id == self.java_vm_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
def getSaxon(self) -> str:
|
def getSaxon(self) -> str:
|
||||||
return self._lookup(app_settings.saxon_jars, self.saxon_jar_id, "version")
|
global app_settings
|
||||||
|
value = [x.version for x in app_settings.saxon_jars if x.id == self.saxon_jar_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
def getApacheFop(self) -> str:
|
def getApacheFop(self) -> str:
|
||||||
return self._lookup(app_settings.apache_fops, self.apache_fop_id, "version")
|
global app_settings
|
||||||
|
value = [x.version for x in app_settings.apache_fops if x.id == self.apache_fop_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
def getDiffPdf(self) -> str:
|
def getDiffPdf(self) -> str:
|
||||||
return self._lookup(app_settings.diff_pdfs, self.diff_pdf_id, "version")
|
global app_settings
|
||||||
|
value = [x.version for x in app_settings.diff_pdfs if x.id == self.diff_pdf_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
def getPostgreSqlDb(self) -> str:
|
def getPostgreSqlDb(self) -> str:
|
||||||
return self._lookup(app_settings.postgresql_dbs, self.postgre_sql_db_id, "name")
|
global app_settings
|
||||||
|
value = [x.name for x in app_settings.postgresql_dbs if x.id == self.postgre_sql_db_id]
|
||||||
|
|
||||||
|
return value[0] if len(value) else ""
|
||||||
|
|
||||||
|
|
||||||
class AppSettings(BaseSettings):
|
class AppSettings(BaseSettings):
|
||||||
@@ -199,15 +143,11 @@ class AppSettings(BaseSettings):
|
|||||||
postgresql_dbs: list[PostgreSqlDb] = []
|
postgresql_dbs: list[PostgreSqlDb] = []
|
||||||
theme: str | None = None
|
theme: str | None = None
|
||||||
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
|
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
|
||||||
use_saxon_worker_pool: bool = True # SaxonWorkerPool aktivieren (schneller, benötigt JDK)
|
|
||||||
saxon_xslt_version: XsltVersion = XsltVersion.XSLT_2_0_3_0 # XSLT-Version für Saxon (Standard: 2.0/3.0 mit s9api)
|
|
||||||
use_fop_worker_pool: bool = True # FopWorkerPool aktivieren (schneller, benötigt JDK)
|
|
||||||
|
|
||||||
# UI-Zustand
|
# UI-Zustand
|
||||||
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
||||||
splitter_sizes: list[int] | None = None # Splitter-Positionen
|
splitter_sizes: list[int] | None = None # Splitter-Positionen
|
||||||
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
|
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
|
||||||
graph_layout_settings: GraphLayoutSettings = Field(default_factory=GraphLayoutSettings)
|
|
||||||
|
|
||||||
model_config = SettingsConfigDict(json_file=config_path)
|
model_config = SettingsConfigDict(json_file=config_path)
|
||||||
|
|
||||||
@@ -223,6 +163,7 @@ class AppSettings(BaseSettings):
|
|||||||
return (JsonConfigSettingsSource(settings_cls),)
|
return (JsonConfigSettingsSource(settings_cls),)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
global config_path
|
||||||
# Ordner existert nicht
|
# Ordner existert nicht
|
||||||
if not config_path.parent.exists():
|
if not config_path.parent.exists():
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -266,7 +207,6 @@ class ProjectData(BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
nodes: list[TreeNode] = []
|
nodes: list[TreeNode] = []
|
||||||
expanded_nodes: list[tuple] | None = None # Optional: IDs der aufgeklappten Knoten (TreeNode und XslFile)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def readSettings(cls, project_dir: Path):
|
def readSettings(cls, project_dir: Path):
|
||||||
|
|||||||
-282
@@ -1,282 +0,0 @@
|
|||||||
"""
|
|
||||||
FOP Worker Pool - Persistente JVM-Prozesse für schnelle PDF-Generierung.
|
|
||||||
|
|
||||||
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
|
|
||||||
Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen nacheinander.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from worker_pool_base import BaseWorkerPool, _CLASSPATH_SEP
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
|
||||||
FOP_WORKER_JAVA = """
|
|
||||||
import org.apache.fop.apps.*;
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
import javax.xml.transform.*;
|
|
||||||
import javax.xml.transform.sax.SAXResult;
|
|
||||||
import javax.xml.transform.stream.StreamSource;
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
public class FopWorker {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
|
||||||
String line;
|
|
||||||
|
|
||||||
System.err.println("FopWorker starting...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Create FopFactory once and reuse (major performance boost!)
|
|
||||||
FopFactory fopFactory = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if config file is provided as first argument
|
|
||||||
if (args.length > 0 && !args[0].isEmpty()) {
|
|
||||||
File configFile = new File(args[0]);
|
|
||||||
if (configFile.exists()) {
|
|
||||||
System.err.println("Loading FOP config: " + configFile.getAbsolutePath());
|
|
||||||
fopFactory = FopFactory.newInstance(configFile);
|
|
||||||
} else {
|
|
||||||
System.err.println("Config file not found, using default configuration");
|
|
||||||
fopFactory = FopFactory.newInstance(new File(".").toURI());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("No config file specified, using default FOP configuration");
|
|
||||||
fopFactory = FopFactory.newInstance(new File(".").toURI());
|
|
||||||
}
|
|
||||||
|
|
||||||
System.err.println("FopWorker started and ready");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("FATAL: Failed to initialize FopFactory: " + e.getMessage());
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
if ("EXIT".equals(line.trim())) {
|
|
||||||
System.err.println("FopWorker exiting");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse job
|
|
||||||
System.err.println("DEBUG: Parsing job...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
String[] parts = line.split("\\t");
|
|
||||||
System.err.println("DEBUG: Parts count: " + parts.length);
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
if (parts.length < 2) {
|
|
||||||
System.out.println("ERROR: Invalid job format");
|
|
||||||
System.out.flush();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String inputFo = parts[0];
|
|
||||||
String outputPdf = parts[1];
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Input FO: " + inputFo);
|
|
||||||
System.err.println("DEBUG: Output PDF: " + outputPdf);
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Create FOUserAgent for this transformation
|
|
||||||
FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
|
|
||||||
|
|
||||||
// Note: Event Listener für detailliertes Error-Logging könnte hier hinzugefügt werden,
|
|
||||||
// aber ist nicht kritisch - Fehler werden durch Exceptions gefangen
|
|
||||||
|
|
||||||
// Create output stream
|
|
||||||
File outputFile = new File(outputPdf);
|
|
||||||
outputFile.getParentFile().mkdirs();
|
|
||||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile));
|
|
||||||
|
|
||||||
try {
|
|
||||||
System.err.println("DEBUG: Creating Fop instance...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Create Fop instance
|
|
||||||
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Setting up transformer...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Setup Transformer
|
|
||||||
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
|
||||||
Transformer transformer = transformerFactory.newTransformer();
|
|
||||||
|
|
||||||
// Setup input and output
|
|
||||||
Source src = new StreamSource(new File(inputFo));
|
|
||||||
Result res = new SAXResult(fop.getDefaultHandler());
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Running FOP transformation...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Run transformation
|
|
||||||
transformer.transform(src, res);
|
|
||||||
|
|
||||||
System.err.println("DEBUG: FOP transformation completed");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transformation erfolgreich
|
|
||||||
System.out.println("OK");
|
|
||||||
System.out.flush();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
|
|
||||||
System.err.flush();
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
|
|
||||||
String errorMsg = e.getMessage();
|
|
||||||
if (errorMsg == null || errorMsg.isEmpty()) {
|
|
||||||
errorMsg = e.getClass().getSimpleName();
|
|
||||||
}
|
|
||||||
System.out.println("ERROR: " + errorMsg);
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println("FopWorker I/O error: " + e.getMessage());
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FopWorkerPool(BaseWorkerPool):
|
|
||||||
"""
|
|
||||||
Pool von lang-laufenden JVM-Prozessen für Apache FOP PDF-Generierung.
|
|
||||||
|
|
||||||
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
num_workers: int,
|
|
||||||
java_vm_path: Path,
|
|
||||||
apache_fop_dir: Path,
|
|
||||||
fop_config_file: Optional[Path] = None,
|
|
||||||
log_dir: Optional[Path] = None,
|
|
||||||
):
|
|
||||||
super().__init__(num_workers, java_vm_path, log_dir)
|
|
||||||
self.apache_fop_dir = apache_fop_dir
|
|
||||||
self.fop_config_file = fop_config_file
|
|
||||||
self.fop_classpath: Optional[str] = None
|
|
||||||
|
|
||||||
self._build_fop_classpath()
|
|
||||||
self._compile_worker_class()
|
|
||||||
self._start_workers()
|
|
||||||
logger.info(f"FopWorkerPool initialisiert mit {num_workers} Workern")
|
|
||||||
|
|
||||||
def _build_fop_classpath(self):
|
|
||||||
"""Erstellt den Classpath für Apache FOP."""
|
|
||||||
all_jars = glob.glob(str(self.apache_fop_dir / "build" / "*.jar"))
|
|
||||||
lib_dir = self.apache_fop_dir / "lib"
|
|
||||||
if lib_dir.exists() and lib_dir.is_dir():
|
|
||||||
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
|
|
||||||
|
|
||||||
if not all_jars:
|
|
||||||
raise RuntimeError(f"Keine FOP JAR-Dateien gefunden in {self.apache_fop_dir}")
|
|
||||||
|
|
||||||
self.fop_classpath = _CLASSPATH_SEP.join(all_jars)
|
|
||||||
logger.debug(f"FOP Classpath: {len(all_jars)} JARs")
|
|
||||||
|
|
||||||
# --- Abstrakte Properties ---
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _pool_name(self) -> str:
|
|
||||||
return "FOP"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _java_source_code(self) -> str:
|
|
||||||
return FOP_WORKER_JAVA
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _java_class_name(self) -> str:
|
|
||||||
return "FopWorker"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _temp_dir_prefix(self) -> str:
|
|
||||||
return "fop_worker_"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _worker_init_sleep(self) -> float:
|
|
||||||
return 0.2 # FOP braucht etwas länger zum Initialisieren
|
|
||||||
|
|
||||||
# --- Abstrakte Methoden ---
|
|
||||||
|
|
||||||
def _get_classpath(self) -> str:
|
|
||||||
return self.fop_classpath
|
|
||||||
|
|
||||||
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
|
|
||||||
cmd = [str(self.java_vm_path), "-cp", full_classpath, "FopWorker"]
|
|
||||||
if self.fop_config_file and self.fop_config_file.exists():
|
|
||||||
cmd.append(str(self.fop_config_file))
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def _stderr_log_name(self, i: int) -> str:
|
|
||||||
return f"fop_worker_{i}_stderr.log"
|
|
||||||
|
|
||||||
# --- FOP-spezifische Job-Methode ---
|
|
||||||
|
|
||||||
def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Generiert PDF aus FO-Datei mit einem Worker aus dem Pool.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_fo: Pfad zur FO-Eingabedatei
|
|
||||||
output_pdf: Pfad zur PDF-Ausgabedatei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
|
|
||||||
"""
|
|
||||||
worker_idx = self._acquire_worker()
|
|
||||||
try:
|
|
||||||
worker = self.workers[worker_idx]
|
|
||||||
|
|
||||||
if worker.poll() is not None:
|
|
||||||
stderr_content = self._read_stderr_log(worker_idx)
|
|
||||||
error_msg = f"FOP Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
return False, error_msg
|
|
||||||
|
|
||||||
job = f"{input_fo}\t{output_pdf}\n"
|
|
||||||
logger.debug(f"Sende FOP-Job an Worker {worker_idx}: {input_fo.name} → {output_pdf.name}")
|
|
||||||
worker.stdin.write(job)
|
|
||||||
worker.stdin.flush()
|
|
||||||
|
|
||||||
response = worker.stdout.readline().strip()
|
|
||||||
logger.debug(f"FOP Worker {worker_idx} Antwort: '{response}'")
|
|
||||||
|
|
||||||
if response == "OK":
|
|
||||||
return True, "Erfolgreich"
|
|
||||||
elif response.startswith("ERROR:"):
|
|
||||||
return False, f"FOP-Fehler: {response[6:].strip()}"
|
|
||||||
elif not response:
|
|
||||||
stderr_content = self._read_stderr_log(worker_idx, tail=500)
|
|
||||||
return False, f"FOP Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
|
|
||||||
else:
|
|
||||||
return False, f"Unerwartete Antwort: {response}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler bei FOP Worker {worker_idx}: {e}")
|
|
||||||
return False, f"Worker-Fehler: {str(e)}"
|
|
||||||
finally:
|
|
||||||
self.worker_locks[worker_idx].release()
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
"""
|
|
||||||
Parser für THIRD_PARTY_LICENSES.txt.
|
|
||||||
|
|
||||||
Extrahiert strukturierte Lizenzinformationen und ergänzt sie
|
|
||||||
mit den tatsächlich installierten Paketversionen via importlib.metadata.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from importlib.metadata import PackageNotFoundError, version
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Pfad zur Lizenzdatei: im PyInstaller-Bundle aus sys._MEIPASS,
|
|
||||||
# im Entwicklungsmodus relativ zum Projektroot
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
LICENSE_FILE = Path(sys._MEIPASS) / "THIRD_PARTY_LICENSES.txt" # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
LICENSE_FILE = Path(__file__).parent.parent / "THIRD_PARTY_LICENSES.txt"
|
|
||||||
|
|
||||||
# Mapping von Anzeigenamen zu PyPI-Paketnamen für Sonderfälle
|
|
||||||
_PACKAGE_NAME_MAP: dict[str, str] = {
|
|
||||||
"PySide6": "PySide6",
|
|
||||||
"Pydantic": "pydantic",
|
|
||||||
"Pydantic-Settings": "pydantic-settings",
|
|
||||||
"Pydantic-YAML": "pydantic-yaml",
|
|
||||||
"Polars": "polars",
|
|
||||||
"ConnectorX (via Polars)": "connectorx",
|
|
||||||
"PyArrow (via Polars)": "pyarrow",
|
|
||||||
"psutil": "psutil",
|
|
||||||
"lxml": "lxml",
|
|
||||||
"Ruff (Development)": "ruff",
|
|
||||||
"PyInstaller (Development)": "pyinstaller",
|
|
||||||
"Pillow (Development)": "pillow",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Regex für den Beginn eines nummerierten Eintrags (z.B. "1. PySide6" oder "10. PyInstaller (Development)")
|
|
||||||
_ENTRY_PATTERN = re.compile(r"^\d+\.\s+(.+)$")
|
|
||||||
|
|
||||||
# Bekannte Feldschlüssel in der Lizenzdatei
|
|
||||||
_KNOWN_KEYS = {"Version", "Lizenz", "Webseite", "GitHub", "Beschreibung", "Copyright", "Datei", "Hinweis"}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LicenseEntry:
|
|
||||||
"""Ein einzelner Eintrag aus THIRD_PARTY_LICENSES.txt."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
license: str = ""
|
|
||||||
installed_version: str = ""
|
|
||||||
website: str = ""
|
|
||||||
description: str = ""
|
|
||||||
copyright: str = ""
|
|
||||||
category: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ParsedLicenses:
|
|
||||||
"""Ergebnis des Parsens der Lizenzdatei."""
|
|
||||||
|
|
||||||
entries: list[LicenseEntry] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_package_name(display_name: str) -> str:
|
|
||||||
"""Normalisiert einen Anzeigenamen zu einem PyPI-Paketnamen."""
|
|
||||||
if display_name in _PACKAGE_NAME_MAP:
|
|
||||||
return _PACKAGE_NAME_MAP[display_name]
|
|
||||||
# Fallback: lowercase, Leerzeichen/Klammern entfernen
|
|
||||||
return display_name.lower().split("(")[0].strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_installed_version(package_name: str) -> str:
|
|
||||||
"""Ermittelt die installierte Version eines Pakets."""
|
|
||||||
try:
|
|
||||||
return version(package_name)
|
|
||||||
except PackageNotFoundError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def parse_license_file(license_file: Path | None = None) -> ParsedLicenses:
|
|
||||||
"""
|
|
||||||
Parst THIRD_PARTY_LICENSES.txt und gibt strukturierte Einträge zurück.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
license_file: Pfad zur Lizenzdatei. Standard: LICENSE_FILE
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedLicenses mit allen gefundenen Einträgen inkl. installierter Versionen
|
|
||||||
"""
|
|
||||||
if license_file is None:
|
|
||||||
license_file = LICENSE_FILE
|
|
||||||
|
|
||||||
if not license_file.exists():
|
|
||||||
logger.warning(f"Lizenzdatei nicht gefunden: {license_file}")
|
|
||||||
return ParsedLicenses()
|
|
||||||
|
|
||||||
text = license_file.read_text(encoding="utf-8")
|
|
||||||
lines = text.splitlines()
|
|
||||||
|
|
||||||
result = ParsedLicenses()
|
|
||||||
current_category = ""
|
|
||||||
current_entry: LicenseEntry | None = None
|
|
||||||
last_key: str | None = None # Letzter erkannter Schlüssel (für Fortsetzungszeilen)
|
|
||||||
separator_line = "=" * 20 # Mindestlänge für Sektions-Trennlinien
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(lines):
|
|
||||||
line = lines[i]
|
|
||||||
|
|
||||||
# Sektions-Trennlinie erkannt → nächste Zeile ist Sektionsname
|
|
||||||
if line.startswith(separator_line):
|
|
||||||
i += 1
|
|
||||||
if i < len(lines):
|
|
||||||
section_name = lines[i].strip()
|
|
||||||
|
|
||||||
# Bei "Lizenztexte" aufhören — ab hier kommen nur noch Volltexte
|
|
||||||
if section_name == "Lizenztexte":
|
|
||||||
# Letzten Eintrag noch sichern
|
|
||||||
if current_entry:
|
|
||||||
result.entries.append(current_entry)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Neue Kategorie setzen (ignoriere den Datei-Header "THIRD PARTY LICENSES")
|
|
||||||
if section_name != "THIRD PARTY LICENSES":
|
|
||||||
current_category = section_name
|
|
||||||
|
|
||||||
# Schließende Trennlinie der Sektion überspringen
|
|
||||||
i += 1
|
|
||||||
if i < len(lines) and lines[i].startswith(separator_line):
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Nummerierter Eintrag (z.B. "1. PySide6")
|
|
||||||
match = _ENTRY_PATTERN.match(line.strip())
|
|
||||||
if match:
|
|
||||||
# Vorherigen Eintrag speichern
|
|
||||||
if current_entry:
|
|
||||||
result.entries.append(current_entry)
|
|
||||||
|
|
||||||
entry_name = match.group(1).strip()
|
|
||||||
current_entry = LicenseEntry(name=entry_name, category=current_category)
|
|
||||||
|
|
||||||
# Installierte Version ermitteln
|
|
||||||
pypi_name = _normalize_package_name(entry_name)
|
|
||||||
current_entry.installed_version = _get_installed_version(pypi_name)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Schlüssel-Wert-Zeile innerhalb eines Eintrags (z.B. " Version: >=6.10.1")
|
|
||||||
stripped = line.strip()
|
|
||||||
if current_entry and ":" in stripped:
|
|
||||||
key, _, value = stripped.partition(":")
|
|
||||||
key = key.strip()
|
|
||||||
value = value.strip()
|
|
||||||
|
|
||||||
if key in _KNOWN_KEYS:
|
|
||||||
last_key = key
|
|
||||||
if key == "Lizenz":
|
|
||||||
current_entry.license = value
|
|
||||||
elif key == "Webseite":
|
|
||||||
current_entry.website = value
|
|
||||||
elif key == "GitHub" and not current_entry.website:
|
|
||||||
# GitHub nur als Fallback wenn keine Webseite vorhanden
|
|
||||||
current_entry.website = value
|
|
||||||
elif key == "Beschreibung":
|
|
||||||
current_entry.description = value
|
|
||||||
elif key == "Copyright":
|
|
||||||
current_entry.copyright = value
|
|
||||||
else:
|
|
||||||
last_key = None
|
|
||||||
elif current_entry and stripped and not stripped.startswith("="):
|
|
||||||
# Fortsetzungszeile (z.B. mehrzeiliger Copyright-Eintrag)
|
|
||||||
if last_key == "Copyright":
|
|
||||||
current_entry.copyright += "\n" + stripped
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# Letzten Eintrag sichern (falls Datei nicht mit "Lizenztexte" endet)
|
|
||||||
if current_entry and current_entry not in result.entries:
|
|
||||||
result.entries.append(current_entry)
|
|
||||||
|
|
||||||
logger.info(f"{len(result.entries)} Lizenzeinträge aus {license_file.name} geladen")
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Standalone-Test
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
parsed = parse_license_file()
|
|
||||||
for entry in parsed.entries:
|
|
||||||
ver = entry.installed_version or "—"
|
|
||||||
logger.debug(f"[{entry.category}] {entry.name}: {ver} ({entry.license})")
|
|
||||||
+1
-52
@@ -1,8 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtGui import QIcon
|
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from ui.MainWindow import MainWindow
|
from ui.MainWindow import MainWindow
|
||||||
@@ -10,39 +8,6 @@ from ui.AppSettings import AppSettingsDlg
|
|||||||
from conf import app_settings
|
from conf import app_settings
|
||||||
|
|
||||||
|
|
||||||
def cleanup_old_logs(log_dir, max_age_hours=24):
|
|
||||||
"""
|
|
||||||
Löscht Log-Dateien, die älter als die angegebene Anzahl von Stunden sind.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
log_dir: Pfad zum Log-Verzeichnis
|
|
||||||
max_age_hours: Maximales Alter der Log-Dateien in Stunden (Standard: 24)
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
|
|
||||||
if not log_dir.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
cutoff_time = time.time() - (max_age_hours * 3600)
|
|
||||||
deleted_count = 0
|
|
||||||
|
|
||||||
# Alle .log Dateien im Verzeichnis durchsuchen
|
|
||||||
for log_file in log_dir.glob("*.log"):
|
|
||||||
try:
|
|
||||||
# Änderungszeit der Datei abrufen
|
|
||||||
file_mtime = log_file.stat().st_mtime
|
|
||||||
|
|
||||||
# Wenn die Datei älter als die Grenzzeit ist, löschen
|
|
||||||
if file_mtime < cutoff_time:
|
|
||||||
log_file.unlink()
|
|
||||||
deleted_count += 1
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Fehler beim Löschen von {log_file}: {e}")
|
|
||||||
|
|
||||||
if deleted_count > 0:
|
|
||||||
logging.info(f"{deleted_count} alte Log-Datei(en) gelöscht (älter als {max_age_hours} Stunden)")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Haupteinstiegspunkt der Anwendung."""
|
"""Haupteinstiegspunkt der Anwendung."""
|
||||||
# Logging konfigurieren - sowohl Datei als auch Konsole
|
# Logging konfigurieren - sowohl Datei als auch Konsole
|
||||||
@@ -52,7 +17,7 @@ def main():
|
|||||||
from conf import config_path
|
from conf import config_path
|
||||||
|
|
||||||
log_dir = config_path.parent / "logs"
|
log_dir = config_path.parent / "logs"
|
||||||
log_dir.mkdir(parents=True, exist_ok=True)
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# Log-Dateiname mit Timestamp
|
# Log-Dateiname mit Timestamp
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
@@ -79,25 +44,9 @@ def main():
|
|||||||
|
|
||||||
logging.info(f"Logging initialisiert: {log_file}")
|
logging.info(f"Logging initialisiert: {log_file}")
|
||||||
|
|
||||||
# Alte Log-Dateien aufräumen (erst nach Logger-Init)
|
|
||||||
cleanup_old_logs(log_dir, max_age_hours=24)
|
|
||||||
|
|
||||||
# Unter Windows: AppUserModelID setzen, damit die Taskleiste das richtige Symbol zeigt
|
|
||||||
if sys.platform == "win32":
|
|
||||||
import ctypes
|
|
||||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("de.vitaligraf.documentor")
|
|
||||||
|
|
||||||
# QApplication-Instanz erstellen
|
# QApplication-Instanz erstellen
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
# App-Symbol setzen (PyInstaller: sys._MEIPASS, sonst Quellpfad)
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
icon_path = Path(sys._MEIPASS) / "resources" / "icon.ico"
|
|
||||||
else:
|
|
||||||
icon_path = Path(__file__).parent.parent / "resources" / "icon.ico"
|
|
||||||
if icon_path.exists():
|
|
||||||
app.setWindowIcon(QIcon(str(icon_path)))
|
|
||||||
|
|
||||||
# Hauptfenster erstellen
|
# Hauptfenster erstellen
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
|
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
"""
|
|
||||||
Obsolete-Detector — Erkennung veralteter Projekteinträge nach DB-Import.
|
|
||||||
|
|
||||||
Reine Analyselogik ohne Qt-Abhängigkeit. Findet XslFile-Einträge im Projekt,
|
|
||||||
die nicht mehr in der Datenbank vorhanden sind.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from conf import TreeNode, XslFile
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ObsoleteXslEntry:
|
|
||||||
"""Ein XslFile-Eintrag, der nicht mehr in der Datenbank vorhanden ist."""
|
|
||||||
|
|
||||||
xsl_file: XslFile
|
|
||||||
parent_node: TreeNode
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ObsoleteGroup:
|
|
||||||
"""Gruppe veralteter XslFile-Einträge unter einem gemeinsamen Eltern-Pfad."""
|
|
||||||
|
|
||||||
node_path: list[str]
|
|
||||||
parent_node: TreeNode
|
|
||||||
xsl_entries: list[ObsoleteXslEntry] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_db_xsl_ids(new_nodes: list[TreeNode]) -> set[tuple]:
|
|
||||||
"""
|
|
||||||
Extrahiert alle XslFile-IDs aus den frisch aus der DB geladenen Nodes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_nodes: Nodes aus _process_sql_data()
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Set aller XslFile-IDs (Tupel)
|
|
||||||
"""
|
|
||||||
ids: set[tuple] = set()
|
|
||||||
_collect_ids_recursive(new_nodes, ids)
|
|
||||||
return ids
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_ids_recursive(nodes: list, ids: set[tuple]) -> None:
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, XslFile):
|
|
||||||
ids.add(node.id)
|
|
||||||
elif isinstance(node, TreeNode) and node.children:
|
|
||||||
_collect_ids_recursive(node.children, ids)
|
|
||||||
|
|
||||||
|
|
||||||
def find_obsolete_xsl_entries(
|
|
||||||
project_nodes: list[TreeNode],
|
|
||||||
db_xsl_ids: set[tuple],
|
|
||||||
) -> list[ObsoleteGroup]:
|
|
||||||
"""
|
|
||||||
Findet alle XslFile-Einträge im Projekt, die nicht mehr in der DB vorhanden sind.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_nodes: Aktuelle Nodes aus pdf_project.nodes
|
|
||||||
db_xsl_ids: Set aller XslFile-IDs aus der DB (von extract_db_xsl_ids)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste von ObsoleteGroup, sortiert nach Hierarchiepfad
|
|
||||||
"""
|
|
||||||
groups: dict[int, ObsoleteGroup] = {}
|
|
||||||
|
|
||||||
_find_obsolete_recursive(project_nodes, db_xsl_ids, path=[], groups=groups)
|
|
||||||
|
|
||||||
# Sortieren nach Pfad für stabile Darstellung
|
|
||||||
return sorted(groups.values(), key=lambda g: g.node_path)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_obsolete_recursive(
|
|
||||||
nodes: list,
|
|
||||||
db_xsl_ids: set[tuple],
|
|
||||||
path: list[str],
|
|
||||||
groups: dict[int, ObsoleteGroup],
|
|
||||||
) -> None:
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, TreeNode):
|
|
||||||
_find_obsolete_in_tree_node(node, db_xsl_ids, path, groups)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_obsolete_in_tree_node(
|
|
||||||
node: TreeNode,
|
|
||||||
db_xsl_ids: set[tuple],
|
|
||||||
parent_path: list[str],
|
|
||||||
groups: dict[int, ObsoleteGroup],
|
|
||||||
) -> None:
|
|
||||||
current_path = parent_path + [node.bez]
|
|
||||||
|
|
||||||
for child in node.children:
|
|
||||||
if isinstance(child, XslFile):
|
|
||||||
if child.id not in db_xsl_ids:
|
|
||||||
node_id = id(node)
|
|
||||||
if node_id not in groups:
|
|
||||||
groups[node_id] = ObsoleteGroup(
|
|
||||||
node_path=current_path,
|
|
||||||
parent_node=node,
|
|
||||||
)
|
|
||||||
groups[node_id].xsl_entries.append(ObsoleteXslEntry(xsl_file=child, parent_node=node))
|
|
||||||
elif isinstance(child, TreeNode):
|
|
||||||
_find_obsolete_in_tree_node(child, db_xsl_ids, current_path, groups)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_empty_tree_nodes(nodes: list) -> list:
|
|
||||||
"""
|
|
||||||
Entfernt rekursiv alle TreeNodes, deren children-Liste nach der Bereinigung leer ist.
|
|
||||||
XslFile-Einträge werden immer behalten (sie wurden bereits entfernt oder sind noch gültig).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nodes: Liste von TreeNode|XslFile
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Bereinigte Liste ohne leere TreeNodes
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, TreeNode):
|
|
||||||
node.children = remove_empty_tree_nodes(node.children)
|
|
||||||
if node.children:
|
|
||||||
result.append(node)
|
|
||||||
# leere TreeNodes stillschweigend verwerfen
|
|
||||||
else:
|
|
||||||
result.append(node)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def collect_unused_xml_files(
|
|
||||||
obsolete_groups: list[ObsoleteGroup],
|
|
||||||
project_dir: Path,
|
|
||||||
is_xml_used_elsewhere_fn,
|
|
||||||
) -> list[tuple[Path, Path]]:
|
|
||||||
"""
|
|
||||||
Sammelt XML-Dateien der veralteten XslFiles, die nirgends anders mehr verwendet werden.
|
|
||||||
|
|
||||||
WICHTIG: Muss nach dem Entfernen der XslFiles aus dem Modell aufgerufen werden,
|
|
||||||
damit is_xml_used_elsewhere_fn korrekte Ergebnisse liefert.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obsolete_groups: Die veralteten Gruppen
|
|
||||||
project_dir: Absoluter Pfad zum Projektverzeichnis
|
|
||||||
is_xml_used_elsewhere_fn: Callable(xml_path, exclude_xsl_file) -> bool
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste von (xml_path_relativ, xml_path_absolut) für nicht mehr verwendete XML-Dateien
|
|
||||||
"""
|
|
||||||
unused: list[tuple[Path, Path]] = []
|
|
||||||
seen: set[str] = set()
|
|
||||||
|
|
||||||
for group in obsolete_groups:
|
|
||||||
for entry in group.xsl_entries:
|
|
||||||
for xml_file_obj in entry.xsl_file.xmls:
|
|
||||||
xml_path_str = str(xml_file_obj.xml)
|
|
||||||
if xml_path_str in seen:
|
|
||||||
continue
|
|
||||||
seen.add(xml_path_str)
|
|
||||||
xml_abs = project_dir / xml_file_obj.xml
|
|
||||||
if xml_abs.exists():
|
|
||||||
if not is_xml_used_elsewhere_fn(xml_file_obj.xml, entry.xsl_file):
|
|
||||||
unused.append((xml_file_obj.xml, xml_abs))
|
|
||||||
|
|
||||||
return unused
|
|
||||||
+1
-1
@@ -8,4 +8,4 @@ select
|
|||||||
r3.xsl_datei
|
r3.xsl_datei
|
||||||
from reporttyp r
|
from reporttyp r
|
||||||
inner join report r2 on r.reporttyp = r2.reporttyp and r2.aktiv = 1
|
inner join report r2 on r.reporttyp = r2.reporttyp and r2.aktiv = 1
|
||||||
inner join repfile r3 on r2.reporttyp = r3.reporttyp and r2.report = r3.report and r3.xsl_datei is not null and r3.aktiv = 1 and r3.export = 0
|
inner join repfile r3 on r2.reporttyp = r3.reporttyp and r2.report = r3.report and r3.xsl_datei is not null and r3.aktiv = 1
|
||||||
Vendored
-34
File diff suppressed because one or more lines are too long
+223
-64
@@ -6,10 +6,12 @@ Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheina
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from queue import Queue
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import tempfile
|
||||||
from worker_pool_base import BaseWorkerPool, build_jar_classpath
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,10 +30,7 @@ public class SaxonWorker {
|
|||||||
// Create TransformerFactory once and reuse
|
// Create TransformerFactory once and reuse
|
||||||
TransformerFactory factory = TransformerFactory.newInstance();
|
TransformerFactory factory = TransformerFactory.newInstance();
|
||||||
|
|
||||||
// Cache für kompilierte Stylesheets (Performance-Optimierung)
|
System.err.println("SaxonWorker started and ready (using JAXP Transformer API)");
|
||||||
Map<String, Templates> templatesCache = new HashMap<>();
|
|
||||||
|
|
||||||
System.err.println("SaxonWorker started and ready (using JAXP Transformer API with stylesheet caching)");
|
|
||||||
System.err.flush();
|
System.err.flush();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -63,33 +62,19 @@ public class SaxonWorker {
|
|||||||
String xslStylesheet = parts[1];
|
String xslStylesheet = parts[1];
|
||||||
String outputFo = parts[2];
|
String outputFo = parts[2];
|
||||||
|
|
||||||
// Prüfe ob Stylesheet bereits im Cache ist
|
System.err.println("DEBUG: Creating transformer from stylesheet...");
|
||||||
Templates templates;
|
|
||||||
if (templatesCache.containsKey(xslStylesheet)) {
|
|
||||||
templates = templatesCache.get(xslStylesheet);
|
|
||||||
System.err.println("DEBUG: Using cached stylesheet: " + xslStylesheet);
|
|
||||||
System.err.flush();
|
|
||||||
} else {
|
|
||||||
System.err.println("DEBUG: Compiling and caching stylesheet: " + xslStylesheet);
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
StreamSource xslSource = new StreamSource(new File(xslStylesheet));
|
|
||||||
templates = factory.newTemplates(xslSource);
|
|
||||||
templatesCache.put(xslStylesheet, templates);
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Stylesheet compiled and cached (cache size: " + templatesCache.size() + ")");
|
|
||||||
System.err.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Creating transformer from cached template...");
|
|
||||||
System.err.flush();
|
System.err.flush();
|
||||||
|
|
||||||
// Create Source and Result objects
|
// Create Source and Result objects
|
||||||
|
StreamSource xslSource = new StreamSource(new File(xslStylesheet));
|
||||||
StreamSource xmlSource = new StreamSource(new File(sourceXml));
|
StreamSource xmlSource = new StreamSource(new File(sourceXml));
|
||||||
StreamResult result = new StreamResult(new File(outputFo));
|
StreamResult result = new StreamResult(new File(outputFo));
|
||||||
|
|
||||||
// Create transformer from templates
|
System.err.println("DEBUG: Compiling stylesheet...");
|
||||||
Transformer transformer = templates.newTransformer();
|
System.err.flush();
|
||||||
|
|
||||||
|
// Create transformer from stylesheet
|
||||||
|
Transformer transformer = factory.newTransformer(xslSource);
|
||||||
|
|
||||||
// Set parameters if present
|
// Set parameters if present
|
||||||
if (parts.length > 3 && !parts[3].isEmpty()) {
|
if (parts.length > 3 && !parts[3].isEmpty()) {
|
||||||
@@ -170,9 +155,9 @@ public class SaxonWorker {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SaxonWorkerPool(BaseWorkerPool):
|
class SaxonWorkerPool:
|
||||||
"""
|
"""
|
||||||
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen (JAXP/XSLT 1.0).
|
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen.
|
||||||
|
|
||||||
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
|
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
|
||||||
"""
|
"""
|
||||||
@@ -185,51 +170,144 @@ class SaxonWorkerPool(BaseWorkerPool):
|
|||||||
classpath_cache: dict[Path, str],
|
classpath_cache: dict[Path, str],
|
||||||
log_dir: Optional[Path] = None,
|
log_dir: Optional[Path] = None,
|
||||||
):
|
):
|
||||||
super().__init__(num_workers, java_vm_path, log_dir)
|
"""
|
||||||
|
Initialisiert den Saxon-Worker-Pool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_workers: Anzahl der Worker-Prozesse
|
||||||
|
java_vm_path: Pfad zur Java VM Binary
|
||||||
|
saxon_jar_path: Pfad zur Saxon JAR-Datei
|
||||||
|
classpath_cache: Cache für Saxon-Classpaths
|
||||||
|
log_dir: Optionales Verzeichnis für Worker-Logs (Standard: temp_dir/temp)
|
||||||
|
"""
|
||||||
|
self.num_workers = num_workers
|
||||||
|
self.java_vm_path = java_vm_path
|
||||||
self.saxon_jar_path = saxon_jar_path
|
self.saxon_jar_path = saxon_jar_path
|
||||||
self.classpath_cache = classpath_cache
|
self.classpath_cache = classpath_cache
|
||||||
|
self.log_dir = log_dir
|
||||||
|
|
||||||
|
# Worker-Prozesse und Queues
|
||||||
|
self.workers: list[subprocess.Popen] = []
|
||||||
|
self.job_queue: Queue = Queue()
|
||||||
|
self.result_queue: Queue = Queue()
|
||||||
|
self.worker_locks: list[threading.Lock] = []
|
||||||
|
|
||||||
|
# Temporäres Verzeichnis für kompilierte Java-Klasse
|
||||||
|
self.temp_dir: Optional[Path] = None
|
||||||
|
self.worker_class_path: Optional[Path] = None
|
||||||
|
self.worker_log_dir: Optional[Path] = None
|
||||||
|
|
||||||
|
# Initialisierung
|
||||||
self._compile_worker_class()
|
self._compile_worker_class()
|
||||||
self._start_workers()
|
self._start_workers()
|
||||||
|
|
||||||
logger.info(f"SaxonWorkerPool initialisiert mit {num_workers} Workern")
|
logger.info(f"SaxonWorkerPool initialisiert mit {num_workers} Workern")
|
||||||
|
|
||||||
# --- Abstrakte Properties ---
|
def _compile_worker_class(self):
|
||||||
|
"""Kompiliert die SaxonWorker-Java-Klasse."""
|
||||||
|
try:
|
||||||
|
# Erstelle temporäres Verzeichnis
|
||||||
|
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
|
||||||
|
|
||||||
@property
|
# Schreibe Java-Quellcode
|
||||||
def _pool_name(self) -> str:
|
java_file = self.temp_dir / "SaxonWorker.java"
|
||||||
return "Saxon"
|
java_file.write_text(SAXON_WORKER_JAVA, encoding="utf-8")
|
||||||
|
|
||||||
@property
|
# Hole Classpath
|
||||||
def _java_source_code(self) -> str:
|
saxon_dir = self.saxon_jar_path.parent
|
||||||
return SAXON_WORKER_JAVA
|
if saxon_dir in self.classpath_cache:
|
||||||
|
classpath = self.classpath_cache[saxon_dir]
|
||||||
|
else:
|
||||||
|
# Fallback: Baue Classpath neu
|
||||||
|
import glob
|
||||||
|
import sys
|
||||||
|
|
||||||
@property
|
all_jars = glob.glob(str(saxon_dir / "*.jar"))
|
||||||
def _java_class_name(self) -> str:
|
lib_dir = saxon_dir / "lib"
|
||||||
return "SaxonWorker"
|
if lib_dir.exists():
|
||||||
|
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
|
||||||
|
|
||||||
@property
|
classpath_separator = ";" if sys.platform == "win32" else ":"
|
||||||
def _temp_dir_prefix(self) -> str:
|
classpath = classpath_separator.join(all_jars)
|
||||||
return "saxon_worker_"
|
|
||||||
|
|
||||||
@property
|
# Kompiliere Java-Klasse
|
||||||
def _worker_init_sleep(self) -> float:
|
javac_cmd = [str(self.java_vm_path).replace("java", "javac"), "-cp", classpath, str(java_file)]
|
||||||
return 0.1
|
|
||||||
|
|
||||||
# --- Abstrakte Methoden ---
|
logger.debug(f"Kompiliere SaxonWorker: {' '.join(javac_cmd)}")
|
||||||
|
|
||||||
def _get_classpath(self) -> str:
|
result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}")
|
||||||
|
|
||||||
|
self.worker_class_path = self.temp_dir
|
||||||
|
|
||||||
|
logger.info(f"SaxonWorker erfolgreich kompiliert: {self.temp_dir}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Kompilieren von SaxonWorker: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _start_workers(self):
|
||||||
|
"""Startet N Worker-Prozesse."""
|
||||||
|
# Hole Classpath
|
||||||
saxon_dir = self.saxon_jar_path.parent
|
saxon_dir = self.saxon_jar_path.parent
|
||||||
if saxon_dir not in self.classpath_cache:
|
classpath = self.classpath_cache.get(saxon_dir, "")
|
||||||
self.classpath_cache[saxon_dir] = build_jar_classpath(saxon_dir)
|
|
||||||
return self.classpath_cache[saxon_dir]
|
|
||||||
|
|
||||||
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
|
# Füge Worker-Classpath hinzu
|
||||||
return [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
|
import sys
|
||||||
|
|
||||||
def _stderr_log_name(self, i: int) -> str:
|
classpath_separator = ";" if sys.platform == "win32" else ":"
|
||||||
return f"worker_{i}_stderr.log"
|
full_classpath = str(self.worker_class_path) + classpath_separator + classpath
|
||||||
|
|
||||||
# --- Saxon-spezifische Job-Methode ---
|
# Bestimme Log-Verzeichnis
|
||||||
|
self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir
|
||||||
|
if self.log_dir:
|
||||||
|
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for i in range(self.num_workers):
|
||||||
|
try:
|
||||||
|
# Starte JVM-Prozess mit SaxonWorker
|
||||||
|
cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
|
||||||
|
|
||||||
|
# Öffne stderr-Log-Datei für diesen Worker
|
||||||
|
stderr_log = self.worker_log_dir / f"worker_{i}_stderr.log"
|
||||||
|
stderr_file = open(stderr_log, "w", encoding="utf-8")
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=stderr_file, # Redirect stderr to file
|
||||||
|
text=True,
|
||||||
|
bufsize=1, # Line buffered
|
||||||
|
)
|
||||||
|
|
||||||
|
self.workers.append(process)
|
||||||
|
self.worker_locks.append(threading.Lock())
|
||||||
|
|
||||||
|
logger.debug(f"Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
|
||||||
|
|
||||||
|
# Warte kurz damit Worker initialisieren kann
|
||||||
|
import time
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Prüfe ob Worker noch läuft
|
||||||
|
if process.poll() is not None:
|
||||||
|
# Worker ist bereits beendet - Fehler!
|
||||||
|
stderr_file.close()
|
||||||
|
with open(stderr_log, "r") as f:
|
||||||
|
stderr_content = f.read()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(f"{len(self.workers)} Saxon-Worker erfolgreich gestartet")
|
||||||
|
|
||||||
def transform(
|
def transform(
|
||||||
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
|
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
|
||||||
@@ -246,38 +324,119 @@ class SaxonWorkerPool(BaseWorkerPool):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
|
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
|
||||||
"""
|
"""
|
||||||
worker_idx = self._acquire_worker()
|
# Finde freien Worker
|
||||||
|
worker_idx = None
|
||||||
|
for i, lock in enumerate(self.worker_locks):
|
||||||
|
if lock.acquire(blocking=False):
|
||||||
|
worker_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if worker_idx is None:
|
||||||
|
# Kein freier Worker, warte auf ersten verfügbaren
|
||||||
|
for i, lock in enumerate(self.worker_locks):
|
||||||
|
lock.acquire()
|
||||||
|
worker_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
worker = self.workers[worker_idx]
|
worker = self.workers[worker_idx]
|
||||||
|
|
||||||
|
# Prüfe ob Worker noch läuft
|
||||||
if worker.poll() is not None:
|
if worker.poll() is not None:
|
||||||
stderr_content = self._read_stderr_log(worker_idx)
|
# Worker ist tot!
|
||||||
error_msg = f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
|
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
|
||||||
|
try:
|
||||||
|
with open(stderr_log, "r") as f:
|
||||||
|
stderr_content = f.read()
|
||||||
|
error_msg = (
|
||||||
|
f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
error_msg = f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
return False, error_msg
|
return False, error_msg
|
||||||
|
|
||||||
params_str = "|||".join([f"{k}={v}" for k, v in xslt_params.items()])
|
# Formatiere Parameter
|
||||||
|
params_str = "|||".join([f"{key}={value}" for key, value in xslt_params.items()])
|
||||||
|
|
||||||
|
# Erstelle Job-String (Tab-separated)
|
||||||
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
|
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
|
||||||
|
|
||||||
logger.debug(f"Sende Job an Worker {worker_idx}: {source_xml.name}")
|
logger.debug(f"Sende Job an Worker {worker_idx}: {source_xml.name}")
|
||||||
|
|
||||||
|
# Sende Job an Worker
|
||||||
worker.stdin.write(job)
|
worker.stdin.write(job)
|
||||||
worker.stdin.flush()
|
worker.stdin.flush()
|
||||||
|
|
||||||
|
# Warte auf Antwort
|
||||||
response = worker.stdout.readline().strip()
|
response = worker.stdout.readline().strip()
|
||||||
|
|
||||||
logger.debug(f"Worker {worker_idx} Antwort: '{response}'")
|
logger.debug(f"Worker {worker_idx} Antwort: '{response}'")
|
||||||
|
|
||||||
if response == "OK":
|
if response == "OK":
|
||||||
return True, "Erfolgreich"
|
return True, "Erfolgreich"
|
||||||
elif response.startswith("ERROR:"):
|
elif response.startswith("ERROR:"):
|
||||||
return False, f"Saxon-Fehler: {response[6:].strip()}"
|
error_msg = response[6:].strip()
|
||||||
elif not response:
|
return False, f"Saxon-Fehler: {error_msg}"
|
||||||
stderr_content = self._read_stderr_log(worker_idx, tail=500)
|
|
||||||
return False, f"Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
|
|
||||||
else:
|
else:
|
||||||
|
# Leere Antwort bedeutet Worker ist crashed
|
||||||
|
if not response:
|
||||||
|
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
|
||||||
|
try:
|
||||||
|
with open(stderr_log, "r") as f:
|
||||||
|
stderr_content = f.read()[-500:] # Letzte 500 Zeichen
|
||||||
|
return False, f"Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
|
||||||
|
except Exception:
|
||||||
|
return False, f"Worker {worker_idx} crashed (keine Antwort)"
|
||||||
return False, f"Unerwartete Antwort: {response}"
|
return False, f"Unerwartete Antwort: {response}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler bei Worker {worker_idx}: {e}")
|
logger.error(f"Fehler bei Worker {worker_idx}: {e}")
|
||||||
return False, f"Worker-Fehler: {str(e)}"
|
return False, f"Worker-Fehler: {str(e)}"
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
# Gebe Worker-Lock frei
|
||||||
self.worker_locks[worker_idx].release()
|
self.worker_locks[worker_idx].release()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Beendet alle Worker-Prozesse sauber."""
|
||||||
|
logger.info("Beende Saxon-Worker-Pool...")
|
||||||
|
|
||||||
|
for i, worker in enumerate(self.workers):
|
||||||
|
try:
|
||||||
|
# Sende EXIT-Befehl
|
||||||
|
if worker.stdin and not worker.stdin.closed:
|
||||||
|
worker.stdin.write("EXIT\n")
|
||||||
|
worker.stdin.flush()
|
||||||
|
|
||||||
|
# Warte auf Beendigung (max 2 Sekunden)
|
||||||
|
worker.wait(timeout=2)
|
||||||
|
logger.debug(f"Worker {i} beendet")
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
# Force kill falls nötig
|
||||||
|
worker.kill()
|
||||||
|
logger.warning(f"Worker {i} musste gekillt werden")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Beenden von Worker {i}: {e}")
|
||||||
|
|
||||||
|
# Lösche temporäres Verzeichnis
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.rmtree(self.temp_dir)
|
||||||
|
logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}")
|
||||||
|
|
||||||
|
logger.info("Saxon-Worker-Pool beendet")
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Context manager entry."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Context manager exit."""
|
||||||
|
self.shutdown()
|
||||||
|
|||||||
@@ -1,267 +0,0 @@
|
|||||||
"""
|
|
||||||
Saxon Worker Pool (s9api) - Persistente JVM-Prozesse für XSLT 2.0/3.0 Transformationen.
|
|
||||||
|
|
||||||
Diese Variante verwendet die Saxon s9api API anstatt JAXP und ist für XSLT 2.0 und 3.0 geeignet.
|
|
||||||
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
|
|
||||||
Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheinander.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from worker_pool_base import BaseWorkerPool, build_jar_classpath
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Java-Worker-Code für s9api (wird zur Laufzeit kompiliert)
|
|
||||||
SAXON_S9API_WORKER_JAVA = """
|
|
||||||
import net.sf.saxon.s9api.*;
|
|
||||||
import javax.xml.transform.stream.StreamSource;
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class SaxonS9ApiWorker {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
|
||||||
String line;
|
|
||||||
|
|
||||||
// Create Processor once and reuse (equivalent to TransformerFactory)
|
|
||||||
Processor processor = new Processor(false);
|
|
||||||
|
|
||||||
// Cache für kompilierte Stylesheets (Performance-Optimierung)
|
|
||||||
Map<String, XsltExecutable> stylesheetCache = new HashMap<>();
|
|
||||||
|
|
||||||
System.err.println("SaxonS9ApiWorker started and ready (using s9api for XSLT 2.0/3.0 with stylesheet caching)");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
if ("EXIT".equals(line.trim())) {
|
|
||||||
System.err.println("SaxonS9ApiWorker exiting");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse job
|
|
||||||
System.err.println("DEBUG: Parsing job...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
String[] parts = line.split("\\\\t");
|
|
||||||
System.err.println("DEBUG: Parts count: " + parts.length);
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
if (parts.length < 3) {
|
|
||||||
System.out.println("ERROR: Invalid job format");
|
|
||||||
System.out.flush();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String sourceXml = parts[0];
|
|
||||||
String xslStylesheet = parts[1];
|
|
||||||
String outputFo = parts[2];
|
|
||||||
|
|
||||||
// Prüfe ob Stylesheet bereits im Cache ist
|
|
||||||
XsltExecutable executable;
|
|
||||||
if (stylesheetCache.containsKey(xslStylesheet)) {
|
|
||||||
executable = stylesheetCache.get(xslStylesheet);
|
|
||||||
System.err.println("DEBUG: Using cached stylesheet: " + xslStylesheet);
|
|
||||||
System.err.flush();
|
|
||||||
} else {
|
|
||||||
System.err.println("DEBUG: Compiling and caching stylesheet: " + xslStylesheet);
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
XsltCompiler compiler = processor.newXsltCompiler();
|
|
||||||
executable = compiler.compile(new StreamSource(new File(xslStylesheet)));
|
|
||||||
stylesheetCache.put(xslStylesheet, executable);
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Stylesheet compiled and cached (cache size: " + stylesheetCache.size() + ")");
|
|
||||||
System.err.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Creating transformer...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Create transformer
|
|
||||||
XsltTransformer transformer = executable.load();
|
|
||||||
|
|
||||||
// Set source
|
|
||||||
transformer.setSource(new StreamSource(new File(sourceXml)));
|
|
||||||
|
|
||||||
// Set destination
|
|
||||||
Serializer serializer = processor.newSerializer(new File(outputFo));
|
|
||||||
transformer.setDestination(serializer);
|
|
||||||
|
|
||||||
// Set parameters if present
|
|
||||||
if (parts.length > 3 && !parts[3].isEmpty()) {
|
|
||||||
String[] params = parts[3].split("\\\\|\\\\|\\\\|");
|
|
||||||
for (String param : params) {
|
|
||||||
if (!param.isEmpty() && param.contains("=")) {
|
|
||||||
String[] kv = param.split("=", 2);
|
|
||||||
transformer.setParameter(new QName(kv[0]), new XdmAtomicValue(kv[1]));
|
|
||||||
System.err.println("DEBUG: Set parameter: " + kv[0] + " = " + kv[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.err.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Running transformation...");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
// Transform
|
|
||||||
transformer.transform();
|
|
||||||
|
|
||||||
System.err.println("DEBUG: Transformation completed");
|
|
||||||
System.err.flush();
|
|
||||||
|
|
||||||
System.out.println("OK");
|
|
||||||
System.out.flush();
|
|
||||||
|
|
||||||
} catch (SaxonApiException e) {
|
|
||||||
System.err.println("DEBUG: SaxonApiException: " + e.getClass().getName());
|
|
||||||
System.err.flush();
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
|
|
||||||
String errorMsg = e.getMessage();
|
|
||||||
if (errorMsg == null || errorMsg.isEmpty()) {
|
|
||||||
errorMsg = e.getClass().getSimpleName();
|
|
||||||
}
|
|
||||||
System.out.println("ERROR: " + errorMsg);
|
|
||||||
System.out.flush();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
|
|
||||||
System.err.flush();
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
System.out.println("ERROR: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getName()));
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println("SaxonS9ApiWorker I/O error: " + e.getMessage());
|
|
||||||
e.printStackTrace(System.err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SaxonWorkerPoolS9Api(BaseWorkerPool):
|
|
||||||
"""
|
|
||||||
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen mit s9api.
|
|
||||||
|
|
||||||
Diese Variante verwendet die Saxon s9api API anstatt JAXP und unterstützt
|
|
||||||
vollständig XSLT 2.0 und 3.0 Transformationen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
num_workers: int,
|
|
||||||
java_vm_path: Path,
|
|
||||||
saxon_jar_path: Path,
|
|
||||||
classpath_cache: dict[Path, str],
|
|
||||||
log_dir: Optional[Path] = None,
|
|
||||||
):
|
|
||||||
super().__init__(num_workers, java_vm_path, log_dir)
|
|
||||||
self.saxon_jar_path = saxon_jar_path
|
|
||||||
self.classpath_cache = classpath_cache
|
|
||||||
|
|
||||||
self._compile_worker_class()
|
|
||||||
self._start_workers()
|
|
||||||
logger.info(f"SaxonWorkerPoolS9Api initialisiert mit {num_workers} Workern (XSLT 2.0/3.0)")
|
|
||||||
|
|
||||||
# --- Abstrakte Properties ---
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _pool_name(self) -> str:
|
|
||||||
return "Saxon-S9Api"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _java_source_code(self) -> str:
|
|
||||||
return SAXON_S9API_WORKER_JAVA
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _java_class_name(self) -> str:
|
|
||||||
return "SaxonS9ApiWorker"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _temp_dir_prefix(self) -> str:
|
|
||||||
return "saxon_s9api_worker_"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _worker_init_sleep(self) -> float:
|
|
||||||
return 0.1
|
|
||||||
|
|
||||||
# --- Abstrakte Methoden ---
|
|
||||||
|
|
||||||
def _get_classpath(self) -> str:
|
|
||||||
saxon_dir = self.saxon_jar_path.parent
|
|
||||||
if saxon_dir not in self.classpath_cache:
|
|
||||||
self.classpath_cache[saxon_dir] = build_jar_classpath(saxon_dir)
|
|
||||||
logger.debug(f"Classpath für {saxon_dir} neu erstellt und gecacht")
|
|
||||||
return self.classpath_cache[saxon_dir]
|
|
||||||
|
|
||||||
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
|
|
||||||
return [str(self.java_vm_path), "-cp", full_classpath, "SaxonS9ApiWorker"]
|
|
||||||
|
|
||||||
def _stderr_log_name(self, i: int) -> str:
|
|
||||||
return f"s9api_worker_{i}_stderr.log"
|
|
||||||
|
|
||||||
# --- Saxon-s9api-spezifische Job-Methode ---
|
|
||||||
|
|
||||||
def transform(
|
|
||||||
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
"""
|
|
||||||
Führt eine XSLT-Transformation mit einem Worker aus dem Pool aus.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source_xml: Pfad zur XML-Eingabedatei
|
|
||||||
xsl_stylesheet: Pfad zur XSL-Stylesheet-Datei
|
|
||||||
output_fo: Pfad zur FO-Ausgabedatei
|
|
||||||
xslt_params: Dictionary mit XSLT-Parametern
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
|
|
||||||
"""
|
|
||||||
worker_idx = self._acquire_worker()
|
|
||||||
try:
|
|
||||||
worker = self.workers[worker_idx]
|
|
||||||
|
|
||||||
if worker.poll() is not None:
|
|
||||||
stderr_content = self._read_stderr_log(worker_idx)
|
|
||||||
error_msg = (
|
|
||||||
f"S9Api Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
|
|
||||||
)
|
|
||||||
logger.error(error_msg)
|
|
||||||
return False, error_msg
|
|
||||||
|
|
||||||
params_str = "|||".join([f"{k}={v}" for k, v in xslt_params.items()])
|
|
||||||
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
|
|
||||||
|
|
||||||
logger.debug(f"Sende Job an S9Api Worker {worker_idx}: {source_xml.name}")
|
|
||||||
worker.stdin.write(job)
|
|
||||||
worker.stdin.flush()
|
|
||||||
|
|
||||||
response = worker.stdout.readline().strip()
|
|
||||||
logger.debug(f"S9Api Worker {worker_idx} Antwort: '{response}'")
|
|
||||||
|
|
||||||
if response == "OK":
|
|
||||||
return True, "Erfolgreich"
|
|
||||||
elif response.startswith("ERROR:"):
|
|
||||||
return False, f"Saxon-Fehler (s9api): {response[6:].strip()}"
|
|
||||||
elif not response:
|
|
||||||
stderr_content = self._read_stderr_log(worker_idx, tail=500)
|
|
||||||
return False, f"S9Api Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
|
|
||||||
else:
|
|
||||||
return False, f"Unerwartete Antwort: {response}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler bei S9Api Worker {worker_idx}: {e}")
|
|
||||||
return False, f"Worker-Fehler: {str(e)}"
|
|
||||||
finally:
|
|
||||||
self.worker_locks[worker_idx].release()
|
|
||||||
+7
-96
@@ -9,31 +9,20 @@ Dieses Modul implementiert die Transformations-Pipeline:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
# Verhindert Konsolenfenster bei Subprozessen in PyInstaller-EXE (Windows)
|
|
||||||
_SUBPROCESS_FLAGS = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from saxon_pool import SaxonWorkerPool
|
from saxon_pool import SaxonWorkerPool
|
||||||
from saxon_pool_s9api import SaxonWorkerPoolS9Api
|
|
||||||
from fop_pool import FopWorkerPool
|
|
||||||
from xsl_dependencies import XslDependencyGraph
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
|
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
|
||||||
# Kann entweder JAXP oder s9api Variante sein
|
_saxon_worker_pool: Optional["SaxonWorkerPool"] = None
|
||||||
_saxon_worker_pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"] = None
|
|
||||||
|
|
||||||
# Globaler FOP-Worker-Pool (wird von MainWindow initialisiert)
|
|
||||||
_fop_worker_pool: Optional["FopWorkerPool"] = None
|
|
||||||
|
|
||||||
|
|
||||||
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"]):
|
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]):
|
||||||
"""Setzt den globalen Saxon-Worker-Pool."""
|
"""Setzt den globalen Saxon-Worker-Pool."""
|
||||||
global _saxon_worker_pool
|
global _saxon_worker_pool
|
||||||
_saxon_worker_pool = pool
|
_saxon_worker_pool = pool
|
||||||
@@ -43,26 +32,6 @@ def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api
|
|||||||
logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)")
|
logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)")
|
||||||
|
|
||||||
|
|
||||||
def get_saxon_worker_pool() -> Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"]:
|
|
||||||
"""Gibt den aktuellen globalen Saxon-Worker-Pool zurück."""
|
|
||||||
return _saxon_worker_pool
|
|
||||||
|
|
||||||
|
|
||||||
def set_fop_worker_pool(pool: Optional["FopWorkerPool"]):
|
|
||||||
"""Setzt den globalen FOP-Worker-Pool."""
|
|
||||||
global _fop_worker_pool
|
|
||||||
_fop_worker_pool = pool
|
|
||||||
if pool:
|
|
||||||
logger.info(f"FOP-Worker-Pool aktiviert mit {pool.num_workers} Workern")
|
|
||||||
else:
|
|
||||||
logger.info("FOP-Worker-Pool deaktiviert (Fallback auf subprocess)")
|
|
||||||
|
|
||||||
|
|
||||||
def get_fop_worker_pool() -> Optional["FopWorkerPool"]:
|
|
||||||
"""Gibt den aktuellen globalen FOP-Worker-Pool zurück."""
|
|
||||||
return _fop_worker_pool
|
|
||||||
|
|
||||||
|
|
||||||
class TransformationJob:
|
class TransformationJob:
|
||||||
"""
|
"""
|
||||||
Repräsentiert einen einzelnen Transformations-Job.
|
Repräsentiert einen einzelnen Transformations-Job.
|
||||||
@@ -86,7 +55,6 @@ class TransformationJob:
|
|||||||
diff_pdf_params: list[str],
|
diff_pdf_params: list[str],
|
||||||
xsl_id: tuple | None = None,
|
xsl_id: tuple | None = None,
|
||||||
fop_config_dir: Path | None = None,
|
fop_config_dir: Path | None = None,
|
||||||
dependency_graph: Optional["XslDependencyGraph"] = None,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialisiert einen Transformations-Job.
|
Initialisiert einen Transformations-Job.
|
||||||
@@ -103,7 +71,6 @@ class TransformationJob:
|
|||||||
diff_pdf_params: Standard-Parameter für diff-pdf
|
diff_pdf_params: Standard-Parameter für diff-pdf
|
||||||
xsl_id: ID der XSL-Datei (als Tuple)
|
xsl_id: ID der XSL-Datei (als Tuple)
|
||||||
fop_config_dir: Optionaler Pfad zum FOP-Config-Verzeichnis (überschreibt Standardpfad)
|
fop_config_dir: Optionaler Pfad zum FOP-Config-Verzeichnis (überschreibt Standardpfad)
|
||||||
dependency_graph: Optionaler XSL-Abhängigkeitsgraph für Import/Include-Prüfung
|
|
||||||
"""
|
"""
|
||||||
self.project_dir = project_dir
|
self.project_dir = project_dir
|
||||||
self.xml_file = xml_file # Relativ
|
self.xml_file = xml_file # Relativ
|
||||||
@@ -118,7 +85,6 @@ class TransformationJob:
|
|||||||
self.fop_config_dir = fop_config_dir
|
self.fop_config_dir = fop_config_dir
|
||||||
self.diff_pdf_path = diff_pdf_path
|
self.diff_pdf_path = diff_pdf_path
|
||||||
self.diff_pdf_params = diff_pdf_params
|
self.diff_pdf_params = diff_pdf_params
|
||||||
self.dependency_graph = dependency_graph
|
|
||||||
|
|
||||||
# Ausgabe-Verzeichnisse im Projektordner
|
# Ausgabe-Verzeichnisse im Projektordner
|
||||||
self.new_dir = project_dir / "new"
|
self.new_dir = project_dir / "new"
|
||||||
@@ -167,12 +133,12 @@ class TransformationJob:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True wenn New-PDF existiert und aktueller ist als alle Inputs
|
bool: True wenn New-PDF existiert und aktueller ist als alle Inputs
|
||||||
"""
|
"""
|
||||||
try:
|
if not self.new_pdf.exists():
|
||||||
output_mtime = self.new_pdf.stat().st_mtime
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.debug(f"New-PDF existiert nicht: {self.new_pdf}")
|
logger.debug(f"New-PDF existiert nicht: {self.new_pdf}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
output_mtime = self.new_pdf.stat().st_mtime
|
||||||
|
|
||||||
# Prüfe XML-Datei
|
# Prüfe XML-Datei
|
||||||
xml_abs = self.project_dir / self.xml_file
|
xml_abs = self.project_dir / self.xml_file
|
||||||
if xml_abs.exists() and xml_abs.stat().st_mtime > output_mtime:
|
if xml_abs.exists() and xml_abs.stat().st_mtime > output_mtime:
|
||||||
@@ -184,13 +150,6 @@ class TransformationJob:
|
|||||||
logger.debug(f"XSL-Datei ist neuer: {self.xsl_file}")
|
logger.debug(f"XSL-Datei ist neuer: {self.xsl_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Prüfe importierte/inkludierte XSL-Dateien (transitiv)
|
|
||||||
if self.dependency_graph and self.xsl_file.exists():
|
|
||||||
for dep_xsl in self.dependency_graph.get_dependencies(self.xsl_file):
|
|
||||||
if dep_xsl.exists() and dep_xsl.stat().st_mtime > output_mtime:
|
|
||||||
logger.debug(f"Importierte XSL-Datei ist neuer: {dep_xsl}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.debug(f"Transformation ist aktuell: {self.new_pdf}")
|
logger.debug(f"Transformation ist aktuell: {self.new_pdf}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -297,7 +256,6 @@ class TransformationJob:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120, # 2 Minuten Timeout
|
timeout=120, # 2 Minuten Timeout
|
||||||
creationflags=_SUBPROCESS_FLAGS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Saxon Ausgaben loggen
|
# Saxon Ausgaben loggen
|
||||||
@@ -345,55 +303,11 @@ class TransformationJob:
|
|||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
return False, error_msg
|
return False, error_msg
|
||||||
|
|
||||||
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
|
|
||||||
|
|
||||||
# Versuche zuerst den Worker-Pool zu nutzen (schneller!)
|
|
||||||
global _fop_worker_pool
|
|
||||||
if _fop_worker_pool:
|
|
||||||
try:
|
|
||||||
success, message = _fop_worker_pool.build_pdf(
|
|
||||||
input_fo=self.temp_fo,
|
|
||||||
output_pdf=self.new_pdf,
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info(f"FOP PDF-Generierung erfolgreich (Worker-Pool): {self.xml_file.name}")
|
|
||||||
|
|
||||||
# Temporäre FO-Datei löschen
|
|
||||||
if self.temp_fo.exists():
|
|
||||||
try:
|
|
||||||
self.temp_fo.unlink()
|
|
||||||
logger.debug(f"Temporäre FO-Datei gelöscht: {self.temp_fo}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte FO-Datei nicht löschen: {e}")
|
|
||||||
|
|
||||||
# Wenn kein Ref-PDF existiert, erstelle es
|
|
||||||
if not self.ref_pdf.exists():
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
shutil.copy2(self.new_pdf, self.ref_pdf)
|
|
||||||
logger.info(f"Ref-PDF erstellt: {self.ref_pdf}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte Ref-PDF nicht erstellen: {e}")
|
|
||||||
|
|
||||||
return True, "Erfolgreich"
|
|
||||||
else:
|
|
||||||
logger.error(f"FOP PDF-Generierung fehlgeschlagen (Worker-Pool): {message}")
|
|
||||||
return False, message
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"FOP Worker-Pool-Fehler, Fallback auf subprocess: {e}")
|
|
||||||
# Fallback auf subprocess unten
|
|
||||||
|
|
||||||
# Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster)
|
|
||||||
|
|
||||||
# Apache FOP Kommandozeile
|
# Apache FOP Kommandozeile
|
||||||
fop_conf_exists = self.fop_conf.exists()
|
|
||||||
cmd_line = [
|
cmd_line = [
|
||||||
str(self.fop_cmd),
|
str(self.fop_cmd),
|
||||||
"-c",
|
"-c",
|
||||||
str(self.fop_conf) if fop_conf_exists else "",
|
str(self.fop_conf) if self.fop_conf.exists() else "",
|
||||||
"-r",
|
"-r",
|
||||||
"-fo",
|
"-fo",
|
||||||
str(self.temp_fo),
|
str(self.temp_fo),
|
||||||
@@ -402,7 +316,7 @@ class TransformationJob:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Entferne leere Config-Parameter wenn fop.xconf nicht existiert
|
# Entferne leere Config-Parameter wenn fop.xconf nicht existiert
|
||||||
if not fop_conf_exists:
|
if not self.fop_conf.exists():
|
||||||
cmd_line = [c for c in cmd_line if c not in ["-c", ""]]
|
cmd_line = [c for c in cmd_line if c not in ["-c", ""]]
|
||||||
|
|
||||||
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
|
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
|
||||||
@@ -414,7 +328,6 @@ class TransformationJob:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=180, # 3 Minuten Timeout
|
timeout=180, # 3 Minuten Timeout
|
||||||
creationflags=_SUBPROCESS_FLAGS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apache FOP Ausgaben loggen
|
# Apache FOP Ausgaben loggen
|
||||||
@@ -494,7 +407,6 @@ class TransformationJob:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60, # 1 Minute Timeout
|
timeout=60, # 1 Minute Timeout
|
||||||
creationflags=_SUBPROCESS_FLAGS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
@@ -530,7 +442,6 @@ class TransformationJob:
|
|||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=90, # 1.5 Minuten Timeout
|
timeout=90, # 1.5 Minuten Timeout
|
||||||
creationflags=_SUBPROCESS_FLAGS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if result_diff.returncode == 0 or self.diff_pdf.exists():
|
if result_diff.returncode == 0 or self.diff_pdf.exists():
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
"""
|
|
||||||
Info-Dialog für DocuMentor.
|
|
||||||
|
|
||||||
Zeigt Programmversion, Python-Version und alle Drittanbieter-Bibliotheken
|
|
||||||
mit installierten Versionen und Lizenzinformationen an.
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from importlib.metadata import PackageNotFoundError, version
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QFont
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QFrame,
|
|
||||||
QHBoxLayout,
|
|
||||||
QHeaderView,
|
|
||||||
QLabel,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
QVBoxLayout,
|
|
||||||
)
|
|
||||||
|
|
||||||
from license_parser import parse_license_file
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AboutDialog(QDialog):
|
|
||||||
"""Info-Dialog mit Versionsinformationen und Drittanbieter-Lizenzen."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Über DocuMentor")
|
|
||||||
self.resize(950, 620)
|
|
||||||
self.setSizeGripEnabled(True)
|
|
||||||
self.setModal(True)
|
|
||||||
self._setup_ui()
|
|
||||||
self._populate_data()
|
|
||||||
|
|
||||||
def _get_app_version(self) -> str:
|
|
||||||
"""Ermittelt die Programmversion."""
|
|
||||||
try:
|
|
||||||
return version("DocuMentor")
|
|
||||||
except PackageNotFoundError:
|
|
||||||
# Fallback: pyproject.toml parsen (PyInstaller-Bundle oder Entwicklungsmodus)
|
|
||||||
try:
|
|
||||||
import tomllib
|
|
||||||
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
pyproject = Path(sys._MEIPASS) / "pyproject.toml" # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
pyproject = Path(__file__).parent.parent.parent / "pyproject.toml"
|
|
||||||
with open(pyproject, "rb") as f:
|
|
||||||
return tomllib.load(f)["project"]["version"]
|
|
||||||
except Exception:
|
|
||||||
return "unbekannt"
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
|
||||||
"""Erstellt das Dialog-Layout programmatisch."""
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setSpacing(8)
|
|
||||||
|
|
||||||
# App-Name
|
|
||||||
self.app_name_label = QLabel("DocuMentor")
|
|
||||||
font = QFont()
|
|
||||||
font.setPointSize(18)
|
|
||||||
font.setBold(True)
|
|
||||||
self.app_name_label.setFont(font)
|
|
||||||
self.app_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
layout.addWidget(self.app_name_label)
|
|
||||||
|
|
||||||
# Beschreibung
|
|
||||||
self.description_label = QLabel("Professionelle XSL-Transformations-Verwaltung und PDF-Generierung")
|
|
||||||
self.description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.description_label.setWordWrap(True)
|
|
||||||
layout.addWidget(self.description_label)
|
|
||||||
|
|
||||||
# Version + Python-Version
|
|
||||||
self.version_label = QLabel()
|
|
||||||
self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
layout.addWidget(self.version_label)
|
|
||||||
|
|
||||||
# Lizenz
|
|
||||||
self.license_label = QLabel("Lizenz: MIT")
|
|
||||||
self.license_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
layout.addWidget(self.license_label)
|
|
||||||
|
|
||||||
# Separator
|
|
||||||
separator = QFrame()
|
|
||||||
separator.setFrameShape(QFrame.Shape.HLine)
|
|
||||||
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
||||||
layout.addWidget(separator)
|
|
||||||
|
|
||||||
# Überschrift für Tabelle
|
|
||||||
header_layout = QHBoxLayout()
|
|
||||||
table_header = QLabel("Drittanbieter-Bibliotheken:")
|
|
||||||
header_font = QFont()
|
|
||||||
header_font.setBold(True)
|
|
||||||
table_header.setFont(header_font)
|
|
||||||
header_layout.addWidget(table_header)
|
|
||||||
header_layout.addStretch()
|
|
||||||
layout.addLayout(header_layout)
|
|
||||||
|
|
||||||
# Dependency-Tabelle
|
|
||||||
self.table = QTableWidget()
|
|
||||||
self.table.setColumnCount(6)
|
|
||||||
self.table.setHorizontalHeaderLabels(["Name", "Lizenz", "Installiert", "Webseite", "Copyright", "Kategorie"])
|
|
||||||
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
||||||
self.table.setAlternatingRowColors(True)
|
|
||||||
self.table.setSortingEnabled(True)
|
|
||||||
self.table.verticalHeader().setVisible(False)
|
|
||||||
|
|
||||||
# Spaltenbreiten
|
|
||||||
header = self.table.horizontalHeader()
|
|
||||||
header.resizeSection(0, 170) # Name
|
|
||||||
header.resizeSection(1, 180) # Lizenz
|
|
||||||
header.resizeSection(2, 80) # Installiert
|
|
||||||
header.resizeSection(5, 200) # Kategorie
|
|
||||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Webseite
|
|
||||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) # Copyright
|
|
||||||
|
|
||||||
layout.addWidget(self.table)
|
|
||||||
|
|
||||||
# Schließen-Button
|
|
||||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
|
||||||
button_box.rejected.connect(self.reject)
|
|
||||||
button_box.setCenterButtons(True)
|
|
||||||
layout.addWidget(button_box)
|
|
||||||
|
|
||||||
def _populate_data(self):
|
|
||||||
"""Befüllt den Dialog mit Versions- und Lizenzinformationen."""
|
|
||||||
# Version setzen
|
|
||||||
app_version = self._get_app_version()
|
|
||||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
||||||
self.version_label.setText(f"Version: {app_version} | Python: {python_version}")
|
|
||||||
|
|
||||||
# Lizenzdaten laden und Tabelle befüllen
|
|
||||||
parsed = parse_license_file()
|
|
||||||
self.table.setRowCount(len(parsed.entries))
|
|
||||||
|
|
||||||
for row, entry in enumerate(parsed.entries):
|
|
||||||
self.table.setItem(row, 0, QTableWidgetItem(entry.name))
|
|
||||||
self.table.setItem(row, 1, QTableWidgetItem(entry.license))
|
|
||||||
self.table.setItem(row, 2, QTableWidgetItem(entry.installed_version or "—"))
|
|
||||||
if entry.website:
|
|
||||||
website_label = QLabel(f'<a href="{entry.website}">{entry.website}</a>')
|
|
||||||
website_label.setOpenExternalLinks(True)
|
|
||||||
website_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
|
||||||
self.table.setCellWidget(row, 3, website_label)
|
|
||||||
else:
|
|
||||||
self.table.setItem(row, 3, QTableWidgetItem(""))
|
|
||||||
self.table.setItem(row, 4, QTableWidgetItem(entry.copyright))
|
|
||||||
self.table.setItem(row, 5, QTableWidgetItem(entry.category))
|
|
||||||
+503
-282
@@ -1,4 +1,4 @@
|
|||||||
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QHeaderView, QAbstractItemView
|
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QHeaderView
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ from ui.ApacheFopConfigDialog import ApacheFopConfigDialog
|
|||||||
from ui.XslDirConfigDialog import XslDirConfigDialog
|
from ui.XslDirConfigDialog import XslDirConfigDialog
|
||||||
from ui.PostgreSqlConfigDialog import PostgreSqlConfigDialog
|
from ui.PostgreSqlConfigDialog import PostgreSqlConfigDialog
|
||||||
from ui.PdfProject import PdfProjectDlg
|
from ui.PdfProject import PdfProjectDlg
|
||||||
from conf import AppSettings, JavaVm, DiffPdf, SaxonJar, ApacheFop, XslDir, Project, PostgreSqlDb, XsltVersion
|
from conf import AppSettings, JavaVm, DiffPdf, SaxonJar, ApacheFop, XslDir, Project, PostgreSqlDb
|
||||||
|
|
||||||
|
|
||||||
class AppSettingsDlg(QDialog):
|
class AppSettingsDlg(QDialog):
|
||||||
@@ -23,11 +23,14 @@ class AppSettingsDlg(QDialog):
|
|||||||
):
|
):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# UI einrichten
|
||||||
self.ui = Ui_Dialog()
|
self.ui = Ui_Dialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
# Einstellungen speichern
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
|
# Temporäre Listen für Änderungen
|
||||||
self.temp_java_vms = self.settings.java_vms.copy()
|
self.temp_java_vms = self.settings.java_vms.copy()
|
||||||
self.temp_diff_pdfs = self.settings.diff_pdfs.copy()
|
self.temp_diff_pdfs = self.settings.diff_pdfs.copy()
|
||||||
self.temp_saxon_jars = self.settings.saxon_jars.copy()
|
self.temp_saxon_jars = self.settings.saxon_jars.copy()
|
||||||
@@ -36,121 +39,89 @@ class AppSettingsDlg(QDialog):
|
|||||||
self.temp_pdf_projects = self.settings.pdf_projects.copy()
|
self.temp_pdf_projects = self.settings.pdf_projects.copy()
|
||||||
self.temp_postgresql_dbs = self.settings.postgresql_dbs.copy()
|
self.temp_postgresql_dbs = self.settings.postgresql_dbs.copy()
|
||||||
|
|
||||||
|
# Signale verbinden
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
|
|
||||||
|
# Tabellen initialisieren
|
||||||
self._setup_tables()
|
self._setup_tables()
|
||||||
self._populate_tables()
|
self._populate_tables()
|
||||||
self._populate_performance_tab()
|
|
||||||
|
|
||||||
# --- Generische Helfer ---
|
|
||||||
|
|
||||||
def _update_remove_button(self, table, button):
|
|
||||||
"""Aktiviert/deaktiviert einen Entfernen-Button je nach Tabellenauswahl."""
|
|
||||||
button.setEnabled(table.currentRow() >= 0)
|
|
||||||
|
|
||||||
def _add_item(self, dialog_class, temp_list, settings_attr, factory_fn, populate_fn):
|
|
||||||
"""Öffnet einen Dialog, erstellt ein neues Objekt und fügt es der Liste hinzu."""
|
|
||||||
dialog = dialog_class(self)
|
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
||||||
data = dialog.get_data()
|
|
||||||
if data:
|
|
||||||
new_id = max((x.id for x in temp_list), default=0) + 1
|
|
||||||
temp_list.append(factory_fn(new_id, data))
|
|
||||||
populate_fn()
|
|
||||||
setattr(self.settings, settings_attr, temp_list.copy())
|
|
||||||
self.settings.save()
|
|
||||||
self._refresh_main_window_projects_menu()
|
|
||||||
|
|
||||||
def _remove_item(self, table, temp_list, settings_attr, update_fn, populate_fn):
|
|
||||||
"""Entfernt das ausgewählte Element aus der Liste und speichert die Einstellungen."""
|
|
||||||
row = table.currentRow()
|
|
||||||
if row >= 0:
|
|
||||||
del temp_list[row]
|
|
||||||
populate_fn()
|
|
||||||
update_fn()
|
|
||||||
setattr(self.settings, settings_attr, temp_list.copy())
|
|
||||||
self.settings.save()
|
|
||||||
self._refresh_main_window_projects_menu()
|
|
||||||
|
|
||||||
def _edit_item(self, index, temp_list, dialog_class, fields, populate_fn):
|
|
||||||
"""Öffnet einen Bearbeitungs-Dialog für das gewählte Element."""
|
|
||||||
row = index.row()
|
|
||||||
if 0 <= row < len(temp_list):
|
|
||||||
item = temp_list[row]
|
|
||||||
dialog = dialog_class(self)
|
|
||||||
dialog.set_data({f: getattr(item, f) for f in fields})
|
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
||||||
new_data = dialog.get_data()
|
|
||||||
if new_data:
|
|
||||||
for f in fields:
|
|
||||||
setattr(item, f, new_data[f])
|
|
||||||
populate_fn()
|
|
||||||
|
|
||||||
# --- Signale und Tabellen-Setup ---
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
def _connect_signals(self):
|
||||||
"""Verbindet die Signale der UI-Elemente."""
|
"""Verbindet die Signale der UI-Elemente."""
|
||||||
|
# XSL-Ordner Tab
|
||||||
self.ui.addXsl.clicked.connect(self._add_xsl_dir)
|
self.ui.addXsl.clicked.connect(self._add_xsl_dir)
|
||||||
self.ui.removeXsl.clicked.connect(self._remove_xsl_dir)
|
self.ui.removeXsl.clicked.connect(self._remove_xsl_dir)
|
||||||
self.ui.tableXsls.itemSelectionChanged.connect(self._update_xsl_buttons)
|
self.ui.tableXsls.itemSelectionChanged.connect(self._update_xsl_buttons)
|
||||||
|
|
||||||
|
# Java VM Tab
|
||||||
self.ui.addJavaVm.clicked.connect(self._add_java_vm)
|
self.ui.addJavaVm.clicked.connect(self._add_java_vm)
|
||||||
self.ui.removeJavaVm.clicked.connect(self._remove_java_vm)
|
self.ui.removeJavaVm.clicked.connect(self._remove_java_vm)
|
||||||
self.ui.tableJavaVms.itemSelectionChanged.connect(self._update_java_vm_buttons)
|
self.ui.tableJavaVms.itemSelectionChanged.connect(self._update_java_vm_buttons)
|
||||||
|
|
||||||
|
# Saxon Tab
|
||||||
self.ui.addSaxon.clicked.connect(self._add_saxon)
|
self.ui.addSaxon.clicked.connect(self._add_saxon)
|
||||||
self.ui.removeSaxon.clicked.connect(self._remove_saxon)
|
self.ui.removeSaxon.clicked.connect(self._remove_saxon)
|
||||||
self.ui.tableSaxons.itemSelectionChanged.connect(self._update_saxon_buttons)
|
self.ui.tableSaxons.itemSelectionChanged.connect(self._update_saxon_buttons)
|
||||||
|
|
||||||
|
# Apache FOP Tab
|
||||||
self.ui.addApacheFop.clicked.connect(self._add_apache_fop)
|
self.ui.addApacheFop.clicked.connect(self._add_apache_fop)
|
||||||
self.ui.removeApacheFop.clicked.connect(self._remove_apache_fop)
|
self.ui.removeApacheFop.clicked.connect(self._remove_apache_fop)
|
||||||
self.ui.tableApacheFops.itemSelectionChanged.connect(self._update_apache_fop_buttons)
|
self.ui.tableApacheFops.itemSelectionChanged.connect(self._update_apache_fop_buttons)
|
||||||
|
|
||||||
|
# Diff PDF Tab
|
||||||
self.ui.addDiffPdf.clicked.connect(self._add_diff_pdf)
|
self.ui.addDiffPdf.clicked.connect(self._add_diff_pdf)
|
||||||
self.ui.removeDiffPdf.clicked.connect(self._remove_diff_pdf)
|
self.ui.removeDiffPdf.clicked.connect(self._remove_diff_pdf)
|
||||||
self.ui.tableDiffPdfs.itemSelectionChanged.connect(self._update_diff_pdf_buttons)
|
self.ui.tableDiffPdfs.itemSelectionChanged.connect(self._update_diff_pdf_buttons)
|
||||||
|
|
||||||
|
# PDF-Projekte Tab
|
||||||
self.ui.removeProject.clicked.connect(self._remove_pdf_project)
|
self.ui.removeProject.clicked.connect(self._remove_pdf_project)
|
||||||
self.ui.addProject.clicked.connect(self._add_pdf_project)
|
self.ui.addProject.clicked.connect(self._add_pdf_project)
|
||||||
self.ui.tablePdfProjects.itemSelectionChanged.connect(self._update_pdf_project_buttons)
|
self.ui.tablePdfProjects.itemSelectionChanged.connect(self._update_pdf_project_buttons)
|
||||||
|
|
||||||
|
# PostgreSQL Tab
|
||||||
self.ui.addPostgreSql.clicked.connect(self._add_postgresql_db)
|
self.ui.addPostgreSql.clicked.connect(self._add_postgresql_db)
|
||||||
self.ui.removePostgreSql.clicked.connect(self._remove_postgresql_db)
|
self.ui.removePostgreSql.clicked.connect(self._remove_postgresql_db)
|
||||||
self.ui.tablePostgreSqlDbs.itemSelectionChanged.connect(self._update_postgresql_db_buttons)
|
self.ui.tablePostgreSqlDbs.itemSelectionChanged.connect(self._update_postgresql_db_buttons)
|
||||||
|
|
||||||
def _setup_tables(self):
|
def _setup_tables(self):
|
||||||
"""Richtet die Tabellen-Header ein und macht sie unveränderbar."""
|
"""Richtet die Tabellen-Header ein und macht sie unveränderbar."""
|
||||||
|
from PySide6.QtWidgets import QAbstractItemView
|
||||||
|
|
||||||
|
# XSL-Ordner Tabelle
|
||||||
self.ui.tableXsls.setHorizontalHeaderLabels(["Name", "Pfad"])
|
self.ui.tableXsls.setHorizontalHeaderLabels(["Name", "Pfad"])
|
||||||
self.ui.tableXsls.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tableXsls.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tableXsls.doubleClicked.connect(self._edit_xsl_dir)
|
self.ui.tableXsls.doubleClicked.connect(self._edit_xsl_dir)
|
||||||
|
|
||||||
|
# Java VM Tabelle
|
||||||
self.ui.tableJavaVms.setHorizontalHeaderLabels(["Version", "Pfad"])
|
self.ui.tableJavaVms.setHorizontalHeaderLabels(["Version", "Pfad"])
|
||||||
self.ui.tableJavaVms.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tableJavaVms.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tableJavaVms.doubleClicked.connect(self._edit_java_vm)
|
self.ui.tableJavaVms.doubleClicked.connect(self._edit_java_vm)
|
||||||
|
|
||||||
|
# Saxon Tabelle
|
||||||
self.ui.tableSaxons.setHorizontalHeaderLabels(["Version", "JAR-Pfad", "Erweiterung"])
|
self.ui.tableSaxons.setHorizontalHeaderLabels(["Version", "JAR-Pfad", "Erweiterung"])
|
||||||
self.ui.tableSaxons.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tableSaxons.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tableSaxons.doubleClicked.connect(self._edit_saxon)
|
self.ui.tableSaxons.doubleClicked.connect(self._edit_saxon)
|
||||||
|
|
||||||
|
# Apache FOP Tabelle
|
||||||
self.ui.tableApacheFops.setHorizontalHeaderLabels(["Version", "Pfad", "Erweiterung"])
|
self.ui.tableApacheFops.setHorizontalHeaderLabels(["Version", "Pfad", "Erweiterung"])
|
||||||
self.ui.tableApacheFops.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tableApacheFops.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tableApacheFops.doubleClicked.connect(self._edit_apache_fop)
|
self.ui.tableApacheFops.doubleClicked.connect(self._edit_apache_fop)
|
||||||
|
|
||||||
|
# Diff PDF Tabelle
|
||||||
self.ui.tableDiffPdfs.setHorizontalHeaderLabels(["Version", "Pfad", "Parameter", "Erweiterung"])
|
self.ui.tableDiffPdfs.setHorizontalHeaderLabels(["Version", "Pfad", "Parameter", "Erweiterung"])
|
||||||
self.ui.tableDiffPdfs.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tableDiffPdfs.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tableDiffPdfs.doubleClicked.connect(self._edit_diff_pdf)
|
self.ui.tableDiffPdfs.doubleClicked.connect(self._edit_diff_pdf)
|
||||||
|
|
||||||
self.ui.tablePdfProjects.setHorizontalHeaderLabels(
|
# PDF-Projekte Tabelle
|
||||||
["Name", "Projekt-Ordner", "XSL-Ordner", "Java-VM", "Saxon", "Apache FOP", "Diff-PDF"]
|
self.ui.tablePdfProjects.setHorizontalHeaderLabels(["Name", "Projekt-Ordner", "XSL-Ordner", "Java-VM", "Saxon", "Apache FOP", "Diff-PDF"])
|
||||||
)
|
|
||||||
self.ui.tablePdfProjects.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tablePdfProjects.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tablePdfProjects.doubleClicked.connect(self._edit_pdf_project)
|
self.ui.tablePdfProjects.doubleClicked.connect(self._edit_pdf_project)
|
||||||
|
|
||||||
|
# PostgreSQL Tabelle
|
||||||
self.ui.tablePostgreSqlDbs.setHorizontalHeaderLabels(["Name", "Host", "Port", "Datenbank", "Benutzer"])
|
self.ui.tablePostgreSqlDbs.setHorizontalHeaderLabels(["Name", "Host", "Port", "Datenbank", "Benutzer"])
|
||||||
self.ui.tablePostgreSqlDbs.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
self.ui.tablePostgreSqlDbs.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
self.ui.tablePostgreSqlDbs.doubleClicked.connect(self._edit_postgresql_db)
|
self.ui.tablePostgreSqlDbs.doubleClicked.connect(self._edit_postgresql_db)
|
||||||
|
|
||||||
# --- Tabellen befüllen ---
|
|
||||||
|
|
||||||
def _populate_tables(self):
|
def _populate_tables(self):
|
||||||
"""Füllt alle Tabellen mit den aktuellen Einstellungen."""
|
"""Füllt alle Tabellen mit den aktuellen Einstellungen."""
|
||||||
self._populate_xsl_table()
|
self._populate_xsl_table()
|
||||||
@@ -161,333 +132,590 @@ class AppSettingsDlg(QDialog):
|
|||||||
self._populate_pdf_project_table()
|
self._populate_pdf_project_table()
|
||||||
self._populate_postgresql_db_table()
|
self._populate_postgresql_db_table()
|
||||||
|
|
||||||
def _make_centered_item(self, text: str) -> QTableWidgetItem:
|
|
||||||
item = QTableWidgetItem(text)
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
return item
|
|
||||||
|
|
||||||
def _populate_xsl_table(self):
|
def _populate_xsl_table(self):
|
||||||
"""Füllt die XSL-Ordner Tabelle."""
|
"""Füllt die XSL-Ordner Tabelle."""
|
||||||
self.ui.tableXsls.setRowCount(len(self.temp_xsl_dirs))
|
self.ui.tableXsls.setRowCount(len(self.temp_xsl_dirs))
|
||||||
for row, x in enumerate(self.temp_xsl_dirs):
|
for row, xsl_dir in enumerate(self.temp_xsl_dirs):
|
||||||
self.ui.tableXsls.setItem(row, 0, self._make_centered_item(x.name))
|
name_item = QTableWidgetItem(xsl_dir.name)
|
||||||
self.ui.tableXsls.setItem(row, 1, self._make_centered_item(str(x.path_to_root_dir)))
|
name_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableXsls.setItem(row, 0, name_item)
|
||||||
|
|
||||||
|
path_item = QTableWidgetItem(str(xsl_dir.path_to_root_dir))
|
||||||
|
path_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableXsls.setItem(row, 1, path_item)
|
||||||
self.ui.tableXsls.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tableXsls.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_java_vm_table(self):
|
def _populate_java_vm_table(self):
|
||||||
"""Füllt die Java VM Tabelle."""
|
"""Füllt die Java VM Tabelle."""
|
||||||
self.ui.tableJavaVms.setRowCount(len(self.temp_java_vms))
|
self.ui.tableJavaVms.setRowCount(len(self.temp_java_vms))
|
||||||
for row, x in enumerate(self.temp_java_vms):
|
for row, java_vm in enumerate(self.temp_java_vms):
|
||||||
self.ui.tableJavaVms.setItem(row, 0, self._make_centered_item(x.version))
|
version_item = QTableWidgetItem(java_vm.version)
|
||||||
self.ui.tableJavaVms.setItem(row, 1, self._make_centered_item(str(x.path_to_binary_file)))
|
version_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableJavaVms.setItem(row, 0, version_item)
|
||||||
|
|
||||||
|
path_item = QTableWidgetItem(str(java_vm.path_to_binary_file))
|
||||||
|
path_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableJavaVms.setItem(row, 1, path_item)
|
||||||
self.ui.tableJavaVms.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tableJavaVms.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_saxon_table(self):
|
def _populate_saxon_table(self):
|
||||||
"""Füllt die Saxon Tabelle."""
|
"""Füllt die Saxon Tabelle."""
|
||||||
self.ui.tableSaxons.setRowCount(len(self.temp_saxon_jars))
|
self.ui.tableSaxons.setRowCount(len(self.temp_saxon_jars))
|
||||||
for row, x in enumerate(self.temp_saxon_jars):
|
for row, saxon in enumerate(self.temp_saxon_jars):
|
||||||
self.ui.tableSaxons.setItem(row, 0, self._make_centered_item(x.version))
|
version_item = QTableWidgetItem(saxon.version)
|
||||||
self.ui.tableSaxons.setItem(row, 1, self._make_centered_item(str(x.path_to_jar_file)))
|
version_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tableSaxons.setItem(row, 2, self._make_centered_item(x.output_file_extension))
|
self.ui.tableSaxons.setItem(row, 0, version_item)
|
||||||
|
|
||||||
|
path_item = QTableWidgetItem(str(saxon.path_to_jar_file))
|
||||||
|
path_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableSaxons.setItem(row, 1, path_item)
|
||||||
|
|
||||||
|
extension_item = QTableWidgetItem(saxon.output_file_extension)
|
||||||
|
extension_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableSaxons.setItem(row, 2, extension_item)
|
||||||
self.ui.tableSaxons.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tableSaxons.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_apache_fop_table(self):
|
def _populate_apache_fop_table(self):
|
||||||
"""Füllt die Apache FOP Tabelle."""
|
"""Füllt die Apache FOP Tabelle."""
|
||||||
self.ui.tableApacheFops.setRowCount(len(self.temp_apache_fops))
|
self.ui.tableApacheFops.setRowCount(len(self.temp_apache_fops))
|
||||||
for row, x in enumerate(self.temp_apache_fops):
|
for row, fop in enumerate(self.temp_apache_fops):
|
||||||
self.ui.tableApacheFops.setItem(row, 0, self._make_centered_item(x.version))
|
version_item = QTableWidgetItem(fop.version)
|
||||||
self.ui.tableApacheFops.setItem(row, 1, self._make_centered_item(str(x.path_to_dir)))
|
version_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tableApacheFops.setItem(row, 2, self._make_centered_item(x.output_file_extension))
|
self.ui.tableApacheFops.setItem(row, 0, version_item)
|
||||||
|
|
||||||
|
path_item = QTableWidgetItem(str(fop.path_to_dir))
|
||||||
|
path_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableApacheFops.setItem(row, 1, path_item)
|
||||||
|
|
||||||
|
extension_item = QTableWidgetItem(fop.output_file_extension)
|
||||||
|
extension_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableApacheFops.setItem(row, 2, extension_item)
|
||||||
self.ui.tableApacheFops.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tableApacheFops.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_diff_pdf_table(self):
|
def _populate_diff_pdf_table(self):
|
||||||
"""Füllt die Diff PDF Tabelle."""
|
"""Füllt die Diff PDF Tabelle."""
|
||||||
self.ui.tableDiffPdfs.setRowCount(len(self.temp_diff_pdfs))
|
self.ui.tableDiffPdfs.setRowCount(len(self.temp_diff_pdfs))
|
||||||
for row, x in enumerate(self.temp_diff_pdfs):
|
for row, diff_pdf in enumerate(self.temp_diff_pdfs):
|
||||||
self.ui.tableDiffPdfs.setItem(row, 0, self._make_centered_item(x.version))
|
version_item = QTableWidgetItem(diff_pdf.version)
|
||||||
self.ui.tableDiffPdfs.setItem(row, 1, self._make_centered_item(str(x.path_to_binary_file)))
|
version_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tableDiffPdfs.setItem(row, 2, self._make_centered_item(", ".join(x.default_params)))
|
self.ui.tableDiffPdfs.setItem(row, 0, version_item)
|
||||||
self.ui.tableDiffPdfs.setItem(row, 3, self._make_centered_item(x.output_file_extension))
|
|
||||||
|
path_item = QTableWidgetItem(str(diff_pdf.path_to_binary_file))
|
||||||
|
path_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableDiffPdfs.setItem(row, 1, path_item)
|
||||||
|
|
||||||
|
params_item = QTableWidgetItem(", ".join(diff_pdf.default_params))
|
||||||
|
params_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableDiffPdfs.setItem(row, 2, params_item)
|
||||||
|
|
||||||
|
extension_item = QTableWidgetItem(diff_pdf.output_file_extension)
|
||||||
|
extension_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tableDiffPdfs.setItem(row, 3, extension_item)
|
||||||
self.ui.tableDiffPdfs.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tableDiffPdfs.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_pdf_project_table(self):
|
def _populate_pdf_project_table(self):
|
||||||
"""Füllt die PDF-Projekte Tabelle."""
|
"""Füllt die PDF-Projekte Tabelle."""
|
||||||
self.ui.tablePdfProjects.setRowCount(len(self.temp_pdf_projects))
|
self.ui.tablePdfProjects.setRowCount(len(self.temp_pdf_projects))
|
||||||
for row, p in enumerate(self.temp_pdf_projects):
|
for row, pdf_project in enumerate(self.temp_pdf_projects):
|
||||||
self.ui.tablePdfProjects.setItem(row, 0, self._make_centered_item(p.name))
|
name_item = QTableWidgetItem(pdf_project.name)
|
||||||
self.ui.tablePdfProjects.setItem(row, 1, self._make_centered_item(str(p.project_dir)))
|
name_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tablePdfProjects.setItem(row, 2, self._make_centered_item(p.getXsl()))
|
self.ui.tablePdfProjects.setItem(row, 0, name_item)
|
||||||
self.ui.tablePdfProjects.setItem(row, 3, self._make_centered_item(p.getJavaVm()))
|
|
||||||
self.ui.tablePdfProjects.setItem(row, 4, self._make_centered_item(p.getSaxon()))
|
project_dir_item = QTableWidgetItem(str(pdf_project.project_dir))
|
||||||
self.ui.tablePdfProjects.setItem(row, 5, self._make_centered_item(p.getApacheFop()))
|
project_dir_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tablePdfProjects.setItem(row, 6, self._make_centered_item(p.getDiffPdf()))
|
self.ui.tablePdfProjects.setItem(row, 1, project_dir_item)
|
||||||
|
|
||||||
|
xsl_item = QTableWidgetItem(pdf_project.getXsl())
|
||||||
|
xsl_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePdfProjects.setItem(row, 2, xsl_item)
|
||||||
|
|
||||||
|
java_vm_item = QTableWidgetItem(pdf_project.getJavaVm())
|
||||||
|
java_vm_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePdfProjects.setItem(row, 3, java_vm_item)
|
||||||
|
|
||||||
|
saxon_item = QTableWidgetItem(pdf_project.getSaxon())
|
||||||
|
saxon_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePdfProjects.setItem(row, 4, saxon_item)
|
||||||
|
|
||||||
|
apache_fop_item = QTableWidgetItem(pdf_project.getApacheFop())
|
||||||
|
apache_fop_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePdfProjects.setItem(row, 5, apache_fop_item)
|
||||||
|
|
||||||
|
diff_pdf_item = QTableWidgetItem(pdf_project.getDiffPdf())
|
||||||
|
diff_pdf_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePdfProjects.setItem(row, 6, diff_pdf_item)
|
||||||
self.ui.tablePdfProjects.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tablePdfProjects.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_postgresql_db_table(self):
|
def _populate_postgresql_db_table(self):
|
||||||
"""Füllt die PostgreSQL-Datenbank Tabelle."""
|
"""Füllt die PostgreSQL-Datenbank Tabelle."""
|
||||||
self.ui.tablePostgreSqlDbs.setRowCount(len(self.temp_postgresql_dbs))
|
self.ui.tablePostgreSqlDbs.setRowCount(len(self.temp_postgresql_dbs))
|
||||||
for row, x in enumerate(self.temp_postgresql_dbs):
|
for row, postgresql_db in enumerate(self.temp_postgresql_dbs):
|
||||||
self.ui.tablePostgreSqlDbs.setItem(row, 0, self._make_centered_item(x.name))
|
name_item = QTableWidgetItem(postgresql_db.name)
|
||||||
self.ui.tablePostgreSqlDbs.setItem(row, 1, self._make_centered_item(x.host))
|
name_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.ui.tablePostgreSqlDbs.setItem(row, 2, self._make_centered_item(str(x.port)))
|
self.ui.tablePostgreSqlDbs.setItem(row, 0, name_item)
|
||||||
self.ui.tablePostgreSqlDbs.setItem(row, 3, self._make_centered_item(x.database))
|
|
||||||
self.ui.tablePostgreSqlDbs.setItem(row, 4, self._make_centered_item(x.username))
|
host_item = QTableWidgetItem(postgresql_db.host)
|
||||||
|
host_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePostgreSqlDbs.setItem(row, 1, host_item)
|
||||||
|
|
||||||
|
port_item = QTableWidgetItem(str(postgresql_db.port))
|
||||||
|
port_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePostgreSqlDbs.setItem(row, 2, port_item)
|
||||||
|
|
||||||
|
database_item = QTableWidgetItem(postgresql_db.database)
|
||||||
|
database_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePostgreSqlDbs.setItem(row, 3, database_item)
|
||||||
|
|
||||||
|
username_item = QTableWidgetItem(postgresql_db.username)
|
||||||
|
username_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.ui.tablePostgreSqlDbs.setItem(row, 4, username_item)
|
||||||
self.ui.tablePostgreSqlDbs.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
self.ui.tablePostgreSqlDbs.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
def _populate_performance_tab(self):
|
# XSL-Ordner Methoden
|
||||||
"""Initialisiert den Performance-Tab mit den aktuellen Einstellungen."""
|
|
||||||
self.ui.spinBoxWorkerCount.setValue(self.settings.max_workers)
|
|
||||||
self.ui.checkBoxUseSaxonPool.setChecked(self.settings.use_saxon_worker_pool)
|
|
||||||
self.ui.comboBoxXsltVersion.setCurrentIndex(
|
|
||||||
0 if self.settings.saxon_xslt_version == XsltVersion.XSLT_1_0 else 1
|
|
||||||
)
|
|
||||||
self.ui.checkBoxUseFopPool.setChecked(self.settings.use_fop_worker_pool)
|
|
||||||
|
|
||||||
def _refresh_main_window_projects_menu(self):
|
|
||||||
"""Aktualisiert das Projekte-Menü im MainWindow, falls vorhanden."""
|
|
||||||
if self.parent() and hasattr(self.parent(), "_setup_projects_menu"):
|
|
||||||
self.parent()._setup_projects_menu()
|
|
||||||
|
|
||||||
# --- XSL-Ordner ---
|
|
||||||
|
|
||||||
def _add_xsl_dir(self):
|
def _add_xsl_dir(self):
|
||||||
self._add_item(
|
"""Fügt einen neuen XSL-Ordner hinzu."""
|
||||||
XslDirConfigDialog, self.temp_xsl_dirs, "xsl_dirs",
|
dialog = XslDirConfigDialog(self)
|
||||||
lambda id, d: XslDir(id=id, name=d["name"], path_to_root_dir=d["path_to_root_dir"]),
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
self._populate_xsl_table,
|
data = dialog.get_data()
|
||||||
)
|
if data:
|
||||||
|
# Neue ID generieren
|
||||||
|
new_id = max([x.id for x in self.temp_xsl_dirs], default=0) + 1
|
||||||
|
new_xsl_dir = XslDir(id=new_id, name=data["name"], path_to_root_dir=data["path_to_root_dir"])
|
||||||
|
self.temp_xsl_dirs.append(new_xsl_dir)
|
||||||
|
self._populate_xsl_table()
|
||||||
|
|
||||||
|
self.settings.xsl_dirs = self.temp_xsl_dirs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_xsl_dir(self):
|
def _remove_xsl_dir(self):
|
||||||
self._remove_item(
|
"""Entfernt den ausgewählten XSL-Ordner."""
|
||||||
self.ui.tableXsls, self.temp_xsl_dirs, "xsl_dirs",
|
current_row = self.ui.tableXsls.currentRow()
|
||||||
self._update_xsl_buttons, self._populate_xsl_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_xsl_dirs[current_row]
|
||||||
|
self._populate_xsl_table()
|
||||||
|
self._update_xsl_buttons()
|
||||||
|
|
||||||
|
self.settings.xsl_dirs = self.temp_xsl_dirs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_xsl_buttons(self):
|
def _update_xsl_buttons(self):
|
||||||
self._update_remove_button(self.ui.tableXsls, self.ui.removeXsl)
|
"""Aktualisiert den Status der XSL-Buttons."""
|
||||||
|
has_selection = self.ui.tableXsls.currentRow() >= 0
|
||||||
def _edit_xsl_dir(self, index):
|
self.ui.removeXsl.setEnabled(has_selection)
|
||||||
self._edit_item(index, self.temp_xsl_dirs, XslDirConfigDialog,
|
|
||||||
["name", "path_to_root_dir"], self._populate_xsl_table)
|
|
||||||
|
|
||||||
# --- Java VM ---
|
|
||||||
|
|
||||||
|
# Java VM Methoden
|
||||||
def _add_java_vm(self):
|
def _add_java_vm(self):
|
||||||
self._add_item(
|
"""Fügt eine neue Java VM hinzu."""
|
||||||
JavaVmConfigDialog, self.temp_java_vms, "java_vms",
|
dialog = JavaVmConfigDialog(self)
|
||||||
lambda id, d: JavaVm(id=id, version=d["version"], path_to_binary_file=d["path_to_binary_file"]),
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
self._populate_java_vm_table,
|
data = dialog.get_data()
|
||||||
)
|
if data:
|
||||||
|
new_id = max([x.id for x in self.temp_java_vms], default=0) + 1
|
||||||
|
new_java_vm = JavaVm(
|
||||||
|
id=new_id, version=data["version"], path_to_binary_file=data["path_to_binary_file"]
|
||||||
|
)
|
||||||
|
self.temp_java_vms.append(new_java_vm)
|
||||||
|
self._populate_java_vm_table()
|
||||||
|
|
||||||
|
self.settings.java_vms = self.temp_java_vms.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_java_vm(self):
|
def _remove_java_vm(self):
|
||||||
self._remove_item(
|
"""Entfernt die ausgewählte Java VM."""
|
||||||
self.ui.tableJavaVms, self.temp_java_vms, "java_vms",
|
current_row = self.ui.tableJavaVms.currentRow()
|
||||||
self._update_java_vm_buttons, self._populate_java_vm_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_java_vms[current_row]
|
||||||
|
self._populate_java_vm_table()
|
||||||
|
self._update_java_vm_buttons()
|
||||||
|
|
||||||
|
self.settings.java_vms = self.temp_java_vms.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_java_vm_buttons(self):
|
def _update_java_vm_buttons(self):
|
||||||
self._update_remove_button(self.ui.tableJavaVms, self.ui.removeJavaVm)
|
"""Aktualisiert den Status der Java VM-Buttons."""
|
||||||
|
has_selection = self.ui.tableJavaVms.currentRow() >= 0
|
||||||
def _edit_java_vm(self, index):
|
self.ui.removeJavaVm.setEnabled(has_selection)
|
||||||
self._edit_item(index, self.temp_java_vms, JavaVmConfigDialog,
|
|
||||||
["version", "path_to_binary_file"], self._populate_java_vm_table)
|
|
||||||
|
|
||||||
# --- Saxon ---
|
|
||||||
|
|
||||||
|
# Saxon Methoden
|
||||||
def _add_saxon(self):
|
def _add_saxon(self):
|
||||||
self._add_item(
|
"""Fügt eine neue Saxon JAR hinzu."""
|
||||||
SaxonJarConfigDialog, self.temp_saxon_jars, "saxon_jars",
|
dialog = SaxonJarConfigDialog(self)
|
||||||
lambda id, d: SaxonJar(
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
id=id, version=d["version"],
|
data = dialog.get_data()
|
||||||
path_to_jar_file=d["path_to_jar_file"],
|
if data:
|
||||||
output_file_extension=d["output_file_extension"],
|
new_id = max([x.id for x in self.temp_saxon_jars], default=0) + 1
|
||||||
),
|
new_saxon = SaxonJar(
|
||||||
self._populate_saxon_table,
|
id=new_id,
|
||||||
)
|
version=data["version"],
|
||||||
|
path_to_jar_file=data["path_to_jar_file"],
|
||||||
|
output_file_extension=data["output_file_extension"],
|
||||||
|
)
|
||||||
|
self.temp_saxon_jars.append(new_saxon)
|
||||||
|
self._populate_saxon_table()
|
||||||
|
|
||||||
|
self.settings.saxon_jars = self.temp_saxon_jars.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_saxon(self):
|
def _remove_saxon(self):
|
||||||
self._remove_item(
|
"""Entfernt die ausgewählte Saxon JAR."""
|
||||||
self.ui.tableSaxons, self.temp_saxon_jars, "saxon_jars",
|
current_row = self.ui.tableSaxons.currentRow()
|
||||||
self._update_saxon_buttons, self._populate_saxon_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_saxon_jars[current_row]
|
||||||
|
self._populate_saxon_table()
|
||||||
|
self._update_saxon_buttons()
|
||||||
|
|
||||||
|
self.settings.saxon_jars = self.temp_saxon_jars.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_saxon_buttons(self):
|
def _update_saxon_buttons(self):
|
||||||
self._update_remove_button(self.ui.tableSaxons, self.ui.removeSaxon)
|
"""Aktualisiert den Status der Saxon-Buttons."""
|
||||||
|
has_selection = self.ui.tableSaxons.currentRow() >= 0
|
||||||
def _edit_saxon(self, index):
|
self.ui.removeSaxon.setEnabled(has_selection)
|
||||||
self._edit_item(index, self.temp_saxon_jars, SaxonJarConfigDialog,
|
|
||||||
["version", "path_to_jar_file", "output_file_extension"], self._populate_saxon_table)
|
|
||||||
|
|
||||||
# --- Apache FOP ---
|
|
||||||
|
|
||||||
|
# Apache FOP Methoden
|
||||||
def _add_apache_fop(self):
|
def _add_apache_fop(self):
|
||||||
self._add_item(
|
"""Fügt eine neue Apache FOP Konfiguration hinzu."""
|
||||||
ApacheFopConfigDialog, self.temp_apache_fops, "apache_fops",
|
dialog = ApacheFopConfigDialog(self)
|
||||||
lambda id, d: ApacheFop(
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
id=id, version=d["version"],
|
data = dialog.get_data()
|
||||||
path_to_dir=d["path_to_dir"],
|
if data:
|
||||||
output_file_extension=d["output_file_extension"],
|
new_id = max([x.id for x in self.temp_apache_fops], default=0) + 1
|
||||||
),
|
new_fop = ApacheFop(
|
||||||
self._populate_apache_fop_table,
|
id=new_id,
|
||||||
)
|
version=data["version"],
|
||||||
|
path_to_dir=data["path_to_dir"],
|
||||||
|
output_file_extension=data["output_file_extension"],
|
||||||
|
)
|
||||||
|
self.temp_apache_fops.append(new_fop)
|
||||||
|
self._populate_apache_fop_table()
|
||||||
|
|
||||||
|
self.settings.apache_fops = self.temp_apache_fops.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_apache_fop(self):
|
def _remove_apache_fop(self):
|
||||||
self._remove_item(
|
"""Entfernt die ausgewählte Apache FOP Konfiguration."""
|
||||||
self.ui.tableApacheFops, self.temp_apache_fops, "apache_fops",
|
current_row = self.ui.tableApacheFops.currentRow()
|
||||||
self._update_apache_fop_buttons, self._populate_apache_fop_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_apache_fops[current_row]
|
||||||
|
self._populate_apache_fop_table()
|
||||||
|
self._update_apache_fop_buttons()
|
||||||
|
|
||||||
|
self.settings.apache_fops = self.temp_apache_fops.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_apache_fop_buttons(self):
|
def _update_apache_fop_buttons(self):
|
||||||
self._update_remove_button(self.ui.tableApacheFops, self.ui.removeApacheFop)
|
"""Aktualisiert den Status der Apache FOP-Buttons."""
|
||||||
|
has_selection = self.ui.tableApacheFops.currentRow() >= 0
|
||||||
def _edit_apache_fop(self, index):
|
self.ui.removeApacheFop.setEnabled(has_selection)
|
||||||
self._edit_item(index, self.temp_apache_fops, ApacheFopConfigDialog,
|
|
||||||
["version", "path_to_dir", "output_file_extension"], self._populate_apache_fop_table)
|
|
||||||
|
|
||||||
# --- Diff PDF ---
|
|
||||||
|
|
||||||
|
# Diff PDF Methoden
|
||||||
def _add_diff_pdf(self):
|
def _add_diff_pdf(self):
|
||||||
self._add_item(
|
"""Fügt eine neue Diff PDF Konfiguration hinzu."""
|
||||||
DiffPdfConfigDialog, self.temp_diff_pdfs, "diff_pdfs",
|
dialog = DiffPdfConfigDialog(self)
|
||||||
lambda id, d: DiffPdf(
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
id=id, version=d["version"],
|
data = dialog.get_data()
|
||||||
path_to_binary_file=d["path_to_binary_file"],
|
if data:
|
||||||
default_params=d["default_params"],
|
new_id = max([x.id for x in self.temp_diff_pdfs], default=0) + 1
|
||||||
output_file_extension=d["output_file_extension"],
|
new_diff_pdf = DiffPdf(
|
||||||
),
|
id=new_id,
|
||||||
self._populate_diff_pdf_table,
|
version=data["version"],
|
||||||
)
|
path_to_binary_file=data["path_to_binary_file"],
|
||||||
|
default_params=data["default_params"],
|
||||||
|
output_file_extension=data["output_file_extension"],
|
||||||
|
)
|
||||||
|
self.temp_diff_pdfs.append(new_diff_pdf)
|
||||||
|
self._populate_diff_pdf_table()
|
||||||
|
|
||||||
|
self.settings.diff_pdfs = self.temp_diff_pdfs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_diff_pdf(self):
|
def _remove_diff_pdf(self):
|
||||||
self._remove_item(
|
"""Entfernt die ausgewählte Diff PDF Konfiguration."""
|
||||||
self.ui.tableDiffPdfs, self.temp_diff_pdfs, "diff_pdfs",
|
current_row = self.ui.tableDiffPdfs.currentRow()
|
||||||
self._update_diff_pdf_buttons, self._populate_diff_pdf_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_diff_pdfs[current_row]
|
||||||
|
self._populate_diff_pdf_table()
|
||||||
|
self._update_diff_pdf_buttons()
|
||||||
|
|
||||||
|
self.settings.diff_pdfs = self.temp_diff_pdfs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_diff_pdf_buttons(self):
|
def _update_diff_pdf_buttons(self):
|
||||||
self._update_remove_button(self.ui.tableDiffPdfs, self.ui.removeDiffPdf)
|
"""Aktualisiert den Status der Diff PDF-Buttons."""
|
||||||
|
has_selection = self.ui.tableDiffPdfs.currentRow() >= 0
|
||||||
def _edit_diff_pdf(self, index):
|
self.ui.removeDiffPdf.setEnabled(has_selection)
|
||||||
self._edit_item(index, self.temp_diff_pdfs, DiffPdfConfigDialog,
|
|
||||||
["version", "path_to_binary_file", "default_params", "output_file_extension"],
|
|
||||||
self._populate_diff_pdf_table)
|
|
||||||
|
|
||||||
# --- PDF-Projekte (Sonderbehandlung wegen komplexer Logik) ---
|
|
||||||
|
|
||||||
|
# PDF-Projekt Methoden
|
||||||
def _add_pdf_project(self):
|
def _add_pdf_project(self):
|
||||||
"""Fügt ein neues PDF-Projekt hinzu."""
|
"""Fügt ein neues PDF-Projekt hinzu."""
|
||||||
dialog = PdfProjectDlg(self)
|
dialog = PdfProjectDlg(self)
|
||||||
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
|
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
|
||||||
project_data = dialog.get_project_data()
|
project_data = dialog.get_project_data()
|
||||||
new_id = max((p.id for p in self.temp_pdf_projects), default=0) + 1
|
|
||||||
|
# Neue ID generieren
|
||||||
|
new_id = max([p.id for p in self.temp_pdf_projects], default=0) + 1
|
||||||
|
|
||||||
|
# Erstelle PdfProject-Objekt
|
||||||
new_project = Project(
|
new_project = Project(
|
||||||
id=new_id,
|
id=new_id,
|
||||||
name=project_data["name"],
|
name=project_data['name'],
|
||||||
project_dir=Path(project_data["project_dir"]),
|
project_dir=Path(project_data['project_dir']),
|
||||||
java_vm_id=project_data["java_vm_id"] if project_data["java_vm_id"] != -1 else 1,
|
java_vm_id=project_data['java_vm_id'] if project_data['java_vm_id'] != -1 else 1,
|
||||||
diff_pdf_id=project_data["diff_pdf_id"] if project_data["diff_pdf_id"] != -1 else 1,
|
diff_pdf_id=project_data['diff_pdf_id'] if project_data['diff_pdf_id'] != -1 else 1,
|
||||||
saxon_jar_id=project_data["saxon_jar_id"] if project_data["saxon_jar_id"] != -1 else 1,
|
saxon_jar_id=project_data['saxon_jar_id'] if project_data['saxon_jar_id'] != -1 else 1,
|
||||||
apache_fop_id=project_data["apache_fop_id"] if project_data["apache_fop_id"] != -1 else 1,
|
apache_fop_id=project_data['apache_fop_id'] if project_data['apache_fop_id'] != -1 else 1,
|
||||||
xsl_dir_id=project_data["xsl_dir_id"] if project_data["xsl_dir_id"] != -1 else 1,
|
xsl_dir_id=project_data['xsl_dir_id'] if project_data['xsl_dir_id'] != -1 else 1,
|
||||||
postgre_sql_db_id=project_data["postgre_sql_db_id"] if project_data["postgre_sql_db_id"] != -1 else 1,
|
postgre_sql_db_id=project_data['postgre_sql_db_id'] if project_data['postgre_sql_db_id'] != -1 else 1,
|
||||||
fop_config_dir=Path(project_data["fop_config_dir"]) if project_data.get("fop_config_dir") else None,
|
fop_config_dir=Path(project_data['fop_config_dir']) if project_data.get('fop_config_dir') else None,
|
||||||
xslt_params=project_data.get("xslt_params", {}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.temp_pdf_projects.append(new_project)
|
self.temp_pdf_projects.append(new_project)
|
||||||
self._populate_pdf_project_table()
|
self._populate_pdf_project_table()
|
||||||
|
|
||||||
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
||||||
self.settings.save()
|
self.settings.save()
|
||||||
self._refresh_main_window_projects_menu()
|
|
||||||
|
|
||||||
def _remove_pdf_project(self):
|
def _remove_pdf_project(self):
|
||||||
self._remove_item(
|
"""Entfernt das ausgewählte PDF-Projekt."""
|
||||||
self.ui.tablePdfProjects, self.temp_pdf_projects, "pdf_projects",
|
current_row = self.ui.tablePdfProjects.currentRow()
|
||||||
self._update_pdf_project_buttons, self._populate_pdf_project_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_pdf_projects[current_row]
|
||||||
|
self._populate_pdf_project_table()
|
||||||
|
self._update_pdf_project_buttons()
|
||||||
|
|
||||||
|
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_pdf_project_buttons(self):
|
def _update_pdf_project_buttons(self):
|
||||||
self._update_remove_button(self.ui.tablePdfProjects, self.ui.removeProject)
|
"""Aktualisiert den Status der PDF-Projekt-Buttons."""
|
||||||
|
has_selection = self.ui.tablePdfProjects.currentRow() >= 0
|
||||||
|
self.ui.removeProject.setEnabled(has_selection)
|
||||||
|
|
||||||
|
# Bearbeitungsmethoden für Doppelklick-Events
|
||||||
|
def _edit_xsl_dir(self, index):
|
||||||
|
"""Bearbeitet einen XSL-Ordner per Doppelklick."""
|
||||||
|
row = index.row()
|
||||||
|
if 0 <= row < len(self.temp_xsl_dirs):
|
||||||
|
xsl_dir = self.temp_xsl_dirs[row]
|
||||||
|
dialog = XslDirConfigDialog(self)
|
||||||
|
|
||||||
|
# Vorhandene Daten setzen
|
||||||
|
data = {"name": xsl_dir.name, "path_to_root_dir": xsl_dir.path_to_root_dir}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
xsl_dir.name = new_data["name"]
|
||||||
|
xsl_dir.path_to_root_dir = new_data["path_to_root_dir"]
|
||||||
|
self._populate_xsl_table()
|
||||||
|
|
||||||
|
def _edit_java_vm(self, index):
|
||||||
|
"""Bearbeitet eine Java VM per Doppelklick."""
|
||||||
|
row = index.row()
|
||||||
|
if 0 <= row < len(self.temp_java_vms):
|
||||||
|
java_vm = self.temp_java_vms[row]
|
||||||
|
dialog = JavaVmConfigDialog(self)
|
||||||
|
|
||||||
|
# Vorhandene Daten setzen
|
||||||
|
data = {"version": java_vm.version, "path_to_binary_file": java_vm.path_to_binary_file}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
java_vm.version = new_data["version"]
|
||||||
|
java_vm.path_to_binary_file = new_data["path_to_binary_file"]
|
||||||
|
self._populate_java_vm_table()
|
||||||
|
|
||||||
|
def _edit_saxon(self, index):
|
||||||
|
"""Bearbeitet eine Saxon JAR per Doppelklick."""
|
||||||
|
row = index.row()
|
||||||
|
if 0 <= row < len(self.temp_saxon_jars):
|
||||||
|
saxon = self.temp_saxon_jars[row]
|
||||||
|
dialog = SaxonJarConfigDialog(self)
|
||||||
|
|
||||||
|
# Vorhandene Daten setzen
|
||||||
|
data = {
|
||||||
|
"version": saxon.version,
|
||||||
|
"path_to_jar_file": saxon.path_to_jar_file,
|
||||||
|
"output_file_extension": saxon.output_file_extension,
|
||||||
|
}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
saxon.version = new_data["version"]
|
||||||
|
saxon.path_to_jar_file = new_data["path_to_jar_file"]
|
||||||
|
saxon.output_file_extension = new_data["output_file_extension"]
|
||||||
|
self._populate_saxon_table()
|
||||||
|
|
||||||
|
def _edit_apache_fop(self, index):
|
||||||
|
"""Bearbeitet eine Apache FOP Konfiguration per Doppelklick."""
|
||||||
|
row = index.row()
|
||||||
|
if 0 <= row < len(self.temp_apache_fops):
|
||||||
|
fop = self.temp_apache_fops[row]
|
||||||
|
dialog = ApacheFopConfigDialog(self)
|
||||||
|
|
||||||
|
# Vorhandene Daten setzen
|
||||||
|
data = {
|
||||||
|
"version": fop.version,
|
||||||
|
"path_to_dir": fop.path_to_dir,
|
||||||
|
"output_file_extension": fop.output_file_extension,
|
||||||
|
}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
fop.version = new_data["version"]
|
||||||
|
fop.path_to_dir = new_data["path_to_dir"]
|
||||||
|
fop.output_file_extension = new_data["output_file_extension"]
|
||||||
|
self._populate_apache_fop_table()
|
||||||
|
|
||||||
|
def _edit_diff_pdf(self, index):
|
||||||
|
"""Bearbeitet eine Diff PDF Konfiguration per Doppelklick."""
|
||||||
|
row = index.row()
|
||||||
|
if 0 <= row < len(self.temp_diff_pdfs):
|
||||||
|
diff_pdf = self.temp_diff_pdfs[row]
|
||||||
|
dialog = DiffPdfConfigDialog(self)
|
||||||
|
|
||||||
|
# Vorhandene Daten setzen
|
||||||
|
data = {
|
||||||
|
"version": diff_pdf.version,
|
||||||
|
"path_to_binary_file": diff_pdf.path_to_binary_file,
|
||||||
|
"default_params": diff_pdf.default_params,
|
||||||
|
"output_file_extension": diff_pdf.output_file_extension,
|
||||||
|
}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
diff_pdf.version = new_data["version"]
|
||||||
|
diff_pdf.path_to_binary_file = new_data["path_to_binary_file"]
|
||||||
|
diff_pdf.default_params = new_data["default_params"]
|
||||||
|
diff_pdf.output_file_extension = new_data["output_file_extension"]
|
||||||
|
self._populate_diff_pdf_table()
|
||||||
|
|
||||||
def _edit_pdf_project(self, index):
|
def _edit_pdf_project(self, index):
|
||||||
"""Bearbeitet ein PDF-Projekt per Doppelklick (nur Einstellungen)."""
|
"""Bearbeitet ein PDF-Projekt per Doppelklick (nur Einstellungen)."""
|
||||||
row = index.row()
|
row = index.row()
|
||||||
if 0 <= row < len(self.temp_pdf_projects):
|
if 0 <= row < len(self.temp_pdf_projects):
|
||||||
pdf_project = self.temp_pdf_projects[row]
|
pdf_project = self.temp_pdf_projects[row]
|
||||||
|
|
||||||
|
# Projektdaten für Dialog vorbereiten
|
||||||
project_data = {
|
project_data = {
|
||||||
"name": pdf_project.name,
|
'name': pdf_project.name,
|
||||||
"project_dir": str(pdf_project.project_dir),
|
'project_dir': str(pdf_project.project_dir),
|
||||||
"java_vm_id": pdf_project.java_vm_id,
|
'java_vm_id': pdf_project.java_vm_id,
|
||||||
"diff_pdf_id": pdf_project.diff_pdf_id,
|
'diff_pdf_id': pdf_project.diff_pdf_id,
|
||||||
"saxon_jar_id": pdf_project.saxon_jar_id,
|
'saxon_jar_id': pdf_project.saxon_jar_id,
|
||||||
"apache_fop_id": pdf_project.apache_fop_id,
|
'apache_fop_id': pdf_project.apache_fop_id,
|
||||||
"xsl_dir_id": pdf_project.xsl_dir_id,
|
'xsl_dir_id': pdf_project.xsl_dir_id,
|
||||||
"postgre_sql_db_id": pdf_project.postgre_sql_db_id,
|
'postgre_sql_db_id': pdf_project.postgre_sql_db_id,
|
||||||
"fop_config_dir": str(pdf_project.fop_config_dir) if pdf_project.fop_config_dir else None,
|
'fop_config_dir': str(pdf_project.fop_config_dir) if pdf_project.fop_config_dir else None
|
||||||
"xslt_params": dict(pdf_project.xslt_params),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Dialog im Edit-Modus öffnen (Projekt-Name und -Ordner deaktiviert)
|
||||||
dialog = PdfProjectDlg(self, project_data, edit_mode=True)
|
dialog = PdfProjectDlg(self, project_data, edit_mode=True)
|
||||||
|
|
||||||
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
|
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
|
||||||
new_data = dialog.get_project_data()
|
new_data = dialog.get_project_data()
|
||||||
pdf_project.name = new_data["name"]
|
|
||||||
pdf_project.java_vm_id = (
|
# Nur die Einstellungen aktualisieren (Name und Ordner bleiben unverändert)
|
||||||
new_data["java_vm_id"] if new_data["java_vm_id"] != -1 else pdf_project.java_vm_id
|
pdf_project.java_vm_id = new_data['java_vm_id'] if new_data['java_vm_id'] != -1 else pdf_project.java_vm_id
|
||||||
)
|
pdf_project.diff_pdf_id = new_data['diff_pdf_id'] if new_data['diff_pdf_id'] != -1 else pdf_project.diff_pdf_id
|
||||||
pdf_project.diff_pdf_id = (
|
pdf_project.saxon_jar_id = new_data['saxon_jar_id'] if new_data['saxon_jar_id'] != -1 else pdf_project.saxon_jar_id
|
||||||
new_data["diff_pdf_id"] if new_data["diff_pdf_id"] != -1 else pdf_project.diff_pdf_id
|
pdf_project.apache_fop_id = new_data['apache_fop_id'] if new_data['apache_fop_id'] != -1 else pdf_project.apache_fop_id
|
||||||
)
|
pdf_project.xsl_dir_id = new_data['xsl_dir_id'] if new_data['xsl_dir_id'] != -1 else pdf_project.xsl_dir_id
|
||||||
pdf_project.saxon_jar_id = (
|
pdf_project.postgre_sql_db_id = new_data['postgre_sql_db_id'] if new_data['postgre_sql_db_id'] != -1 else pdf_project.postgre_sql_db_id
|
||||||
new_data["saxon_jar_id"] if new_data["saxon_jar_id"] != -1 else pdf_project.saxon_jar_id
|
pdf_project.fop_config_dir = Path(new_data['fop_config_dir']) if new_data.get('fop_config_dir') else None
|
||||||
)
|
|
||||||
pdf_project.apache_fop_id = (
|
|
||||||
new_data["apache_fop_id"] if new_data["apache_fop_id"] != -1 else pdf_project.apache_fop_id
|
|
||||||
)
|
|
||||||
pdf_project.xsl_dir_id = (
|
|
||||||
new_data["xsl_dir_id"] if new_data["xsl_dir_id"] != -1 else pdf_project.xsl_dir_id
|
|
||||||
)
|
|
||||||
pdf_project.postgre_sql_db_id = (
|
|
||||||
new_data["postgre_sql_db_id"]
|
|
||||||
if new_data["postgre_sql_db_id"] != -1
|
|
||||||
else pdf_project.postgre_sql_db_id
|
|
||||||
)
|
|
||||||
pdf_project.fop_config_dir = (
|
|
||||||
Path(new_data["fop_config_dir"]) if new_data.get("fop_config_dir") else None
|
|
||||||
)
|
|
||||||
pdf_project.xslt_params = new_data.get("xslt_params", {})
|
|
||||||
self._populate_pdf_project_table()
|
self._populate_pdf_project_table()
|
||||||
|
|
||||||
|
# Einstellungen speichern
|
||||||
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
||||||
self.settings.save()
|
self.settings.save()
|
||||||
self._refresh_main_window_projects_menu()
|
|
||||||
|
|
||||||
# --- PostgreSQL ---
|
|
||||||
|
|
||||||
|
# PostgreSQL Methoden
|
||||||
def _add_postgresql_db(self):
|
def _add_postgresql_db(self):
|
||||||
self._add_item(
|
"""Fügt eine neue PostgreSQL-Datenbank hinzu."""
|
||||||
PostgreSqlConfigDialog, self.temp_postgresql_dbs, "postgresql_dbs",
|
dialog = PostgreSqlConfigDialog(self)
|
||||||
lambda id, d: PostgreSqlDb(
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
id=id, name=d["name"], host=d["host"], port=d["port"],
|
data = dialog.get_data()
|
||||||
database=d["database"], username=d["username"], password=d["password"],
|
if data:
|
||||||
),
|
new_id = max([x.id for x in self.temp_postgresql_dbs], default=0) + 1
|
||||||
self._populate_postgresql_db_table,
|
new_postgresql_db = PostgreSqlDb(
|
||||||
)
|
id=new_id,
|
||||||
|
name=data["name"],
|
||||||
|
host=data["host"],
|
||||||
|
port=data["port"],
|
||||||
|
database=data["database"],
|
||||||
|
username=data["username"],
|
||||||
|
password=data["password"]
|
||||||
|
)
|
||||||
|
self.temp_postgresql_dbs.append(new_postgresql_db)
|
||||||
|
self._populate_postgresql_db_table()
|
||||||
|
|
||||||
|
self.settings.postgresql_dbs = self.temp_postgresql_dbs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _remove_postgresql_db(self):
|
def _remove_postgresql_db(self):
|
||||||
self._remove_item(
|
"""Entfernt die ausgewählte PostgreSQL-Datenbank."""
|
||||||
self.ui.tablePostgreSqlDbs, self.temp_postgresql_dbs, "postgresql_dbs",
|
current_row = self.ui.tablePostgreSqlDbs.currentRow()
|
||||||
self._update_postgresql_db_buttons, self._populate_postgresql_db_table,
|
if current_row >= 0:
|
||||||
)
|
del self.temp_postgresql_dbs[current_row]
|
||||||
|
self._populate_postgresql_db_table()
|
||||||
|
self._update_postgresql_db_buttons()
|
||||||
|
|
||||||
|
self.settings.postgresql_dbs = self.temp_postgresql_dbs.copy()
|
||||||
|
self.settings.save()
|
||||||
|
|
||||||
def _update_postgresql_db_buttons(self):
|
def _update_postgresql_db_buttons(self):
|
||||||
self._update_remove_button(self.ui.tablePostgreSqlDbs, self.ui.removePostgreSql)
|
"""Aktualisiert den Status der PostgreSQL-Buttons."""
|
||||||
|
has_selection = self.ui.tablePostgreSqlDbs.currentRow() >= 0
|
||||||
|
self.ui.removePostgreSql.setEnabled(has_selection)
|
||||||
|
|
||||||
def _edit_postgresql_db(self, index):
|
def _edit_postgresql_db(self, index):
|
||||||
self._edit_item(index, self.temp_postgresql_dbs, PostgreSqlConfigDialog,
|
"""Bearbeitet eine PostgreSQL-Datenbank per Doppelklick."""
|
||||||
["name", "host", "port", "database", "username", "password"],
|
row = index.row()
|
||||||
self._populate_postgresql_db_table)
|
if 0 <= row < len(self.temp_postgresql_dbs):
|
||||||
|
postgresql_db = self.temp_postgresql_dbs[row]
|
||||||
|
dialog = PostgreSqlConfigDialog(self)
|
||||||
|
|
||||||
# --- Dialog-Abschluss ---
|
# Vorhandene Daten setzen
|
||||||
|
data = {
|
||||||
|
"name": postgresql_db.name,
|
||||||
|
"host": postgresql_db.host,
|
||||||
|
"port": postgresql_db.port,
|
||||||
|
"database": postgresql_db.database,
|
||||||
|
"username": postgresql_db.username,
|
||||||
|
"password": postgresql_db.password
|
||||||
|
}
|
||||||
|
dialog.set_data(data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
new_data = dialog.get_data()
|
||||||
|
if new_data:
|
||||||
|
# Daten aktualisieren
|
||||||
|
postgresql_db.name = new_data["name"]
|
||||||
|
postgresql_db.host = new_data["host"]
|
||||||
|
postgresql_db.port = new_data["port"]
|
||||||
|
postgresql_db.database = new_data["database"]
|
||||||
|
postgresql_db.username = new_data["username"]
|
||||||
|
postgresql_db.password = new_data["password"]
|
||||||
|
self._populate_postgresql_db_table()
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
"""Übernimmt die Änderungen und schließt den Dialog."""
|
"""Übernimmt die Änderungen und schließt den Dialog."""
|
||||||
|
# Aktualisiere die ursprünglichen Einstellungen
|
||||||
self.settings.java_vms = self.temp_java_vms.copy()
|
self.settings.java_vms = self.temp_java_vms.copy()
|
||||||
self.settings.diff_pdfs = self.temp_diff_pdfs.copy()
|
self.settings.diff_pdfs = self.temp_diff_pdfs.copy()
|
||||||
self.settings.saxon_jars = self.temp_saxon_jars.copy()
|
self.settings.saxon_jars = self.temp_saxon_jars.copy()
|
||||||
@@ -495,15 +723,8 @@ class AppSettingsDlg(QDialog):
|
|||||||
self.settings.xsl_dirs = self.temp_xsl_dirs.copy()
|
self.settings.xsl_dirs = self.temp_xsl_dirs.copy()
|
||||||
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
self.settings.pdf_projects = self.temp_pdf_projects.copy()
|
||||||
|
|
||||||
self.settings.max_workers = self.ui.spinBoxWorkerCount.value()
|
|
||||||
self.settings.use_saxon_worker_pool = self.ui.checkBoxUseSaxonPool.isChecked()
|
|
||||||
self.settings.saxon_xslt_version = (
|
|
||||||
XsltVersion.XSLT_1_0 if self.ui.comboBoxXsltVersion.currentIndex() == 0 else XsltVersion.XSLT_2_0_3_0
|
|
||||||
)
|
|
||||||
self.settings.use_fop_worker_pool = self.ui.checkBoxUseFopPool.isChecked()
|
|
||||||
|
|
||||||
self.settings.save()
|
self.settings.save()
|
||||||
self._refresh_main_window_projects_menu()
|
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|
||||||
def get_settings(self) -> AppSettings:
|
def get_settings(self) -> AppSettings:
|
||||||
|
|||||||
+1
-203
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>833</width>
|
<width>833</width>
|
||||||
<height>526</height>
|
<height>387</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -520,208 +520,6 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="tabPerformance">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Performance</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="groupBoxWorker">
|
|
||||||
<property name="title">
|
|
||||||
<string>ThreadPoolExecutor Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_10">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="labelWorkerCount">
|
|
||||||
<property name="text">
|
|
||||||
<string>Anzahl paralleler Worker für Transformationen:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QSpinBox" name="spinBoxWorkerCount">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Anzahl der parallelen Worker-Threads für Transformationen (Standard: 8)</string>
|
|
||||||
</property>
|
|
||||||
<property name="minimum">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
|
||||||
<number>32</number>
|
|
||||||
</property>
|
|
||||||
<property name="value">
|
|
||||||
<number>8</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="groupBoxSaxonPool">
|
|
||||||
<property name="title">
|
|
||||||
<string>SaxonWorkerPool Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_11">
|
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="checkBoxUseSaxonPool">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Aktiviert persistente JVM-Prozesse für Saxon-Transformationen.
|
|
||||||
Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead
|
|
||||||
Nachteile: Benötigt JDK (javac) - funktioniert nicht mit JRE allein
|
|
||||||
|
|
||||||
Deaktivieren Sie diese Option, wenn:
|
|
||||||
• Sie nur ein JRE (keine JDK) installiert haben
|
|
||||||
• Sie Probleme mit dem Worker-Pool haben
|
|
||||||
• Sie die Funktion testen möchten</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>SaxonWorkerPool verwenden (empfohlen)</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayoutXsltVersion">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="labelXsltVersion">
|
|
||||||
<property name="text">
|
|
||||||
<string>XSLT-Version:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="comboBoxXsltVersion">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Wählen Sie die XSLT-Version für Saxon-Transformationen:
|
|
||||||
|
|
||||||
XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API
|
|
||||||
• Nur für XSLT 1.0 vollständig spezifiziert
|
|
||||||
• Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben führen
|
|
||||||
|
|
||||||
XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api
|
|
||||||
• Vollständige Unterstützung für XSLT 2.0 und 3.0
|
|
||||||
• Empfohlen für moderne XSLT-Stylesheets</string>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>XSLT 1.0 (JAXP)</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>XSLT 2.0/3.0 (s9api) - Empfohlen</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacerXsltVersion">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="labelSaxonPoolInfo">
|
|
||||||
<property name="text">
|
|
||||||
<string><i>Hinweis: SaxonWorkerPool benötigt ein JDK (Java Development Kit).<br>Mit JRE allein werden Transformationen im Fallback-Modus ausgeführt.</i></string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="groupBoxFopPool">
|
|
||||||
<property name="title">
|
|
||||||
<string>FopWorkerPool Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_12">
|
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="checkBoxUseFopPool">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Aktiviert persistente JVM-Prozesse für Apache FOP PDF-Generierung.
|
|
||||||
Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead
|
|
||||||
Nachteile: Benötigt JDK (javac) - funktioniert nicht mit JRE allein
|
|
||||||
|
|
||||||
Deaktivieren Sie diese Option, wenn:
|
|
||||||
• Sie nur ein JRE (keine JDK) installiert haben
|
|
||||||
• Sie Probleme mit dem Worker-Pool haben
|
|
||||||
• Sie die Funktion testen möchten</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>FopWorkerPool verwenden (empfohlen)</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="labelFopPoolInfo">
|
|
||||||
<property name="text">
|
|
||||||
<string><i>Hinweis: FopWorkerPool benötigt ein JDK (Java Development Kit).<br>Mit JRE allein werden PDFs im Fallback-Modus generiert.</i></string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="mouseTracking">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:700; font-style:italic;">Hinweis: Änderungen in diesem Dialog sind unter Umständen erst nach neu start der Anwendung wirksam.</span></p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacerPerformance">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|||||||
+351
-498
@@ -1,498 +1,351 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'AppSettings.ui'
|
## Form generated from reading UI file 'AppSettings.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.9.2
|
## Created by: Qt User Interface Compiler version 6.9.1
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
QMetaObject, QObject, QPoint, QRect,
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
QSize, QTime, QUrl, Qt)
|
QSize, QTime, QUrl, Qt)
|
||||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
||||||
QDialog, QDialogButtonBox, QFrame, QGroupBox,
|
QFrame, QHBoxLayout, QHeaderView, QPushButton,
|
||||||
QHBoxLayout, QHeaderView, QLabel, QPushButton,
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
||||||
QSizePolicy, QSpacerItem, QSpinBox, QTabWidget,
|
QVBoxLayout, QWidget)
|
||||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
|
|
||||||
|
class Ui_Dialog(object):
|
||||||
class Ui_Dialog(object):
|
def setupUi(self, Dialog):
|
||||||
def setupUi(self, Dialog):
|
if not Dialog.objectName():
|
||||||
if not Dialog.objectName():
|
Dialog.setObjectName(u"Dialog")
|
||||||
Dialog.setObjectName(u"Dialog")
|
Dialog.resize(833, 387)
|
||||||
Dialog.resize(833, 526)
|
self.verticalLayout = QVBoxLayout(Dialog)
|
||||||
self.verticalLayout = QVBoxLayout(Dialog)
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
self.tabSettings = QTabWidget(Dialog)
|
||||||
self.tabSettings = QTabWidget(Dialog)
|
self.tabSettings.setObjectName(u"tabSettings")
|
||||||
self.tabSettings.setObjectName(u"tabSettings")
|
self.tabSettings.setEnabled(True)
|
||||||
self.tabSettings.setEnabled(True)
|
self.tabSettings.setElideMode(Qt.TextElideMode.ElideRight)
|
||||||
self.tabSettings.setElideMode(Qt.TextElideMode.ElideRight)
|
self.tabXsls = QWidget()
|
||||||
self.tabXsls = QWidget()
|
self.tabXsls.setObjectName(u"tabXsls")
|
||||||
self.tabXsls.setObjectName(u"tabXsls")
|
self.verticalLayout_5 = QVBoxLayout(self.tabXsls)
|
||||||
self.verticalLayout_5 = QVBoxLayout(self.tabXsls)
|
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
|
||||||
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
|
self.tableXsls = QTableWidget(self.tabXsls)
|
||||||
self.tableXsls = QTableWidget(self.tabXsls)
|
if (self.tableXsls.columnCount() < 2):
|
||||||
if (self.tableXsls.columnCount() < 2):
|
self.tableXsls.setColumnCount(2)
|
||||||
self.tableXsls.setColumnCount(2)
|
self.tableXsls.setObjectName(u"tableXsls")
|
||||||
self.tableXsls.setObjectName(u"tableXsls")
|
self.tableXsls.setColumnCount(2)
|
||||||
self.tableXsls.setColumnCount(2)
|
|
||||||
|
self.verticalLayout_5.addWidget(self.tableXsls)
|
||||||
self.verticalLayout_5.addWidget(self.tableXsls)
|
|
||||||
|
self.frame_2 = QFrame(self.tabXsls)
|
||||||
self.frame_2 = QFrame(self.tabXsls)
|
self.frame_2.setObjectName(u"frame_2")
|
||||||
self.frame_2.setObjectName(u"frame_2")
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred)
|
||||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth())
|
self.frame_2.setSizePolicy(sizePolicy)
|
||||||
self.frame_2.setSizePolicy(sizePolicy)
|
self.frame_2.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_2.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
|
||||||
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
self.horizontalLayout_2.setContentsMargins(0, 1, 0, 0)
|
||||||
self.horizontalLayout_2.setContentsMargins(0, 1, 0, 0)
|
self.addXsl = QPushButton(self.frame_2)
|
||||||
self.addXsl = QPushButton(self.frame_2)
|
self.addXsl.setObjectName(u"addXsl")
|
||||||
self.addXsl.setObjectName(u"addXsl")
|
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
|
||||||
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
|
self.addXsl.setIcon(icon)
|
||||||
self.addXsl.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.addXsl)
|
||||||
self.horizontalLayout_2.addWidget(self.addXsl)
|
|
||||||
|
self.removeXsl = QPushButton(self.frame_2)
|
||||||
self.removeXsl = QPushButton(self.frame_2)
|
self.removeXsl.setObjectName(u"removeXsl")
|
||||||
self.removeXsl.setObjectName(u"removeXsl")
|
self.removeXsl.setEnabled(False)
|
||||||
self.removeXsl.setEnabled(False)
|
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
|
||||||
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
|
self.removeXsl.setIcon(icon1)
|
||||||
self.removeXsl.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.removeXsl)
|
||||||
self.horizontalLayout_2.addWidget(self.removeXsl)
|
|
||||||
|
|
||||||
|
self.verticalLayout_5.addWidget(self.frame_2)
|
||||||
self.verticalLayout_5.addWidget(self.frame_2)
|
|
||||||
|
self.tabSettings.addTab(self.tabXsls, "")
|
||||||
self.tabSettings.addTab(self.tabXsls, "")
|
self.tabJavaVm = QWidget()
|
||||||
self.tabJavaVm = QWidget()
|
self.tabJavaVm.setObjectName(u"tabJavaVm")
|
||||||
self.tabJavaVm.setObjectName(u"tabJavaVm")
|
self.verticalLayout_2 = QVBoxLayout(self.tabJavaVm)
|
||||||
self.verticalLayout_2 = QVBoxLayout(self.tabJavaVm)
|
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
||||||
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
|
self.tableJavaVms = QTableWidget(self.tabJavaVm)
|
||||||
self.tableJavaVms = QTableWidget(self.tabJavaVm)
|
if (self.tableJavaVms.columnCount() < 2):
|
||||||
if (self.tableJavaVms.columnCount() < 2):
|
self.tableJavaVms.setColumnCount(2)
|
||||||
self.tableJavaVms.setColumnCount(2)
|
self.tableJavaVms.setObjectName(u"tableJavaVms")
|
||||||
self.tableJavaVms.setObjectName(u"tableJavaVms")
|
self.tableJavaVms.setColumnCount(2)
|
||||||
self.tableJavaVms.setColumnCount(2)
|
|
||||||
|
self.verticalLayout_2.addWidget(self.tableJavaVms)
|
||||||
self.verticalLayout_2.addWidget(self.tableJavaVms)
|
|
||||||
|
self.frame_3 = QFrame(self.tabJavaVm)
|
||||||
self.frame_3 = QFrame(self.tabJavaVm)
|
self.frame_3.setObjectName(u"frame_3")
|
||||||
self.frame_3.setObjectName(u"frame_3")
|
sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth())
|
self.frame_3.setSizePolicy(sizePolicy)
|
||||||
self.frame_3.setSizePolicy(sizePolicy)
|
self.frame_3.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_3.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_3 = QHBoxLayout(self.frame_3)
|
||||||
self.horizontalLayout_3 = QHBoxLayout(self.frame_3)
|
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
|
||||||
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
|
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
|
self.addJavaVm = QPushButton(self.frame_3)
|
||||||
self.addJavaVm = QPushButton(self.frame_3)
|
self.addJavaVm.setObjectName(u"addJavaVm")
|
||||||
self.addJavaVm.setObjectName(u"addJavaVm")
|
self.addJavaVm.setIcon(icon)
|
||||||
self.addJavaVm.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_3.addWidget(self.addJavaVm)
|
||||||
self.horizontalLayout_3.addWidget(self.addJavaVm)
|
|
||||||
|
self.removeJavaVm = QPushButton(self.frame_3)
|
||||||
self.removeJavaVm = QPushButton(self.frame_3)
|
self.removeJavaVm.setObjectName(u"removeJavaVm")
|
||||||
self.removeJavaVm.setObjectName(u"removeJavaVm")
|
self.removeJavaVm.setEnabled(False)
|
||||||
self.removeJavaVm.setEnabled(False)
|
self.removeJavaVm.setIcon(icon1)
|
||||||
self.removeJavaVm.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_3.addWidget(self.removeJavaVm)
|
||||||
self.horizontalLayout_3.addWidget(self.removeJavaVm)
|
|
||||||
|
|
||||||
|
self.verticalLayout_2.addWidget(self.frame_3)
|
||||||
self.verticalLayout_2.addWidget(self.frame_3)
|
|
||||||
|
self.tabSettings.addTab(self.tabJavaVm, "")
|
||||||
self.tabSettings.addTab(self.tabJavaVm, "")
|
self.tabSaxon = QWidget()
|
||||||
self.tabSaxon = QWidget()
|
self.tabSaxon.setObjectName(u"tabSaxon")
|
||||||
self.tabSaxon.setObjectName(u"tabSaxon")
|
self.verticalLayout_4 = QVBoxLayout(self.tabSaxon)
|
||||||
self.verticalLayout_4 = QVBoxLayout(self.tabSaxon)
|
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
|
||||||
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
|
self.tableSaxons = QTableWidget(self.tabSaxon)
|
||||||
self.tableSaxons = QTableWidget(self.tabSaxon)
|
if (self.tableSaxons.columnCount() < 3):
|
||||||
if (self.tableSaxons.columnCount() < 3):
|
self.tableSaxons.setColumnCount(3)
|
||||||
self.tableSaxons.setColumnCount(3)
|
self.tableSaxons.setObjectName(u"tableSaxons")
|
||||||
self.tableSaxons.setObjectName(u"tableSaxons")
|
self.tableSaxons.setColumnCount(3)
|
||||||
self.tableSaxons.setColumnCount(3)
|
|
||||||
|
self.verticalLayout_4.addWidget(self.tableSaxons)
|
||||||
self.verticalLayout_4.addWidget(self.tableSaxons)
|
|
||||||
|
self.frame_4 = QFrame(self.tabSaxon)
|
||||||
self.frame_4 = QFrame(self.tabSaxon)
|
self.frame_4.setObjectName(u"frame_4")
|
||||||
self.frame_4.setObjectName(u"frame_4")
|
sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth())
|
self.frame_4.setSizePolicy(sizePolicy)
|
||||||
self.frame_4.setSizePolicy(sizePolicy)
|
self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_4 = QHBoxLayout(self.frame_4)
|
||||||
self.horizontalLayout_4 = QHBoxLayout(self.frame_4)
|
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
|
||||||
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
|
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
|
self.addSaxon = QPushButton(self.frame_4)
|
||||||
self.addSaxon = QPushButton(self.frame_4)
|
self.addSaxon.setObjectName(u"addSaxon")
|
||||||
self.addSaxon.setObjectName(u"addSaxon")
|
self.addSaxon.setIcon(icon)
|
||||||
self.addSaxon.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.addSaxon)
|
||||||
self.horizontalLayout_4.addWidget(self.addSaxon)
|
|
||||||
|
self.removeSaxon = QPushButton(self.frame_4)
|
||||||
self.removeSaxon = QPushButton(self.frame_4)
|
self.removeSaxon.setObjectName(u"removeSaxon")
|
||||||
self.removeSaxon.setObjectName(u"removeSaxon")
|
self.removeSaxon.setEnabled(False)
|
||||||
self.removeSaxon.setEnabled(False)
|
self.removeSaxon.setIcon(icon1)
|
||||||
self.removeSaxon.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.removeSaxon)
|
||||||
self.horizontalLayout_4.addWidget(self.removeSaxon)
|
|
||||||
|
|
||||||
|
self.verticalLayout_4.addWidget(self.frame_4)
|
||||||
self.verticalLayout_4.addWidget(self.frame_4)
|
|
||||||
|
self.tabSettings.addTab(self.tabSaxon, "")
|
||||||
self.tabSettings.addTab(self.tabSaxon, "")
|
self.tabApacheFop = QWidget()
|
||||||
self.tabApacheFop = QWidget()
|
self.tabApacheFop.setObjectName(u"tabApacheFop")
|
||||||
self.tabApacheFop.setObjectName(u"tabApacheFop")
|
self.verticalLayout_3 = QVBoxLayout(self.tabApacheFop)
|
||||||
self.verticalLayout_3 = QVBoxLayout(self.tabApacheFop)
|
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
|
||||||
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
|
self.tableApacheFops = QTableWidget(self.tabApacheFop)
|
||||||
self.tableApacheFops = QTableWidget(self.tabApacheFop)
|
if (self.tableApacheFops.columnCount() < 3):
|
||||||
if (self.tableApacheFops.columnCount() < 3):
|
self.tableApacheFops.setColumnCount(3)
|
||||||
self.tableApacheFops.setColumnCount(3)
|
self.tableApacheFops.setObjectName(u"tableApacheFops")
|
||||||
self.tableApacheFops.setObjectName(u"tableApacheFops")
|
self.tableApacheFops.setColumnCount(3)
|
||||||
self.tableApacheFops.setColumnCount(3)
|
|
||||||
|
self.verticalLayout_3.addWidget(self.tableApacheFops)
|
||||||
self.verticalLayout_3.addWidget(self.tableApacheFops)
|
|
||||||
|
self.frame = QFrame(self.tabApacheFop)
|
||||||
self.frame = QFrame(self.tabApacheFop)
|
self.frame.setObjectName(u"frame")
|
||||||
self.frame.setObjectName(u"frame")
|
sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth())
|
self.frame.setSizePolicy(sizePolicy)
|
||||||
self.frame.setSizePolicy(sizePolicy)
|
self.frame.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout = QHBoxLayout(self.frame)
|
||||||
self.horizontalLayout = QHBoxLayout(self.frame)
|
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
self.addApacheFop = QPushButton(self.frame)
|
||||||
self.addApacheFop = QPushButton(self.frame)
|
self.addApacheFop.setObjectName(u"addApacheFop")
|
||||||
self.addApacheFop.setObjectName(u"addApacheFop")
|
self.addApacheFop.setIcon(icon)
|
||||||
self.addApacheFop.setIcon(icon)
|
|
||||||
|
self.horizontalLayout.addWidget(self.addApacheFop)
|
||||||
self.horizontalLayout.addWidget(self.addApacheFop)
|
|
||||||
|
self.removeApacheFop = QPushButton(self.frame)
|
||||||
self.removeApacheFop = QPushButton(self.frame)
|
self.removeApacheFop.setObjectName(u"removeApacheFop")
|
||||||
self.removeApacheFop.setObjectName(u"removeApacheFop")
|
self.removeApacheFop.setEnabled(False)
|
||||||
self.removeApacheFop.setEnabled(False)
|
self.removeApacheFop.setIcon(icon1)
|
||||||
self.removeApacheFop.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout.addWidget(self.removeApacheFop)
|
||||||
self.horizontalLayout.addWidget(self.removeApacheFop)
|
|
||||||
|
|
||||||
|
self.verticalLayout_3.addWidget(self.frame)
|
||||||
self.verticalLayout_3.addWidget(self.frame)
|
|
||||||
|
self.tabSettings.addTab(self.tabApacheFop, "")
|
||||||
self.tabSettings.addTab(self.tabApacheFop, "")
|
self.tabDiffPdf = QWidget()
|
||||||
self.tabDiffPdf = QWidget()
|
self.tabDiffPdf.setObjectName(u"tabDiffPdf")
|
||||||
self.tabDiffPdf.setObjectName(u"tabDiffPdf")
|
self.verticalLayout_6 = QVBoxLayout(self.tabDiffPdf)
|
||||||
self.verticalLayout_6 = QVBoxLayout(self.tabDiffPdf)
|
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
|
||||||
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
|
self.tableDiffPdfs = QTableWidget(self.tabDiffPdf)
|
||||||
self.tableDiffPdfs = QTableWidget(self.tabDiffPdf)
|
if (self.tableDiffPdfs.columnCount() < 4):
|
||||||
if (self.tableDiffPdfs.columnCount() < 4):
|
self.tableDiffPdfs.setColumnCount(4)
|
||||||
self.tableDiffPdfs.setColumnCount(4)
|
self.tableDiffPdfs.setObjectName(u"tableDiffPdfs")
|
||||||
self.tableDiffPdfs.setObjectName(u"tableDiffPdfs")
|
self.tableDiffPdfs.setColumnCount(4)
|
||||||
self.tableDiffPdfs.setColumnCount(4)
|
|
||||||
|
self.verticalLayout_6.addWidget(self.tableDiffPdfs)
|
||||||
self.verticalLayout_6.addWidget(self.tableDiffPdfs)
|
|
||||||
|
self.frame_5 = QFrame(self.tabDiffPdf)
|
||||||
self.frame_5 = QFrame(self.tabDiffPdf)
|
self.frame_5.setObjectName(u"frame_5")
|
||||||
self.frame_5.setObjectName(u"frame_5")
|
sizePolicy.setHeightForWidth(self.frame_5.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_5.sizePolicy().hasHeightForWidth())
|
self.frame_5.setSizePolicy(sizePolicy)
|
||||||
self.frame_5.setSizePolicy(sizePolicy)
|
self.frame_5.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_5.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_5.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_5.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_5 = QHBoxLayout(self.frame_5)
|
||||||
self.horizontalLayout_5 = QHBoxLayout(self.frame_5)
|
self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
|
||||||
self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
|
self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
|
self.addDiffPdf = QPushButton(self.frame_5)
|
||||||
self.addDiffPdf = QPushButton(self.frame_5)
|
self.addDiffPdf.setObjectName(u"addDiffPdf")
|
||||||
self.addDiffPdf.setObjectName(u"addDiffPdf")
|
self.addDiffPdf.setIcon(icon)
|
||||||
self.addDiffPdf.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_5.addWidget(self.addDiffPdf)
|
||||||
self.horizontalLayout_5.addWidget(self.addDiffPdf)
|
|
||||||
|
self.removeDiffPdf = QPushButton(self.frame_5)
|
||||||
self.removeDiffPdf = QPushButton(self.frame_5)
|
self.removeDiffPdf.setObjectName(u"removeDiffPdf")
|
||||||
self.removeDiffPdf.setObjectName(u"removeDiffPdf")
|
self.removeDiffPdf.setEnabled(False)
|
||||||
self.removeDiffPdf.setEnabled(False)
|
self.removeDiffPdf.setIcon(icon1)
|
||||||
self.removeDiffPdf.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_5.addWidget(self.removeDiffPdf)
|
||||||
self.horizontalLayout_5.addWidget(self.removeDiffPdf)
|
|
||||||
|
|
||||||
|
self.verticalLayout_6.addWidget(self.frame_5)
|
||||||
self.verticalLayout_6.addWidget(self.frame_5)
|
|
||||||
|
self.tabSettings.addTab(self.tabDiffPdf, "")
|
||||||
self.tabSettings.addTab(self.tabDiffPdf, "")
|
self.tabPostgreSql = QWidget()
|
||||||
self.tabPostgreSql = QWidget()
|
self.tabPostgreSql.setObjectName(u"tabPostgreSql")
|
||||||
self.tabPostgreSql.setObjectName(u"tabPostgreSql")
|
self.verticalLayout_8 = QVBoxLayout(self.tabPostgreSql)
|
||||||
self.verticalLayout_8 = QVBoxLayout(self.tabPostgreSql)
|
self.verticalLayout_8.setObjectName(u"verticalLayout_8")
|
||||||
self.verticalLayout_8.setObjectName(u"verticalLayout_8")
|
self.tablePostgreSqlDbs = QTableWidget(self.tabPostgreSql)
|
||||||
self.tablePostgreSqlDbs = QTableWidget(self.tabPostgreSql)
|
if (self.tablePostgreSqlDbs.columnCount() < 5):
|
||||||
if (self.tablePostgreSqlDbs.columnCount() < 5):
|
self.tablePostgreSqlDbs.setColumnCount(5)
|
||||||
self.tablePostgreSqlDbs.setColumnCount(5)
|
self.tablePostgreSqlDbs.setObjectName(u"tablePostgreSqlDbs")
|
||||||
self.tablePostgreSqlDbs.setObjectName(u"tablePostgreSqlDbs")
|
self.tablePostgreSqlDbs.setColumnCount(5)
|
||||||
self.tablePostgreSqlDbs.setColumnCount(5)
|
|
||||||
|
self.verticalLayout_8.addWidget(self.tablePostgreSqlDbs)
|
||||||
self.verticalLayout_8.addWidget(self.tablePostgreSqlDbs)
|
|
||||||
|
self.frame_7 = QFrame(self.tabPostgreSql)
|
||||||
self.frame_7 = QFrame(self.tabPostgreSql)
|
self.frame_7.setObjectName(u"frame_7")
|
||||||
self.frame_7.setObjectName(u"frame_7")
|
sizePolicy.setHeightForWidth(self.frame_7.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_7.sizePolicy().hasHeightForWidth())
|
self.frame_7.setSizePolicy(sizePolicy)
|
||||||
self.frame_7.setSizePolicy(sizePolicy)
|
self.frame_7.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_7.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_7.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_7.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_7 = QHBoxLayout(self.frame_7)
|
||||||
self.horizontalLayout_7 = QHBoxLayout(self.frame_7)
|
self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
|
||||||
self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
|
self.horizontalLayout_7.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout_7.setContentsMargins(0, 0, 0, 0)
|
self.addPostgreSql = QPushButton(self.frame_7)
|
||||||
self.addPostgreSql = QPushButton(self.frame_7)
|
self.addPostgreSql.setObjectName(u"addPostgreSql")
|
||||||
self.addPostgreSql.setObjectName(u"addPostgreSql")
|
self.addPostgreSql.setIcon(icon)
|
||||||
self.addPostgreSql.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_7.addWidget(self.addPostgreSql)
|
||||||
self.horizontalLayout_7.addWidget(self.addPostgreSql)
|
|
||||||
|
self.removePostgreSql = QPushButton(self.frame_7)
|
||||||
self.removePostgreSql = QPushButton(self.frame_7)
|
self.removePostgreSql.setObjectName(u"removePostgreSql")
|
||||||
self.removePostgreSql.setObjectName(u"removePostgreSql")
|
self.removePostgreSql.setEnabled(False)
|
||||||
self.removePostgreSql.setEnabled(False)
|
self.removePostgreSql.setIcon(icon1)
|
||||||
self.removePostgreSql.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_7.addWidget(self.removePostgreSql)
|
||||||
self.horizontalLayout_7.addWidget(self.removePostgreSql)
|
|
||||||
|
|
||||||
|
self.verticalLayout_8.addWidget(self.frame_7)
|
||||||
self.verticalLayout_8.addWidget(self.frame_7)
|
|
||||||
|
self.tabSettings.addTab(self.tabPostgreSql, "")
|
||||||
self.tabSettings.addTab(self.tabPostgreSql, "")
|
self.tabPdfProject = QWidget()
|
||||||
self.tabPdfProject = QWidget()
|
self.tabPdfProject.setObjectName(u"tabPdfProject")
|
||||||
self.tabPdfProject.setObjectName(u"tabPdfProject")
|
self.verticalLayout_7 = QVBoxLayout(self.tabPdfProject)
|
||||||
self.verticalLayout_7 = QVBoxLayout(self.tabPdfProject)
|
self.verticalLayout_7.setObjectName(u"verticalLayout_7")
|
||||||
self.verticalLayout_7.setObjectName(u"verticalLayout_7")
|
self.tablePdfProjects = QTableWidget(self.tabPdfProject)
|
||||||
self.tablePdfProjects = QTableWidget(self.tabPdfProject)
|
if (self.tablePdfProjects.columnCount() < 7):
|
||||||
if (self.tablePdfProjects.columnCount() < 7):
|
self.tablePdfProjects.setColumnCount(7)
|
||||||
self.tablePdfProjects.setColumnCount(7)
|
self.tablePdfProjects.setObjectName(u"tablePdfProjects")
|
||||||
self.tablePdfProjects.setObjectName(u"tablePdfProjects")
|
self.tablePdfProjects.setColumnCount(7)
|
||||||
self.tablePdfProjects.setColumnCount(7)
|
|
||||||
|
self.verticalLayout_7.addWidget(self.tablePdfProjects)
|
||||||
self.verticalLayout_7.addWidget(self.tablePdfProjects)
|
|
||||||
|
self.frame_6 = QFrame(self.tabPdfProject)
|
||||||
self.frame_6 = QFrame(self.tabPdfProject)
|
self.frame_6.setObjectName(u"frame_6")
|
||||||
self.frame_6.setObjectName(u"frame_6")
|
sizePolicy.setHeightForWidth(self.frame_6.sizePolicy().hasHeightForWidth())
|
||||||
sizePolicy.setHeightForWidth(self.frame_6.sizePolicy().hasHeightForWidth())
|
self.frame_6.setSizePolicy(sizePolicy)
|
||||||
self.frame_6.setSizePolicy(sizePolicy)
|
self.frame_6.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.frame_6.setFrameShape(QFrame.Shape.NoFrame)
|
self.frame_6.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.frame_6.setFrameShadow(QFrame.Shadow.Raised)
|
self.horizontalLayout_6 = QHBoxLayout(self.frame_6)
|
||||||
self.horizontalLayout_6 = QHBoxLayout(self.frame_6)
|
self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
|
||||||
self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
|
self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0)
|
||||||
self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0)
|
self.addProject = QPushButton(self.frame_6)
|
||||||
self.addProject = QPushButton(self.frame_6)
|
self.addProject.setObjectName(u"addProject")
|
||||||
self.addProject.setObjectName(u"addProject")
|
self.addProject.setIcon(icon)
|
||||||
self.addProject.setIcon(icon)
|
|
||||||
|
self.horizontalLayout_6.addWidget(self.addProject)
|
||||||
self.horizontalLayout_6.addWidget(self.addProject)
|
|
||||||
|
self.removeProject = QPushButton(self.frame_6)
|
||||||
self.removeProject = QPushButton(self.frame_6)
|
self.removeProject.setObjectName(u"removeProject")
|
||||||
self.removeProject.setObjectName(u"removeProject")
|
self.removeProject.setEnabled(False)
|
||||||
self.removeProject.setEnabled(False)
|
self.removeProject.setIcon(icon1)
|
||||||
self.removeProject.setIcon(icon1)
|
|
||||||
|
self.horizontalLayout_6.addWidget(self.removeProject)
|
||||||
self.horizontalLayout_6.addWidget(self.removeProject)
|
|
||||||
|
|
||||||
|
self.verticalLayout_7.addWidget(self.frame_6)
|
||||||
self.verticalLayout_7.addWidget(self.frame_6)
|
|
||||||
|
self.tabSettings.addTab(self.tabPdfProject, "")
|
||||||
self.tabSettings.addTab(self.tabPdfProject, "")
|
|
||||||
self.tabPerformance = QWidget()
|
self.verticalLayout.addWidget(self.tabSettings)
|
||||||
self.tabPerformance.setObjectName(u"tabPerformance")
|
|
||||||
self.verticalLayout_9 = QVBoxLayout(self.tabPerformance)
|
self.buttonBox = QDialogButtonBox(Dialog)
|
||||||
self.verticalLayout_9.setObjectName(u"verticalLayout_9")
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
self.groupBoxWorker = QGroupBox(self.tabPerformance)
|
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||||
self.groupBoxWorker.setObjectName(u"groupBoxWorker")
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
self.verticalLayout_10 = QVBoxLayout(self.groupBoxWorker)
|
self.buttonBox.setCenterButtons(True)
|
||||||
self.verticalLayout_10.setObjectName(u"verticalLayout_10")
|
|
||||||
self.labelWorkerCount = QLabel(self.groupBoxWorker)
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
self.labelWorkerCount.setObjectName(u"labelWorkerCount")
|
|
||||||
|
|
||||||
self.verticalLayout_10.addWidget(self.labelWorkerCount)
|
self.retranslateUi(Dialog)
|
||||||
|
self.buttonBox.accepted.connect(Dialog.accept)
|
||||||
self.spinBoxWorkerCount = QSpinBox(self.groupBoxWorker)
|
self.buttonBox.rejected.connect(Dialog.reject)
|
||||||
self.spinBoxWorkerCount.setObjectName(u"spinBoxWorkerCount")
|
|
||||||
self.spinBoxWorkerCount.setMinimum(1)
|
self.tabSettings.setCurrentIndex(0)
|
||||||
self.spinBoxWorkerCount.setMaximum(32)
|
|
||||||
self.spinBoxWorkerCount.setValue(8)
|
|
||||||
|
QMetaObject.connectSlotsByName(Dialog)
|
||||||
self.verticalLayout_10.addWidget(self.spinBoxWorkerCount)
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, Dialog):
|
||||||
self.verticalLayout_9.addWidget(self.groupBoxWorker)
|
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Programm Einstellungen", None))
|
||||||
|
self.addXsl.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
self.groupBoxSaxonPool = QGroupBox(self.tabPerformance)
|
self.removeXsl.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.groupBoxSaxonPool.setObjectName(u"groupBoxSaxonPool")
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabXsls), QCoreApplication.translate("Dialog", u"XSL-Ordner", None))
|
||||||
self.verticalLayout_11 = QVBoxLayout(self.groupBoxSaxonPool)
|
self.addJavaVm.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
self.verticalLayout_11.setObjectName(u"verticalLayout_11")
|
self.removeJavaVm.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.checkBoxUseSaxonPool = QCheckBox(self.groupBoxSaxonPool)
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabJavaVm), QCoreApplication.translate("Dialog", u"Java VM", None))
|
||||||
self.checkBoxUseSaxonPool.setObjectName(u"checkBoxUseSaxonPool")
|
self.addSaxon.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
|
self.removeSaxon.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.verticalLayout_11.addWidget(self.checkBoxUseSaxonPool)
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabSaxon), QCoreApplication.translate("Dialog", u"Saxon", None))
|
||||||
|
self.addApacheFop.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
self.horizontalLayoutXsltVersion = QHBoxLayout()
|
self.removeApacheFop.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.horizontalLayoutXsltVersion.setObjectName(u"horizontalLayoutXsltVersion")
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabApacheFop), QCoreApplication.translate("Dialog", u"Apache FOP", None))
|
||||||
self.labelXsltVersion = QLabel(self.groupBoxSaxonPool)
|
self.addDiffPdf.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
self.labelXsltVersion.setObjectName(u"labelXsltVersion")
|
self.removeDiffPdf.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabDiffPdf), QCoreApplication.translate("Dialog", u"Diff-PDF", None))
|
||||||
self.horizontalLayoutXsltVersion.addWidget(self.labelXsltVersion)
|
self.addPostgreSql.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
|
self.removePostgreSql.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.comboBoxXsltVersion = QComboBox(self.groupBoxSaxonPool)
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPostgreSql), QCoreApplication.translate("Dialog", u"PostgreSQL", None))
|
||||||
self.comboBoxXsltVersion.addItem("")
|
self.addProject.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
||||||
self.comboBoxXsltVersion.addItem("")
|
self.removeProject.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
||||||
self.comboBoxXsltVersion.setObjectName(u"comboBoxXsltVersion")
|
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPdfProject), QCoreApplication.translate("Dialog", u"PDF-Projekte", None))
|
||||||
|
# retranslateUi
|
||||||
self.horizontalLayoutXsltVersion.addWidget(self.comboBoxXsltVersion)
|
|
||||||
|
|
||||||
self.horizontalSpacerXsltVersion = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
||||||
|
|
||||||
self.horizontalLayoutXsltVersion.addItem(self.horizontalSpacerXsltVersion)
|
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout_11.addLayout(self.horizontalLayoutXsltVersion)
|
|
||||||
|
|
||||||
self.labelSaxonPoolInfo = QLabel(self.groupBoxSaxonPool)
|
|
||||||
self.labelSaxonPoolInfo.setObjectName(u"labelSaxonPoolInfo")
|
|
||||||
self.labelSaxonPoolInfo.setWordWrap(True)
|
|
||||||
|
|
||||||
self.verticalLayout_11.addWidget(self.labelSaxonPoolInfo)
|
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout_9.addWidget(self.groupBoxSaxonPool)
|
|
||||||
|
|
||||||
self.groupBoxFopPool = QGroupBox(self.tabPerformance)
|
|
||||||
self.groupBoxFopPool.setObjectName(u"groupBoxFopPool")
|
|
||||||
self.verticalLayout_12 = QVBoxLayout(self.groupBoxFopPool)
|
|
||||||
self.verticalLayout_12.setObjectName(u"verticalLayout_12")
|
|
||||||
self.checkBoxUseFopPool = QCheckBox(self.groupBoxFopPool)
|
|
||||||
self.checkBoxUseFopPool.setObjectName(u"checkBoxUseFopPool")
|
|
||||||
|
|
||||||
self.verticalLayout_12.addWidget(self.checkBoxUseFopPool)
|
|
||||||
|
|
||||||
self.labelFopPoolInfo = QLabel(self.groupBoxFopPool)
|
|
||||||
self.labelFopPoolInfo.setObjectName(u"labelFopPoolInfo")
|
|
||||||
self.labelFopPoolInfo.setWordWrap(True)
|
|
||||||
|
|
||||||
self.verticalLayout_12.addWidget(self.labelFopPoolInfo)
|
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout_9.addWidget(self.groupBoxFopPool)
|
|
||||||
|
|
||||||
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
|
|
||||||
self.verticalLayout_9.addItem(self.verticalSpacer)
|
|
||||||
|
|
||||||
self.label = QLabel(self.tabPerformance)
|
|
||||||
self.label.setObjectName(u"label")
|
|
||||||
self.label.setMouseTracking(True)
|
|
||||||
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.label.setWordWrap(True)
|
|
||||||
|
|
||||||
self.verticalLayout_9.addWidget(self.label)
|
|
||||||
|
|
||||||
self.verticalSpacerPerformance = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
|
|
||||||
self.verticalLayout_9.addItem(self.verticalSpacerPerformance)
|
|
||||||
|
|
||||||
self.tabSettings.addTab(self.tabPerformance, "")
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.tabSettings)
|
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(Dialog)
|
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
|
||||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
|
||||||
self.buttonBox.setCenterButtons(True)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
|
||||||
|
|
||||||
|
|
||||||
self.retranslateUi(Dialog)
|
|
||||||
self.buttonBox.accepted.connect(Dialog.accept)
|
|
||||||
self.buttonBox.rejected.connect(Dialog.reject)
|
|
||||||
|
|
||||||
self.tabSettings.setCurrentIndex(0)
|
|
||||||
|
|
||||||
|
|
||||||
QMetaObject.connectSlotsByName(Dialog)
|
|
||||||
# setupUi
|
|
||||||
|
|
||||||
def retranslateUi(self, Dialog):
|
|
||||||
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Programm Einstellungen", None))
|
|
||||||
self.addXsl.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeXsl.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabXsls), QCoreApplication.translate("Dialog", u"XSL-Ordner", None))
|
|
||||||
self.addJavaVm.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeJavaVm.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabJavaVm), QCoreApplication.translate("Dialog", u"Java VM", None))
|
|
||||||
self.addSaxon.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeSaxon.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabSaxon), QCoreApplication.translate("Dialog", u"Saxon", None))
|
|
||||||
self.addApacheFop.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeApacheFop.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabApacheFop), QCoreApplication.translate("Dialog", u"Apache FOP", None))
|
|
||||||
self.addDiffPdf.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeDiffPdf.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabDiffPdf), QCoreApplication.translate("Dialog", u"Diff-PDF", None))
|
|
||||||
self.addPostgreSql.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removePostgreSql.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPostgreSql), QCoreApplication.translate("Dialog", u"PostgreSQL", None))
|
|
||||||
self.addProject.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
|
|
||||||
self.removeProject.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPdfProject), QCoreApplication.translate("Dialog", u"PDF-Projekte", None))
|
|
||||||
self.groupBoxWorker.setTitle(QCoreApplication.translate("Dialog", u"ThreadPoolExecutor Einstellungen", None))
|
|
||||||
self.labelWorkerCount.setText(QCoreApplication.translate("Dialog", u"Anzahl paralleler Worker f\u00fcr Transformationen:", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.spinBoxWorkerCount.setToolTip(QCoreApplication.translate("Dialog", u"Anzahl der parallelen Worker-Threads f\u00fcr Transformationen (Standard: 8)", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.groupBoxSaxonPool.setTitle(QCoreApplication.translate("Dialog", u"SaxonWorkerPool Einstellungen", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.checkBoxUseSaxonPool.setToolTip(QCoreApplication.translate("Dialog", u"Aktiviert persistente JVM-Prozesse f\u00fcr Saxon-Transformationen.\n"
|
|
||||||
"Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead\n"
|
|
||||||
"Nachteile: Ben\u00f6tigt JDK (javac) - funktioniert nicht mit JRE allein\n"
|
|
||||||
"\n"
|
|
||||||
"Deaktivieren Sie diese Option, wenn:\n"
|
|
||||||
"\u2022 Sie nur ein JRE (keine JDK) installiert haben\n"
|
|
||||||
"\u2022 Sie Probleme mit dem Worker-Pool haben\n"
|
|
||||||
"\u2022 Sie die Funktion testen m\u00f6chten", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.checkBoxUseSaxonPool.setText(QCoreApplication.translate("Dialog", u"SaxonWorkerPool verwenden (empfohlen)", None))
|
|
||||||
self.labelXsltVersion.setText(QCoreApplication.translate("Dialog", u"XSLT-Version:", None))
|
|
||||||
self.comboBoxXsltVersion.setItemText(0, QCoreApplication.translate("Dialog", u"XSLT 1.0 (JAXP)", None))
|
|
||||||
self.comboBoxXsltVersion.setItemText(1, QCoreApplication.translate("Dialog", u"XSLT 2.0/3.0 (s9api) - Empfohlen", None))
|
|
||||||
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.comboBoxXsltVersion.setToolTip(QCoreApplication.translate("Dialog", u"W\u00e4hlen Sie die XSLT-Version f\u00fcr Saxon-Transformationen:\n"
|
|
||||||
"\n"
|
|
||||||
"XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API\n"
|
|
||||||
"\u2022 Nur f\u00fcr XSLT 1.0 vollst\u00e4ndig spezifiziert\n"
|
|
||||||
"\u2022 Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben f\u00fchren\n"
|
|
||||||
"\n"
|
|
||||||
"XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api\n"
|
|
||||||
"\u2022 Vollst\u00e4ndige Unterst\u00fctzung f\u00fcr XSLT 2.0 und 3.0\n"
|
|
||||||
"\u2022 Empfohlen f\u00fcr moderne XSLT-Stylesheets", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.labelSaxonPoolInfo.setText(QCoreApplication.translate("Dialog", u"<i>Hinweis: SaxonWorkerPool ben\u00f6tigt ein JDK (Java Development Kit).<br>Mit JRE allein werden Transformationen im Fallback-Modus ausgef\u00fchrt.</i>", None))
|
|
||||||
self.groupBoxFopPool.setTitle(QCoreApplication.translate("Dialog", u"FopWorkerPool Einstellungen", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.checkBoxUseFopPool.setToolTip(QCoreApplication.translate("Dialog", u"Aktiviert persistente JVM-Prozesse f\u00fcr Apache FOP PDF-Generierung.\n"
|
|
||||||
"Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead\n"
|
|
||||||
"Nachteile: Ben\u00f6tigt JDK (javac) - funktioniert nicht mit JRE allein\n"
|
|
||||||
"\n"
|
|
||||||
"Deaktivieren Sie diese Option, wenn:\n"
|
|
||||||
"\u2022 Sie nur ein JRE (keine JDK) installiert haben\n"
|
|
||||||
"\u2022 Sie Probleme mit dem Worker-Pool haben\n"
|
|
||||||
"\u2022 Sie die Funktion testen m\u00f6chten", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.checkBoxUseFopPool.setText(QCoreApplication.translate("Dialog", u"FopWorkerPool verwenden (empfohlen)", None))
|
|
||||||
self.labelFopPoolInfo.setText(QCoreApplication.translate("Dialog", u"<i>Hinweis: FopWorkerPool ben\u00f6tigt ein JDK (Java Development Kit).<br>Mit JRE allein werden PDFs im Fallback-Modus generiert.</i>", None))
|
|
||||||
self.label.setText(QCoreApplication.translate("Dialog", u"<html><head/><body><p><span style=\" font-weight:700; font-style:italic;\">Hinweis: \u00c4nderungen in diesem Dialog sind unter Umst\u00e4nden erst nach neu start der Anwendung wirksam.</span></p></body></html>", None))
|
|
||||||
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPerformance), QCoreApplication.translate("Dialog", u"Performance", None))
|
|
||||||
# retranslateUi
|
|
||||||
|
|
||||||
|
|||||||
+83
-107
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1263</width>
|
<width>1263</width>
|
||||||
<height>779</height>
|
<height>774</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -61,26 +61,6 @@
|
|||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="projectPath">
|
|
||||||
<property name="styleSheet">
|
|
||||||
<string notr="true">QLabel { padding: 5px; font-weight: bold; }</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Kein Projekt geladen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="searchEdit">
|
|
||||||
<property name="placeholderText">
|
|
||||||
<string>Knoten oder XSL-Datei filtern...</string>
|
|
||||||
</property>
|
|
||||||
<property name="clearButtonEnabled">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTreeWidget" name="treeWidget">
|
<widget class="QTreeWidget" name="treeWidget">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@@ -89,18 +69,8 @@
|
|||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="styleSheet">
|
|
||||||
<string notr="true">QTreeWidget::item {
|
|
||||||
padding: 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidget::item:selected {
|
|
||||||
background-color: palette(highlight);
|
|
||||||
color: palette(highlighted-text);
|
|
||||||
}</string>
|
|
||||||
</property>
|
|
||||||
<property name="columnCount">
|
<property name="columnCount">
|
||||||
<number>2</number>
|
<number>3</number>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="headerHighlightSections">
|
<attribute name="headerHighlightSections">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -118,6 +88,81 @@ QTreeWidget::item:selected {
|
|||||||
<string notr="true">2</string>
|
<string notr="true">2</string>
|
||||||
</property>
|
</property>
|
||||||
</column>
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string notr="true">3</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QFrame" name="frame_2">
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Shadow::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButton">
|
||||||
|
<property name="layoutDirection">
|
||||||
|
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>nur geänderte generieren</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::MediaPlaybackStart"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButton_2">
|
||||||
|
<property name="autoFillBackground">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Alle generieren</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::MediaSeekForward"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pB_lade_aus_fn2">
|
||||||
|
<property name="text">
|
||||||
|
<string>lade aus FN2</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::GoDown"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
@@ -144,7 +189,7 @@ QTreeWidget::item:selected {
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>68</width>
|
<width>68</width>
|
||||||
<height>733</height>
|
<height>728</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
@@ -233,26 +278,14 @@ QTreeWidget::item:selected {
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="view_ref_pdf">
|
<widget class="QLabel" name="label_6">
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Vorher (Referenz)</string>
|
<string>Vorher (Referenz)</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSlider" name="alpha">
|
<widget class="QSlider" name="alpha">
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Blendet zwischen Referenz-PDF (links) und neuer PDF (rechts) um. Doppelklick setzt auf Mitte zurück.</string>
|
|
||||||
</property>
|
|
||||||
<property name="minimum">
|
<property name="minimum">
|
||||||
<number>-100</number>
|
<number>-100</number>
|
||||||
</property>
|
</property>
|
||||||
@@ -265,16 +298,10 @@ QTreeWidget::item:selected {
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="view_new_pdf">
|
<widget class="QLabel" name="label_7">
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Nachher (Neu)</string>
|
<string>Nachher (Neu)</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@@ -299,12 +326,6 @@ QTreeWidget::item:selected {
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSlider" name="zoom">
|
<widget class="QSlider" name="zoom">
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Vergrößert oder verkleinert die PDF-Ansicht (25% bis 300%). Doppelklick setzt auf 100% zurück.</string>
|
|
||||||
</property>
|
|
||||||
<property name="minimum">
|
<property name="minimum">
|
||||||
<number>25</number>
|
<number>25</number>
|
||||||
</property>
|
</property>
|
||||||
@@ -338,10 +359,7 @@ QTreeWidget::item:selected {
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Änderungen übernehmen</string>
|
<string>✅ Änderungen übernehmen</string>
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="emblem-default"/>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -363,9 +381,6 @@ QTreeWidget::item:selected {
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QScrollArea" name="scrollArea_2">
|
<widget class="QScrollArea" name="scrollArea_2">
|
||||||
<property name="enabled">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::Shape::NoFrame</enum>
|
<enum>QFrame::Shape::NoFrame</enum>
|
||||||
</property>
|
</property>
|
||||||
@@ -380,8 +395,8 @@ QTreeWidget::item:selected {
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>892</width>
|
<width>726</width>
|
||||||
<height>702</height>
|
<height>697</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
@@ -433,20 +448,7 @@ QTreeWidget::item:selected {
|
|||||||
<string>Thema</string>
|
<string>Thema</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuAktion">
|
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="title">
|
|
||||||
<string>Aktion</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionAlle_XML_Dateien_transformieren"/>
|
|
||||||
<addaction name="actionAlle_XML_Dateien_neu_transformieren_force"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionAus_Datenbank_laden"/>
|
|
||||||
</widget>
|
|
||||||
<addaction name="menuProjekt"/>
|
<addaction name="menuProjekt"/>
|
||||||
<addaction name="menuAktion"/>
|
|
||||||
<addaction name="menuThema"/>
|
<addaction name="menuThema"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
@@ -484,36 +486,10 @@ QTreeWidget::item:selected {
|
|||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="folder-open"/>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Vorhandene Projekte</string>
|
<string>Vorhandene Projekte</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAlle_XML_Dateien_transformieren">
|
|
||||||
<property name="text">
|
|
||||||
<string>Alle XML-Dateien transformieren</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionAlle_XML_Dateien_neu_transformieren_force">
|
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="QIcon::ThemeIcon::ViewRefresh"/>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Alle XML-Dateien neu transformieren (force)</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionFN2">
|
|
||||||
<property name="text">
|
|
||||||
<string>FN2</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionAus_Datenbank_laden">
|
|
||||||
<property name="text">
|
|
||||||
<string>Aus Datenbank laden</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
+56
-79
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'MainWinddow.ui'
|
## Form generated from reading UI file 'MainWinddow.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
## Created by: Qt User Interface Compiler version 6.9.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -17,10 +17,10 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
|||||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
QTransform)
|
QTransform)
|
||||||
from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView,
|
from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView,
|
||||||
QLabel, QLineEdit, QMainWindow, QMenu,
|
QLabel, QMainWindow, QMenu, QMenuBar,
|
||||||
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
QPushButton, QScrollArea, QSizePolicy, QSlider,
|
||||||
QSlider, QSpacerItem, QSplitter, QStatusBar,
|
QSpacerItem, QSplitter, QStatusBar, QTreeWidget,
|
||||||
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
|
QTreeWidgetItem, QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_MainWindow(object):
|
class Ui_MainWindow(object):
|
||||||
def setupUi(self, MainWindow):
|
def setupUi(self, MainWindow):
|
||||||
@@ -42,18 +42,6 @@ class Ui_MainWindow(object):
|
|||||||
self.actionVorhandene_Projekte = QAction(MainWindow)
|
self.actionVorhandene_Projekte = QAction(MainWindow)
|
||||||
self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte")
|
self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte")
|
||||||
self.actionVorhandene_Projekte.setEnabled(False)
|
self.actionVorhandene_Projekte.setEnabled(False)
|
||||||
icon3 = QIcon(QIcon.fromTheme(u"folder-open"))
|
|
||||||
self.actionVorhandene_Projekte.setIcon(icon3)
|
|
||||||
self.actionAlle_XML_Dateien_transformieren = QAction(MainWindow)
|
|
||||||
self.actionAlle_XML_Dateien_transformieren.setObjectName(u"actionAlle_XML_Dateien_transformieren")
|
|
||||||
self.actionAlle_XML_Dateien_neu_transformieren_force = QAction(MainWindow)
|
|
||||||
self.actionAlle_XML_Dateien_neu_transformieren_force.setObjectName(u"actionAlle_XML_Dateien_neu_transformieren_force")
|
|
||||||
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ViewRefresh))
|
|
||||||
self.actionAlle_XML_Dateien_neu_transformieren_force.setIcon(icon4)
|
|
||||||
self.actionFN2 = QAction(MainWindow)
|
|
||||||
self.actionFN2.setObjectName(u"actionFN2")
|
|
||||||
self.actionAus_Datenbank_laden = QAction(MainWindow)
|
|
||||||
self.actionAus_Datenbank_laden.setObjectName(u"actionAus_Datenbank_laden")
|
|
||||||
self.centralwidget = QWidget(MainWindow)
|
self.centralwidget = QWidget(MainWindow)
|
||||||
self.centralwidget.setObjectName(u"centralwidget")
|
self.centralwidget.setObjectName(u"centralwidget")
|
||||||
self.horizontalLayout = QHBoxLayout(self.centralwidget)
|
self.horizontalLayout = QHBoxLayout(self.centralwidget)
|
||||||
@@ -77,20 +65,9 @@ class Ui_MainWindow(object):
|
|||||||
self.verticalLayout = QVBoxLayout(self.frame)
|
self.verticalLayout = QVBoxLayout(self.frame)
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
|
self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
|
||||||
self.projectPath = QLabel(self.frame)
|
|
||||||
self.projectPath.setObjectName(u"projectPath")
|
|
||||||
self.projectPath.setStyleSheet(u"QLabel { padding: 5px; font-weight: bold; }")
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.projectPath)
|
|
||||||
|
|
||||||
self.searchEdit = QLineEdit(self.frame)
|
|
||||||
self.searchEdit.setObjectName(u"searchEdit")
|
|
||||||
self.searchEdit.setClearButtonEnabled(True)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.searchEdit)
|
|
||||||
|
|
||||||
self.treeWidget = QTreeWidget(self.frame)
|
self.treeWidget = QTreeWidget(self.frame)
|
||||||
__qtreewidgetitem = QTreeWidgetItem()
|
__qtreewidgetitem = QTreeWidgetItem()
|
||||||
|
__qtreewidgetitem.setText(2, u"3");
|
||||||
__qtreewidgetitem.setText(1, u"2");
|
__qtreewidgetitem.setText(1, u"2");
|
||||||
__qtreewidgetitem.setText(0, u"1");
|
__qtreewidgetitem.setText(0, u"1");
|
||||||
self.treeWidget.setHeaderItem(__qtreewidgetitem)
|
self.treeWidget.setHeaderItem(__qtreewidgetitem)
|
||||||
@@ -100,20 +77,48 @@ class Ui_MainWindow(object):
|
|||||||
sizePolicy1.setVerticalStretch(0)
|
sizePolicy1.setVerticalStretch(0)
|
||||||
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
|
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
|
||||||
self.treeWidget.setSizePolicy(sizePolicy1)
|
self.treeWidget.setSizePolicy(sizePolicy1)
|
||||||
self.treeWidget.setStyleSheet(u"QTreeWidget::item {\n"
|
self.treeWidget.setColumnCount(3)
|
||||||
" padding: 4px 4px;\n"
|
|
||||||
"}\n"
|
|
||||||
"\n"
|
|
||||||
"QTreeWidget::item:selected {\n"
|
|
||||||
" background-color: palette(highlight);\n"
|
|
||||||
" color: palette(highlighted-text);\n"
|
|
||||||
"}")
|
|
||||||
self.treeWidget.setColumnCount(2)
|
|
||||||
self.treeWidget.header().setHighlightSections(True)
|
self.treeWidget.header().setHighlightSections(True)
|
||||||
self.treeWidget.header().setStretchLastSection(True)
|
self.treeWidget.header().setStretchLastSection(True)
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.treeWidget)
|
self.verticalLayout.addWidget(self.treeWidget)
|
||||||
|
|
||||||
|
self.frame_2 = QFrame(self.frame)
|
||||||
|
self.frame_2.setObjectName(u"frame_2")
|
||||||
|
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
|
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
|
||||||
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||||
|
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.pushButton = QPushButton(self.frame_2)
|
||||||
|
self.pushButton.setObjectName(u"pushButton")
|
||||||
|
self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
|
||||||
|
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
|
||||||
|
self.pushButton.setIcon(icon3)
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.pushButton)
|
||||||
|
|
||||||
|
self.pushButton_2 = QPushButton(self.frame_2)
|
||||||
|
self.pushButton_2.setObjectName(u"pushButton_2")
|
||||||
|
self.pushButton_2.setAutoFillBackground(False)
|
||||||
|
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward))
|
||||||
|
self.pushButton_2.setIcon(icon4)
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.pushButton_2)
|
||||||
|
|
||||||
|
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addItem(self.horizontalSpacer)
|
||||||
|
|
||||||
|
self.pB_lade_aus_fn2 = QPushButton(self.frame_2)
|
||||||
|
self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2")
|
||||||
|
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown))
|
||||||
|
self.pB_lade_aus_fn2.setIcon(icon5)
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2)
|
||||||
|
|
||||||
|
|
||||||
|
self.verticalLayout.addWidget(self.frame_2)
|
||||||
|
|
||||||
self.splitter.addWidget(self.frame)
|
self.splitter.addWidget(self.frame)
|
||||||
self.scrollArea = QScrollArea(self.splitter)
|
self.scrollArea = QScrollArea(self.splitter)
|
||||||
self.scrollArea.setObjectName(u"scrollArea")
|
self.scrollArea.setObjectName(u"scrollArea")
|
||||||
@@ -164,29 +169,23 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.horizontalLayout_3.addItem(self.horizontalSpacer_4)
|
self.horizontalLayout_3.addItem(self.horizontalSpacer_4)
|
||||||
|
|
||||||
self.view_ref_pdf = QPushButton(self.frame_4)
|
self.label_6 = QLabel(self.frame_4)
|
||||||
self.view_ref_pdf.setObjectName(u"view_ref_pdf")
|
self.label_6.setObjectName(u"label_6")
|
||||||
self.view_ref_pdf.setEnabled(False)
|
|
||||||
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen))
|
|
||||||
self.view_ref_pdf.setIcon(icon5)
|
|
||||||
|
|
||||||
self.horizontalLayout_3.addWidget(self.view_ref_pdf)
|
self.horizontalLayout_3.addWidget(self.label_6)
|
||||||
|
|
||||||
self.alpha = QSlider(self.frame_4)
|
self.alpha = QSlider(self.frame_4)
|
||||||
self.alpha.setObjectName(u"alpha")
|
self.alpha.setObjectName(u"alpha")
|
||||||
self.alpha.setEnabled(False)
|
|
||||||
self.alpha.setMinimum(-100)
|
self.alpha.setMinimum(-100)
|
||||||
self.alpha.setMaximum(100)
|
self.alpha.setMaximum(100)
|
||||||
self.alpha.setOrientation(Qt.Orientation.Horizontal)
|
self.alpha.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
|
||||||
self.horizontalLayout_3.addWidget(self.alpha)
|
self.horizontalLayout_3.addWidget(self.alpha)
|
||||||
|
|
||||||
self.view_new_pdf = QPushButton(self.frame_4)
|
self.label_7 = QLabel(self.frame_4)
|
||||||
self.view_new_pdf.setObjectName(u"view_new_pdf")
|
self.label_7.setObjectName(u"label_7")
|
||||||
self.view_new_pdf.setEnabled(False)
|
|
||||||
self.view_new_pdf.setIcon(icon5)
|
|
||||||
|
|
||||||
self.horizontalLayout_3.addWidget(self.view_new_pdf)
|
self.horizontalLayout_3.addWidget(self.label_7)
|
||||||
|
|
||||||
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
||||||
@@ -199,7 +198,6 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.zoom = QSlider(self.frame_4)
|
self.zoom = QSlider(self.frame_4)
|
||||||
self.zoom.setObjectName(u"zoom")
|
self.zoom.setObjectName(u"zoom")
|
||||||
self.zoom.setEnabled(False)
|
|
||||||
self.zoom.setMinimum(25)
|
self.zoom.setMinimum(25)
|
||||||
self.zoom.setMaximum(300)
|
self.zoom.setMaximum(300)
|
||||||
self.zoom.setValue(100)
|
self.zoom.setValue(100)
|
||||||
@@ -214,8 +212,6 @@ class Ui_MainWindow(object):
|
|||||||
self.accept_changes = QPushButton(self.frame_4)
|
self.accept_changes = QPushButton(self.frame_4)
|
||||||
self.accept_changes.setObjectName(u"accept_changes")
|
self.accept_changes.setObjectName(u"accept_changes")
|
||||||
self.accept_changes.setEnabled(False)
|
self.accept_changes.setEnabled(False)
|
||||||
icon6 = QIcon(QIcon.fromTheme(u"emblem-default"))
|
|
||||||
self.accept_changes.setIcon(icon6)
|
|
||||||
|
|
||||||
self.horizontalLayout_3.addWidget(self.accept_changes)
|
self.horizontalLayout_3.addWidget(self.accept_changes)
|
||||||
|
|
||||||
@@ -228,13 +224,12 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
self.scrollArea_2 = QScrollArea(self.frame_3)
|
self.scrollArea_2 = QScrollArea(self.frame_3)
|
||||||
self.scrollArea_2.setObjectName(u"scrollArea_2")
|
self.scrollArea_2.setObjectName(u"scrollArea_2")
|
||||||
self.scrollArea_2.setEnabled(True)
|
|
||||||
self.scrollArea_2.setFrameShape(QFrame.Shape.NoFrame)
|
self.scrollArea_2.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
self.scrollArea_2.setFrameShadow(QFrame.Shadow.Raised)
|
self.scrollArea_2.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.scrollArea_2.setWidgetResizable(True)
|
self.scrollArea_2.setWidgetResizable(True)
|
||||||
self.scrollAreaWidgetContents_2 = QWidget()
|
self.scrollAreaWidgetContents_2 = QWidget()
|
||||||
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
|
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
|
||||||
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 880, 697))
|
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 726, 697))
|
||||||
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
|
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
|
||||||
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
|
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
|
||||||
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
|
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -254,16 +249,12 @@ class Ui_MainWindow(object):
|
|||||||
self.menuProjekt.setObjectName(u"menuProjekt")
|
self.menuProjekt.setObjectName(u"menuProjekt")
|
||||||
self.menuThema = QMenu(self.menubar)
|
self.menuThema = QMenu(self.menubar)
|
||||||
self.menuThema.setObjectName(u"menuThema")
|
self.menuThema.setObjectName(u"menuThema")
|
||||||
self.menuAktion = QMenu(self.menubar)
|
|
||||||
self.menuAktion.setObjectName(u"menuAktion")
|
|
||||||
self.menuAktion.setEnabled(False)
|
|
||||||
MainWindow.setMenuBar(self.menubar)
|
MainWindow.setMenuBar(self.menubar)
|
||||||
self.statusbar = QStatusBar(MainWindow)
|
self.statusbar = QStatusBar(MainWindow)
|
||||||
self.statusbar.setObjectName(u"statusbar")
|
self.statusbar.setObjectName(u"statusbar")
|
||||||
MainWindow.setStatusBar(self.statusbar)
|
MainWindow.setStatusBar(self.statusbar)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuProjekt.menuAction())
|
self.menubar.addAction(self.menuProjekt.menuAction())
|
||||||
self.menubar.addAction(self.menuAktion.menuAction())
|
|
||||||
self.menubar.addAction(self.menuThema.menuAction())
|
self.menubar.addAction(self.menuThema.menuAction())
|
||||||
self.menuProjekt.addAction(self.actionNeu)
|
self.menuProjekt.addAction(self.actionNeu)
|
||||||
self.menuProjekt.addSeparator()
|
self.menuProjekt.addSeparator()
|
||||||
@@ -272,10 +263,6 @@ class Ui_MainWindow(object):
|
|||||||
self.menuProjekt.addAction(self.actionEinstellungen)
|
self.menuProjekt.addAction(self.actionEinstellungen)
|
||||||
self.menuProjekt.addSeparator()
|
self.menuProjekt.addSeparator()
|
||||||
self.menuProjekt.addAction(self.actionBeenden)
|
self.menuProjekt.addAction(self.actionBeenden)
|
||||||
self.menuAktion.addAction(self.actionAlle_XML_Dateien_transformieren)
|
|
||||||
self.menuAktion.addAction(self.actionAlle_XML_Dateien_neu_transformieren_force)
|
|
||||||
self.menuAktion.addSeparator()
|
|
||||||
self.menuAktion.addAction(self.actionAus_Datenbank_laden)
|
|
||||||
|
|
||||||
self.retranslateUi(MainWindow)
|
self.retranslateUi(MainWindow)
|
||||||
self.actionBeenden.triggered.connect(MainWindow.close)
|
self.actionBeenden.triggered.connect(MainWindow.close)
|
||||||
@@ -295,26 +282,16 @@ class Ui_MainWindow(object):
|
|||||||
self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
|
self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
|
||||||
#endif // QT_CONFIG(shortcut)
|
#endif // QT_CONFIG(shortcut)
|
||||||
self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None))
|
self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None))
|
||||||
self.actionAlle_XML_Dateien_transformieren.setText(QCoreApplication.translate("MainWindow", u"Alle XML-Dateien transformieren", None))
|
self.pushButton.setText(QCoreApplication.translate("MainWindow", u"nur ge\u00e4nderte generieren", None))
|
||||||
self.actionAlle_XML_Dateien_neu_transformieren_force.setText(QCoreApplication.translate("MainWindow", u"Alle XML-Dateien neu transformieren (force)", None))
|
self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"Alle generieren", None))
|
||||||
self.actionFN2.setText(QCoreApplication.translate("MainWindow", u"FN2", None))
|
self.pB_lade_aus_fn2.setText(QCoreApplication.translate("MainWindow", u"lade aus FN2", None))
|
||||||
self.actionAus_Datenbank_laden.setText(QCoreApplication.translate("MainWindow", u"Aus Datenbank laden", None))
|
|
||||||
self.projectPath.setText(QCoreApplication.translate("MainWindow", u"Kein Projekt geladen", None))
|
|
||||||
self.searchEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Knoten oder XSL-Datei filtern...", None))
|
|
||||||
self.label.setText("")
|
self.label.setText("")
|
||||||
self.label_2.setText("")
|
self.label_2.setText("")
|
||||||
self.view_ref_pdf.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None))
|
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None))
|
||||||
#if QT_CONFIG(tooltip)
|
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None))
|
||||||
self.alpha.setToolTip(QCoreApplication.translate("MainWindow", u"Blendet zwischen Referenz-PDF (links) und neuer PDF (rechts) um. Doppelklick setzt auf Mitte zur\u00fcck.", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.view_new_pdf.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None))
|
|
||||||
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None))
|
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None))
|
||||||
#if QT_CONFIG(tooltip)
|
self.accept_changes.setText(QCoreApplication.translate("MainWindow", u"\u2705 \u00c4nderungen \u00fcbernehmen", None))
|
||||||
self.zoom.setToolTip(QCoreApplication.translate("MainWindow", u"Vergr\u00f6\u00dfert oder verkleinert die PDF-Ansicht (25% bis 300%). Doppelklick setzt auf 100% zur\u00fcck.", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.accept_changes.setText(QCoreApplication.translate("MainWindow", u"\u00c4nderungen \u00fcbernehmen", None))
|
|
||||||
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None))
|
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None))
|
||||||
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None))
|
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None))
|
||||||
self.menuAktion.setTitle(QCoreApplication.translate("MainWindow", u"Aktion", None))
|
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
+3910
-250
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
|||||||
"""
|
|
||||||
ObsoleteEntriesDialog — Dialog zur Bestätigung des Entfernens veralteter Projekteinträge.
|
|
||||||
|
|
||||||
Zeigt XslFile-Einträge an, die nicht mehr in der Datenbank vorhanden sind,
|
|
||||||
und lässt den Benutzer entscheiden ob sie entfernt werden sollen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QCheckBox,
|
|
||||||
QDialog,
|
|
||||||
QDialogButtonBox,
|
|
||||||
QLabel,
|
|
||||||
QSizePolicy,
|
|
||||||
QTreeWidget,
|
|
||||||
QTreeWidgetItem,
|
|
||||||
QVBoxLayout,
|
|
||||||
)
|
|
||||||
|
|
||||||
from obsolete_detector import ObsoleteGroup
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ObsoleteEntriesDialog(QDialog):
|
|
||||||
"""
|
|
||||||
Dialog zur Anzeige und Bestätigung veralteter Einträge nach einem DB-Import.
|
|
||||||
|
|
||||||
Zeigt die veralteten XslFile-Einträge gruppiert nach ihrer Baumhierarchie an.
|
|
||||||
Der Benutzer kann entscheiden ob die Einträge entfernt und ob nicht mehr
|
|
||||||
verwendete XML-Dateien physisch gelöscht werden sollen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent, obsolete_groups: list[ObsoleteGroup]):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
parent: Eltern-Widget
|
|
||||||
obsolete_groups: Veraltete Einträge gruppiert nach Hierarchiepfad
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
|
||||||
self._obsolete_groups = obsolete_groups
|
|
||||||
self._setup_ui()
|
|
||||||
self._populate_tree()
|
|
||||||
|
|
||||||
def _setup_ui(self) -> None:
|
|
||||||
"""Erstellt die UI-Elemente des Dialogs."""
|
|
||||||
total_count = sum(len(g.xsl_entries) for g in self._obsolete_groups)
|
|
||||||
|
|
||||||
self.setWindowTitle("Veraltete Einträge gefunden")
|
|
||||||
self.resize(640, 420)
|
|
||||||
self.setSizeGripEnabled(True)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setSpacing(10)
|
|
||||||
|
|
||||||
# Erklärungstext
|
|
||||||
info_label = QLabel(
|
|
||||||
f"<b>{total_count} XSL-Datei(en)</b> sind nicht mehr in der Datenbank vorhanden "
|
|
||||||
f"und können aus dem Projekt entfernt werden."
|
|
||||||
)
|
|
||||||
info_label.setWordWrap(True)
|
|
||||||
layout.addWidget(info_label)
|
|
||||||
|
|
||||||
# Baumansicht der veralteten Einträge
|
|
||||||
self._tree = QTreeWidget()
|
|
||||||
self._tree.setColumnCount(3)
|
|
||||||
self._tree.setHeaderLabels(["Bezeichnung", "XSL-Datei", "XML-Dateien"])
|
|
||||||
self._tree.setColumnWidth(0, 280)
|
|
||||||
self._tree.setColumnWidth(1, 200)
|
|
||||||
self._tree.setColumnWidth(2, 80)
|
|
||||||
self._tree.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
||||||
self._tree.setAlternatingRowColors(True)
|
|
||||||
self._tree.setEditTriggers(QTreeWidget.EditTrigger.NoEditTriggers)
|
|
||||||
layout.addWidget(self._tree)
|
|
||||||
|
|
||||||
# Checkbox für physische XML-Löschung
|
|
||||||
self._delete_xml_checkbox = QCheckBox("Nicht mehr verwendete XML-Dateien physisch löschen")
|
|
||||||
self._delete_xml_checkbox.setChecked(False)
|
|
||||||
layout.addWidget(self._delete_xml_checkbox)
|
|
||||||
|
|
||||||
# Dialog-Buttons
|
|
||||||
self._button_box = QDialogButtonBox()
|
|
||||||
remove_button = self._button_box.addButton(
|
|
||||||
"Veraltete Einträge entfernen", QDialogButtonBox.ButtonRole.AcceptRole
|
|
||||||
)
|
|
||||||
remove_button.setToolTip("Entfernt alle aufgelisteten Einträge aus dem Projekt")
|
|
||||||
self._button_box.addButton(QDialogButtonBox.StandardButton.Cancel)
|
|
||||||
self._button_box.accepted.connect(self.accept)
|
|
||||||
self._button_box.rejected.connect(self.reject)
|
|
||||||
layout.addWidget(self._button_box)
|
|
||||||
|
|
||||||
def _populate_tree(self) -> None:
|
|
||||||
"""Befüllt den QTreeWidget mit den veralteten Einträgen."""
|
|
||||||
self._tree.clear()
|
|
||||||
|
|
||||||
for group in self._obsolete_groups:
|
|
||||||
# Hierarchiepfad als verschachtelte Items aufbauen
|
|
||||||
parent_item = self._tree.invisibleRootItem()
|
|
||||||
for path_part in group.node_path:
|
|
||||||
# Prüfe ob dieser Pfadteil bereits als Kind vorhanden ist
|
|
||||||
existing = None
|
|
||||||
for i in range(parent_item.childCount()):
|
|
||||||
child = parent_item.child(i)
|
|
||||||
if child.text(0) == path_part and not child.data(0, Qt.ItemDataRole.UserRole):
|
|
||||||
existing = child
|
|
||||||
break
|
|
||||||
if existing:
|
|
||||||
parent_item = existing
|
|
||||||
else:
|
|
||||||
node_item = QTreeWidgetItem(parent_item, [path_part])
|
|
||||||
font = node_item.font(0)
|
|
||||||
font.setBold(True)
|
|
||||||
node_item.setFont(0, font)
|
|
||||||
parent_item = node_item
|
|
||||||
|
|
||||||
# XslFile-Einträge unter dem Hierarchiepfad
|
|
||||||
for entry in group.xsl_entries:
|
|
||||||
xsl = entry.xsl_file
|
|
||||||
xml_count = str(len(xsl.xmls)) if xsl.xmls else "0"
|
|
||||||
xsl_item = QTreeWidgetItem(
|
|
||||||
parent_item,
|
|
||||||
[xsl.bez, xsl.xsl_file.name, xml_count],
|
|
||||||
)
|
|
||||||
xsl_item.setData(0, Qt.ItemDataRole.UserRole, xsl)
|
|
||||||
xsl_item.setToolTip(1, str(xsl.xsl_file))
|
|
||||||
|
|
||||||
self._tree.expandAll()
|
|
||||||
|
|
||||||
def delete_xml_files(self) -> bool:
|
|
||||||
"""
|
|
||||||
Gibt zurück ob der Benutzer die physische Löschung der XML-Dateien gewünscht hat.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True wenn die Checkbox aktiviert ist
|
|
||||||
"""
|
|
||||||
return self._delete_xml_checkbox.isChecked()
|
|
||||||
+49
-14
@@ -5,7 +5,6 @@ from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox
|
|||||||
|
|
||||||
from conf import app_settings
|
from conf import app_settings
|
||||||
from ui.PdfProject_ui import Ui_projectDlg
|
from ui.PdfProject_ui import Ui_projectDlg
|
||||||
from ui.ProjectXsltParamsDialog import ProjectXsltParamsDialog
|
|
||||||
|
|
||||||
|
|
||||||
class PdfProjectDlg(QDialog):
|
class PdfProjectDlg(QDialog):
|
||||||
@@ -16,7 +15,7 @@ class PdfProjectDlg(QDialog):
|
|||||||
Args:
|
Args:
|
||||||
parent: Übergeordnetes Widget
|
parent: Übergeordnetes Widget
|
||||||
project_data: Bestehende Projektdaten zum Bearbeiten (optional)
|
project_data: Bestehende Projektdaten zum Bearbeiten (optional)
|
||||||
edit_mode: Wenn True, wird der Projekt-Ordner deaktiviert (nur Name und Einstellungen ändern)
|
edit_mode: Wenn True, werden Projekt-Name und -Ordner deaktiviert (nur Einstellungen ändern)
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ class PdfProjectDlg(QDialog):
|
|||||||
# Projektdaten speichern
|
# Projektdaten speichern
|
||||||
self.project_data = project_data or {}
|
self.project_data = project_data or {}
|
||||||
self.edit_mode = edit_mode
|
self.edit_mode = edit_mode
|
||||||
self.xslt_params: dict[str, str] = dict(self.project_data.get("xslt_params", {}))
|
|
||||||
|
|
||||||
# Dialog-Eigenschaften setzen
|
# Dialog-Eigenschaften setzen
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
@@ -55,9 +53,6 @@ class PdfProjectDlg(QDialog):
|
|||||||
# Browse-Button für FOP-Config-Ordner
|
# Browse-Button für FOP-Config-Ordner
|
||||||
self.ui.btnBrowseFopConfig.clicked.connect(self.browse_fop_config_dir)
|
self.ui.btnBrowseFopConfig.clicked.connect(self.browse_fop_config_dir)
|
||||||
|
|
||||||
# XSLT-Parameter bearbeiten
|
|
||||||
self.ui.btnEditXsltParams.clicked.connect(self._edit_xslt_params)
|
|
||||||
|
|
||||||
# OK/Cancel Buttons sind bereits in der UI-Datei verbunden
|
# OK/Cancel Buttons sind bereits in der UI-Datei verbunden
|
||||||
# self.ui.buttonBox.accepted.connect(self.accept)
|
# self.ui.buttonBox.accepted.connect(self.accept)
|
||||||
# self.ui.buttonBox.rejected.connect(self.reject)
|
# self.ui.buttonBox.rejected.connect(self.reject)
|
||||||
@@ -195,12 +190,6 @@ class PdfProjectDlg(QDialog):
|
|||||||
if selected_dir:
|
if selected_dir:
|
||||||
self.ui.lineFopConfigDir.setText(selected_dir)
|
self.ui.lineFopConfigDir.setText(selected_dir)
|
||||||
|
|
||||||
def _edit_xslt_params(self):
|
|
||||||
"""Öffnet den Dialog zur Bearbeitung der projektweiten XSLT-Parameter."""
|
|
||||||
dialog = ProjectXsltParamsDialog(self, self.xslt_params)
|
|
||||||
if dialog.exec() == ProjectXsltParamsDialog.DialogCode.Accepted:
|
|
||||||
self.xslt_params = dialog.get_params()
|
|
||||||
|
|
||||||
def validate_and_accept(self):
|
def validate_and_accept(self):
|
||||||
"""Validiert die Eingaben und akzeptiert den Dialog."""
|
"""Validiert die Eingaben und akzeptiert den Dialog."""
|
||||||
# Projekt-Name prüfen
|
# Projekt-Name prüfen
|
||||||
@@ -280,8 +269,7 @@ class PdfProjectDlg(QDialog):
|
|||||||
'apache_fop_id': self.ui.cB_ApacheFop.currentData(),
|
'apache_fop_id': self.ui.cB_ApacheFop.currentData(),
|
||||||
'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(),
|
'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(),
|
||||||
'postgre_sql_db_id': self.ui.cB_Postgres.currentData(),
|
'postgre_sql_db_id': self.ui.cB_Postgres.currentData(),
|
||||||
'fop_config_dir': fop_config_dir if fop_config_dir else None,
|
'fop_config_dir': fop_config_dir if fop_config_dir else None
|
||||||
'xslt_params': self.xslt_params,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _configure_edit_mode(self):
|
def _configure_edit_mode(self):
|
||||||
@@ -303,3 +291,50 @@ class PdfProjectDlg(QDialog):
|
|||||||
self.project_data = project_data
|
self.project_data = project_data
|
||||||
self._load_project_data()
|
self._load_project_data()
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience-Funktionen für einfache Verwendung
|
||||||
|
def create_project_dialog(parent=None):
|
||||||
|
"""
|
||||||
|
Erstellt einen neuen Projekt-Dialog für ein neues Projekt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PdfProjectDlg: Der Dialog
|
||||||
|
"""
|
||||||
|
return PdfProjectDlg(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_project_dialog(parent=None, project_data=None):
|
||||||
|
"""
|
||||||
|
Erstellt einen Projekt-Dialog zum Bearbeiten eines bestehenden Projekts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
project_data: Bestehende Projektdaten
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PdfProjectDlg: Der Dialog
|
||||||
|
"""
|
||||||
|
return PdfProjectDlg(parent, project_data)
|
||||||
|
|
||||||
|
|
||||||
|
def show_project_dialog(parent=None, project_data=None):
|
||||||
|
"""
|
||||||
|
Zeigt einen Projekt-Dialog an und gibt die Ergebnisse zurück.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
project_data: Bestehende Projektdaten (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (accepted: bool, project_data: dict)
|
||||||
|
"""
|
||||||
|
dialog = PdfProjectDlg(parent, project_data)
|
||||||
|
result = dialog.exec()
|
||||||
|
|
||||||
|
if result == QDialog.DialogCode.Accepted:
|
||||||
|
return True, dialog.get_project_data()
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|||||||
+1
-15
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>608</width>
|
<width>608</width>
|
||||||
<height>375</height>
|
<height>331</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -135,20 +135,6 @@
|
|||||||
<item row="8" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QComboBox" name="cB_Postgres"/>
|
<widget class="QComboBox" name="cB_Postgres"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0">
|
|
||||||
<widget class="QLabel" name="label_10">
|
|
||||||
<property name="text">
|
|
||||||
<string>XSLT-Parameter:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="9" column="1">
|
|
||||||
<widget class="QPushButton" name="btnEditXsltParams">
|
|
||||||
<property name="text">
|
|
||||||
<string>Bearbeiten ...</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QFrame" name="frame_2">
|
<widget class="QFrame" name="frame_2">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
|
|||||||
+187
-199
@@ -1,199 +1,187 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'PdfProject.ui'
|
## Form generated from reading UI file 'PdfProject.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
## Created by: Qt User Interface Compiler version 6.9.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
QMetaObject, QObject, QPoint, QRect,
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
QSize, QTime, QUrl, Qt)
|
QSize, QTime, QUrl, Qt)
|
||||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||||
QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout,
|
QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout,
|
||||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||||
QVBoxLayout, QWidget)
|
QVBoxLayout, QWidget)
|
||||||
|
|
||||||
class Ui_projectDlg(object):
|
class Ui_projectDlg(object):
|
||||||
def setupUi(self, projectDlg):
|
def setupUi(self, projectDlg):
|
||||||
if not projectDlg.objectName():
|
if not projectDlg.objectName():
|
||||||
projectDlg.setObjectName(u"projectDlg")
|
projectDlg.setObjectName(u"projectDlg")
|
||||||
projectDlg.resize(608, 331)
|
projectDlg.resize(608, 331)
|
||||||
self.verticalLayout = QVBoxLayout(projectDlg)
|
self.verticalLayout = QVBoxLayout(projectDlg)
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
self.widget = QWidget(projectDlg)
|
self.widget = QWidget(projectDlg)
|
||||||
self.widget.setObjectName(u"widget")
|
self.widget.setObjectName(u"widget")
|
||||||
self.formLayout = QFormLayout(self.widget)
|
self.formLayout = QFormLayout(self.widget)
|
||||||
self.formLayout.setObjectName(u"formLayout")
|
self.formLayout.setObjectName(u"formLayout")
|
||||||
self.label = QLabel(self.widget)
|
self.label = QLabel(self.widget)
|
||||||
self.label.setObjectName(u"label")
|
self.label.setObjectName(u"label")
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label)
|
||||||
|
|
||||||
self.lineProjectName = QLineEdit(self.widget)
|
self.lineProjectName = QLineEdit(self.widget)
|
||||||
self.lineProjectName.setObjectName(u"lineProjectName")
|
self.lineProjectName.setObjectName(u"lineProjectName")
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.lineProjectName)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.lineProjectName)
|
||||||
|
|
||||||
self.label_2 = QLabel(self.widget)
|
self.label_2 = QLabel(self.widget)
|
||||||
self.label_2.setObjectName(u"label_2")
|
self.label_2.setObjectName(u"label_2")
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2)
|
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2)
|
||||||
|
|
||||||
self.frame = QFrame(self.widget)
|
self.frame = QFrame(self.widget)
|
||||||
self.frame.setObjectName(u"frame")
|
self.frame.setObjectName(u"frame")
|
||||||
self.frame.setFrameShape(QFrame.Shape.StyledPanel)
|
self.frame.setFrameShape(QFrame.Shape.StyledPanel)
|
||||||
self.frame.setFrameShadow(QFrame.Shadow.Raised)
|
self.frame.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
self.horizontalLayout = QHBoxLayout(self.frame)
|
self.horizontalLayout = QHBoxLayout(self.frame)
|
||||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||||
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.lineProjectDir = QLineEdit(self.frame)
|
self.lineProjectDir = QLineEdit(self.frame)
|
||||||
self.lineProjectDir.setObjectName(u"lineProjectDir")
|
self.lineProjectDir.setObjectName(u"lineProjectDir")
|
||||||
|
|
||||||
self.horizontalLayout.addWidget(self.lineProjectDir)
|
self.horizontalLayout.addWidget(self.lineProjectDir)
|
||||||
|
|
||||||
self.pushButton = QPushButton(self.frame)
|
self.pushButton = QPushButton(self.frame)
|
||||||
self.pushButton.setObjectName(u"pushButton")
|
self.pushButton.setObjectName(u"pushButton")
|
||||||
|
|
||||||
self.horizontalLayout.addWidget(self.pushButton)
|
self.horizontalLayout.addWidget(self.pushButton)
|
||||||
|
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.frame)
|
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.frame)
|
||||||
|
|
||||||
self.label_3 = QLabel(self.widget)
|
self.label_3 = QLabel(self.widget)
|
||||||
self.label_3.setObjectName(u"label_3")
|
self.label_3.setObjectName(u"label_3")
|
||||||
|
|
||||||
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.label_3)
|
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.label_3)
|
||||||
|
|
||||||
self.cB_XslDir = QComboBox(self.widget)
|
self.cB_XslDir = QComboBox(self.widget)
|
||||||
self.cB_XslDir.setObjectName(u"cB_XslDir")
|
self.cB_XslDir.setObjectName(u"cB_XslDir")
|
||||||
|
|
||||||
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.cB_XslDir)
|
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.cB_XslDir)
|
||||||
|
|
||||||
self.label_4 = QLabel(self.widget)
|
self.label_4 = QLabel(self.widget)
|
||||||
self.label_4.setObjectName(u"label_4")
|
self.label_4.setObjectName(u"label_4")
|
||||||
|
|
||||||
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.label_4)
|
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.label_4)
|
||||||
|
|
||||||
self.cB_JavaVm = QComboBox(self.widget)
|
self.cB_JavaVm = QComboBox(self.widget)
|
||||||
self.cB_JavaVm.setObjectName(u"cB_JavaVm")
|
self.cB_JavaVm.setObjectName(u"cB_JavaVm")
|
||||||
|
|
||||||
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.cB_JavaVm)
|
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.cB_JavaVm)
|
||||||
|
|
||||||
self.label_5 = QLabel(self.widget)
|
self.label_5 = QLabel(self.widget)
|
||||||
self.label_5.setObjectName(u"label_5")
|
self.label_5.setObjectName(u"label_5")
|
||||||
|
|
||||||
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.label_5)
|
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.label_5)
|
||||||
|
|
||||||
self.cB_SaxonJar = QComboBox(self.widget)
|
self.cB_SaxonJar = QComboBox(self.widget)
|
||||||
self.cB_SaxonJar.setObjectName(u"cB_SaxonJar")
|
self.cB_SaxonJar.setObjectName(u"cB_SaxonJar")
|
||||||
|
|
||||||
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.cB_SaxonJar)
|
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.cB_SaxonJar)
|
||||||
|
|
||||||
self.label_6 = QLabel(self.widget)
|
self.label_6 = QLabel(self.widget)
|
||||||
self.label_6.setObjectName(u"label_6")
|
self.label_6.setObjectName(u"label_6")
|
||||||
|
|
||||||
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.label_6)
|
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.label_6)
|
||||||
|
|
||||||
self.cB_ApacheFop = QComboBox(self.widget)
|
self.cB_ApacheFop = QComboBox(self.widget)
|
||||||
self.cB_ApacheFop.setObjectName(u"cB_ApacheFop")
|
self.cB_ApacheFop.setObjectName(u"cB_ApacheFop")
|
||||||
|
|
||||||
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop)
|
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop)
|
||||||
|
|
||||||
self.label_9 = QLabel(self.widget)
|
self.label_9 = QLabel(self.widget)
|
||||||
self.label_9.setObjectName(u"label_9")
|
self.label_9.setObjectName(u"label_9")
|
||||||
|
|
||||||
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_9)
|
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_9)
|
||||||
|
|
||||||
self.label_7 = QLabel(self.widget)
|
self.label_7 = QLabel(self.widget)
|
||||||
self.label_7.setObjectName(u"label_7")
|
self.label_7.setObjectName(u"label_7")
|
||||||
|
|
||||||
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_7)
|
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_7)
|
||||||
|
|
||||||
self.cB_Diff_Pdf = QComboBox(self.widget)
|
self.cB_Diff_Pdf = QComboBox(self.widget)
|
||||||
self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf")
|
self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf")
|
||||||
|
|
||||||
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
|
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
|
||||||
|
|
||||||
self.label_8 = QLabel(self.widget)
|
self.label_8 = QLabel(self.widget)
|
||||||
self.label_8.setObjectName(u"label_8")
|
self.label_8.setObjectName(u"label_8")
|
||||||
|
|
||||||
self.formLayout.setWidget(8, QFormLayout.ItemRole.LabelRole, self.label_8)
|
self.formLayout.setWidget(8, QFormLayout.ItemRole.LabelRole, self.label_8)
|
||||||
|
|
||||||
self.cB_Postgres = QComboBox(self.widget)
|
self.cB_Postgres = QComboBox(self.widget)
|
||||||
self.cB_Postgres.setObjectName(u"cB_Postgres")
|
self.cB_Postgres.setObjectName(u"cB_Postgres")
|
||||||
|
|
||||||
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
|
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
|
||||||
|
|
||||||
self.label_10 = QLabel(self.widget)
|
self.frame_2 = QFrame(self.widget)
|
||||||
self.label_10.setObjectName(u"label_10")
|
self.frame_2.setObjectName(u"frame_2")
|
||||||
|
self.frame_2.setFrameShape(QFrame.Shape.StyledPanel)
|
||||||
self.formLayout.setWidget(9, QFormLayout.ItemRole.LabelRole, self.label_10)
|
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
|
||||||
|
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
|
||||||
self.btnEditXsltParams = QPushButton(self.widget)
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||||
self.btnEditXsltParams.setObjectName(u"btnEditXsltParams")
|
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.lineFopConfigDir = QLineEdit(self.frame_2)
|
||||||
self.formLayout.setWidget(9, QFormLayout.ItemRole.FieldRole, self.btnEditXsltParams)
|
self.lineFopConfigDir.setObjectName(u"lineFopConfigDir")
|
||||||
|
|
||||||
self.frame_2 = QFrame(self.widget)
|
self.horizontalLayout_2.addWidget(self.lineFopConfigDir)
|
||||||
self.frame_2.setObjectName(u"frame_2")
|
|
||||||
self.frame_2.setFrameShape(QFrame.Shape.StyledPanel)
|
self.btnBrowseFopConfig = QPushButton(self.frame_2)
|
||||||
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
|
self.btnBrowseFopConfig.setObjectName(u"btnBrowseFopConfig")
|
||||||
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
|
|
||||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
self.horizontalLayout_2.addWidget(self.btnBrowseFopConfig)
|
||||||
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.lineFopConfigDir = QLineEdit(self.frame_2)
|
|
||||||
self.lineFopConfigDir.setObjectName(u"lineFopConfigDir")
|
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.frame_2)
|
||||||
|
|
||||||
self.horizontalLayout_2.addWidget(self.lineFopConfigDir)
|
|
||||||
|
self.verticalLayout.addWidget(self.widget)
|
||||||
self.btnBrowseFopConfig = QPushButton(self.frame_2)
|
|
||||||
self.btnBrowseFopConfig.setObjectName(u"btnBrowseFopConfig")
|
self.buttonBox = QDialogButtonBox(projectDlg)
|
||||||
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
self.horizontalLayout_2.addWidget(self.btnBrowseFopConfig)
|
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||||
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
self.buttonBox.setCenterButtons(True)
|
||||||
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.frame_2)
|
|
||||||
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.widget)
|
|
||||||
|
self.retranslateUi(projectDlg)
|
||||||
self.buttonBox = QDialogButtonBox(projectDlg)
|
self.buttonBox.accepted.connect(projectDlg.accept)
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
self.buttonBox.rejected.connect(projectDlg.reject)
|
||||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
QMetaObject.connectSlotsByName(projectDlg)
|
||||||
self.buttonBox.setCenterButtons(True)
|
# setupUi
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
def retranslateUi(self, projectDlg):
|
||||||
|
projectDlg.setWindowTitle(QCoreApplication.translate("projectDlg", u"PDF-Projekt", None))
|
||||||
|
self.label.setText(QCoreApplication.translate("projectDlg", u"Name:", None))
|
||||||
self.retranslateUi(projectDlg)
|
self.label_2.setText(QCoreApplication.translate("projectDlg", u"Projekt-Ordner:", None))
|
||||||
self.buttonBox.accepted.connect(projectDlg.accept)
|
self.pushButton.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
|
||||||
self.buttonBox.rejected.connect(projectDlg.reject)
|
self.label_3.setText(QCoreApplication.translate("projectDlg", u"XSL-Ordner:", None))
|
||||||
|
self.label_4.setText(QCoreApplication.translate("projectDlg", u"Java VM:", None))
|
||||||
QMetaObject.connectSlotsByName(projectDlg)
|
self.label_5.setText(QCoreApplication.translate("projectDlg", u"Saxon Jar:", None))
|
||||||
# setupUi
|
self.label_6.setText(QCoreApplication.translate("projectDlg", u"Apache FOP:", None))
|
||||||
|
self.label_9.setText(QCoreApplication.translate("projectDlg", u"FOP-Config-Ordner:", None))
|
||||||
def retranslateUi(self, projectDlg):
|
self.label_7.setText(QCoreApplication.translate("projectDlg", u"diff-pdf:", None))
|
||||||
projectDlg.setWindowTitle(QCoreApplication.translate("projectDlg", u"PDF-Projekt", None))
|
self.label_8.setText(QCoreApplication.translate("projectDlg", u"Postgres:", None))
|
||||||
self.label.setText(QCoreApplication.translate("projectDlg", u"Name:", None))
|
self.btnBrowseFopConfig.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
|
||||||
self.label_2.setText(QCoreApplication.translate("projectDlg", u"Projekt-Ordner:", None))
|
# retranslateUi
|
||||||
self.pushButton.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
|
|
||||||
self.label_3.setText(QCoreApplication.translate("projectDlg", u"XSL-Ordner:", None))
|
|
||||||
self.label_4.setText(QCoreApplication.translate("projectDlg", u"Java VM:", None))
|
|
||||||
self.label_5.setText(QCoreApplication.translate("projectDlg", u"Saxon Jar:", None))
|
|
||||||
self.label_6.setText(QCoreApplication.translate("projectDlg", u"Apache FOP:", None))
|
|
||||||
self.label_9.setText(QCoreApplication.translate("projectDlg", u"FOP-Config-Ordner:", None))
|
|
||||||
self.label_7.setText(QCoreApplication.translate("projectDlg", u"diff-pdf:", None))
|
|
||||||
self.label_8.setText(QCoreApplication.translate("projectDlg", u"Postgres:", None))
|
|
||||||
self.label_10.setText(QCoreApplication.translate("projectDlg", u"XSLT-Parameter:", None))
|
|
||||||
self.btnEditXsltParams.setText(QCoreApplication.translate("projectDlg", u"Bearbeiten ...", None))
|
|
||||||
self.btnBrowseFopConfig.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
|
|
||||||
# retranslateUi
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,29 +3,24 @@ from PySide6.QtCore import QThread, Signal, Qt
|
|||||||
|
|
||||||
from ui.PostgreSqlConfigDialog_ui import Ui_PostgreSqlConfigDialog
|
from ui.PostgreSqlConfigDialog_ui import Ui_PostgreSqlConfigDialog
|
||||||
|
|
||||||
|
import polars as pl
|
||||||
|
|
||||||
|
|
||||||
class DatabaseTestThread(QThread):
|
class DatabaseTestThread(QThread):
|
||||||
"""Thread für den Datenbankverbindungstest."""
|
"""Thread für den Datenbankverbindungstest."""
|
||||||
|
|
||||||
# Signale für die Kommunikation mit dem UI-Thread
|
# Signale für die Kommunikation mit dem UI-Thread
|
||||||
test_completed = Signal(bool, str) # success, message
|
test_completed = Signal(bool, str) # success, message
|
||||||
|
|
||||||
def __init__(self, connection_data):
|
def __init__(self, connection_data):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.connection_data = connection_data
|
self.connection_data = connection_data
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Führt den Datenbanktest in einem separaten Thread aus."""
|
"""Führt den Datenbanktest in einem separaten Thread aus."""
|
||||||
import polars as pl
|
|
||||||
try:
|
try:
|
||||||
timeout = self.connection_data.get("timeout", 10)
|
uri = f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}@{self.connection_data['host']}:{self.connection_data['port']}/{self.connection_data['database']}"
|
||||||
uri = (
|
|
||||||
f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}"
|
|
||||||
f"@{self.connection_data['host']}:{self.connection_data['port']}"
|
|
||||||
f"/{self.connection_data['database']}"
|
|
||||||
f"?connect_timeout={timeout}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Datenbankverbindung testen
|
# Datenbankverbindung testen
|
||||||
r = pl.read_database_uri(
|
r = pl.read_database_uri(
|
||||||
query="SELECT 1",
|
query="SELECT 1",
|
||||||
@@ -111,7 +106,6 @@ class PostgreSqlConfigDialog(QDialog):
|
|||||||
self.ui.usernameEdit.setText(data.get("username", ""))
|
self.ui.usernameEdit.setText(data.get("username", ""))
|
||||||
self.ui.passwordEdit.setText(data.get("password", ""))
|
self.ui.passwordEdit.setText(data.get("password", ""))
|
||||||
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
|
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
|
||||||
self.ui.timeoutSpinBox.setValue(data.get("timeout", 10))
|
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
"""Gibt die eingegebenen Daten zurück."""
|
"""Gibt die eingegebenen Daten zurück."""
|
||||||
@@ -132,5 +126,4 @@ class PostgreSqlConfigDialog(QDialog):
|
|||||||
"username": self.ui.usernameEdit.text().strip(),
|
"username": self.ui.usernameEdit.text().strip(),
|
||||||
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein
|
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein
|
||||||
"ssl_mode": self.ui.sslModeComboBox.currentText(),
|
"ssl_mode": self.ui.sslModeComboBox.currentText(),
|
||||||
"timeout": self.ui.timeoutSpinBox.value(),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,27 +138,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0">
|
|
||||||
<widget class="QLabel" name="timeoutLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>Timeout (s):</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="QSpinBox" name="timeoutSpinBox">
|
|
||||||
<property name="minimum">
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
|
||||||
<number>300</number>
|
|
||||||
</property>
|
|
||||||
<property name="value">
|
|
||||||
<number>10</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="1">
|
|
||||||
<widget class="QPushButton" name="testConnectionButton">
|
<widget class="QPushButton" name="testConnectionButton">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Verbindung testen</string>
|
<string>Verbindung testen</string>
|
||||||
|
|||||||
+156
-170
@@ -1,170 +1,156 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
|
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
## Created by: Qt User Interface Compiler version 6.9.1
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
QMetaObject, QObject, QPoint, QRect,
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
QSize, QTime, QUrl, Qt)
|
QSize, QTime, QUrl, Qt)
|
||||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
QFont, QFontDatabase, QGradient, QIcon,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||||
QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
|
QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
|
||||||
QPushButton, QSizePolicy, QSpinBox, QVBoxLayout,
|
QPushButton, QSizePolicy, QSpinBox, QVBoxLayout,
|
||||||
QWidget)
|
QWidget)
|
||||||
|
|
||||||
class Ui_PostgreSqlConfigDialog(object):
|
class Ui_PostgreSqlConfigDialog(object):
|
||||||
def setupUi(self, PostgreSqlConfigDialog):
|
def setupUi(self, PostgreSqlConfigDialog):
|
||||||
if not PostgreSqlConfigDialog.objectName():
|
if not PostgreSqlConfigDialog.objectName():
|
||||||
PostgreSqlConfigDialog.setObjectName(u"PostgreSqlConfigDialog")
|
PostgreSqlConfigDialog.setObjectName(u"PostgreSqlConfigDialog")
|
||||||
PostgreSqlConfigDialog.resize(397, 268)
|
PostgreSqlConfigDialog.resize(397, 268)
|
||||||
PostgreSqlConfigDialog.setModal(True)
|
PostgreSqlConfigDialog.setModal(True)
|
||||||
self.verticalLayout = QVBoxLayout(PostgreSqlConfigDialog)
|
self.verticalLayout = QVBoxLayout(PostgreSqlConfigDialog)
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
self.formLayout = QFormLayout()
|
self.formLayout = QFormLayout()
|
||||||
self.formLayout.setObjectName(u"formLayout")
|
self.formLayout.setObjectName(u"formLayout")
|
||||||
self.nameLabel = QLabel(PostgreSqlConfigDialog)
|
self.nameLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.nameLabel.setObjectName(u"nameLabel")
|
self.nameLabel.setObjectName(u"nameLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||||
|
|
||||||
self.nameEdit = QLineEdit(PostgreSqlConfigDialog)
|
self.nameEdit = QLineEdit(PostgreSqlConfigDialog)
|
||||||
self.nameEdit.setObjectName(u"nameEdit")
|
self.nameEdit.setObjectName(u"nameEdit")
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||||
|
|
||||||
self.hostLabel = QLabel(PostgreSqlConfigDialog)
|
self.hostLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.hostLabel.setObjectName(u"hostLabel")
|
self.hostLabel.setObjectName(u"hostLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.hostLabel)
|
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.hostLabel)
|
||||||
|
|
||||||
self.hostEdit = QLineEdit(PostgreSqlConfigDialog)
|
self.hostEdit = QLineEdit(PostgreSqlConfigDialog)
|
||||||
self.hostEdit.setObjectName(u"hostEdit")
|
self.hostEdit.setObjectName(u"hostEdit")
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.hostEdit)
|
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.hostEdit)
|
||||||
|
|
||||||
self.portLabel = QLabel(PostgreSqlConfigDialog)
|
self.portLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.portLabel.setObjectName(u"portLabel")
|
self.portLabel.setObjectName(u"portLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.portLabel)
|
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.portLabel)
|
||||||
|
|
||||||
self.portSpinBox = QSpinBox(PostgreSqlConfigDialog)
|
self.portSpinBox = QSpinBox(PostgreSqlConfigDialog)
|
||||||
self.portSpinBox.setObjectName(u"portSpinBox")
|
self.portSpinBox.setObjectName(u"portSpinBox")
|
||||||
self.portSpinBox.setMinimum(1)
|
self.portSpinBox.setMinimum(1)
|
||||||
self.portSpinBox.setMaximum(65535)
|
self.portSpinBox.setMaximum(65535)
|
||||||
self.portSpinBox.setValue(5432)
|
self.portSpinBox.setValue(5432)
|
||||||
|
|
||||||
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.portSpinBox)
|
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.portSpinBox)
|
||||||
|
|
||||||
self.databaseLabel = QLabel(PostgreSqlConfigDialog)
|
self.databaseLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.databaseLabel.setObjectName(u"databaseLabel")
|
self.databaseLabel.setObjectName(u"databaseLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.databaseLabel)
|
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.databaseLabel)
|
||||||
|
|
||||||
self.databaseEdit = QLineEdit(PostgreSqlConfigDialog)
|
self.databaseEdit = QLineEdit(PostgreSqlConfigDialog)
|
||||||
self.databaseEdit.setObjectName(u"databaseEdit")
|
self.databaseEdit.setObjectName(u"databaseEdit")
|
||||||
|
|
||||||
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.databaseEdit)
|
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.databaseEdit)
|
||||||
|
|
||||||
self.usernameLabel = QLabel(PostgreSqlConfigDialog)
|
self.usernameLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.usernameLabel.setObjectName(u"usernameLabel")
|
self.usernameLabel.setObjectName(u"usernameLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.usernameLabel)
|
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.usernameLabel)
|
||||||
|
|
||||||
self.usernameEdit = QLineEdit(PostgreSqlConfigDialog)
|
self.usernameEdit = QLineEdit(PostgreSqlConfigDialog)
|
||||||
self.usernameEdit.setObjectName(u"usernameEdit")
|
self.usernameEdit.setObjectName(u"usernameEdit")
|
||||||
|
|
||||||
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.usernameEdit)
|
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.usernameEdit)
|
||||||
|
|
||||||
self.passwordLabel = QLabel(PostgreSqlConfigDialog)
|
self.passwordLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.passwordLabel.setObjectName(u"passwordLabel")
|
self.passwordLabel.setObjectName(u"passwordLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.passwordLabel)
|
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.passwordLabel)
|
||||||
|
|
||||||
self.passwordEdit = QLineEdit(PostgreSqlConfigDialog)
|
self.passwordEdit = QLineEdit(PostgreSqlConfigDialog)
|
||||||
self.passwordEdit.setObjectName(u"passwordEdit")
|
self.passwordEdit.setObjectName(u"passwordEdit")
|
||||||
self.passwordEdit.setEchoMode(QLineEdit.EchoMode.Password)
|
self.passwordEdit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
|
||||||
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.passwordEdit)
|
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.passwordEdit)
|
||||||
|
|
||||||
self.sslModeLabel = QLabel(PostgreSqlConfigDialog)
|
self.sslModeLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
self.sslModeLabel.setObjectName(u"sslModeLabel")
|
self.sslModeLabel.setObjectName(u"sslModeLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.sslModeLabel)
|
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.sslModeLabel)
|
||||||
|
|
||||||
self.sslModeComboBox = QComboBox(PostgreSqlConfigDialog)
|
self.sslModeComboBox = QComboBox(PostgreSqlConfigDialog)
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.addItem("")
|
self.sslModeComboBox.addItem("")
|
||||||
self.sslModeComboBox.setObjectName(u"sslModeComboBox")
|
self.sslModeComboBox.setObjectName(u"sslModeComboBox")
|
||||||
|
|
||||||
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
|
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
|
||||||
|
|
||||||
self.timeoutLabel = QLabel(PostgreSqlConfigDialog)
|
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
|
||||||
self.timeoutLabel.setObjectName(u"timeoutLabel")
|
self.testConnectionButton.setObjectName(u"testConnectionButton")
|
||||||
|
|
||||||
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.timeoutLabel)
|
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
|
||||||
|
|
||||||
self.timeoutSpinBox = QSpinBox(PostgreSqlConfigDialog)
|
|
||||||
self.timeoutSpinBox.setObjectName(u"timeoutSpinBox")
|
self.verticalLayout.addLayout(self.formLayout)
|
||||||
self.timeoutSpinBox.setMinimum(1)
|
|
||||||
self.timeoutSpinBox.setMaximum(300)
|
self.buttonBox = QDialogButtonBox(PostgreSqlConfigDialog)
|
||||||
self.timeoutSpinBox.setValue(10)
|
self.buttonBox.setObjectName(u"buttonBox")
|
||||||
|
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
||||||
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.timeoutSpinBox)
|
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
||||||
|
self.buttonBox.setCenterButtons(True)
|
||||||
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
|
|
||||||
self.testConnectionButton.setObjectName(u"testConnectionButton")
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
|
|
||||||
|
self.retranslateUi(PostgreSqlConfigDialog)
|
||||||
|
self.buttonBox.accepted.connect(PostgreSqlConfigDialog.accept)
|
||||||
self.verticalLayout.addLayout(self.formLayout)
|
self.buttonBox.rejected.connect(PostgreSqlConfigDialog.reject)
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(PostgreSqlConfigDialog)
|
QMetaObject.connectSlotsByName(PostgreSqlConfigDialog)
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
# setupUi
|
||||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
def retranslateUi(self, PostgreSqlConfigDialog):
|
||||||
self.buttonBox.setCenterButtons(True)
|
PostgreSqlConfigDialog.setWindowTitle(QCoreApplication.translate("PostgreSqlConfigDialog", u"PostgreSQL Datenbank Konfiguration", None))
|
||||||
|
self.nameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Name:", None))
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
self.hostLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Host:", None))
|
||||||
|
self.hostEdit.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"localhost", None))
|
||||||
|
self.portLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Port:", None))
|
||||||
self.retranslateUi(PostgreSqlConfigDialog)
|
self.databaseLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Datenbank:", None))
|
||||||
self.buttonBox.accepted.connect(PostgreSqlConfigDialog.accept)
|
self.usernameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Benutzername:", None))
|
||||||
self.buttonBox.rejected.connect(PostgreSqlConfigDialog.reject)
|
self.passwordLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Passwort:", None))
|
||||||
|
self.sslModeLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"SSL-Modus:", None))
|
||||||
QMetaObject.connectSlotsByName(PostgreSqlConfigDialog)
|
self.sslModeComboBox.setItemText(0, QCoreApplication.translate("PostgreSqlConfigDialog", u"disable", None))
|
||||||
# setupUi
|
self.sslModeComboBox.setItemText(1, QCoreApplication.translate("PostgreSqlConfigDialog", u"allow", None))
|
||||||
|
self.sslModeComboBox.setItemText(2, QCoreApplication.translate("PostgreSqlConfigDialog", u"prefer", None))
|
||||||
def retranslateUi(self, PostgreSqlConfigDialog):
|
self.sslModeComboBox.setItemText(3, QCoreApplication.translate("PostgreSqlConfigDialog", u"require", None))
|
||||||
PostgreSqlConfigDialog.setWindowTitle(QCoreApplication.translate("PostgreSqlConfigDialog", u"PostgreSQL Datenbank Konfiguration", None))
|
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
|
||||||
self.nameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Name:", None))
|
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
|
||||||
self.hostLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Host:", None))
|
|
||||||
self.hostEdit.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"localhost", None))
|
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
|
||||||
self.portLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Port:", None))
|
# retranslateUi
|
||||||
self.databaseLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Datenbank:", None))
|
|
||||||
self.usernameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Benutzername:", None))
|
|
||||||
self.passwordLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Passwort:", None))
|
|
||||||
self.sslModeLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"SSL-Modus:", None))
|
|
||||||
self.sslModeComboBox.setItemText(0, QCoreApplication.translate("PostgreSqlConfigDialog", u"disable", None))
|
|
||||||
self.sslModeComboBox.setItemText(1, QCoreApplication.translate("PostgreSqlConfigDialog", u"allow", None))
|
|
||||||
self.sslModeComboBox.setItemText(2, QCoreApplication.translate("PostgreSqlConfigDialog", u"prefer", None))
|
|
||||||
self.sslModeComboBox.setItemText(3, QCoreApplication.translate("PostgreSqlConfigDialog", u"require", None))
|
|
||||||
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
|
|
||||||
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
|
|
||||||
|
|
||||||
self.timeoutLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Timeout (s):", None))
|
|
||||||
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
|
|
||||||
# retranslateUi
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
from PySide6.QtWidgets import QDialog, QTableWidgetItem
|
|
||||||
from ui.ProjectXsltParamsDialog_ui import Ui_ProjectXsltParamsDialog
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectXsltParamsDialog(QDialog):
|
|
||||||
"""Dialog zur Bearbeitung projektweiter XSLT-Parameter."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None, xslt_params: dict[str, str] | None = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.ui = Ui_ProjectXsltParamsDialog()
|
|
||||||
self.ui.setupUi(self)
|
|
||||||
|
|
||||||
self.ui.addParamButton.clicked.connect(self._add_parameter)
|
|
||||||
self.ui.removeParamButton.clicked.connect(self._remove_parameter)
|
|
||||||
|
|
||||||
self._setup_table()
|
|
||||||
|
|
||||||
if xslt_params:
|
|
||||||
self._load_params(xslt_params)
|
|
||||||
|
|
||||||
def _setup_table(self):
|
|
||||||
"""Konfiguriert die Tabelle."""
|
|
||||||
self.ui.xsltParamsTable.setColumnWidth(0, 200)
|
|
||||||
self.ui.xsltParamsTable.setColumnWidth(1, 300)
|
|
||||||
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
|
|
||||||
|
|
||||||
def _load_params(self, params: dict[str, str]):
|
|
||||||
"""Lädt die XSLT-Parameter in die Tabelle."""
|
|
||||||
self.ui.xsltParamsTable.setRowCount(len(params))
|
|
||||||
for row, (key, value) in enumerate(params.items()):
|
|
||||||
self.ui.xsltParamsTable.setItem(row, 0, QTableWidgetItem(str(key)))
|
|
||||||
self.ui.xsltParamsTable.setItem(row, 1, QTableWidgetItem(str(value)))
|
|
||||||
|
|
||||||
def _add_parameter(self):
|
|
||||||
"""Fügt einen neuen Parameter hinzu."""
|
|
||||||
row_count = self.ui.xsltParamsTable.rowCount()
|
|
||||||
self.ui.xsltParamsTable.insertRow(row_count)
|
|
||||||
self.ui.xsltParamsTable.setItem(row_count, 0, QTableWidgetItem(""))
|
|
||||||
self.ui.xsltParamsTable.setItem(row_count, 1, QTableWidgetItem(""))
|
|
||||||
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
|
|
||||||
|
|
||||||
def _remove_parameter(self):
|
|
||||||
"""Entfernt den ausgewählten Parameter."""
|
|
||||||
current_row = self.ui.xsltParamsTable.currentRow()
|
|
||||||
if current_row >= 0:
|
|
||||||
self.ui.xsltParamsTable.removeRow(current_row)
|
|
||||||
|
|
||||||
def get_params(self) -> dict[str, str]:
|
|
||||||
"""
|
|
||||||
Gibt die bearbeiteten XSLT-Parameter zurück.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict[str, str]: Dictionary mit allen XSLT-Parametern
|
|
||||||
"""
|
|
||||||
params = {}
|
|
||||||
for row in range(self.ui.xsltParamsTable.rowCount()):
|
|
||||||
key_item = self.ui.xsltParamsTable.item(row, 0)
|
|
||||||
value_item = self.ui.xsltParamsTable.item(row, 1)
|
|
||||||
if key_item and value_item:
|
|
||||||
key = key_item.text().strip()
|
|
||||||
if key:
|
|
||||||
params[key] = value_item.text().strip()
|
|
||||||
return params
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>ProjectXsltParamsDialog</class>
|
|
||||||
<widget class="QDialog" name="ProjectXsltParamsDialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>600</width>
|
|
||||||
<height>400</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Projektweite XSLT-Parameter</string>
|
|
||||||
</property>
|
|
||||||
<property name="modal">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="xsltParamsGroupBox">
|
|
||||||
<property name="title">
|
|
||||||
<string>XSLT-Parameter</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="xsltParamsLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<widget class="QTableWidget" name="xsltParamsTable">
|
|
||||||
<property name="frameShape">
|
|
||||||
<enum>QFrame::Shape::NoFrame</enum>
|
|
||||||
</property>
|
|
||||||
<property name="columnCount">
|
|
||||||
<number>2</number>
|
|
||||||
</property>
|
|
||||||
<attribute name="horizontalHeaderVisible">
|
|
||||||
<bool>true</bool>
|
|
||||||
</attribute>
|
|
||||||
<column>
|
|
||||||
<property name="text">
|
|
||||||
<string>Parameter</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
<column>
|
|
||||||
<property name="text">
|
|
||||||
<string>Wert</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_left">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="addParamButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>Parameter hinzufügen</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="QIcon::ThemeIcon::ListAdd"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="removeParamButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>Parameter entfernen</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="QIcon::ThemeIcon::ListRemove"/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="horizontalSpacer_right">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
|
|
||||||
</property>
|
|
||||||
<property name="centerButtons">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>ProjectXsltParamsDialog</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>248</x>
|
|
||||||
<y>254</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>ProjectXsltParamsDialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>316</x>
|
|
||||||
<y>260</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>286</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
## Form generated from reading UI file 'ProjectXsltParamsDialog.ui'
|
|
||||||
##
|
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
|
||||||
##
|
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
|
||||||
QMetaObject, QObject, QPoint, QRect,
|
|
||||||
QSize, QTime, QUrl, Qt)
|
|
||||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|
||||||
QFont, QFontDatabase, QGradient, QIcon,
|
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
|
|
||||||
QFrame, QGroupBox, QHBoxLayout, QHeaderView,
|
|
||||||
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
|
|
||||||
QTableWidgetItem, QVBoxLayout, QWidget)
|
|
||||||
|
|
||||||
class Ui_ProjectXsltParamsDialog(object):
|
|
||||||
def setupUi(self, ProjectXsltParamsDialog):
|
|
||||||
if not ProjectXsltParamsDialog.objectName():
|
|
||||||
ProjectXsltParamsDialog.setObjectName(u"ProjectXsltParamsDialog")
|
|
||||||
ProjectXsltParamsDialog.resize(600, 400)
|
|
||||||
ProjectXsltParamsDialog.setModal(True)
|
|
||||||
self.verticalLayout = QVBoxLayout(ProjectXsltParamsDialog)
|
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
|
||||||
self.xsltParamsGroupBox = QGroupBox(ProjectXsltParamsDialog)
|
|
||||||
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
|
|
||||||
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
|
|
||||||
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
|
|
||||||
self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
|
|
||||||
if (self.xsltParamsTable.columnCount() < 2):
|
|
||||||
self.xsltParamsTable.setColumnCount(2)
|
|
||||||
__qtablewidgetitem = QTableWidgetItem()
|
|
||||||
self.xsltParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
|
||||||
__qtablewidgetitem1 = QTableWidgetItem()
|
|
||||||
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
|
||||||
self.xsltParamsTable.setObjectName(u"xsltParamsTable")
|
|
||||||
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
|
|
||||||
self.xsltParamsTable.setColumnCount(2)
|
|
||||||
self.xsltParamsTable.horizontalHeader().setVisible(True)
|
|
||||||
|
|
||||||
self.xsltParamsLayout.addWidget(self.xsltParamsTable)
|
|
||||||
|
|
||||||
self.xsltParamsButtonLayout = QHBoxLayout()
|
|
||||||
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
|
|
||||||
self.horizontalSpacer_left = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
||||||
|
|
||||||
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_left)
|
|
||||||
|
|
||||||
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
|
|
||||||
self.addParamButton.setObjectName(u"addParamButton")
|
|
||||||
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
|
|
||||||
self.addParamButton.setIcon(icon)
|
|
||||||
|
|
||||||
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
|
|
||||||
|
|
||||||
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
|
|
||||||
self.removeParamButton.setObjectName(u"removeParamButton")
|
|
||||||
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
|
|
||||||
self.removeParamButton.setIcon(icon1)
|
|
||||||
|
|
||||||
self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
|
|
||||||
|
|
||||||
self.horizontalSpacer_right = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
||||||
|
|
||||||
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_right)
|
|
||||||
|
|
||||||
|
|
||||||
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
|
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
|
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(ProjectXsltParamsDialog)
|
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
|
||||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
|
|
||||||
self.buttonBox.setCenterButtons(True)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
|
||||||
|
|
||||||
|
|
||||||
self.retranslateUi(ProjectXsltParamsDialog)
|
|
||||||
self.buttonBox.accepted.connect(ProjectXsltParamsDialog.accept)
|
|
||||||
self.buttonBox.rejected.connect(ProjectXsltParamsDialog.reject)
|
|
||||||
|
|
||||||
QMetaObject.connectSlotsByName(ProjectXsltParamsDialog)
|
|
||||||
# setupUi
|
|
||||||
|
|
||||||
def retranslateUi(self, ProjectXsltParamsDialog):
|
|
||||||
ProjectXsltParamsDialog.setWindowTitle(QCoreApplication.translate("ProjectXsltParamsDialog", u"Projektweite XSLT-Parameter", None))
|
|
||||||
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("ProjectXsltParamsDialog", u"XSLT-Parameter", None))
|
|
||||||
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
|
|
||||||
___qtablewidgetitem.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter", None));
|
|
||||||
___qtablewidgetitem1 = self.xsltParamsTable.horizontalHeaderItem(1)
|
|
||||||
___qtablewidgetitem1.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Wert", None));
|
|
||||||
self.addParamButton.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter hinzuf\u00fcgen", None))
|
|
||||||
self.removeParamButton.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter entfernen", None))
|
|
||||||
# retranslateUi
|
|
||||||
|
|
||||||
@@ -1,9 +1,161 @@
|
|||||||
|
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog
|
from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog
|
||||||
from ui.XsltParamsEditDialog import XsltParamsEditDialog
|
|
||||||
|
|
||||||
|
|
||||||
class TreeNodeEditDialog(XsltParamsEditDialog):
|
class TreeNodeEditDialog(QDialog):
|
||||||
"""Dialog zur Bearbeitung von TreeNode-Objekten."""
|
"""Dialog zur Bearbeitung von TreeNode-Objekten."""
|
||||||
|
|
||||||
def _create_ui(self):
|
def __init__(self, parent=None, node=None, parent_params=None):
|
||||||
return Ui_TreeNodeEditDialog()
|
"""
|
||||||
|
Initialisiert den Dialog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
node: TreeNode-Objekt zum Bearbeiten
|
||||||
|
parent_params: Dictionary mit Eltern-Parametern (nur anzeigen)
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# UI einrichten
|
||||||
|
self.ui = Ui_TreeNodeEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
# Node-Objekt speichern
|
||||||
|
self.node = node
|
||||||
|
self.parent_params = parent_params or {}
|
||||||
|
|
||||||
|
# Signale verbinden
|
||||||
|
self.ui.addParamButton.clicked.connect(self.add_parameter)
|
||||||
|
self.ui.removeParamButton.clicked.connect(self.remove_parameter)
|
||||||
|
|
||||||
|
# Tabellen konfigurieren
|
||||||
|
self._setup_tables()
|
||||||
|
|
||||||
|
# Daten laden
|
||||||
|
if self.node:
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
def _setup_tables(self):
|
||||||
|
"""Konfiguriert die Tabellen."""
|
||||||
|
# XSLT Parameter Tabelle
|
||||||
|
self.ui.xsltParamsTable.setColumnWidth(0, 200)
|
||||||
|
self.ui.xsltParamsTable.setColumnWidth(1, 300)
|
||||||
|
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
|
# Eltern-Parameter Tabelle
|
||||||
|
self.ui.parentParamsTable.setColumnWidth(0, 200)
|
||||||
|
self.ui.parentParamsTable.setColumnWidth(1, 300)
|
||||||
|
self.ui.parentParamsTable.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
"""Lädt die Daten des TreeNode in den Dialog."""
|
||||||
|
if not self.node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Bezeichnung setzen
|
||||||
|
self.ui.bezEdit.setText(str(self.node.bez) if self.node.bez else "")
|
||||||
|
|
||||||
|
# XSLT Parameter laden
|
||||||
|
self._load_xslt_params()
|
||||||
|
|
||||||
|
# Eltern-Parameter laden
|
||||||
|
self._load_parent_params()
|
||||||
|
|
||||||
|
def _load_xslt_params(self):
|
||||||
|
"""Lädt die XSLT Parameter in die Tabelle."""
|
||||||
|
if not self.node or not self.node.xslt_params:
|
||||||
|
return
|
||||||
|
|
||||||
|
params = self.node.xslt_params
|
||||||
|
self.ui.xsltParamsTable.setRowCount(len(params))
|
||||||
|
|
||||||
|
for row, (key, value) in enumerate(params.items()):
|
||||||
|
# Parameter-Name
|
||||||
|
key_item = QTableWidgetItem(str(key))
|
||||||
|
self.ui.xsltParamsTable.setItem(row, 0, key_item)
|
||||||
|
|
||||||
|
# Parameter-Wert
|
||||||
|
value_item = QTableWidgetItem(str(value))
|
||||||
|
self.ui.xsltParamsTable.setItem(row, 1, value_item)
|
||||||
|
|
||||||
|
def _load_parent_params(self):
|
||||||
|
"""Lädt die Eltern-Parameter in die Tabelle (nur anzeigen)."""
|
||||||
|
if not self.parent_params:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ui.parentParamsTable.setRowCount(len(self.parent_params))
|
||||||
|
|
||||||
|
for row, (key, value) in enumerate(self.parent_params.items()):
|
||||||
|
# Parameter-Name
|
||||||
|
key_item = QTableWidgetItem(str(key))
|
||||||
|
key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
|
self.ui.parentParamsTable.setItem(row, 0, key_item)
|
||||||
|
|
||||||
|
# Parameter-Wert
|
||||||
|
value_item = QTableWidgetItem(str(value))
|
||||||
|
value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
|
self.ui.parentParamsTable.setItem(row, 1, value_item)
|
||||||
|
|
||||||
|
def add_parameter(self):
|
||||||
|
"""Fügt einen neuen Parameter hinzu."""
|
||||||
|
row_count = self.ui.xsltParamsTable.rowCount()
|
||||||
|
self.ui.xsltParamsTable.insertRow(row_count)
|
||||||
|
|
||||||
|
# Leere Items hinzufügen
|
||||||
|
key_item = QTableWidgetItem("")
|
||||||
|
value_item = QTableWidgetItem("")
|
||||||
|
|
||||||
|
self.ui.xsltParamsTable.setItem(row_count, 0, key_item)
|
||||||
|
self.ui.xsltParamsTable.setItem(row_count, 1, value_item)
|
||||||
|
|
||||||
|
# Fokus auf den neuen Parameter setzen
|
||||||
|
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
|
||||||
|
|
||||||
|
def remove_parameter(self):
|
||||||
|
"""Entfernt den ausgewählten Parameter."""
|
||||||
|
current_row = self.ui.xsltParamsTable.currentRow()
|
||||||
|
if current_row >= 0:
|
||||||
|
self.ui.xsltParamsTable.removeRow(current_row)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
"""
|
||||||
|
Gibt die bearbeiteten Daten zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary mit den bearbeiteten Daten oder None bei Fehler
|
||||||
|
"""
|
||||||
|
# Bezeichnung prüfen
|
||||||
|
bez = self.ui.bezEdit.text().strip()
|
||||||
|
if not bez:
|
||||||
|
QMessageBox.warning(self, "Warnung", "Bitte geben Sie eine Bezeichnung ein.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# XSLT Parameter sammeln
|
||||||
|
xslt_params = {}
|
||||||
|
for row in range(self.ui.xsltParamsTable.rowCount()):
|
||||||
|
key_item = self.ui.xsltParamsTable.item(row, 0)
|
||||||
|
value_item = self.ui.xsltParamsTable.item(row, 1)
|
||||||
|
|
||||||
|
if key_item and value_item:
|
||||||
|
key = key_item.text().strip()
|
||||||
|
value = value_item.text().strip()
|
||||||
|
|
||||||
|
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
|
||||||
|
xslt_params[key] = value
|
||||||
|
|
||||||
|
# CheckBox für Force-Transformation prüfen
|
||||||
|
force_transform = self.ui.alle_xml_transformieren.isChecked()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bez": bez,
|
||||||
|
"xslt_params": xslt_params,
|
||||||
|
"force_transform": force_transform
|
||||||
|
}
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Überschreibt accept() um Datenvalidierung durchzuführen."""
|
||||||
|
data = self.get_data()
|
||||||
|
if data is not None:
|
||||||
|
super().accept()
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
"""
|
|
||||||
Worker Pool Metriken-Dialog.
|
|
||||||
|
|
||||||
Zeigt Performance- und Ressourcenverbrauch-Metriken für Saxon- und FOP-Worker-Pools an.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QDialog,
|
|
||||||
QVBoxLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QGroupBox,
|
|
||||||
QLabel,
|
|
||||||
QPushButton,
|
|
||||||
QTabWidget,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerPoolMetricsDialog(QDialog):
|
|
||||||
"""
|
|
||||||
Dialog zur Anzeige von Worker-Pool-Metriken.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
"""
|
|
||||||
Initialisiert den Metriken-Dialog.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: Eltern-Widget
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setWindowTitle("Worker Pool Performance-Metriken")
|
|
||||||
self.resize(800, 600)
|
|
||||||
self._setup_ui()
|
|
||||||
self._load_metrics()
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
|
||||||
"""Erstellt die UI-Elemente."""
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
|
|
||||||
# Tab-Widget für Saxon und FOP
|
|
||||||
self.tab_widget = QTabWidget()
|
|
||||||
layout.addWidget(self.tab_widget)
|
|
||||||
|
|
||||||
# Saxon-Tab
|
|
||||||
self.saxon_tab = self._create_metrics_tab("Saxon Worker Pool")
|
|
||||||
self.tab_widget.addTab(self.saxon_tab, "Saxon (XSLT)")
|
|
||||||
|
|
||||||
# FOP-Tab
|
|
||||||
self.fop_tab = self._create_metrics_tab("FOP Worker Pool")
|
|
||||||
self.tab_widget.addTab(self.fop_tab, "FOP (PDF)")
|
|
||||||
|
|
||||||
# Schließen-Button
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
button_layout.addStretch()
|
|
||||||
close_button = QPushButton("Schließen")
|
|
||||||
close_button.clicked.connect(self.accept)
|
|
||||||
button_layout.addWidget(close_button)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
def _create_metrics_tab(self, title: str) -> QWidget:
|
|
||||||
"""
|
|
||||||
Erstellt ein Tab-Widget für Metriken.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
title: Titel der Metrik-Gruppe
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
QWidget mit Metriken
|
|
||||||
"""
|
|
||||||
tab = QWidget()
|
|
||||||
layout = QVBoxLayout(tab)
|
|
||||||
|
|
||||||
# Status-Label
|
|
||||||
status_label = QLabel("Status: <i>Nicht initialisiert</i>")
|
|
||||||
status_label.setObjectName("status_label")
|
|
||||||
layout.addWidget(status_label)
|
|
||||||
|
|
||||||
# Kompilierungs-Metriken
|
|
||||||
compile_group = QGroupBox("Kompilierung")
|
|
||||||
compile_layout = QVBoxLayout(compile_group)
|
|
||||||
|
|
||||||
compile_time_label = QLabel("Kompilierungszeit: -")
|
|
||||||
compile_time_label.setObjectName("compile_time_label")
|
|
||||||
compile_layout.addWidget(compile_time_label)
|
|
||||||
|
|
||||||
layout.addWidget(compile_group)
|
|
||||||
|
|
||||||
# Worker-Start-Metriken
|
|
||||||
worker_start_group = QGroupBox("Worker-Start")
|
|
||||||
worker_start_layout = QVBoxLayout(worker_start_group)
|
|
||||||
|
|
||||||
worker_count_label = QLabel("Anzahl Worker: -")
|
|
||||||
worker_count_label.setObjectName("worker_count_label")
|
|
||||||
worker_start_layout.addWidget(worker_count_label)
|
|
||||||
|
|
||||||
total_start_time_label = QLabel("Summe Startzeit: -")
|
|
||||||
total_start_time_label.setObjectName("total_start_time_label")
|
|
||||||
worker_start_layout.addWidget(total_start_time_label)
|
|
||||||
|
|
||||||
avg_start_time_label = QLabel("Durchschnitt Startzeit: -")
|
|
||||||
avg_start_time_label.setObjectName("avg_start_time_label")
|
|
||||||
worker_start_layout.addWidget(avg_start_time_label)
|
|
||||||
|
|
||||||
layout.addWidget(worker_start_group)
|
|
||||||
|
|
||||||
# RAM-Metriken
|
|
||||||
ram_group = QGroupBox("Arbeitsspeicher (RAM)")
|
|
||||||
ram_layout = QVBoxLayout(ram_group)
|
|
||||||
|
|
||||||
ram_before_label = QLabel("RAM vor Transformation: -")
|
|
||||||
ram_before_label.setObjectName("ram_before_label")
|
|
||||||
ram_layout.addWidget(ram_before_label)
|
|
||||||
|
|
||||||
ram_before_avg_label = QLabel("RAM vor Transformation (Durchschnitt/Worker): -")
|
|
||||||
ram_before_avg_label.setObjectName("ram_before_avg_label")
|
|
||||||
ram_layout.addWidget(ram_before_avg_label)
|
|
||||||
|
|
||||||
ram_after_label = QLabel("RAM nach Transformation: -")
|
|
||||||
ram_after_label.setObjectName("ram_after_label")
|
|
||||||
ram_layout.addWidget(ram_after_label)
|
|
||||||
|
|
||||||
ram_after_avg_label = QLabel("RAM nach Transformation (Durchschnitt/Worker): -")
|
|
||||||
ram_after_avg_label.setObjectName("ram_after_avg_label")
|
|
||||||
ram_layout.addWidget(ram_after_avg_label)
|
|
||||||
|
|
||||||
ram_delta_label = QLabel("RAM-Zunahme: -")
|
|
||||||
ram_delta_label.setObjectName("ram_delta_label")
|
|
||||||
ram_layout.addWidget(ram_delta_label)
|
|
||||||
|
|
||||||
layout.addWidget(ram_group)
|
|
||||||
|
|
||||||
layout.addStretch()
|
|
||||||
|
|
||||||
return tab
|
|
||||||
|
|
||||||
def _load_metrics(self):
|
|
||||||
"""Lädt und zeigt die Metriken an."""
|
|
||||||
# Hole MainWindow-Instanz (parent)
|
|
||||||
main_window = self.parent()
|
|
||||||
|
|
||||||
# Saxon Worker Pool - verwende gespeicherte Metriken
|
|
||||||
if hasattr(main_window, "last_saxon_metrics") and main_window.last_saxon_metrics:
|
|
||||||
self._update_metrics_tab_from_metrics(
|
|
||||||
self.saxon_tab, main_window.last_saxon_metrics, "Saxon Worker Pool"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._set_tab_status(
|
|
||||||
self.saxon_tab, "<i>Keine Metriken verfügbar - bitte erst eine Transformation durchführen</i>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# FOP Worker Pool - verwende gespeicherte Metriken
|
|
||||||
if hasattr(main_window, "last_fop_metrics") and main_window.last_fop_metrics:
|
|
||||||
self._update_metrics_tab_from_metrics(self.fop_tab, main_window.last_fop_metrics, "FOP Worker Pool")
|
|
||||||
else:
|
|
||||||
self._set_tab_status(
|
|
||||||
self.fop_tab, "<i>Keine Metriken verfügbar - bitte erst eine Transformation durchführen</i>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _set_tab_status(self, tab: QWidget, status: str):
|
|
||||||
"""
|
|
||||||
Setzt den Status-Text eines Tabs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tab: Das Tab-Widget
|
|
||||||
status: Der Status-Text
|
|
||||||
"""
|
|
||||||
status_label = tab.findChild(QLabel, "status_label")
|
|
||||||
if status_label:
|
|
||||||
status_label.setText(f"Status: {status}")
|
|
||||||
|
|
||||||
def _update_metrics_tab_from_metrics(self, tab: QWidget, metrics, pool_name: str):
|
|
||||||
"""
|
|
||||||
Aktualisiert die Metriken in einem Tab (direkt aus Metriken-Objekt).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tab: Das Tab-Widget
|
|
||||||
metrics: Das WorkerPoolMetrics-Objekt
|
|
||||||
pool_name: Name des Pools
|
|
||||||
"""
|
|
||||||
# Status - berechne Worker-Anzahl aus Metriken
|
|
||||||
num_workers = len(metrics.worker_start_times) if metrics.worker_start_times else 0
|
|
||||||
self._set_tab_status(tab, f"<b>Letzte Transformation</b> ({num_workers} Worker)")
|
|
||||||
|
|
||||||
# Kompilierung
|
|
||||||
compile_time_label = tab.findChild(QLabel, "compile_time_label")
|
|
||||||
if compile_time_label:
|
|
||||||
compile_time_label.setText(f"Kompilierungszeit: <b>{metrics.compilation_time_seconds:.3f} s</b>")
|
|
||||||
|
|
||||||
# Worker-Start
|
|
||||||
worker_count_label = tab.findChild(QLabel, "worker_count_label")
|
|
||||||
if worker_count_label:
|
|
||||||
worker_count_label.setText(f"Anzahl Worker: <b>{len(metrics.worker_start_times)}</b>")
|
|
||||||
|
|
||||||
total_start_time_label = tab.findChild(QLabel, "total_start_time_label")
|
|
||||||
if total_start_time_label:
|
|
||||||
total_start_time_label.setText(
|
|
||||||
f"Summe Startzeit: <b>{metrics.total_worker_start_time_seconds:.3f} s</b>"
|
|
||||||
)
|
|
||||||
|
|
||||||
avg_start_time_label = tab.findChild(QLabel, "avg_start_time_label")
|
|
||||||
if avg_start_time_label:
|
|
||||||
avg_start_time_label.setText(
|
|
||||||
f"Durchschnitt Startzeit: <b>{metrics.average_worker_start_time_seconds:.3f} s/Worker</b>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# RAM
|
|
||||||
ram_before_label = tab.findChild(QLabel, "ram_before_label")
|
|
||||||
if ram_before_label:
|
|
||||||
if metrics.total_ram_before_mb > 0:
|
|
||||||
ram_before_label.setText(f"RAM vor Transformation: <b>{metrics.total_ram_before_mb:.1f} MB</b>")
|
|
||||||
else:
|
|
||||||
ram_before_label.setText("RAM vor Transformation: <i>Noch nicht gemessen</i>")
|
|
||||||
|
|
||||||
ram_before_avg_label = tab.findChild(QLabel, "ram_before_avg_label")
|
|
||||||
if ram_before_avg_label:
|
|
||||||
if metrics.average_ram_before_mb > 0:
|
|
||||||
ram_before_avg_label.setText(
|
|
||||||
f"RAM vor Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_before_mb:.1f} MB</b>"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ram_before_avg_label.setText(
|
|
||||||
"RAM vor Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
|
|
||||||
)
|
|
||||||
|
|
||||||
ram_after_label = tab.findChild(QLabel, "ram_after_label")
|
|
||||||
if ram_after_label:
|
|
||||||
if metrics.total_ram_after_mb > 0:
|
|
||||||
ram_after_label.setText(f"RAM nach Transformation: <b>{metrics.total_ram_after_mb:.1f} MB</b>")
|
|
||||||
else:
|
|
||||||
ram_after_label.setText("RAM nach Transformation: <i>Noch nicht gemessen</i>")
|
|
||||||
|
|
||||||
ram_after_avg_label = tab.findChild(QLabel, "ram_after_avg_label")
|
|
||||||
if ram_after_avg_label:
|
|
||||||
if metrics.average_ram_after_mb > 0:
|
|
||||||
ram_after_avg_label.setText(
|
|
||||||
f"RAM nach Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_after_mb:.1f} MB</b>"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ram_after_avg_label.setText(
|
|
||||||
"RAM nach Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
|
|
||||||
)
|
|
||||||
|
|
||||||
ram_delta_label = tab.findChild(QLabel, "ram_delta_label")
|
|
||||||
if ram_delta_label:
|
|
||||||
if metrics.total_ram_before_mb > 0 and metrics.total_ram_after_mb > 0:
|
|
||||||
delta = metrics.total_ram_after_mb - metrics.total_ram_before_mb
|
|
||||||
delta_percent = (delta / metrics.total_ram_before_mb * 100) if metrics.total_ram_before_mb > 0 else 0
|
|
||||||
ram_delta_label.setText(f"RAM-Zunahme: <b>{delta:.1f} MB ({delta_percent:+.1f}%)</b>")
|
|
||||||
else:
|
|
||||||
ram_delta_label.setText("RAM-Zunahme: <i>Noch nicht gemessen</i>")
|
|
||||||
@@ -13,23 +13,14 @@ logger = logging.getLogger(__name__)
|
|||||||
class XmlToXslAssignDialog(QDialog):
|
class XmlToXslAssignDialog(QDialog):
|
||||||
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
|
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, parent=None, xml_file_path=None, project_nodes=None):
|
||||||
self,
|
|
||||||
parent=None,
|
|
||||||
xml_file_path=None,
|
|
||||||
project_nodes=None,
|
|
||||||
preselected_xsl_ids: set | None = None,
|
|
||||||
edit_mode: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Initialisiert den Dialog.
|
Initialisiert den Dialog.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent: Übergeordnetes Widget
|
parent: Übergeordnetes Widget
|
||||||
xml_file_path: Pfad zur XML-Datei
|
xml_file_path: Pfad zur XML-Datei
|
||||||
project_nodes: Liste der Projekt-Knoten
|
project_nodes: Liste der Projekt-Knoten
|
||||||
preselected_xsl_ids: Set von XslFile-IDs (tuple), deren Checkbox initial angehakt sein soll
|
|
||||||
edit_mode: True = Bearbeiten-Modus (Zuordnungen ändern), False = Neu-Zuordnen
|
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@@ -40,23 +31,9 @@ class XmlToXslAssignDialog(QDialog):
|
|||||||
# Parameter speichern
|
# Parameter speichern
|
||||||
self.xml_file_path = Path(xml_file_path) if xml_file_path else None
|
self.xml_file_path = Path(xml_file_path) if xml_file_path else None
|
||||||
self.project_nodes = project_nodes or []
|
self.project_nodes = project_nodes or []
|
||||||
self.preselected_xsl_ids: set = set(preselected_xsl_ids) if preselected_xsl_ids else set()
|
|
||||||
self.edit_mode = edit_mode
|
|
||||||
|
|
||||||
# Dictionary zum Speichern der Checkbox-Referenzen
|
# Dictionary zum Speichern der Checkbox-Referenzen
|
||||||
self.xsl_checkboxes = {} # {python_id(node): checkbox}
|
self.xsl_checkboxes = {} # {xsl_node_id: checkbox}
|
||||||
self.xsl_nodes = {} # {python_id(node): XslFile}
|
|
||||||
# Initial ausgewählte XslFile-IDs (tuple), um Diff beim Accept zu berechnen
|
|
||||||
self._initial_selected_ids: set = set()
|
|
||||||
|
|
||||||
# Edit-Modus: UI anpassen
|
|
||||||
if self.edit_mode:
|
|
||||||
self.setWindowTitle("XML-Zuordnungen bearbeiten")
|
|
||||||
self.ui.infoLabel.setText(
|
|
||||||
"Passen Sie die Zuordnungen der XML-Datei an. Hinzufügen per Haken, "
|
|
||||||
"Entfernen durch Abhaken (zugehörige PDFs werden gelöscht):"
|
|
||||||
)
|
|
||||||
self.ui.alle_xml.setVisible(False)
|
|
||||||
|
|
||||||
# Signale verbinden
|
# Signale verbinden
|
||||||
self.ui.selectAllButton.clicked.connect(self.select_all)
|
self.ui.selectAllButton.clicked.connect(self.select_all)
|
||||||
@@ -68,10 +45,6 @@ class XmlToXslAssignDialog(QDialog):
|
|||||||
# Daten laden
|
# Daten laden
|
||||||
self._load_data()
|
self._load_data()
|
||||||
|
|
||||||
# Duplikat-Warnung nur im Edit-Modus (um bestehende Aufrufer nicht bei jeder XML zu stören)
|
|
||||||
if self.edit_mode:
|
|
||||||
self._warn_on_duplicate_xsl_ids()
|
|
||||||
|
|
||||||
def _setup_tree(self):
|
def _setup_tree(self):
|
||||||
"""Konfiguriert das TreeWidget."""
|
"""Konfiguriert das TreeWidget."""
|
||||||
# Spaltenbreiten setzen
|
# Spaltenbreiten setzen
|
||||||
@@ -137,19 +110,13 @@ class XmlToXslAssignDialog(QDialog):
|
|||||||
if isinstance(node, XslFile):
|
if isinstance(node, XslFile):
|
||||||
# Erstelle zentrierte Checkbox für XSL-Knoten
|
# Erstelle zentrierte Checkbox für XSL-Knoten
|
||||||
checkbox_widget, checkbox = self._create_centered_checkbox()
|
checkbox_widget, checkbox = self._create_centered_checkbox()
|
||||||
|
|
||||||
# Vorauswahl setzen, wenn ID in preselected_xsl_ids
|
|
||||||
if node.id in self.preselected_xsl_ids:
|
|
||||||
checkbox.setChecked(True)
|
|
||||||
self._initial_selected_ids.add(node.id)
|
|
||||||
|
|
||||||
# Setze das Widget in Spalte 2
|
# Setze das Widget in Spalte 2
|
||||||
self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget)
|
self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget)
|
||||||
|
|
||||||
# Speichere Checkbox- und Node-Referenzen (id() als Key, da XSL-IDs theoretisch doppelt sein können)
|
# Speichere Checkbox-Referenz
|
||||||
self.xsl_checkboxes[id(node)] = checkbox
|
self.xsl_checkboxes[id(node)] = checkbox
|
||||||
self.xsl_nodes[id(node)] = node
|
|
||||||
|
|
||||||
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
|
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
|
||||||
|
|
||||||
# Rekursiv für Kinder
|
# Rekursiv für Kinder
|
||||||
@@ -174,8 +141,6 @@ class XmlToXslAssignDialog(QDialog):
|
|||||||
# TreeWidget leeren
|
# TreeWidget leeren
|
||||||
self.ui.xslNodesTree.clear()
|
self.ui.xslNodesTree.clear()
|
||||||
self.xsl_checkboxes.clear()
|
self.xsl_checkboxes.clear()
|
||||||
self.xsl_nodes.clear()
|
|
||||||
self._initial_selected_ids.clear()
|
|
||||||
|
|
||||||
# Sortiere Root-Nodes alphabetisch nach ID
|
# Sortiere Root-Nodes alphabetisch nach ID
|
||||||
sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id)
|
sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id)
|
||||||
@@ -330,66 +295,16 @@ class XmlToXslAssignDialog(QDialog):
|
|||||||
"""
|
"""
|
||||||
return self.ui.alle_xml.isChecked()
|
return self.ui.alle_xml.isChecked()
|
||||||
|
|
||||||
def get_selection_diff(self) -> tuple[list, list]:
|
|
||||||
"""
|
|
||||||
Gibt die Änderungen der Auswahl gegenüber der Vorauswahl zurück.
|
|
||||||
Nützlich im Edit-Modus, um nur die tatsächlich geänderten Zuordnungen zu verarbeiten.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[list[XslFile], list[XslFile]]: (added_nodes, removed_nodes)
|
|
||||||
- added_nodes: XslFile-Objekte, die neu angehakt wurden
|
|
||||||
- removed_nodes: XslFile-Objekte, deren Haken entfernt wurde
|
|
||||||
"""
|
|
||||||
added_nodes = []
|
|
||||||
removed_nodes = []
|
|
||||||
|
|
||||||
for py_id, checkbox in self.xsl_checkboxes.items():
|
|
||||||
node = self.xsl_nodes.get(py_id)
|
|
||||||
if node is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
was_selected = node.id in self._initial_selected_ids
|
|
||||||
is_selected = checkbox.isChecked()
|
|
||||||
|
|
||||||
if is_selected and not was_selected:
|
|
||||||
added_nodes.append(node)
|
|
||||||
elif not is_selected and was_selected:
|
|
||||||
removed_nodes.append(node)
|
|
||||||
|
|
||||||
return added_nodes, removed_nodes
|
|
||||||
|
|
||||||
def _warn_on_duplicate_xsl_ids(self):
|
|
||||||
"""
|
|
||||||
Zeigt eine Warnung, wenn im Projekt mehrere XslFile-Instanzen mit identischer ID existieren.
|
|
||||||
Die ID soll eindeutig sein - Duplikate weisen auf einen Datenfehler hin.
|
|
||||||
"""
|
|
||||||
id_to_nodes: dict = {}
|
|
||||||
for py_id, node in self.xsl_nodes.items():
|
|
||||||
id_to_nodes.setdefault(node.id, []).append(node)
|
|
||||||
|
|
||||||
duplicates = {xsl_id: nodes for xsl_id, nodes in id_to_nodes.items() if len(nodes) > 1}
|
|
||||||
if not duplicates:
|
|
||||||
return
|
|
||||||
|
|
||||||
dup_lines = [f" - ID {xsl_id}: {len(nodes)}× ({', '.join(n.bez for n in nodes)})" for xsl_id, nodes in duplicates.items()]
|
|
||||||
logger.warning(f"Doppelte XSL-IDs im Projekt gefunden:\n{chr(10).join(dup_lines)}")
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Doppelte XSL-IDs",
|
|
||||||
"Im Projekt existieren XSL-Knoten mit identischer ID. "
|
|
||||||
"Die IDs sollten eindeutig sein:\n\n" + "\n".join(dup_lines),
|
|
||||||
)
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
"""Überschreibt accept() um Validierung durchzuführen."""
|
"""Überschreibt accept() um Validierung durchzuführen."""
|
||||||
# Im Edit-Modus: 0 Auswahlen erlauben (bedeutet: XML überall entfernen)
|
|
||||||
if self.edit_mode:
|
|
||||||
super().accept()
|
|
||||||
return
|
|
||||||
|
|
||||||
selected_nodes = self.get_selected_xsl_nodes()
|
selected_nodes = self.get_selected_xsl_nodes()
|
||||||
|
|
||||||
if not selected_nodes:
|
if not selected_nodes:
|
||||||
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie mindestens einen XSL-Knoten aus.")
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Warnung",
|
||||||
|
"Bitte wählen Sie mindestens einen XSL-Knoten aus."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|||||||
@@ -42,16 +42,6 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTreeWidget" name="xslNodesTree">
|
<widget class="QTreeWidget" name="xslNodesTree">
|
||||||
<property name="styleSheet">
|
|
||||||
<string notr="true"> QTreeWidget::item {
|
|
||||||
padding: 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTreeWidget::item:selected {
|
|
||||||
background-color: palette(highlight);
|
|
||||||
color: palette(highlighted-text);
|
|
||||||
}</string>
|
|
||||||
</property>
|
|
||||||
<property name="headerHidden">
|
<property name="headerHidden">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'XmlToXslAssignDialog.ui'
|
## Form generated from reading UI file 'XmlToXslAssignDialog.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
## Created by: Qt User Interface Compiler version 6.9.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -42,14 +42,6 @@ class Ui_XmlToXslAssignDialog(object):
|
|||||||
|
|
||||||
self.xslNodesTree = QTreeWidget(XmlToXslAssignDialog)
|
self.xslNodesTree = QTreeWidget(XmlToXslAssignDialog)
|
||||||
self.xslNodesTree.setObjectName(u"xslNodesTree")
|
self.xslNodesTree.setObjectName(u"xslNodesTree")
|
||||||
self.xslNodesTree.setStyleSheet(u" QTreeWidget::item {\n"
|
|
||||||
" padding: 4px 4px;\n"
|
|
||||||
" }\n"
|
|
||||||
"\n"
|
|
||||||
"QTreeWidget::item:selected {\n"
|
|
||||||
" background-color: palette(highlight);\n"
|
|
||||||
" color: palette(highlighted-text);\n"
|
|
||||||
"}")
|
|
||||||
self.xslNodesTree.setHeaderHidden(False)
|
self.xslNodesTree.setHeaderHidden(False)
|
||||||
self.xslNodesTree.setColumnCount(3)
|
self.xslNodesTree.setColumnCount(3)
|
||||||
self.xslNodesTree.header().setVisible(True)
|
self.xslNodesTree.header().setVisible(True)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,349 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>XslDependencyDialog</class>
|
|
||||||
<widget class="QDialog" name="XslDependencyDialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>1000</width>
|
|
||||||
<height>700</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>XSL-Abhängigkeitsgraph</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="toolbarLayout">
|
|
||||||
<item>
|
|
||||||
<spacer name="toolbarSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="settingsButton">
|
|
||||||
<property name="maximumSize">
|
|
||||||
<size>
|
|
||||||
<width>28</width>
|
|
||||||
<height>28</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Einstellungen ein-/ausblenden</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset theme="applications-system"/>
|
|
||||||
</property>
|
|
||||||
<property name="flat">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QSplitter" name="mainSplitter">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="childrenCollapsible">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<widget class="QTabWidget" name="tabWidget">
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="treeTab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Baumansicht</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QVBoxLayout" name="treeTabLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QSplitter" name="splitter">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="leftWidget" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="leftLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="leftLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>XSL-Dateien</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QTreeWidget" name="fileTree">
|
|
||||||
<property name="alternatingRowColors">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="selectionMode">
|
|
||||||
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
|
|
||||||
</property>
|
|
||||||
<property name="rootIsDecorated">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<column>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">1</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="rightWidget" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="rightLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="rightLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>Abhängigkeiten</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QTreeWidget" name="depTree">
|
|
||||||
<property name="alternatingRowColors">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="rootIsDecorated">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<column>
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">1</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="graphTab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Netzwerkgraph</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QVBoxLayout" name="graphTabLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<widget class="QWidget" name="graphContainer" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="graphContainerLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="sidebarWidget" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="sidebarLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="sidebarLabel">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="searchLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>Suche:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="searchEdit">
|
|
||||||
<property name="placeholderText">
|
|
||||||
<string>XSL-Datei filtern...</string>
|
|
||||||
</property>
|
|
||||||
<property name="clearButtonEnabled">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QStackedWidget" name="settingsStack">
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="treeSettingsPage">
|
|
||||||
<layout class="QVBoxLayout" name="treeSettingsLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="treeSettingsLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>Baumansicht-Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="treeSettingsSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QWidget" name="graphSettingsPage">
|
|
||||||
<layout class="QVBoxLayout" name="graphSettingsLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="graphSettingsLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>Graph-Einstellungen</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="graphFilterConnectedCheck">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Nur von der Suche betroffene Dateien anzeigen</string>
|
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="graphSettingsSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="statusLabel">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::StandardButton::Close</set>
|
|
||||||
</property>
|
|
||||||
<property name="centerButtons">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>XslDependencyDialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>500</x>
|
|
||||||
<y>678</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>500</x>
|
|
||||||
<y>350</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
## Form generated from reading UI file 'XslDependencyDialog.ui'
|
|
||||||
##
|
|
||||||
## Created by: Qt User Interface Compiler version 6.10.1
|
|
||||||
##
|
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
|
||||||
QMetaObject, QObject, QPoint, QRect,
|
|
||||||
QSize, QTime, QUrl, Qt)
|
|
||||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
|
||||||
QFont, QFontDatabase, QGradient, QIcon,
|
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
|
||||||
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
|
|
||||||
QDialog, QDialogButtonBox, QHBoxLayout, QHeaderView,
|
|
||||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
|
||||||
QSpacerItem, QSplitter, QStackedWidget, QTabWidget,
|
|
||||||
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
|
|
||||||
|
|
||||||
class Ui_XslDependencyDialog(object):
|
|
||||||
def setupUi(self, XslDependencyDialog):
|
|
||||||
if not XslDependencyDialog.objectName():
|
|
||||||
XslDependencyDialog.setObjectName(u"XslDependencyDialog")
|
|
||||||
XslDependencyDialog.resize(1000, 700)
|
|
||||||
self.verticalLayout = QVBoxLayout(XslDependencyDialog)
|
|
||||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
|
||||||
self.toolbarLayout = QHBoxLayout()
|
|
||||||
self.toolbarLayout.setObjectName(u"toolbarLayout")
|
|
||||||
self.toolbarSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
|
||||||
|
|
||||||
self.toolbarLayout.addItem(self.toolbarSpacer)
|
|
||||||
|
|
||||||
self.settingsButton = QPushButton(XslDependencyDialog)
|
|
||||||
self.settingsButton.setObjectName(u"settingsButton")
|
|
||||||
self.settingsButton.setMaximumSize(QSize(28, 28))
|
|
||||||
icon = QIcon(QIcon.fromTheme(u"applications-system"))
|
|
||||||
self.settingsButton.setIcon(icon)
|
|
||||||
self.settingsButton.setFlat(True)
|
|
||||||
|
|
||||||
self.toolbarLayout.addWidget(self.settingsButton)
|
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout.addLayout(self.toolbarLayout)
|
|
||||||
|
|
||||||
self.mainSplitter = QSplitter(XslDependencyDialog)
|
|
||||||
self.mainSplitter.setObjectName(u"mainSplitter")
|
|
||||||
self.mainSplitter.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.mainSplitter.setChildrenCollapsible(True)
|
|
||||||
self.tabWidget = QTabWidget(self.mainSplitter)
|
|
||||||
self.tabWidget.setObjectName(u"tabWidget")
|
|
||||||
self.treeTab = QWidget()
|
|
||||||
self.treeTab.setObjectName(u"treeTab")
|
|
||||||
self.treeTabLayout = QVBoxLayout(self.treeTab)
|
|
||||||
self.treeTabLayout.setObjectName(u"treeTabLayout")
|
|
||||||
self.splitter = QSplitter(self.treeTab)
|
|
||||||
self.splitter.setObjectName(u"splitter")
|
|
||||||
self.splitter.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.leftWidget = QWidget(self.splitter)
|
|
||||||
self.leftWidget.setObjectName(u"leftWidget")
|
|
||||||
self.leftLayout = QVBoxLayout(self.leftWidget)
|
|
||||||
self.leftLayout.setObjectName(u"leftLayout")
|
|
||||||
self.leftLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.leftLabel = QLabel(self.leftWidget)
|
|
||||||
self.leftLabel.setObjectName(u"leftLabel")
|
|
||||||
|
|
||||||
self.leftLayout.addWidget(self.leftLabel)
|
|
||||||
|
|
||||||
self.fileTree = QTreeWidget(self.leftWidget)
|
|
||||||
__qtreewidgetitem = QTreeWidgetItem()
|
|
||||||
__qtreewidgetitem.setText(0, u"1");
|
|
||||||
self.fileTree.setHeaderItem(__qtreewidgetitem)
|
|
||||||
self.fileTree.setObjectName(u"fileTree")
|
|
||||||
self.fileTree.setAlternatingRowColors(True)
|
|
||||||
self.fileTree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
||||||
self.fileTree.setRootIsDecorated(True)
|
|
||||||
|
|
||||||
self.leftLayout.addWidget(self.fileTree)
|
|
||||||
|
|
||||||
self.splitter.addWidget(self.leftWidget)
|
|
||||||
self.rightWidget = QWidget(self.splitter)
|
|
||||||
self.rightWidget.setObjectName(u"rightWidget")
|
|
||||||
self.rightLayout = QVBoxLayout(self.rightWidget)
|
|
||||||
self.rightLayout.setObjectName(u"rightLayout")
|
|
||||||
self.rightLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.rightLabel = QLabel(self.rightWidget)
|
|
||||||
self.rightLabel.setObjectName(u"rightLabel")
|
|
||||||
|
|
||||||
self.rightLayout.addWidget(self.rightLabel)
|
|
||||||
|
|
||||||
self.depTree = QTreeWidget(self.rightWidget)
|
|
||||||
__qtreewidgetitem1 = QTreeWidgetItem()
|
|
||||||
__qtreewidgetitem1.setText(0, u"1");
|
|
||||||
self.depTree.setHeaderItem(__qtreewidgetitem1)
|
|
||||||
self.depTree.setObjectName(u"depTree")
|
|
||||||
self.depTree.setAlternatingRowColors(True)
|
|
||||||
self.depTree.setRootIsDecorated(True)
|
|
||||||
|
|
||||||
self.rightLayout.addWidget(self.depTree)
|
|
||||||
|
|
||||||
self.splitter.addWidget(self.rightWidget)
|
|
||||||
|
|
||||||
self.treeTabLayout.addWidget(self.splitter)
|
|
||||||
|
|
||||||
self.tabWidget.addTab(self.treeTab, "")
|
|
||||||
self.graphTab = QWidget()
|
|
||||||
self.graphTab.setObjectName(u"graphTab")
|
|
||||||
self.graphTabLayout = QVBoxLayout(self.graphTab)
|
|
||||||
self.graphTabLayout.setObjectName(u"graphTabLayout")
|
|
||||||
self.graphTabLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.graphContainer = QWidget(self.graphTab)
|
|
||||||
self.graphContainer.setObjectName(u"graphContainer")
|
|
||||||
self.graphContainerLayout = QVBoxLayout(self.graphContainer)
|
|
||||||
self.graphContainerLayout.setObjectName(u"graphContainerLayout")
|
|
||||||
self.graphContainerLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
self.graphTabLayout.addWidget(self.graphContainer)
|
|
||||||
|
|
||||||
self.tabWidget.addTab(self.graphTab, "")
|
|
||||||
self.mainSplitter.addWidget(self.tabWidget)
|
|
||||||
self.sidebarWidget = QWidget(self.mainSplitter)
|
|
||||||
self.sidebarWidget.setObjectName(u"sidebarWidget")
|
|
||||||
self.sidebarLayout = QVBoxLayout(self.sidebarWidget)
|
|
||||||
self.sidebarLayout.setObjectName(u"sidebarLayout")
|
|
||||||
self.sidebarLabel = QLabel(self.sidebarWidget)
|
|
||||||
self.sidebarLabel.setObjectName(u"sidebarLabel")
|
|
||||||
font = QFont()
|
|
||||||
font.setBold(True)
|
|
||||||
self.sidebarLabel.setFont(font)
|
|
||||||
|
|
||||||
self.sidebarLayout.addWidget(self.sidebarLabel)
|
|
||||||
|
|
||||||
self.searchLabel = QLabel(self.sidebarWidget)
|
|
||||||
self.searchLabel.setObjectName(u"searchLabel")
|
|
||||||
|
|
||||||
self.sidebarLayout.addWidget(self.searchLabel)
|
|
||||||
|
|
||||||
self.searchEdit = QLineEdit(self.sidebarWidget)
|
|
||||||
self.searchEdit.setObjectName(u"searchEdit")
|
|
||||||
self.searchEdit.setClearButtonEnabled(True)
|
|
||||||
|
|
||||||
self.sidebarLayout.addWidget(self.searchEdit)
|
|
||||||
|
|
||||||
self.settingsStack = QStackedWidget(self.sidebarWidget)
|
|
||||||
self.settingsStack.setObjectName(u"settingsStack")
|
|
||||||
self.treeSettingsPage = QWidget()
|
|
||||||
self.treeSettingsPage.setObjectName(u"treeSettingsPage")
|
|
||||||
self.treeSettingsLayout = QVBoxLayout(self.treeSettingsPage)
|
|
||||||
self.treeSettingsLayout.setObjectName(u"treeSettingsLayout")
|
|
||||||
self.treeSettingsLabel = QLabel(self.treeSettingsPage)
|
|
||||||
self.treeSettingsLabel.setObjectName(u"treeSettingsLabel")
|
|
||||||
|
|
||||||
self.treeSettingsLayout.addWidget(self.treeSettingsLabel)
|
|
||||||
|
|
||||||
self.treeSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
|
|
||||||
self.treeSettingsLayout.addItem(self.treeSettingsSpacer)
|
|
||||||
|
|
||||||
self.settingsStack.addWidget(self.treeSettingsPage)
|
|
||||||
self.graphSettingsPage = QWidget()
|
|
||||||
self.graphSettingsPage.setObjectName(u"graphSettingsPage")
|
|
||||||
self.graphSettingsLayout = QVBoxLayout(self.graphSettingsPage)
|
|
||||||
self.graphSettingsLayout.setObjectName(u"graphSettingsLayout")
|
|
||||||
self.graphSettingsLabel = QLabel(self.graphSettingsPage)
|
|
||||||
self.graphSettingsLabel.setObjectName(u"graphSettingsLabel")
|
|
||||||
|
|
||||||
self.graphSettingsLayout.addWidget(self.graphSettingsLabel)
|
|
||||||
|
|
||||||
self.graphFilterConnectedCheck = QCheckBox(self.graphSettingsPage)
|
|
||||||
self.graphFilterConnectedCheck.setObjectName(u"graphFilterConnectedCheck")
|
|
||||||
self.graphFilterConnectedCheck.setChecked(True)
|
|
||||||
|
|
||||||
self.graphSettingsLayout.addWidget(self.graphFilterConnectedCheck)
|
|
||||||
|
|
||||||
self.graphSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
|
|
||||||
self.graphSettingsLayout.addItem(self.graphSettingsSpacer)
|
|
||||||
|
|
||||||
self.settingsStack.addWidget(self.graphSettingsPage)
|
|
||||||
|
|
||||||
self.sidebarLayout.addWidget(self.settingsStack)
|
|
||||||
|
|
||||||
self.mainSplitter.addWidget(self.sidebarWidget)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.mainSplitter)
|
|
||||||
|
|
||||||
self.statusLabel = QLabel(XslDependencyDialog)
|
|
||||||
self.statusLabel.setObjectName(u"statusLabel")
|
|
||||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
|
|
||||||
sizePolicy.setHorizontalStretch(0)
|
|
||||||
sizePolicy.setVerticalStretch(0)
|
|
||||||
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
|
|
||||||
self.statusLabel.setSizePolicy(sizePolicy)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.statusLabel)
|
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(XslDependencyDialog)
|
|
||||||
self.buttonBox.setObjectName(u"buttonBox")
|
|
||||||
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close)
|
|
||||||
self.buttonBox.setCenterButtons(True)
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
|
||||||
|
|
||||||
|
|
||||||
self.retranslateUi(XslDependencyDialog)
|
|
||||||
self.buttonBox.rejected.connect(XslDependencyDialog.reject)
|
|
||||||
|
|
||||||
self.tabWidget.setCurrentIndex(0)
|
|
||||||
self.settingsStack.setCurrentIndex(0)
|
|
||||||
|
|
||||||
|
|
||||||
QMetaObject.connectSlotsByName(XslDependencyDialog)
|
|
||||||
# setupUi
|
|
||||||
|
|
||||||
def retranslateUi(self, XslDependencyDialog):
|
|
||||||
XslDependencyDialog.setWindowTitle(QCoreApplication.translate("XslDependencyDialog", u"XSL-Abh\u00e4ngigkeitsgraph", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.settingsButton.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen ein-/ausblenden", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.settingsButton.setText("")
|
|
||||||
self.leftLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Dateien", None))
|
|
||||||
self.rightLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Abh\u00e4ngigkeiten", None))
|
|
||||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.treeTab), QCoreApplication.translate("XslDependencyDialog", u"Baumansicht", None))
|
|
||||||
self.tabWidget.setTabText(self.tabWidget.indexOf(self.graphTab), QCoreApplication.translate("XslDependencyDialog", u"Netzwerkgraph", None))
|
|
||||||
self.sidebarLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen", None))
|
|
||||||
self.searchLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Suche:", None))
|
|
||||||
self.searchEdit.setPlaceholderText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Datei filtern...", None))
|
|
||||||
self.treeSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Baumansicht-Einstellungen", None))
|
|
||||||
self.graphSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Graph-Einstellungen", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.graphFilterConnectedCheck.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.graphFilterConnectedCheck.setText(QCoreApplication.translate("XslDependencyDialog", u"Nur von der Suche betroffene Dateien anzeigen", None))
|
|
||||||
self.statusLabel.setText("")
|
|
||||||
# retranslateUi
|
|
||||||
|
|
||||||
+152
-7
@@ -1,16 +1,161 @@
|
|||||||
|
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from ui.XslFileEditDialog_ui import Ui_XslFileEditDialog
|
from ui.XslFileEditDialog_ui import Ui_XslFileEditDialog
|
||||||
from ui.XsltParamsEditDialog import XsltParamsEditDialog
|
|
||||||
|
|
||||||
|
|
||||||
class XslFileEditDialog(XsltParamsEditDialog):
|
class XslFileEditDialog(QDialog):
|
||||||
"""Dialog zur Bearbeitung von XslFile-Objekten."""
|
"""Dialog zur Bearbeitung von XslFile-Objekten."""
|
||||||
|
|
||||||
def _create_ui(self):
|
def __init__(self, parent=None, node=None, parent_params=None):
|
||||||
return Ui_XslFileEditDialog()
|
"""
|
||||||
|
Initialisiert den Dialog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
node: XslFile-Objekt zum Bearbeiten
|
||||||
|
parent_params: Dictionary mit Eltern-Parametern (nur anzeigen)
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
# UI einrichten
|
||||||
|
self.ui = Ui_XslFileEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
# Node-Objekt speichern
|
||||||
|
self.node = node
|
||||||
|
self.parent_params = parent_params or {}
|
||||||
|
|
||||||
|
# Signale verbinden
|
||||||
|
self.ui.addParamButton.clicked.connect(self.add_parameter)
|
||||||
|
self.ui.removeParamButton.clicked.connect(self.remove_parameter)
|
||||||
|
|
||||||
|
# Tabellen konfigurieren
|
||||||
|
self._setup_tables()
|
||||||
|
|
||||||
|
# Daten laden
|
||||||
|
if self.node:
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
def _setup_tables(self):
|
||||||
|
"""Konfiguriert die Tabellen."""
|
||||||
|
# XSLT Parameter Tabelle
|
||||||
|
self.ui.xsltParamsTable.setColumnWidth(0, 200)
|
||||||
|
self.ui.xsltParamsTable.setColumnWidth(1, 300)
|
||||||
|
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
|
# Eltern-Parameter Tabelle
|
||||||
|
self.ui.parentParamsTable.setColumnWidth(0, 200)
|
||||||
|
self.ui.parentParamsTable.setColumnWidth(1, 300)
|
||||||
|
self.ui.parentParamsTable.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
def _load_data(self):
|
def _load_data(self):
|
||||||
"""Lädt die Daten des XslFile-Knotens in den Dialog."""
|
"""Lädt die Daten des XslFile in den Dialog."""
|
||||||
if not self.node:
|
if not self.node:
|
||||||
return
|
return
|
||||||
self.ui.xslFileValueLabel.setText(str(self.node.xsl_file) if self.node.xsl_file else "")
|
|
||||||
super()._load_data()
|
# Bezeichnung setzen
|
||||||
|
self.ui.bezEdit.setText(str(self.node.bez) if self.node.bez else "")
|
||||||
|
|
||||||
|
# XSLT Parameter laden
|
||||||
|
self._load_xslt_params()
|
||||||
|
|
||||||
|
# Eltern-Parameter laden
|
||||||
|
self._load_parent_params()
|
||||||
|
|
||||||
|
def _load_xslt_params(self):
|
||||||
|
"""Lädt die XSLT Parameter in die Tabelle."""
|
||||||
|
if not self.node or not self.node.xslt_params:
|
||||||
|
return
|
||||||
|
|
||||||
|
params = self.node.xslt_params
|
||||||
|
self.ui.xsltParamsTable.setRowCount(len(params))
|
||||||
|
|
||||||
|
for row, (key, value) in enumerate(params.items()):
|
||||||
|
# Parameter-Name
|
||||||
|
key_item = QTableWidgetItem(str(key))
|
||||||
|
self.ui.xsltParamsTable.setItem(row, 0, key_item)
|
||||||
|
|
||||||
|
# Parameter-Wert
|
||||||
|
value_item = QTableWidgetItem(str(value))
|
||||||
|
self.ui.xsltParamsTable.setItem(row, 1, value_item)
|
||||||
|
|
||||||
|
def _load_parent_params(self):
|
||||||
|
"""Lädt die Eltern-Parameter in die Tabelle (nur anzeigen)."""
|
||||||
|
if not self.parent_params:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ui.parentParamsTable.setRowCount(len(self.parent_params))
|
||||||
|
|
||||||
|
for row, (key, value) in enumerate(self.parent_params.items()):
|
||||||
|
# Parameter-Name
|
||||||
|
key_item = QTableWidgetItem(str(key))
|
||||||
|
key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
|
self.ui.parentParamsTable.setItem(row, 0, key_item)
|
||||||
|
|
||||||
|
# Parameter-Wert
|
||||||
|
value_item = QTableWidgetItem(str(value))
|
||||||
|
value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
|
self.ui.parentParamsTable.setItem(row, 1, value_item)
|
||||||
|
|
||||||
|
def add_parameter(self):
|
||||||
|
"""Fügt einen neuen Parameter hinzu."""
|
||||||
|
row_count = self.ui.xsltParamsTable.rowCount()
|
||||||
|
self.ui.xsltParamsTable.insertRow(row_count)
|
||||||
|
|
||||||
|
# Leere Items hinzufügen
|
||||||
|
key_item = QTableWidgetItem("")
|
||||||
|
value_item = QTableWidgetItem("")
|
||||||
|
|
||||||
|
self.ui.xsltParamsTable.setItem(row_count, 0, key_item)
|
||||||
|
self.ui.xsltParamsTable.setItem(row_count, 1, value_item)
|
||||||
|
|
||||||
|
# Fokus auf den neuen Parameter setzen
|
||||||
|
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
|
||||||
|
|
||||||
|
def remove_parameter(self):
|
||||||
|
"""Entfernt den ausgewählten Parameter."""
|
||||||
|
current_row = self.ui.xsltParamsTable.currentRow()
|
||||||
|
if current_row >= 0:
|
||||||
|
self.ui.xsltParamsTable.removeRow(current_row)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
"""
|
||||||
|
Gibt die bearbeiteten Daten zurück.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary mit den bearbeiteten Daten oder None bei Fehler
|
||||||
|
"""
|
||||||
|
# Bezeichnung prüfen
|
||||||
|
bez = self.ui.bezEdit.text().strip()
|
||||||
|
if not bez:
|
||||||
|
QMessageBox.warning(self, "Warnung", "Bitte geben Sie eine Bezeichnung ein.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# XSLT Parameter sammeln
|
||||||
|
xslt_params = {}
|
||||||
|
for row in range(self.ui.xsltParamsTable.rowCount()):
|
||||||
|
key_item = self.ui.xsltParamsTable.item(row, 0)
|
||||||
|
value_item = self.ui.xsltParamsTable.item(row, 1)
|
||||||
|
|
||||||
|
if key_item and value_item:
|
||||||
|
key = key_item.text().strip()
|
||||||
|
value = value_item.text().strip()
|
||||||
|
|
||||||
|
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
|
||||||
|
xslt_params[key] = value
|
||||||
|
|
||||||
|
# CheckBox für Force-Transformation prüfen
|
||||||
|
force_transform = self.ui.alle_xml_transformieren.isChecked()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bez": bez,
|
||||||
|
"xslt_params": xslt_params,
|
||||||
|
"force_transform": force_transform
|
||||||
|
}
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""Überschreibt accept() um Datenvalidierung durchzuführen."""
|
||||||
|
data = self.get_data()
|
||||||
|
if data is not None:
|
||||||
|
super().accept()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>865</width>
|
<width>865</width>
|
||||||
<height>403</height>
|
<height>400</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -23,30 +23,13 @@
|
|||||||
<enum>QLayout::SizeConstraint::SetMaximumSize</enum>
|
<enum>QLayout::SizeConstraint::SetMaximumSize</enum>
|
||||||
</property>
|
</property>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="xslFileLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string>XSL-Datei:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLabel" name="xslFileValueLabel">
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
<property name="textInteractionFlags">
|
|
||||||
<set>Qt::TextInteractionFlag::TextSelectableByMouse</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="bezLabel">
|
<widget class="QLabel" name="bezLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Bezeichnung:</string>
|
<string>Bezeichnung:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLineEdit" name="bezEdit"/>
|
<widget class="QLineEdit" name="bezEdit"/>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
@@ -33,26 +33,15 @@ class Ui_XslFileEditDialog(object):
|
|||||||
self.formLayout = QFormLayout()
|
self.formLayout = QFormLayout()
|
||||||
self.formLayout.setObjectName(u"formLayout")
|
self.formLayout.setObjectName(u"formLayout")
|
||||||
self.formLayout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
self.formLayout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
||||||
self.xslFileLabel = QLabel(XslFileEditDialog)
|
|
||||||
self.xslFileLabel.setObjectName(u"xslFileLabel")
|
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.xslFileLabel)
|
|
||||||
|
|
||||||
self.xslFileValueLabel = QLabel(XslFileEditDialog)
|
|
||||||
self.xslFileValueLabel.setObjectName(u"xslFileValueLabel")
|
|
||||||
self.xslFileValueLabel.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
||||||
|
|
||||||
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.xslFileValueLabel)
|
|
||||||
|
|
||||||
self.bezLabel = QLabel(XslFileEditDialog)
|
self.bezLabel = QLabel(XslFileEditDialog)
|
||||||
self.bezLabel.setObjectName(u"bezLabel")
|
self.bezLabel.setObjectName(u"bezLabel")
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.bezLabel)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.bezLabel)
|
||||||
|
|
||||||
self.bezEdit = QLineEdit(XslFileEditDialog)
|
self.bezEdit = QLineEdit(XslFileEditDialog)
|
||||||
self.bezEdit.setObjectName(u"bezEdit")
|
self.bezEdit.setObjectName(u"bezEdit")
|
||||||
|
|
||||||
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.bezEdit)
|
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.bezEdit)
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout.addLayout(self.formLayout)
|
self.verticalLayout.addLayout(self.formLayout)
|
||||||
@@ -162,8 +151,6 @@ class Ui_XslFileEditDialog(object):
|
|||||||
|
|
||||||
def retranslateUi(self, XslFileEditDialog):
|
def retranslateUi(self, XslFileEditDialog):
|
||||||
XslFileEditDialog.setWindowTitle(QCoreApplication.translate("XslFileEditDialog", u"XSL-Datei bearbeiten", None))
|
XslFileEditDialog.setWindowTitle(QCoreApplication.translate("XslFileEditDialog", u"XSL-Datei bearbeiten", None))
|
||||||
self.xslFileLabel.setText(QCoreApplication.translate("XslFileEditDialog", u"XSL-Datei:", None))
|
|
||||||
self.xslFileValueLabel.setText("")
|
|
||||||
self.bezLabel.setText(QCoreApplication.translate("XslFileEditDialog", u"Bezeichnung:", None))
|
self.bezLabel.setText(QCoreApplication.translate("XslFileEditDialog", u"Bezeichnung:", None))
|
||||||
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("XslFileEditDialog", u"XSLT-Parameter", None))
|
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("XslFileEditDialog", u"XSLT-Parameter", None))
|
||||||
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
|
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
|
|
||||||
|
|
||||||
class XsltParamsEditDialog(QDialog):
|
|
||||||
"""Gemeinsame Basisklasse für Dialoge zur Bearbeitung von XSLT-Parametern."""
|
|
||||||
|
|
||||||
def __init__(self, parent=None, node=None, parent_params=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.ui = self._create_ui()
|
|
||||||
self.ui.setupUi(self)
|
|
||||||
|
|
||||||
self.node = node
|
|
||||||
self.parent_params = parent_params or {}
|
|
||||||
|
|
||||||
self.ui.addParamButton.clicked.connect(self.add_parameter)
|
|
||||||
self.ui.removeParamButton.clicked.connect(self.remove_parameter)
|
|
||||||
|
|
||||||
self._setup_tables()
|
|
||||||
|
|
||||||
if self.node:
|
|
||||||
self._load_data()
|
|
||||||
|
|
||||||
def _create_ui(self):
|
|
||||||
"""Gibt eine Instanz der konkreten UI-Klasse zurück. Muss überschrieben werden."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _setup_tables(self):
|
|
||||||
"""Konfiguriert die Tabellen."""
|
|
||||||
self.ui.xsltParamsTable.setColumnWidth(0, 200)
|
|
||||||
self.ui.xsltParamsTable.setColumnWidth(1, 300)
|
|
||||||
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
|
|
||||||
|
|
||||||
self.ui.parentParamsTable.setColumnWidth(0, 200)
|
|
||||||
self.ui.parentParamsTable.setColumnWidth(1, 300)
|
|
||||||
self.ui.parentParamsTable.horizontalHeader().setStretchLastSection(True)
|
|
||||||
|
|
||||||
def _load_data(self):
|
|
||||||
"""Lädt die Daten des Knotens in den Dialog."""
|
|
||||||
if not self.node:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.ui.bezEdit.setText(str(self.node.bez) if self.node.bez else "")
|
|
||||||
self._load_xslt_params()
|
|
||||||
self._load_parent_params()
|
|
||||||
|
|
||||||
def _load_xslt_params(self):
|
|
||||||
"""Lädt die XSLT-Parameter in die Tabelle."""
|
|
||||||
if not self.node or not self.node.xslt_params:
|
|
||||||
return
|
|
||||||
|
|
||||||
params = self.node.xslt_params
|
|
||||||
self.ui.xsltParamsTable.setRowCount(len(params))
|
|
||||||
|
|
||||||
for row, (key, value) in enumerate(params.items()):
|
|
||||||
self.ui.xsltParamsTable.setItem(row, 0, QTableWidgetItem(str(key)))
|
|
||||||
self.ui.xsltParamsTable.setItem(row, 1, QTableWidgetItem(str(value)))
|
|
||||||
|
|
||||||
def _load_parent_params(self):
|
|
||||||
"""Lädt die Eltern-Parameter in die Tabelle (nur anzeigen)."""
|
|
||||||
if not self.parent_params:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.ui.parentParamsTable.setRowCount(len(self.parent_params))
|
|
||||||
|
|
||||||
for row, (key, value) in enumerate(self.parent_params.items()):
|
|
||||||
key_item = QTableWidgetItem(str(key))
|
|
||||||
key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
||||||
self.ui.parentParamsTable.setItem(row, 0, key_item)
|
|
||||||
|
|
||||||
value_item = QTableWidgetItem(str(value))
|
|
||||||
value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
||||||
self.ui.parentParamsTable.setItem(row, 1, value_item)
|
|
||||||
|
|
||||||
def add_parameter(self):
|
|
||||||
"""Fügt einen neuen Parameter hinzu."""
|
|
||||||
row_count = self.ui.xsltParamsTable.rowCount()
|
|
||||||
self.ui.xsltParamsTable.insertRow(row_count)
|
|
||||||
self.ui.xsltParamsTable.setItem(row_count, 0, QTableWidgetItem(""))
|
|
||||||
self.ui.xsltParamsTable.setItem(row_count, 1, QTableWidgetItem(""))
|
|
||||||
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
|
|
||||||
|
|
||||||
def remove_parameter(self):
|
|
||||||
"""Entfernt den ausgewählten Parameter."""
|
|
||||||
current_row = self.ui.xsltParamsTable.currentRow()
|
|
||||||
if current_row >= 0:
|
|
||||||
self.ui.xsltParamsTable.removeRow(current_row)
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
"""
|
|
||||||
Gibt die bearbeiteten Daten zurück.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict mit 'bez', 'xslt_params', 'force_transform', oder None bei Validierungsfehler
|
|
||||||
"""
|
|
||||||
bez = self.ui.bezEdit.text().strip()
|
|
||||||
if not bez:
|
|
||||||
QMessageBox.warning(self, "Warnung", "Bitte geben Sie eine Bezeichnung ein.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
xslt_params = {}
|
|
||||||
for row in range(self.ui.xsltParamsTable.rowCount()):
|
|
||||||
key_item = self.ui.xsltParamsTable.item(row, 0)
|
|
||||||
value_item = self.ui.xsltParamsTable.item(row, 1)
|
|
||||||
if key_item and value_item:
|
|
||||||
key = key_item.text().strip()
|
|
||||||
if key:
|
|
||||||
xslt_params[key] = value_item.text().strip()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"bez": bez,
|
|
||||||
"xslt_params": xslt_params,
|
|
||||||
"force_transform": self.ui.alle_xml_transformieren.isChecked(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def accept(self):
|
|
||||||
"""Überschreibt accept() um Datenvalidierung durchzuführen."""
|
|
||||||
if self.get_data() is not None:
|
|
||||||
super().accept()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""
|
|
||||||
Mixins für das MainWindow.
|
|
||||||
|
|
||||||
Dieses Paket enthält Mixins, die Funktionalität in separate Module auslagern,
|
|
||||||
um die MainWindow-Klasse übersichtlicher zu gestalten.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ui.mixins.tree_manager import TreeManagerMixin
|
|
||||||
from ui.mixins.pdf_viewer import PdfViewerMixin
|
|
||||||
from ui.mixins.worker_pool import WorkerPoolMixin
|
|
||||||
from ui.mixins.database import DatabaseMixin
|
|
||||||
from ui.mixins.drag_drop import DragDropMixin
|
|
||||||
from ui.mixins.hash_calculation import HashCalculationMixin
|
|
||||||
from ui.mixins.transformation import TransformationMixin
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"TreeManagerMixin",
|
|
||||||
"PdfViewerMixin",
|
|
||||||
"WorkerPoolMixin",
|
|
||||||
"DatabaseMixin",
|
|
||||||
"DragDropMixin",
|
|
||||||
"HashCalculationMixin",
|
|
||||||
"TransformationMixin",
|
|
||||||
]
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
"""
|
|
||||||
DatabaseMixin - Mixin für Datenbank-Operationen.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden zur PostgreSQL-Datenbankanbindung
|
|
||||||
und Datenverarbeitung für das MainWindow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtCore import QThread, Signal, Qt
|
|
||||||
from PySide6.QtWidgets import QMessageBox, QProgressDialog
|
|
||||||
|
|
||||||
from conf import app_settings, TreeNode, XslFile
|
|
||||||
from obsolete_detector import (
|
|
||||||
collect_unused_xml_files,
|
|
||||||
extract_db_xsl_ids,
|
|
||||||
find_obsolete_xsl_entries,
|
|
||||||
remove_empty_tree_nodes,
|
|
||||||
)
|
|
||||||
from ui.ObsoleteEntriesDialog import ObsoleteEntriesDialog
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseQueryThread(QThread):
|
|
||||||
"""Thread für asynchrone Datenbankabfragen."""
|
|
||||||
|
|
||||||
query_completed = Signal(object) # pl.DataFrame
|
|
||||||
query_failed = Signal(str) # Fehlermeldung
|
|
||||||
|
|
||||||
def __init__(self, sql_query, connection_string):
|
|
||||||
super().__init__()
|
|
||||||
self.sql_query = sql_query
|
|
||||||
self.connection_string = connection_string
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import polars as pl
|
|
||||||
|
|
||||||
try:
|
|
||||||
df = pl.read_database_uri(self.sql_query, self.connection_string, engine="connectorx").sort(
|
|
||||||
["reporttyp_bez", "report_bez", "repfile_bez"]
|
|
||||||
)
|
|
||||||
self.query_completed.emit(df)
|
|
||||||
except Exception as e:
|
|
||||||
self.query_failed.emit(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseMixin:
|
|
||||||
"""
|
|
||||||
Mixin für Datenbank-Operationen.
|
|
||||||
|
|
||||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
|
||||||
- self.project: Das aktuelle Projekt
|
|
||||||
- self.pdf_project: Die Projekt-Daten (ProjectData)
|
|
||||||
- self._load_nodes_to_tree(): Methode zum Laden der Nodes in den Tree
|
|
||||||
- self._save_project_settings(): Methode zum Speichern der Projekt-Einstellungen
|
|
||||||
"""
|
|
||||||
|
|
||||||
def on_load_from_fn2_clicked(self):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
|
|
||||||
Startet SQL-Abfrage asynchron mit Fortschrittsdialog.
|
|
||||||
"""
|
|
||||||
logger.debug("Button 'lade aus FN2' wurde geklickt!")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Prüfe ob ein Projekt geladen ist
|
|
||||||
if not hasattr(self, "project") or not self.project:
|
|
||||||
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole die PostgreSQL-Datenbank-Konfiguration
|
|
||||||
db_config = self._get_database_config(self.project.postgre_sql_db_id)
|
|
||||||
if not db_config:
|
|
||||||
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# SQL-Abfrage und Connection-String vorbereiten
|
|
||||||
sql_query, connection_string = self._prepare_sql_query(db_config)
|
|
||||||
if sql_query is None:
|
|
||||||
return # Fehler bereits angezeigt
|
|
||||||
|
|
||||||
# Fortschrittsdialog erstellen
|
|
||||||
self._db_progress = QProgressDialog("Verbinde mit Datenbank...", "Abbrechen", 0, 0, self)
|
|
||||||
self._db_progress.setWindowTitle("Datenbank-Abfrage")
|
|
||||||
self._db_progress.setWindowModality(Qt.WindowModal)
|
|
||||||
self._db_progress.setMinimumDuration(0)
|
|
||||||
|
|
||||||
# Query-Thread erstellen und starten
|
|
||||||
self._db_query_thread = DatabaseQueryThread(sql_query, connection_string)
|
|
||||||
self._db_query_thread.query_completed.connect(self._on_db_query_completed)
|
|
||||||
self._db_query_thread.query_failed.connect(self._on_db_query_failed)
|
|
||||||
self._db_query_thread.finished.connect(self._cleanup_db_query)
|
|
||||||
self._db_progress.canceled.connect(self._on_db_query_canceled)
|
|
||||||
self._db_query_thread.start()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Laden aus FN2: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
|
|
||||||
|
|
||||||
def _on_db_query_completed(self, df):
|
|
||||||
"""Wird aufgerufen, wenn die Datenbankabfrage erfolgreich war."""
|
|
||||||
# Ignoriere Ergebnis, falls der Benutzer abgebrochen hat
|
|
||||||
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
new_nodes = self._process_sql_data(df)
|
|
||||||
self._merge_nodes_with_existing(new_nodes)
|
|
||||||
self._check_and_remove_obsolete_entries(new_nodes)
|
|
||||||
self._save_project_settings()
|
|
||||||
self._load_nodes_to_tree()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Verarbeiten der DB-Daten: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Verarbeiten der Daten:\n{str(e)}")
|
|
||||||
|
|
||||||
def _on_db_query_failed(self, error_msg):
|
|
||||||
"""Wird aufgerufen, wenn die Datenbankabfrage fehlgeschlagen ist."""
|
|
||||||
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
|
|
||||||
return
|
|
||||||
|
|
||||||
error = f"Fehler beim Ausführen der SQL-Abfrage: {error_msg}"
|
|
||||||
logger.error(error)
|
|
||||||
QMessageBox.critical(self, "Fehler", error)
|
|
||||||
|
|
||||||
def _on_db_query_canceled(self):
|
|
||||||
"""Wird aufgerufen, wenn der Benutzer die Abfrage abbricht."""
|
|
||||||
logger.info("Datenbankabfrage vom Benutzer abgebrochen")
|
|
||||||
|
|
||||||
def _cleanup_db_query(self):
|
|
||||||
"""Räumt nach der Datenbankabfrage auf."""
|
|
||||||
if hasattr(self, "_db_progress"):
|
|
||||||
self._db_progress.canceled.disconnect(self._on_db_query_canceled)
|
|
||||||
self._db_progress.close()
|
|
||||||
self._db_progress.deleteLater()
|
|
||||||
del self._db_progress
|
|
||||||
if hasattr(self, "_db_query_thread"):
|
|
||||||
self._db_query_thread.deleteLater()
|
|
||||||
del self._db_query_thread
|
|
||||||
|
|
||||||
def _get_database_config(self, db_id):
|
|
||||||
"""
|
|
||||||
Holt die Datenbank-Konfiguration anhand der ID.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_id: ID der PostgreSQL-Datenbank
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PostgreSqlDb|None: Die Datenbank-Konfiguration oder None
|
|
||||||
"""
|
|
||||||
for db in app_settings.postgresql_dbs:
|
|
||||||
if db.id == db_id:
|
|
||||||
return db
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _prepare_sql_query(self, db_config):
|
|
||||||
"""
|
|
||||||
Bereitet SQL-Abfrage und Connection-String vor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_config: PostgreSQL-Datenbank-Konfiguration
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# PyInstaller entpackt Ressourcen nach sys._MEIPASS;
|
|
||||||
# im Entwicklungsmodus liegt die Datei relativ zum Repo-Root
|
|
||||||
if hasattr(sys, "_MEIPASS"):
|
|
||||||
sql_file_path = Path(sys._MEIPASS) / "res" / "data.sql" # type: ignore[attr-defined]
|
|
||||||
else:
|
|
||||||
sql_file_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
|
|
||||||
if not sql_file_path.exists():
|
|
||||||
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
with open(sql_file_path, "r", encoding="utf-8") as f:
|
|
||||||
sql_query = f.read()
|
|
||||||
|
|
||||||
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
|
||||||
|
|
||||||
connection_string = (
|
|
||||||
"postgresql://"
|
|
||||||
f"{db_config.username}:"
|
|
||||||
f"{db_config.password}@"
|
|
||||||
f"{db_config.host}:"
|
|
||||||
f"{db_config.port}/"
|
|
||||||
f"{db_config.database}?"
|
|
||||||
f"sslmode={db_config.ssl_mode.value}"
|
|
||||||
f"&connect_timeout={db_config.timeout}"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
|
|
||||||
|
|
||||||
return sql_query, connection_string
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Vorbereiten der SQL-Abfrage: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
QMessageBox.critical(self, "Fehler", error_msg)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def _process_sql_data(self, df):
|
|
||||||
"""
|
|
||||||
Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
df: Polars DataFrame mit den SQL-Ergebnissen
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[TreeNode]: Liste der erstellten Root-Nodes
|
|
||||||
"""
|
|
||||||
import polars as pl
|
|
||||||
|
|
||||||
try:
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
# Gruppiere die Daten wie in readCsv.py
|
|
||||||
ebene_1 = df.group_by(["reporttyp", "reporttyp_bez"]).len()
|
|
||||||
ebene_2 = df.group_by(["reporttyp", "report", "report_bez"]).len()
|
|
||||||
ebene_3 = df.group_by(["reporttyp", "report", "repfile", "repfile_bez", "xsl_datei"]).len()
|
|
||||||
|
|
||||||
group_time = time.time() - start_time
|
|
||||||
logger.debug(f"Performance: Gruppierung in {group_time:.3f}s")
|
|
||||||
|
|
||||||
new_nodes = []
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
# Erstelle Node-Struktur wie in readCsv.py
|
|
||||||
for r1 in ebene_1.rows(named=True):
|
|
||||||
tn_1 = TreeNode(id=(r1["reporttyp"],), bez=r1["reporttyp_bez"], children=[])
|
|
||||||
r1_children = ebene_2.filter(pl.col("reporttyp") == r1["reporttyp"])
|
|
||||||
|
|
||||||
for r2 in r1_children.rows(named=True):
|
|
||||||
tn_2 = TreeNode(id=(r2["reporttyp"], r2["report"]), bez=r2["report_bez"], children=[])
|
|
||||||
r2_children = ebene_3.filter(
|
|
||||||
(pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"])
|
|
||||||
)
|
|
||||||
|
|
||||||
for r3 in r2_children.rows(named=True):
|
|
||||||
x = XslFile(
|
|
||||||
id=(r3["reporttyp"], r3["report"], r3["repfile"]),
|
|
||||||
bez=r3["repfile_bez"],
|
|
||||||
xsl_file=Path(r3["xsl_datei"]),
|
|
||||||
xmls=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
tn_2.children.append(x)
|
|
||||||
tn_1.children.append(tn_2)
|
|
||||||
new_nodes.append(tn_1)
|
|
||||||
|
|
||||||
nodes_time = time.time() - start_time
|
|
||||||
logger.debug(f"Performance: Node-Erstellung in {nodes_time:.3f}s")
|
|
||||||
logger.info(f"Erstellt: {len(new_nodes)} Root-Nodes")
|
|
||||||
|
|
||||||
return new_nodes
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Verarbeiten der SQL-Daten: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _merge_nodes_with_existing(self, new_nodes):
|
|
||||||
"""
|
|
||||||
Merged neue Nodes mit vorhandenen Nodes basierend auf IDs.
|
|
||||||
Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_nodes: Liste der neuen Nodes
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info("Merge neue Nodes mit vorhandenen...")
|
|
||||||
|
|
||||||
# Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff
|
|
||||||
new_nodes_dict = {}
|
|
||||||
self._build_nodes_dict(new_nodes, new_nodes_dict)
|
|
||||||
|
|
||||||
logger.debug(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge")
|
|
||||||
|
|
||||||
# Merge mit vorhandenen Nodes
|
|
||||||
if self.pdf_project and self.pdf_project.nodes:
|
|
||||||
self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict)
|
|
||||||
|
|
||||||
# Füge komplett neue Root-Nodes hinzu
|
|
||||||
if self.pdf_project and self.pdf_project.nodes:
|
|
||||||
existing_root_ids = {node.id for node in self.pdf_project.nodes}
|
|
||||||
for new_node in new_nodes:
|
|
||||||
if new_node.id not in existing_root_ids:
|
|
||||||
self.pdf_project.nodes.append(new_node)
|
|
||||||
logger.info(f"Neue Root-Node hinzugefügt: {new_node.bez}")
|
|
||||||
elif self.pdf_project:
|
|
||||||
# Wenn keine Nodes vorhanden sind, füge alle neuen Nodes hinzu
|
|
||||||
self.pdf_project.nodes = new_nodes
|
|
||||||
logger.info(f"Alle {len(new_nodes)} Root-Nodes hinzugefügt (keine vorhandenen Nodes)")
|
|
||||||
|
|
||||||
logger.info("Merge abgeschlossen")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Mergen der Nodes: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _build_nodes_dict(self, nodes, nodes_dict):
|
|
||||||
"""
|
|
||||||
Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nodes: Liste der Nodes
|
|
||||||
nodes_dict: Dictionary zum Füllen
|
|
||||||
"""
|
|
||||||
for node in nodes:
|
|
||||||
nodes_dict[node.id] = node
|
|
||||||
|
|
||||||
if isinstance(node, TreeNode) and node.children:
|
|
||||||
self._build_nodes_dict(node.children, nodes_dict)
|
|
||||||
|
|
||||||
def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict):
|
|
||||||
"""
|
|
||||||
Merged rekursiv vorhandene Nodes mit neuen Nodes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
existing_nodes: Liste der vorhandenen Nodes
|
|
||||||
new_nodes_dict: Dictionary der neuen Nodes
|
|
||||||
"""
|
|
||||||
for existing_node in existing_nodes:
|
|
||||||
if existing_node.id in new_nodes_dict:
|
|
||||||
new_node = new_nodes_dict[existing_node.id]
|
|
||||||
|
|
||||||
# Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
|
|
||||||
if existing_node.bez != new_node.bez:
|
|
||||||
logger.info(
|
|
||||||
f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'"
|
|
||||||
)
|
|
||||||
existing_node.bez = new_node.bez
|
|
||||||
|
|
||||||
# Für XslFile: Aktualisiere xsl_file Pfad
|
|
||||||
if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile):
|
|
||||||
if existing_node.xsl_file != new_node.xsl_file:
|
|
||||||
logger.info(
|
|
||||||
f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'"
|
|
||||||
)
|
|
||||||
existing_node.xsl_file = new_node.xsl_file
|
|
||||||
|
|
||||||
# Rekursiv für Knoten (nur bei TreeNode)
|
|
||||||
if isinstance(existing_node, TreeNode) and existing_node.children:
|
|
||||||
self._merge_nodes_recursive(existing_node.children, new_nodes_dict)
|
|
||||||
|
|
||||||
# Füge neue Knoten hinzu, die noch nicht existieren
|
|
||||||
if existing_node.id in new_nodes_dict:
|
|
||||||
new_node = new_nodes_dict[existing_node.id]
|
|
||||||
if isinstance(new_node, TreeNode) and new_node.children:
|
|
||||||
existing_child_ids = {child.id for child in existing_node.children}
|
|
||||||
for new_child in new_node.children:
|
|
||||||
if new_child.id not in existing_child_ids:
|
|
||||||
existing_node.children.append(new_child)
|
|
||||||
logger.info(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}")
|
|
||||||
|
|
||||||
def _check_and_remove_obsolete_entries(self, new_nodes: list[TreeNode]) -> None:
|
|
||||||
"""
|
|
||||||
Prüft nach dem Merge ob XslFile-Einträge nicht mehr in der DB vorhanden sind.
|
|
||||||
Zeigt einen Bestätigungsdialog und entfernt veraltete Einträge inklusive
|
|
||||||
PDF- und optionaler XML-Bereinigung.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_nodes: Frisch aus der DB geladene Nodes (Ergebnis von _process_sql_data)
|
|
||||||
"""
|
|
||||||
if not self.pdf_project or not self.pdf_project.nodes:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
db_xsl_ids = extract_db_xsl_ids(new_nodes)
|
|
||||||
obsolete_groups = find_obsolete_xsl_entries(self.pdf_project.nodes, db_xsl_ids)
|
|
||||||
|
|
||||||
if not obsolete_groups:
|
|
||||||
logger.debug("Keine veralteten Einträge gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_count = sum(len(g.xsl_entries) for g in obsolete_groups)
|
|
||||||
logger.info(f"{total_count} veraltete XSL-Einträge gefunden")
|
|
||||||
|
|
||||||
dialog = ObsoleteEntriesDialog(self, obsolete_groups)
|
|
||||||
if dialog.exec() != ObsoleteEntriesDialog.DialogCode.Accepted:
|
|
||||||
logger.info("Entfernung veralteter Einträge vom Benutzer abgebrochen")
|
|
||||||
return
|
|
||||||
|
|
||||||
delete_xml = dialog.delete_xml_files()
|
|
||||||
|
|
||||||
# Phase 1: XslFiles aus dem Datenmodell entfernen
|
|
||||||
# WICHTIG: Zuerst entfernen, damit _is_xml_xsl_combination_used_elsewhere
|
|
||||||
# und _is_xml_file_used_elsewhere die gelöschten Einträge nicht mehr sehen
|
|
||||||
# (gleiche Reihenfolge wie in _delete_tree_node)
|
|
||||||
for group in obsolete_groups:
|
|
||||||
parent = group.parent_node
|
|
||||||
obsolete_xsl_ids = {id(entry.xsl_file) for entry in group.xsl_entries}
|
|
||||||
parent.children = [c for c in parent.children if id(c) not in obsolete_xsl_ids]
|
|
||||||
logger.info(f"{len(group.xsl_entries)} XSL-Einträge aus '{' > '.join(group.node_path)}' entfernt")
|
|
||||||
|
|
||||||
# Phase 2: PDF-Bereinigung für alle entfernten XslFiles
|
|
||||||
total_deleted_pdfs = 0
|
|
||||||
for group in obsolete_groups:
|
|
||||||
for entry in group.xsl_entries:
|
|
||||||
xsl_id = entry.xsl_file.id
|
|
||||||
for xml_file_obj in entry.xsl_file.xmls:
|
|
||||||
is_used = self._is_xml_xsl_combination_used_elsewhere(xml_file_obj.xml, xsl_id, entry.xsl_file)
|
|
||||||
if not is_used:
|
|
||||||
total_deleted_pdfs += self._delete_pdf_files_for_xml_xsl_combination(
|
|
||||||
xml_file_obj.xml, xsl_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if total_deleted_pdfs > 0:
|
|
||||||
logger.info(f"{total_deleted_pdfs} PDF-Datei(en) bereinigt")
|
|
||||||
|
|
||||||
# Phase 3: Leere TreeNodes bereinigen (bottom-up durch rekursiven Aufruf)
|
|
||||||
self.pdf_project.nodes = remove_empty_tree_nodes(self.pdf_project.nodes)
|
|
||||||
|
|
||||||
# Phase 4: Optionale physische XML-Löschung
|
|
||||||
if delete_xml:
|
|
||||||
unused_xml = collect_unused_xml_files(
|
|
||||||
obsolete_groups,
|
|
||||||
Path(self.project.project_dir),
|
|
||||||
self._is_xml_file_used_elsewhere,
|
|
||||||
)
|
|
||||||
for _rel, xml_abs in unused_xml:
|
|
||||||
try:
|
|
||||||
xml_abs.unlink()
|
|
||||||
logger.info(f"Physische XML-Datei gelöscht: {xml_abs}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte XML-Datei nicht löschen: {xml_abs} - {e}")
|
|
||||||
|
|
||||||
logger.info(f"Bereinigung abgeschlossen: {total_count} veraltete Einträge entfernt")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Entfernen veralteter Einträge: {str(e)}"
|
|
||||||
logger.exception(error_msg)
|
|
||||||
QMessageBox.critical(self, "Fehler", error_msg)
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
"""
|
|
||||||
DragDropMixin - Mixin für Drag-and-Drop-Funktionalität.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden zur Verarbeitung von Drag-and-Drop-Operationen
|
|
||||||
für das MainWindow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtGui import QDragEnterEvent, QDropEvent
|
|
||||||
from PySide6.QtWidgets import QMessageBox, QProgressBar
|
|
||||||
|
|
||||||
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
|
|
||||||
from ui.threads import XmlBatchProcessingThread
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DragDropMixin:
|
|
||||||
"""
|
|
||||||
Mixin für Drag-and-Drop-Funktionalität.
|
|
||||||
|
|
||||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
|
||||||
- self.ui: Das UI-Objekt mit treeWidget
|
|
||||||
- self.project: Das aktuelle Projekt
|
|
||||||
- self.pdf_project: Die Projekt-Daten (ProjectData)
|
|
||||||
- self.batch_processing_thread: Thread für Batch-Verarbeitung
|
|
||||||
- self.batch_progress_bar: QProgressBar für Batch-Fortschritt
|
|
||||||
- self._save_project_settings(): Methode zum Speichern der Projekt-Einstellungen
|
|
||||||
- self._load_nodes_to_tree(): Methode zum Laden der Nodes in den Tree
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _setup_drag_drop(self):
|
|
||||||
"""Aktiviert Drag&Drop für das TreeWidget."""
|
|
||||||
try:
|
|
||||||
# Aktiviere Drag&Drop für das TreeWidget
|
|
||||||
self.ui.treeWidget.setAcceptDrops(True)
|
|
||||||
self.ui.treeWidget.setDragDropMode(self.ui.treeWidget.DragDropMode.DropOnly)
|
|
||||||
|
|
||||||
# Überschreibe die Drag&Drop-Events
|
|
||||||
self.ui.treeWidget.dragEnterEvent = self.tree_drag_enter_event
|
|
||||||
self.ui.treeWidget.dragMoveEvent = self.tree_drag_move_event
|
|
||||||
self.ui.treeWidget.dropEvent = self.tree_drop_event
|
|
||||||
|
|
||||||
logger.debug("Drag&Drop für TreeWidget aktiviert")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Aktivieren von Drag&Drop: {e}")
|
|
||||||
|
|
||||||
def tree_drag_enter_event(self, event: QDragEnterEvent):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget beginnt.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Das Drag-Enter-Event
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Prüfe ob URLs (Dateien) gedraggt werden
|
|
||||||
if event.mimeData().hasUrls():
|
|
||||||
urls = event.mimeData().urls()
|
|
||||||
|
|
||||||
# Prüfe ob mindestens eine XML-Datei dabei ist
|
|
||||||
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
|
|
||||||
|
|
||||||
if xml_files:
|
|
||||||
event.acceptProposedAction()
|
|
||||||
logger.debug(f"Drag-Enter akzeptiert: {len(xml_files)} XML-Dateien")
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
logger.debug("Drag-Enter ignoriert: Keine XML-Dateien")
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
logger.debug("Drag-Enter ignoriert: Keine URLs")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler in tree_drag_enter_event: {e}")
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
def tree_drag_move_event(self, event):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget bewegt wird.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Das Drag-Move-Event
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Prüfe ob URLs (Dateien) gedraggt werden
|
|
||||||
if event.mimeData().hasUrls():
|
|
||||||
urls = event.mimeData().urls()
|
|
||||||
|
|
||||||
# Prüfe ob mindestens eine XML-Datei dabei ist
|
|
||||||
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
|
|
||||||
|
|
||||||
if xml_files:
|
|
||||||
event.acceptProposedAction()
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler in tree_drag_move_event: {e}")
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
def tree_drop_event(self, event: QDropEvent):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn Dateien auf das TreeWidget gedroppt werden.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Das Drop-Event
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Prüfe ob ein Projekt geladen ist
|
|
||||||
if not hasattr(self, "project") or not self.project:
|
|
||||||
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
||||||
event.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
|
||||||
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
|
|
||||||
event.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole die URLs aus dem Drop-Event
|
|
||||||
if not event.mimeData().hasUrls():
|
|
||||||
event.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
urls = event.mimeData().urls()
|
|
||||||
xml_files = []
|
|
||||||
|
|
||||||
# Sammle alle XML-Dateien
|
|
||||||
for url in urls:
|
|
||||||
file_path = url.toLocalFile()
|
|
||||||
if file_path.lower().endswith(".xml"):
|
|
||||||
xml_files.append(Path(file_path))
|
|
||||||
|
|
||||||
if not xml_files:
|
|
||||||
QMessageBox.information(self, "Information", "Keine XML-Dateien zum Hinzufügen gefunden.")
|
|
||||||
event.ignore()
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Drop-Event: {len(xml_files)} XML-Dateien erkannt")
|
|
||||||
|
|
||||||
# Verarbeite alle XML-Dateien mit optionalem "Alle zuordnen" Feature
|
|
||||||
self._handle_multiple_xml_files_drop(xml_files)
|
|
||||||
|
|
||||||
event.acceptProposedAction()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Verarbeiten des Drop-Events: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
QMessageBox.critical(self, "Fehler", error_msg)
|
|
||||||
event.ignore()
|
|
||||||
|
|
||||||
def _handle_multiple_xml_files_drop(self, xml_files: list):
|
|
||||||
"""
|
|
||||||
Verarbeitet mehrere XML-Dateien asynchron per Drag&Drop.
|
|
||||||
Zeigt einen Dialog zur Auswahl der XSL-Knoten und startet dann die Batch-Verarbeitung im Hintergrund.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_files: Liste von Pfaden zu XML-Dateien
|
|
||||||
"""
|
|
||||||
if not xml_files:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Prüfe ob Projekt-Nodes verfügbar sind
|
|
||||||
if not self.pdf_project or not self.pdf_project.nodes:
|
|
||||||
QMessageBox.warning(self, "Warnung", "Keine Projekt-Nodes verfügbar für die Zuordnung.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Zeige Dialog für die erste Datei
|
|
||||||
dialog = XmlToXslAssignDialog(parent=self, xml_file_path=xml_files[0], project_nodes=self.pdf_project.nodes)
|
|
||||||
|
|
||||||
if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
|
|
||||||
logger.debug("Dialog abgebrochen - keine Dateien verarbeitet")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole die ausgewählten XSL-Knoten
|
|
||||||
selected_xsl_nodes = dialog.get_selected_xsl_nodes()
|
|
||||||
|
|
||||||
if not selected_xsl_nodes:
|
|
||||||
logger.warning("Keine XSL-Knoten ausgewählt")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prüfe ob "Alle zuordnen" aktiviert wurde
|
|
||||||
apply_to_all = dialog.is_apply_to_all()
|
|
||||||
|
|
||||||
# Bestimme welche Dateien verarbeitet werden sollen
|
|
||||||
files_to_process = xml_files if apply_to_all else [xml_files[0]]
|
|
||||||
|
|
||||||
# Stoppe vorherigen Batch-Thread falls noch aktiv
|
|
||||||
if self.batch_processing_thread and self.batch_processing_thread.isRunning():
|
|
||||||
self.batch_processing_thread.quit()
|
|
||||||
self.batch_processing_thread.wait()
|
|
||||||
|
|
||||||
# Zusätzliche Sicherheitsprüfung für project_dir
|
|
||||||
if not self.project or not self.project.project_dir:
|
|
||||||
QMessageBox.warning(self, "Fehler", "Projekt-Verzeichnis ist nicht verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle und starte neuen Batch-Verarbeitungs-Thread
|
|
||||||
self.batch_processing_thread = XmlBatchProcessingThread(
|
|
||||||
xml_files=files_to_process,
|
|
||||||
selected_xsl_nodes=selected_xsl_nodes,
|
|
||||||
project_dir=Path(self.project.project_dir),
|
|
||||||
pdf_project=self.pdf_project,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verbinde Signale
|
|
||||||
self.batch_processing_thread.progress_update.connect(self._on_batch_progress_update)
|
|
||||||
self.batch_processing_thread.processing_finished.connect(self._on_batch_processing_finished)
|
|
||||||
|
|
||||||
# Zeige Progressbar
|
|
||||||
self._show_batch_progress_bar(len(files_to_process))
|
|
||||||
|
|
||||||
# Starte Thread
|
|
||||||
self.batch_processing_thread.start()
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Batch-Verarbeitung von {len(files_to_process)} Datei(en) gestartet (apply_to_all={apply_to_all})"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Starten der Batch-Verarbeitung: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
QMessageBox.critical(self, "Fehler", error_msg)
|
|
||||||
|
|
||||||
def _show_batch_progress_bar(self, total_files: int):
|
|
||||||
"""
|
|
||||||
Zeigt einen Progressbar in der Statusbar für die Batch-Verarbeitung.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
total_files: Gesamtanzahl der zu verarbeitenden Dateien
|
|
||||||
"""
|
|
||||||
if self.batch_progress_bar is None:
|
|
||||||
self.batch_progress_bar = QProgressBar()
|
|
||||||
self.batch_progress_bar.setMaximumHeight(20)
|
|
||||||
self.batch_progress_bar.setMaximumWidth(300)
|
|
||||||
|
|
||||||
self.batch_progress_bar.setMinimum(0)
|
|
||||||
self.batch_progress_bar.setMaximum(total_files)
|
|
||||||
self.batch_progress_bar.setValue(0)
|
|
||||||
self.batch_progress_bar.setFormat("%v/%m Dateien")
|
|
||||||
|
|
||||||
# Füge Progressbar zur Statusbar hinzu
|
|
||||||
self.statusBar().addPermanentWidget(self.batch_progress_bar)
|
|
||||||
self.batch_progress_bar.show()
|
|
||||||
|
|
||||||
def _hide_batch_progress_bar(self):
|
|
||||||
"""Versteckt und entfernt den Progressbar aus der Statusbar."""
|
|
||||||
if self.batch_progress_bar:
|
|
||||||
self.statusBar().removeWidget(self.batch_progress_bar)
|
|
||||||
self.batch_progress_bar.hide()
|
|
||||||
|
|
||||||
def _on_batch_progress_update(self, current: int, total: int, current_file: str):
|
|
||||||
"""
|
|
||||||
Wird aufgerufen wenn der Batch-Thread einen Fortschritt meldet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current: Aktuelle Dateinummer
|
|
||||||
total: Gesamtanzahl der Dateien
|
|
||||||
current_file: Name der aktuellen Datei
|
|
||||||
"""
|
|
||||||
if self.batch_progress_bar:
|
|
||||||
self.batch_progress_bar.setValue(current)
|
|
||||||
|
|
||||||
self.statusBar().showMessage(f"Verarbeite: {current_file} ({current}/{total})")
|
|
||||||
|
|
||||||
def _on_batch_processing_finished(self, stats: dict):
|
|
||||||
"""
|
|
||||||
Wird aufgerufen wenn die Batch-Verarbeitung abgeschlossen ist.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Verstecke Progressbar
|
|
||||||
self._hide_batch_progress_bar()
|
|
||||||
|
|
||||||
# Speichere Projekt-Einstellungen
|
|
||||||
if stats["processed"] > 0:
|
|
||||||
self._save_project_settings()
|
|
||||||
|
|
||||||
# Aktualisiere Tree
|
|
||||||
self._load_nodes_to_tree()
|
|
||||||
|
|
||||||
# Zeige Zusammenfassungsdialog
|
|
||||||
self._show_drop_summary_dialog(stats)
|
|
||||||
|
|
||||||
# Statusbar-Nachricht
|
|
||||||
self.statusBar().showMessage(
|
|
||||||
f"Batch-Verarbeitung abgeschlossen: {stats['processed']}/{stats['total']} Dateien", 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Abschließen der Batch-Verarbeitung: {e}")
|
|
||||||
|
|
||||||
def _show_drop_summary_dialog(self, stats: dict):
|
|
||||||
"""
|
|
||||||
Zeigt einen Zusammenfassungsdialog über die verarbeiteten XML-Dateien.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
|
|
||||||
"""
|
|
||||||
# Erstelle Zusammenfassungstext
|
|
||||||
summary_lines = []
|
|
||||||
summary_lines.append("Verarbeitung abgeschlossen:\n")
|
|
||||||
summary_lines.append(f"Gesamt: {stats['total']} Datei(en)")
|
|
||||||
summary_lines.append(f"Verarbeitet: {stats['processed']} Datei(en)")
|
|
||||||
|
|
||||||
if stats["new_added"] > 0:
|
|
||||||
summary_lines.append(f"Neu hinzugefuegt: {stats['new_added']} Datei(en)")
|
|
||||||
|
|
||||||
if stats["existing_added"] > 0:
|
|
||||||
summary_lines.append(f"Vorhandene zugeordnet: {stats['existing_added']} Datei(en)")
|
|
||||||
|
|
||||||
if stats["already_assigned"] > 0:
|
|
||||||
summary_lines.append(f"Bereits zugeordnet: {stats['already_assigned']} Datei(en)")
|
|
||||||
|
|
||||||
if stats["cancelled"] > 0:
|
|
||||||
summary_lines.append(f"Abgebrochen: {stats['cancelled']} Datei(en)")
|
|
||||||
|
|
||||||
if stats["renamed_files"]:
|
|
||||||
summary_lines.append("\nUmbenannte Dateien:")
|
|
||||||
for renamed in stats["renamed_files"]:
|
|
||||||
summary_lines.append(f" - {renamed}")
|
|
||||||
|
|
||||||
if stats["errors"] > 0:
|
|
||||||
summary_lines.append(f"\nFehler: {stats['errors']}")
|
|
||||||
for error_msg in stats["error_messages"][:5]: # Zeige max. 5 Fehler
|
|
||||||
summary_lines.append(f" - {error_msg}")
|
|
||||||
if len(stats["error_messages"]) > 5:
|
|
||||||
summary_lines.append(f" ... und {len(stats['error_messages']) - 5} weitere Fehler")
|
|
||||||
|
|
||||||
summary_text = "\n".join(summary_lines)
|
|
||||||
|
|
||||||
# Wähle Icon basierend auf Erfolg
|
|
||||||
if stats["errors"] > 0:
|
|
||||||
QMessageBox.warning(self, "Verarbeitung mit Fehlern abgeschlossen", summary_text)
|
|
||||||
elif stats["cancelled"] > 0:
|
|
||||||
QMessageBox.information(self, "Verarbeitung abgebrochen", summary_text)
|
|
||||||
else:
|
|
||||||
QMessageBox.information(self, "Verarbeitung erfolgreich", summary_text)
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
"""
|
|
||||||
HashCalculationMixin - Mixin für Hash-Berechnung und XML-Dateiverwaltung.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden zur blake2b-Hash-Berechnung,
|
|
||||||
XML-Datei-Zuordnung und Duplikatserkennung für das MainWindow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from conf import TreeNode, XslFile, XmlFile
|
|
||||||
from ui.threads import XmlHashCalculatorThread
|
|
||||||
from utils import calculate_blake2b_hash
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HashCalculationMixin:
|
|
||||||
"""
|
|
||||||
Mixin für Hash-Berechnung und XML-Dateiverwaltung.
|
|
||||||
|
|
||||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
|
||||||
- self.project: Das aktuelle Projekt
|
|
||||||
- self.pdf_project: Die Projekt-Daten (ProjectData)
|
|
||||||
- self.hash_calculator_thread: Thread für Hash-Berechnung
|
|
||||||
- self._save_project_settings(): Methode zum Speichern der Projekt-Einstellungen
|
|
||||||
- self._load_nodes_to_tree(): Methode zum Laden der Nodes in den Tree
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _assign_xml_to_xsl_nodes(self, xml_file_path: Path, selected_xsl_nodes: list):
|
|
||||||
"""
|
|
||||||
Ordnet eine XML-Datei den ausgewählten XSL-Knoten zu.
|
|
||||||
Implementiert Hash-basierte Duplikatserkennung und intelligente Dateinamen-Verwaltung.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur XML-Datei
|
|
||||||
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Statistiken über die Verarbeitung
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu")
|
|
||||||
|
|
||||||
# 1. Hash für die neue XML-Datei berechnen
|
|
||||||
file_hash = self._calculate_hash_for_file(xml_file_path)
|
|
||||||
if not file_hash:
|
|
||||||
logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")
|
|
||||||
|
|
||||||
# 2. Prüfe ob eine XML-Datei mit gleichem Hash bereits im Projekt existiert
|
|
||||||
existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None
|
|
||||||
|
|
||||||
if existing_xml:
|
|
||||||
# 3. Hash-Match gefunden: Ordne vorhandene XML-Datei zu
|
|
||||||
logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}")
|
|
||||||
return self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
|
|
||||||
else:
|
|
||||||
# 4. Kein Hash-Match: Verarbeite als neue XML-Datei
|
|
||||||
logger.info(f"Keine Hash-Duplikate gefunden für {xml_file_path.name}, verarbeite als neue Datei")
|
|
||||||
return self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
return {"status": "error", "error_msg": error_msg}
|
|
||||||
|
|
||||||
def _start_xml_hash_calculation(self):
|
|
||||||
"""
|
|
||||||
Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
|
||||||
logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sammle alle XML-Dateien aus dem Projekt
|
|
||||||
xml_files = self._collect_all_xml_files()
|
|
||||||
|
|
||||||
if not xml_files:
|
|
||||||
logger.debug("Keine XML-Dateien für Hash-Berechnung gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Starte Hash-Berechnung für {len(xml_files)} XML-Dateien")
|
|
||||||
|
|
||||||
# Prüfe ob Projekt verfügbar ist
|
|
||||||
if not self.project or not self.project.project_dir:
|
|
||||||
logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Stoppe vorherigen Thread falls noch aktiv
|
|
||||||
if self.hash_calculator_thread and self.hash_calculator_thread.isRunning():
|
|
||||||
self.hash_calculator_thread.quit()
|
|
||||||
self.hash_calculator_thread.wait()
|
|
||||||
|
|
||||||
# Erstelle und starte neuen Hash-Berechnungs-Thread
|
|
||||||
self.hash_calculator_thread = XmlHashCalculatorThread(
|
|
||||||
project_dir=Path(self.project.project_dir), xml_files=xml_files
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verbinde Signale
|
|
||||||
self.hash_calculator_thread.hash_calculated.connect(self._on_hash_calculated)
|
|
||||||
self.hash_calculator_thread.calculation_finished.connect(self._on_hash_calculation_finished)
|
|
||||||
self.hash_calculator_thread.error_occurred.connect(self._on_hash_calculation_error)
|
|
||||||
|
|
||||||
# Starte Thread
|
|
||||||
self.hash_calculator_thread.start()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Starten der Hash-Berechnung: {e}")
|
|
||||||
|
|
||||||
def _collect_all_xml_files(self) -> list[XmlFile]:
|
|
||||||
"""
|
|
||||||
Sammelt alle XmlFile-Objekte aus der Projektstruktur.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[XmlFile]: Liste aller gefundenen XML-Dateien
|
|
||||||
"""
|
|
||||||
xml_files: list[XmlFile] = []
|
|
||||||
seen_paths: set = set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.pdf_project and self.pdf_project.nodes:
|
|
||||||
self._collect_xml_files_recursive(self.pdf_project.nodes, xml_files, seen_paths)
|
|
||||||
|
|
||||||
logger.debug(f"Gesammelt: {len(xml_files)} XML-Dateien")
|
|
||||||
return xml_files
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Sammeln der XML-Dateien: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _collect_xml_files_recursive(self, nodes, xml_files: list[XmlFile], seen_paths: set):
|
|
||||||
"""
|
|
||||||
Sammelt rekursiv alle XML-Dateien aus den Nodes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nodes: Liste der zu durchsuchenden Nodes
|
|
||||||
xml_files: Liste zum Sammeln der XML-Dateien
|
|
||||||
seen_paths: Set bereits gesehener Pfade (verhindert Duplikate)
|
|
||||||
"""
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, XslFile) and node.xmls:
|
|
||||||
for xml_file in node.xmls:
|
|
||||||
if xml_file.xml not in seen_paths:
|
|
||||||
xml_files.append(xml_file)
|
|
||||||
seen_paths.add(xml_file.xml)
|
|
||||||
elif isinstance(node, TreeNode) and node.children:
|
|
||||||
self._collect_xml_files_recursive(node.children, xml_files, seen_paths)
|
|
||||||
|
|
||||||
def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str):
|
|
||||||
"""
|
|
||||||
Wird aufgerufen, wenn ein Hash-Wert berechnet wurde.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file: Das XmlFile-Objekt
|
|
||||||
hash_value: Der berechnete Hash-Wert mit Präfix
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Setze den Hash-Wert
|
|
||||||
xml_file.hashsum = hash_value
|
|
||||||
logger.debug(f"Hash gesetzt für {xml_file.xml}: {hash_value}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Setzen des Hash-Werts: {e}")
|
|
||||||
|
|
||||||
def _on_hash_calculation_finished(self, processed_count: int, total_count: int):
|
|
||||||
"""
|
|
||||||
Wird aufgerufen, wenn die Hash-Berechnung abgeschlossen ist.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
processed_count: Anzahl der verarbeiteten Dateien
|
|
||||||
total_count: Gesamtanzahl der Dateien
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info(f"Hash-Berechnung abgeschlossen: {processed_count}/{total_count} Dateien verarbeitet")
|
|
||||||
|
|
||||||
# Speichere die aktualisierten Projekt-Einstellungen
|
|
||||||
if processed_count > 0:
|
|
||||||
self._save_project_settings()
|
|
||||||
logger.info("Projekt-Einstellungen mit neuen Hash-Werten gespeichert")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Abschließen der Hash-Berechnung: {e}")
|
|
||||||
|
|
||||||
def _on_hash_calculation_error(self, xml_file_path: str, error_message: str):
|
|
||||||
"""
|
|
||||||
Wird aufgerufen, wenn ein Fehler bei der Hash-Berechnung auftritt.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur XML-Datei
|
|
||||||
error_message: Fehlermeldung
|
|
||||||
"""
|
|
||||||
logger.warning(f"Hash-Berechnungsfehler für {xml_file_path}: {error_message}")
|
|
||||||
|
|
||||||
def _calculate_hash_for_xml_file(self, xml_file: XmlFile):
|
|
||||||
"""
|
|
||||||
Berechnet synchron den Hash für eine einzelne XML-Datei.
|
|
||||||
Wird verwendet beim Hinzufügen neuer XML-Dateien.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file: Das XmlFile-Objekt
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if xml_file.hashsum:
|
|
||||||
logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.project or not self.project.project_dir:
|
|
||||||
logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
xml_file_path = Path(self.project.project_dir) / xml_file.xml
|
|
||||||
hash_value = calculate_blake2b_hash(xml_file_path)
|
|
||||||
if hash_value:
|
|
||||||
xml_file.hashsum = hash_value
|
|
||||||
logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}")
|
|
||||||
|
|
||||||
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
|
|
||||||
"""
|
|
||||||
Sucht eine XML-Datei mit dem angegebenen Hash im gesamten Projekt.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target_hash: Der zu suchende Hash-Wert (mit blake2b: Präfix)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
XmlFile|None: Die gefundene XML-Datei oder None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not target_hash:
|
|
||||||
return None
|
|
||||||
|
|
||||||
all_xml_files = self._collect_all_xml_files()
|
|
||||||
|
|
||||||
for xml_file in all_xml_files:
|
|
||||||
if xml_file.hashsum == target_hash:
|
|
||||||
logger.debug(f"Hash-Match gefunden: {xml_file.xml} hat Hash {target_hash}")
|
|
||||||
return xml_file
|
|
||||||
|
|
||||||
logger.debug(f"Kein Hash-Match für {target_hash} gefunden")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler bei Hash-Suche für {target_hash}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path:
|
|
||||||
"""
|
|
||||||
Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ...
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original_path: Ursprünglicher Dateipfad
|
|
||||||
xml_dir: Ziel-XML-Verzeichnis
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path: Pfad mit alternativem Dateinamen
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
base_name = original_path.stem # "datei"
|
|
||||||
extension = original_path.suffix # ".xml"
|
|
||||||
|
|
||||||
# Sammle einmalig alle verwendeten Dateinamen (Performance-Optimierung)
|
|
||||||
all_xml_files = self._collect_all_xml_files()
|
|
||||||
used_names = {xml_file.xml.name for xml_file in all_xml_files}
|
|
||||||
|
|
||||||
counter = 1
|
|
||||||
while True:
|
|
||||||
new_name = f"{base_name}_{counter}{extension}"
|
|
||||||
new_path = xml_dir / new_name
|
|
||||||
|
|
||||||
# Prüfe sowohl physische Existenz als auch Verwendung im Projekt (optimierter Set-Lookup)
|
|
||||||
if not new_path.exists() and new_name not in used_names:
|
|
||||||
logger.debug(f"Alternativer Dateiname generiert: {new_name}")
|
|
||||||
return new_path
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# Sicherheitsgrenze um Endlosschleifen zu vermeiden
|
|
||||||
if counter > 1000:
|
|
||||||
raise Exception("Zu viele alternative Dateinamen generiert")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Generieren alternativer Dateinamen für {original_path}: {e}")
|
|
||||||
# Fallback: Zeitstempel verwenden
|
|
||||||
timestamp = int(time.time())
|
|
||||||
fallback_name = f"{original_path.stem}_{timestamp}{original_path.suffix}"
|
|
||||||
return xml_dir / fallback_name
|
|
||||||
|
|
||||||
def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
|
|
||||||
"""
|
|
||||||
Prüft ob ein relativer XML-Dateipfad bereits im Projekt verwendet wird.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
relative_xml_path: Relativer Pfad zur XML-Datei (z.B. xml/datei_1.xml)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True wenn der Dateiname bereits verwendet wird
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
all_xml_files = self._collect_all_xml_files()
|
|
||||||
|
|
||||||
for xml_file in all_xml_files:
|
|
||||||
if xml_file.xml == relative_xml_path:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Prüfen der Dateiname-Verwendung für {relative_xml_path}: {e}")
|
|
||||||
return True # Im Zweifelsfall annehmen, dass der Name verwendet wird
|
|
||||||
|
|
||||||
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
|
|
||||||
"""Berechnet synchron den blake2b-Hash für eine Datei."""
|
|
||||||
return calculate_blake2b_hash(file_path)
|
|
||||||
|
|
||||||
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
|
|
||||||
"""
|
|
||||||
Ordnet eine bereits vorhandene XML-Datei (basierend auf Hash-Match) den XSL-Knoten zu.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
existing_xml: Die bereits vorhandene XML-Datei
|
|
||||||
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Statistiken mit 'status', 'added_count', 'existing_file'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
added_count = 0
|
|
||||||
|
|
||||||
for xsl_node in selected_xsl_nodes:
|
|
||||||
# Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
|
|
||||||
already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls)
|
|
||||||
|
|
||||||
if not already_assigned:
|
|
||||||
# Erstelle neue XmlFile-Referenz mit gleichem Pfad und Hash
|
|
||||||
new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum)
|
|
||||||
xsl_node.xmls.append(new_xml_ref)
|
|
||||||
added_count += 1
|
|
||||||
logger.info(f"Vorhandene XML-Datei '{existing_xml.xml}' zu XSL-Node '{xsl_node.bez}' zugeordnet")
|
|
||||||
else:
|
|
||||||
logger.debug(f"XML-Datei '{existing_xml.xml}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")
|
|
||||||
|
|
||||||
if added_count > 0:
|
|
||||||
# Speichere die aktualisierten Projekt-Einstellungen
|
|
||||||
self._save_project_settings()
|
|
||||||
|
|
||||||
# Aktualisiere das TreeWidget
|
|
||||||
self._load_nodes_to_tree()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "existing_added",
|
|
||||||
"added_count": added_count,
|
|
||||||
"existing_file": existing_xml.xml.name,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Zuordnen der vorhandenen XML-Datei: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
return {"status": "error", "error_msg": error_msg}
|
|
||||||
|
|
||||||
def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None):
|
|
||||||
"""
|
|
||||||
Verarbeitet eine neue XML-Datei (kein Hash-Match gefunden).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur neuen XML-Datei
|
|
||||||
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
||||||
file_hash: Berechneter Hash der Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Statistiken mit 'status', 'added_count', 'new_file', 'renamed_from'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Prüfe ob Projekt verfügbar ist
|
|
||||||
if not self.project or not self.project.project_dir:
|
|
||||||
logger.error("Kein Projekt-Verzeichnis für neue XML-Datei verfügbar")
|
|
||||||
return {"status": "error", "error_msg": "Kein Projekt-Verzeichnis verfügbar."}
|
|
||||||
|
|
||||||
# Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert
|
|
||||||
xml_dir = Path(self.project.project_dir) / "xml"
|
|
||||||
xml_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Bestimme den Ziel-Pfad in xml-Ordner
|
|
||||||
target_xml_path = xml_dir / xml_file_path.name
|
|
||||||
|
|
||||||
# Prüfe ob eine Datei mit gleichem Namen bereits existiert
|
|
||||||
if target_xml_path.exists() or self._is_filename_used_in_project(Path("xml") / xml_file_path.name):
|
|
||||||
# Generiere alternative Dateinamen
|
|
||||||
alternative_paths = []
|
|
||||||
for i in range(1, 6): # Generiere bis zu 5 Alternativen
|
|
||||||
alt_path = self._generate_alternative_filename(xml_file_path, xml_dir)
|
|
||||||
if alt_path not in alternative_paths:
|
|
||||||
alternative_paths.append(alt_path)
|
|
||||||
|
|
||||||
# Zeige Dialog zur Auswahl des Dateinamens
|
|
||||||
selected_path = self._show_filename_selection_dialog(xml_file_path.name, alternative_paths)
|
|
||||||
|
|
||||||
if not selected_path:
|
|
||||||
# Benutzer hat abgebrochen
|
|
||||||
return {"status": "cancelled", "added_count": 0}
|
|
||||||
|
|
||||||
target_xml_path = selected_path
|
|
||||||
|
|
||||||
# Kopiere die XML-Datei in den xml-Ordner
|
|
||||||
shutil.copy2(xml_file_path, target_xml_path)
|
|
||||||
logger.info(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}")
|
|
||||||
|
|
||||||
# Erstelle relatives Path zur XML-Datei (relativ zum Projekt-Verzeichnis)
|
|
||||||
relative_xml_path = Path("xml") / target_xml_path.name
|
|
||||||
|
|
||||||
# Füge die XML-Datei zu allen ausgewählten XSL-Knoten hinzu
|
|
||||||
added_count = 0
|
|
||||||
for xsl_node in selected_xsl_nodes:
|
|
||||||
# Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
|
|
||||||
existing_xml = None
|
|
||||||
for xml_file in xsl_node.xmls:
|
|
||||||
if xml_file.xml == relative_xml_path:
|
|
||||||
existing_xml = xml_file
|
|
||||||
break
|
|
||||||
|
|
||||||
if not existing_xml:
|
|
||||||
# Erstelle neues XmlFile-Objekt mit Hash
|
|
||||||
new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash)
|
|
||||||
xsl_node.xmls.append(new_xml_file)
|
|
||||||
added_count += 1
|
|
||||||
logger.info(f"XML-Datei '{target_xml_path.name}' zu XSL-Node '{xsl_node.bez}' hinzugefügt")
|
|
||||||
else:
|
|
||||||
logger.debug(f"XML-Datei '{target_xml_path.name}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")
|
|
||||||
|
|
||||||
if added_count > 0:
|
|
||||||
# Speichere die aktualisierten Projekt-Einstellungen
|
|
||||||
self._save_project_settings()
|
|
||||||
|
|
||||||
# Aktualisiere das TreeWidget
|
|
||||||
self._load_nodes_to_tree()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "new_added",
|
|
||||||
"added_count": added_count,
|
|
||||||
"new_file": target_xml_path.name,
|
|
||||||
"renamed_from": xml_file_path.name if target_xml_path.name != xml_file_path.name else None,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"status": "already_assigned",
|
|
||||||
"added_count": 0,
|
|
||||||
"new_file": target_xml_path.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler beim Verarbeiten der neuen XML-Datei: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
return {"status": "error", "error_msg": error_msg}
|
|
||||||
|
|
||||||
def _show_filename_selection_dialog(self, original_name: str, alternative_paths: list[Path]) -> Path | None:
|
|
||||||
"""
|
|
||||||
Zeigt einen Dialog zur Auswahl eines alternativen Dateinamens.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original_name: Ursprünglicher Dateiname
|
|
||||||
alternative_paths: Liste alternativer Pfade
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path|None: Ausgewählter Pfad oder None bei Abbruch
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from PySide6.QtWidgets import (
|
|
||||||
QDialog,
|
|
||||||
QVBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QRadioButton,
|
|
||||||
QButtonGroup,
|
|
||||||
QPushButton,
|
|
||||||
QHBoxLayout,
|
|
||||||
)
|
|
||||||
|
|
||||||
dialog = QDialog(self)
|
|
||||||
dialog.setWindowTitle("Dateiname auswählen")
|
|
||||||
dialog.setModal(True)
|
|
||||||
dialog.resize(400, 300)
|
|
||||||
|
|
||||||
layout = QVBoxLayout(dialog)
|
|
||||||
|
|
||||||
# Erklärungstext
|
|
||||||
info_label = QLabel(
|
|
||||||
f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n"
|
|
||||||
"Bitte wählen Sie einen alternativen Dateinamen:"
|
|
||||||
)
|
|
||||||
layout.addWidget(info_label)
|
|
||||||
|
|
||||||
# Radio-Buttons für alternative Namen
|
|
||||||
button_group = QButtonGroup(dialog)
|
|
||||||
radio_buttons = []
|
|
||||||
|
|
||||||
for i, alt_path in enumerate(alternative_paths):
|
|
||||||
radio_button = QRadioButton(alt_path.name)
|
|
||||||
if i == 0: # Ersten als Standard auswählen
|
|
||||||
radio_button.setChecked(True)
|
|
||||||
button_group.addButton(radio_button, i)
|
|
||||||
radio_buttons.append(radio_button)
|
|
||||||
layout.addWidget(radio_button)
|
|
||||||
|
|
||||||
# Buttons
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
ok_button = QPushButton("OK")
|
|
||||||
cancel_button = QPushButton("Abbrechen")
|
|
||||||
|
|
||||||
button_layout.addWidget(ok_button)
|
|
||||||
button_layout.addWidget(cancel_button)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
# Event-Handler
|
|
||||||
ok_button.clicked.connect(dialog.accept)
|
|
||||||
cancel_button.clicked.connect(dialog.reject)
|
|
||||||
|
|
||||||
# Dialog anzeigen
|
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
||||||
selected_id = button_group.checkedId()
|
|
||||||
if 0 <= selected_id < len(alternative_paths):
|
|
||||||
return alternative_paths[selected_id]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Anzeigen des Dateiname-Auswahl-Dialogs: {e}")
|
|
||||||
# Fallback: Ersten alternativen Namen verwenden
|
|
||||||
return alternative_paths[0] if alternative_paths else None
|
|
||||||
@@ -1,679 +0,0 @@
|
|||||||
"""
|
|
||||||
PdfViewerMixin - Mixin für PDF-Viewer-Operationen.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden für die PDF-Anzeige und -Vergleich im MainWindow:
|
|
||||||
- PDF-Rendering und -Anzeige
|
|
||||||
- Alpha-Blending und Zoom
|
|
||||||
- Thumbnail-Navigation
|
|
||||||
- Drag-to-Scroll
|
|
||||||
- PDF-Dokument-Management
|
|
||||||
"""
|
|
||||||
|
|
||||||
import gc
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QSize, QTimer, QUrl
|
|
||||||
from PySide6.QtGui import QCursor, QPixmap, QPainter, QDesktopServices
|
|
||||||
from PySide6.QtWidgets import QLabel, QMessageBox, QSpacerItem, QSizePolicy
|
|
||||||
from PySide6.QtPdf import QPdfDocument
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PdfViewerMixin:
|
|
||||||
"""
|
|
||||||
Mixin-Klasse für PDF-Viewer-Operationen.
|
|
||||||
|
|
||||||
Dieses Mixin erwartet, dass die verwendende Klasse folgende Attribute hat:
|
|
||||||
- self.ui: UI-Objekt mit Layouts, Slidern, etc.
|
|
||||||
- self.project: Aktuelles Projekt
|
|
||||||
- self.pdf_documents: Dict für PDF-Dokumente
|
|
||||||
- self.current_rendered_pixmaps: Cache für gerenderte Pixmaps
|
|
||||||
- self.fullsize_label: QLabel für Vollbild-Anzeige
|
|
||||||
- self.thumbnail_to_page: Dict für Thumbnail-zu-Seite-Mapping
|
|
||||||
- self.current_zoom: Aktueller Zoom-Faktor
|
|
||||||
- self.current_page: Aktuelle Seitennummer
|
|
||||||
- self.current_pdf: Aktueller PDF-Dateiname
|
|
||||||
- self.is_dragging: Drag-Status
|
|
||||||
- self.last_drag_position: Letzte Drag-Position
|
|
||||||
- self.drag_threshold: Mindestbewegung für Drag
|
|
||||||
- self.scroll_sensitivity: Scroll-Empfindlichkeit
|
|
||||||
"""
|
|
||||||
|
|
||||||
def render_and_display_page(self, pdf_filename, page_num):
|
|
||||||
"""
|
|
||||||
Rendert und zeigt eine spezifische Seite in der Vollansicht an.
|
|
||||||
Cached die gerenderten Pixmaps für bessere Performance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pdf_filename: Name der PDF-Datei
|
|
||||||
page_num: Seitennummer (0-basiert)
|
|
||||||
"""
|
|
||||||
logger.debug(f"Rendere Seite {page_num + 1} von {pdf_filename}")
|
|
||||||
|
|
||||||
if pdf_filename not in self.pdf_documents:
|
|
||||||
logger.warning(f"PDF-Dokument {pdf_filename} nicht gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
try:
|
|
||||||
docs = self.pdf_documents[pdf_filename]
|
|
||||||
|
|
||||||
# Diff-Seite laden (bestimmt die Abmessungen)
|
|
||||||
diff_doc = docs["diff"]
|
|
||||||
page_size = diff_doc.pagePointSize(page_num)
|
|
||||||
|
|
||||||
# Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF)
|
|
||||||
scale_factor = 2.0
|
|
||||||
render_size = QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor))
|
|
||||||
|
|
||||||
# Diff-Seite rendern (immer vorhanden)
|
|
||||||
diff_image = diff_doc.render(page_num, render_size)
|
|
||||||
diff_pixmap = QPixmap.fromImage(diff_image)
|
|
||||||
|
|
||||||
# Ermittle die Abmessungen für weiße Seiten
|
|
||||||
diff_width = diff_pixmap.width()
|
|
||||||
diff_height = diff_pixmap.height()
|
|
||||||
|
|
||||||
# Ref-Seite prüfen und rendern oder weiße Seite erstellen
|
|
||||||
ref_doc = docs["ref"]
|
|
||||||
if page_num < ref_doc.pageCount():
|
|
||||||
ref_image = ref_doc.render(page_num, render_size)
|
|
||||||
ref_pixmap = QPixmap.fromImage(ref_image)
|
|
||||||
logger.debug(f"Ref-Seite {page_num + 1} gerendert")
|
|
||||||
else:
|
|
||||||
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
||||||
ref_pixmap = QPixmap(diff_width, diff_height)
|
|
||||||
ref_pixmap.fill(Qt.GlobalColor.white)
|
|
||||||
logger.debug(f"Weiße Ref-Seite {page_num + 1} erstellt")
|
|
||||||
|
|
||||||
# New-Seite prüfen und rendern oder weiße Seite erstellen
|
|
||||||
new_doc = docs["new"]
|
|
||||||
if page_num < new_doc.pageCount():
|
|
||||||
new_image = new_doc.render(page_num, render_size)
|
|
||||||
new_pixmap = QPixmap.fromImage(new_image)
|
|
||||||
logger.debug(f"New-Seite {page_num + 1} gerendert")
|
|
||||||
else:
|
|
||||||
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
||||||
new_pixmap = QPixmap(diff_width, diff_height)
|
|
||||||
new_pixmap.fill(Qt.GlobalColor.white)
|
|
||||||
logger.debug(f"Weiße New-Seite {page_num + 1} erstellt")
|
|
||||||
|
|
||||||
# Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen
|
|
||||||
self.current_rendered_pixmaps = {"ref": ref_pixmap, "diff": diff_pixmap, "new": new_pixmap}
|
|
||||||
|
|
||||||
# Aktualisiere aktuelle Seite
|
|
||||||
self.current_page = page_num
|
|
||||||
self.current_pdf = pdf_filename
|
|
||||||
|
|
||||||
# Zeige das Bild mit aktuellem Alpha- und Zoom-Wert an
|
|
||||||
self.update_current_display()
|
|
||||||
|
|
||||||
render_time = time.time() - start_time
|
|
||||||
logger.debug(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Rendern der Seite {page_num + 1}: {e}", exc_info=True)
|
|
||||||
|
|
||||||
def update_current_display(self):
|
|
||||||
"""
|
|
||||||
Aktualisiert die Anzeige der aktuellen Seite basierend auf gecachten Pixmaps.
|
|
||||||
Verwendet für Alpha- und Zoom-Änderungen ohne erneutes PDF-Rendering.
|
|
||||||
"""
|
|
||||||
if not self.current_rendered_pixmaps:
|
|
||||||
logger.warning("Keine gerenderten Pixmaps verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.fullsize_label is None:
|
|
||||||
logger.warning("Fullsize-Label ist nicht verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Hole die gecachten Pixmaps
|
|
||||||
ref_pixmap = self.current_rendered_pixmaps["ref"]
|
|
||||||
diff_pixmap = self.current_rendered_pixmaps["diff"]
|
|
||||||
new_pixmap = self.current_rendered_pixmaps["new"]
|
|
||||||
|
|
||||||
# Erstelle das überlagerte Bild mit aktuellem Alpha-Wert
|
|
||||||
alpha_value = self.ui.alpha.value()
|
|
||||||
layered_pixmap = self.create_layered_pixmap(ref_pixmap, diff_pixmap, new_pixmap, alpha_value)
|
|
||||||
|
|
||||||
# Wende aktuellen Zoom an
|
|
||||||
zoom_factor = self.current_zoom / 100.0
|
|
||||||
if zoom_factor != 1.0:
|
|
||||||
new_width = int(layered_pixmap.width() * zoom_factor)
|
|
||||||
layered_pixmap = layered_pixmap.scaledToWidth(new_width, Qt.TransformationMode.SmoothTransformation)
|
|
||||||
|
|
||||||
# Setze das überlagerte Bild
|
|
||||||
self.fullsize_label.setPixmap(layered_pixmap)
|
|
||||||
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
except RuntimeError as e:
|
|
||||||
# C++-Objekt wurde bereits gelöscht
|
|
||||||
logger.warning(f"Fullsize-Label wurde bereits gelöscht: {e}")
|
|
||||||
self.fullsize_label = None
|
|
||||||
|
|
||||||
def create_layered_pixmap(self, ref_pixmap, diff_pixmap, new_pixmap, alpha_value):
|
|
||||||
"""
|
|
||||||
Erstellt ein übergelagertes Pixmap basierend auf dem Alpha-Wert.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ref_pixmap: Unterste Ebene (ref)
|
|
||||||
diff_pixmap: Mittlere Ebene (diff)
|
|
||||||
new_pixmap: Oberste Ebene (new)
|
|
||||||
alpha_value: Alpha-Wert (-100 bis 100)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
QPixmap: Das überlagerte Bild
|
|
||||||
"""
|
|
||||||
# Verwende die Größe des größten Bildes
|
|
||||||
max_width = max(ref_pixmap.width(), diff_pixmap.width(), new_pixmap.width())
|
|
||||||
max_height = max(ref_pixmap.height(), diff_pixmap.height(), new_pixmap.height())
|
|
||||||
|
|
||||||
# Erstelle ein leeres Pixmap für das Ergebnis
|
|
||||||
result = QPixmap(max_width, max_height)
|
|
||||||
result.fill(Qt.GlobalColor.white)
|
|
||||||
|
|
||||||
painter = QPainter(result)
|
|
||||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
||||||
|
|
||||||
if alpha_value <= 0:
|
|
||||||
# Alpha von -100 bis 0: Übergang von ref zu diff
|
|
||||||
ref_opacity = abs(alpha_value) / 100
|
|
||||||
diff_opacity = 1.0 - abs(alpha_value) / 100.0
|
|
||||||
new_opacity = 0.0
|
|
||||||
else:
|
|
||||||
ref_opacity = 0.0
|
|
||||||
diff_opacity = 1.0 - alpha_value / 100.0
|
|
||||||
new_opacity = alpha_value / 100.0
|
|
||||||
|
|
||||||
# Zeichne die Ebenen mit entsprechender Transparenz
|
|
||||||
if ref_opacity > 0:
|
|
||||||
painter.setOpacity(ref_opacity)
|
|
||||||
painter.drawPixmap(0, 0, ref_pixmap)
|
|
||||||
|
|
||||||
if diff_opacity > 0:
|
|
||||||
painter.setOpacity(diff_opacity)
|
|
||||||
painter.drawPixmap(0, 0, diff_pixmap)
|
|
||||||
|
|
||||||
if new_opacity > 0:
|
|
||||||
painter.setOpacity(new_opacity)
|
|
||||||
painter.drawPixmap(0, 0, new_pixmap)
|
|
||||||
|
|
||||||
painter.end()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _schedule_thumbnail_rendering(self, diff_doc, pdf_basename, thumbnail_labels, page_num):
|
|
||||||
"""Rendert Thumbnails progressiv — ein Thumbnail pro Event-Loop-Iteration."""
|
|
||||||
if page_num >= len(thumbnail_labels):
|
|
||||||
return
|
|
||||||
|
|
||||||
thumbnail = thumbnail_labels[page_num]
|
|
||||||
# Prüfe ob das Label noch existiert (Benutzer könnte inzwischen anderes PDF geöffnet haben)
|
|
||||||
if thumbnail_labels[page_num] not in self.thumbnail_to_page:
|
|
||||||
return
|
|
||||||
|
|
||||||
page_size = diff_doc.pagePointSize(page_num)
|
|
||||||
scale_factor = 200.0 / page_size.width()
|
|
||||||
page_image = diff_doc.render(
|
|
||||||
page_num,
|
|
||||||
QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor)),
|
|
||||||
)
|
|
||||||
thumbnail.setPixmap(QPixmap.fromImage(page_image).scaledToWidth(200, Qt.TransformationMode.SmoothTransformation))
|
|
||||||
thumbnail.setMinimumHeight(0)
|
|
||||||
|
|
||||||
QTimer.singleShot(0, lambda: self._schedule_thumbnail_rendering(diff_doc, pdf_basename, thumbnail_labels, page_num + 1))
|
|
||||||
|
|
||||||
def _clear_layout(self, layout):
|
|
||||||
"""Entfernt alle Widgets aus einem Layout."""
|
|
||||||
if layout is not None:
|
|
||||||
while layout.count():
|
|
||||||
item = layout.takeAt(0)
|
|
||||||
widget = item.widget()
|
|
||||||
if widget is not None:
|
|
||||||
widget.deleteLater()
|
|
||||||
|
|
||||||
def on_alpha_changed(self, alpha_value):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn der Alpha-Slider geändert wird.
|
|
||||||
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
alpha_value: Der neue Alpha-Wert (-100 bis 100)
|
|
||||||
"""
|
|
||||||
logger.debug(f"Alpha geändert auf {alpha_value}")
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
# Verwende gecachte Pixmaps für schnelle Alpha-Änderungen
|
|
||||||
self.update_current_display()
|
|
||||||
alpha_time = time.time() - start_time
|
|
||||||
logger.debug(f"Alpha-Update in {alpha_time:.6f}s")
|
|
||||||
|
|
||||||
def on_thumbnail_clicked(self, event, thumbnail):
|
|
||||||
"""
|
|
||||||
Wird ausgeführt, wenn ein Thumbnail angeklickt wird.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: Das Maus-Event
|
|
||||||
thumbnail: Das geklickte Thumbnail-Label
|
|
||||||
"""
|
|
||||||
page_info = self.thumbnail_to_page.get(thumbnail)
|
|
||||||
if page_info:
|
|
||||||
pdf_filename = page_info["pdf_filename"]
|
|
||||||
page_num = page_info["page_num"]
|
|
||||||
|
|
||||||
logger.debug(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
|
|
||||||
|
|
||||||
# Rendere und zeige die gewählte Seite an
|
|
||||||
self.render_and_display_page(pdf_filename, page_num)
|
|
||||||
|
|
||||||
def apply_zoom(self, zoom_value):
|
|
||||||
"""
|
|
||||||
Wendet den Zoom-Faktor auf das aktuelle Bild an.
|
|
||||||
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
zoom_value: Der neue Zoom-Wert (in Prozent)
|
|
||||||
"""
|
|
||||||
self.current_zoom = zoom_value
|
|
||||||
logger.debug(f"Zoom geändert auf {zoom_value}%")
|
|
||||||
|
|
||||||
# Verwende gecachte Pixmaps für schnelle Zoom-Änderungen
|
|
||||||
self.update_current_display()
|
|
||||||
|
|
||||||
def on_fullsize_mouse_press(self, event, fullsize_label):
|
|
||||||
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild gedrückt wird."""
|
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
|
||||||
self.is_dragging = True
|
|
||||||
self.last_drag_position = event.globalPosition().toPoint()
|
|
||||||
fullsize_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
|
|
||||||
|
|
||||||
def on_fullsize_mouse_move(self, event, fullsize_label):
|
|
||||||
"""Wird ausgeführt, wenn die Maus über einem großen Bild bewegt wird."""
|
|
||||||
if self.is_dragging and self.last_drag_position is not None:
|
|
||||||
current_pos = event.globalPosition().toPoint()
|
|
||||||
delta = current_pos - self.last_drag_position
|
|
||||||
|
|
||||||
if abs(delta.x()) >= self.drag_threshold or abs(delta.y()) >= self.drag_threshold:
|
|
||||||
v_scrollbar = self.ui.scrollArea_2.verticalScrollBar()
|
|
||||||
h_scrollbar = self.ui.scrollArea_2.horizontalScrollBar()
|
|
||||||
|
|
||||||
scroll_delta_y = int(-delta.y() * self.scroll_sensitivity)
|
|
||||||
scroll_delta_x = int(-delta.x() * self.scroll_sensitivity)
|
|
||||||
|
|
||||||
new_v_value = v_scrollbar.value() + scroll_delta_y
|
|
||||||
new_h_value = h_scrollbar.value() + scroll_delta_x
|
|
||||||
|
|
||||||
v_scrollbar.setValue(new_v_value)
|
|
||||||
h_scrollbar.setValue(new_h_value)
|
|
||||||
|
|
||||||
self.last_drag_position = current_pos
|
|
||||||
|
|
||||||
def on_fullsize_mouse_release(self, event, fullsize_label):
|
|
||||||
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild losgelassen wird."""
|
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
|
||||||
self.is_dragging = False
|
|
||||||
self.last_drag_position = None
|
|
||||||
fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
||||||
|
|
||||||
def _load_pdf_for_comparison(self, xml_file_path: Path, xsl_id_str: str):
|
|
||||||
"""
|
|
||||||
Lädt die PDFs (diff, ref, new) einer Transformation in den Vergleichs-Viewer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur XML-Datei (relativ)
|
|
||||||
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not self.project:
|
|
||||||
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ermittle PDF-Dateinamen basierend auf XML und XSL-ID
|
|
||||||
xml_stem = xml_file_path.stem
|
|
||||||
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
||||||
|
|
||||||
# Pfade zu den drei PDFs
|
|
||||||
diff_dir = self.project.project_dir / "diff"
|
|
||||||
ref_dir = self.project.project_dir / "ref"
|
|
||||||
new_dir = self.project.project_dir / "new"
|
|
||||||
|
|
||||||
diff_pdf_path = diff_dir / pdf_basename
|
|
||||||
ref_pdf_path = ref_dir / pdf_basename
|
|
||||||
new_pdf_path = new_dir / pdf_basename
|
|
||||||
|
|
||||||
# Prüfe ob PDFs existieren
|
|
||||||
if not diff_pdf_path.exists():
|
|
||||||
QMessageBox.information(self, "Keine Diff-PDF", f"Diff-PDF nicht gefunden:\n{pdf_basename}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not ref_pdf_path.exists() or not new_pdf_path.exists():
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Fehlende PDFs",
|
|
||||||
f"Ref-PDF oder New-PDF nicht gefunden:\n{pdf_basename}\n\nNur Diff-PDF vorhanden.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Lade PDFs für Vergleich: {pdf_basename}")
|
|
||||||
|
|
||||||
# Entferne bestehende Widgets aus den Layouts
|
|
||||||
self._clear_layout(self.ui.verticalLayout_2)
|
|
||||||
self._clear_layout(self.ui.verticalLayout_3)
|
|
||||||
|
|
||||||
# Setze kompaktes Spacing für Thumbnail-Layout
|
|
||||||
self.ui.verticalLayout_2.setSpacing(5) # Minimaler Abstand zwischen Widgets
|
|
||||||
self.ui.verticalLayout_2.setContentsMargins(0, 0, 0, 0) # Keine Ränder
|
|
||||||
|
|
||||||
# Dicts zurücksetzen
|
|
||||||
self.thumbnail_to_page = {}
|
|
||||||
self.pdf_documents = {}
|
|
||||||
self.current_rendered_pixmaps = None
|
|
||||||
self.fullsize_label = None # Label wurde durch _clear_layout gelöscht
|
|
||||||
|
|
||||||
# Alle drei PDF-Dateien öffnen mit QtPdf
|
|
||||||
diff_doc = QPdfDocument()
|
|
||||||
ref_doc = QPdfDocument()
|
|
||||||
new_doc = QPdfDocument()
|
|
||||||
|
|
||||||
# PDF-Dateien laden
|
|
||||||
diff_doc.load(str(diff_pdf_path))
|
|
||||||
ref_doc.load(str(ref_pdf_path))
|
|
||||||
new_doc.load(str(new_pdf_path))
|
|
||||||
|
|
||||||
# Warten bis PDFs geladen sind
|
|
||||||
if (
|
|
||||||
diff_doc.status() != QPdfDocument.Status.Ready
|
|
||||||
or ref_doc.status() != QPdfDocument.Status.Ready
|
|
||||||
or new_doc.status() != QPdfDocument.Status.Ready
|
|
||||||
):
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der PDFs:\n{pdf_basename}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# PDF-Dokumente speichern
|
|
||||||
self.pdf_documents[pdf_basename] = {"diff": diff_doc, "ref": ref_doc, "new": new_doc}
|
|
||||||
|
|
||||||
# PDF-Pfade für System-Viewer speichern
|
|
||||||
self.current_ref_pdf_path = ref_pdf_path
|
|
||||||
self.current_new_pdf_path = new_pdf_path
|
|
||||||
|
|
||||||
# Buttons zum Öffnen der PDFs im System-Viewer aktivieren
|
|
||||||
self.ui.view_ref_pdf.setEnabled(True)
|
|
||||||
self.ui.view_new_pdf.setEnabled(True)
|
|
||||||
|
|
||||||
# Slider aktivieren
|
|
||||||
self.ui.alpha.setEnabled(True)
|
|
||||||
self.ui.zoom.setEnabled(True)
|
|
||||||
|
|
||||||
logger.info(f"PDFs geladen: {pdf_basename}")
|
|
||||||
logger.info(f" diff: {diff_doc.pageCount()} Seiten")
|
|
||||||
logger.info(f" ref: {ref_doc.pageCount()} Seiten")
|
|
||||||
logger.info(f" new: {new_doc.pageCount()} Seiten")
|
|
||||||
|
|
||||||
# Nehme die Seitenzahl der diff-PDF als Basis
|
|
||||||
max_pages = diff_doc.pageCount()
|
|
||||||
|
|
||||||
# Erstelle Placeholder-Labels für alle Seiten (sofort, ohne Rendern)
|
|
||||||
thumbnail_labels = []
|
|
||||||
for page_num in range(max_pages):
|
|
||||||
thumbnail = QLabel()
|
|
||||||
thumbnail.setObjectName(f"thumbnail_{pdf_basename}_page_{page_num + 1}")
|
|
||||||
thumbnail.setText(f"Seite {page_num + 1}\n…")
|
|
||||||
thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
||||||
thumbnail.setMouseTracking(True)
|
|
||||||
thumbnail.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
thumbnail.setMinimumHeight(150)
|
|
||||||
self.ui.verticalLayout_2.addWidget(thumbnail)
|
|
||||||
|
|
||||||
thumbnail_info = QLabel(f"Seite {page_num + 1}")
|
|
||||||
thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
thumbnail_info.setMaximumHeight(18)
|
|
||||||
thumbnail_info.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.ui.verticalLayout_2.addWidget(thumbnail_info)
|
|
||||||
|
|
||||||
self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_basename, "page_num": page_num}
|
|
||||||
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
|
|
||||||
thumbnail_labels.append(thumbnail)
|
|
||||||
|
|
||||||
# Thumbnails progressiv rendern (nicht blockierend)
|
|
||||||
self._schedule_thumbnail_rendering(diff_doc, pdf_basename, thumbnail_labels, 0)
|
|
||||||
|
|
||||||
# Füge expandierenden Spacer am Ende hinzu, damit Thumbnails oben bleiben
|
|
||||||
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
self.ui.verticalLayout_2.addItem(spacer)
|
|
||||||
|
|
||||||
# Erstelle das Vollbild-Label für die rechte Spalte (falls noch nicht vorhanden)
|
|
||||||
if self.fullsize_label is None:
|
|
||||||
self.fullsize_label = QLabel()
|
|
||||||
self.fullsize_label.setObjectName("fullsize_current_page")
|
|
||||||
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
||||||
self.ui.verticalLayout_3.addWidget(self.fullsize_label)
|
|
||||||
|
|
||||||
# Drag-to-Scroll Events für das große Bild einrichten
|
|
||||||
self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setze die aktuelle PDF
|
|
||||||
self.current_pdf = pdf_basename
|
|
||||||
|
|
||||||
# Speichere Diff-PDF-Informationen für Accept Changes
|
|
||||||
self.current_diff_xml_path = xml_file_path
|
|
||||||
self.current_diff_xsl_id = xsl_id_str
|
|
||||||
|
|
||||||
# Aktiviere Accept-Changes-Button
|
|
||||||
self.ui.accept_changes.setEnabled(True)
|
|
||||||
|
|
||||||
# Zeige die erste Seite initial an
|
|
||||||
self.render_and_display_page(pdf_basename, 0)
|
|
||||||
|
|
||||||
logger.info(f"PDF-Vergleich geladen: {pdf_basename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Laden der PDFs für Vergleich: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Konnte PDFs nicht laden:\n{str(e)}")
|
|
||||||
|
|
||||||
def _close_all_pdf_documents(self):
|
|
||||||
"""Schließt alle geöffneten PDF-Dokumente explizit (wichtig für Windows)."""
|
|
||||||
if self.pdf_documents:
|
|
||||||
for pdf_basename, docs in self.pdf_documents.items():
|
|
||||||
for doc_type, doc in docs.items():
|
|
||||||
if doc:
|
|
||||||
doc.close()
|
|
||||||
logger.debug(f"PDF-Dokument geschlossen: {pdf_basename} ({doc_type})")
|
|
||||||
|
|
||||||
# Lösche alle Referenzen
|
|
||||||
self.pdf_documents.clear()
|
|
||||||
|
|
||||||
# Lösche gerenderte Pixmaps
|
|
||||||
self.current_rendered_pixmaps = None
|
|
||||||
|
|
||||||
# Erzwinge Garbage Collection um Dateihandles freizugeben (wichtig für Windows)
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
logger.info("Alle PDF-Dokumente geschlossen und Referenzen freigegeben")
|
|
||||||
|
|
||||||
def _clear_pdf_viewer(self):
|
|
||||||
"""Leert den PDF-Viewer und alle Thumbnails."""
|
|
||||||
# Schließe alle PDF-Dokumente explizit (wichtig für Windows)
|
|
||||||
self._close_all_pdf_documents()
|
|
||||||
|
|
||||||
# Entferne Widgets aus Layouts
|
|
||||||
self._clear_layout(self.ui.verticalLayout_2)
|
|
||||||
self._clear_layout(self.ui.verticalLayout_3)
|
|
||||||
|
|
||||||
# Zurücksetzen der Datenstrukturen
|
|
||||||
self.thumbnail_to_page = {}
|
|
||||||
self.pdf_documents = {}
|
|
||||||
self.current_rendered_pixmaps = None
|
|
||||||
self.fullsize_label = None
|
|
||||||
self.current_pdf = None
|
|
||||||
self.current_diff_xml_path = None
|
|
||||||
self.current_diff_xsl_id = None
|
|
||||||
|
|
||||||
# PDF-Pfade zurücksetzen und Buttons deaktivieren
|
|
||||||
self.current_ref_pdf_path = None
|
|
||||||
self.current_new_pdf_path = None
|
|
||||||
self.ui.view_ref_pdf.setEnabled(False)
|
|
||||||
self.ui.view_new_pdf.setEnabled(False)
|
|
||||||
self.ui.accept_changes.setEnabled(False)
|
|
||||||
|
|
||||||
# Slider deaktivieren
|
|
||||||
self.ui.alpha.setEnabled(False)
|
|
||||||
self.ui.zoom.setEnabled(False)
|
|
||||||
|
|
||||||
logger.info("PDF-Viewer geleert")
|
|
||||||
|
|
||||||
def _on_view_ref_pdf_clicked(self):
|
|
||||||
"""
|
|
||||||
Handler für view_ref_pdf Button.
|
|
||||||
Öffnet die Referenz-PDF im systemseitig installierten PDF-Viewer.
|
|
||||||
"""
|
|
||||||
if not self.current_ref_pdf_path or not self.current_ref_pdf_path.exists():
|
|
||||||
QMessageBox.warning(self, "Fehler", "Referenz-PDF nicht gefunden")
|
|
||||||
logger.warning("Referenz-PDF nicht verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Öffne Referenz-PDF im System-Viewer: {self.current_ref_pdf_path}")
|
|
||||||
url = QUrl.fromLocalFile(str(self.current_ref_pdf_path))
|
|
||||||
if not QDesktopServices.openUrl(url):
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Konnte Referenz-PDF nicht öffnen:\n{self.current_ref_pdf_path}")
|
|
||||||
logger.error(f"Fehler beim Öffnen der Referenz-PDF: {self.current_ref_pdf_path}")
|
|
||||||
|
|
||||||
def _load_ref_pdf_for_display(self, xml_file_path: Path, xsl_id_str: str):
|
|
||||||
"""
|
|
||||||
Lädt nur die Ref-PDF in den Viewer (wenn keine Diff-PDF vorhanden ist).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur XML-Datei (relativ)
|
|
||||||
xsl_id_str: XSL-ID als String
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not self.project:
|
|
||||||
return
|
|
||||||
|
|
||||||
xml_stem = xml_file_path.stem
|
|
||||||
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
||||||
ref_pdf_path = self.project.project_dir / "ref" / pdf_basename
|
|
||||||
|
|
||||||
if not ref_pdf_path.exists():
|
|
||||||
if self.pdf_documents:
|
|
||||||
self._clear_pdf_viewer()
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Lade Ref-PDF für Anzeige: {pdf_basename}")
|
|
||||||
|
|
||||||
self._clear_layout(self.ui.verticalLayout_2)
|
|
||||||
self._clear_layout(self.ui.verticalLayout_3)
|
|
||||||
self.ui.verticalLayout_2.setSpacing(5)
|
|
||||||
self.ui.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
self.thumbnail_to_page = {}
|
|
||||||
self.pdf_documents = {}
|
|
||||||
self.current_rendered_pixmaps = None
|
|
||||||
self.fullsize_label = None
|
|
||||||
|
|
||||||
ref_doc = QPdfDocument()
|
|
||||||
ref_doc.load(str(ref_pdf_path))
|
|
||||||
|
|
||||||
if ref_doc.status() != QPdfDocument.Status.Ready:
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der Ref-PDF:\n{pdf_basename}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ref-Doc in allen drei Slots speichern, damit render_and_display_page funktioniert
|
|
||||||
self.pdf_documents[pdf_basename] = {"diff": ref_doc, "ref": ref_doc, "new": ref_doc}
|
|
||||||
|
|
||||||
self.current_ref_pdf_path = ref_pdf_path
|
|
||||||
self.current_new_pdf_path = None
|
|
||||||
self.ui.view_ref_pdf.setEnabled(True)
|
|
||||||
self.ui.view_new_pdf.setEnabled(False)
|
|
||||||
|
|
||||||
# Alpha deaktivieren (Blending nicht sinnvoll ohne Diff), Zoom aktivieren
|
|
||||||
self.ui.alpha.setValue(0)
|
|
||||||
self.ui.alpha.setEnabled(False)
|
|
||||||
self.ui.zoom.setEnabled(True)
|
|
||||||
|
|
||||||
self.ui.accept_changes.setEnabled(False)
|
|
||||||
|
|
||||||
max_pages = ref_doc.pageCount()
|
|
||||||
logger.info(f"Ref-PDF geladen: {pdf_basename}, {max_pages} Seiten")
|
|
||||||
|
|
||||||
thumbnail_labels = []
|
|
||||||
for page_num in range(max_pages):
|
|
||||||
thumbnail = QLabel()
|
|
||||||
thumbnail.setObjectName(f"thumbnail_{pdf_basename}_page_{page_num + 1}")
|
|
||||||
thumbnail.setText(f"Seite {page_num + 1}\n…")
|
|
||||||
thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
||||||
thumbnail.setMouseTracking(True)
|
|
||||||
thumbnail.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
thumbnail.setMinimumHeight(150)
|
|
||||||
self.ui.verticalLayout_2.addWidget(thumbnail)
|
|
||||||
|
|
||||||
thumbnail_info = QLabel(f"Seite {page_num + 1}")
|
|
||||||
thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
thumbnail_info.setMaximumHeight(18)
|
|
||||||
thumbnail_info.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.ui.verticalLayout_2.addWidget(thumbnail_info)
|
|
||||||
|
|
||||||
self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_basename, "page_num": page_num}
|
|
||||||
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
|
|
||||||
thumbnail_labels.append(thumbnail)
|
|
||||||
|
|
||||||
self._schedule_thumbnail_rendering(ref_doc, pdf_basename, thumbnail_labels, 0)
|
|
||||||
|
|
||||||
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
||||||
self.ui.verticalLayout_2.addItem(spacer)
|
|
||||||
|
|
||||||
if self.fullsize_label is None:
|
|
||||||
self.fullsize_label = QLabel()
|
|
||||||
self.fullsize_label.setObjectName("fullsize_current_page")
|
|
||||||
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
||||||
self.ui.verticalLayout_3.addWidget(self.fullsize_label)
|
|
||||||
|
|
||||||
self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(
|
|
||||||
event, self.fullsize_label
|
|
||||||
)
|
|
||||||
|
|
||||||
self.current_pdf = pdf_basename
|
|
||||||
self.current_diff_xml_path = None
|
|
||||||
self.current_diff_xsl_id = None
|
|
||||||
|
|
||||||
self.render_and_display_page(pdf_basename, 0)
|
|
||||||
logger.info(f"Ref-PDF-Anzeige geladen: {pdf_basename}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Laden der Ref-PDF: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Konnte Ref-PDF nicht laden:\n{str(e)}")
|
|
||||||
|
|
||||||
def _on_view_new_pdf_clicked(self):
|
|
||||||
"""
|
|
||||||
Handler für view_new_pdf Button.
|
|
||||||
Öffnet die neue PDF im systemseitig installierten PDF-Viewer.
|
|
||||||
"""
|
|
||||||
if not self.current_new_pdf_path or not self.current_new_pdf_path.exists():
|
|
||||||
QMessageBox.warning(self, "Fehler", "Neue PDF nicht gefunden")
|
|
||||||
logger.warning("Neue PDF nicht verfügbar")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Öffne neue PDF im System-Viewer: {self.current_new_pdf_path}")
|
|
||||||
url = QUrl.fromLocalFile(str(self.current_new_pdf_path))
|
|
||||||
if not QDesktopServices.openUrl(url):
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Konnte neue PDF nicht öffnen:\n{self.current_new_pdf_path}")
|
|
||||||
logger.error(f"Fehler beim Öffnen der neuen PDF: {self.current_new_pdf_path}")
|
|
||||||
@@ -1,846 +0,0 @@
|
|||||||
"""
|
|
||||||
TransformationMixin - Mixin für XSL-Transformationen.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden zur Durchführung und Verwaltung von
|
|
||||||
XSL-Transformationen für das MainWindow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from copy import deepcopy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtWidgets import QMessageBox, QProgressBar, QTreeWidgetItem
|
|
||||||
|
|
||||||
from conf import app_settings, TreeNode, XslFile, XmlFile
|
|
||||||
from transform import TransformationJob
|
|
||||||
from ui.threads import TransformationThread
|
|
||||||
from xsl_dependencies import XslDependencyGraph
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class TransformationMixin:
|
|
||||||
"""
|
|
||||||
Mixin für XSL-Transformationen.
|
|
||||||
|
|
||||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
|
||||||
- self.project: Das aktuelle Projekt
|
|
||||||
- self.ui: Das UI-Objekt mit treeWidget, statusBar
|
|
||||||
- self.transformation_thread: Thread für Transformationen
|
|
||||||
- self.transformation_progress_bar: QProgressBar für Transformation-Fortschritt
|
|
||||||
- self.xml_item_map: Mapping von xml_path|xsl_id zu TreeWidgetItems
|
|
||||||
- self.last_saxon_metrics: Gecachte Saxon-Worker-Pool-Metriken
|
|
||||||
- self.last_fop_metrics: Gecachte FOP-Worker-Pool-Metriken
|
|
||||||
- self.xsl_dependency_graph: XslDependencyGraph für Import/Include-Erkennung
|
|
||||||
|
|
||||||
Erwartet folgende Methoden von anderen Mixins:
|
|
||||||
- self._initialize_saxon_worker_pool(): Von WorkerPoolMixin
|
|
||||||
- self._initialize_fop_worker_pool(): Von WorkerPoolMixin
|
|
||||||
- self._shutdown_saxon_worker_pool(): Von WorkerPoolMixin
|
|
||||||
- self._shutdown_fop_worker_pool(): Von WorkerPoolMixin
|
|
||||||
- self._create_centered_progress_bar(): Von TreeManagerMixin
|
|
||||||
- self._create_centered_diff_icon(): Von TreeManagerMixin
|
|
||||||
- self._collect_parent_params(): Von TreeManagerMixin
|
|
||||||
- self._update_diff_icons_for_existing_pdfs(): Von MainWindow
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _show_transformation_progress_bar(self, total_jobs: int):
|
|
||||||
"""
|
|
||||||
Zeigt einen Progressbar in der Statusbar für Transformationen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
total_jobs: Gesamtanzahl der Transformations-Jobs
|
|
||||||
"""
|
|
||||||
if self.transformation_progress_bar is None:
|
|
||||||
self.transformation_progress_bar = QProgressBar()
|
|
||||||
self.transformation_progress_bar.setMaximumHeight(20)
|
|
||||||
self.transformation_progress_bar.setMaximumWidth(300)
|
|
||||||
|
|
||||||
self.transformation_progress_bar.setMinimum(0)
|
|
||||||
self.transformation_progress_bar.setMaximum(total_jobs)
|
|
||||||
self.transformation_progress_bar.setValue(0)
|
|
||||||
self.transformation_progress_bar.setFormat("%v/%m Jobs")
|
|
||||||
|
|
||||||
# Füge Progressbar zur Statusbar hinzu
|
|
||||||
self.statusBar().addPermanentWidget(self.transformation_progress_bar)
|
|
||||||
self.transformation_progress_bar.show()
|
|
||||||
|
|
||||||
def _hide_transformation_progress_bar(self):
|
|
||||||
"""Versteckt und entfernt den Transformation-Progressbar aus der Statusbar."""
|
|
||||||
if self.transformation_progress_bar:
|
|
||||||
self.statusBar().removeWidget(self.transformation_progress_bar)
|
|
||||||
self.transformation_progress_bar.hide()
|
|
||||||
|
|
||||||
def _update_transformation_progress(self):
|
|
||||||
"""Aktualisiert den Transformation-Progressbar um einen Schritt."""
|
|
||||||
if self.transformation_progress_bar:
|
|
||||||
current_value = self.transformation_progress_bar.value()
|
|
||||||
self.transformation_progress_bar.setValue(current_value + 1)
|
|
||||||
|
|
||||||
def _transform_xml_file(self, item: QTreeWidgetItem, force: bool = False):
|
|
||||||
"""
|
|
||||||
Transformiert eine einzelne XML-Datei.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: Das TreeWidgetItem der XML-Datei
|
|
||||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Hole XslFile vom Parent-Item
|
|
||||||
parent_item = item.parent()
|
|
||||||
if not parent_item:
|
|
||||||
logger.error("XML-Datei hat kein Parent-Item (XslFile)")
|
|
||||||
QMessageBox.warning(self, "Fehler", "XML-Datei hat keine zugeordnete XSL-Datei")
|
|
||||||
return
|
|
||||||
|
|
||||||
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
if not isinstance(xsl_file_obj, XslFile):
|
|
||||||
logger.error(f"Parent-Item ist kein XslFile: {type(xsl_file_obj)}")
|
|
||||||
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole XmlFile-Objekt
|
|
||||||
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
if not isinstance(xml_file_obj, XmlFile):
|
|
||||||
logger.error(f"Item ist kein XmlFile: {type(xml_file_obj)}")
|
|
||||||
QMessageBox.warning(self, "Fehler", "Konnte XML-Datei nicht ermitteln")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle TransformationJob mit TreeWidgetItem-Kontext für Parameter-Sammlung
|
|
||||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, parent_item)
|
|
||||||
if not job:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Fehler",
|
|
||||||
"Transformation kann nicht gestartet werden.\n\n"
|
|
||||||
"Bitte überprüfen Sie, ob XML- und XSL-Dateien existieren.\n"
|
|
||||||
"Details finden Sie im Log.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Starte Transformation in separatem Thread
|
|
||||||
self._start_transformation([job], force=force)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Transformieren der XML-Datei: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
||||||
|
|
||||||
def _transform_xsl_file(self, item: QTreeWidgetItem, force: bool = False):
|
|
||||||
"""
|
|
||||||
Transformiert alle XML-Dateien einer XSL-Datei.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: Das TreeWidgetItem der XSL-Datei
|
|
||||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Hole XslFile-Objekt
|
|
||||||
xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
if not isinstance(xsl_file_obj, XslFile):
|
|
||||||
logger.error(f"Item ist kein XslFile: {type(xsl_file_obj)}")
|
|
||||||
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prüfe ob XML-Dateien vorhanden sind
|
|
||||||
if not xsl_file_obj.xmls:
|
|
||||||
QMessageBox.information(self, "Info", "Keine XML-Dateien zugeordnet")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle TransformationJobs für alle XML-Dateien
|
|
||||||
jobs = []
|
|
||||||
skipped_count = 0
|
|
||||||
for xml_file_obj in xsl_file_obj.xmls:
|
|
||||||
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
|
||||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, item)
|
|
||||||
if job:
|
|
||||||
jobs.append(job)
|
|
||||||
else:
|
|
||||||
skipped_count += 1
|
|
||||||
|
|
||||||
if not jobs:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Fehler",
|
|
||||||
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
|
|
||||||
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Informiere Benutzer wenn einige Jobs übersprungen wurden
|
|
||||||
if skipped_count > 0:
|
|
||||||
logger.warning(f"{skipped_count} von {len(xsl_file_obj.xmls)} Jobs übersprungen (fehlende Dateien)")
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Warnung",
|
|
||||||
f"{skipped_count} von {len(xsl_file_obj.xmls)} XML-Datei(en) werden übersprungen "
|
|
||||||
f"(fehlende XML- oder XSL-Dateien).\n\n"
|
|
||||||
f"{len(jobs)} Job(s) werden ausgeführt.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Starte Transformation in separatem Thread
|
|
||||||
self._start_transformation(jobs, force=force)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Transformieren der XSL-Datei: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
||||||
|
|
||||||
def _count_diff_pdfs_under_node(self, node: TreeNode | XslFile, node_item: QTreeWidgetItem) -> int:
|
|
||||||
"""
|
|
||||||
Zählt die Anzahl der existierenden Diff-PDFs unter einem Knoten.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
node: TreeNode oder XslFile Objekt
|
|
||||||
node_item: Das TreeWidgetItem des Knotens
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Anzahl der existierenden Diff-PDF-Dateien
|
|
||||||
"""
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
if isinstance(node, XslFile):
|
|
||||||
# Für XslFile: Zähle Diff-PDFs für jede XML-Datei
|
|
||||||
if not self.project:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
diff_dir = self.project.project_dir / "diff"
|
|
||||||
xsl_id_str = "_".join(str(x) for x in node.id) if node.id else ""
|
|
||||||
|
|
||||||
for xml_file_obj in node.xmls:
|
|
||||||
xml_stem = xml_file_obj.xml.stem
|
|
||||||
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
||||||
diff_pdf_path = diff_dir / pdf_basename
|
|
||||||
|
|
||||||
if diff_pdf_path.exists():
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
elif isinstance(node, TreeNode):
|
|
||||||
# Für TreeNode: Rekursiv alle Kinder durchgehen
|
|
||||||
for i in range(node_item.childCount()):
|
|
||||||
child_item = node_item.child(i)
|
|
||||||
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
|
|
||||||
if isinstance(child_node, (XslFile, TreeNode)):
|
|
||||||
count += self._count_diff_pdfs_under_node(child_node, child_item)
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
def _update_diff_pdf_counts_recursive(self, tree_item: QTreeWidgetItem):
|
|
||||||
"""
|
|
||||||
Aktualisiert rekursiv die Diff-PDF-Anzahl in Spalte 1 für alle TreeNode und XslFile Items.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tree_item: Das TreeWidgetItem (kann Root oder beliebiger Knoten sein)
|
|
||||||
"""
|
|
||||||
node = tree_item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
|
|
||||||
# Aktualisiere nur für TreeNode und XslFile, nicht für XmlFile
|
|
||||||
if isinstance(node, (TreeNode, XslFile)):
|
|
||||||
count = self._count_diff_pdfs_under_node(node, tree_item)
|
|
||||||
tree_item.setText(1, str(count) if count > 0 else "")
|
|
||||||
|
|
||||||
# Rekursiv für alle Kinder
|
|
||||||
for i in range(tree_item.childCount()):
|
|
||||||
child_item = tree_item.child(i)
|
|
||||||
self._update_diff_pdf_counts_recursive(child_item)
|
|
||||||
|
|
||||||
def _update_all_diff_pdf_counts(self):
|
|
||||||
"""
|
|
||||||
Aktualisiert die Diff-PDF-Anzahl für alle Knoten im TreeWidget.
|
|
||||||
"""
|
|
||||||
root = self.ui.treeWidget.invisibleRootItem()
|
|
||||||
for i in range(root.childCount()):
|
|
||||||
self._update_diff_pdf_counts_recursive(root.child(i))
|
|
||||||
|
|
||||||
def _has_xml_files_recursive(self, node: TreeNode) -> bool:
|
|
||||||
"""
|
|
||||||
Prüft rekursiv, ob unter einem TreeNode mindestens eine XML-Datei vorhanden ist.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
node: Der TreeNode
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True wenn mindestens eine XML-Datei gefunden wurde
|
|
||||||
"""
|
|
||||||
if not hasattr(node, "children") or not node.children:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for child in node.children:
|
|
||||||
if isinstance(child, XslFile):
|
|
||||||
if child.xmls:
|
|
||||||
return True
|
|
||||||
elif isinstance(child, TreeNode):
|
|
||||||
if self._has_xml_files_recursive(child):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _collect_all_xsl_xml_pairs_recursive(
|
|
||||||
self, tree_node: TreeNode, tree_item: QTreeWidgetItem
|
|
||||||
) -> list[tuple[XslFile, XmlFile, QTreeWidgetItem]]:
|
|
||||||
"""
|
|
||||||
Sammelt rekursiv alle (XslFile, XmlFile, XslFileItem) Tupel unter einem TreeNode.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tree_node: Der TreeNode
|
|
||||||
tree_item: Das TreeWidgetItem des TreeNode
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Liste von (XslFile, XmlFile, XslFileItem) Tupeln
|
|
||||||
"""
|
|
||||||
pairs = []
|
|
||||||
|
|
||||||
if not hasattr(tree_node, "children") or not tree_node.children:
|
|
||||||
return pairs
|
|
||||||
|
|
||||||
# Durchlaufe alle Kinder des TreeNode
|
|
||||||
for i in range(tree_item.childCount()):
|
|
||||||
child_item = tree_item.child(i)
|
|
||||||
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
|
|
||||||
if isinstance(child_node, XslFile):
|
|
||||||
# XslFile gefunden - sammle alle XML-Dateien
|
|
||||||
for xml_file_obj in child_node.xmls:
|
|
||||||
pairs.append((child_node, xml_file_obj, child_item))
|
|
||||||
|
|
||||||
elif isinstance(child_node, TreeNode):
|
|
||||||
# Rekursiv in Unterknoten suchen
|
|
||||||
pairs.extend(self._collect_all_xsl_xml_pairs_recursive(child_node, child_item))
|
|
||||||
|
|
||||||
return pairs
|
|
||||||
|
|
||||||
def _transform_tree_node(self, item: QTreeWidgetItem, force: bool = False):
|
|
||||||
"""
|
|
||||||
Transformiert alle XML-Dateien unter einem TreeNode (rekursiv).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: Das TreeWidgetItem des TreeNode
|
|
||||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Hole TreeNode-Objekt
|
|
||||||
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
if not isinstance(tree_node_obj, TreeNode):
|
|
||||||
logger.error(f"Item ist kein TreeNode: {type(tree_node_obj)}")
|
|
||||||
QMessageBox.warning(self, "Fehler", "Konnte TreeNode nicht ermitteln")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prüfe ob XML-Dateien vorhanden sind
|
|
||||||
if not self._has_xml_files_recursive(tree_node_obj):
|
|
||||||
QMessageBox.information(self, "Info", "Keine XML-Dateien unter diesem Knoten gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sammle alle XSL/XML-Paare rekursiv
|
|
||||||
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(tree_node_obj, item)
|
|
||||||
|
|
||||||
if not xsl_xml_pairs:
|
|
||||||
QMessageBox.information(self, "Info", "Keine XML-Dateien gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle TransformationJobs für alle XML-Dateien
|
|
||||||
jobs = []
|
|
||||||
skipped_count = 0
|
|
||||||
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
|
|
||||||
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
|
||||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
|
|
||||||
if job:
|
|
||||||
jobs.append(job)
|
|
||||||
else:
|
|
||||||
skipped_count += 1
|
|
||||||
|
|
||||||
if not jobs:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Fehler",
|
|
||||||
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
|
|
||||||
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Informiere Benutzer wenn einige Jobs übersprungen wurden
|
|
||||||
if skipped_count > 0:
|
|
||||||
logger.warning(f"{skipped_count} von {len(xsl_xml_pairs)} Jobs übersprungen (fehlende Dateien)")
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Warnung",
|
|
||||||
f"{skipped_count} von {len(xsl_xml_pairs)} XML-Datei(en) werden übersprungen "
|
|
||||||
f"(fehlende XML- oder XSL-Dateien).\n\n"
|
|
||||||
f"{len(jobs)} Job(s) werden ausgeführt.",
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Starte Transformation für {len(jobs)} XML-Dateien unter TreeNode '{tree_node_obj.bez}'")
|
|
||||||
|
|
||||||
# Starte Transformation in separatem Thread
|
|
||||||
self._start_transformation(jobs, force=force)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Transformieren des TreeNode: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
||||||
|
|
||||||
def _get_cached_project_tools(self):
|
|
||||||
"""
|
|
||||||
Gibt die aufgelösten Tool-Konfigurationen des aktuellen Projekts zurück (gecacht).
|
|
||||||
|
|
||||||
Der Cache wird invalidiert, sobald sich die Projekt-ID ändert.
|
|
||||||
"""
|
|
||||||
project_id = self.project.id if self.project else None
|
|
||||||
if getattr(self, "_tool_cache_project_id", None) != project_id:
|
|
||||||
self._tool_cache_project_id = project_id
|
|
||||||
self._tool_cache = (
|
|
||||||
next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None),
|
|
||||||
next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None),
|
|
||||||
next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None),
|
|
||||||
next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None),
|
|
||||||
next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None),
|
|
||||||
)
|
|
||||||
return self._tool_cache
|
|
||||||
|
|
||||||
def _create_transformation_job(
|
|
||||||
self, xsl_file_obj: XslFile, xml_file_obj: XmlFile, xsl_file_item: QTreeWidgetItem | None = None
|
|
||||||
) -> TransformationJob | None:
|
|
||||||
"""
|
|
||||||
Erstellt einen TransformationJob für eine XML/XSL-Kombination.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file_obj: Das XslFile-Objekt
|
|
||||||
xml_file_obj: Das XmlFile-Objekt
|
|
||||||
xsl_file_item: Optional das TreeWidgetItem des XslFile für hierarchische Parameter-Sammlung
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
TransformationJob oder None bei Fehler
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not self.project:
|
|
||||||
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Tool-Konfigurationen gecacht auflösen (einmalig pro Projekt)
|
|
||||||
java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir = self._get_cached_project_tools()
|
|
||||||
|
|
||||||
# Prüfe ob alle Konfigurationen vorhanden sind
|
|
||||||
if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
|
|
||||||
missing = []
|
|
||||||
if not java_vm:
|
|
||||||
missing.append("Java VM")
|
|
||||||
if not saxon_jar:
|
|
||||||
missing.append("Saxon JAR")
|
|
||||||
if not apache_fop:
|
|
||||||
missing.append("Apache FOP")
|
|
||||||
if not diff_pdf:
|
|
||||||
missing.append("diff-pdf")
|
|
||||||
if not xsl_dir:
|
|
||||||
missing.append("XSL-Verzeichnis")
|
|
||||||
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Zusätzliche Sicherheitsprüfung für path_to_binary_file Attribute
|
|
||||||
if java_vm is None or not hasattr(java_vm, "path_to_binary_file") or java_vm.path_to_binary_file is None:
|
|
||||||
QMessageBox.warning(self, "Konfigurationsfehler", "Java VM Pfad ist nicht konfiguriert")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if saxon_jar is None or not hasattr(saxon_jar, "path_to_jar_file") or saxon_jar.path_to_jar_file is None:
|
|
||||||
QMessageBox.warning(self, "Konfigurationsfehler", "Saxon JAR Pfad ist nicht konfiguriert")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if apache_fop is None or not hasattr(apache_fop, "path_to_dir") or apache_fop.path_to_dir is None:
|
|
||||||
QMessageBox.warning(self, "Konfigurationsfehler", "Apache FOP Pfad ist nicht konfiguriert")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if diff_pdf is None or not hasattr(diff_pdf, "path_to_binary_file") or diff_pdf.path_to_binary_file is None:
|
|
||||||
QMessageBox.warning(self, "Konfigurationsfehler", "diff-pdf Pfad ist nicht konfiguriert")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if xsl_dir is None or not hasattr(xsl_dir, "path_to_root_dir") or xsl_dir.path_to_root_dir is None:
|
|
||||||
QMessageBox.warning(self, "Konfigurationsfehler", "XSL-Verzeichnis Pfad ist nicht konfiguriert")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Erstelle absoluten Pfad zur XSL-Datei
|
|
||||||
xsl_file_abs = xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file
|
|
||||||
|
|
||||||
# Prüfe ob XSL-Datei existiert
|
|
||||||
if not xsl_file_abs.exists():
|
|
||||||
error_msg = f"XSL-Datei nicht gefunden: {xsl_file_abs}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
# Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Erstelle absoluten Pfad zur XML-Datei
|
|
||||||
xml_file_abs = self.project.project_dir / xml_file_obj.xml
|
|
||||||
|
|
||||||
# Prüfe ob XML-Datei existiert
|
|
||||||
if not xml_file_abs.exists():
|
|
||||||
error_msg = f"XML-Datei nicht gefunden: {xml_file_abs}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
# Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Sammle XSLT-Parameter hierarchisch (TreeNode-Eltern → XslFile)
|
|
||||||
xslt_params = {}
|
|
||||||
|
|
||||||
# 1. Sammle Parameter von übergeordneten TreeNodes (falls TreeWidgetItem verfügbar)
|
|
||||||
if xsl_file_item is not None:
|
|
||||||
parent_params = self._collect_parent_params(xsl_file_item)
|
|
||||||
xslt_params.update(parent_params)
|
|
||||||
logger.debug(f"Hierarchische Parameter gesammelt: {parent_params}")
|
|
||||||
else:
|
|
||||||
# Ohne TreeWidgetItem-Kontext: nur Projekt-Parameter als Basis
|
|
||||||
if hasattr(self, "project") and self.project and self.project.xslt_params:
|
|
||||||
xslt_params.update(self.project.xslt_params)
|
|
||||||
logger.warning(
|
|
||||||
"Kein TreeWidgetItem-Kontext verfügbar - "
|
|
||||||
"übergeordnete TreeNode-Parameter werden nicht berücksichtigt"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Überschreibe mit XslFile-eigenen Parametern (höchste Priorität)
|
|
||||||
xslt_params.update(xsl_file_obj.xslt_params)
|
|
||||||
|
|
||||||
logger.info(f"Finale XSLT-Parameter für {xml_file_obj.xml} mit {xsl_file_obj.bez}: {xslt_params}")
|
|
||||||
|
|
||||||
# Initialisiere XSL-Abhängigkeitsgraph (lazy, einmalig pro Mixin-Instanz)
|
|
||||||
if not hasattr(self, "xsl_dependency_graph") or self.xsl_dependency_graph is None:
|
|
||||||
self.xsl_dependency_graph = XslDependencyGraph()
|
|
||||||
|
|
||||||
# Erstelle TransformationJob
|
|
||||||
job = TransformationJob(
|
|
||||||
project_dir=self.project.project_dir,
|
|
||||||
xml_file=xml_file_obj.xml,
|
|
||||||
xsl_file=xsl_file_abs,
|
|
||||||
xslt_params=xslt_params,
|
|
||||||
java_vm_path=java_vm.path_to_binary_file,
|
|
||||||
saxon_jar_path=saxon_jar.path_to_jar_file,
|
|
||||||
apache_fop_dir=apache_fop.path_to_dir,
|
|
||||||
diff_pdf_path=diff_pdf.path_to_binary_file,
|
|
||||||
diff_pdf_params=diff_pdf.default_params,
|
|
||||||
xsl_id=xsl_file_obj.id,
|
|
||||||
fop_config_dir=self.project.fop_config_dir,
|
|
||||||
dependency_graph=self.xsl_dependency_graph,
|
|
||||||
)
|
|
||||||
|
|
||||||
return job
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Erstellen des TransformationJobs: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen des Jobs: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _start_transformation(self, jobs: list[TransformationJob], force: bool = False):
|
|
||||||
"""
|
|
||||||
Startet die Transformation in einem separaten Thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
jobs: Liste der TransformationJobs
|
|
||||||
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Prüfe ob bereits ein Thread läuft
|
|
||||||
if self.transformation_thread and self.transformation_thread.isRunning():
|
|
||||||
QMessageBox.warning(self, "Warnung", "Es läuft bereits eine Transformation")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle und konfiguriere Thread
|
|
||||||
self.transformation_thread = TransformationThread(jobs, force=force, max_workers=app_settings.max_workers)
|
|
||||||
|
|
||||||
# Verbinde Signale
|
|
||||||
self.transformation_thread.job_started.connect(self._on_transformation_job_started)
|
|
||||||
self.transformation_thread.job_finished.connect(self._on_transformation_job_finished)
|
|
||||||
self.transformation_thread.job_error.connect(self._on_transformation_job_error)
|
|
||||||
self.transformation_thread.all_jobs_finished.connect(self._on_all_transformations_finished)
|
|
||||||
|
|
||||||
# Zeige Progressbar
|
|
||||||
self._show_transformation_progress_bar(len(jobs))
|
|
||||||
|
|
||||||
# Initialisiere Worker-Pools (lazy loading - nur wenn benötigt)
|
|
||||||
self._initialize_saxon_worker_pool()
|
|
||||||
self._initialize_fop_worker_pool()
|
|
||||||
|
|
||||||
# Erfasse RAM-Verbrauch vor Transformation
|
|
||||||
import transform
|
|
||||||
|
|
||||||
if transform._saxon_worker_pool:
|
|
||||||
transform._saxon_worker_pool.capture_ram_before_transform()
|
|
||||||
if transform._fop_worker_pool:
|
|
||||||
transform._fop_worker_pool.capture_ram_before_transform()
|
|
||||||
|
|
||||||
# Starte Thread
|
|
||||||
self.transformation_thread.start()
|
|
||||||
|
|
||||||
logger.info(f"Transformation von {len(jobs)} Job(s) gestartet (force={force})")
|
|
||||||
self.statusBar().showMessage(f"Transformation von {len(jobs)} Job(s) gestartet...")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Starten der Transformation: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}")
|
|
||||||
|
|
||||||
def _expand_tree_item_parents(self, item: QTreeWidgetItem):
|
|
||||||
"""
|
|
||||||
Öffnet alle Eltern-Knoten eines Tree-Items rekursiv.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: Das Tree-Item, dessen Eltern geöffnet werden sollen
|
|
||||||
"""
|
|
||||||
if item is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Rekursiv alle Eltern öffnen
|
|
||||||
parent = item.parent()
|
|
||||||
while parent is not None:
|
|
||||||
parent.setExpanded(True)
|
|
||||||
parent = parent.parent()
|
|
||||||
|
|
||||||
def _on_transformation_job_started(self, xml_file_name: str, xsl_id_str: str):
|
|
||||||
"""
|
|
||||||
Signal-Handler: Ein Job wurde gestartet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_name: Name der XML-Datei
|
|
||||||
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
|
||||||
"""
|
|
||||||
logger.info(f"Transformation gestartet: {xml_file_name} (XSL-ID: {xsl_id_str})")
|
|
||||||
self.statusBar().showMessage(f"Transformiere: {xml_file_name}")
|
|
||||||
|
|
||||||
# Progress Bar anzeigen
|
|
||||||
map_key = f"{xml_file_name}|{xsl_id_str}"
|
|
||||||
logger.info(f"Suche TreeWidget-Item für: '{map_key}'")
|
|
||||||
logger.info(f"Map hat {len(self.xml_item_map)} Einträge")
|
|
||||||
tree_item = self.xml_item_map.get(map_key)
|
|
||||||
if tree_item:
|
|
||||||
# Öffne alle Eltern-Knoten, damit der Benutzer den Fortschritt sehen kann
|
|
||||||
self._expand_tree_item_parents(tree_item)
|
|
||||||
|
|
||||||
# Scrolle zum Item, damit es sichtbar ist
|
|
||||||
self.ui.treeWidget.scrollToItem(tree_item)
|
|
||||||
|
|
||||||
# Entferne vorhandenes Widget (falls Icon vorhanden)
|
|
||||||
self.ui.treeWidget.removeItemWidget(tree_item, 1)
|
|
||||||
|
|
||||||
# Erstelle und setze Progress Bar
|
|
||||||
progress_widget, progress_bar = self._create_centered_progress_bar()
|
|
||||||
self.ui.treeWidget.setItemWidget(tree_item, 1, progress_widget)
|
|
||||||
|
|
||||||
logger.debug(f"Progress Bar für {xml_file_name} gesetzt und Eltern-Knoten geöffnet")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Kein TreeWidget-Item für {xml_file_name} gefunden")
|
|
||||||
|
|
||||||
def _on_transformation_job_finished(self, result: dict):
|
|
||||||
"""
|
|
||||||
Signal-Handler: Ein Job wurde abgeschlossen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
result: Ergebnis-Dictionary
|
|
||||||
"""
|
|
||||||
# Aktualisiere Transformation-Progressbar
|
|
||||||
self._update_transformation_progress()
|
|
||||||
|
|
||||||
xml_file = result.get("xml_file", "?")
|
|
||||||
success = result.get("success", False)
|
|
||||||
duration = result.get("duration", 0)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info(f"Transformation erfolgreich: {xml_file} ({duration:.2f}s)")
|
|
||||||
pdfs_identical = result.get("pdfs_identical", False)
|
|
||||||
if pdfs_identical:
|
|
||||||
self.statusBar().showMessage(f"✓ {xml_file} - PDFs identisch ({duration:.2f}s)", 3000)
|
|
||||||
else:
|
|
||||||
self.statusBar().showMessage(f"⚠ {xml_file} - Unterschiede gefunden ({duration:.2f}s)", 3000)
|
|
||||||
else:
|
|
||||||
logger.error(f"Transformation fehlgeschlagen: {xml_file}")
|
|
||||||
# Zeige Fehlerdetails an
|
|
||||||
steps = result.get("steps", {})
|
|
||||||
error_msgs = []
|
|
||||||
for step_name, step_info in steps.items():
|
|
||||||
if not step_info.get("success", True):
|
|
||||||
error_msgs.append(f"{step_name}: {step_info.get('message', 'Unbekannter Fehler')}")
|
|
||||||
|
|
||||||
error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
|
|
||||||
QMessageBox.critical(
|
|
||||||
self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update Widget in Spalte 1: Entferne Progress Bar, zeige Icon wenn Diff-PDF existiert
|
|
||||||
xml_file_str = result.get("xml_file", "")
|
|
||||||
xsl_id = result.get("xsl_id", None)
|
|
||||||
xsl_id_str = "_".join(str(x) for x in xsl_id) if xsl_id else ""
|
|
||||||
map_key = f"{xml_file_str}|{xsl_id_str}"
|
|
||||||
diff_pdf_str = result.get("diff_pdf", None)
|
|
||||||
tree_item = self.xml_item_map.get(map_key)
|
|
||||||
|
|
||||||
if tree_item:
|
|
||||||
# Entferne Progress Bar
|
|
||||||
self.ui.treeWidget.removeItemWidget(tree_item, 1)
|
|
||||||
|
|
||||||
# Wenn Diff-PDF existiert, zeige Icon
|
|
||||||
if diff_pdf_str and Path(diff_pdf_str).exists():
|
|
||||||
xml_file_path = Path(xml_file_str)
|
|
||||||
icon_widget = self._create_centered_diff_icon(xml_file_path, xsl_id_str)
|
|
||||||
self.ui.treeWidget.setItemWidget(tree_item, 1, icon_widget)
|
|
||||||
logger.debug(f"Diff-Icon für {xml_file_str} gesetzt")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Keine Diff-PDF für {xml_file_str}, kein Icon gesetzt")
|
|
||||||
|
|
||||||
def _on_transformation_job_error(self, xml_file_name: str, xsl_id_str: str, error_message: str):
|
|
||||||
"""
|
|
||||||
Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_name: Name der XML-Datei
|
|
||||||
xsl_id_str: XSL-ID als String
|
|
||||||
error_message: Fehlermeldung
|
|
||||||
"""
|
|
||||||
# Aktualisiere Transformation-Progressbar
|
|
||||||
self._update_transformation_progress()
|
|
||||||
|
|
||||||
logger.error(f"Transformation-Fehler bei {xml_file_name} (XSL-ID: {xsl_id_str}): {error_message}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler bei {xml_file_name}:\n{error_message}")
|
|
||||||
|
|
||||||
# Entferne Progress Bar bei Fehler
|
|
||||||
map_key = f"{xml_file_name}|{xsl_id_str}"
|
|
||||||
tree_item = self.xml_item_map.get(map_key)
|
|
||||||
if tree_item:
|
|
||||||
self.ui.treeWidget.removeItemWidget(tree_item, 1)
|
|
||||||
logger.debug(f"Progress Bar für {map_key} entfernt (Fehler)")
|
|
||||||
|
|
||||||
def _on_all_transformations_finished(self, successful_count: int, total_count: int, total_duration: float):
|
|
||||||
"""
|
|
||||||
Signal-Handler: Alle Jobs wurden abgeschlossen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
successful_count: Anzahl erfolgreicher Jobs
|
|
||||||
total_count: Gesamtanzahl der Jobs
|
|
||||||
total_duration: Gesamtdauer aller Transformationen in Sekunden
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Erfasse RAM-Verbrauch nach Transformation
|
|
||||||
import transform
|
|
||||||
|
|
||||||
if transform._saxon_worker_pool:
|
|
||||||
transform._saxon_worker_pool.capture_ram_after_transform()
|
|
||||||
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
|
|
||||||
self.last_saxon_metrics = deepcopy(transform._saxon_worker_pool.metrics)
|
|
||||||
logger.debug("Saxon Worker-Pool Metriken gespeichert")
|
|
||||||
|
|
||||||
if transform._fop_worker_pool:
|
|
||||||
transform._fop_worker_pool.capture_ram_after_transform()
|
|
||||||
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
|
|
||||||
self.last_fop_metrics = deepcopy(transform._fop_worker_pool.metrics)
|
|
||||||
logger.debug("FOP Worker-Pool Metriken gespeichert")
|
|
||||||
|
|
||||||
# Beende Worker-Pools (RAM-Optimierung - Pools werden bei nächster Transformation neu gestartet)
|
|
||||||
self._shutdown_saxon_worker_pool()
|
|
||||||
self._shutdown_fop_worker_pool()
|
|
||||||
|
|
||||||
# Verstecke Transformation-Progressbar
|
|
||||||
self._hide_transformation_progress_bar()
|
|
||||||
|
|
||||||
# Aktualisiere Diff-PDF-Anzahl und Icons in allen Knoten
|
|
||||||
self._update_all_diff_pdf_counts()
|
|
||||||
self._update_diff_icons_for_existing_pdfs()
|
|
||||||
|
|
||||||
# Formatiere Dauer für Anzeige
|
|
||||||
duration_str = f"{total_duration:.2f}s"
|
|
||||||
|
|
||||||
if successful_count == total_count:
|
|
||||||
self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich ({duration_str})", 5000)
|
|
||||||
QMessageBox.information(
|
|
||||||
self,
|
|
||||||
"Abgeschlossen",
|
|
||||||
f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen.\n\nGesamtdauer: {duration_str}",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
failed_count = total_count - successful_count
|
|
||||||
self.statusBar().showMessage(
|
|
||||||
f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen ({duration_str})", 5000
|
|
||||||
)
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Abgeschlossen mit Fehlern",
|
|
||||||
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def _transform_all_xml_files(self, force: bool = False):
|
|
||||||
"""
|
|
||||||
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
force: Wenn True, werden alle Dateien unabhängig vom Änderungsstatus neu transformiert.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not self.project or not self.pdf_project:
|
|
||||||
QMessageBox.warning(self, "Fehler", "Kein Projekt geladen")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sammle alle XSL/XML-Paare aus allen Root-Nodes
|
|
||||||
all_jobs = []
|
|
||||||
root = self.ui.treeWidget.invisibleRootItem()
|
|
||||||
|
|
||||||
for i in range(root.childCount()):
|
|
||||||
root_item = root.child(i)
|
|
||||||
root_node = root_item.data(0, Qt.ItemDataRole.UserRole)
|
|
||||||
|
|
||||||
if isinstance(root_node, TreeNode):
|
|
||||||
# Sammle alle XSL/XML-Paare rekursiv
|
|
||||||
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(root_node, root_item)
|
|
||||||
|
|
||||||
# Erstelle TransformationJobs
|
|
||||||
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
|
|
||||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
|
|
||||||
if job:
|
|
||||||
all_jobs.append(job)
|
|
||||||
|
|
||||||
elif isinstance(root_node, XslFile):
|
|
||||||
# Direkt XslFile als Root-Element
|
|
||||||
for xml_file_obj in root_node.xmls:
|
|
||||||
job = self._create_transformation_job(root_node, xml_file_obj, root_item)
|
|
||||||
if job:
|
|
||||||
all_jobs.append(job)
|
|
||||||
|
|
||||||
if not all_jobs:
|
|
||||||
QMessageBox.information(self, "Info", "Keine XML-Dateien zum Transformieren gefunden")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Frage Benutzer um Bestätigung
|
|
||||||
if force:
|
|
||||||
title = "Alle XML-Dateien neu transformieren (force)"
|
|
||||||
message = (
|
|
||||||
f"Möchten Sie wirklich ALLE {len(all_jobs)} XML-Dateien NEU transformieren?\n\n"
|
|
||||||
f"⚠ WARNUNG: Im Force-Modus werden alle Dateien unabhängig von ihrem Status neu verarbeitet!\n"
|
|
||||||
f"Dies kann längere Zeit in Anspruch nehmen."
|
|
||||||
)
|
|
||||||
default_button = QMessageBox.StandardButton.No
|
|
||||||
else:
|
|
||||||
title = "Alle XML-Dateien transformieren"
|
|
||||||
message = (
|
|
||||||
f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n"
|
|
||||||
f"Nur Dateien mit Änderungen werden verarbeitet."
|
|
||||||
)
|
|
||||||
default_button = QMessageBox.StandardButton.Yes
|
|
||||||
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
default_button,
|
|
||||||
)
|
|
||||||
|
|
||||||
if reply != QMessageBox.StandardButton.Yes:
|
|
||||||
logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"Starte {'Force-' if force else ''}Transformation für alle {len(all_jobs)} XML-Dateien")
|
|
||||||
|
|
||||||
# Starte Transformation in separatem Thread
|
|
||||||
self._start_transformation(all_jobs, force=force)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Transformieren aller XML-Dateien: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Transformation: {str(e)}")
|
|
||||||
|
|
||||||
def _transform_all_xml_files_force(self):
|
|
||||||
"""Transformiert ALLE XML-Dateien im Force-Modus."""
|
|
||||||
self._transform_all_xml_files(force=True)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,184 +0,0 @@
|
|||||||
"""
|
|
||||||
WorkerPoolMixin - Mixin für Worker-Pool-Verwaltung.
|
|
||||||
|
|
||||||
Dieses Mixin enthält alle Methoden zur Verwaltung der Saxon- und FOP-Worker-Pools
|
|
||||||
für das MainWindow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from PySide6.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from conf import app_settings, XsltVersion
|
|
||||||
from transform import TransformationJob, get_saxon_worker_pool, set_saxon_worker_pool, get_fop_worker_pool, set_fop_worker_pool
|
|
||||||
from saxon_pool import SaxonWorkerPool
|
|
||||||
from saxon_pool_s9api import SaxonWorkerPoolS9Api
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerPoolMixin:
|
|
||||||
"""
|
|
||||||
Mixin für Worker-Pool-Verwaltung.
|
|
||||||
|
|
||||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
|
||||||
- self.project: Das aktuelle Projekt
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _initialize_saxon_worker_pool(self):
|
|
||||||
"""Initialisiert den Saxon-Worker-Pool für schnelle Transformationen."""
|
|
||||||
try:
|
|
||||||
# Shutdown vorherigen Pool falls vorhanden
|
|
||||||
self._shutdown_saxon_worker_pool()
|
|
||||||
|
|
||||||
# Prüfe ob SaxonWorkerPool aktiviert ist
|
|
||||||
if not app_settings.use_saxon_worker_pool:
|
|
||||||
logger.info("SaxonWorkerPool deaktiviert - Verwende Fallback-Modus (subprocess)")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.project:
|
|
||||||
logger.warning("Kein Projekt geladen, Saxon-Worker-Pool nicht initialisiert")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole Tool-Konfigurationen
|
|
||||||
java_vm = next((vm for vm in app_settings.java_vms if vm.id == self.project.java_vm_id), None)
|
|
||||||
saxon_jar = next((jar for jar in app_settings.saxon_jars if jar.id == self.project.saxon_jar_id), None)
|
|
||||||
|
|
||||||
if not java_vm or not saxon_jar:
|
|
||||||
logger.warning("Java VM oder Saxon JAR nicht gefunden, Pool nicht initialisiert")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Erstelle Worker-Pool (wähle richtige Variante basierend auf XSLT-Version)
|
|
||||||
num_workers = app_settings.max_workers
|
|
||||||
log_dir = self.project.project_dir / "tmp"
|
|
||||||
|
|
||||||
# Wähle die richtige Worker-Pool-Implementierung
|
|
||||||
if app_settings.saxon_xslt_version == XsltVersion.XSLT_1_0:
|
|
||||||
# JAXP-basierte Variante für XSLT 1.0
|
|
||||||
pool = SaxonWorkerPool(
|
|
||||||
num_workers=num_workers,
|
|
||||||
java_vm_path=java_vm.path_to_binary_file,
|
|
||||||
saxon_jar_path=saxon_jar.path_to_jar_file,
|
|
||||||
classpath_cache=TransformationJob._classpath_cache,
|
|
||||||
log_dir=log_dir,
|
|
||||||
)
|
|
||||||
pool_type = "JAXP (XSLT 1.0)"
|
|
||||||
else:
|
|
||||||
# s9api-basierte Variante für XSLT 2.0/3.0
|
|
||||||
pool = SaxonWorkerPoolS9Api(
|
|
||||||
num_workers=num_workers,
|
|
||||||
java_vm_path=java_vm.path_to_binary_file,
|
|
||||||
saxon_jar_path=saxon_jar.path_to_jar_file,
|
|
||||||
classpath_cache=TransformationJob._classpath_cache,
|
|
||||||
log_dir=log_dir,
|
|
||||||
)
|
|
||||||
pool_type = "s9api (XSLT 2.0/3.0)"
|
|
||||||
|
|
||||||
# Setze globalen Pool
|
|
||||||
set_saxon_worker_pool(pool)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Saxon-Worker-Pool initialisiert: {num_workers} Worker mit {pool_type} "
|
|
||||||
f"(erwartet: {num_workers}x schneller für Saxon-Transformationen)"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Initialisieren des Saxon-Worker-Pools: {e}")
|
|
||||||
logger.info("Fallback auf subprocess-Modus")
|
|
||||||
# Kein Pool ist OK - Fallback auf subprocess
|
|
||||||
|
|
||||||
def _shutdown_saxon_worker_pool(self):
|
|
||||||
"""Beendet den Saxon-Worker-Pool sauber."""
|
|
||||||
try:
|
|
||||||
pool = get_saxon_worker_pool()
|
|
||||||
if pool:
|
|
||||||
logger.info("Beende Saxon-Worker-Pool...")
|
|
||||||
pool.shutdown()
|
|
||||||
set_saxon_worker_pool(None)
|
|
||||||
logger.info("Saxon-Worker-Pool beendet")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Beenden des Saxon-Worker-Pools: {e}")
|
|
||||||
|
|
||||||
def _initialize_fop_worker_pool(self):
|
|
||||||
"""Initialisiert den FOP-Worker-Pool für schnelle PDF-Generierung."""
|
|
||||||
try:
|
|
||||||
# Shutdown vorherigen Pool falls vorhanden
|
|
||||||
self._shutdown_fop_worker_pool()
|
|
||||||
|
|
||||||
# Prüfe ob FopWorkerPool aktiviert ist
|
|
||||||
if not app_settings.use_fop_worker_pool:
|
|
||||||
logger.info("FopWorkerPool deaktiviert - Verwende Fallback-Modus (subprocess)")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.project:
|
|
||||||
logger.warning("Kein Projekt geladen, FOP-Worker-Pool nicht initialisiert")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hole Tool-Konfigurationen
|
|
||||||
java_vm = next((vm for vm in app_settings.java_vms if vm.id == self.project.java_vm_id), None)
|
|
||||||
apache_fop = next((fop for fop in app_settings.apache_fops if fop.id == self.project.apache_fop_id), None)
|
|
||||||
|
|
||||||
if not java_vm or not apache_fop:
|
|
||||||
logger.warning("Java VM oder Apache FOP nicht gefunden, Pool nicht initialisiert")
|
|
||||||
return
|
|
||||||
|
|
||||||
# FOP-Konfigurationsdatei (falls vorhanden)
|
|
||||||
fop_config_file = None
|
|
||||||
if self.project.fop_config_dir:
|
|
||||||
fop_config_file = self.project.fop_config_dir / "fop.xconf"
|
|
||||||
else:
|
|
||||||
default_config = apache_fop.path_to_dir / "conf" / "fop.xconf"
|
|
||||||
if default_config.exists():
|
|
||||||
fop_config_file = default_config
|
|
||||||
|
|
||||||
# Importiere FopWorkerPool
|
|
||||||
from fop_pool import FopWorkerPool
|
|
||||||
|
|
||||||
# Erstelle Worker-Pool
|
|
||||||
num_workers = app_settings.max_workers
|
|
||||||
log_dir = self.project.project_dir / "tmp"
|
|
||||||
pool = FopWorkerPool(
|
|
||||||
num_workers=num_workers,
|
|
||||||
java_vm_path=java_vm.path_to_binary_file,
|
|
||||||
apache_fop_dir=apache_fop.path_to_dir,
|
|
||||||
fop_config_file=fop_config_file,
|
|
||||||
log_dir=log_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setze globalen Pool
|
|
||||||
set_fop_worker_pool(pool)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"FOP-Worker-Pool initialisiert: {num_workers} Worker "
|
|
||||||
f"(erwartet: {num_workers}x schneller für PDF-Generierung)"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Initialisieren des FOP-Worker-Pools: {e}")
|
|
||||||
logger.info("Fallback auf subprocess-Modus")
|
|
||||||
# Kein Pool ist OK - Fallback auf subprocess
|
|
||||||
|
|
||||||
def _shutdown_fop_worker_pool(self):
|
|
||||||
"""Beendet den FOP-Worker-Pool sauber."""
|
|
||||||
try:
|
|
||||||
pool = get_fop_worker_pool()
|
|
||||||
if pool:
|
|
||||||
logger.info("Beende FOP-Worker-Pool...")
|
|
||||||
pool.shutdown()
|
|
||||||
set_fop_worker_pool(None)
|
|
||||||
logger.info("FOP-Worker-Pool beendet")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Beenden des FOP-Worker-Pools: {e}")
|
|
||||||
|
|
||||||
def _show_worker_pool_metrics(self):
|
|
||||||
"""Zeigt den Worker-Pool-Metriken-Dialog an."""
|
|
||||||
try:
|
|
||||||
from ui.WorkerPoolMetricsDialog import WorkerPoolMetricsDialog
|
|
||||||
|
|
||||||
dialog = WorkerPoolMetricsDialog(self)
|
|
||||||
dialog.exec()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Öffnen des Metriken-Dialogs: {e}")
|
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Öffnen des Metriken-Dialogs:\n{str(e)}")
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
"""
|
|
||||||
Thread-Klassen für asynchrone Operationen in DocuMentor.
|
|
||||||
|
|
||||||
Dieses Modul enthält alle QThread-Klassen, die für Hintergrundoperationen verwendet werden:
|
|
||||||
- XmlHashCalculatorThread: Berechnung von blake2b-Hashes für XML-Dateien
|
|
||||||
- XmlBatchProcessingThread: Batch-Verarbeitung von XML-Dateien
|
|
||||||
- TransformationThread: Ausführung von XSL-Transformationen
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from PySide6.QtCore import QThread, Signal
|
|
||||||
|
|
||||||
from conf import TreeNode, XslFile, XmlFile
|
|
||||||
from transform import TransformationJob
|
|
||||||
from utils import calculate_blake2b_hash
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class XmlHashCalculatorThread(QThread):
|
|
||||||
"""
|
|
||||||
Thread für die asynchrone Berechnung von blake2b-Hash-Werten für XML-Dateien.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Signale für die Kommunikation mit dem Haupt-Thread
|
|
||||||
hash_calculated = Signal(object, str) # xml_file_object, hash_value
|
|
||||||
calculation_finished = Signal(int, int) # processed_count, total_count
|
|
||||||
error_occurred = Signal(str, str) # xml_file_path, error_message
|
|
||||||
|
|
||||||
def __init__(self, project_dir: Path, xml_files: list[XmlFile]):
|
|
||||||
"""
|
|
||||||
Initialisiert den Hash-Berechnungs-Thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_dir: Pfad zum Projekt-Verzeichnis
|
|
||||||
xml_files: Liste der XmlFile-Objekte für die Hash-Berechnung
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.project_dir = project_dir
|
|
||||||
self.xml_files = xml_files
|
|
||||||
self.processed_count = 0
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Führt die Hash-Berechnung für alle XML-Dateien aus.
|
|
||||||
"""
|
|
||||||
logger.info(f"Starte Hash-Berechnung für {len(self.xml_files)} XML-Dateien")
|
|
||||||
|
|
||||||
for xml_file in self.xml_files:
|
|
||||||
try:
|
|
||||||
# Prüfe ob hashsum bereits vorhanden ist
|
|
||||||
if xml_file.hashsum:
|
|
||||||
logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
|
|
||||||
self.processed_count += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Berechne Hash für die XML-Datei
|
|
||||||
xml_file_path = self.project_dir / xml_file.xml
|
|
||||||
hash_value = self._calculate_blake2b_hash(xml_file_path)
|
|
||||||
|
|
||||||
if hash_value:
|
|
||||||
# Sende Signal mit berechnetem Hash
|
|
||||||
self.hash_calculated.emit(xml_file, hash_value)
|
|
||||||
logger.debug(f"Hash berechnet für {xml_file.xml}: {hash_value}")
|
|
||||||
|
|
||||||
self.processed_count += 1
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler bei Hash-Berechnung für {xml_file.xml}: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
self.error_occurred.emit(str(xml_file.xml), error_msg)
|
|
||||||
self.processed_count += 1
|
|
||||||
|
|
||||||
# Sende Abschluss-Signal
|
|
||||||
self.calculation_finished.emit(self.processed_count, len(self.xml_files))
|
|
||||||
logger.info(f"Hash-Berechnung abgeschlossen: {self.processed_count}/{len(self.xml_files)} verarbeitet")
|
|
||||||
|
|
||||||
def _calculate_blake2b_hash(self, file_path: Path) -> str | None:
|
|
||||||
"""Berechnet den blake2b-Hash einer XML-Datei."""
|
|
||||||
return calculate_blake2b_hash(file_path)
|
|
||||||
|
|
||||||
|
|
||||||
class XmlBatchProcessingThread(QThread):
|
|
||||||
"""
|
|
||||||
Thread für die asynchrone Batch-Verarbeitung von mehreren XML-Dateien.
|
|
||||||
Verarbeitet XML-Dateien mit Hash-Berechnung, Duplikatserkennung und Dateikopieren.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Signale für die Kommunikation mit dem Haupt-Thread
|
|
||||||
progress_update = Signal(int, int, str) # current, total, current_file_name
|
|
||||||
file_processed = Signal(dict) # result dictionary
|
|
||||||
processing_finished = Signal(dict) # stats dictionary
|
|
||||||
error_occurred = Signal(str) # error_message
|
|
||||||
|
|
||||||
def __init__(self, xml_files: list, selected_xsl_nodes: list, project_dir: Path, pdf_project):
|
|
||||||
"""
|
|
||||||
Initialisiert den Batch-Verarbeitungs-Thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_files: Liste von Pfaden zu XML-Dateien
|
|
||||||
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
||||||
project_dir: Pfad zum Projekt-Verzeichnis
|
|
||||||
pdf_project: ProjectData-Objekt
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.xml_files = xml_files
|
|
||||||
self.selected_xsl_nodes = selected_xsl_nodes
|
|
||||||
self.project_dir = project_dir
|
|
||||||
self.pdf_project = pdf_project
|
|
||||||
|
|
||||||
# Statistiken
|
|
||||||
self.stats = {
|
|
||||||
"total": len(xml_files),
|
|
||||||
"processed": 0,
|
|
||||||
"new_added": 0,
|
|
||||||
"existing_added": 0,
|
|
||||||
"already_assigned": 0,
|
|
||||||
"cancelled": 0,
|
|
||||||
"errors": 0,
|
|
||||||
"error_messages": [],
|
|
||||||
"renamed_files": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Führt die Batch-Verarbeitung aller XML-Dateien aus.
|
|
||||||
"""
|
|
||||||
logger.info(f"Starte Batch-Verarbeitung für {len(self.xml_files)} XML-Dateien")
|
|
||||||
|
|
||||||
for i, xml_file_path in enumerate(self.xml_files):
|
|
||||||
try:
|
|
||||||
# Sende Progress-Update
|
|
||||||
self.progress_update.emit(i + 1, len(self.xml_files), xml_file_path.name)
|
|
||||||
|
|
||||||
# Prüfe ob die Datei existiert
|
|
||||||
if not xml_file_path.exists():
|
|
||||||
self.stats["errors"] += 1
|
|
||||||
self.stats["error_messages"].append(f"{xml_file_path.name}: Datei existiert nicht")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Verarbeite die XML-Datei
|
|
||||||
result = self._process_xml_file(xml_file_path)
|
|
||||||
|
|
||||||
# Aktualisiere Statistiken
|
|
||||||
self._update_stats(result)
|
|
||||||
|
|
||||||
# Sende Ergebnis
|
|
||||||
self.file_processed.emit(result)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Fehler bei {xml_file_path.name}: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
self.stats["errors"] += 1
|
|
||||||
self.stats["error_messages"].append(error_msg)
|
|
||||||
|
|
||||||
# Sende Abschluss-Signal mit Statistiken
|
|
||||||
self.processing_finished.emit(self.stats)
|
|
||||||
logger.info(f"Batch-Verarbeitung abgeschlossen: {self.stats['processed']}/{self.stats['total']} verarbeitet")
|
|
||||||
|
|
||||||
def _process_xml_file(self, xml_file_path: Path) -> dict:
|
|
||||||
"""
|
|
||||||
Verarbeitet eine einzelne XML-Datei.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xml_file_path: Pfad zur XML-Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Ergebnis-Dictionary mit Status
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 1. Hash berechnen
|
|
||||||
file_hash = self._calculate_hash_for_file(xml_file_path)
|
|
||||||
if not file_hash:
|
|
||||||
logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")
|
|
||||||
|
|
||||||
# 2. Prüfe auf Hash-Duplikat
|
|
||||||
existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None
|
|
||||||
|
|
||||||
if existing_xml:
|
|
||||||
# Hash-Match: Ordne vorhandene Datei zu
|
|
||||||
return self._assign_existing_xml_to_nodes(existing_xml)
|
|
||||||
else:
|
|
||||||
# Keine Duplikate: Verarbeite als neue Datei
|
|
||||||
return self._process_new_xml_file(xml_file_path, file_hash)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"status": "error", "error_msg": str(e)}
|
|
||||||
|
|
||||||
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
|
|
||||||
"""Berechnet blake2b Hash für eine Datei."""
|
|
||||||
return calculate_blake2b_hash(file_path)
|
|
||||||
|
|
||||||
def _find_xml_file_by_hash(self, hash_value: str) -> XmlFile | None:
|
|
||||||
"""Sucht eine XML-Datei anhand ihres Hash-Werts."""
|
|
||||||
if not hash_value or not self.pdf_project.nodes:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def search_recursive(nodes):
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, XslFile) and node.xmls:
|
|
||||||
for xml_file in node.xmls:
|
|
||||||
if xml_file.hashsum == hash_value:
|
|
||||||
return xml_file
|
|
||||||
elif isinstance(node, TreeNode) and node.children:
|
|
||||||
found = search_recursive(node.children)
|
|
||||||
if found:
|
|
||||||
return found
|
|
||||||
return None
|
|
||||||
|
|
||||||
return search_recursive(self.pdf_project.nodes)
|
|
||||||
|
|
||||||
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile) -> dict:
|
|
||||||
"""Ordnet eine vorhandene XML-Datei den Knoten zu."""
|
|
||||||
try:
|
|
||||||
added_count = 0
|
|
||||||
|
|
||||||
for xsl_node in self.selected_xsl_nodes:
|
|
||||||
already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls)
|
|
||||||
|
|
||||||
if not already_assigned:
|
|
||||||
new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum)
|
|
||||||
xsl_node.xmls.append(new_xml_ref)
|
|
||||||
added_count += 1
|
|
||||||
|
|
||||||
if added_count > 0:
|
|
||||||
return {
|
|
||||||
"status": "existing_added",
|
|
||||||
"added_count": added_count,
|
|
||||||
"existing_file": existing_xml.xml.name,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"status": "error", "error_msg": str(e)}
|
|
||||||
|
|
||||||
def _process_new_xml_file(self, xml_file_path: Path, file_hash: str | None) -> dict:
|
|
||||||
"""Verarbeitet eine neue XML-Datei."""
|
|
||||||
try:
|
|
||||||
# Erstelle xml-Ordner
|
|
||||||
xml_dir = self.project_dir / "xml"
|
|
||||||
xml_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Bestimme Ziel-Pfad
|
|
||||||
target_xml_path = xml_dir / xml_file_path.name
|
|
||||||
|
|
||||||
# Set aller bereits im Projekt verwendeten Dateinamen — einmalig aufbauen
|
|
||||||
used_filenames = self._collect_project_filenames()
|
|
||||||
|
|
||||||
# Prüfe auf Namenskonflikte und generiere ggf. alternativen Namen
|
|
||||||
original_name = xml_file_path.name
|
|
||||||
counter = 1
|
|
||||||
while target_xml_path.exists() or (Path("xml") / target_xml_path.name) in used_filenames:
|
|
||||||
# Generiere alternativen Namen
|
|
||||||
stem = xml_file_path.stem
|
|
||||||
suffix = xml_file_path.suffix
|
|
||||||
target_xml_path = xml_dir / f"{stem}_{counter}{suffix}"
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# Sicherheit: Maximal 1000 Versuche
|
|
||||||
if counter > 1000:
|
|
||||||
return {"status": "error", "error_msg": "Konnte keinen eindeutigen Dateinamen finden"}
|
|
||||||
|
|
||||||
# Kopiere Datei
|
|
||||||
shutil.copy2(xml_file_path, target_xml_path)
|
|
||||||
|
|
||||||
# Erstelle relatives Path
|
|
||||||
relative_xml_path = Path("xml") / target_xml_path.name
|
|
||||||
|
|
||||||
# Füge zu XSL-Knoten hinzu
|
|
||||||
added_count = 0
|
|
||||||
for xsl_node in self.selected_xsl_nodes:
|
|
||||||
existing_xml = any(xml_file.xml == relative_xml_path for xml_file in xsl_node.xmls)
|
|
||||||
|
|
||||||
if not existing_xml:
|
|
||||||
new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash)
|
|
||||||
xsl_node.xmls.append(new_xml_file)
|
|
||||||
added_count += 1
|
|
||||||
|
|
||||||
if added_count > 0:
|
|
||||||
return {
|
|
||||||
"status": "new_added",
|
|
||||||
"added_count": added_count,
|
|
||||||
"new_file": target_xml_path.name,
|
|
||||||
"renamed_from": original_name if target_xml_path.name != original_name else None,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"status": "already_assigned", "added_count": 0, "new_file": target_xml_path.name}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"status": "error", "error_msg": str(e)}
|
|
||||||
|
|
||||||
def _collect_project_filenames(self) -> set[Path]:
|
|
||||||
"""Gibt ein Set aller im Projekt verwendeten XML-Dateipfade zurück."""
|
|
||||||
result: set[Path] = set()
|
|
||||||
|
|
||||||
def collect(nodes):
|
|
||||||
for node in nodes:
|
|
||||||
if isinstance(node, XslFile) and node.xmls:
|
|
||||||
for xml_file in node.xmls:
|
|
||||||
result.add(xml_file.xml)
|
|
||||||
elif isinstance(node, TreeNode) and node.children:
|
|
||||||
collect(node.children)
|
|
||||||
|
|
||||||
collect(self.pdf_project.nodes or [])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _update_stats(self, result: dict):
|
|
||||||
"""Aktualisiert die Statistiken."""
|
|
||||||
self.stats["processed"] += 1
|
|
||||||
|
|
||||||
status = result.get("status")
|
|
||||||
if status == "new_added":
|
|
||||||
self.stats["new_added"] += 1
|
|
||||||
if result.get("renamed_from"):
|
|
||||||
self.stats["renamed_files"].append(f"{result['renamed_from']} → {result['new_file']}")
|
|
||||||
elif status == "existing_added":
|
|
||||||
self.stats["existing_added"] += 1
|
|
||||||
elif status == "already_assigned":
|
|
||||||
self.stats["already_assigned"] += 1
|
|
||||||
elif status == "error":
|
|
||||||
self.stats["errors"] += 1
|
|
||||||
self.stats["error_messages"].append(result.get("error_msg", "Unbekannter Fehler"))
|
|
||||||
|
|
||||||
|
|
||||||
class TransformationThread(QThread):
|
|
||||||
"""
|
|
||||||
Thread für die asynchrone Ausführung von Transformations-Jobs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Signale für die Kommunikation mit dem Haupt-Thread
|
|
||||||
job_started = Signal(str, str) # xml_file_name, xsl_id_str
|
|
||||||
job_finished = Signal(dict) # result_dict
|
|
||||||
job_error = Signal(str, str, str) # xml_file_name, xsl_id_str, error_message
|
|
||||||
all_jobs_finished = Signal(int, int, float) # successful_count, total_count, total_duration
|
|
||||||
|
|
||||||
def __init__(self, jobs: list[TransformationJob], force: bool = False, max_workers: int = 8):
|
|
||||||
"""
|
|
||||||
Initialisiert den Transformations-Thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
jobs: Liste der TransformationJob-Objekte
|
|
||||||
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
|
|
||||||
max_workers: Maximale Anzahl paralleler Worker (Standard: 8)
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.jobs = jobs
|
|
||||||
self.force = force
|
|
||||||
self.max_workers = max_workers
|
|
||||||
self.successful_count = 0
|
|
||||||
|
|
||||||
def _process_single_job(self, job: TransformationJob) -> dict:
|
|
||||||
"""
|
|
||||||
Verarbeitet einen einzelnen Transformations-Job (Thread-safe).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: Der zu verarbeitende TransformationJob
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Ergebnis-Dictionary des Jobs
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Sende Start-Signal mit XSL-ID
|
|
||||||
xsl_id_str = "_".join(str(x) for x in job.xsl_id) if job.xsl_id else ""
|
|
||||||
self.job_started.emit(str(job.xml_file), xsl_id_str)
|
|
||||||
|
|
||||||
# Führe Transformations-Pipeline aus
|
|
||||||
result = job.run_full_pipeline(force=self.force)
|
|
||||||
|
|
||||||
# Sende Abschluss-Signal
|
|
||||||
self.job_finished.emit(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Unerwarteter Fehler bei Transformation: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
xsl_id_str = "_".join(str(x) for x in job.xsl_id) if job.xsl_id else ""
|
|
||||||
self.job_error.emit(str(job.xml_file), xsl_id_str, error_msg)
|
|
||||||
return {"success": False, "error": error_msg}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Führt alle Transformations-Jobs parallel aus mit ThreadPoolExecutor.
|
|
||||||
"""
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from datetime import datetime
|
|
||||||
import threading
|
|
||||||
|
|
||||||
start_time = datetime.now()
|
|
||||||
logger.info(f"Starte parallele Transformation von {len(self.jobs)} Jobs mit {self.max_workers} Workern")
|
|
||||||
|
|
||||||
# Thread-sicherer Counter
|
|
||||||
successful_lock = threading.Lock()
|
|
||||||
|
|
||||||
# Verwende ThreadPoolExecutor für parallele Verarbeitung
|
|
||||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
||||||
# Starte alle Jobs
|
|
||||||
future_to_job = {executor.submit(self._process_single_job, job): job for job in self.jobs}
|
|
||||||
|
|
||||||
# Warte auf Abschluss und sammle Ergebnisse
|
|
||||||
for future in as_completed(future_to_job):
|
|
||||||
try:
|
|
||||||
result = future.result()
|
|
||||||
if result.get("success", False):
|
|
||||||
with successful_lock:
|
|
||||||
self.successful_count += 1
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Verarbeiten des Future: {e}")
|
|
||||||
|
|
||||||
# Berechne Gesamtdauer
|
|
||||||
total_duration = (datetime.now() - start_time).total_seconds()
|
|
||||||
|
|
||||||
# Sende Abschluss-Signal für alle Jobs mit Gesamtdauer
|
|
||||||
self.all_jobs_finished.emit(self.successful_count, len(self.jobs), total_duration)
|
|
||||||
logger.info(
|
|
||||||
f"Transformation abgeschlossen: {self.successful_count}/{len(self.jobs)} erfolgreich ({total_duration:.2f}s) "
|
|
||||||
f"[{len(self.jobs) / total_duration:.2f} Jobs/s mit {self.max_workers} Workern]"
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"""
|
|
||||||
Gemeinsame Utility-Funktionen für DocuMentor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
HASH_PREFIX = "blake2b"
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_blake2b_hash(file_path: Path) -> str | None:
|
|
||||||
"""
|
|
||||||
Berechnet den blake2b-Hash einer Datei.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Pfad zur Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not file_path.exists():
|
|
||||||
logger.warning(f"Datei für Hash-Berechnung nicht gefunden: {file_path}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
hash_obj = hashlib.blake2b()
|
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
for chunk in iter(lambda: f.read(8192), b""):
|
|
||||||
hash_obj.update(chunk)
|
|
||||||
|
|
||||||
return f"{HASH_PREFIX}:{hash_obj.hexdigest()}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
|
|
||||||
return None
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
"""
|
|
||||||
Worker Pool Performance-Metriken.
|
|
||||||
|
|
||||||
Gemeinsame Datenstrukturen für Performance- und Ressourcenverbrauch-Metriken
|
|
||||||
der Worker-Pools (Saxon, FOP).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WorkerPoolMetrics:
|
|
||||||
"""
|
|
||||||
Metriken für Worker-Pool Performance und Ressourcenverbrauch.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Kompilierungszeit
|
|
||||||
compilation_time_seconds: float = 0.0
|
|
||||||
|
|
||||||
# Worker-Start-Zeiten
|
|
||||||
worker_start_times: list[float] = field(default_factory=list)
|
|
||||||
total_worker_start_time_seconds: float = 0.0
|
|
||||||
average_worker_start_time_seconds: float = 0.0
|
|
||||||
|
|
||||||
# RAM-Verbrauch (in MB)
|
|
||||||
ram_before_transform_mb_per_worker: list[float] = field(default_factory=list)
|
|
||||||
ram_after_transform_mb_per_worker: list[float] = field(default_factory=list)
|
|
||||||
total_ram_before_mb: float = 0.0
|
|
||||||
total_ram_after_mb: float = 0.0
|
|
||||||
average_ram_before_mb: float = 0.0
|
|
||||||
average_ram_after_mb: float = 0.0
|
|
||||||
|
|
||||||
# XSL-Kompilierungszeiten (nur für Saxon)
|
|
||||||
xsl_compilation_times: list[float] = field(default_factory=list)
|
|
||||||
total_xsl_compilation_time_seconds: float = 0.0
|
|
||||||
average_xsl_compilation_time_seconds: float = 0.0
|
|
||||||
|
|
||||||
def calculate_aggregates(self):
|
|
||||||
"""Berechnet aggregierte Werte (Summen, Durchschnitte)."""
|
|
||||||
# Worker-Start-Zeiten
|
|
||||||
if self.worker_start_times:
|
|
||||||
self.total_worker_start_time_seconds = sum(self.worker_start_times)
|
|
||||||
self.average_worker_start_time_seconds = self.total_worker_start_time_seconds / len(
|
|
||||||
self.worker_start_times
|
|
||||||
)
|
|
||||||
|
|
||||||
# RAM vor Transformation
|
|
||||||
if self.ram_before_transform_mb_per_worker:
|
|
||||||
self.total_ram_before_mb = sum(self.ram_before_transform_mb_per_worker)
|
|
||||||
self.average_ram_before_mb = self.total_ram_before_mb / len(self.ram_before_transform_mb_per_worker)
|
|
||||||
|
|
||||||
# RAM nach Transformation
|
|
||||||
if self.ram_after_transform_mb_per_worker:
|
|
||||||
self.total_ram_after_mb = sum(self.ram_after_transform_mb_per_worker)
|
|
||||||
self.average_ram_after_mb = self.total_ram_after_mb / len(self.ram_after_transform_mb_per_worker)
|
|
||||||
|
|
||||||
# XSL-Kompilierungszeiten
|
|
||||||
if self.xsl_compilation_times:
|
|
||||||
self.total_xsl_compilation_time_seconds = sum(self.xsl_compilation_times)
|
|
||||||
self.average_xsl_compilation_time_seconds = self.total_xsl_compilation_time_seconds / len(
|
|
||||||
self.xsl_compilation_times
|
|
||||||
)
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
"""
|
|
||||||
BaseWorkerPool - Gemeinsame Basisklasse für JVM-Worker-Pools.
|
|
||||||
|
|
||||||
Enthält alle gemeinsamen Methoden für SaxonWorkerPool, SaxonWorkerPoolS9Api
|
|
||||||
und FopWorkerPool: Kompilierung, Worker-Start, RAM-Messung und Shutdown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import logging
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# Verhindert Konsolenfenster bei Subprozessen in PyInstaller-EXE (Windows)
|
|
||||||
_SUBPROCESS_FLAGS = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
||||||
|
|
||||||
import psutil
|
|
||||||
|
|
||||||
from worker_metrics import WorkerPoolMetrics
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_CLASSPATH_SEP = ";" if sys.platform == "win32" else ":"
|
|
||||||
|
|
||||||
|
|
||||||
def build_jar_classpath(base_dir: Path, include_lib: bool = True) -> str:
|
|
||||||
"""
|
|
||||||
Baut einen Classpath aus allen JAR-Dateien in einem Verzeichnis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_dir: Hauptverzeichnis mit JAR-Dateien
|
|
||||||
include_lib: Ob das lib/-Unterverzeichnis ebenfalls einbezogen wird
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Classpath-String (plattformspezifischer Trennzeichen)
|
|
||||||
"""
|
|
||||||
all_jars = glob.glob(str(base_dir / "*.jar"))
|
|
||||||
if include_lib:
|
|
||||||
lib_dir = base_dir / "lib"
|
|
||||||
if lib_dir.exists():
|
|
||||||
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
|
|
||||||
return _CLASSPATH_SEP.join(all_jars)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseWorkerPool(ABC):
|
|
||||||
"""
|
|
||||||
Abstrakte Basisklasse für JVM-basierte Worker-Pools.
|
|
||||||
|
|
||||||
Konkrete Unterklassen müssen folgende Properties/Methoden implementieren:
|
|
||||||
- _pool_name: Anzeigename für Log-Meldungen
|
|
||||||
- _java_source_code: Java-Quellcode-String
|
|
||||||
- _java_class_name: Name der Java-Klasse (ohne .java)
|
|
||||||
- _temp_dir_prefix: Präfix für das temporäre Verzeichnis
|
|
||||||
- _worker_init_sleep: Wartezeit nach Worker-Start (Sekunden)
|
|
||||||
- _get_classpath(): Gibt den Classpath für Kompilierung und Ausführung zurück
|
|
||||||
- _build_worker_cmd(full_classpath): Gibt die Worker-Startkommando-Liste zurück
|
|
||||||
- _stderr_log_name(i): Gibt den Dateinamen der stderr-Logdatei für Worker i zurück
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, num_workers: int, java_vm_path: Path, log_dir: Optional[Path] = None):
|
|
||||||
self.num_workers = num_workers
|
|
||||||
self.java_vm_path = java_vm_path
|
|
||||||
self.log_dir = log_dir
|
|
||||||
|
|
||||||
# Worker-Prozesse
|
|
||||||
self.workers: list[subprocess.Popen] = []
|
|
||||||
self.worker_locks: list[threading.Lock] = []
|
|
||||||
self._worker_stderr_files: list = []
|
|
||||||
|
|
||||||
# Temporäres Verzeichnis für kompilierte Java-Klasse
|
|
||||||
self.temp_dir: Optional[Path] = None
|
|
||||||
self.worker_class_path: Optional[Path] = None
|
|
||||||
self.worker_log_dir: Optional[Path] = None
|
|
||||||
|
|
||||||
# Performance-Metriken
|
|
||||||
self.metrics = WorkerPoolMetrics()
|
|
||||||
|
|
||||||
# --- Abstrakte Schnittstelle ---
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _pool_name(self) -> str:
|
|
||||||
"""Anzeigename für Log-Meldungen (z.B. 'Saxon', 'FOP')."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _java_source_code(self) -> str:
|
|
||||||
"""Java-Quellcode-String der Worker-Klasse."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _java_class_name(self) -> str:
|
|
||||||
"""Name der Java-Klasse ohne Suffix (z.B. 'SaxonWorker')."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _temp_dir_prefix(self) -> str:
|
|
||||||
"""Präfix für das temporäre Verzeichnis (z.B. 'saxon_worker_')."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _worker_init_sleep(self) -> float:
|
|
||||||
"""Wartezeit in Sekunden nach Worker-Start."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _get_classpath(self) -> str:
|
|
||||||
"""Gibt den Classpath-String für Kompilierung und Worker-Start zurück."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
|
|
||||||
"""Gibt die Worker-Startkommando-Liste zurück."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _stderr_log_name(self, i: int) -> str:
|
|
||||||
"""Gibt den Dateinamen der stderr-Logdatei für Worker i zurück."""
|
|
||||||
|
|
||||||
# --- Gemeinsame Implementierungen ---
|
|
||||||
|
|
||||||
def _compile_worker_class(self):
|
|
||||||
"""Kompiliert die Worker-Java-Klasse ins temporäre Verzeichnis."""
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
self.temp_dir = Path(tempfile.mkdtemp(prefix=self._temp_dir_prefix))
|
|
||||||
|
|
||||||
java_file = self.temp_dir / f"{self._java_class_name}.java"
|
|
||||||
java_file.write_text(self._java_source_code, encoding="utf-8")
|
|
||||||
|
|
||||||
classpath = self._get_classpath()
|
|
||||||
javac_cmd = [str(self.java_vm_path).replace("java", "javac"), "-cp", classpath, str(java_file)]
|
|
||||||
|
|
||||||
logger.debug(f"Kompiliere {self._java_class_name}: {' '.join(javac_cmd[:3])}...")
|
|
||||||
|
|
||||||
result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30, creationflags=_SUBPROCESS_FLAGS)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}")
|
|
||||||
|
|
||||||
self.worker_class_path = self.temp_dir
|
|
||||||
self.metrics.compilation_time_seconds = time.time() - start_time
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"{self._java_class_name} erfolgreich kompiliert: {self.temp_dir} "
|
|
||||||
f"({self.metrics.compilation_time_seconds:.3f}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Kompilieren von {self._java_class_name}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _start_workers(self):
|
|
||||||
"""Startet N Worker-Prozesse."""
|
|
||||||
classpath = self._get_classpath()
|
|
||||||
full_classpath = str(self.worker_class_path) + _CLASSPATH_SEP + classpath
|
|
||||||
|
|
||||||
self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir
|
|
||||||
if self.log_dir:
|
|
||||||
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
cmd = self._build_worker_cmd(full_classpath)
|
|
||||||
|
|
||||||
for i in range(self.num_workers):
|
|
||||||
worker_start_time = time.time()
|
|
||||||
try:
|
|
||||||
stderr_log = self.worker_log_dir / self._stderr_log_name(i)
|
|
||||||
stderr_file = open(stderr_log, "w", encoding="utf-8")
|
|
||||||
self._worker_stderr_files.append(stderr_file)
|
|
||||||
|
|
||||||
process = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=stderr_file,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
creationflags=_SUBPROCESS_FLAGS,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.workers.append(process)
|
|
||||||
self.worker_locks.append(threading.Lock())
|
|
||||||
|
|
||||||
logger.debug(f"{self._pool_name} Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
|
|
||||||
|
|
||||||
time.sleep(self._worker_init_sleep)
|
|
||||||
|
|
||||||
if process.poll() is not None:
|
|
||||||
stderr_file.close()
|
|
||||||
with open(stderr_log, "r") as f:
|
|
||||||
stderr_content = f.read()
|
|
||||||
raise RuntimeError(
|
|
||||||
f"{self._pool_name} Worker {i} ist sofort beendet "
|
|
||||||
f"(Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.metrics.worker_start_times.append(time.time() - worker_start_time)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Starten von {self._pool_name} Worker {i}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.metrics.calculate_aggregates()
|
|
||||||
logger.info(
|
|
||||||
f"{len(self.workers)} {self._pool_name}-Worker erfolgreich gestartet "
|
|
||||||
f"(Summe: {self.metrics.total_worker_start_time_seconds:.3f}s, "
|
|
||||||
f"Durchschnitt: {self.metrics.average_worker_start_time_seconds:.3f}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _acquire_worker(self) -> int:
|
|
||||||
"""Gibt den Index eines freien Workers zurück (blockiert bis einer frei ist)."""
|
|
||||||
for i, lock in enumerate(self.worker_locks):
|
|
||||||
if lock.acquire(blocking=False):
|
|
||||||
return i
|
|
||||||
# Kein freier Worker — warte auf den ersten verfügbaren
|
|
||||||
for i, lock in enumerate(self.worker_locks):
|
|
||||||
lock.acquire()
|
|
||||||
return i
|
|
||||||
|
|
||||||
def _read_stderr_log(self, worker_idx: int, tail: int = 0) -> str:
|
|
||||||
"""Liest die stderr-Logdatei eines Workers (optional nur die letzten N Zeichen)."""
|
|
||||||
try:
|
|
||||||
stderr_log = self.worker_log_dir / self._stderr_log_name(worker_idx)
|
|
||||||
with open(stderr_log, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
return content[-tail:] if tail else content
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def measure_ram_usage(self) -> tuple[float, float, list[float]]:
|
|
||||||
"""
|
|
||||||
Misst den aktuellen RAM-Verbrauch aller Worker-Prozesse.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (total_mb, average_mb, per_worker_mb_list)
|
|
||||||
"""
|
|
||||||
ram_per_worker = []
|
|
||||||
for i, worker in enumerate(self.workers):
|
|
||||||
try:
|
|
||||||
if worker.poll() is None:
|
|
||||||
process = psutil.Process(worker.pid)
|
|
||||||
ram_mb = process.memory_info().rss / (1024 * 1024)
|
|
||||||
ram_per_worker.append(ram_mb)
|
|
||||||
else:
|
|
||||||
logger.warning(f"{self._pool_name} Worker {i} ist nicht mehr aktiv (kann RAM nicht messen)")
|
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
||||||
logger.warning(f"Konnte RAM für {self._pool_name} Worker {i} nicht messen: {e}")
|
|
||||||
|
|
||||||
total_ram = sum(ram_per_worker)
|
|
||||||
average_ram = total_ram / len(ram_per_worker) if ram_per_worker else 0.0
|
|
||||||
return total_ram, average_ram, ram_per_worker
|
|
||||||
|
|
||||||
def capture_ram_before_transform(self):
|
|
||||||
"""Erfasst RAM-Verbrauch vor der ersten Transformation."""
|
|
||||||
total, average, per_worker = self.measure_ram_usage()
|
|
||||||
self.metrics.ram_before_transform_mb_per_worker = per_worker
|
|
||||||
self.metrics.total_ram_before_mb = total
|
|
||||||
self.metrics.average_ram_before_mb = average
|
|
||||||
logger.info(
|
|
||||||
f"RAM vor Transformation ({self._pool_name}): {total:.1f} MB (Ø {average:.1f} MB/Worker)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def capture_ram_after_transform(self):
|
|
||||||
"""Erfasst RAM-Verbrauch nach allen Transformationen."""
|
|
||||||
total, average, per_worker = self.measure_ram_usage()
|
|
||||||
self.metrics.ram_after_transform_mb_per_worker = per_worker
|
|
||||||
self.metrics.total_ram_after_mb = total
|
|
||||||
self.metrics.average_ram_after_mb = average
|
|
||||||
logger.info(
|
|
||||||
f"RAM nach Transformation ({self._pool_name}): {total:.1f} MB (Ø {average:.1f} MB/Worker)"
|
|
||||||
)
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
"""Beendet alle Worker-Prozesse sauber."""
|
|
||||||
logger.info(f"Beende {self._pool_name}-Worker-Pool...")
|
|
||||||
|
|
||||||
for i, worker in enumerate(self.workers):
|
|
||||||
try:
|
|
||||||
if worker.stdin and not worker.stdin.closed:
|
|
||||||
worker.stdin.write("EXIT\n")
|
|
||||||
worker.stdin.flush()
|
|
||||||
worker.wait(timeout=2)
|
|
||||||
logger.debug(f"{self._pool_name} Worker {i} beendet")
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
worker.kill()
|
|
||||||
logger.warning(f"{self._pool_name} Worker {i} musste gekillt werden")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Beenden von {self._pool_name} Worker {i}: {e}")
|
|
||||||
|
|
||||||
for f in self._worker_stderr_files:
|
|
||||||
try:
|
|
||||||
f.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._worker_stderr_files.clear()
|
|
||||||
|
|
||||||
if self.temp_dir and self.temp_dir.exists():
|
|
||||||
try:
|
|
||||||
shutil.rmtree(self.temp_dir)
|
|
||||||
logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}")
|
|
||||||
|
|
||||||
logger.info(f"{self._pool_name}-Worker-Pool beendet")
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.shutdown()
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
"""
|
|
||||||
XSL-Abhängigkeitsgraph für die Erkennung transitiver Imports/Includes.
|
|
||||||
|
|
||||||
Dieses Modul parst XSL-Dateien und baut einen Abhängigkeitsgraph auf,
|
|
||||||
um alle direkt und transitiv importierten/inkludierten Dateien zu ermitteln.
|
|
||||||
Der Graph wird im Speicher gehalten und bei Änderungen (mtime) automatisch invalidiert.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from lxml import etree
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_import_include_hrefs(content: str) -> list[str]:
|
|
||||||
"""Parst XSL-Dateiinhalt und gibt alle href-Werte von xsl:import/xsl:include zurück."""
|
|
||||||
try:
|
|
||||||
root = etree.fromstring(content.encode("utf-8"))
|
|
||||||
except etree.XMLSyntaxError:
|
|
||||||
return []
|
|
||||||
hrefs = []
|
|
||||||
for tag in ("import", "include"):
|
|
||||||
for elem in root.iter(f"{{{_XSL_NAMESPACE}}}{tag}"):
|
|
||||||
href = elem.get("href")
|
|
||||||
if href:
|
|
||||||
hrefs.append(href)
|
|
||||||
return hrefs
|
|
||||||
|
|
||||||
|
|
||||||
class XslDependencyGraph:
|
|
||||||
"""
|
|
||||||
Verwaltet einen Cache von XSL-Abhängigkeiten (import/include).
|
|
||||||
|
|
||||||
Für jede XSL-Datei wird die Menge aller transitiv referenzierten
|
|
||||||
XSL-Dateien gespeichert. Der Cache wird über die mtime der Dateien
|
|
||||||
automatisch invalidiert.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Cache: xsl_path -> (mtime_bei_parse, set[Path] aller Abhängigkeiten)
|
|
||||||
self._cache: dict[Path, tuple[float, set[Path]]] = {}
|
|
||||||
|
|
||||||
def get_dependencies(self, xsl_file: Path) -> set[Path]:
|
|
||||||
"""
|
|
||||||
Gibt alle transitiven Abhängigkeiten einer XSL-Datei zurück.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
set[Path]: Menge aller transitiv importierten/inkludierten XSL-Dateien
|
|
||||||
"""
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
|
|
||||||
if not xsl_file.exists():
|
|
||||||
return set()
|
|
||||||
|
|
||||||
current_mtime = xsl_file.stat().st_mtime
|
|
||||||
|
|
||||||
# Cache-Hit prüfen
|
|
||||||
if xsl_file in self._cache:
|
|
||||||
cached_mtime, cached_deps = self._cache[xsl_file]
|
|
||||||
if cached_mtime == current_mtime:
|
|
||||||
return cached_deps
|
|
||||||
|
|
||||||
# Neu parsen
|
|
||||||
deps = self._resolve_recursive(xsl_file, set())
|
|
||||||
self._cache[xsl_file] = (current_mtime, deps)
|
|
||||||
logger.debug(f"XSL-Abhängigkeiten aufgelöst für {xsl_file.name}: {len(deps)} Abhängigkeit(en)")
|
|
||||||
return deps
|
|
||||||
|
|
||||||
def _resolve_recursive(self, xsl_file: Path, visited: set[Path]) -> set[Path]:
|
|
||||||
"""
|
|
||||||
Löst rekursiv alle import/include-Referenzen auf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
|
||||||
visited: Bereits besuchte Dateien (Zykluserkennung)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
set[Path]: Alle transitiven Abhängigkeiten (ohne die Datei selbst)
|
|
||||||
"""
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
|
|
||||||
if xsl_file in visited or not xsl_file.exists():
|
|
||||||
return set()
|
|
||||||
|
|
||||||
visited.add(xsl_file)
|
|
||||||
result: set[Path] = set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = xsl_file.read_text(encoding="utf-8", errors="replace")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Finde alle import/include-Referenzen
|
|
||||||
for href in _parse_import_include_hrefs(content):
|
|
||||||
referenced_path = (xsl_file.parent / href).resolve()
|
|
||||||
|
|
||||||
if referenced_path.exists() and referenced_path not in visited:
|
|
||||||
result.add(referenced_path)
|
|
||||||
# Rekursiv Abhängigkeiten der referenzierten Datei auflösen
|
|
||||||
result.update(self._resolve_recursive(referenced_path, visited))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_reverse_dependencies(self, xsl_file: Path, xsl_root_dir: Path) -> set[Path]:
|
|
||||||
"""
|
|
||||||
Gibt alle Dateien zurück, die die gegebene XSL-Datei direkt oder transitiv importieren/inkludieren.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
|
||||||
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien (für den Scan)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
set[Path]: Menge aller Dateien, die diese Datei importieren
|
|
||||||
"""
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
result: set[Path] = set()
|
|
||||||
|
|
||||||
# Scanne alle XSL-Dateien im Verzeichnis
|
|
||||||
if not xsl_root_dir.exists():
|
|
||||||
return result
|
|
||||||
|
|
||||||
for candidate in xsl_root_dir.rglob("*.xsl"):
|
|
||||||
candidate = candidate.resolve()
|
|
||||||
if candidate == xsl_file:
|
|
||||||
continue
|
|
||||||
deps = self.get_dependencies(candidate)
|
|
||||||
if xsl_file in deps:
|
|
||||||
result.add(candidate)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def build_full_graph(self, xsl_root_dir: Path) -> dict[Path, set[Path]]:
|
|
||||||
"""
|
|
||||||
Baut den vollständigen Abhängigkeitsgraph für alle XSL-Dateien in einem Verzeichnis auf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict[Path, set[Path]]: Mapping von XSL-Datei zu ihren Abhängigkeiten
|
|
||||||
"""
|
|
||||||
graph: dict[Path, set[Path]] = {}
|
|
||||||
|
|
||||||
if not xsl_root_dir.exists():
|
|
||||||
return graph
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
deps = self.get_dependencies(xsl_file)
|
|
||||||
graph[xsl_file] = deps
|
|
||||||
elapsed = time.perf_counter() - start
|
|
||||||
|
|
||||||
logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
|
|
||||||
return graph
|
|
||||||
|
|
||||||
def _get_direct_dependencies(self, xsl_file: Path) -> set[Path]:
|
|
||||||
"""
|
|
||||||
Gibt nur die direkten (nicht-transitiven) import/include-Abhängigkeiten zurück.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
set[Path]: Menge der direkt importierten/inkludierten XSL-Dateien
|
|
||||||
"""
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
result: set[Path] = set()
|
|
||||||
|
|
||||||
if not xsl_file.exists():
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = xsl_file.read_text(encoding="utf-8", errors="replace")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
|
|
||||||
return result
|
|
||||||
|
|
||||||
for href in _parse_import_include_hrefs(content):
|
|
||||||
referenced_path = (xsl_file.parent / href).resolve()
|
|
||||||
if referenced_path.exists():
|
|
||||||
result.add(referenced_path)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def build_direct_graph(self, xsl_root_dir: Path) -> dict[Path, set[Path]]:
|
|
||||||
"""
|
|
||||||
Baut einen Graph mit nur direkten (nicht-transitiven) Abhängigkeiten auf.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict[Path, set[Path]]: Mapping von XSL-Datei zu ihren direkten Abhängigkeiten
|
|
||||||
"""
|
|
||||||
graph: dict[Path, set[Path]] = {}
|
|
||||||
|
|
||||||
if not xsl_root_dir.exists():
|
|
||||||
return graph
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
graph[xsl_file] = self._get_direct_dependencies(xsl_file)
|
|
||||||
elapsed = time.perf_counter() - start
|
|
||||||
|
|
||||||
logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
|
|
||||||
return graph
|
|
||||||
|
|
||||||
def invalidate(self, xsl_file: Path | None = None):
|
|
||||||
"""
|
|
||||||
Invalidiert den Cache für eine bestimmte Datei oder den gesamten Cache.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
xsl_file: Pfad zur XSL-Datei oder None für vollständige Invalidierung
|
|
||||||
"""
|
|
||||||
if xsl_file is None:
|
|
||||||
self._cache.clear()
|
|
||||||
logger.debug("XSL-Abhängigkeitscache vollständig invalidiert")
|
|
||||||
else:
|
|
||||||
xsl_file = xsl_file.resolve()
|
|
||||||
# Entferne nicht nur den direkten Eintrag, sondern auch alle Einträge,
|
|
||||||
# die diese Datei als Abhängigkeit haben
|
|
||||||
keys_to_remove = [xsl_file]
|
|
||||||
for cached_path, (_, deps) in self._cache.items():
|
|
||||||
if xsl_file in deps:
|
|
||||||
keys_to_remove.append(cached_path)
|
|
||||||
|
|
||||||
for key in keys_to_remove:
|
|
||||||
self._cache.pop(key, None)
|
|
||||||
|
|
||||||
if keys_to_remove:
|
|
||||||
logger.debug(f"XSL-Abhängigkeitscache invalidiert für {len(keys_to_remove)} Einträg(e)")
|
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13, <3.15"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "altgraph"
|
|
||||||
version = "0.17.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@@ -22,220 +13,66 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "connectorx"
|
name = "connectorx"
|
||||||
version = "0.4.5"
|
version = "0.4.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/de/65de3629f3bbb0597cc3393078085a27b5b52a9fa5b701a60c1c11a9868c/connectorx-0.4.5-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:ff2f4236a0fc14cd724b03df1f11c03b714442f4381575465f7d0f4f91135766", size = 37900697, upload-time = "2026-01-18T01:31:30.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/d5/406fe74dba7e608d2edde3a345c407b1362c59f7044f077b3195e37c2658/connectorx-0.4.4-cp313-cp313-macosx_10_7_x86_64.whl", hash = "sha256:3bf1498e662a5461ba27f8a90d04ff67fddc9bf9d8d9d9687db037a89202fcf7", size = 37965958, upload-time = "2025-08-19T05:37:27.414Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/59/6a4542bc57c53e99b366a8f377ebb8c9b9915d08a8915726c4a5f0fd8219/connectorx-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5d4754069644a712bd3105345e4f7c680420c5bb1d1264070cda058c7f07fb3", size = 35972573, upload-time = "2026-01-18T01:31:46.945Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/c9/db8c9d153eb0a8472997f2e746cd9b1e0da39b6eaac734f40de098333e0a/connectorx-0.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf4c94bc74db19262b6e8fa72f4df92c5ad7249212863400be66300564f89f09", size = 36168586, upload-time = "2025-08-19T05:37:44.241Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/8e/96abe0aecb5e121a80f64ded08c4d8b4115df85f1b246a2680c9f29d4d3d/connectorx-0.4.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:31a65ff4ec8fde7ea7aa2812f2b21e7a512a3216b1b22ca1b02d3975b0bf1e75", size = 43746476, upload-time = "2026-01-18T01:30:56.296Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/e4/0e066245624967ba403b83887d362e730a00981cdfe38a89b49fc305efd2/connectorx-0.4.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:66a07c5215aed34a101137e7cc8c4509939974cad60faebb19972d73c1960d82", size = 43710796, upload-time = "2025-08-19T05:38:02.845Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/9d/3bae67718e0bfbefba41959f2f1dc0a5765392aea311b02d98457c8efd34/connectorx-0.4.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ab1d62a26350055c5e901daa4d6dddb75b11addb923797158c809dffc4f0ac9e", size = 43761125, upload-time = "2026-01-18T01:31:14.253Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/0f/36ae620a0ab457eb90d7a4ffd2cf5a7b42bc234f3bd47b167602603bed23/connectorx-0.4.4-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:f1b4fe628623db5dd99713e9195c8cd9cc63a6d2aff206a47ff87d719806836e", size = 40922979, upload-time = "2025-08-19T05:37:10.312Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/32/54a45bc796b2e5a572bb76c17ebaf971ec57bf9960ba23731d76a7f70962/connectorx-0.4.5-cp313-none-win_amd64.whl", hash = "sha256:50c20558beff2719be34ff325213526c1700c3a20743e9e0ba592774ebc9cc92", size = 34645527, upload-time = "2026-01-18T01:59:41.303Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/19/f4751510ad196e2894205d68d029c1699e67b4026ed655aee5487528cb5a/connectorx-0.4.4-cp313-none-win_amd64.whl", hash = "sha256:ef6ce2e95d04163862c1a2e5a673ae6371d74a3ca66a4e3fec4a7c281944b102", size = 34557019, upload-time = "2025-08-19T05:38:18.784Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/76/8f89e0d1c973af07eed12c584c52c33b51dc119b7ed85e3c0f91615bfec6/connectorx-0.4.5-cp314-cp314-macosx_10_7_x86_64.whl", hash = "sha256:3ddfe372065b974365bff3b383e39c29cad468c0e7556543dd23753446c441ed", size = 37898985, upload-time = "2026-01-18T01:31:34.162Z" },
|
]
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/38/6243f6c83e9515ebab87c0b21be1c2599bd3f6148b8905802b12dba4bf83/connectorx-0.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3fd7788294417cbbb3811f8942e4fe3b4c190b80627a3c706ceae6c321824bcf", size = 35968795, upload-time = "2026-01-18T01:59:28.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/1a/031079b5c597b83df8548012095c23c10d471e6a4c29617d55dd4cf5a9b8/connectorx-0.4.5-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:38ad8a032fddf25c36c6911d857fbe54220fe28439f02a4beb273b29bdef1eb8", size = 43742341, upload-time = "2026-01-18T01:31:00.55Z" },
|
[[package]]
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/97/525b11d7e3c3a286d83dc138f4e4ef948307338527b442be8fa3b1b379d7/connectorx-0.4.5-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:cc01ca122f649e62707f49f7220ba1ae67961b260e2dcff9e8647ea9915a01cf", size = 43757296, upload-time = "2026-01-18T01:31:17.503Z" },
|
name = "darkdetect"
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/3a/5e1a7cb3b0175c249c232f487918494ec7e26c13f3d543a55c8c7752b993/connectorx-0.4.5-cp314-none-win_amd64.whl", hash = "sha256:2073970532a8e6e2a8a2c0b163497eb8e58216e28fdab6693fcd7e58bfc47bfc", size = 34641445, upload-time = "2026-01-18T01:59:44.351Z" },
|
version = "0.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/48/2e/346667de53b48417e6237efd9d076d6530c413666fcbc381adbfeff21ce7/darkdetect-0.7.1.tar.gz", hash = "sha256:47be3cf5134432ddb616bbffc927237718407914993c82809983e7ccebf49013", size = 6976, upload-time = "2022-07-18T21:10:27.64Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/bd/b31abc8fcaab163e0b9501020309dd9094b47d609035a23e6ec0a0a8ba10/darkdetect-0.7.1-py2.py3-none-any.whl", hash = "sha256:3efe69f8ecd5f1b7f4fbb0d1d93f656b0e493c45cc49222380ffe2a529cbc866", size = 8199, upload-time = "2022-07-18T21:10:26.178Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "documentor"
|
name = "documentor"
|
||||||
version = "1.6.4"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "connectorx" },
|
|
||||||
{ name = "lxml" },
|
|
||||||
{ name = "polars", extra = ["connectorx", "pyarrow"] },
|
{ name = "polars", extra = ["connectorx", "pyarrow"] },
|
||||||
{ name = "psutil" },
|
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pydantic-yaml" },
|
{ name = "pydantic-yaml" },
|
||||||
|
{ name = "pyqtdarktheme" },
|
||||||
{ name = "pyside6" },
|
{ name = "pyside6" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pyinstaller" },
|
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "connectorx", specifier = ">=0.4.0" },
|
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
|
||||||
{ name = "lxml", specifier = ">=6.0.2" },
|
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
||||||
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.37.0" },
|
{ name = "pydantic-yaml", specifier = ">=1.5.1" },
|
||||||
{ name = "psutil", specifier = ">=6.1.1" },
|
{ name = "pyqtdarktheme", specifier = ">=2.1.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.12.0" },
|
{ name = "pyside6", specifier = ">=6.9.1" },
|
||||||
{ name = "pydantic-yaml", specifier = ">=1.6.0" },
|
|
||||||
{ name = "pyside6", specifier = ">=6.10.1" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [{ name = "ruff", specifier = ">=0.14.8" }]
|
||||||
{ name = "pillow", specifier = ">=10.0.0" },
|
|
||||||
{ name = "pyinstaller", specifier = ">=6.0.0" },
|
|
||||||
{ name = "ruff", specifier = ">=0.14.11" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lxml"
|
|
||||||
version = "6.1.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "macholib"
|
|
||||||
version = "1.16.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "altgraph" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "26.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pefile"
|
|
||||||
version = "2024.8.26"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pillow"
|
|
||||||
version = "12.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polars"
|
name = "polars"
|
||||||
version = "1.41.0"
|
version = "1.33.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
sdist = { url = "https://files.pythonhosted.org/packages/85/da/8246f1d69d7e49f96f0c5529057a19af1536621748ef214bbd4112c83b8e/polars-1.33.1.tar.gz", hash = "sha256:fa3fdc34eab52a71498264d6ff9b0aa6955eb4b0ae8add5d3cb43e4b84644007", size = 4822485, upload-time = "2025-09-09T08:37:49.062Z" }
|
||||||
{ name = "polars-runtime-32" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/13/fe30b3e2f9ab54a27d82af04fb2edc51c7342cbaa88815e175769a9f5901/polars-1.41.0.tar.gz", hash = "sha256:7cb5465eb66eb868fde779bf5c41c9f2f244481d72c52133e8ed10ba64372e4f", size = 737530, upload-time = "2026-05-22T20:20:56.209Z" }
|
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/c8/5807714256c5f3de08593113df17f14f99417a451cb2d91530ad94785003/polars-1.41.0-py3-none-any.whl", hash = "sha256:35dcd24de88a198dc50929924f064ba12a0a0a4a3e77e116689491b4b3ab58ac", size = 832953, upload-time = "2026-05-22T20:19:30.958Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/79/c51e7e1d707d8359bcb76e543a8315b7ae14069ecf5e75262a0ecb32e044/polars-1.33.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3881c444b0f14778ba94232f077a709d435977879c1b7d7bd566b55bd1830bb5", size = 39132875, upload-time = "2025-09-09T08:36:38.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/15/1094099a1b9cb4fbff58cd8ed3af8964f4d22a5b682ea0b7bb72bf4bc3d9/polars-1.33.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:29200b89c9a461e6f06fc1660bc9c848407640ee30fe0e5ef4947cfd49d55337", size = 35638783, upload-time = "2025-09-09T08:36:43.748Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/b9/9ac769e4d8e8f22b0f2e974914a63dd14dec1340cd23093de40f0d67d73b/polars-1.33.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:444940646e76342abaa47f126c70e3e40b56e8e02a9e89e5c5d1c24b086db58a", size = 39742297, upload-time = "2025-09-09T08:36:47.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/26/4c5da9f42fa067b2302fe62bcbf91faac5506c6513d910fae9548fc78d65/polars-1.33.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:094a37d06789286649f654f229ec4efb9376630645ba8963b70cb9c0b008b3e1", size = 36684940, upload-time = "2025-09-09T08:36:50.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/a6/dc535da476c93b2efac619e04ab81081e004e4b4553352cd10e0d33a015d/polars-1.33.1-cp39-abi3-win_amd64.whl", hash = "sha256:c9781c704432a2276a185ee25898aa427f39a904fbe8fde4ae779596cdbd7a9e", size = 39456676, upload-time = "2025-09-09T08:36:54.612Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/4e/a4300d52dd81b58130ccadf3873f11b3c6de54836ad4a8f32bac2bd2ba17/polars-1.33.1-cp39-abi3-win_arm64.whl", hash = "sha256:c3cfddb3b78eae01a218222bdba8048529fef7e14889a71e33a5198644427642", size = 35445171, upload-time = "2025-09-09T08:36:58.043Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -246,89 +83,31 @@ pyarrow = [
|
|||||||
{ name = "pyarrow" },
|
{ name = "pyarrow" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "polars-runtime-32"
|
|
||||||
version = "1.41.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/8f/30dc715ea1135b4b80397edf33fe7b1bb124850e96e38d9918e2b3d6d0b0/polars_runtime_32-1.41.0.tar.gz", hash = "sha256:37ffbe5414f14bf43bcc8e08a0386c97c692e3fd4e87af74529d7f14b1b2d1cb", size = 2985826, upload-time = "2026-05-22T20:20:57.622Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e7/9d1630d666eca6a67e2096c0ed2c0e18f1355fe440043fd0830de1b71ab6/polars_runtime_32-1.41.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:766b60c74550382731b604ed62a385a8403b341bf18282d3fd2f746fa3c4cafd", size = 52163350, upload-time = "2026-05-22T20:19:34.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/b3/01538d51cd2790729ae13c23db44bc787bdfe20867faeb1087afc390c53b/polars_runtime_32-1.41.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bee0d294daca79cedd5749e1bf3373c2d4107eb849fe544a60df6c08abc972ce", size = 46474331, upload-time = "2026-05-22T20:19:37.936Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/29/efa82e1b3e6711f254df3793f3d3fd99f26ef1bcaffa6533266fa6522de4/polars_runtime_32-1.41.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b3f915468bf00d327b4a1236935a4ec3174dcb163785fcd98185ad1319a503", size = 50358997, upload-time = "2026-05-22T20:19:42.209Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/18/5a04b06b773047cbf43912ab802eef3c1d50ef7e66d51a41b16726f9bc62/polars_runtime_32-1.41.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72269f768c57229190dba0cb5abb8a1b228e96cc6331273a77a7957576885bd", size = 56332032, upload-time = "2026-05-22T20:19:45.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/d5/d728ce7a39ea925555db7d2c9f7b5df3ca17568e483db6501783f653e0b9/polars_runtime_32-1.41.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1532c7560a5c0fd06943080ee42aade721f186becdfbb1baafa622e3199c3b62", size = 50529017, upload-time = "2026-05-22T20:19:49.489Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/92/6b2092dfed4278f636499217767204fce19ed72695690e2c4eba99e2892e/polars_runtime_32-1.41.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:43368d8a754ee274f1e1099a7c09aae59cd69d22b8a6f83c0f782a00cd3a6662", size = 54244707, upload-time = "2026-05-22T20:19:52.852Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/46b43be0f5becbef65843f438de3950ac6f8d0fa0008d7de0025eed00097/polars_runtime_32-1.41.0-cp310-abi3-win_amd64.whl", hash = "sha256:a9bd6095ecadc6799d166b9e8f7183a7ca8ba0a5aef8a426ec41df8ed8b09df7", size = 51918379, upload-time = "2026-05-22T20:19:55.832Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/da/30f15f0c3959b70e7a6583eccd37140a30b5c643ca374792d150b3a357df/polars_runtime_32-1.41.0-cp310-abi3-win_arm64.whl", hash = "sha256:ed922400f0eb393345fd7b6874b150eb943af2b816297a3dde03735cb5f3de08", size = 45921961, upload-time = "2026-05-22T20:19:59.525Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psutil"
|
|
||||||
version = "7.2.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyarrow"
|
name = "pyarrow"
|
||||||
version = "24.0.0"
|
version = "21.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" },
|
{ url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.13.4"
|
version = "2.11.9"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "annotated-types" },
|
{ name = "annotated-types" },
|
||||||
@@ -336,79 +115,51 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.46.4"
|
version = "2.33.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.14.1"
|
version = "2.10.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -426,49 +177,20 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyinstaller"
|
name = "pyqtdarktheme"
|
||||||
version = "6.20.0"
|
version = "2.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "altgraph" },
|
{ name = "darkdetect" },
|
||||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
|
||||||
{ name = "pyinstaller-hooks-contrib" },
|
|
||||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
|
||||||
{ name = "setuptools" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/f1/786feaad7a333072b34a913dbe38aef94b5ae43ad188934f5d70007aea79/pyqtdarktheme-2.1.0.tar.gz", hash = "sha256:5f8274ddfa3a5481ed9743cdb0f9debfeb7ff695b3a0d202a8104361d17dadb8", size = 42186, upload-time = "2022-12-25T08:33:11.662Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/cd/8ce0ac84e9f68dc549edcc5cbdeac7511439c5f7ee6c05f1f8826ef05d44/pyqtdarktheme-2.1.0-py3-none-any.whl", hash = "sha256:8739d99502230fbaca42551ea033c9ae31c81c4ebfec2f1ffde38f32a18bea7a", size = 54242, upload-time = "2022-12-25T08:33:10.125Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyinstaller-hooks-contrib"
|
|
||||||
version = "2026.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "setuptools" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6"
|
name = "pyside6"
|
||||||
version = "6.11.1"
|
version = "6.9.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyside6-addons" },
|
{ name = "pyside6-addons" },
|
||||||
@@ -476,146 +198,118 @@ dependencies = [
|
|||||||
{ name = "shiboken6" },
|
{ name = "shiboken6" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6", size = 571921, upload-time = "2026-05-13T09:47:36.402Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/42/43577413bd5ab26f5f21e7a43c9396aac158a5d01900c87e4609c0e96278/pyside6-6.9.2-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:71245c76bfbe5c41794ffd8546730ec7cc869d4bbe68535639e026e4ef8a7714", size = 558102, upload-time = "2025-08-26T07:52:57.302Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/de/af89d71410c83b10654d86ff9aff2a4f87c30163658f1cc145242e222526/pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2", size = 572102, upload-time = "2026-05-13T09:47:38.249Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/df/cb84f802df3dcc1d196d2f9f37dbb8227761826f936987c9386b8ae1ffcc/pyside6-6.9.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:64a9e2146e207d858e00226f68d7c1b4ab332954742a00dcabb721bb9e4aa0cd", size = 558243, upload-time = "2025-08-26T07:52:59.272Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/0e/d583bd3f7bf5046a4497b36f3902cfb64aa29554489a5a25c18e6b4ac0ac/pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84", size = 572098, upload-time = "2026-05-13T09:47:39.44Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/2d/715db9da437b4632d06e2c4718aee9937760b84cf36c23d5441989e581b0/pyside6-6.9.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a78fad16241a1f2ed0fa0098cf3d621f591fc75b4badb7f3fa3959c9d861c806", size = 558245, upload-time = "2025-08-26T07:53:00.838Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6", size = 578377, upload-time = "2026-05-13T09:47:40.76Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/90/2e75cbff0e17f16b83d2b7e8434ae9175cae8d6ff816c9b56d307cf53c86/pyside6-6.9.2-cp39-abi3-win_amd64.whl", hash = "sha256:d1afbf48f9a5612b9ee2dc7c384c1a65c08b5830ba5e7d01f66d82678e5459df", size = 564604, upload-time = "2025-08-26T07:53:02.402Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/02/a6057d8bd2bdb1940820fff2d627fdf4013148c9c57adf69fa40d3452ac3/pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805", size = 561765, upload-time = "2026-05-13T09:47:41.9Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/34/e3dd4e046673efcbcfbe0aa2760df06b2877739b8f4da60f0229379adebd/pyside6-6.9.2-cp39-abi3-win_arm64.whl", hash = "sha256:1499b1d7629ab92119118e2636b4ace836b25e457ddf01003fdca560560b8c0a", size = 401833, upload-time = "2025-08-26T07:53:03.742Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-addons"
|
name = "pyside6-addons"
|
||||||
version = "6.11.1"
|
version = "6.9.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyside6-essentials" },
|
{ name = "pyside6-essentials" },
|
||||||
{ name = "shiboken6" },
|
{ name = "shiboken6" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9", size = 331714589, upload-time = "2026-05-13T09:39:12.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/39/a8f4a55001b6a0aaee042e706de2447f21c6dc2a610f3d3debb7d04db821/pyside6_addons-6.9.2-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:7019fdcc0059626eb1608b361371f4dc8cb7f2d02f066908fd460739ff5a07cd", size = 316693692, upload-time = "2025-08-26T07:33:31.529Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/62/fb1428a523b2a4541e232aab50d9e789e6b4526f37fd9593452a7ea5b6b3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f", size = 175063224, upload-time = "2026-05-13T09:39:34.185Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/48/0b16e9dabd4cafe02d59531832bc30b6f0e14c92076e90dd02379d365cb2/pyside6_addons-6.9.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:24350e5415317f269e743d1f7b4933fe5f59d90894aa067676c9ce6bfe9e7988", size = 166984613, upload-time = "2025-08-26T07:33:47.569Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/9b/2ccd52f66db55c06de65d0501170a1935d04d64d0a230c0d892284a02ce3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993", size = 170553429, upload-time = "2026-05-13T09:39:54.424Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/55/dc42a73387379bae82f921b7659cd2006ec0e80f7052f83ddc07e9eb9cca/pyside6_addons-6.9.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:af8dee517de8d336735a6543f7dd496eb580e852c14b4d2304b890e2a29de499", size = 162908466, upload-time = "2025-08-26T07:39:49.331Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923", size = 168816308, upload-time = "2026-05-13T09:40:13.541Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/fa/396a2e86230c493b565e2dc89dc64e4b1c63582ac69afe77b693c3817a53/pyside6_addons-6.9.2-cp39-abi3-win_amd64.whl", hash = "sha256:98d2413904ee4b2b754b077af7875fa6ec08468c01a6628a2c9c3d2cece4874f", size = 160216647, upload-time = "2025-08-26T07:42:18.903Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/b7/9a840d97f0f0f04e372a87e205dd30ee285b4e3b021b188459a917c9dc76/pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37", size = 35759347, upload-time = "2026-05-13T09:40:21.199Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/fe/25f61259f1d5ec4648c9f6d2abd8e2cba2188f10735a57abafda719958e5/pyside6_addons-6.9.2-cp39-abi3-win_arm64.whl", hash = "sha256:b430cae782ff1a99fb95868043557f22c31b30c94afb9cf73278584e220a2ab6", size = 27126649, upload-time = "2025-08-26T07:42:37.696Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyside6-essentials"
|
name = "pyside6-essentials"
|
||||||
version = "6.11.1"
|
version = "6.9.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "shiboken6" },
|
{ name = "shiboken6" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957", size = 110352945, upload-time = "2026-05-13T09:43:08.006Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/21/41960c03721a99e7be99a96ebb8570bdfd6f76f512b5d09074365e27ce28/pyside6_essentials-6.9.2-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:713eb8dcbb016ff10e6fca129c1bf2a0fd8cfac979e689264e0be3b332f9398e", size = 133092348, upload-time = "2025-08-26T07:43:57.231Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/49/0e1237c4400bec7e335d2c4eeb49bc40d9fd88a9ac44ca9083ce1abdc308/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60", size = 79908535, upload-time = "2026-05-13T09:43:24.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/02/e38ff18f3d2d8d3071aa6823031aad6089267aa4668181db65ce9948bfc0/pyside6_essentials-6.9.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:84b8ca4fa56506e2848bdb4c7a0851a5e7adcb916bef9bce25ce2eeb6c7002cc", size = 96569791, upload-time = "2025-08-26T07:44:41.392Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/c5/da4c5f23c6540ac5211a1f60177c8dee84b1bf40f2719479587ab8c60731/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d", size = 78960051, upload-time = "2026-05-13T09:43:35.423Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/a1/1203d4db6919b42a937d9ac5ddb84b20ea42eb119f7c1ddeb77cb8fdb00c/pyside6_essentials-6.9.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:d0f701503974bd51b408966539aa6956f3d8536e547ea8002fbfb3d77796bbc3", size = 94311809, upload-time = "2025-08-26T07:46:44.924Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40", size = 77549267, upload-time = "2026-05-13T09:43:44.92Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/e3/3b3e869d3e332b6db93f6f64fac3b12f5c48b84f03f2aa50ee5c044ec0de/pyside6_essentials-6.9.2-cp39-abi3-win_amd64.whl", hash = "sha256:b2f746f795138ac63eb173f9850a6db293461a1b6ce22cf6dafac7d194a38951", size = 72624566, upload-time = "2025-08-26T07:48:04.64Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/eb6723faf5cb7fa581145da1c15f40d641b96e080f0491af2f1859fdeedb/pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f", size = 57964512, upload-time = "2026-05-13T09:43:52.968Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/70/db78afc8b60b2e53f99145bde2f644cca43924a4dd869ffe664e0792730a/pyside6_essentials-6.9.2-cp39-abi3-win_arm64.whl", hash = "sha256:ecd7b5cd9e271f397fb89a6357f4ec301d8163e50869c6c557f9ccc6bed42789", size = 49561720, upload-time = "2025-08-26T07:49:43.708Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pywin32-ctypes"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruamel-yaml"
|
name = "ruamel-yaml"
|
||||||
version = "0.18.17"
|
version = "0.18.15"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" },
|
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/3e/db/f3950f5e5031b618aae9f423a39bf81a55c148aecd15a34527898e752cf4/ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700", size = 146865, upload-time = "2025-08-19T11:15:10.694Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701", size = 119702, upload-time = "2025-08-19T11:15:07.696Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruamel-yaml-clib"
|
name = "ruamel-yaml-clib"
|
||||||
version = "0.2.15"
|
version = "0.2.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload-time = "2024-10-20T10:13:04.377Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload-time = "2024-10-20T10:13:05.906Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload-time = "2024-10-20T10:13:07.26Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload-time = "2024-10-20T10:13:08.504Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload-time = "2024-10-21T11:26:48.866Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload-time = "2024-10-21T11:26:50.213Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload-time = "2024-12-11T19:58:18.846Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload-time = "2024-10-20T10:13:09.658Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.14"
|
version = "0.14.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
|
||||||
]
|
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "setuptools"
|
|
||||||
version = "82.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shiboken6"
|
name = "shiboken6"
|
||||||
version = "6.11.1"
|
version = "6.9.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02", size = 476874, upload-time = "2026-05-13T09:47:01.091Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/1e/62a8757aa0aa8d5dbf876f6cb6f652a60be9852e7911b59269dd983a7fb5/shiboken6-6.9.2-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:8bb1c4326330e53adeac98bfd9dcf57f5173a50318a180938dcc4825d9ca38da", size = 406337, upload-time = "2025-08-26T07:52:39.614Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/9b/e0355d8897b5c150770f1d95718aad17d432fcc9c035c04f3f58427d4693/shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe", size = 272222, upload-time = "2026-05-13T09:47:02.653Z" },
|
{ url = "https://files.pythonhosted.org/packages/3b/bb/72a8ed0f0542d9ea935f385b396ee6a4bbd94749c817cbf2be34e80a16d3/shiboken6-6.9.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3b54c0a12ea1b03b9dc5dcfb603c366e957dc75341bf7cb1cc436d0d848308ee", size = 206733, upload-time = "2025-08-26T07:52:41.768Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/d5/dd4f1defed400be03340f2ede34b61f846776650b4e7ed9ebaf4c71979a2/shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f", size = 270350, upload-time = "2026-05-13T09:47:04.02Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/c4/09e902f5612a509cef2c8712c516e4fe44f3a1ae9fcd8921baddb5e6bae4/shiboken6-6.9.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a5f5985938f5acb604c23536a0ff2efb3cccb77d23da91fbaff8fd8ded3dceb4", size = 202784, upload-time = "2025-08-26T07:52:43.172Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7", size = 1223647, upload-time = "2026-05-13T09:47:05.924Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/ea/a56b094a4bf6facf89f52f58e83684e168b1be08c14feb8b99969f3d4189/shiboken6-6.9.2-cp39-abi3-win_amd64.whl", hash = "sha256:68c33d565cd4732be762d19ff67dfc53763256bac413d392aa8598b524980bc4", size = 1152089, upload-time = "2025-08-26T07:52:45.162Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/d1/f15ca0e1666faae02c945f48e745ea35f8fcd8243b176109b4e2c4251f47/shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d", size = 1784170, upload-time = "2026-05-13T09:47:07.617Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/64/562a527fc55fbf41fa70dae735929988215505cb5ec0809fb0aef921d4a0/shiboken6-6.9.2-cp39-abi3-win_arm64.whl", hash = "sha256:c5b827797b3d89d9b9a3753371ff533fcd4afc4531ca51a7c696952132098054", size = 1708948, upload-time = "2025-08-26T07:52:48.016Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -629,12 +323,12 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-inspection"
|
name = "typing-inspection"
|
||||||
version = "0.4.2"
|
version = "0.4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,951 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Datenschutz — DocuMentor</title>
|
|
||||||
<link rel="stylesheet" href="fonts/fonts.css">
|
|
||||||
<style>
|
|
||||||
/* ============================================================
|
|
||||||
RESET & BASE
|
|
||||||
============================================================ */
|
|
||||||
*, *::before, *::after {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--concrete-dark: #1a1a1a;
|
|
||||||
--concrete-mid: #2a2a2a;
|
|
||||||
--concrete-light: #3a3a3a;
|
|
||||||
--concrete-surface: #444444;
|
|
||||||
--warning-yellow: #f0c030;
|
|
||||||
--warning-yellow-dim: #e8b800;
|
|
||||||
--safety-orange: #e05020;
|
|
||||||
--steel-text: #b0b0b0;
|
|
||||||
--steel-bright: #d0d0d0;
|
|
||||||
--white: #f0efe8;
|
|
||||||
--grid-line: rgba(240, 192, 48, 0.07);
|
|
||||||
--grid-dot: rgba(240, 192, 48, 0.12);
|
|
||||||
--led-green: #30e060;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Barlow', sans-serif;
|
|
||||||
background-color: var(--concrete-dark);
|
|
||||||
color: var(--steel-text);
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-image:
|
|
||||||
repeating-linear-gradient(
|
|
||||||
0deg,
|
|
||||||
transparent,
|
|
||||||
transparent 2px,
|
|
||||||
rgba(255,255,255,0.008) 2px,
|
|
||||||
rgba(255,255,255,0.008) 4px
|
|
||||||
),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
90deg,
|
|
||||||
transparent,
|
|
||||||
transparent 2px,
|
|
||||||
rgba(255,255,255,0.005) 2px,
|
|
||||||
rgba(255,255,255,0.005) 4px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle, var(--grid-dot) 1px, transparent 1px);
|
|
||||||
background-size: 40px 40px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
UTILITY
|
|
||||||
============================================================ */
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
STENCIL SECTION LABELS
|
|
||||||
============================================================ */
|
|
||||||
.section-label {
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.3em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--warning-yellow-dim);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label::before {
|
|
||||||
content: attr(data-num);
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--concrete-surface);
|
|
||||||
border: 1px dashed var(--concrete-surface);
|
|
||||||
padding: 0.15rem 0.4rem;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: var(--white);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
HAZARD STRIPE
|
|
||||||
============================================================ */
|
|
||||||
.hazard-stripe {
|
|
||||||
height: 6px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
var(--warning-yellow),
|
|
||||||
var(--warning-yellow) 8px,
|
|
||||||
var(--concrete-dark) 8px,
|
|
||||||
var(--concrete-dark) 16px
|
|
||||||
);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hazard-stripe-thin {
|
|
||||||
height: 3px;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
var(--warning-yellow-dim),
|
|
||||||
var(--warning-yellow-dim) 5px,
|
|
||||||
var(--concrete-mid) 5px,
|
|
||||||
var(--concrete-mid) 10px
|
|
||||||
);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
CORNER BRACKETS
|
|
||||||
============================================================ */
|
|
||||||
.corner-brackets {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.corner-brackets::before,
|
|
||||||
.corner-brackets::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.corner-brackets::before {
|
|
||||||
top: -1px;
|
|
||||||
left: -1px;
|
|
||||||
border-top: 2px solid var(--warning-yellow-dim);
|
|
||||||
border-left: 2px solid var(--warning-yellow-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.corner-brackets::after {
|
|
||||||
bottom: -1px;
|
|
||||||
right: -1px;
|
|
||||||
border-bottom: 2px solid var(--warning-yellow-dim);
|
|
||||||
border-right: 2px solid var(--warning-yellow-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
RIVET DECORATIONS
|
|
||||||
============================================================ */
|
|
||||||
.riveted {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.riveted .rivet {
|
|
||||||
position: absolute;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle at 35% 35%, #555, #2a2a2a 60%, #1a1a1a);
|
|
||||||
box-shadow: inset 0 1px 1px rgba(255,255,255,0.15), 0 1px 2px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.riveted .rivet-tl { top: 10px; left: 10px; }
|
|
||||||
.riveted .rivet-tr { top: 10px; right: 10px; }
|
|
||||||
.riveted .rivet-bl { bottom: 10px; left: 10px; }
|
|
||||||
.riveted .rivet-br { bottom: 10px; right: 10px; }
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
LED INDICATOR
|
|
||||||
============================================================ */
|
|
||||||
.led {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 6px;
|
|
||||||
vertical-align: middle;
|
|
||||||
box-shadow: 0 0 4px currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.led-green {
|
|
||||||
background-color: var(--led-green);
|
|
||||||
color: var(--led-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
SCROLL REVEAL
|
|
||||||
============================================================ */
|
|
||||||
.reveal {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
transition: opacity 0.6s linear, transform 0.6s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
NAVBAR
|
|
||||||
============================================================ */
|
|
||||||
.navbar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
background: rgba(26, 26, 26, 0.92);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-bottom: 1px solid rgba(240, 192, 48, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 2rem;
|
|
||||||
height: 56px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: 2px solid var(--warning-yellow);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--warning-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand-text {
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--white);
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-links a {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.15em;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
transition: color 0.2s linear, border-color 0.2s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-links a:hover,
|
|
||||||
.navbar-links a.active {
|
|
||||||
color: var(--warning-yellow);
|
|
||||||
border-bottom-color: var(--warning-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-mobile-toggle {
|
|
||||||
display: none;
|
|
||||||
background: none;
|
|
||||||
border: 1px solid var(--concrete-surface);
|
|
||||||
color: var(--steel-text);
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
PAGE HEADER
|
|
||||||
============================================================ */
|
|
||||||
.page-header {
|
|
||||||
padding-top: 56px;
|
|
||||||
background: linear-gradient(135deg, var(--concrete-dark) 0%, var(--concrete-mid) 50%, var(--concrete-dark) 100%);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
|
||||||
background-size: 80px 80px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-inner {
|
|
||||||
padding: 3rem 0 2.5rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-tag {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--concrete-surface);
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-title {
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 3rem;
|
|
||||||
color: var(--white);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-title span {
|
|
||||||
color: var(--warning-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-sub {
|
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
LEGAL CONTENT
|
|
||||||
============================================================ */
|
|
||||||
.legal-section {
|
|
||||||
padding: 5rem 0;
|
|
||||||
background: var(--concrete-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-panel {
|
|
||||||
background: var(--concrete-mid);
|
|
||||||
border: 1px solid rgba(240, 192, 48, 0.12);
|
|
||||||
padding: 3rem;
|
|
||||||
max-width: 860px;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block {
|
|
||||||
margin-bottom: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block-label {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--concrete-surface);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block-label::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--concrete-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block h2 {
|
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: var(--steel-bright);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block p {
|
|
||||||
font-family: 'Barlow', sans-serif;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
line-height: 1.7;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block a {
|
|
||||||
color: var(--warning-yellow-dim);
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
transition: border-color 0.2s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block a:hover {
|
|
||||||
border-bottom-color: var(--warning-yellow-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block ul {
|
|
||||||
margin: 0.5rem 0 0.5rem 1.5rem;
|
|
||||||
font-family: 'Barlow', sans-serif;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-block ul li {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(240, 192, 48, 0.15) 20%,
|
|
||||||
rgba(240, 192, 48, 0.15) 80%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
margin: 2.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-address {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
line-height: 2;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
background: rgba(26, 26, 26, 0.6);
|
|
||||||
border: 1px dashed rgba(240, 192, 48, 0.1);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-address strong {
|
|
||||||
color: var(--steel-bright);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-note {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-left: 3px solid var(--safety-orange);
|
|
||||||
background: rgba(224, 80, 32, 0.05);
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-note strong {
|
|
||||||
color: var(--safety-orange);
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
FOOTER
|
|
||||||
============================================================ */
|
|
||||||
.footer {
|
|
||||||
padding: 3rem 0 2rem;
|
|
||||||
background: var(--concrete-dark);
|
|
||||||
border-top: 1px solid rgba(240, 192, 48, 0.08);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-inner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-brand-icon {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border: 1.5px solid var(--warning-yellow-dim);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--warning-yellow-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-brand-text {
|
|
||||||
font-family: 'Oswald', sans-serif;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-meta {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: var(--concrete-surface);
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
font-family: 'Share Tech Mono', monospace;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--steel-text);
|
|
||||||
text-decoration: none;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
transition: color 0.2s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a:hover,
|
|
||||||
.footer-links a.active {
|
|
||||||
color: var(--warning-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
RESPONSIVE
|
|
||||||
============================================================ */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.navbar-links {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 56px;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: rgba(26, 26, 26, 0.97);
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
border-bottom: 1px solid rgba(240, 192, 48, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-links.open {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-mobile-toggle {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-title {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-panel {
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-inner {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-meta {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-panel {
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-inner {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
NAVBAR
|
|
||||||
============================================================ -->
|
|
||||||
<nav class="navbar">
|
|
||||||
<div class="navbar-inner">
|
|
||||||
<a href="index.html" class="navbar-brand">
|
|
||||||
<div class="navbar-brand-icon">D</div>
|
|
||||||
<span class="navbar-brand-text">DocuMentor</span>
|
|
||||||
</a>
|
|
||||||
<button class="navbar-mobile-toggle" id="navToggle">MENU</button>
|
|
||||||
<ul class="navbar-links" id="navLinks">
|
|
||||||
<li><a href="index.html#features">Features</a></li>
|
|
||||||
<li><a href="index.html#workflow">Workflow</a></li>
|
|
||||||
<li><a href="index.html#techstack">Technik</a></li>
|
|
||||||
<li><a href="index.html#download">Download</a></li>
|
|
||||||
<li><a href="datenschutz.html" class="active">Datenschutz</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
PAGE HEADER
|
|
||||||
============================================================ -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="hazard-stripe"></div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="page-header-inner">
|
|
||||||
<div class="page-header-tag">
|
|
||||||
<span class="led led-green"></span>
|
|
||||||
SYSTEM AKTIV — DATENSCHUTZINFORMATIONEN
|
|
||||||
</div>
|
|
||||||
<h1 class="page-header-title">Daten<span>schutz</span></h1>
|
|
||||||
<p class="page-header-sub">Datenschutzerklärung gemäß DSGVO / BDSG</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hazard-stripe-thin"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
LEGAL CONTENT
|
|
||||||
============================================================ -->
|
|
||||||
<section class="legal-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-label reveal" data-num="DSE-01">Datenschutzerklärung</div>
|
|
||||||
<h2 class="section-title reveal" style="margin-bottom: 2rem;">Datenschutz</h2>
|
|
||||||
|
|
||||||
<div class="legal-panel corner-brackets riveted reveal">
|
|
||||||
<span class="rivet rivet-tl"></span>
|
|
||||||
<span class="rivet rivet-tr"></span>
|
|
||||||
<span class="rivet rivet-bl"></span>
|
|
||||||
<span class="rivet rivet-br"></span>
|
|
||||||
|
|
||||||
<!-- 1. Verantwortlicher -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 1 — VERANTWORTLICHER</div>
|
|
||||||
<h2>Verantwortlicher</h2>
|
|
||||||
<p>Verantwortlicher im Sinne der Datenschutz-Grundverordnung (DSGVO) und des Bundesdatenschutzgesetzes (BDSG) ist:</p>
|
|
||||||
<div class="legal-address" style="margin-top: 0.75rem;">
|
|
||||||
<strong>Vitali Graf</strong><br>
|
|
||||||
Kulenkampffallee 65b<br>
|
|
||||||
28213 Bremen<br>
|
|
||||||
Deutschland
|
|
||||||
E-Mail: <a href="mailto:info@vitaligraf.de">info@vitaligraf.de</a>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="legal-note" style="margin-top: 1rem;">
|
|
||||||
<strong>HINWEIS:</strong> Bitte Adresse vor Veröffentlichung ergänzen.
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 2. Allgemeines -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 2 — GRUNDSATZ</div>
|
|
||||||
<h2>Allgemeines zur Datenverarbeitung</h2>
|
|
||||||
<p>
|
|
||||||
Diese Website ist eine statische Informationsseite über das Open-Source-Projekt
|
|
||||||
DocuMentor. Es werden <strong style="color: var(--steel-bright); font-weight: 400;">keine Cookies</strong>,
|
|
||||||
<strong style="color: var(--steel-bright); font-weight: 400;">keine Tracking-Skripte</strong> und
|
|
||||||
<strong style="color: var(--steel-bright); font-weight: 400;">keine Webanalyse-Dienste</strong> verwendet.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Alle Schriftarten werden lokal ausgeliefert. Es werden keine externen
|
|
||||||
Ressourcen von Drittanbietern geladen. Es findet keine Verarbeitung
|
|
||||||
personenbezogener Daten durch diese Website statt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 3. Server-Log -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 3 — SERVER-LOGDATEIEN</div>
|
|
||||||
<h2>Server-Logdateien</h2>
|
|
||||||
<p>
|
|
||||||
Beim Abruf dieser Website werden durch den Hosting-Anbieter automatisch
|
|
||||||
technische Zugriffsdaten in sogenannten Server-Logdateien erfasst.
|
|
||||||
Diese Daten werden nicht aktiv durch uns erhoben und umfassen:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Browsertyp und Browserversion</li>
|
|
||||||
<li>Verwendetes Betriebssystem</li>
|
|
||||||
<li>Referrer-URL (zuvor besuchte Seite)</li>
|
|
||||||
<li>Hostname des zugreifenden Rechners</li>
|
|
||||||
<li>Uhrzeit der Serveranfrage</li>
|
|
||||||
<li>IP-Adresse</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Die Rechtsgrundlage für diese technisch notwendige Verarbeitung ist
|
|
||||||
Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am sicheren Betrieb
|
|
||||||
der Website). Die Daten werden nicht mit anderen Datenquellen zusammengeführt
|
|
||||||
und nach kurzer Zeit automatisch gelöscht.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 4. Kontakt per E-Mail -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 4 — KONTAKTAUFNAHME</div>
|
|
||||||
<h2>Kontaktaufnahme per E-Mail</h2>
|
|
||||||
<p>
|
|
||||||
Wenn Sie uns per E-Mail kontaktieren, werden die übermittelten Daten
|
|
||||||
(E-Mail-Adresse, Inhalt Ihrer Nachricht sowie ggf. weitere von Ihnen
|
|
||||||
angegebene Daten) zur Bearbeitung Ihrer Anfrage und für den Fall von
|
|
||||||
Anschlussfragen gespeichert.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1
|
|
||||||
lit. b DSGVO, sofern Ihre Anfrage im Zusammenhang mit der Erfüllung
|
|
||||||
eines Vertrages steht oder zur Durchführung vorvertraglicher Maßnahmen
|
|
||||||
erforderlich ist. In allen übrigen Fällen beruht die Verarbeitung auf
|
|
||||||
unserem berechtigten Interesse an der effektiven Bearbeitung der an uns
|
|
||||||
gerichteten Anfragen (Art. 6 Abs. 1 lit. f DSGVO).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Die Daten werden gelöscht, sobald Ihre Anfrage abschließend bearbeitet
|
|
||||||
wurde und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 5. Desktop-Anwendung -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 5 — DESKTOP-ANWENDUNG</div>
|
|
||||||
<h2>DocuMentor Desktop-Anwendung</h2>
|
|
||||||
<p>
|
|
||||||
Die DocuMentor-Anwendung selbst läuft ausschließlich lokal auf Ihrem
|
|
||||||
Rechner. Sie überträgt keine Daten an externe Server. Alle
|
|
||||||
Konfigurationsdaten und Projektdateien werden ausschließlich lokal
|
|
||||||
gespeichert:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Linux: <code style="font-family: 'Share Tech Mono', monospace; font-size: 0.85em; color: var(--warning-yellow-dim);">~/.config/DocuMentor/config.json</code></li>
|
|
||||||
<li>Windows: <code style="font-family: 'Share Tech Mono', monospace; font-size: 0.85em; color: var(--warning-yellow-dim);">%APPDATA%\DocuMentor\config.json</code></li>
|
|
||||||
<li>macOS: <code style="font-family: 'Share Tech Mono', monospace; font-size: 0.85em; color: var(--warning-yellow-dim);">~/Library/Application Support/DocuMentor/config.json</code></li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Datenbankverbindungen zu PostgreSQL werden nur auf ausdrückliche
|
|
||||||
Konfiguration durch den Nutzer hergestellt und ausschließlich für den
|
|
||||||
in der Anwendung angezeigten Zweck verwendet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 6. Betroffenenrechte -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 6 — IHRE RECHTE</div>
|
|
||||||
<h2>Ihre Rechte als betroffene Person</h2>
|
|
||||||
<p>Sie haben gegenüber uns folgende Rechte hinsichtlich Ihrer personenbezogenen Daten:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Auskunftsrecht</strong> (Art. 15 DSGVO)</li>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Recht auf Berichtigung</strong> (Art. 16 DSGVO)</li>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Recht auf Löschung</strong> (Art. 17 DSGVO)</li>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Recht auf Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)</li>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Recht auf Datenübertragbarkeit</strong> (Art. 20 DSGVO)</li>
|
|
||||||
<li><strong style="color: var(--steel-bright); font-weight: 400;">Widerspruchsrecht</strong> (Art. 21 DSGVO)</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an:
|
|
||||||
<a href="mailto:info@vitaligraf.de">info@vitaligraf.de</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Sie haben zudem das Recht, sich bei einer Datenschutz-Aufsichtsbehörde
|
|
||||||
über die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legal-divider"></div>
|
|
||||||
|
|
||||||
<!-- 7. Aktualität -->
|
|
||||||
<div class="legal-block">
|
|
||||||
<div class="legal-block-label">§ 7 — AKTUALITÄT</div>
|
|
||||||
<h2>Aktualität dieser Datenschutzerklärung</h2>
|
|
||||||
<p>
|
|
||||||
Diese Datenschutzerklärung ist aktuell gültig und hat den Stand April 2026.
|
|
||||||
Durch die Weiterentwicklung dieser Website oder aufgrund geänderter
|
|
||||||
gesetzlicher bzw. behördlicher Vorgaben kann es notwendig werden,
|
|
||||||
diese Datenschutzerklärung zu ändern.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Die jeweils aktuelle Datenschutzerklärung kann jederzeit auf dieser
|
|
||||||
Seite abgerufen werden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="hazard-stripe-thin"></div>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
FOOTER
|
|
||||||
============================================================ -->
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container">
|
|
||||||
<div class="footer-inner">
|
|
||||||
<div class="footer-brand">
|
|
||||||
<div class="footer-brand-icon">D</div>
|
|
||||||
<span class="footer-brand-text">DocuMentor</span>
|
|
||||||
</div>
|
|
||||||
<ul class="footer-links">
|
|
||||||
<li><a href="index.html#features">Features</a></li>
|
|
||||||
<li><a href="index.html#workflow">Workflow</a></li>
|
|
||||||
<li><a href="index.html#techstack">Technik</a></li>
|
|
||||||
<li><a href="index.html#download">Download</a></li>
|
|
||||||
<li><a href="impressum.html">Impressum</a></li>
|
|
||||||
<li><a href="datenschutz.html" class="active">Datenschutz</a></li>
|
|
||||||
</ul>
|
|
||||||
<div class="footer-meta">
|
|
||||||
SYSTEM // DOCUMENTOR // BUILD 2026.02<br>
|
|
||||||
PySide6 Desktop-Anwendung // Open Source
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const toggle = document.getElementById('navToggle');
|
|
||||||
const links = document.getElementById('navLinks');
|
|
||||||
if (toggle && links) {
|
|
||||||
toggle.addEventListener('click', function() {
|
|
||||||
links.classList.toggle('open');
|
|
||||||
toggle.textContent = links.classList.contains('open') ? 'CLOSE' : 'MENU';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
const reveals = document.querySelectorAll('.reveal');
|
|
||||||
const observer = new IntersectionObserver(function(entries) {
|
|
||||||
entries.forEach(function(entry) {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('visible');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
|
||||||
reveals.forEach(function(el) { observer.observe(el); });
|
|
||||||
})();
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
const navbar = document.querySelector('.navbar');
|
|
||||||
let ticking = false;
|
|
||||||
window.addEventListener('scroll', function() {
|
|
||||||
if (!ticking) {
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
navbar.style.borderBottomColor = window.scrollY > 80
|
|
||||||
? 'rgba(240, 192, 48, 0.25)'
|
|
||||||
: 'rgba(240, 192, 48, 0.12)';
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user