qt_wrapper, dialog;wrapper, Pruef_ergebnis und Pruefmanager überarbeitet, so dass die Übergaben jetzt stimmen. Nutzerabfragen werden tatsächlich ausgelöst- Nutzerabfrage Datei überschreiebn... ist noch Blödsinn

This commit is contained in:
2026-03-04 15:32:49 +01:00
parent f8be65f6f6
commit 3b56725e4f
7 changed files with 756 additions and 1053 deletions

3
__pdoc__.py Normal file
View File

@@ -0,0 +1,3 @@
__pdoc__ = {
"main": False,
}

View File

@@ -1,62 +1,37 @@
""" """
sn_basis/functions/dialog_wrapper.py Benutzer-Dialoge sn_basis/functions/dialog_wrapper.py Benutzer-Dialoge (Qt5/6/Mock-kompatibel)
Dieser Wrapper kapselt alle Benutzer-Dialoge (z. B. Ja/Nein-Abfragen)
und sorgt dafür, dass sie sowohl in QGIS als auch im Mock-/Testmodus
einheitlich funktionieren.
""" """
from typing import Any from typing import Any
# Import der abstrahierten Qt-Klassen aus dem qt_wrapper.
# QMessageBox, YES und NO sind bereits kompatibel zu Qt5/Qt6
# und im Mock-Modus durch Dummy-Objekte ersetzt.
from sn_basis.functions.qt_wrapper import ( from sn_basis.functions.qt_wrapper import (
QMessageBox, QMessageBox, YES, NO, QT_VERSION
YES,
NO,
) )
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def ask_yes_no( def ask_yes_no(
title: str, title: str,
message: str, message: str,
default: bool = False, default: bool = True,
parent: Any = None, parent: Any = None,
) -> bool: ) -> bool:
""" """
Stellt dem Benutzer eine Ja/Nein-Frage. Stellt Ja/Nein-Frage. Funktioniert in PyQt5/6 UND Mock-Modus.
- In einer echten QGIS-Umgebung wird ein QMessageBox-Dialog angezeigt.
- Im Mock-/Testmodus wird kein Dialog geöffnet, sondern der Default-Wert
zurückgegeben, damit Tests ohne UI laufen können.
:param title: Titel des Dialogs
:param message: Nachrichtentext
:param default: Rückgabewert im Fehler- oder Mock-Fall
:param parent: Optionales Parent-Widget
:return: True bei "Ja", False bei "Nein"
""" """
try: try:
# Definiert die beiden Buttons, die angezeigt werden sollen. if QT_VERSION == 0: # Mock-Modus
buttons = QMessageBox.Yes | QMessageBox.No print(f"🔍 Mock-Modus: ask_yes_no('{title}') → {default}")
return default
# ✅ KORREKT: Verwende YES/NO-Aliase aus qt_wrapper!
buttons = YES | NO
default_button = YES if default else NO
# Öffnet den Dialog (oder im Mock-Modus: simuliert ihn).
result = QMessageBox.question( result = QMessageBox.question(
parent, parent, title, message, buttons, default_button
title,
message,
buttons,
YES if default else NO, # Vorauswahl abhängig vom Default
) )
# Gibt True zurück, wenn der Benutzer "Ja" gewählt hat. # ✅ int(result) == int(YES) funktioniert Qt5/6/Mock
return result == YES print(f"DEBUG ask_yes_no: result={result}, YES={YES}, match={int(result) == int(YES)}")
return int(result) == int(YES)
except Exception: except Exception as e:
# Falls Qt nicht verfügbar ist (Mock/CI), wird der Default-Wert genutzt. print(f"⚠️ ask_yes_no Fehler: {e}")
return default return default

View File

