From b5f663d9de03059232782cfb3acb21aef3e9ed27 Mon Sep 17 00:00:00 2001 From: daniel Date: Sat, 14 Feb 2026 22:15:58 +0100 Subject: [PATCH] =?UTF-8?q?Button=20Fachdaten=20laden=20hinzugef=C3=BCgt?= =?UTF-8?q?=20und=20angebunden=20(pipeline=20datagrabber-pr=C3=BCfer-daten?= =?UTF-8?q?lader-datenschreiber)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/listenauswerter.py | 137 +++++++++++++++++++++++++++++++ ui/tab_a_logic.py | 97 +--------------------- ui/tab_a_ui.py | 164 ++++++++++++++++++++++++++++++++----- 3 files changed, 282 insertions(+), 116 deletions(-) create mode 100644 modules/listenauswerter.py diff --git a/modules/listenauswerter.py b/modules/listenauswerter.py new file mode 100644 index 0000000..cf9a654 --- /dev/null +++ b/modules/listenauswerter.py @@ -0,0 +1,137 @@ +#sn_plan41/modules/listenauswerter.py +from typing import Any, Dict, List, Mapping, Optional, Tuple +from collections.abc import Mapping as _Mapping +# Prüfer-Typen (werden als Instanzen erwartet) +from sn_basis.modules.Pruefmanager import Pruefmanager # type: ignore +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.modules.stilpruefer import Stilpruefer # type: ignore + +class Listenauswerter: + """ + Validiert Zeilen aus einem DataDict, das vom DataGrabber stammt. + Erwartet wird die Struktur:: + + {"rows": [ {attr}, ... ]} + + Die Linkprüfung entfällt vollständig, da der DataGrabber nur gültige + Links liefert. Diese Methode prüft ausschließlich die Konsistenz der + Zeilen mit dem erwarteten Datenschema und führt optional eine + Stilprüfung durch. + """ + def __init__(self, pruefmanager, stil_pruefer): + """ Parameters + ---------- + pruefmanager: Instanz des Pruefmanagers, der pruef_ergebnis verarbeitet. + stil_pruefer: Instanz des Stilpruefers, der Stildateien prüft. + """ + self.pruefmanager = pruefmanager + self.stil_pruefer = stil_pruefer + + def validate_rows( + self, + data_dict: Dict[str, List[Mapping[str, Any]]] + ) -> Tuple[Dict[str, List[Mapping[str, Any]]], List[Any]]: + """ + Validiert die Zeilen aus ``data_dict`` anhand des erwarteten Schemas. + + Erwartete Felder pro Zeile + -------------------------- + Pflichtfelder: + - ``ident``: eindeutige Kennung + - ``Link``: bereits geprüfter Link (vom DataGrabber garantiert gültig) + - ``Provider``: Datenquelle (wird in Großbuchstaben normalisiert) + + Optionale Felder: + - ``Inhalt``: thematische Beschreibung + - ``Stildatei``: Pfad zur Stildatei (falls vorhanden) + + Verhalten + --------- + - Zeilen, denen Pflichtfelder fehlen oder deren Werte leer sind, + werden verworfen. + - ``Provider`` wird in Großbuchstaben normalisiert. + - Wenn ``Stildatei`` vorhanden ist, wird sie durch + ``self.stil_pruefer.pruefe(...)`` geprüft. + - Bei OK bleibt der Wert erhalten. + - Bei nicht OK wird ``Stildatei`` auf ``None`` gesetzt und das + verarbeitete Prüfergebnis gesammelt. + - Alle Prüfergebnisse werden durch ``self.pruefmanager.verarbeite(...)`` + geleitet und in der Rückgabe gesammelt. + + Rückgabe + -------- + Tuple[Dict[str, List[Mapping[str, Any]]]], List[Any]] + - ``valid_data_dict``: enthält nur Zeilen, die dem Schema entsprechen + - ``processed_results``: Liste der verarbeiteten Prüfergebnisse + + Hinweise + -------- + - Diese Methode führt **keine Linkprüfung** durch. + - Die Verantwortung für die Linkvalidität liegt vollständig beim DataGrabber. + - Die Methode verändert die Zeilen nur minimal (Provider‑Normalisierung, + Stildatei ggf. auf ``None``). + """ + + processed_results: List[Any] = [] + valid_rows: List[Mapping[str, Any]] = [] + + # Grundstruktur prüfen + if not isinstance(data_dict, dict): + return {"rows": []}, processed_results + + rows = data_dict.get("rows", []) + if not isinstance(rows, (list, tuple)): + return {"rows": []}, processed_results + + for raw in rows: + # Sicherstellen, dass raw ein Mapping ist + if not isinstance(raw, _Mapping): + continue + + ident = raw.get("ident") + inhalt = raw.get("Inhalt") + link = raw.get("Link") + stildatei = raw.get("Stildatei") + provider = raw.get("Provider") + + # Pflichtfelder prüfen + if not ident or not link or not provider: + # Fehler dokumentieren + pe = pruef_ergebnis( + ok=False, + meldung="Pflichtfelder fehlen oder sind leer", + aktion="pflichtfelder_fehlen", + kontext=raw, + ) + processed_results.append(self.pruefmanager.verarbeite(pe)) + continue + + # Provider normalisieren + provider_norm = str(provider).upper() + + # Stildatei prüfen (falls vorhanden) + if stildatei: + pe_stil = self.stil_pruefer.pruefe(stildatei) + processed_stil = self.pruefmanager.verarbeite(pe_stil) + + if not getattr(processed_stil, "ok", False): + processed_results.append(processed_stil) + stildatei_value: Optional[str] = None + else: + stildatei_value = stildatei + else: + stildatei_value = None + + # Validierte Zeile zusammenbauen + validated_row = { + "ident": ident, + "Inhalt": inhalt, + "Link": link, + "Stildatei": stildatei_value, + "Provider": provider_norm, + } + + valid_rows.append(validated_row) + + result_dict = {"rows": valid_rows} + return result_dict, processed_results diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py index 61d18aa..61bdbb7 100644 --- a/ui/tab_a_logic.py +++ b/ui/tab_a_logic.py @@ -155,99 +155,4 @@ class TabALogic: layer_type = get_layer_type(layer) return layer_type == "vector" - # ------------------------------- - # Validierung und Filterung von data_dict - # ------------------------------- - - def validate_and_filter_rows(self, data_dict: DataDict) -> Tuple[DataDict, List[Any]]: - """ - Validiert und filtert die Zeilen aus `data_dict`. - - Erwartete Struktur von `data_dict`: {'rows': [ {attr}, ... ]}. - - Für jede Zeile werden die folgenden Attribute gelesen: - ident = attr['ident'] (Pflicht) - thema = attr['Inhalt'] (optional) - url = attr['Link'] (Pflicht) - stildatei = attr['Stildatei'] (optional) - provider = attr['Provider'] (Pflicht, wird uppercased) - - Verhalten - - Pflichtfelder (ident, Link, Provider) müssen vorhanden und nicht-leer sein, - sonst wird die Zeile verworfen. - - Wenn Link nicht leer ist, wird self.link_pruefer.pruefe(url) aufgerufen. - - Ist das Ergebnis ok: Zeile wird behalten. - - Ist das Ergebnis nicht ok: Zeile wird verworfen; das verarbeitete - pruef_ergebnis wird gesammelt. - - Wenn Stildatei nicht leer ist, wird self.stil_pruefer.pruefe(stildatei) aufgerufen. - - Ist das Ergebnis ok: der Wert bleibt erhalten. - - Ist das Ergebnis nicht ok: das Feld `Stildatei` wird in der zurückgegebenen - Zeile auf None gesetzt; das verarbeitete pruef_ergebnis wird gesammelt. - - Alle pruef_ergebnis-Objekte werden an self.pruefmanager.verarbeite(...) übergeben. - Die verarbeiteten Ergebnisse werden in der Rückgabe-Liste gesammelt. - - Rückgabe - - (valid_data_dict, processed_results) - valid_data_dict: {'rows': [valid_row1, valid_row2, ...]} - processed_results: Liste der vom Pruefmanager verarbeiteten pruef_ergebnis-Objekte - """ - processed_results: List[Any] = [] - valid_rows: List[Row] = [] - - # Grundstruktur prüfen - if not isinstance(data_dict, dict): - return {"rows": []}, processed_results - - rows = data_dict.get("rows", []) - if not isinstance(rows, (list, tuple)): - return {"rows": []}, processed_results - - for raw in rows: - # Sicherstellen, dass raw ein Mapping ist - if not isinstance(raw, _Mapping): - continue - - ident = raw.get("ident") - inhalt = raw.get("Inhalt") - link = raw.get("Link") - stildatei = raw.get("Stildatei") - provider = raw.get("Provider") - - # Pflichtfelder prüfen - if not ident or not link or not provider: - continue - - # Provider normalisieren - provider_norm = str(provider).upper() - - # Link prüfen - pe_link = self.link_pruefer.pruefe(link) - processed_link = self.pruefmanager.verarbeite(pe_link) - if not getattr(processed_link, "ok", False): - processed_results.append(processed_link) - continue # Zeile verwerfen - - # Stil prüfen (falls vorhanden) - if stildatei: - pe_stil = self.stil_pruefer.pruefe(stildatei) - processed_stil = self.pruefmanager.verarbeite(pe_stil) - if not getattr(processed_stil, "ok", False): - processed_results.append(processed_stil) - stildatei_value: Optional[str] = None - else: - stildatei_value = stildatei - else: - stildatei_value = None - - # Validierte Zeile zusammenbauen - validated_row: Row = { - "ident": ident, - "Inhalt": inhalt, - "Link": link, - "Stildatei": stildatei_value, - "Provider": provider_norm, - } - valid_rows.append(validated_row) - - result_dict: DataDict = {"rows": valid_rows} - return result_dict, processed_results + \ No newline at end of file diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index bb60bcd..aaf8716 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -16,7 +16,7 @@ from sn_basis.functions.qt_wrapper import ( ArrowRight, SizePolicyPreferred, SizePolicyMaximum, - ComboBox, + QComboBox, ) from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel @@ -30,13 +30,15 @@ from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.stilpruefer import Stilpruefer +from sn_basis.modules.Datenschreiber import Datenschreiber + # Raumfilter-Optionen RAUMFILTER_VAR = "Raumfilter" RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") RAUMFILTER_DEFAULT = "Pufferlayer" -pm = Pruefmanager(ui_modus="qgis") -lp = Linkpruefer() -sp = Stilpruefer() +pm = Pruefmanager(ui_modus="qgis") +lp = Linkpruefer() +sp = Stilpruefer() class TabA(QWidget): @@ -62,6 +64,8 @@ class TabA(QWidget): self.pruefmanager = Pruefmanager(ui_modus="qgis") # DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer) + # Hinweis: DataGrabber erwartet ggf. Prüfer-Objekte; hier werden sie nicht übergeben, + # da TabALogic / Pruefmanager diese zur Laufzeit bereitstellen können. self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager) # Platzhalter, die vom Plugin oder Nutzer gesetzt werden können @@ -72,7 +76,7 @@ class TabA(QWidget): self.lokale_linkliste: Optional[str] = None # UI-Widget-Referenz für Raumfilter - self._raumfilter_combo: Optional[ComboBox] = None + self._raumfilter_combo: Optional[QComboBox] = None if build_ui: self._build_ui() @@ -164,7 +168,7 @@ class TabA(QWidget): # Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl) main_layout.addWidget(QLabel("Raumfilter")) - self._raumfilter_combo = ComboBox(self) + self._raumfilter_combo = QComboBox(self) # Fülle Optionen (Wrapper stellt addItems bereit) try: self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS)) @@ -211,8 +215,14 @@ class TabA(QWidget): main_layout.addWidget(self._raumfilter_combo) - # Aktion: Fachdaten laden - self.btn_load = QPushButton("Fachdaten laden") + # Neuer Button direkt unterhalb der Raumfilter-Combo: "Fachdaten laden" + self.btn_pipeline = QPushButton("Fachdaten laden") + self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log") + self.btn_pipeline.clicked.connect(self._on_run_pipeline) + main_layout.addWidget(self.btn_pipeline) + + # (Optional) bestehender Button weiter unten für alternative Platzierung + self.btn_load = QPushButton("Fachdaten laden (alt)") self.btn_load.clicked.connect(self._on_load_fachdaten) main_layout.addWidget(self.btn_load) @@ -307,11 +317,8 @@ class TabA(QWidget): def _on_load_fachdaten(self) -> None: """ - Platzhalter-Handler für 'Fachdaten laden'. - - Keine Prüfungen oder Exception-Handling hier. Die fachliche Prüfung - und Fehlerbehandlung erfolgen zur Laufzeit durch den Pruefmanager und - die Prüfer, die vom DataGrabber verwendet werden. + Bestehender, kompakter Handler für 'Fachdaten laden'. + Führt Dateiprüfung und DataGrabber.run aus (wie zuvor). """ pfad = self.file_widget.filePath() @@ -327,13 +334,130 @@ class TabA(QWidget): except Exception: zielpfad = ergebnis.kontext - self.data_grabber.run( - attributes_list=self._attributes_list, - pufferlayer=self._pufferlayer, - zielpfad=zielpfad, - temporaer=(ergebnis.aktion == "temporaer_erzeugen"), - temporaer_erlaubt=True, - ) + # DataGrabber.run wird wie bisher aufgerufen; Signatur kann variieren. + # Wir übergeben die bekannten Parameter; DataGrabber ist verantwortlich, + # die Linkliste intern zu verwenden (z. B. aus TabALogic oder über Argumente). + try: + self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + except Exception: + # Fehler werden vom Pruefmanager / DataGrabber protokolliert + pass + + def _on_run_pipeline(self) -> None: + """ + Neuer, vollständiger Pipeline-Handler, der: + - Dateiprüfung (Verfahrens-DB) + - DataGrabber-Ausführung (mit Linkliste) + - Datenschreiber (schreiben, laden) + - Logschreiber (Log-Datei) + ausführt und Ergebnisse über den Pruefmanager protokolliert. + """ + # 1) Verfahrens-DB prüfen / ermitteln + pfad = self.file_widget.filePath() + pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) + ergebnis = pruefer.pruefe() + ergebnis = self.pruefmanager.verarbeite(ergebnis) + + zielpfad = None + if ergebnis.kontext is not None: + try: + zielpfad = str(ergebnis.kontext) + except Exception: + zielpfad = ergebnis.kontext + + if not zielpfad: + # Falls kein Zielpfad ermittelt werden konnte, protokollieren und abbrechen + pe_err = pruef_ergebnis( + ok=False, + meldung="Kein gültiger Speicherort für Verfahrens-DB ermittelt; Pipeline abgebrochen.", + aktion="kein_dateipfad", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # 2) DataGrabber ausführen + # Erwartung: DataGrabber.run gibt (daten_dict, processed_results) zurück. + # Falls die konkrete Implementierung anders ist, passt dieser Aufruf entsprechend an. + try: + run_result = self.data_grabber.run( + attributes_list=self._attributes_list, + pufferlayer=self._pufferlayer, + zielpfad=zielpfad, + temporaer=(ergebnis.aktion == "temporaer_erzeugen"), + temporaer_erlaubt=True, + ) + except Exception as exc: + pe_err = pruef_ergebnis( + ok=False, + meldung=f"DataGrabber-Fehler: {exc}", + aktion="datenabruf", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # Normalisiere Rückgabe: unterstütze sowohl None, einzelnes dict oder Tuple + daten_dict = {} + processed_results = [] + if isinstance(run_result, tuple) and len(run_result) >= 2: + daten_dict, processed_results = run_result[0], run_result[1] + elif isinstance(run_result, dict) and "daten" in run_result: + daten_dict = run_result + # processed_results bleiben leer oder werden vom DataGrabber intern protokolliert + else: + # Wenn run() nichts zurückgibt, versuchen wir, auf DataGrabber intern gespeicherte Ergebnisse zuzugreifen + daten_dict = getattr(self.data_grabber, "last_daten_dict", {}) or {} + processed_results = getattr(self.data_grabber, "last_processed_results", []) or [] + + # 3) Datenschreiber: Daten in GPKG schreiben + try: + ds = Datenschreiber(pruefmanager=self.pruefmanager, gpkg_path=zielpfad) + layer_infos = ds.schreibe_Daten(daten_dict=daten_dict, processed_results=processed_results, speicherort=zielpfad) + except Exception as exc: + pe_err = pruef_ergebnis( + ok=False, + meldung=f"Fehler beim Schreiben der Daten: {exc}", + aktion="save_exception", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_err) + return + + # 4) Layer laden und Stile anwenden + try: + ds.lade_Layer(layer_infos) + except Exception as exc: + pe_warn = pruef_ergebnis( + ok=True, + meldung=f"Fehler beim Laden der Layer: {exc}", + aktion="layer_nicht_gefunden", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_warn) + + # 5) Log schreiben + try: + log_path = ds.schreibe_log(processed_results=processed_results, speicherort=zielpfad) + # Optional: zeige Erfolgsmeldung + try: + QMessageBox.information(self, "Pipeline abgeschlossen", f"Pipeline erfolgreich abgeschlossen.\nLog: {log_path}") + except Exception: + pass + except Exception as exc: + pe_warn = pruef_ergebnis( + ok=True, + meldung=f"Log konnte nicht geschrieben werden: {exc}", + aktion="standarddatei_vorschlagen", + kontext={}, + ) + self.pruefmanager.verarbeite(pe_warn) # --------------------------------------------------------- # Raumfilter Callback