Auf Wrapper umgestellt, Prüfarchitektur QT6-kompatibel gemacht (Nicht lauffähig)

This commit is contained in:
2025-12-18 22:00:31 +01:00
parent f64d56d4bc
commit e8fea163b5
31 changed files with 2791 additions and 889 deletions

View File

@@ -1,100 +1,126 @@
#Modul zur Prüfung und zum Exception Handling für Dateieingaben
#Dateipruefer.py
"""
sn_basis/modulesdateipruefer.py Prüfung von Dateieingaben für das Plugin.
Verwendet syswrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
"""
import os
from enum import Enum, auto
from sn_basis.functions.syswrapper import (
file_exists,
is_file,
join_path,
)
from sn_basis.modules.Pruefmanager import pruef_ergebnis
# -------------------------------
# ENUMS
# -------------------------------
class LeererPfadModus(Enum):#legt die modi fest, die für Dateipfade möglich sind
VERBOTEN = auto() #ein leeres Eingabefeld stellt einen Fehler dar
NUTZE_STANDARD = auto() #ein leeres Eingabefeld fordert zur Entscheidung auf: nutze Standard oder brich ab
TEMPORAER_ERLAUBT = auto() #ein leeres Eingabefeld fordert zur Entscheidung auf: arbeite temporär oder brich ab.
class DateiEntscheidung(Enum):#legt die Modi fest, wie mit bestehenden Dateien umgegangen werden soll (hat das das QGSFile-Objekt schon selbst?)
ERSETZEN = auto()#Ergebnis der Nutzerentscheidung: bestehende Datei ersetzen
ANHAENGEN = auto()#Ergebnis der Nutzerentscheidung: an bestehende Datei anhängen
ABBRECHEN = auto()#bricht den Vorgang ab. (muss das eine definierte Option sein? oder geht das auch mit einem normalen Abbruch-Button)
# -------------------------------
# RÜCKGABEOBJEKT
# -------------------------------
#Das Dateiprüfergebnis wird an den Prüfmanager übergeben. Alle GUI-Abfragen werden im Prüfmanager behandelt.
class DateipruefErgebnis:
#Definition der Parameter und Festlegung auf den Parametertyp,bzw den Standardwert
def __init__(self, erfolgreich: bool, pfad: str = None, temporär: bool = False,
entscheidung: DateiEntscheidung = None, fehler: list = None):
self.erfolgreich = erfolgreich
self.pfad = pfad
self.temporär = temporär
self.entscheidung = entscheidung
self.fehler = fehler or []
def __repr__(self):
return (f"DateipruefErgebnis(erfolgreich={self.erfolgreich}, "
f"pfad={repr(self.pfad)}, temporär={self.temporär}, "
f"entscheidung={repr(self.entscheidung)}, fehler={repr(self.fehler)})")
# -------------------------------
# DATEIPRÜFER
# -------------------------------
class Dateipruefer:
def pruefe(self, pfad: str,
leer_modus: LeererPfadModus,
standardname: str = None,
plugin_pfad: str = None,
vorhandene_datei_entscheidung: DateiEntscheidung = None) -> DateipruefErgebnis: #Rückgabetypannotation; "Die Funktion "pruefe" gibt ein Objekt vom Typ "DateipruefErgebnis" zurück
"""
Prüft Dateieingaben und liefert ein pruef_ergebnis zurück.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
"""
# 1. Prüfe, ob das Eingabefeld leer ist
if not pfad or pfad.strip() == "":#wenn der angegebene Pfad leer oder ungültig ist:
if leer_modus == LeererPfadModus.VERBOTEN: #wenn der Modus "verboten" vorgegeben ist, gib zurück, dass der Test fehlgeschlagen ist
return DateipruefErgebnis(
erfolgreich=False,
fehler=["Kein Pfad angegeben."]
)
elif leer_modus == LeererPfadModus.NUTZE_STANDARD:#wenn der Modus "Nutze_Standard" vorgegeben ist...
if not plugin_pfad or not standardname:#wenn kein gültiger Pluginpfad angegeben ist oder die Standarddatei fehlt...
return DateipruefErgebnis(
erfolgreich=False,
fehler=["Standardpfad oder -name fehlen."]#..gib zurück, dass der Test fehlgeschlagen ist
)
pfad = os.path.join(plugin_pfad, standardname)#...wenn es Standarddatei und Pluginpfad gibt...setze sie zum Pfad zusammen...
elif leer_modus == LeererPfadModus.TEMPORAER_ERLAUBT:#wenn der Modus "temporär" vorgegeben ist,...
return DateipruefErgebnis(#...gib zurück, dass das Prüfergebnis erfolgreich ist (Entscheidung, ob temporör gearbeitet werden soll oder nicht, kommt woanders)
erfolgreich=True,
pfad=None
)
def __init__(
self,
pfad: str,
basis_pfad: str = "",
leereingabe_erlaubt: bool = False,
standarddatei: str | None = None,
temporaer_erlaubt: bool = False,
):
self.pfad = pfad
self.basis_pfad = basis_pfad
self.leereingabe_erlaubt = leereingabe_erlaubt
self.standarddatei = standarddatei
self.temporaer_erlaubt = temporaer_erlaubt
# 2. Existiert die Datei bereits?
if os.path.exists(pfad):#wenn die Datei vorhanden ist...
if not vorhandene_datei_entscheidung:#aber noch keine Entscheidung getroffen ist...
return DateipruefErgebnis(
erfolgreich=True,#ist die Prüfung erfolgreich, aber es muss noch eine Entscheidung verlangt werden
pfad=pfad,
entscheidung=None,
fehler=["Datei existiert bereits Entscheidung ausstehend."]
)
if vorhandene_datei_entscheidung == DateiEntscheidung.ABBRECHEN:
return DateipruefErgebnis(#...der Nutzer aber abgebrochen hat...
erfolgreich=False,#ist die Prüfung fehlgeschlagen ISSUE: ergibt das Sinn?
pfad=pfad,
fehler=["Benutzer hat abgebrochen."]
)
# ---------------------------------------------------------
# Hilfsfunktion
# ---------------------------------------------------------
return DateipruefErgebnis(
erfolgreich=True,
def _pfad(self, relativer_pfad: str) -> str:
"""Erzeugt einen OSunabhängigen Pfad relativ zum Basisverzeichnis."""
return join_path(self.basis_pfad, relativer_pfad)
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
"""
Prüft eine Dateieingabe und liefert ein pruef_ergebnis zurück.
Der Pruefmanager entscheidet später, wie der Nutzer gefragt wird.
"""
# -----------------------------------------------------
# 1. Fall: Eingabe ist leer
# -----------------------------------------------------
if not self.pfad:
return self._handle_leere_eingabe()
# -----------------------------------------------------
# 2. Fall: Eingabe ist nicht leer → Datei prüfen
# -----------------------------------------------------
pfad = self._pfad(self.pfad)
if not file_exists(pfad) or not is_file(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
aktion="datei_nicht_gefunden",
pfad=pfad,
entscheidung=vorhandene_datei_entscheidung
)
# 3. Pfad gültig und Datei nicht vorhanden
#wenn alle Varianten NICHT zutreffen, weil ein gültiger Pfad eingegeben wurde und die Datei noch nicht vorhanden ist:
return DateipruefErgebnis(
erfolgreich=True,
pfad=pfad
# -----------------------------------------------------
# 3. Datei existiert → Erfolg
# -----------------------------------------------------
return pruef_ergebnis(
ok=True,
meldung="Datei gefunden.",
aktion="ok",
pfad=pfad,
)
# ---------------------------------------------------------
# Behandlung leerer Eingaben
# ---------------------------------------------------------
def _handle_leere_eingabe(self) -> pruef_ergebnis:
"""
Liefert ein pruef_ergebnis für den Fall, dass das Dateifeld leer ist.
Der Pruefmanager fragt später den Nutzer.
"""
# 1. Leereingabe erlaubt → Nutzer fragen, ob das beabsichtigt war
if self.leereingabe_erlaubt:
return pruef_ergebnis(
ok=False,
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
aktion="leereingabe_erlaubt",
pfad=None,
)
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
if self.standarddatei:
return pruef_ergebnis(
ok=False,
meldung=f"Es wurde keine Datei angegeben. Soll die Standarddatei '{self.standarddatei}' verwendet werden?",
aktion="standarddatei_vorschlagen",
pfad=self._pfad(self.standarddatei),
)
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
if self.temporaer_erlaubt:
return pruef_ergebnis(
ok=False,
meldung="Es wurde keine Datei angegeben. Soll eine temporäre Datei erzeugt werden?",
aktion="temporaer_erlaubt",
pfad=None,
)
# 4. Leereingabe nicht erlaubt → Fehler
return pruef_ergebnis(
ok=False,
meldung="Es wurde keine Datei angegeben.",
aktion="leereingabe_nicht_erlaubt",
pfad=None,
)

View File

@@ -1,52 +1,138 @@
#Pruefmanager.py
from modules.qt_compat import QMessageBox, QFileDialog, YES, NO, CANCEL, ICON_QUESTION, exec_dialog
from modules.Dateipruefer import DateiEntscheidung
"""
sn_basis/modules/pruefmanager.py zentrale Verarbeitung von pruef_ergebnis-Objekten.
Steuert die Nutzerinteraktion über qgisqt_wrapper.
"""
class PruefManager:
from sn_basis.functions.qgisqt_wrapper import (
ask_yes_no,
info,
warning,
error,
set_layer_visible, # optional, falls implementiert
)
def __init__(self, iface=None, plugin_pfad=None):
self.iface = iface
self.plugin_pfad = plugin_pfad
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung:
msg = QMessageBox()
msg.setIcon(ICON_QUESTION)
msg.setWindowTitle("Datei existiert")
msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?")
msg.setStandardButtons(YES | NO | CANCEL)
msg.setDefaultButton(YES)
class Pruefmanager:
"""
Verarbeitet pruef_ergebnis-Objekte und steuert die Nutzerinteraktion.
"""
msg.button(YES).setText("Ersetzen")
msg.button(NO).setText("Anhängen")
msg.button(CANCEL).setText("Abbrechen")
def __init__(self, ui_modus: str = "qgis"):
self.ui_modus = ui_modus
result = exec_dialog(msg)
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
if result == YES:
return DateiEntscheidung.ERSETZEN
elif result == NO:
return DateiEntscheidung.ANHAENGEN
else:
return DateiEntscheidung.ABBRECHEN
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
"""
Verarbeitet ein pruef_ergebnis und führt ggf. Nutzerinteraktion durch.
Rückgabe: neues oder unverändertes pruef_ergebnis.
"""
def frage_temporär_verwenden(self) -> bool:
msg = QMessageBox()
msg.setIcon(ICON_QUESTION)
msg.setWindowTitle("Temporäre Layer")
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
if ergebnis.ok:
return ergebnis
msg.setStandardButtons(YES | NO)
msg.setDefaultButton(YES)
aktion = ergebnis.aktion
result = exec_dialog(msg)
return result == YES
# -----------------------------------------------------
# Allgemeine Aktionen
# -----------------------------------------------------
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
pfad, _ = QFileDialog.getSaveFileName(
parent=None,
caption=titel,
directory=self.plugin_pfad or "",
filter=filter
)
return pfad
if aktion == "leer":
warning("Eingabe fehlt", ergebnis.meldung)
return ergebnis
if aktion == "leereingabe_erlaubt":
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
return pruef_ergebnis(True, "Ohne Eingabe fortgefahren.", "ok", None)
return ergebnis
if aktion == "leereingabe_nicht_erlaubt":
warning("Eingabe erforderlich", ergebnis.meldung)
return ergebnis
if aktion == "standarddatei_vorschlagen":
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
return pruef_ergebnis(True, "Standarddatei wird verwendet.", "ok", ergebnis.pfad)
return ergebnis
if aktion == "temporaer_erlaubt":
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
return pruef_ergebnis(True, "Temporäre Datei soll erzeugt werden.", "temporaer_erzeugen", None)
return ergebnis
if aktion == "datei_nicht_gefunden":
warning("Datei nicht gefunden", ergebnis.meldung)
return ergebnis
if aktion == "kein_dateipfad":
warning("Ungültiger Pfad", ergebnis.meldung)
return ergebnis
if aktion == "pfad_nicht_gefunden":
warning("Pfad nicht gefunden", ergebnis.meldung)
return ergebnis
if aktion == "url_nicht_erreichbar":
warning("URL nicht erreichbar", ergebnis.meldung)
return ergebnis
if aktion == "netzwerkfehler":
error("Netzwerkfehler", ergebnis.meldung)
return ergebnis
# -----------------------------------------------------
# Layer-Aktionen
# -----------------------------------------------------
if aktion == "layer_nicht_gefunden":
error("Layer fehlt", ergebnis.meldung)
return ergebnis
if aktion == "layer_unsichtbar":
if ask_yes_no("Layer einblenden", ergebnis.meldung):
# Falls set_layer_visible implementiert ist
try:
set_layer_visible(ergebnis.pfad, True)
except Exception:
pass
return pruef_ergebnis(True, "Layer wurde eingeblendet.", "ok", ergebnis.pfad)
return ergebnis
if aktion == "falscher_geotyp":
warning("Falscher Geometrietyp", ergebnis.meldung)
return ergebnis
if aktion == "layer_leer":
warning("Layer enthält keine Objekte", ergebnis.meldung)
return ergebnis
if aktion == "falscher_layertyp":
warning("Falscher Layertyp", ergebnis.meldung)
return ergebnis
if aktion == "falsches_crs":
warning("Falsches CRS", ergebnis.meldung)
return ergebnis
if aktion == "felder_fehlen":
warning("Fehlende Felder", ergebnis.meldung)
return ergebnis
if aktion == "datenquelle_unerwartet":
warning("Unerwartete Datenquelle", ergebnis.meldung)
return ergebnis
if aktion == "layer_nicht_editierbar":
warning("Layer nicht editierbar", ergebnis.meldung)
return ergebnis
# -----------------------------------------------------
# Fallback
# -----------------------------------------------------
warning("Unbekannte Aktion", f"Unbekannte Aktion: {aktion}")
return ergebnis

170
modules/layerpruefer.py Normal file
View File

@@ -0,0 +1,170 @@
"""
sn_basis/modules/layerpruefer.py Prüfung von QGIS-Layern.
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
"""
from sn_basis.functions.qgisqt_wrapper import (
layer_exists,
get_layer_geometry_type,
get_layer_feature_count,
is_layer_visible,
get_layer_type,
get_layer_crs,
get_layer_fields,
get_layer_source,
is_layer_editable,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class Layerpruefer:
"""
Prüft Layer auf Existenz, Sichtbarkeit, Geometrietyp, Objektanzahl,
Layertyp, CRS, Felder, Datenquelle und Editierbarkeit.
"""
def __init__(
self,
layer,
erwarteter_geotyp: str | None = None,
muss_sichtbar_sein: bool = False,
erwarteter_layertyp: str | None = None,
erwartetes_crs: str | None = None,
erforderliche_felder: list[str] | None = None,
erlaubte_datenquellen: list[str] | None = None,
muss_editierbar_sein: bool = False,
):
self.layer = layer
self.erwarteter_geotyp = erwarteter_geotyp
self.muss_sichtbar_sein = muss_sichtbar_sein
self.erwarteter_layertyp = erwarteter_layertyp
self.erwartetes_crs = erwartetes_crs
self.erforderliche_felder = erforderliche_felder or []
self.erlaubte_datenquellen = erlaubte_datenquellen or []
self.muss_editierbar_sein = muss_editierbar_sein
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
# -----------------------------------------------------
# 1. Existenz
# -----------------------------------------------------
if not layer_exists(self.layer):
return pruef_ergebnis(
ok=False,
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
aktion="layer_nicht_gefunden", # type: ignore
pfad=None,
)
# -----------------------------------------------------
# 2. Sichtbarkeit
# -----------------------------------------------------
sichtbar = is_layer_visible(self.layer)
if self.muss_sichtbar_sein and not sichtbar:
return pruef_ergebnis(
ok=False,
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
aktion="layer_unsichtbar", # type: ignore
pfad=self.layer, # Layerobjekt wird übergeben
)
# -----------------------------------------------------
# 3. Layertyp
# -----------------------------------------------------
layertyp = get_layer_type(self.layer)
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat den Typ '{layertyp}', erwartet wurde '{self.erwarteter_layertyp}'.",
aktion="falscher_layertyp",
pfad=None,
)
# -----------------------------------------------------
# 4. Geometrietyp
# -----------------------------------------------------
geotyp = get_layer_geometry_type(self.layer)
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat den Geometrietyp '{geotyp}', erwartet wurde '{self.erwarteter_geotyp}'.",
aktion="falscher_geotyp",
pfad=None,
)
# -----------------------------------------------------
# 5. Featureanzahl
# -----------------------------------------------------
anzahl = get_layer_feature_count(self.layer)
if anzahl == 0:
return pruef_ergebnis(
ok=False,
meldung="Der Layer enthält keine Objekte.",
aktion="layer_leer",
pfad=None,
)
# -----------------------------------------------------
# 6. CRS
# -----------------------------------------------------
crs = get_layer_crs(self.layer)
if self.erwartetes_crs and crs != self.erwartetes_crs:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat das CRS '{crs}', erwartet wurde '{self.erwartetes_crs}'.",
aktion="falsches_crs",
pfad=None,
)
# -----------------------------------------------------
# 7. Felder
# -----------------------------------------------------
felder = get_layer_fields(self.layer)
fehlende = [f for f in self.erforderliche_felder if f not in felder]
if fehlende:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer enthält nicht alle erforderlichen Felder: {', '.join(fehlende)}",
aktion="felder_fehlen",
pfad=None,
)
# -----------------------------------------------------
# 8. Datenquelle
# -----------------------------------------------------
quelle = get_layer_source(self.layer)
if self.erlaubte_datenquellen and quelle not in self.erlaubte_datenquellen:
return pruef_ergebnis(
ok=False,
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
aktion="datenquelle_unerwartet",
pfad=None,
)
# -----------------------------------------------------
# 9. Editierbarkeit
# -----------------------------------------------------
editable = is_layer_editable(self.layer)
if self.muss_editierbar_sein and not editable:
return pruef_ergebnis(
ok=False,
meldung="Der Layer ist nicht editierbar.",
aktion="layer_nicht_editierbar",
pfad=None,
)
# -----------------------------------------------------
# 10. Alles OK
# -----------------------------------------------------
return pruef_ergebnis(
ok=True,
meldung="Layerprüfung erfolgreich.",
aktion="ok",
pfad=None,
)