@@ -1,157 +1,64 @@
""" """
sn_basis/functions/qt_wrapper.py zentrale Qt-Abstraktion (PyQt5 / PyQt6 / Mock) sn_basis/functions/qt_wrapper.py zentrale Qt-Abstraktion (PyQt6 primär / PyQt5 Fallback / Mock)
""" """
from typing import Optional, Type, Any from typing import Optional, Type, Any, Callable
# ---------------------------------------------------------
# Qt-Symbole (werden dynamisch gesetzt)
# ---------------------------------------------------------
QDockWidget: Type[Any]
QMessageBox: Type[Any]
QFileDialog: Type[Any]
QEventLoop: Type[Any]
QUrl: Type[Any]
QNetworkRequest: Type[Any]
QNetworkReply: Type[Any]
QCoreApplication: Type[Any]
QWidget: Type[Any]
QGridLayout: Type[Any]
QLabel: Type[Any]
QLineEdit: Type[Any]
QGroupBox: Type[Any]
QVBoxLayout: Type[Any]
QPushButton: Type[Any]
QAction: Type[Any]
QMenu: Type[Any]
QToolBar: Type[Any]
QActionGroup: Type[Any]
QTabWidget: type
QToolButton: Type[Any]
QSizePolicy: Type[Any]
Qt: Type[Any]
QComboBox: Type[Any]
# Globale Qt-Symbole (werden dynamisch gesetzt)
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
YES: Optional[Any] = None YES: Optional[Any] = None
NO: Optional[Any] = None NO: Optional[Any] = None
CANCEL: Optional[Any] = None CANCEL: Optional[Any] = None
ICON_QUESTION: Optional[Any] = None ICON_QUESTION: Optional[Any] = None
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6 # Qt-Klassen (werden dynamisch gesetzt)
QDockWidget: Type[Any] = object
QMessageBox: Type[Any] = object
QFileDialog: Type[Any] = object
QEventLoop: Type[Any] = object
QUrl: Type[Any] = object
QNetworkRequest: Type[Any] = object
QNetworkReply: Type[Any] = object
QCoreApplication: Type[Any] = object
QWidget: Type[Any] = object
QGridLayout: Type[Any] = object
QLabel: Type[Any] = object
QLineEdit: Type[Any] = object
QGroupBox: Type[Any] = object
QVBoxLayout: Type[Any] = object
QPushButton: Type[Any] = object
QAction: Type[Any] = object
QMenu: Type[Any] = object
QToolBar: Type[Any] = object
QActionGroup: Type[Any] = object
QTabWidget: Type[Any] = object
QToolButton: Type[Any] = object
QSizePolicy: Type[Any] = object
Qt: Type[Any] = object
QComboBox: Type[Any] = object
def exec_dialog(dialog: Any) -> Any: def exec_dialog(dialog: Any) -> Any:
raise NotImplementedError """Führt Dialog modal aus (Qt6: exec(), Qt5: exec_(), Mock: YES)"""
raise NotImplementedError("Qt nicht initialisiert")
def debug_qt_status() -> None:
"""Debug: Zeigt Qt-Status für Troubleshooting."""
print(f"🔍 QT_VERSION: {QT_VERSION}")
print(f"🔍 QMessageBox Typ: {getattr(QMessageBox, '__name__', type(QMessageBox).__name__)}")
print(f"🔍 YES Wert: {YES} (Typ: {type(YES) if YES is not None else 'None'})")
# --------------------------------------------------------- if QT_VERSION == 0:
# Versuch: PyQt6 print("❌ MOCK-MODUS AKTIV! Keine Dialoge möglich!")
# --------------------------------------------------------- elif QT_VERSION == 5:
print("✅ PyQt5 geladen (Fallback) Dialoge sollten funktionieren!")
elif QT_VERSION == 6:
print("✅ PyQt6 geladen (primär) Dialoge sollten funktionieren!")
else:
print("❓ Unbekannte Qt-Version!")
# --------------------------- PYQT6 PRIMÄR ---------------------------
try: try:
from qgis.PyQt.QtWidgets import ( # type: ignore from qgis.PyQt.QtWidgets import (
QMessageBox as _QMessageBox,# type: ignore
QFileDialog as _QFileDialog,# type: ignore
QWidget as _QWidget,# type: ignore
QGridLayout as _QGridLayout,# type: ignore
QLabel as _QLabel,# type: ignore
QLineEdit as _QLineEdit,# type: ignore
QGroupBox as _QGroupBox,# type: ignore
QVBoxLayout as _QVBoxLayout,# type: ignore
QPushButton as _QPushButton,# type: ignore
QAction as _QAction,
QMenu as _QMenu,# type: ignore
QToolBar as _QToolBar,# type: ignore
QActionGroup as _QActionGroup,# type: ignore
QDockWidget as _QDockWidget,# type: ignore
QTabWidget as _QTabWidget,# type: ignore
QToolButton as _QToolButton,#type:ignore
QSizePolicy as _QSizePolicy,#type:ignore
QComboBox as _QComboBox,
)
from qgis.PyQt.QtCore import ( # type: ignore
QEventLoop as _QEventLoop,# type: ignore
QUrl as _QUrl,# type: ignore
QCoreApplication as _QCoreApplication,# type: ignore
Qt as _Qt#type:ignore
)
from qgis.PyQt.QtNetwork import ( # type: ignore
QNetworkRequest as _QNetworkRequest,# type: ignore
QNetworkReply as _QNetworkReply,# type: ignore
)
QT_VERSION = 6
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
Qt=_Qt
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
QToolButton=_QToolButton
QSizePolicy=_QSizePolicy
QComboBox=_QComboBox
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
# ---------------------------------------------------------
# Qt6 Enum-Aliase (vereinheitlicht)
# ---------------------------------------------------------
ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
ArrowDown = Qt.ArrowType.DownArrow
ArrowRight = Qt.ArrowType.RightArrow
# QSizePolicy Enum-Aliase (Qt6)
SizePolicyPreferred = QSizePolicy.Policy.Preferred
SizePolicyMaximum = QSizePolicy.Policy.Maximum
# ---------------------------------------------------------
# QDockWidget Feature-Aliase (Qt6)
# ---------------------------------------------------------
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
# ---------------------------------------------------------
# Dock-Area-Aliase (Qt6)
# ---------------------------------------------------------
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
# ---------------------------------------------------------
# Versuch: PyQt5
# ---------------------------------------------------------
except Exception:
try:
from PyQt5.QtWidgets import (# type: ignore
QMessageBox as _QMessageBox, QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog, QFileDialog as _QFileDialog,
QWidget as _QWidget, QWidget as _QWidget,
@@ -171,18 +78,19 @@ except Exception:
QSizePolicy as _QSizePolicy, QSizePolicy as _QSizePolicy,
QComboBox as _QComboBox, QComboBox as _QComboBox,
) )
from PyQt5.QtCore import (# type: ignore from qgis.PyQt.QtCore import (
QEventLoop as _QEventLoop, QEventLoop as _QEventLoop,
QUrl as _QUrl, QUrl as _QUrl,
QCoreApplication as _QCoreApplication, QCoreApplication as _QCoreApplication,
Qt as _Qt, Qt as _Qt,
) )
from PyQt5.QtNetwork import (# type: ignore from qgis.PyQt.QtNetwork import (
QNetworkRequest as _QNetworkRequest, QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply, QNetworkReply as _QNetworkReply,
) )
# ✅ ALLE GLOBALS ZUWEISEN
QT_VERSION = 6
QMessageBox = _QMessageBox QMessageBox = _QMessageBox
QFileDialog = _QFileDialog QFileDialog = _QFileDialog
QEventLoop = _QEventLoop QEventLoop = _QEventLoop
@@ -190,10 +98,8 @@ except Exception:
QNetworkRequest = _QNetworkRequest QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication QCoreApplication = _QCoreApplication
Qt=_Qt Qt = _Qt
QDockWidget = _QDockWidget QDockWidget = _QDockWidget
QWidget = _QWidget QWidget = _QWidget
QGridLayout = _QGridLayout QGridLayout = _QGridLayout
QLabel = _QLabel QLabel = _QLabel
@@ -206,55 +112,125 @@ except Exception:
QToolBar = _QToolBar QToolBar = _QToolBar
QActionGroup = _QActionGroup QActionGroup = _QActionGroup
QTabWidget = _QTabWidget QTabWidget = _QTabWidget
QToolButton=_QToolButton QToolButton = _QToolButton
QSizePolicy=_QSizePolicy QSizePolicy = _QSizePolicy
ComboBox=_QComboBox QComboBox = _QComboBox
# ✅ QT6 ENUMS
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
# Qt6 Enum-Aliase
ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
ArrowDown = Qt.ArrowType.DownArrow
ArrowRight = Qt.ArrowType.RightArrow
SizePolicyPreferred = QSizePolicy.Policy.Preferred
SizePolicyMaximum = QSizePolicy.Policy.Maximum
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
print(f"✅ qt_wrapper: PyQt6 geladen (QT_VERSION={QT_VERSION})")
# --------------------------- PYQT5 FALLBACK ---------------------------
except (ImportError, AttributeError):
try:
from PyQt5.QtWidgets import (
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
QAction as _QAction,
QMenu as _QMenu,
QToolBar as _QToolBar,
QActionGroup as _QActionGroup,
QDockWidget as _QDockWidget,
QTabWidget as _QTabWidget,
QToolButton as _QToolButton,
QSizePolicy as _QSizePolicy,
QComboBox as _QComboBox,
)
from PyQt5.QtCore import (
QEventLoop as _QEventLoop,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
Qt as _Qt,
)
from PyQt5.QtNetwork import (
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
# ✅ ALLE GLOBALS ZUWEISEN
QT_VERSION = 5
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
Qt = _Qt
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
QToolButton = _QToolButton
QSizePolicy = _QSizePolicy
QComboBox = _QComboBox
# ✅ PYQT5 ENUMS
YES = QMessageBox.Yes YES = QMessageBox.Yes
NO = QMessageBox.No NO = QMessageBox.No
CANCEL = QMessageBox.Cancel CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question ICON_QUESTION = QMessageBox.Question
QT_VERSION = 5 # PyQt5 Enum-Aliase
# then try next backend
# ---------------------------------------------------------
# Qt5 Enum-Aliase (vereinheitlicht)
# ---------------------------------------------------------
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
ArrowDown = Qt.DownArrow ArrowDown = Qt.DownArrow
ArrowRight = Qt.RightArrow ArrowRight = Qt.RightArrow
# QSizePolicy Enum-Aliase (Qt5)
SizePolicyPreferred = QSizePolicy.Preferred SizePolicyPreferred = QSizePolicy.Preferred
SizePolicyMaximum = QSizePolicy.Maximum SizePolicyMaximum = QSizePolicy.Maximum
# ---------------------------------------------------------
# QDockWidget Feature-Aliase (Qt5)
# ---------------------------------------------------------
DockWidgetMovable = QDockWidget.DockWidgetMovable DockWidgetMovable = QDockWidget.DockWidgetMovable
DockWidgetFloatable = QDockWidget.DockWidgetFloatable DockWidgetFloatable = QDockWidget.DockWidgetFloatable
DockWidgetClosable = QDockWidget.DockWidgetClosable DockWidgetClosable = QDockWidget.DockWidgetClosable
# ---------------------------------------------------------
# Dock-Area-Aliase (Qt5)
# ---------------------------------------------------------
DockAreaLeft = Qt.LeftDockWidgetArea DockAreaLeft = Qt.LeftDockWidgetArea
DockAreaRight = Qt.RightDockWidgetArea DockAreaRight = Qt.RightDockWidgetArea
def exec_dialog(dialog: Any) -> Any: def exec_dialog(dialog: Any) -> Any:
return dialog.exec_() return dialog.exec_()
# --------------------------------------------------------- print(f"✅ qt_wrapper: PyQt5 Fallback geladen (QT_VERSION={QT_VERSION})")
# Mock-Modus
# ---------------------------------------------------------
# --------------------------- MOCK-MODUS ---------------------------
except Exception: except Exception:
QT_VERSION = 0 QT_VERSION = 0
print("⚠️ qt_wrapper: Mock-Modus aktiviert (QT_VERSION=0)")
# Fake Enum für Bit-Operationen
class FakeEnum(int): class FakeEnum(int):
def __or__(self, other: int) -> "FakeEnum": def __or__(self, other: Any) -> "FakeEnum":
return FakeEnum(int(self) | int(other)) return FakeEnum(int(self) | int(other))
YES = FakeEnum(1) YES = FakeEnum(1)
@@ -262,103 +238,72 @@ except Exception:
CANCEL = FakeEnum(4) CANCEL = FakeEnum(4)
ICON_QUESTION = FakeEnum(8) ICON_QUESTION = FakeEnum(8)
# Im Mock-Block von qt_wrapper.py:
class _MockQMessageBox: class _MockQMessageBox:
Yes = YES Yes = YES
No = NO No = NO
Cancel = CANCEL Cancel = CANCEL
Question = ICON_QUESTION Question = ICON_QUESTION
@classmethod
def question(cls, parent, title, message, buttons, default_button):
"""Mock: Gibt immer default_button zurück"""
print(f"🔍 Mock QMessageBox.question: '{title}'{default_button}")
return default_button
QMessageBox = _MockQMessageBox QMessageBox = _MockQMessageBox
class _MockQFileDialog: class _MockQFileDialog:
@staticmethod @staticmethod
def getOpenFileName(*args, **kwargs): def getOpenFileName(*args, **kwargs): return ("", "")
return ("", "")
@staticmethod @staticmethod
def getSaveFileName(*args, **kwargs): def getSaveFileName(*args, **kwargs): return ("", "")
return ("", "")
QFileDialog = _MockQFileDialog QFileDialog = _MockQFileDialog
class _MockQEventLoop: class _MockQEventLoop:
def exec(self) -> int: def exec(self) -> int: return 0
return 0 def quit(self) -> None: pass
def quit(self) -> None:
pass
QEventLoop = _MockQEventLoop QEventLoop = _MockQEventLoop
class _MockQUrl(str): class _MockQUrl(str):
def isValid(self) -> bool: def isValid(self) -> bool: return True
return True
QUrl = _MockQUrl QUrl = _MockQUrl
class _MockQNetworkRequest: class _MockQNetworkRequest:
def __init__(self, url: Any): def __init__(self, url: Any): self.url = url
self.url = url
QNetworkRequest = _MockQNetworkRequest QNetworkRequest = _MockQNetworkRequest
class _MockQNetworkReply: class _MockQNetworkReply:
def error(self) -> int: def error(self) -> int: return 0
return 0 def errorString(self) -> str: return ""
def readAll(self) -> bytes: return b""
def errorString(self) -> str: def deleteLater(self) -> None: pass
return ""
def readAll(self) -> bytes:
return b""
def deleteLater(self) -> None:
pass
QNetworkReply = _MockQNetworkReply QNetworkReply = _MockQNetworkReply
class _MockWidget: class _MockWidget: pass
def __init__(self, *args, **kwargs):
pass
class _MockLayout: class _MockLayout:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._widgets = []
self._widgets = [] def addWidget(self, widget): self._widgets.append(widget)
def addLayout(self, layout): pass
def addWidget(self, widget): def addStretch(self, *args, **kwargs): pass
self._widgets.append(widget) def setSpacing(self, *args, **kwargs): pass
def setContentsMargins(self, *args, **kwargs): pass
def addLayout(self, layout):
pass
def addStretch(self, *args, **kwargs):
pass
def setSpacing(self, *args, **kwargs):
pass
def setContentsMargins(self, *args, **kwargs):
pass
class _MockLabel: class _MockLabel:
def __init__(self, text: str = ""): def __init__(self, text: str = ""): self._text = text
self._text = text
class _MockLineEdit: class _MockLineEdit:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._text = ""
self._text = "" def text(self) -> str: return self._text
def setText(self, value: str) -> None: self._text = value
def text(self) -> str:
return self._text
def setText(self, value: str) -> None:
self._text = value
class _MockButton: class _MockButton:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self.clicked = lambda *a, **k: None
self.clicked = lambda *a, **k: None
QWidget = _MockWidget QWidget = _MockWidget
QGridLayout = _MockLayout QGridLayout = _MockLayout
@@ -367,101 +312,61 @@ except Exception:
QGroupBox = _MockWidget QGroupBox = _MockWidget
QVBoxLayout = _MockLayout QVBoxLayout = _MockLayout
QPushButton = _MockButton QPushButton = _MockButton
QCoreApplication = object()
class _MockQCoreApplication:
pass
QCoreApplication = _MockQCoreApplication
class _MockQt: class _MockQt:
# ToolButtonStyle
ToolButtonTextBesideIcon = 0 ToolButtonTextBesideIcon = 0
# ArrowType
ArrowDown = 1 ArrowDown = 1
ArrowRight = 2 ArrowRight = 2
LeftDockWidgetArea = 1
RightDockWidgetArea = 2
Qt=_MockQt Qt = _MockQt()
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
ArrowDown = Qt.ArrowDown ArrowDown = Qt.ArrowDown
ArrowRight = Qt.ArrowRight ArrowRight = Qt.ArrowRight
DockAreaLeft = Qt.LeftDockWidgetArea
DockAreaRight = Qt.RightDockWidgetArea
class _MockQDockWidget(_MockWidget): class _MockQDockWidget(_MockWidget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._object_name = "" self._object_name = ""
def setObjectName(self, name: str) -> None: self._object_name = name
def objectName(self) -> str: return self._object_name
def show(self) -> None: pass
def deleteLater(self) -> None: pass
def setObjectName(self, name: str) -> None:
self._object_name = name
def objectName(self) -> str:
return self._object_name
def show(self) -> None:
pass
def deleteLater(self) -> None:
pass
QDockWidget = _MockQDockWidget QDockWidget = _MockQDockWidget
class _MockAction: class _MockAction:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._checked = False self._checked = False
self.triggered = lambda *a, **k: None self.triggered = lambda *a, **k: None
def setToolTip(self, text: str) -> None: pass
def setToolTip(self, text: str) -> None: def setCheckable(self, value: bool) -> None: pass
pass def setChecked(self, value: bool) -> None: self._checked = value
def setCheckable(self, value: bool) -> None:
pass
def setChecked(self, value: bool) -> None:
self._checked = value
class _MockMenu: class _MockMenu:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._actions = []
self._actions = [] def addAction(self, action): self._actions.append(action)
def addAction(self, action):
self._actions.append(action)
def removeAction(self, action): def removeAction(self, action):
if action in self._actions: if action in self._actions: self._actions.remove(action)
self._actions.remove(action) def clear(self): self._actions.clear()
def menuAction(self): return self
def clear(self):
self._actions.clear()
def menuAction(self):
return self
class _MockToolBar: class _MockToolBar:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._actions = []
self._actions = [] def setObjectName(self, name: str) -> None: pass
def addAction(self, action): self._actions.append(action)
def setObjectName(self, name: str) -> None:
pass
def addAction(self, action):
self._actions.append(action)
def removeAction(self, action): def removeAction(self, action):
if action in self._actions: if action in self._actions: self._actions.remove(action)
self._actions.remove(action) def clear(self): self._actions.clear()
def clear(self):
self._actions.clear()
class _MockActionGroup: class _MockActionGroup:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._actions = []
self._actions = [] def setExclusive(self, value: bool) -> None: pass
def addAction(self, action): self._actions.append(action)
def setExclusive(self, value: bool) -> None:
pass
def addAction(self, action):
self._actions.append(action)
QAction = _MockAction QAction = _MockAction
QMenu = _MockMenu QMenu = _MockMenu
QToolBar = _MockToolBar QToolBar = _MockToolBar
@@ -469,112 +374,58 @@ except Exception:
class _MockToolButton(_MockWidget): class _MockToolButton(_MockWidget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._checked = False self._checked = False
self.toggled = lambda *a, **k: None self.toggled = lambda *a, **k: None
def setText(self, text: str) -> None: pass
def setCheckable(self, value: bool) -> None: pass
def setChecked(self, value: bool) -> None: self._checked = value
def setToolButtonStyle(self, *args, **kwargs): pass
def setArrowType(self, *args, **kwargs): pass
def setStyleSheet(self, *args, **kwargs): pass
def setText(self, text: str) -> None: QToolButton = _MockToolButton
pass
def setCheckable(self, value: bool) -> None:
pass
def setChecked(self, value: bool) -> None:
self._checked = value
def setToolButtonStyle(self, *args, **kwargs):
pass
def setArrowType(self, *args, **kwargs):
pass
def setStyleSheet(self, *args, **kwargs):
pass
QToolButton=_MockToolButton
class _MockQSizePolicy: class _MockQSizePolicy:
# horizontale Policies
Fixed = 0
Minimum = 1
Maximum = 2
Preferred = 3 Preferred = 3
Expanding = 4 Maximum = 2
MinimumExpanding = 5
Ignored = 6
# vertikale Policies (Qt nutzt dieselben Werte) QSizePolicy = _MockQSizePolicy
def __init__(self, horizontal=None, vertical=None):
self.horizontal = horizontal
self.vertical = vertical
QSizePolicy=_MockQSizePolicy
SizePolicyPreferred = QSizePolicy.Preferred SizePolicyPreferred = QSizePolicy.Preferred
SizePolicyMaximum = QSizePolicy.Maximum SizePolicyMaximum = QSizePolicy.Maximum
DockWidgetMovable = 1 DockWidgetMovable = 1
DockWidgetFloatable = 2 DockWidgetFloatable = 2
DockWidgetClosable = 4 DockWidgetClosable = 4
DockAreaLeft = 1
DockAreaRight = 2
def exec_dialog(dialog: Any) -> Any:
return YES
class _MockTabWidget: class _MockTabWidget:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): self._tabs = []
self._tabs = [] def addTab(self, widget, title: str): self._tabs.append((widget, title))
def addTab(self, widget, title: str):
self._tabs.append((widget, title))
QTabWidget = _MockTabWidget QTabWidget = _MockTabWidget
# -------------------------
# Mock ComboBox Implementation
# -------------------------
class _MockSignal:
def __init__(self):
self._slots = []
def connect(self, cb):
self._slots.append(cb)
def emit(self, value):
for s in list(self._slots):
try:
s(value)
except Exception:
pass
class _MockComboBox: class _MockComboBox:
def __init__(self, parent=None): def __init__(self, parent=None):
self._items = [] self._items = []
self._index = -1 self._index = -1
self.currentTextChanged = _MockSignal() self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
def addItem(self, text: str) -> None: self._items.append(text)
def addItem(self, text: str) -> None: def addItems(self, items): [self.addItem(it) for it in items]
self._items.append(text)
def addItems(self, items):
for it in items:
self.addItem(it)
def findText(self, text: str) -> int: def findText(self, text: str) -> int:
try: return self._items.index(text) if text in self._items else -1
return self._items.index(text)
except ValueError:
return -1
def setCurrentIndex(self, idx: int) -> None: def setCurrentIndex(self, idx: int) -> None:
if 0 <= idx < len(self._items): if 0 <= idx < len(self._items):
self._index = idx self._index = idx
self.currentTextChanged.emit(self.currentText()) self.currentTextChanged.emit(self.currentText())
def setCurrentText(self, text: str) -> None: def setCurrentText(self, text: str) -> None:
idx = self.findText(text) idx = self.findText(text)
if idx >= 0: if idx >= 0: self.setCurrentIndex(idx)
self.setCurrentIndex(idx)
def currentText(self) -> str: def currentText(self) -> str:
if 0 <= self._index < len(self._items): return self._items[self._index] if 0 <= self._index < len(self._items) else ""
return self._items[self._index]
return ""
ComboBox = _MockComboBox QComboBox = _MockComboBox
def exec_dialog(dialog: Any) -> Any:
return YES
# --------------------------- TEST ---------------------------
if __name__ == "__main__":
debug_qt_status()

View File

@@ -1,40 +1,27 @@
# sn_basis/modules/DataGrabber.py
""" """
DataGrabber module DataGrabber module
================== ==================
Leichter Orchestrator, der eine Quelle (Datei, Einzellink, Datenbank) UIfreier Orchestrator für die Prüfung und Klassifikation von Datenquellen.
analysiert, passende Prüfer aufruft und die Ergebnisse an den
:class:`sn_basis.modules.Pruefmanager.Pruefmanager` delegiert.
Dieses vereinfachte Modul geht davon aus, dass alle benötigten Prüfer Der DataGrabber:
und der ExcelImporter vorhanden und importierbar sind. Es enthält - klassifiziert die übergebene Quelle (Datei, Dienst, Datenbank, Excel),
keine Fallbacks oder defensive Exception-Handling-Pfade für fehlende - ruft passende Prüfer (Dateipruefer, Linkpruefer, Layerpruefer, Stilpruefer) auf,
Prüfer-Module — fehlende Komponenten führen zu Import- oder Laufzeitfehlern, - sammelt alle rohen ``pruef_ergebnis``Objekte,
die bewusst nicht unterdrückt werden. - aggregiert diese zu einem zusammenfassenden Ergebnis,
- **löst selbst keinerlei UIInteraktion aus**.
Alle Nutzerinteraktionen (MessageBar, QMessageBox, Logging) erfolgen
ausschließlich über den ``Pruefmanager`` im aufrufenden Kontext (UI / Pipeline).
""" """
from __future__ import annotations from __future__ import annotations
from typing import ( from typing import Any, Dict, List, Mapping, Optional, Tuple, Literal
Optional,
Any,
Mapping,
Iterable,
Dict,
Protocol,
Literal,
Tuple,
List,
)
from pathlib import Path
import sqlite3
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
# In dieser vereinfachten Variante werden die Prüfer und der ExcelImporter
# direkt importiert. Fehlende Module führen zu ImportError (gewollt).
from sn_basis.modules.Dateipruefer import Dateipruefer from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.layerpruefer import Layerpruefer from sn_basis.modules.layerpruefer import Layerpruefer
@@ -42,383 +29,144 @@ from sn_basis.modules.stilpruefer import Stilpruefer
from sn_basis.modules.excel_importer import ExcelImporter from sn_basis.modules.excel_importer import ExcelImporter
SourceType = Literal["file", "link", "database", "unknown"] SourceType = Literal["service", "database", "excel", "unknown"]
SourceDict = Dict[str, List[Mapping[str, Any]]]
class LinklistAdapter(Protocol):
"""
Minimal-Protokoll für Adapter, die Linklisten liefern/normalisieren.
Implementierende Klassen sollten:
- load() -> Iterable[Mapping[str, Any]]
- normalize(raw_item) -> Mapping[str, Any]
"""
def load(self) -> Iterable[Mapping[str, Any]]:
...
def normalize(self, raw_item: Mapping[str, Any]) -> Mapping[str, Any]:
...
class DataGrabber: class DataGrabber:
""" """
DataGrabber orchestriert das Einlesen einer Quelle und die Übergabe an Prüfer. Analysiert und prüft Datenquellen für den Fachdatenabruf.
Diese vereinfachte Implementierung erwartet, dass alle Prüferklassen und Der DataGrabber ist **UIfrei**. Er erzeugt ausschließlich rohe
der ExcelImporter vorhanden sind. Es gibt keine defensive Logik für ``pruef_ergebnis``Objekte und überlässt deren Verarbeitung
fehlende Komponenten. vollständig dem aufrufenden Code.
Konstruktor-Parameter
--------------------
:param pruefmanager: Instanz des Pruefmanagers (verpflichtend).
:param datei_pruefer_cls: Klasse des Dateipruefers (Standard: Dateipruefer).
:param link_pruefer: Instanz des Linkpruefers.
:param layer_pruefer: Instanz des Layerpruefers.
:param stil_pruefer: Instanz des Stilpruefers.
""" """
def __init__( def __init__(
self, self,
pruefmanager: Pruefmanager, pruefmanager: Pruefmanager,
*, *,
datei_pruefer_cls=Dateipruefer, datei_pruefer_cls: type[Dateipruefer] = Dateipruefer,
link_pruefer: Linkpruefer, link_pruefer: Optional[Linkpruefer] = None,
layer_pruefer: Layerpruefer, layer_pruefer: Optional[Layerpruefer] = None,
stil_pruefer: Stilpruefer, stil_pruefer: Optional[Stilpruefer] = None,
excel_importer_cls: type[ExcelImporter] = ExcelImporter,
) -> None: ) -> None:
# Pruefmanager ist verpflichtend self.pruefmanager = pruefmanager
self.pruefmanager: Pruefmanager = pruefmanager
# Dateipruefer-Klasse (wird zur Laufzeit mit einem Pfad instanziert)
self._datei_pruefer_cls = datei_pruefer_cls self._datei_pruefer_cls = datei_pruefer_cls
self.link_pruefer = link_pruefer
self.layer_pruefer = layer_pruefer
self.stil_pruefer = stil_pruefer
self._excel_importer_cls = excel_importer_cls
# Prüfer-Instanzen (werden direkt verwendet) self._source: Optional[str] = None
self.link_pruefer: Linkpruefer = link_pruefer
self.layer_pruefer: Layerpruefer = layer_pruefer
self.stil_pruefer: Stilpruefer = stil_pruefer
# Quelle (wird später gesetzt) # ------------------------------------------------------------------
self.source: Optional[str] = None # Öffentliche API
# ------------------------------------------------------------------
# ------------------------------------------------------------------ #
# Source Management
# ------------------------------------------------------------------ #
def set_source(self, source: str) -> None: def set_source(self, source: str) -> None:
"""Setzt die aktuell zu untersuchende Rohquelle."""
self._source = source
def analyze_source_type(self, source: str) -> SourceType:
""" """
Setzt die Quelle für den DataGrabber. Klassifiziert die Quelle.
Die Quelle ist ein String, der entweder ein lokaler Dateipfad, Aktuell Platzhalter liefert ``"unknown"``.
ein Einzellink (URL/URI) oder ein Pfad zu einer Datenbank/GeoPackage ist.
""" """
self.source = source
def analyze_source(self, source: str) -> SourceType:
"""
Klassifiziert die angegebene Quelle ausschließlich anhand des Dateipruefers.
Ablauf
------
1. Instanziere den Dateipruefer mit `pfad=source` und `temporaer_erlaubt=False`.
2. Rufe `pruefe()` auf und werte das zurückgegebene :class:`pruef_ergebnis` aus.
3. Bei `ok==True` wird anhand der Dateiendung zwischen "database" (gpkg/sqlite/db)
und "file" unterschieden.
4. Bei `ok==False` werden typische Aktionen wie "datei_nicht_gefunden" als "link"
interpretiert; bei "falsche_endung" wird anhand der Endung klassifiziert.
"""
dp = self._datei_pruefer_cls(pfad=source, temporaer_erlaubt=False)
pe: pruef_ergebnis = dp.pruefe()
if getattr(pe, "ok", False):
suffix = Path(source).suffix.lower()
if suffix in (".gpkg", ".sqlite", ".db"):
return "database"
return "file"
aktion = getattr(pe, "aktion", None)
if aktion in ("datei_nicht_gefunden", "pfad_nicht_gefunden", "kein_dateipfad"):
return "link"
if aktion == "falsche_endung":
lower = source.lower()
for db_ext in (".gpkg", ".sqlite", ".db"):
if lower.endswith(db_ext):
return "database"
for file_ext in (".xlsx", ".xls", ".csv"):
if lower.endswith(file_ext):
return "file"
return "unknown" return "unknown"
# ------------------------------------------------------------------ # def run(self, source: str) -> Tuple[SourceDict, pruef_ergebnis]:
# Excel-Verarbeitung
#Es werden alle Werte mit gültigem Link übernommen. Die restliche Struktur
#wird nicht überprüft, da alle Fachplugins unterschiedliche Strukturen haben können
# ------------------------------------------------------------------ #
def process_excel_source(
self,
filepath: str
) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], Any]:
""" """
Liest eine Excel-Datei ein und übernimmt ausschließlich die Zeilen, Führt die vollständige Quellprüfung aus.
deren Link durch den Linkpruefer als gültig eingestuft wurde.
Ablauf Diese Methode ist **UIfrei**. Sie gibt rohe Ergebnisse zurück,
------ die vom Aufrufer über den ``Pruefmanager`` verarbeitet werden.
1. Die Excel-Datei wird mit dem ``ExcelImporter`` eingelesen.
Erwartet wird eine Liste von Mappings (z.B. dicts), die jeweils
die Linkparameter enthalten.
2. Für jede Zeile wird der Wert ``row["Link"]`` extrahiert und durch
``self.link_pruefer.pruefe(...)`` geprüft.
3. Das Prüfergebnis wird durch ``self.pruefmanager.verarbeite(...)``
geleitet, der UI-Interaktion, Logging und finale Entscheidung übernimmt.
4. Nur Zeilen, deren verarbeitete Prüfergebnisse ``ok == True`` liefern,
werden in die Ergebnisliste übernommen.
5. Wenn mindestens eine Zeile gültig ist, wird ein Dict der Form::
{"rows": [row1, row2, ...]}
zurückgegeben.
Wenn keine Zeile gültig ist, wird ``None`` zurückgegeben.
Parameter
---------
filepath:
Pfad zur Excel-Datei, die eingelesen werden soll.
Returns
-------
Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]
- ``data``: ``{"rows": [...]} `` wenn gültige Zeilen existieren,
sonst ``None``.
- ``pruef_ergebnis``: ein zusammenfassendes Prüfergebnis, das
den Lesevorgang beschreibt (nicht die Einzelprüfungen).
Hinweise
--------
- Diese Methode führt **keine Normalisierung** durch.
- Die Verantwortung für die Struktur der Excel-Zeilen liegt beim Fachplugin.
- Der Linkpruefer prüft ausschließlich den Wert ``row["Link"]``.
""" """
self.set_source(source)
source_type = self.analyze_source_type(source)
# 1) Excel einlesen source_dict: SourceDict = {}
importer = ExcelImporter(filepath=filepath, pruefmanager=self.pruefmanager) partial_results: List[pruef_ergebnis] = []
rows = importer.import_xlsx() # erwartet: List[Mapping[str, Any]]
valid_rows: List[Mapping[str, Any]] = [] if source_type == "excel":
source_dict, partial_results = self._process_excel_source(source)
# 2) Jede Zeile einzeln prüfen elif source_type == "database":
for row in rows: source_dict, partial_results = self._process_database_source(source)
raw_link = row.get("Link") elif source_type == "service":
source_dict, partial_results = self._process_service_source(source)
# 2a) Fachliche Prüfung else:
pe = self.link_pruefer.pruefe(raw_link) partial_results.append(
pruef_ergebnis(
# 2b) Verarbeitung durch den Pruefmanager
processed = self.pruefmanager.verarbeite(pe)
# 2c) Nur gültige Zeilen übernehmen
if getattr(processed, "ok", False):
valid_rows.append(row)
# 3) Zusammenfassendes Prüfergebnis erzeugen
if valid_rows:
pe_ok = pruef_ergebnis(
ok=True,
meldung=f"{len(valid_rows)} gültige Zeilen aus Excel gelesen",
aktion="ok",
kontext=filepath,
)
processed_summary = self.pruefmanager.verarbeite(pe_ok)
return {"rows": valid_rows}, processed_summary
# Keine gültigen Zeilen
pe_fail = pruef_ergebnis(
ok=False, ok=False,
meldung="Keine gültigen Links in der Excel-Datei gefunden", meldung="Quelle konnte nicht klassifiziert werden",
aktion="read_error", aktion="kein_dateipfad",
kontext=filepath, kontext={"source": source},
)
) )
processed_summary = self.pruefmanager.verarbeite(pe_fail)
return None, processed_summary
summary = self._aggregate_results(source, source_dict, partial_results)
return source_dict, summary
# ------------------------------------------------------------------ # # ------------------------------------------------------------------
# Einzellink-Verarbeitung # ExcelQuellen
# ------------------------------------------------------------------ # # ------------------------------------------------------------------
def process_single_link( def _process_excel_source(
self, filepath: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
source_dict: SourceDict = {}
results: List[pruef_ergebnis] = []
return source_dict, results
# ------------------------------------------------------------------
# DatenbankQuellen
# ------------------------------------------------------------------
def _process_database_source(
self, db_path: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
source_dict: SourceDict = {}
results: List[pruef_ergebnis] = []
return source_dict, results
# ------------------------------------------------------------------
# DienstQuellen
# ------------------------------------------------------------------
def _process_service_source(
self, link: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
source_dict: SourceDict = {}
results: List[pruef_ergebnis] = []
return source_dict, results
# ------------------------------------------------------------------
# Aggregation
# ------------------------------------------------------------------
def _aggregate_results(
self, self,
link: Mapping[str, Any] source: str,
) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], Any]: source_dict: SourceDict,
partial_results: List[pruef_ergebnis],
) -> pruef_ergebnis:
""" """
Prüft einen einzelnen Link anhand der im Link-Dict enthaltenen Link-URL. Aggregiert Einzelprüfungen zu einem Gesamt``pruef_ergebnis``.
Ablauf **Keine UIInteraktion.**
------
1. Erwartet wird ein Mapping (z.B. dict), das die Linkparameter enthält.
Mindestens der Schlüssel ``"Link"`` muss vorhanden sein.
2. Der eigentliche Link (z.B. URL) wird aus ``link["Link"]`` extrahiert
und an ``self.link_pruefer.pruefe(...)`` übergeben.
3. Das Prüfergebnis wird anschließend durch ``self.pruefmanager.verarbeite(...)``
geleitet, der UIInteraktion, Logging und finale Entscheidung übernimmt.
4. Wenn das verarbeitete Prüfergebnis **nicht OK** ist, wird
``(None, pruef_ergebnis)`` zurückgegeben.
5. Wenn das Prüfergebnis **OK** ist, wird das unveränderte LinkDict
in der Struktur ``{"rows": [link]}`` zurückgegeben.
Parameter
---------
link:
Ein Mapping mit den Linkparametern (z.B. id, Thema, Gruppe, Link,
Anbieter, Stildatei). Diese Methode verändert das Mapping nicht.
Returns
-------
Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]
- ``data``: ``{"rows": [link]}`` wenn gültig, sonst ``None``
- ``pruef_ergebnis``: das vom Pruefmanager verarbeitete Ergebnis
Hinweise
--------
- Diese Methode führt **keine Normalisierung** durch.
- Die Verantwortung für die Struktur des Link-Dicts liegt beim Fachplugin.
- Der Linkpruefer prüft ausschließlich den Wert ``link["Link"]``.
""" """
if source_dict:
return pruef_ergebnis(
ok=True,
meldung="Quelle erfolgreich geprüft",
aktion="ok",
kontext={
"source": source,
"valid_entries": sum(len(v) for v in source_dict.values()),
},
)
# 1) Link extrahieren (Fachplugin garantiert, dass "Link" existiert) return pruef_ergebnis(
raw_link = link.get("Link") ok=False,
meldung="Keine gültigen Einträge in der Quelle gefunden",
# 2) Fachliche Prüfung durch den Linkpruefer aktion="read_error",
pruef_ergebnis = self.link_pruefer.pruefe(raw_link) kontext={"source": source},
)
# 3) Verarbeitung durch den Pruefmanager
processed = self.pruefmanager.verarbeite(pruef_ergebnis)
# 4) Wenn Prüfung nicht OK → keine Daten zurückgeben
if not getattr(processed, "ok", False):
return None, processed
# 5) Prüfung OK → unverändertes Link-Dict zurückgeben
data = {"rows": [link]}
return data, processed
# ------------------------------------------------------------------ #
# Datenbank-Verarbeitung
# ------------------------------------------------------------------ #
#def process_database_table(self, db_path: str, table: Optional[str] = None) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]:
#noch nicht implementiert
"""
Liest eine Tabelle aus einer SQLite/GeoPackage-Datei.
Verhalten
---------
1. Validiert die Datei mit dem Dateipruefer.
2. Falls OK, versucht es, die angegebene Tabelle zu lesen; falls keine Tabelle
angegeben ist, wird nach einer typischen Metadaten-Tabelle 'layer_metadaten'
gesucht und diese gelesen.
3. Gibt die Zeilen als Liste von Dicts zurück.
"""
dp = self._datei_pruefer_cls(pfad=db_path, temporaer_erlaubt=False)
pe = dp.pruefe()
processed = self.pruefmanager.verarbeite(pe)
if not getattr(processed, "ok", False):
return None, processed
conn = sqlite3.connect(db_path)
cur = conn.cursor()
if table:
cur.execute(f"SELECT * FROM {table}")
cols = [d[0] for d in cur.description]
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
else:
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='layer_metadaten'")
if cur.fetchone():
cur.execute("SELECT * FROM layer_metadaten")
cols = [d[0] for d in cur.description]
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
else:
rows = []
conn.close()
pe_ok = pruef_ergebnis(ok=True, meldung="DB gelesen", aktion="ok", kontext=db_path)
processed_ok = self.pruefmanager.verarbeite(pe_ok)
return {"rows": rows}, processed_ok
# ------------------------------------------------------------------ #
# Hauptlauf / Dispatch
# ------------------------------------------------------------------ #
def run(self) -> Dict[str, Any]:
"""
Hauptmethode des DataGrabbers.
Ablauf
------
1. Prüft, ob eine Quelle gesetzt ist.
2. Klassifiziert die Quelle via :meth:`analyze_source`.
3. Dispatch:
- file (.xlsx/.xls) -> :meth:`process_excel_source`
- link -> :meth:`process_single_link`
- database -> :meth:`process_database_table`
- unknown -> Fehler
4. Aggregiert geladene Einträge in einem Ergebnis-Dict und gibt dieses zurück.
Rückgabeformat
-------------
Ein Dict mit den Schlüsseln:
- 'geladen' : Liste der geladenen Themen/Namen
- 'fehler' : Mapping Thema -> Fehlermeldung
- 'ausserhalb': Liste der als ausserhalb klassifizierten Themen
- 'relevant' : Liste der relevanten Themen
- 'details' : zusätzliche Detailinformationen (z. B. Anzahl Zeilen)
"""
result: Dict[str, Any] = {"geladen": [], "fehler": {}, "ausserhalb": [], "relevant": [], "details": {}}
if not self.source:
pe = pruef_ergebnis(ok=False, meldung="Keine Quelle gesetzt", aktion="kein_dateipfad", kontext=None)
processed = self.pruefmanager.verarbeite(pe)
result["fehler"]["source"] = getattr(processed, "meldung", "Keine Quelle")
return result
src_type = self.analyze_source(self.source)
if src_type == "file":
suffix = Path(self.source).suffix.lower()
if suffix in (".xlsx", ".xls"):
data_dict, pe = self.process_excel_source(self.source)
else:
pe = pruef_ergebnis(ok=False, meldung="Dateityp nicht unterstützt", aktion="falsche_endung", kontext=self.source)
pe = self.pruefmanager.verarbeite(pe)
data_dict = None
elif src_type == "link":
data_dict, pe = self.process_single_link(self.source)
#elif src_type == "database":
#data_dict, pe = self.process_database_table(self.source, table=None)
else:
pe = pruef_ergebnis(ok=False, meldung="Quelle unbekannt", aktion="kein_dateipfad", kontext=self.source)
pe = self.pruefmanager.verarbeite(pe)
data_dict = None
# Falls Daten vorhanden: fülle result['geladen'] und details
if data_dict and "rows" in data_dict:
rows = data_dict["rows"]
for r in rows:
thema = r.get("Inhalt") or r.get("ident") or r.get("Link") or "unbenannt"
result["geladen"].append(thema)
result["details"]["source_rows"] = len(rows)
# Falls das letzte pruef_ergebnis einen Fehler enthält, übernehme es
if not getattr(pe, "ok", False):
result["fehler"]["source"] = getattr(pe, "meldung", "Fehler bei Quelle")
return result

