diff --git a/src/conf.py b/src/conf.py index 1122676..281d0d1 100644 --- a/src/conf.py +++ b/src/conf.py @@ -104,41 +104,29 @@ class Project(BaseModel): 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") - def getXsl(self) -> str: - global app_settings - value = [x.name for x in app_settings.xsl_dirs if x.id == self.xsl_dir_id] + @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 "" - return value[0] if len(value) else "" + def getXsl(self) -> str: + return self._lookup(app_settings.xsl_dirs, self.xsl_dir_id, "name") def getJavaVm(self) -> str: - 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 "" + return self._lookup(app_settings.java_vms, self.java_vm_id, "version") def getSaxon(self) -> str: - 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 "" + return self._lookup(app_settings.saxon_jars, self.saxon_jar_id, "version") def getApacheFop(self) -> str: - 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 "" + return self._lookup(app_settings.apache_fops, self.apache_fop_id, "version") def getDiffPdf(self) -> str: - 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 "" + return self._lookup(app_settings.diff_pdfs, self.diff_pdf_id, "version") def getPostgreSqlDb(self) -> str: - 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 "" + return self._lookup(app_settings.postgresql_dbs, self.postgre_sql_db_id, "name") class AppSettings(BaseSettings): diff --git a/src/fop_pool.py b/src/fop_pool.py index 64d7e99..c2dcb7d 100644 --- a/src/fop_pool.py +++ b/src/fop_pool.py @@ -11,7 +11,6 @@ import threading import time import psutil from pathlib import Path -from queue import Queue from typing import Optional import tempfile @@ -196,10 +195,8 @@ class FopWorkerPool: self.fop_config_file = fop_config_file self.log_dir = log_dir - # Worker-Prozesse und Queues + # Worker-Prozesse 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 diff --git a/src/saxon_pool.py b/src/saxon_pool.py index b6a0fd5..0222348 100644 --- a/src/saxon_pool.py +++ b/src/saxon_pool.py @@ -11,7 +11,6 @@ import threading import time import psutil from pathlib import Path -from queue import Queue from typing import Optional import tempfile @@ -207,10 +206,8 @@ class SaxonWorkerPool: self.classpath_cache = classpath_cache self.log_dir = log_dir - # Worker-Prozesse und Queues + # Worker-Prozesse 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 diff --git a/src/saxon_pool_s9api.py b/src/saxon_pool_s9api.py index be2f854..2da19b9 100644 --- a/src/saxon_pool_s9api.py +++ b/src/saxon_pool_s9api.py @@ -12,7 +12,6 @@ import threading import time import psutil from pathlib import Path -from queue import Queue from typing import Optional import tempfile @@ -188,10 +187,8 @@ class SaxonWorkerPoolS9Api: self.classpath_cache = classpath_cache self.log_dir = log_dir - # Worker-Prozesse und Queues + # Worker-Prozesse 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 diff --git a/src/ui/PdfProject.py b/src/ui/PdfProject.py index 620dc86..3afefae 100644 --- a/src/ui/PdfProject.py +++ b/src/ui/PdfProject.py @@ -291,50 +291,3 @@ class PdfProjectDlg(QDialog): self.project_data = 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 diff --git a/src/ui/WorkerPoolMetricsDialog.py b/src/ui/WorkerPoolMetricsDialog.py index c80bff5..fd143ba 100644 --- a/src/ui/WorkerPoolMetricsDialog.py +++ b/src/ui/WorkerPoolMetricsDialog.py @@ -12,12 +12,9 @@ from PySide6.QtWidgets import ( QGroupBox, QLabel, QPushButton, - QTextEdit, QTabWidget, QWidget, ) -from PySide6.QtCore import Qt - logger = logging.getLogger(__name__) diff --git a/src/ui/mixins/hash_calculation.py b/src/ui/mixins/hash_calculation.py index 4789d11..3ec5f72 100644 --- a/src/ui/mixins/hash_calculation.py +++ b/src/ui/mixins/hash_calculation.py @@ -5,9 +5,8 @@ Dieses Mixin enthält alle Methoden zur blake2b-Hash-Berechnung, XML-Datei-Zuordnung und Duplikatserkennung für das MainWindow. """ -import time -import hashlib import shutil +import time import logging from pathlib import Path from typing import List @@ -17,6 +16,7 @@ from PySide6.QtWidgets import QMessageBox from conf import TreeNode, XslFile, XmlFile from ui.XmlToXslAssignDialog import XmlToXslAssignDialog from ui.threads import XmlHashCalculatorThread +from utils import calculate_blake2b_hash logger = logging.getLogger(__name__) @@ -185,14 +185,14 @@ class HashCalculationMixin: nodes: Liste der zu durchsuchenden Nodes xml_files: Liste zum Sammeln der XML-Dateien """ + seen_paths = {xf.xml for xf in xml_files} for node in nodes: if isinstance(node, XslFile) and node.xmls: - # Füge alle XML-Dateien dieser XSL-Datei hinzu for xml_file in node.xmls: - if xml_file not in xml_files: # Vermeide Duplikate + 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: - # Rekursiv in Kinder-Nodes suchen self._collect_xml_files_recursive(node.children, xml_files) def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str): @@ -253,68 +253,22 @@ class HashCalculationMixin: logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}") return - # 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 xml_file_path = Path(self.project.project_dir) / xml_file.xml - - if not xml_file_path.exists(): - logger.warning(f"XML-Datei nicht gefunden: {xml_file_path}") - return - - # Datei binär lesen und Hash berechnen - with open(xml_file_path, "rb") as f: - file_content = f.read() - hash_obj = hashlib.blake2b(file_content) - hash_hex = hash_obj.hexdigest() - - # Hash mit Präfix setzen - xml_file.hashsum = f"blake2b:{hash_hex}" - logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}") + 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 _get_all_project_xml_files(self) -> List[XmlFile]: - """ - Sammelt alle XmlFile-Objekte aus dem gesamten Projekt für Hash-Vergleiche. - - Returns: - List[XmlFile]: Liste aller XML-Dateien im Projekt - """ - xml_files = [] - - try: - if self.pdf_project and self.pdf_project.nodes: - self._collect_xml_files_for_hash_comparison(self.pdf_project.nodes, xml_files) - - logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden") - return xml_files - - except Exception as e: - logger.error(f"Fehler beim Sammeln der XML-Dateien für Hash-Vergleich: {e}") - return [] - - def _collect_xml_files_for_hash_comparison(self, nodes, xml_files: List[XmlFile]): - """ - Sammelt rekursiv alle XML-Dateien aus den Nodes für Hash-Vergleiche. - - Args: - nodes: Liste der zu durchsuchenden Nodes - xml_files: Liste zum Sammeln der XML-Dateien - """ - for node in nodes: - if isinstance(node, XslFile) and node.xmls: - # Füge alle XML-Dateien dieser XSL-Datei hinzu - for xml_file in node.xmls: - # Vermeide Duplikate basierend auf Pfad - if not any(existing.xml == xml_file.xml for existing in xml_files): - xml_files.append(xml_file) - elif isinstance(node, TreeNode) and node.children: - # Rekursiv in Kinder-Nodes suchen - self._collect_xml_files_for_hash_comparison(node.children, xml_files) + """Sammelt alle XmlFile-Objekte aus dem gesamten Projekt.""" + return self._collect_all_xml_files() def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None: """ @@ -410,34 +364,8 @@ class HashCalculationMixin: 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. - - Args: - file_path: Pfad zur Datei - - Returns: - str|None: 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 - - # Datei binär lesen und Hash berechnen - with open(file_path, "rb") as f: - file_content = f.read() - hash_obj = hashlib.blake2b(file_content) - hash_hex = hash_obj.hexdigest() - - # Hash mit Präfix zurückgeben - hash_value = f"blake2b:{hash_hex}" - logger.debug(f"Hash berechnet für {file_path}: {hash_value}") - return hash_value - - except Exception as e: - logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}") - return 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): """ diff --git a/src/ui/mixins/transformation.py b/src/ui/mixins/transformation.py index c251cd0..1f92419 100644 --- a/src/ui/mixins/transformation.py +++ b/src/ui/mixins/transformation.py @@ -579,9 +579,6 @@ class TransformationMixin: # Progress Bar anzeigen map_key = f"{xml_file_name}|{xsl_id_str}" - if map_key not in self.xml_item_map and self.xml_item_map: - # Zeige erste Keys zur Diagnose - list(self.xml_item_map.keys())[:3] 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) @@ -741,10 +738,12 @@ class TransformationMixin: f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}", ) - def _transform_all_xml_files(self): + def _transform_all_xml_files(self, force: bool = False): """ Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets. - Nur Dateien, die nicht up-to-date sind, werden transformiert. + + Args: + force: Wenn True, werden alle Dateien unabhängig vom Änderungsstatus neu transformiert. """ try: if not self.project or not self.pdf_project: @@ -781,87 +780,43 @@ class TransformationMixin: 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, - "Alle XML-Dateien transformieren", - f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n" - f"Nur Dateien mit Änderungen werden verarbeitet.", + title, + message, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.Yes, + default_button, ) if reply != QMessageBox.StandardButton.Yes: - logger.info("Transformation abgebrochen durch Benutzer") + logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer") return - logger.info(f"Starte Transformation für alle {len(all_jobs)} XML-Dateien (nicht-force)") + 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=False) + 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 in allen TreeNodes des TreeWidgets (force). - Alle Dateien werden 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 (mit Warnung wegen force) - reply = QMessageBox.question( - self, - "Alle XML-Dateien neu transformieren (force)", - 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.", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No, - ) - - if reply != QMessageBox.StandardButton.Yes: - logger.info("Force-Transformation abgebrochen durch Benutzer") - return - - logger.info(f"Starte Force-Transformation für alle {len(all_jobs)} XML-Dateien") - - # Starte Transformation in separatem Thread (mit force=True) - self._start_transformation(all_jobs, force=True) - - except Exception as e: - logger.error(f"Fehler beim Force-Transformieren aller XML-Dateien: {e}") - QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Force-Transformation: {str(e)}") + """Transformiert ALLE XML-Dateien im Force-Modus.""" + self._transform_all_xml_files(force=True) diff --git a/src/ui/threads.py b/src/ui/threads.py index 8c3bddb..56f7827 100644 --- a/src/ui/threads.py +++ b/src/ui/threads.py @@ -7,16 +7,14 @@ Dieses Modul enthält alle QThread-Klassen, die für Hintergrundoperationen verw - TransformationThread: Ausführung von XSL-Transformationen """ -import hashlib import logging import shutil from pathlib import Path -from typing import List - 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__) @@ -32,7 +30,7 @@ class XmlHashCalculatorThread(QThread): 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]): + def __init__(self, project_dir: Path, xml_files: list[XmlFile]): """ Initialisiert den Hash-Berechnungs-Thread. @@ -81,32 +79,8 @@ class XmlHashCalculatorThread(QThread): 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. - - Args: - file_path: Pfad zur XML-Datei - - Returns: - str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler - """ - try: - if not file_path.exists(): - logger.warning(f"XML-Datei nicht gefunden: {file_path}") - return None - - # Datei binär lesen und Hash berechnen - with open(file_path, "rb") as f: - file_content = f.read() - hash_obj = hashlib.blake2b(file_content) - hash_hex = hash_obj.hexdigest() - - # Präfix hinzufügen - return f"blake2b:{hash_hex}" - - except Exception as e: - logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}") - return None + """Berechnet den blake2b-Hash einer XML-Datei.""" + return calculate_blake2b_hash(file_path) class XmlBatchProcessingThread(QThread): @@ -217,20 +191,7 @@ class XmlBatchProcessingThread(QThread): def _calculate_hash_for_file(self, file_path: Path) -> str | None: """Berechnet blake2b Hash für eine Datei.""" - try: - if not file_path.exists(): - return None - - with open(file_path, "rb") as f: - file_content = f.read() - hash_obj = hashlib.blake2b(file_content) - hash_hex = hash_obj.hexdigest() - - return f"blake2b:{hash_hex}" - - except Exception as e: - logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}") - return None + 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.""" diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..3c45d66 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,38 @@ +""" +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