forked from AG_QGIS/Plugin_SN_Basis
Angefangen, DataGrabber anzulegen (Grundlagen gelegt, noch nicht lauffähig)
This commit is contained in:
BIN
assets/datagrabber.jpeg
Normal file
BIN
assets/datagrabber.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
38
assets/datagrabber.md
Normal file
38
assets/datagrabber.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph Plugin
|
||||||
|
P[sn_plan41 Fachplugin]
|
||||||
|
A[Adapter Plan41LinklistAdapter]
|
||||||
|
PM[Pruefmanager]
|
||||||
|
LP[Layerpruefer]
|
||||||
|
KP[Linkpruefer]
|
||||||
|
SP[Stilpruefer]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Core
|
||||||
|
DG[DataGrabber]
|
||||||
|
NL[normalized entries]
|
||||||
|
LL[Layer Loader Provider Dispatch]
|
||||||
|
SM[Spatial Matcher]
|
||||||
|
ST[Storage GPKG / PostGIS]
|
||||||
|
PR[Project QGIS - addMapLayer]
|
||||||
|
LOG[Log / Ergebnisstruktur]
|
||||||
|
end
|
||||||
|
|
||||||
|
P -->|gibt Adapter, Prüfer, Pruefmanager| DG
|
||||||
|
A -->|load liefert Rohdaten| DG
|
||||||
|
DG -->|adapter.normalize| NL
|
||||||
|
NL --> DG
|
||||||
|
DG -->|für jeden Eintrag: _check_link -> KP.check| KP
|
||||||
|
DG -->|für jeden Eintrag: _check_style -> SP.check| SP
|
||||||
|
DG -->|prüfe vorhandene Layer| LP
|
||||||
|
DG -->|lade Layer via provider| LL
|
||||||
|
LL -->|Features| SM
|
||||||
|
SM -->|Abgleich| DG
|
||||||
|
DG -->|speichern| ST
|
||||||
|
ST --> PR
|
||||||
|
DG --> PR
|
||||||
|
DG -->|Ergebnis/Fehler| LOG
|
||||||
|
LOG --> PM
|
||||||
|
DG --> PM
|
||||||
|
```
|
||||||
BIN
assets/datagrabber.pdf
Normal file
BIN
assets/datagrabber.pdf
Normal file
Binary file not shown.
@@ -32,6 +32,7 @@ QTabWidget: type
|
|||||||
QToolButton: Type[Any]
|
QToolButton: Type[Any]
|
||||||
QSizePolicy: Type[Any]
|
QSizePolicy: Type[Any]
|
||||||
Qt: Type[Any]
|
Qt: Type[Any]
|
||||||
|
ComboBox: Type[Any]
|
||||||
|
|
||||||
YES: Optional[Any] = None
|
YES: Optional[Any] = None
|
||||||
NO: Optional[Any] = None
|
NO: Optional[Any] = None
|
||||||
@@ -68,6 +69,7 @@ try:
|
|||||||
QTabWidget as _QTabWidget,# type: ignore
|
QTabWidget as _QTabWidget,# type: ignore
|
||||||
QToolButton as _QToolButton,#type:ignore
|
QToolButton as _QToolButton,#type:ignore
|
||||||
QSizePolicy as _QSizePolicy,#type:ignore
|
QSizePolicy as _QSizePolicy,#type:ignore
|
||||||
|
QComboBox as _QComboBox,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +109,7 @@ try:
|
|||||||
QTabWidget = _QTabWidget
|
QTabWidget = _QTabWidget
|
||||||
QToolButton=_QToolButton
|
QToolButton=_QToolButton
|
||||||
QSizePolicy=_QSizePolicy
|
QSizePolicy=_QSizePolicy
|
||||||
|
QComboBox=_QComboBox
|
||||||
|
|
||||||
YES = QMessageBox.StandardButton.Yes
|
YES = QMessageBox.StandardButton.Yes
|
||||||
NO = QMessageBox.StandardButton.No
|
NO = QMessageBox.StandardButton.No
|
||||||
@@ -148,7 +151,7 @@ try:
|
|||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (# type: ignore
|
||||||
QMessageBox as _QMessageBox,
|
QMessageBox as _QMessageBox,
|
||||||
QFileDialog as _QFileDialog,
|
QFileDialog as _QFileDialog,
|
||||||
QWidget as _QWidget,
|
QWidget as _QWidget,
|
||||||
@@ -166,18 +169,20 @@ except Exception:
|
|||||||
QTabWidget as _QTabWidget,
|
QTabWidget as _QTabWidget,
|
||||||
QToolButton as _QToolButton,
|
QToolButton as _QToolButton,
|
||||||
QSizePolicy as _QSizePolicy,
|
QSizePolicy as _QSizePolicy,
|
||||||
|
QComboBox as _QComboBox,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (# type: ignore
|
||||||
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 (
|
from PyQt5.QtNetwork import (# type: ignore
|
||||||
QNetworkRequest as _QNetworkRequest,
|
QNetworkRequest as _QNetworkRequest,
|
||||||
QNetworkReply as _QNetworkReply,
|
QNetworkReply as _QNetworkReply,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
QMessageBox = _QMessageBox
|
QMessageBox = _QMessageBox
|
||||||
QFileDialog = _QFileDialog
|
QFileDialog = _QFileDialog
|
||||||
QEventLoop = _QEventLoop
|
QEventLoop = _QEventLoop
|
||||||
@@ -203,6 +208,7 @@ except Exception:
|
|||||||
QTabWidget = _QTabWidget
|
QTabWidget = _QTabWidget
|
||||||
QToolButton=_QToolButton
|
QToolButton=_QToolButton
|
||||||
QSizePolicy=_QSizePolicy
|
QSizePolicy=_QSizePolicy
|
||||||
|
ComboBox=_QComboBox
|
||||||
|
|
||||||
YES = QMessageBox.Yes
|
YES = QMessageBox.Yes
|
||||||
NO = QMessageBox.No
|
NO = QMessageBox.No
|
||||||
@@ -210,6 +216,8 @@ except Exception:
|
|||||||
ICON_QUESTION = QMessageBox.Question
|
ICON_QUESTION = QMessageBox.Question
|
||||||
|
|
||||||
QT_VERSION = 5
|
QT_VERSION = 5
|
||||||
|
|
||||||
|
# then try next backend
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Qt5 Enum-Aliase (vereinheitlicht)
|
# Qt5 Enum-Aliase (vereinheitlicht)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -246,7 +254,7 @@ except Exception:
|
|||||||
QT_VERSION = 0
|
QT_VERSION = 0
|
||||||
|
|
||||||
class FakeEnum(int):
|
class FakeEnum(int):
|
||||||
def __or__(self, other: "FakeEnum") -> "FakeEnum":
|
def __or__(self, other: int) -> "FakeEnum":
|
||||||
return FakeEnum(int(self) | int(other))
|
return FakeEnum(int(self) | int(other))
|
||||||
|
|
||||||
YES = FakeEnum(1)
|
YES = FakeEnum(1)
|
||||||
@@ -518,3 +526,55 @@ except Exception:
|
|||||||
self._tabs.append((widget, title))
|
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:
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
self._items = []
|
||||||
|
self._index = -1
|
||||||
|
self.currentTextChanged = _MockSignal()
|
||||||
|
|
||||||
|
def addItem(self, text: str) -> None:
|
||||||
|
self._items.append(text)
|
||||||
|
|
||||||
|
def addItems(self, items):
|
||||||
|
for it in items:
|
||||||
|
self.addItem(it)
|
||||||
|
|
||||||
|
def findText(self, text: str) -> int:
|
||||||
|
try:
|
||||||
|
return self._items.index(text)
|
||||||
|
except ValueError:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def setCurrentIndex(self, idx: int) -> None:
|
||||||
|
if 0 <= idx < len(self._items):
|
||||||
|
self._index = idx
|
||||||
|
self.currentTextChanged.emit(self.currentText())
|
||||||
|
|
||||||
|
def setCurrentText(self, text: str) -> None:
|
||||||
|
idx = self.findText(text)
|
||||||
|
if idx >= 0:
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def currentText(self) -> str:
|
||||||
|
if 0 <= self._index < len(self._items):
|
||||||
|
return self._items[self._index]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
ComboBox = _MockComboBox
|
||||||
|
|||||||
14
functions/test.md
Normal file
14
functions/test.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
mermaid´´´
|
||||||
|
flowchart TD
|
||||||
|
A[Projekt]
|
||||||
|
|
||||||
|
subgraph children[ ]
|
||||||
|
direction TB
|
||||||
|
B[src]
|
||||||
|
C[docs]
|
||||||
|
D[README.md]
|
||||||
|
end
|
||||||
|
|
||||||
|
A --> B
|
||||||
|
A --> C
|
||||||
|
A --> D
|
||||||
324
modules/DataGrabber.py
Normal file
324
modules/DataGrabber.py
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# sn_basis/modules/DataGrabber.py
|
||||||
|
"""
|
||||||
|
DataGrabber module
|
||||||
|
==================
|
||||||
|
|
||||||
|
Leichter Orchestrator, der eine Quelle (Datei, Einzellink, Datenbank)
|
||||||
|
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
|
||||||
|
und der ExcelImporter vorhanden und importierbar sind. Es enthält
|
||||||
|
keine Fallbacks oder defensive Exception-Handling-Pfade für fehlende
|
||||||
|
Prüfer-Module — fehlende Komponenten führen zu Import- oder Laufzeitfehlern,
|
||||||
|
die bewusst nicht unterdrückt werden.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Any,
|
||||||
|
Mapping,
|
||||||
|
Iterable,
|
||||||
|
Dict,
|
||||||
|
Protocol,
|
||||||
|
Literal,
|
||||||
|
Tuple,
|
||||||
|
List,
|
||||||
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
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.linkpruefer import Linkpruefer
|
||||||
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
|
from sn_basis.modules.excel_importer import ExcelImporter
|
||||||
|
|
||||||
|
|
||||||
|
SourceType = Literal["file", "link", "database", "unknown"]
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
DataGrabber orchestriert das Einlesen einer Quelle und die Übergabe an Prüfer.
|
||||||
|
|
||||||
|
Diese vereinfachte Implementierung erwartet, dass alle Prüferklassen und
|
||||||
|
der ExcelImporter vorhanden sind. Es gibt keine defensive Logik für
|
||||||
|
fehlende Komponenten.
|
||||||
|
|
||||||
|
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__(
|
||||||
|
self,
|
||||||
|
pruefmanager: Pruefmanager,
|
||||||
|
*,
|
||||||
|
datei_pruefer_cls=Dateipruefer,
|
||||||
|
link_pruefer: Linkpruefer,
|
||||||
|
layer_pruefer: Layerpruefer,
|
||||||
|
stil_pruefer: Stilpruefer,
|
||||||
|
) -> None:
|
||||||
|
# Pruefmanager ist verpflichtend
|
||||||
|
self.pruefmanager: Pruefmanager = pruefmanager
|
||||||
|
|
||||||
|
# Dateipruefer-Klasse (wird zur Laufzeit mit einem Pfad instanziert)
|
||||||
|
self._datei_pruefer_cls = datei_pruefer_cls
|
||||||
|
|
||||||
|
# Prüfer-Instanzen (werden direkt verwendet)
|
||||||
|
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
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Source Management
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def set_source(self, source: str) -> None:
|
||||||
|
"""
|
||||||
|
Setzt die Quelle für den DataGrabber.
|
||||||
|
|
||||||
|
Die Quelle ist ein String, der entweder ein lokaler Dateipfad,
|
||||||
|
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"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Excel-Verarbeitung
|
||||||
|
#Es werden alle Werte ohne Prüfung der Links, Pfade oder Stile geladen, da verschiedene Plugins verschiedene xlsx-Strukturen haben können
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def process_excel_source(self, filepath: str) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]:
|
||||||
|
"""
|
||||||
|
Liest eine Excel-Datei (.xlsx/.xls) mit dem ExcelImporter und gibt ein Dict
|
||||||
|
mit den Zeilen zurück sowie das vom Pruefmanager verarbeitete pruef_ergebnis.
|
||||||
|
|
||||||
|
Rückgabe
|
||||||
|
-------
|
||||||
|
- (data_dict, processed_pruef_ergebnis)
|
||||||
|
data_dict: {'rows': [Mapping,...]} oder None bei Fehlern
|
||||||
|
processed_pruef_ergebnis: das Ergebnis, nachdem der Pruefmanager das
|
||||||
|
interne pruef_ergebnis verarbeitet hat.
|
||||||
|
"""
|
||||||
|
importer = ExcelImporter(filepath=filepath, pruefmanager=self.pruefmanager)
|
||||||
|
rows = importer.import_xlsx() # erwartet: List[Mapping[str, Any]]
|
||||||
|
data = {"rows": rows}
|
||||||
|
pe_ok = pruef_ergebnis(ok=True, meldung="Excel erfolgreich gelesen", aktion="ok", kontext=filepath)
|
||||||
|
processed = self.pruefmanager.verarbeite(pe_ok)
|
||||||
|
return data, processed
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Einzellink-Verarbeitung
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def process_single_link(self, link: str) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]:
|
||||||
|
"""
|
||||||
|
Verarbeitet einen Einzellink.
|
||||||
|
|
||||||
|
Ablauf
|
||||||
|
------
|
||||||
|
1. Führt die fachliche Prüfung über self.link_pruefer.pruefe(link) aus.
|
||||||
|
2. Übergibt das Ergebnis an den Pruefmanager (self.pruefmanager.verarbeite).
|
||||||
|
3. Wenn die Prüfung nicht OK ist, wird nur das verarbeitete pruef_ergebnis zurückgegeben.
|
||||||
|
4. Wenn die Prüfung OK ist, erwartet diese Implementierung, dass der Prüfer
|
||||||
|
die Link-Parameter im pruef_ergebnis.kontext als Mapping bereitstellt.
|
||||||
|
Dieses Mapping wird unverändert in ein Dict {'rows': [kontext]} überführt
|
||||||
|
und zusammen mit dem verarbeiteten pruef_ergebnis zurückgegeben.
|
||||||
|
|
||||||
|
Hinweis
|
||||||
|
------
|
||||||
|
Diese Funktion enthält keine Fallbacks, keine normalize-/load-Aufrufe und
|
||||||
|
keine zusätzlichen Validierungen. Der Linkpruefer ist verantwortlich dafür,
|
||||||
|
bei OK ein geeignetes Mapping im pruef_ergebnis.kontext bereitzustellen.
|
||||||
|
"""
|
||||||
|
# 1) Fachliche Prüfung durch den Linkpruefer
|
||||||
|
pe = self.link_pruefer.pruefe(link)
|
||||||
|
|
||||||
|
# 2) Pruefmanager verarbeiten lassen (Logging / UI / Entscheidung)
|
||||||
|
processed = self.pruefmanager.verarbeite(pe)
|
||||||
|
|
||||||
|
# 3) Wenn Prüfung nicht OK -> nur das verarbeitete pruef_ergebnis zurückgeben
|
||||||
|
if not getattr(processed, "ok", False):
|
||||||
|
return None, processed
|
||||||
|
|
||||||
|
# 4) Prüfung OK -> Prüfer liefert die Link-Parameter im pruef_ergebnis.kontext
|
||||||
|
kontext = getattr(pe, "kontext", None)
|
||||||
|
data = {"rows": [kontext]}
|
||||||
|
# Erwartung: kontext ist ein Mapping mit den Link-Parametern.
|
||||||
|
# Wir übergeben es unverändert in das rows-Format.
|
||||||
|
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
|
||||||
@@ -39,7 +39,7 @@ class Dateipruefer:
|
|||||||
|
|
||||||
def _pfad(self, relativer_pfad: str) -> Path:
|
def _pfad(self, relativer_pfad: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Erzeugt einen 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)
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ class Dateipruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung=(
|
meldung=(
|
||||||
"Es wurde keine Datei angegeben. "
|
"Es wurde keine Datei angegeben. "
|
||||||
"Soll eine temporäre Datei erzeugt werden?"
|
"Sollen temporäre Layer erzeugt werden?"
|
||||||
),
|
),
|
||||||
aktion="temporaer_erlaubt",
|
aktion="temporaer_erlaubt",
|
||||||
kontext=None,
|
kontext=None,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"""
|
from __future__ import annotations
|
||||||
sn_basis/modules/Pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
from typing import Optional, Any
|
||||||
Steuert die Nutzerinteraktion über Wrapper.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from sn_basis.functions import (
|
from sn_basis.functions import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
@@ -11,150 +9,196 @@ from sn_basis.functions import (
|
|||||||
set_layer_visible,
|
set_layer_visible,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||||
|
|
||||||
|
|
||||||
class Pruefmanager:
|
class Pruefmanager:
|
||||||
"""
|
"""
|
||||||
Verarbeitet pruef_ergebnis-Objekte und steuert die Nutzerinteraktion.
|
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"):
|
def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None):
|
||||||
self.ui_modus = ui_modus
|
self.ui_modus = ui_modus
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
# Hauptfunktion
|
# Basis-API: Meldungen / Zusammenfassungen
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
def report_error(self, thema: str, meldung: str, *, aktion: Optional[PruefAktion] = None, kontext: Optional[Any] = None) -> None:
|
||||||
|
"""
|
||||||
|
Einheitliche Meldung für Fehler/Warnungen aus dem Core.
|
||||||
|
Keine Rückgabe; dient als zentraler Hook für Logging/UI.
|
||||||
|
"""
|
||||||
|
critical_actions = {
|
||||||
|
"netzwerkfehler",
|
||||||
|
"pruefe_exception",
|
||||||
|
"save_exception",
|
||||||
|
"layer_create_failed",
|
||||||
|
"read_error",
|
||||||
|
"open_error",
|
||||||
|
}
|
||||||
|
warn_actions = {
|
||||||
|
"datei_nicht_gefunden",
|
||||||
|
"pfad_nicht_gefunden",
|
||||||
|
"url_nicht_erreichbar",
|
||||||
|
"falsche_endung",
|
||||||
|
"kein_header",
|
||||||
|
"kein_arbeitsblatt",
|
||||||
|
}
|
||||||
|
|
||||||
|
if aktion in critical_actions:
|
||||||
|
error(thema, meldung)
|
||||||
|
return
|
||||||
|
|
||||||
|
if aktion in warn_actions:
|
||||||
|
warning(thema, meldung)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Default: informative Warnung
|
||||||
|
warning(thema, meldung)
|
||||||
|
|
||||||
|
def report_summary(self, summary: dict) -> None:
|
||||||
|
geladen = summary.get("geladen", [])
|
||||||
|
fehler = summary.get("fehler", {})
|
||||||
|
ausserhalb = summary.get("ausserhalb", [])
|
||||||
|
relevant = summary.get("relevant", [])
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Geladene Dienste: {len(geladen)}\n"
|
||||||
|
f"Relevante Dienste: {len(relevant)}\n"
|
||||||
|
f"Dienste ausserhalb: {len(ausserhalb)}\n"
|
||||||
|
f"Fehler: {len(fehler)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
info("DataGrabber Zusammenfassung", message)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Entscheidungs-API
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
def request_decision(self, pruef_res: Any) -> str:
|
||||||
|
"""
|
||||||
|
Synchronously request a decision from the user (or return a default in headless mode).
|
||||||
|
|
||||||
|
Returns one of:
|
||||||
|
- "abort"
|
||||||
|
- "continue"
|
||||||
|
- "temporaer_erzeugen"
|
||||||
|
- "ignore"
|
||||||
|
"""
|
||||||
|
aktion = getattr(pruef_res, "aktion", None)
|
||||||
|
meldung = getattr(pruef_res, "meldung", str(pruef_res))
|
||||||
|
|
||||||
|
interactive_actions = {
|
||||||
|
"leereingabe_erlaubt",
|
||||||
|
"standarddatei_vorschlagen",
|
||||||
|
"temporaer_erlaubt",
|
||||||
|
"layer_unsichtbar",
|
||||||
|
}
|
||||||
|
|
||||||
|
if aktion in interactive_actions:
|
||||||
|
if self.ui_modus == "qgis":
|
||||||
|
title_map = {
|
||||||
|
"leereingabe_erlaubt": "Ohne Eingabe fortfahren",
|
||||||
|
"standarddatei_vorschlagen": "Standarddatei verwenden",
|
||||||
|
"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":
|
||||||
|
return "abort"
|
||||||
|
|
||||||
|
informational_actions = {
|
||||||
|
"leer",
|
||||||
|
"datei_nicht_gefunden",
|
||||||
|
"pfad_nicht_gefunden",
|
||||||
|
"url_nicht_erreichbar",
|
||||||
|
"netzwerkfehler",
|
||||||
|
"falscher_geotyp",
|
||||||
|
"layer_leer",
|
||||||
|
"falscher_layertyp",
|
||||||
|
"falsches_crs",
|
||||||
|
"felder_fehlen",
|
||||||
|
"datenquelle_unerwartet",
|
||||||
|
"layer_nicht_editierbar",
|
||||||
|
"kein_header",
|
||||||
|
"kein_arbeitsblatt",
|
||||||
|
"read_error",
|
||||||
|
"open_error",
|
||||||
|
}
|
||||||
|
if aktion in informational_actions:
|
||||||
|
return "abort"
|
||||||
|
|
||||||
|
return "abort"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Höhere Abstraktion: verarbeite
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Verarbeitet ein pruef_ergebnis und führt ggf. Nutzerinteraktion durch.
|
Verarbeitet ein pruef_ergebnis-Objekt und führt ggf. Nutzerinteraktion durch.
|
||||||
Rückgabe: neues oder unverändertes pruef_ergebnis.
|
Liefert ein ggf. modifiziertes pruef_ergebnis zurück.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if ergebnis.ok:
|
if ergebnis.ok:
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
aktion = ergebnis.aktion
|
aktion = ergebnis.aktion
|
||||||
kontext = ergebnis.kontext
|
kontext = ergebnis.kontext
|
||||||
|
meldung = ergebnis.meldung
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# Zentrale Meldung
|
||||||
# Allgemeine Aktionen
|
self.report_error(aktion or "pruefung", meldung or "", aktion=aktion, kontext=kontext)
|
||||||
# -----------------------------------------------------
|
|
||||||
|
|
||||||
if aktion == "leer":
|
# Interaktive Entscheidungen
|
||||||
warning("Eingabe fehlt", ergebnis.meldung)
|
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
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "leereingabe_erlaubt":
|
if aktion == "kein_arbeitsblatt":
|
||||||
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
|
warning("Excel-Import", meldung or "")
|
||||||
return pruef_ergebnis(
|
|
||||||
ok=True,
|
|
||||||
meldung="Ohne Eingabe fortgefahren.",
|
|
||||||
aktion="ok",
|
|
||||||
kontext=None,
|
|
||||||
)
|
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "leereingabe_nicht_erlaubt":
|
if aktion in ("read_error", "open_error"):
|
||||||
warning("Eingabe erforderlich", ergebnis.meldung)
|
error("Excel-Import", meldung or "")
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
if aktion == "standarddatei_vorschlagen":
|
|
||||||
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
|
|
||||||
return pruef_ergebnis(
|
|
||||||
ok=True,
|
|
||||||
meldung="Standarddatei wird verwendet.",
|
|
||||||
aktion="ok",
|
|
||||||
kontext=kontext,
|
|
||||||
)
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
if aktion == "temporaer_erlaubt":
|
|
||||||
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
|
|
||||||
return pruef_ergebnis(
|
|
||||||
ok=True,
|
|
||||||
meldung="Temporäre Datei soll erzeugt werden.",
|
|
||||||
aktion="temporaer_erzeugen",
|
|
||||||
kontext=None,
|
|
||||||
)
|
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "datei_nicht_gefunden":
|
if aktion == "datei_nicht_gefunden":
|
||||||
warning("Datei nicht gefunden", ergebnis.meldung)
|
warning("Datei nicht gefunden", meldung or "")
|
||||||
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
|
return ergebnis
|
||||||
|
|
||||||
|
# Spezieller Fall: layer_unsichtbar (falls nicht interaktiv behandelt)
|
||||||
if aktion == "layer_unsichtbar":
|
if aktion == "layer_unsichtbar":
|
||||||
if ask_yes_no("Layer einblenden", ergebnis.meldung):
|
|
||||||
if kontext is not None:
|
if kontext is not None:
|
||||||
try:
|
try:
|
||||||
set_layer_visible(kontext, True)
|
set_layer_visible(kontext, True)
|
||||||
|
return pruef_ergebnis(ok=True, meldung="Layer wurde eingeblendet.", aktion="ok", kontext=kontext)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return ergebnis
|
||||||
|
|
||||||
return pruef_ergebnis(
|
|
||||||
ok=True,
|
|
||||||
meldung="Layer wurde eingeblendet.",
|
|
||||||
aktion="ok",
|
|
||||||
kontext=kontext,
|
|
||||||
)
|
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "falscher_geotyp":
|
# Standard: keine Änderung
|
||||||
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
|
return ergebnis
|
||||||
|
|||||||
91
modules/excel_importer.py
Normal file
91
modules/excel_importer.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# sn_plan41/modules/excel_importer.py
|
||||||
|
import os
|
||||||
|
from typing import Optional, Iterable, Mapping, Any, List, cast
|
||||||
|
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl.workbook.workbook import Workbook
|
||||||
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
|
|
||||||
|
from sn_basis.modules.Dateipruefer import Dateipruefer
|
||||||
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
class ExcelImporter:
|
||||||
|
"""
|
||||||
|
Excel-Importer für Linklisten, verwendet Dateipruefer und Pruefmanager zur Meldungsbehandlung.
|
||||||
|
|
||||||
|
- Der Aufrufer übergibt einen konkreten Dateipfad.
|
||||||
|
- Vor dem Öffnen wird der Pfad mit Dateipruefer geprüft.
|
||||||
|
- Link- und Stilprüfungen erfolgen nicht hier, sondern im DataGrabber.
|
||||||
|
- Nach dem Ladevorgang wird die Arbeitsmappe geschlossen, damit die Datei vom OS freigegeben wird.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filepath: str, pruefmanager: Pruefmanager):
|
||||||
|
if not filepath:
|
||||||
|
raise ValueError("ExcelImporter benötigt einen gültigen Dateipfad.")
|
||||||
|
if pruefmanager is None:
|
||||||
|
raise ValueError("ExcelImporter benötigt einen Pruefmanager.")
|
||||||
|
self.filepath = filepath
|
||||||
|
self.pruefmanager = pruefmanager
|
||||||
|
|
||||||
|
def import_xlsx(self) -> List[Mapping[str, Any]]:
|
||||||
|
"""
|
||||||
|
Liest die Excel-Datei und gibt eine Liste von Dicts (Zeilen) zurück.
|
||||||
|
Bei Prüf- oder Leseproblemen wird der Pruefmanager zur Verarbeitung des pruef_ergebnis aufgerufen.
|
||||||
|
Im Fehlerfall wird eine leere Liste zurückgegeben.
|
||||||
|
"""
|
||||||
|
# 1) Dateiprüfung über Dateipruefer
|
||||||
|
datei_pruefer = Dateipruefer(pfad=self.filepath, temporaer_erlaubt=False)
|
||||||
|
ergebnis: pruef_ergebnis = datei_pruefer.pruefe()
|
||||||
|
ergebnis = self.pruefmanager.verarbeite(ergebnis)
|
||||||
|
|
||||||
|
if not ergebnis.ok:
|
||||||
|
return []
|
||||||
|
|
||||||
|
workbook: Optional[Workbook] = None
|
||||||
|
try:
|
||||||
|
workbook = load_workbook(filename=self.filepath, data_only=True)
|
||||||
|
|
||||||
|
# workbook.active kann typmäßig als Optional angesehen werden; cast/prüfen, damit Pylance weiß, dass sheet ein Worksheet ist
|
||||||
|
sheet = workbook.active
|
||||||
|
if sheet is None:
|
||||||
|
pe = pruef_ergebnis(ok=False, meldung=f"Kein aktives Blatt in der Arbeitsmappe: {self.filepath}", aktion="kein_arbeitsblatt", kontext=self.filepath)
|
||||||
|
self.pruefmanager.verarbeite(pe)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Typengranularität für den Linter
|
||||||
|
sheet = cast(Worksheet, sheet)
|
||||||
|
|
||||||
|
# Header aus erster Zeile (als Werte)
|
||||||
|
header_row = next(sheet.iter_rows(min_row=1, max_row=1, values_only=True), None)
|
||||||
|
if not header_row:
|
||||||
|
pe = pruef_ergebnis(ok=False, meldung=f"Excel-Datei enthält keine Header-Zeile: {self.filepath}", aktion="kein_header", kontext=self.filepath)
|
||||||
|
self.pruefmanager.verarbeite(pe)
|
||||||
|
return []
|
||||||
|
|
||||||
|
header = list(header_row)
|
||||||
|
if not header or all(h is None for h in header):
|
||||||
|
pe = pruef_ergebnis(ok=False, meldung=f"Excel-Header ist leer oder ungültig: {self.filepath}", aktion="kein_header", kontext=self.filepath)
|
||||||
|
self.pruefmanager.verarbeite(pe)
|
||||||
|
return []
|
||||||
|
|
||||||
|
ergebnis_list: List[Mapping[str, Any]] = []
|
||||||
|
# Werte-only lesen für Performance und Einfachheit
|
||||||
|
for row in sheet.iter_rows(min_row=2, values_only=True):
|
||||||
|
if row is None:
|
||||||
|
continue
|
||||||
|
# zip stoppt bei kürzerer Länge; das ist beabsichtigt
|
||||||
|
attributes = dict(zip(header, row))
|
||||||
|
ergebnis_list.append(attributes)
|
||||||
|
|
||||||
|
return ergebnis_list
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
pe = pruef_ergebnis(ok=False, meldung=f"Fehler beim Lesen der Excel-Datei '{self.filepath}': {exc}", aktion="read_error", kontext=self.filepath)
|
||||||
|
self.pruefmanager.verarbeite(pe)
|
||||||
|
return []
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if workbook is not None:
|
||||||
|
workbook.close()
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
sn_basis/modules/layerpruefer.py – Prüfung von QGIS-Layern.
|
sn_basis/modules/layerpruefer.py – Prüfung von QGIS-Layern.
|
||||||
Verwendet ausschließlich Wrapper und gibt pruef_ergebnis zurück.
|
Verwendet ausschließlich Wrapper und gibt pruef_ergebnis zurück.
|
||||||
"""
|
"""
|
||||||
|
from typing import Optional, Any
|
||||||
from sn_basis.functions import (
|
from sn_basis.functions import (
|
||||||
layer_exists,
|
layer_exists,
|
||||||
get_layer_geometry_type,
|
get_layer_geometry_type,
|
||||||
@@ -26,7 +26,7 @@ class Layerpruefer:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
layer,
|
layer:Optional[Any]=None,
|
||||||
erwarteter_geotyp: str | None = None,
|
erwarteter_geotyp: str | None = None,
|
||||||
muss_sichtbar_sein: bool = False,
|
muss_sichtbar_sein: bool = False,
|
||||||
erwarteter_layertyp: str | None = None,
|
erwarteter_layertyp: str | None = None,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Linkpruefer:
|
|||||||
|
|
||||||
def _pfad(self, relativer_pfad: str) -> Path:
|
def _pfad(self, relativer_pfad: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis.
|
Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis.
|
||||||
"""
|
"""
|
||||||
if not self.basis:
|
if not self.basis:
|
||||||
return Path(relativer_pfad)
|
return Path(relativer_pfad)
|
||||||
@@ -79,7 +79,7 @@ class Linkpruefer:
|
|||||||
|
|
||||||
def _pruefe_url(self, url: str) -> pruef_ergebnis:
|
def _pruefe_url(self, url: str) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Prüft eine URL über einen HEAD‑Request.
|
Prüft eine URL über einen HEAD-Request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
reply = network_head(url)
|
reply = network_head(url)
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
"""
|
from __future__ import annotations
|
||||||
sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Optional, Literal
|
from typing import Any, Optional, Literal
|
||||||
|
|
||||||
|
# Erweitertes Literal mit allen erlaubten Aktionen (PruefAktion)
|
||||||
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
|
|
||||||
# Erweiterbar ohne Umbau der Klasse.
|
|
||||||
PruefAktion = Literal[
|
PruefAktion = Literal[
|
||||||
"ok",
|
"ok",
|
||||||
"leer",
|
"leer",
|
||||||
@@ -16,34 +10,52 @@ PruefAktion = Literal[
|
|||||||
"leereingabe_nicht_erlaubt",
|
"leereingabe_nicht_erlaubt",
|
||||||
"standarddatei_vorschlagen",
|
"standarddatei_vorschlagen",
|
||||||
"temporaer_erlaubt",
|
"temporaer_erlaubt",
|
||||||
|
"temporaer_erzeugen",
|
||||||
"datei_nicht_gefunden",
|
"datei_nicht_gefunden",
|
||||||
"kein_dateipfad",
|
"kein_dateipfad",
|
||||||
"pfad_nicht_gefunden",
|
"pfad_nicht_gefunden",
|
||||||
"url_nicht_erreichbar",
|
"url_nicht_erreichbar",
|
||||||
"netzwerkfehler",
|
"netzwerkfehler",
|
||||||
"falscher_layertyp",
|
"layer_nicht_gefunden",
|
||||||
|
"layer_unsichtbar",
|
||||||
"falscher_geotyp",
|
"falscher_geotyp",
|
||||||
"layer_leer",
|
"layer_leer",
|
||||||
|
"falscher_layertyp",
|
||||||
"falsches_crs",
|
"falsches_crs",
|
||||||
"felder_fehlen",
|
"felder_fehlen",
|
||||||
"datenquelle_unerwartet",
|
"datenquelle_unerwartet",
|
||||||
"layer_nicht_editierbar",
|
"layer_nicht_editierbar",
|
||||||
"temporaer_erzeugen",
|
|
||||||
"stil_nicht_anwendbar",
|
|
||||||
"layer_unsichtbar",
|
|
||||||
"layer_nicht_gefunden",
|
|
||||||
"unbekannt",
|
|
||||||
"stil_anwendbar",
|
|
||||||
"falsche_endung",
|
"falsche_endung",
|
||||||
|
# Excel / Import-spezifische Aktionen
|
||||||
|
"kein_header",
|
||||||
|
"kein_arbeitsblatt",
|
||||||
|
"read_error",
|
||||||
|
"open_error",
|
||||||
|
# Generische Prüf-/Speicher-Aktionen
|
||||||
|
"pruefe_exception",
|
||||||
|
"save_exception",
|
||||||
|
"save_not_implemented",
|
||||||
|
"stil_not_implemented",
|
||||||
|
"datei_unbekannt",
|
||||||
|
"needs_user_action",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class pruef_ergebnis:
|
class pruef_ergebnis:
|
||||||
|
"""
|
||||||
|
Einheitliches Ergebnisobjekt für Prüfer.
|
||||||
|
- ok: True wenn Prüfung bestanden
|
||||||
|
- meldung: menschenlesbare Meldung
|
||||||
|
- aktion: maschinenlesbarer Aktionscode (PruefAktion)
|
||||||
|
- kontext: optionaler Zusatzkontext (z. B. Pfad, Layer-Objekt)
|
||||||
|
"""
|
||||||
ok: bool
|
ok: bool
|
||||||
meldung: str
|
meldung: Optional[str] = None
|
||||||
aktion: PruefAktion
|
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):
|
||||||
|
self.ok = ok
|
||||||
|
self.meldung = meldung
|
||||||
|
self.aktion = aktion
|
||||||
|
self.kontext = kontext
|
||||||
|
|||||||
Reference in New Issue
Block a user