View File

@@ -1,98 +1,175 @@
""" """
sn_basis/modules/Dateipruefer.py Prüfung von Dateieingaben für das Plugin. sn_basis/modules/Dateipruefer.py
Verwendet sys_wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
Erweiterter Dateiprüfer für Verfahrens-DB-Workflows mit vollständiger Unterstützung
der Anforderungen 1-2.e (leerer Pfad, fehlende Datei, bestehende Datei).
""" """
from pathlib import Path from pathlib import Path
from typing import Optional
from sn_basis.functions.sys_wrapper import ( from sn_basis.functions.sys_wrapper import join_path, file_exists
join_path, from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
file_exists,
)
from sn_basis.modules.Pruefmanager import pruef_ergebnis
class Dateipruefer: class Dateipruefer:
""" """
Prüft Dateieingaben und liefert ein pruef_ergebnis zurück. Prüft Dateieingaben für Verfahrens-DB-Workflows und liefert :class:`pruef_ergebnis`.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
**Funktionsweise (deine Anforderungen 1-2.e):**
+---------------------+------------------------------------------+---------------+
| **Fall** | **Ergebnis** | **ok** |
+=====================+==========================================+===============+
| 1. Leerer Pfad | ``temporaer_erlaubt`` | False |
+---------------------+------------------------------------------+---------------+
| 2.a Leerer Pfad | Pruefmanager fragt → ``temporaer_erzeugen`` | True |
+---------------------+------------------------------------------+---------------+
| 2.b Datei existiert | ``ok`` | True |
+---------------------+------------------------------------------+---------------+
| 2.c Ungültiger Pfad | ``datei_nicht_gefunden`` | False |
+---------------------+------------------------------------------+---------------+
| **2.d Datei fehlt** | **``datei_wird_erzeugt``** | **True** |
+---------------------+------------------------------------------+---------------+
| **2.e Datei da** | **``datei_existiert``** | **False** |
+---------------------+------------------------------------------+---------------+
Der Dateiprüfer führt **keine UI-Interaktion** durch.
Entscheidungen werden ausschließlich vom :class:`Pruefmanager` getroffen.
""" """
def __init__( def __init__(
self, self,
pfad: str, pfad: Optional[str],
basis_pfad: str = "", basis_pfad: str = "",
leereingabe_erlaubt: bool = False, leereingabe_erlaubt: bool = False,
standarddatei: str | None = None, standarddatei: Optional[str] = None,
temporaer_erlaubt: bool = False, temporaer_erlaubt: bool = False,
): *,
verfahrens_db_modus: bool = True, # 🆕 Verfahrens-DB-spezifische Logik
) -> None:
"""
Parameters
----------
pfad : Optional[str]
Vom UI gelieferter Dateipfad (kann leer oder Whitespace sein).
basis_pfad : str, optional
Basisverzeichnis für relative Pfade (default: "").
leereingabe_erlaubt : bool, optional
Ob leere Eingabe grundsätzlich erlaubt ist (default: False).
standarddatei : Optional[str], optional
Optionaler Standardpfad (default: None).
temporaer_erlaubt : bool, optional
Ob bei leerer Eingabe temporäre Layer erlaubt sind (default: False).
verfahrens_db_modus : bool, optional
Aktiviert Verfahrens-DB-spezifische Logik (2.d, 2.e) (default: True).
"""
self.pfad = pfad self.pfad = pfad
self.basis_pfad = basis_pfad self.basis_pfad = basis_pfad
self.leereingabe_erlaubt = leereingabe_erlaubt self.leereingabe_erlaubt = leereingabe_erlaubt
self.standarddatei = standarddatei self.standarddatei = standarddatei
self.temporaer_erlaubt = temporaer_erlaubt self.temporaer_erlaubt = temporaer_erlaubt
self.verfahrens_db_modus = verfahrens_db_modus
# --------------------------------------------------------- # ------------------------------------------------------------------
# Hilfsfunktion # Hilfsfunktionen
# --------------------------------------------------------- # ------------------------------------------------------------------
def _pfad(self, relativer_pfad: str) -> Path: def _pfad(self, relativer_pfad: str) -> Path:
""" """Erzeugt OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis.
"""
return join_path(self.basis_pfad, relativer_pfad) return join_path(self.basis_pfad, relativer_pfad)
# --------------------------------------------------------- def _ist_leer(self) -> bool:
# Hauptfunktion """
# --------------------------------------------------------- Prüft robust, ob Eingabe als „leer" zu behandeln ist.
Returns
-------
bool
True bei None, leerem String oder reinem Whitespace.
"""
if self.pfad is None:
return True
if not isinstance(self.pfad, str):
return True
return not self.pfad.strip()
def _ist_gueltiger_gpkg_pfad(self, pfad: Path) -> bool:
"""
Prüft, ob Pfad für GPKG geeignet ist (Endung + Schreibrechte).
Returns
-------
bool
True wenn `.gpkg`-Endung und Verzeichnis beschreibbar.
"""
if not str(pfad).lower().endswith('.gpkg'):
return False
# Verzeichnis muss beschreibbar sein
return pfad.parent.exists() and pfad.parent.is_dir()
# ------------------------------------------------------------------
# Hauptlogik: deine Anforderungen 1-2.e
# ------------------------------------------------------------------
def pruefe(self) -> pruef_ergebnis: def pruefe(self) -> pruef_ergebnis:
""" """
Prüft eine Dateieingabe und liefert ein pruef_ergebnis zurück. 🆕 Prüft Dateieingabe gemäß Anforderungen 1-2.e.
Der Pruefmanager entscheidet später, wie der Nutzer gefragt wird.
"""
# ----------------------------------------------------- **Workflow:**
# 1. Fall: Eingabe ist leer 1. **Leere Eingabe** → ``temporaer_erlaubt`` (Pruefmanager fragt)
# ----------------------------------------------------- 2. **Pfad prüfen**:
if not self.pfad: - **Ungültig** → 2.c ``datei_nicht_gefunden``
- **Gültig, fehlt** → **2.d** ``datei_wird_erzeugt`` (ok=True!)
- **Gültig, existiert** → **2.e** ``datei_existiert`` (Pruefmanager fragt)
3. **Datei OK** → 2.b ``ok``
Returns
-------
pruef_ergebnis
Mit korrekter Aktion für jeden Fall.
"""
# 1. 🎯 ANFORDERUNG 1: Leere Eingabe
if self._ist_leer():
return self._handle_leere_eingabe() return self._handle_leere_eingabe()
# ----------------------------------------------------- # 2. Pfad normalisieren
# 2. Fall: Eingabe ist nicht leer → Datei prüfen pfad = self._pfad(self.pfad.strip())
# -----------------------------------------------------
pfad = self._pfad(self.pfad)
if not file_exists(pfad): # 🆕 2.c: Ungültiger GPKG-Pfad?
if not self.verfahrens_db_modus or not self._ist_gueltiger_gpkg_pfad(pfad):
return pruef_ergebnis( return pruef_ergebnis(
ok=False, ok=False,
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.", meldung=f"Der Pfad '{self.pfad}' ist kein gültiger GPKG-Pfad.",
aktion="datei_nicht_gefunden", aktion="datei_nicht_gefunden",
kontext=pfad, kontext=pfad,
) )
# ----------------------------------------------------- # 🆕 2.d: Gültiger Pfad, Datei fehlt → DIREKT WEITER (ok=True!)
# 3. Datei existiert → Erfolg if not file_exists(pfad):
# -----------------------------------------------------
return pruef_ergebnis( return pruef_ergebnis(
ok=True, ok=True, # 🎯 WICHTIG: Pipeline fortsetzen!
meldung="Datei gefunden.", meldung=f"Datei '{self.pfad}' wird erzeugt.",
aktion="ok", aktion="datei_wird_erzeugt",
kontext=pfad, kontext=pfad,
) )
# --------------------------------------------------------- # 🆕 2.e: Datei existiert → Pruefmanager fragt Überschreiben/etc.
# Behandlung leerer Eingaben return pruef_ergebnis(
# --------------------------------------------------------- ok=False, # 🎯 Pruefmanager soll 4-Optionen-Dialog zeigen
meldung=f"Datei '{self.pfad}' existiert bereits.",
aktion="datei_existiert",
kontext=pfad,
)
# 2.b: Wird nicht erreicht (durch 2.e abgefangen)
# ------------------------------------------------------------------
# Leere Eingabe (ANFORDERUNG 1, 2.a)
# ------------------------------------------------------------------
def _handle_leere_eingabe(self) -> pruef_ergebnis: def _handle_leere_eingabe(self) -> pruef_ergebnis:
""" """
Liefert ein pruef_ergebnis für den Fall, dass das Dateifeld leer ist. Behandelt leere Eingaben (Priorität: leereingabe → Standard → temporär → Fehler).
Der Pruefmanager fragt später den Nutzer.
""" """
# 1. Leereingabe erlaubt → Nutzer fragen, ob das beabsichtigt war
if self.leereingabe_erlaubt: if self.leereingabe_erlaubt:
return pruef_ergebnis( return pruef_ergebnis(
ok=False, ok=False,
@@ -101,19 +178,17 @@ class Dateipruefer:
kontext=None, kontext=None,
) )
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
if self.standarddatei: if self.standarddatei:
return pruef_ergebnis( return pruef_ergebnis(
ok=False, ok=False,
meldung=( meldung=(
f"Es wurde keine Datei angegeben. " "Es wurde keine Datei angegeben. "
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?" f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
), ),
aktion="standarddatei_vorschlagen", aktion="standarddatei_vorschlagen",
kontext=self._pfad(self.standarddatei), kontext=self._pfad(self.standarddatei),
) )
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
if self.temporaer_erlaubt: if self.temporaer_erlaubt:
return pruef_ergebnis( return pruef_ergebnis(
ok=False, ok=False,
@@ -125,7 +200,6 @@ class Dateipruefer:
kontext=None, kontext=None,
) )
# 4. Leereingabe nicht erlaubt → Fehler
return pruef_ergebnis( return pruef_ergebnis(
ok=False, ok=False,
meldung="Es wurde keine Datei angegeben.", meldung="Es wurde keine Datei angegeben.",

View File

@@ -1,66 +1,45 @@
"""
sn_basis/modules/Pruefmanager.py
"""
from __future__ import annotations from __future__ import annotations
from typing import Optional, Any from typing import Optional, Any
from sn_basis.functions import ( from sn_basis.functions import ask_yes_no, info, warning, error
ask_yes_no,
info,
warning,
error,
set_layer_visible,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
print("DEBUG: Pruefmanager DATEI GELADEN:", __file__)
class Pruefmanager: class Pruefmanager:
""" def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None) -> None:
Zentrale Verarbeitung von pruef_ergebnis-Objekten.
Erwartete öffentliche API (verwendet von Core-Komponenten wie DataGrabber):
- report_error(thema, meldung, *, aktion: Optional[PruefAktion]=None, kontext=None) -> None
- request_decision(pruef_res) -> str
- report_summary(summary: dict) -> None
- verarbeite(ergebnis: pruef_ergebnis) -> pruef_ergebnis
"""
def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None):
self.ui_modus = ui_modus self.ui_modus = ui_modus
self.parent = parent self.parent = parent
# --------------------------------------------------------------------- # ------------------------------------------------------------------
# Basis-API: Meldungen / Zusammenfassungen # Meldungen / Zusammenfassungen
# --------------------------------------------------------------------- # ------------------------------------------------------------------
def report_error(self, thema: str, meldung: str, *, aktion: Optional[PruefAktion] = None, kontext: Optional[Any] = None) -> None: def report_error(
""" self,
Einheitliche Meldung für Fehler/Warnungen aus dem Core. thema: str,
Keine Rückgabe; dient als zentraler Hook für Logging/UI. meldung: str,
""" *,
aktion: Optional[PruefAktion] = None,
kontext: Optional[Any] = None,
) -> None:
critical_actions = { critical_actions = {
"netzwerkfehler", "netzwerkfehler", "pruefe_exception", "save_exception",
"pruefe_exception", "layer_create_failed", "read_error", "open_error",
"save_exception",
"layer_create_failed",
"read_error",
"open_error",
} }
warn_actions = { warn_actions = {
"datei_nicht_gefunden", "datei_nicht_gefunden", "pfad_nicht_gefunden", "url_nicht_erreichbar",
"pfad_nicht_gefunden", "falsche_endung", "kein_header", "kein_arbeitsblatt",
"url_nicht_erreichbar",
"falsche_endung",
"kein_header",
"kein_arbeitsblatt",
} }
if aktion in critical_actions: if aktion in critical_actions:
error(thema, meldung) error(thema, meldung)
return return
if aktion in warn_actions: if aktion in warn_actions:
warning(thema, meldung) warning(thema, meldung)
return return
# Default: informative Warnung
warning(thema, meldung) warning(thema, meldung)
def report_summary(self, summary: dict) -> None: def report_summary(self, summary: dict) -> None:
@@ -75,202 +54,159 @@ class Pruefmanager:
f"Dienste ausserhalb: {len(ausserhalb)}\n" f"Dienste ausserhalb: {len(ausserhalb)}\n"
f"Fehler: {len(fehler)}" f"Fehler: {len(fehler)}"
) )
info("DataGrabber Zusammenfassung", message) info("DataGrabber Zusammenfassung", message)
# --------------------------------------------------------------------- # ------------------------------------------------------------------
# Entscheidungs-API # VERFAHRENS-DB-spezifische Entscheidungen
# --------------------------------------------------------------------- # ------------------------------------------------------------------
def request_decision(self, pruef_res: Any) -> str: def _handle_datei_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
""" if self.ui_modus != "qgis":
Synchronously request a decision from the user (or return a default in headless mode). return ergebnis
Returns one of: pfad = ergebnis.kontext
- "abort" pfad_str = str(pfad) if pfad else "unbekannt"
- "continue"
- "temporaer_erzeugen"
- "ignore"
"""
aktion = getattr(pruef_res, "aktion", None)
meldung = getattr(pruef_res, "meldung", str(pruef_res))
titel = "Verfahrens-DB existiert bereits"
meldung = (
f"Die Datei '{pfad_str}' existiert bereits.\n\n"
"Was soll geschehen?\n\n"
"• **Überschreiben**: Bestehende Layer ersetzen\n"
"• **Anhängen**: Neue Layer hinzufügen\n"
"• **Überspringen**: Nur temporäre Layer erzeugen"
)
# Vereinfacht: Erst Überschreiben? → Dann Anhängen? → Überspringen
if ask_yes_no(
titel,
f"{meldung}\n\n**Überschreiben** (alle Layer ersetzen)?",
default=False,
parent=self.parent
):
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_ueberschreiben",
kontext=ergebnis.kontext,
)
if ask_yes_no(
titel,
f"{meldung}\n\n**Anhängen** (neue Layer hinzufügen)?",
default=False,
parent=self.parent
):
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_anhaengen",
kontext=ergebnis.kontext,
)
if ask_yes_no(
titel,
f"{meldung}\n\n**Überspringen** (nur temporäre Layer)?",
default=True,
parent=self.parent
):
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_ueberspringen",
kontext=ergebnis.kontext,
)
return ergebnis
# ------------------------------------------------------------------
# Basis-Entscheidungen (KORREKT: → pruef_ergebnis)
# ------------------------------------------------------------------
def _handle_basic_decision(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
"""Basis-Entscheidung für einfache Ja/Nein-Fragen."""
print(f"DEBUG _handle_basic_decision: aktion='{ergebnis.aktion}', ui_modus='{self.ui_modus}'")
if self.ui_modus != "qgis":
print("DEBUG: Nicht QGIS → ergebnis unverändert")
return ergebnis
title_map = {
"leereingabe_erlaubt": "Ohne Eingabe fortfahren",
"standarddatei_vorschlagen": "Standarddatei verwenden",
"temporaer_erlaubt": "Temporäre Layer erzeugen",
"layer_unsichtbar": "Layer einblenden",
}
title = title_map.get(ergebnis.aktion, "Entscheidung erforderlich")
meldung = ergebnis.meldung or ""
try:
print(f"DEBUG ask_yes_no: title='{title}', meldung='{meldung[:50]}...'")
yes = ask_yes_no(title, meldung, default=False, parent=self.parent)
print(f"DEBUG ask_yes_no: yes={yes}")
except Exception as e:
print(f"DEBUG ask_yes_no Exception: {e}")
return ergebnis
if not yes:
print("DEBUG: Nutzer sagte Nein → ok=False")
return ergebnis
# Nutzer sagte Ja
if ergebnis.aktion == "temporaer_erlaubt":
print("DEBUG: temporaer_erlaubt bestätigt → ok=True")
return pruef_ergebnis(
ok=True,
aktion="temporaer_erlaubt",
kontext=ergebnis.kontext
)
print("DEBUG: Andere Aktion bestätigt → ok=True, aktion='ok'")
return pruef_ergebnis(
ok=True,
aktion="ok",
kontext=ergebnis.kontext
)
# ------------------------------------------------------------------
# Hauptlogik: verarbeite() (KORRIGIERT!)
# ------------------------------------------------------------------
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
print("🔥 verarbeite() START")
print("DEBUG Pruefmanager:", ergebnis.ok, ergebnis.aktion)
print("DEBUG ergebnis.aktion TYPE:", type(ergebnis.aktion), repr(ergebnis.aktion))
# 1. Erfolg → direkt weiter
print("🔍 Schritt 1: Prüfe ergebnis.ok =", ergebnis.ok)
if ergebnis.ok:
print("✅ Schritt 1: ok=True → return")
return ergebnis
# 2. VERFAHRENS-DB: Bestehende Datei
print("🔍 Schritt 2: Prüfe datei_existiert =", ergebnis.aktion == "datei_existiert")
if ergebnis.aktion == "datei_existiert":
print("✅ Schritt 2: _handle_datei_existiert")
return self._handle_datei_existiert(ergebnis)
# 3. Basis interaktive Aktionen
print("🔍 Schritt 3: Definiere interactive_actions")
interactive_actions = { interactive_actions = {
"leereingabe_erlaubt", "leereingabe_erlaubt",
"standarddatei_vorschlagen", "standarddatei_vorschlagen",
"temporaer_erlaubt", "temporaer_erlaubt",
"layer_unsichtbar", "layer_unsichtbar",
} }
print("DEBUG interactive_actions:", repr(interactive_actions))
print("DEBUG ergebnis.aktion in interactive_actions?", ergebnis.aktion in interactive_actions)
if aktion in interactive_actions: if ergebnis.aktion in interactive_actions:
if self.ui_modus == "qgis": print("✅ Schritt 3: Interaktive Aktion → _handle_basic_decision")
title_map = { decision = self._handle_basic_decision(ergebnis)
"leereingabe_erlaubt": "Ohne Eingabe fortfahren", print(f"DEBUG: _handle_basic_decision Ergebnis: ok={decision.ok}, aktion='{decision.aktion}'")
"standarddatei_vorschlagen": "Standarddatei verwenden", return decision
"temporaer_erlaubt": "Temporäre Datei erzeugen",
"layer_unsichtbar": "Layer einblenden",
}
title = title_map.get(aktion, "Entscheidung erforderlich")
try:
yes = ask_yes_no(title, meldung, default=False, parent=self.parent)
except Exception:
return "abort"
if yes:
if aktion == "temporaer_erlaubt":
return "temporaer_erzeugen"
return "continue"
return "abort"
if self.ui_modus == "headless": # 4. Fehler behandeln
return "abort" print("❌ Schritt 4: FEHLER BEHANDELN")
self.report_error(
informational_actions = { thema=ergebnis.aktion or "pruefung",
"leer", meldung=ergebnis.meldung or "",
"datei_nicht_gefunden", aktion=ergebnis.aktion,
"pfad_nicht_gefunden", kontext=ergebnis.kontext,
"url_nicht_erreichbar", )
"netzwerkfehler", print("🔥 verarbeite() ENDE mit ok=False")
"falscher_geotyp",
"layer_leer",
"falscher_layertyp",
"falsches_crs",
"felder_fehlen",
"datenquelle_unerwartet",
"layer_nicht_editierbar",
"kein_header",
"kein_arbeitsblatt",
"read_error",
"open_error",
"pflichtfelder_fehlen",
}
if aktion in informational_actions:
return "abort"
return "abort"
# ---------------------------------------------------------------------
# Höhere Abstraktion: verarbeite
# ---------------------------------------------------------------------
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
"""
Verarbeitet ein pruef_ergebnis-Objekt und führt ggf. Nutzerinteraktion durch.
Liefert ein ggf. modifiziertes pruef_ergebnis zurück.
"""
if ergebnis.ok:
return ergebnis return ergebnis
aktion = ergebnis.aktion
kontext = ergebnis.kontext
meldung = ergebnis.meldung
# Zentrale Meldung
self.report_error(aktion or "pruefung", meldung or "", aktion=aktion, kontext=kontext)
# Interaktive Entscheidungen
if aktion in ("leereingabe_erlaubt", "standarddatei_vorschlagen", "temporaer_erlaubt", "layer_unsichtbar"):
decision = self.request_decision(ergebnis)
if decision == "temporaer_erzeugen":
return pruef_ergebnis(ok=True, meldung="Temporäre Datei soll erzeugt werden.", aktion="temporaer_erzeugen", kontext=None)
if decision == "continue":
return pruef_ergebnis(ok=True, meldung="Fortgefahren.", aktion="ok", kontext=kontext)
return ergebnis # abort / unverändert
# Spezielle Excel/Importer-Fälle: klare Meldungen, keine interaktive Entscheidung
if aktion == "kein_header":
warning("Excel-Import", meldung or "")
return ergebnis
if aktion == "kein_arbeitsblatt":
warning("Excel-Import", meldung or "")
return ergebnis
if aktion in ("read_error", "open_error"):
error("Excel-Import", meldung or "")
return ergebnis
if aktion == "datei_nicht_gefunden":
warning("Datei nicht gefunden", meldung or "")
return ergebnis
# Spezieller Fall: layer_unsichtbar (falls nicht interaktiv behandelt)
if aktion == "layer_unsichtbar":
if kontext is not None:
try:
set_layer_visible(kontext, True)
return pruef_ergebnis(ok=True, meldung="Layer wurde eingeblendet.", aktion="ok", kontext=kontext)
except Exception:
return ergebnis
return ergebnis
# Standard: keine Änderung
return ergebnis
def ask_overwrite_append_cancel(self, layer_name: str, default: str = "overwrite") -> str:
"""
Zeigt dem Nutzer eine Auswahl für einen bereits existierenden Layer an.
Rückgabe
-------
str
Einer der Werte: "overwrite", "append", "cancel".
Verhalten
--------
- Verwendet bevorzugt die UI-Wrapper-Funktion `qt_wrapper` / `qgisui_wrapper`,
falls vorhanden (z. B. ein QMessageBox-Dialog mit drei Buttons).
- Im Mock- oder Headless-Modus (kein Qt/QGIS verfügbar) wird der übergebene
`default`-Wert zurückgegeben.
- Alle Nutzerinteraktionen laufen über diese zentrale Methode, damit das
Plugin an einer Stelle gesteuert und ggf. getested werden kann.
Parameter
---------
layer_name:
Anzeigename des Layers, der bereits existiert (wird im Dialog angezeigt).
default:
Rückgabewert im Headless/Mock-Modus oder wenn der Dialog nicht verfügbar ist.
Gültige Werte: "overwrite", "append", "cancel". Standard: "overwrite".
"""
# Validierung des Defaults
if default not in ("overwrite", "append", "cancel"):
default = "overwrite"
# Versuche, eine UI-Wrapper-Funktion zu verwenden, falls vorhanden
try:
# qgisui_wrapper kann eine spezialisierte Dialogfunktion bereitstellen
from sn_basis.functions import qgisui_wrapper as qgisui
ask_fn = getattr(qgisui, "ask_overwrite_append_cancel", None)
if callable(ask_fn):
# Die Wrapper-Funktion soll genau die drei Strings zurückgeben
choice = ask_fn(layer_name)
if choice in ("overwrite", "append", "cancel"):
return choice
except Exception:
# Falls Import/Wrapper fehlschlägt, weiter zum Qt-Fallback
pass
# Fallback: direkte Qt-Dialoge über qt_wrapper (wenn verfügbar)
try:
from sn_basis.functions import qt_wrapper as qt
QMessageBox = getattr(qt, "QMessageBox", None)
if QMessageBox is not None:
# Erzeuge und konfiguriere Dialog
msg = QMessageBox()
msg.setWindowTitle("Layer bereits vorhanden")
msg.setText(f"Der Layer '{layer_name}' existiert bereits. Was möchten Sie tun?")
overwrite_btn = msg.addButton("Überschreiben", QMessageBox.AcceptRole)
append_btn = msg.addButton("Anhängen", QMessageBox.AcceptRole)
cancel_btn = msg.addButton("Abbrechen", QMessageBox.RejectRole)
msg.setDefaultButton(overwrite_btn)
# Blockierend anzeigen
msg.exec_()
clicked = msg.clickedButton()
if clicked == overwrite_btn:
return "overwrite"
if clicked == append_btn:
return "append"
return "cancel"
except Exception:
# Qt nicht verfügbar oder Fehler beim Dialogaufbau
pass
# Headless / Mock: gib Default zurück
return default

View File

@@ -1,9 +1,21 @@
"""
sn_basis/modules/pruef_ergebnis.py
Erweitertes Ergebnisobjekt für Dateiprüfungen mit Verfahrens-DB-spezifischen Aktionen.
"""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional, Literal from typing import Any, Optional, Literal
from pathlib import Path
# =============================================================================
# Erweiterte PruefAktionen für Verfahrens-DB-Workflow
# =============================================================================
# Erweitertes Literal mit allen erlaubten Aktionen (PruefAktion)
PruefAktion = Literal[ PruefAktion = Literal[
# Basis-Aktionen (bestehend)
"ok", "ok",
"leer", "leer",
"leereingabe_erlaubt", "leereingabe_erlaubt",
@@ -16,6 +28,8 @@ PruefAktion = Literal[
"pfad_nicht_gefunden", "pfad_nicht_gefunden",
"url_nicht_erreichbar", "url_nicht_erreichbar",
"netzwerkfehler", "netzwerkfehler",
# Layer-spezifisch
"layer_nicht_gefunden", "layer_nicht_gefunden",
"layer_unsichtbar", "layer_unsichtbar",
"falscher_geotyp", "falscher_geotyp",
@@ -25,15 +39,26 @@ PruefAktion = Literal[
"felder_fehlen", "felder_fehlen",
"datenquelle_unerwartet", "datenquelle_unerwartet",
"layer_nicht_editierbar", "layer_nicht_editierbar",
# Dateiendung/Format
"falsche_endung", "falsche_endung",
"pflichtfelder_fehlen", "pflichtfelder_fehlen",
# Excel / Import-spezifische Aktionen
# Excel/Import
"kein_header", "kein_header",
"kein_arbeitsblatt", "kein_arbeitsblatt",
"read_error", "read_error",
"open_error", "open_error",
"datenabruf", "datenabruf",
# Generische Prüf-/Speicher-Aktionen
# 🆕 VERFAHRENS-DB SPEZIFISCH (deine Anforderungen 2.d, 2.e)
"datei_wird_erzeugt", # 2.d: Pfad gültig, Datei fehlt → weiter
"datei_existiert", # Datei vorhanden → Layer-Entscheidung
"datei_existiert_ueberschreiben", # 2.e: Nutzer wählt "Überschreiben"
"datei_existiert_anhaengen", # 2.e: Nutzer wählt "Anhängen"
"datei_existiert_ueberspringen", # 2.e: Nutzer wählt "Überspringen"
# Generisch
"pruefe_exception", "pruefe_exception",
"save_exception", "save_exception",
"save_not_implemented", "save_not_implemented",
@@ -42,22 +67,113 @@ PruefAktion = Literal[
"needs_user_action", "needs_user_action",
] ]
@dataclass @dataclass
class pruef_ergebnis: class pruef_ergebnis:
""" """
Einheitliches Ergebnisobjekt für Prüfer. Einheitliches Ergebnisobjekt für Prüfer im Verfahrens-DB-Workflow.
- ok: True wenn Prüfung bestanden
- meldung: menschenlesbare Meldung Attributes
- aktion: maschinenlesbarer Aktionscode (PruefAktion) ----------
- kontext: optionaler Zusatzkontext (z. B. Pfad, Layer-Objekt) ok : bool
True wenn Prüfung bestanden und Pipeline fortgesetzt werden kann.
False signalisiert Fehler oder Nutzerentscheidung erforderlich.
meldung : Optional[str], optional
Menschenlesbare Meldung für UI-Dialoge (BY: Pruefmanager).
aktion : Optional[PruefAktion], optional
Maschinenlesbarer Aktionscode für nachfolgende Pipeline-Schritte.
kontext : Optional[Any], optional
Zusatzkontext: meist `pathlib.Path` für Dateipfade oder Layer-Objekte.
Verfahrens-DB-spezifische Aktionen:
+-----------------------------+-------------------------------------------------+
| Aktion | Bedeutung |
+=============================+=================================================+
| ``datei_wird_erzeugt`` | 2.d: Neues GPKG wird angelegt (Pfad gültig) |
+-----------------------------+-------------------------------------------------+
| ``datei_existiert`` | Datei vorhanden → Layer-Überschreibung prüfen |
+-----------------------------+-------------------------------------------------+
| ``datei_existiert_*`` | 2.e: Nutzerentscheidung für bestehende Datei |
+-----------------------------+-------------------------------------------------+
""" """
ok: bool ok: bool
meldung: Optional[str] = None meldung: Optional[str] = None
aktion: Optional[PruefAktion] = None aktion: Optional[PruefAktion] = None
kontext: Optional[Any] = None kontext: Optional[Any] = None
def __init__(self, ok: bool, meldung: Optional[str] = None, aktion: Optional[PruefAktion] = None, kontext: Optional[Any] = None): def __init__(
self,
ok: bool,
meldung: Optional[str] = None,
aktion: Optional[PruefAktion] = None,
kontext: Optional[Any] = None,
) -> None:
"""
Erstellt ein neues Prüfergebnis.
Parameters
----------
ok : bool
True für "weiter mit Pipeline", False für "Entscheidung/Fehler".
meldung : Optional[str]
UI-Text für Nutzerdialoge.
aktion : Optional[PruefAktion]
Maschinenaktion für nachfolgende Verarbeitung.
kontext : Optional[Any]
Typischerweise `pathlib.Path` (Dateipfad) oder `QgsVectorLayer`.
"""
self.ok = ok self.ok = ok
self.meldung = meldung self.meldung = meldung
self.aktion = aktion self.aktion = aktion
self.kontext = kontext self.kontext = kontext
@property
def ist_verfahrens_db_aktion(self) -> bool:
"""
Prüft, ob es sich um eine Verfahrens-DB-spezifische Aktion handelt.
Returns
-------
bool
True für ``datei_wird_erzeugt`` oder ``datei_existiert*``.
"""
return self.aktion in {
"datei_wird_erzeugt",
"datei_existiert",
"datei_existiert_ueberschreiben",
"datei_existiert_anhaengen",
"datei_existiert_ueberspringen",
}
@property
def dateipfad(self) -> Optional[Path]:
"""
Extrahiert den Dateipfad aus dem Kontext (falls vorhanden).
Returns
-------
Optional[Path]
`Path`-Objekt oder None.
"""
if isinstance(self.kontext, Path):
return self.kontext
return None
@property
def erlaubte_persistierung(self) -> bool:
"""
Prüft, ob die Pipeline Daten persistieren darf.
Returns
-------
bool
True für ``datei_wird_erzeugt``, ``datei_existiert_ueberschreiben``,
``datei_existiert_anhaengen``.
"""
return self.aktion in {
"datei_wird_erzeugt",
"datei_existiert_ueberschreiben",
"datei_existiert_anhaengen",
}