View File

@@ -1,77 +1,141 @@
# Linkpruefer.py Qt5/Qt6-kompatibel über qt_compat
"""
sn_basis/modules/linkpruefer.py Prüfung von URLs und lokalen Links.
Verwendet syswrapper und qgisqt_wrapper.
Gibt pruef_ergebnis an den Pruefmanager zurück.
"""
from modules.qt_compat import (
QEventLoop,
QUrl,
QNetworkRequest,
QNetworkReply
from sn_basis.functions.syswrapper import (
file_exists,
is_file,
join_path,
)
from qgis.core import QgsNetworkAccessManager
from modules.pruef_ergebnis import PruefErgebnis
from sn_basis.functions.qgisqt_wrapper import (
network_head,
)
from sn_basis.modules.Pruefmanager import pruef_ergebnis
class Linkpruefer:
"""Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut."""
"""
Prüft URLs und lokale Pfade.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
"""
ANBIETER_TYPEN: dict[str, str] = {
"REST": "REST",
"WFS": "WFS",
"WMS": "WMS",
"OGR": "OGR"
}
def __init__(self, basis_pfad: str | None = None):
"""
basis_pfad: optionaler Basisordner für relative Pfade.
"""
self.basis = basis_pfad
def __init__(self, link: str, anbieter: str):
self.link = link
self.anbieter = anbieter.upper().strip() if anbieter else ""
self.network_manager = QgsNetworkAccessManager()
# ---------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------
def klassifiziere_anbieter(self):
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
return {"typ": typ, "quelle": quelle}
def _pfad(self, relativer_pfad: str) -> str:
"""Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
if not self.basis:
return relativer_pfad
return join_path(self.basis, relativer_pfad)
def pruefe_link(self):
fehler = []
warnungen = []
def _ist_url(self, text: str) -> bool:
"""Einfache URL-Erkennung."""
return text.startswith("http://") or text.startswith("https://")
if not self.link:
fehler.append("Link fehlt.")
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
if not self.anbieter or not self.anbieter.strip():
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
def pruefe(self, eingabe: str) -> pruef_ergebnis:
"""
Prüft einen Link (URL oder lokalen Pfad).
Rückgabe: pruef_ergebnis
"""
# Remote-Links prüfen
if self.link.startswith(("http://", "https://")):
request = QNetworkRequest(QUrl(self.link))
reply = self.network_manager.head(request)
if not eingabe:
return pruef_ergebnis(
ok=False,
meldung="Es wurde kein Link angegeben.",
aktion="leer",
pfad=None,
)
loop = QEventLoop()
reply.finished.connect(loop.quit)
loop.exec() # Qt5/Qt6-kompatibel über qt_compat
# -----------------------------------------------------
# 1. Fall: URL
# -----------------------------------------------------
if self._ist_url(eingabe):
return self._pruefe_url(eingabe)
# Fehlerprüfung Qt5/Qt6-kompatibel
if reply.error() != QNetworkReply.NetworkError.NoError:
fehler.append(f"Verbindungsfehler: {reply.errorString()}")
else:
status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status is None or status < 200 or status >= 400:
fehler.append(f"Link nicht erreichbar: HTTP {status}")
# -----------------------------------------------------
# 2. Fall: lokaler Pfad
# -----------------------------------------------------
return self._pruefe_dateipfad(eingabe)
reply.deleteLater()
# ---------------------------------------------------------
# URLPrüfung
# ---------------------------------------------------------
else:
# Lokale Pfade: Plausibilitätscheck
if "." not in self.link.split("/")[-1]:
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
def _pruefe_url(self, url: str) -> pruef_ergebnis:
"""
Prüft eine URL über einen HEADRequest.
"""
return PruefErgebnis(
len(fehler) == 0,
daten=self.klassifiziere_anbieter(),
fehler=fehler,
warnungen=warnungen
reply = network_head(url)
if reply is None:
return pruef_ergebnis(
ok=False,
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
aktion="netzwerkfehler",
pfad=url,
)
if reply.error != 0:
return pruef_ergebnis(
ok=False,
meldung=f"Die URL '{url}' ist nicht erreichbar.",
aktion="url_nicht_erreichbar",
pfad=url,
)
return pruef_ergebnis(
ok=True,
meldung="URL ist erreichbar.",
aktion="ok",
pfad=url,
)
def ausfuehren(self):
return self.pruefe_link()
# ---------------------------------------------------------
# Lokale Datei/Pfadprüfung
# ---------------------------------------------------------
def _pruefe_dateipfad(self, eingabe: str) -> pruef_ergebnis:
"""
Prüft einen lokalen Pfad.
"""
pfad = self._pfad(eingabe)
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
aktion="pfad_nicht_gefunden",
pfad=pfad,
)
if not is_file(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{eingabe}' ist keine Datei.",
aktion="kein_dateipfad",
pfad=pfad,
)
return pruef_ergebnis(
ok=True,
meldung="Dateipfad ist gültig.",
aktion="ok",
pfad=pfad,
)

View File

@@ -1,12 +1,58 @@
#pruef_ergebnis.py
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
class PruefErgebnis:
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
self.erfolgreich = erfolgreich
self.daten = daten or {}
self.fehler = fehler or []
self.warnungen = warnungen or []
"""
sn_basis/modules/pruef_ergebnis.py Ergebnisobjekt für alle Prüfer.
def __repr__(self):
return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, "
f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})")
"""
from dataclasses import dataclass
from typing import Optional, Literal
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
# Erweiterbar ohne Umbau der Klasse.
PruefAktion = Literal[
"ok",
"leer",
"leereingabe_erlaubt",
"leereingabe_nicht_erlaubt",
"standarddatei_vorschlagen",
"temporaer_erlaubt",
"datei_nicht_gefunden",
"kein_dateipfad",
"pfad_nicht_gefunden",
"url_nicht_erreichbar",
"netzwerkfehler",
"falscher_layertyp",
"falscher_geotyp",
"layer_leer",
"falsches_crs",
"felder_fehlen",
"datenquelle_unerwartet",
"layer_nicht_editierbar",
"temporaer_erzeugen",
"stil_nicht_anwendbar",
"layer_unsichtbar",
"unbekannt",
]
@dataclass
class pruef_ergebnis:
"""
Reines Datenobjekt, das das Ergebnis einer Prüfung beschreibt.
ok: True → Prüfung erfolgreich
False → Nutzerinteraktion oder Fehler nötig
meldung: Text, der dem Nutzer angezeigt werden soll
aktion: Maschinenlesbarer Code, der dem Pruefmanager sagt,
wie er weiter verfahren soll
pfad: Optionaler Pfad oder URL, die geprüft wurde oder
verwendet werden soll
"""
ok: bool
meldung: str
aktion: PruefAktion
pfad: Optional[str] = None

View File

@@ -1,111 +0,0 @@
"""
qt_compat.py Einheitliche Qt-Kompatibilitätsschicht für QGIS-Plugins.
Ziele:
- PyQt6 bevorzugt
- Fallback auf PyQt5
- Mock-Modus, wenn kein Qt verfügbar ist (z. B. in Unittests)
- OR-fähige Fake-Enums im Mock-Modus
"""
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
# ---------------------------------------------------------
# Versuch: PyQt6 importieren
# ---------------------------------------------------------
try:
from PyQt6.QtWidgets import QMessageBox, QFileDialog
from PyQt6.QtCore import Qt, QEventLoop, QUrl
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
QT_VERSION = 6
def exec_dialog(dialog):
"""Einheitliche Ausführung eines Dialogs."""
return dialog.exec()
# ---------------------------------------------------------
# Versuch: PyQt5 importieren
# ---------------------------------------------------------
except Exception:
try:
from PyQt5.QtWidgets import QMessageBox, QFileDialog
from PyQt5.QtCore import Qt, QEventLoop, QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
YES = QMessageBox.Yes
NO = QMessageBox.No
CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question
QT_VERSION = 5
def exec_dialog(dialog):
return dialog.exec_()
# ---------------------------------------------------------
# Mock-Modus (kein Qt verfügbar)
# ---------------------------------------------------------
except Exception:
QT_VERSION = 0
class FakeEnum(int):
"""Ein OR-fähiger Enum-Ersatz für den Mock-Modus."""
def __or__(self, other):
return FakeEnum(int(self) | int(other))
class QMessageBox:
Yes = FakeEnum(1)
No = FakeEnum(2)
Cancel = FakeEnum(4)
Question = FakeEnum(8)
class QFileDialog:
"""Minimaler Mock für QFileDialog."""
@staticmethod
def getOpenFileName(*args, **kwargs):
return ("", "") # kein Dateipfad
YES = QMessageBox.Yes
NO = QMessageBox.No
CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question
def exec_dialog(dialog):
"""Mock-Ausführung: gibt YES zurück, außer Tests patchen es."""
return YES
# -------------------------
# Mock Netzwerk-Klassen
# -------------------------
class QEventLoop:
def exec(self):
return 0
def quit(self):
pass
class QUrl(str):
pass
class QNetworkRequest:
def __init__(self, url):
self.url = url
class QNetworkReply:
def __init__(self):
self._data = b""
def readAll(self):
return self._data
def error(self):
return 0
def exec_dialog(dialog):
return YES

View File

@@ -1,46 +1,59 @@
#stilpruefer.py
import os
from modules.pruef_ergebnis import PruefErgebnis
"""
sn_basis/modules/stilpruefer.py Prüfung und Anwendung von Layerstilen.
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
"""
from sn_basis.functions.qgisqt_wrapper import (
apply_style,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class Stilpruefer:
"""
Prüft, ob ein angegebener Stilpfad gültig und nutzbar ist.
- Wenn kein Stil angegeben ist, gilt die Prüfung als erfolgreich.
- Wenn angegeben:
* Datei muss existieren
* Dateiendung muss '.qml' sein
Prüft, ob ein Stil auf einen Layer angewendet werden kann.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
"""
def pruefe(self, stilpfad: str) -> PruefErgebnis:
# kein Stil angegeben -> erfolgreich, keine Warnung
if not stilpfad or stilpfad.strip() == "":
return PruefErgebnis(
erfolgreich=True,
daten={"stil": None},
warnungen=["Kein Stil angegeben."]
def __init__(self, layer, stil_pfad: str):
"""
layer: QGIS-Layer oder Mock-Layer
stil_pfad: relativer oder absoluter Pfad zum .qml-Stil
"""
self.layer = layer
self.stil_pfad = stil_pfad
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
"""
Versucht, den Stil anzuwenden.
Rückgabe: pruef_ergebnis
"""
# Wrapper übernimmt:
# - Pfadberechnung
# - Existenzprüfung
# - loadNamedStyle
# - Fehlerbehandlung
# - Mock-Modus
erfolg, meldung = apply_style(self.layer, self.stil_pfad)
if erfolg:
return pruef_ergebnis(
ok=True,
meldung=f"Stil erfolgreich angewendet: {self.stil_pfad}",
aktion="ok",
pfad=self.stil_pfad,
)
fehler = []
warnungen = []
# Prüfung: Datei existiert?
if not os.path.exists(stilpfad):
fehler.append(f"Stildatei nicht gefunden: {stilpfad}")
# Prüfung: Endung .qml?
elif not stilpfad.lower().endswith(".qml"):
fehler.append(f"Ungültige Dateiendung für Stil: {stilpfad}")
else:
# Hinweis: alle Checks bestanden
return PruefErgebnis(
erfolgreich=True,
daten={"stil": stilpfad}
)
return PruefErgebnis(
erfolgreich=False if fehler else True,
daten={"stil": stilpfad},
fehler=fehler,
warnungen=warnungen
# Fehlerfall → Nutzerinteraktion nötig
return pruef_ergebnis(
ok=False,
meldung=meldung,
aktion="stil_nicht_anwendbar",
pfad=self.stil_pfad,
)