Feature: Timeout-Einstellung und asynchrone DB-Abfrage mit Abbrechen-Dialog
DB-Abfragen laufen nun in einem Hintergrund-Thread mit QProgressDialog, sodass die UI nicht mehr einfriert. connect_timeout wird als konfigurierbarer Parameter (1-300s, Standard: 10) im Connection-String übergeben. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,7 @@ class PostgreSqlDb(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
ssl_mode: SSLMode = SSLMode.PREFER
|
ssl_mode: SSLMode = SSLMode.PREFER
|
||||||
|
timeout: int = 10
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel):
|
class Project(BaseModel):
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ class DatabaseTestThread(QThread):
|
|||||||
def run(self):
|
def run(self):
|
||||||
"""Führt den Datenbanktest in einem separaten Thread aus."""
|
"""Führt den Datenbanktest in einem separaten Thread aus."""
|
||||||
try:
|
try:
|
||||||
uri = f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}@{self.connection_data['host']}:{self.connection_data['port']}/{self.connection_data['database']}"
|
timeout = self.connection_data.get("timeout", 10)
|
||||||
|
uri = (
|
||||||
|
f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}"
|
||||||
|
f"@{self.connection_data['host']}:{self.connection_data['port']}"
|
||||||
|
f"/{self.connection_data['database']}"
|
||||||
|
f"?connect_timeout={timeout}"
|
||||||
|
)
|
||||||
|
|
||||||
# Datenbankverbindung testen
|
# Datenbankverbindung testen
|
||||||
r = pl.read_database_uri(
|
r = pl.read_database_uri(
|
||||||
@@ -106,6 +112,7 @@ class PostgreSqlConfigDialog(QDialog):
|
|||||||
self.ui.usernameEdit.setText(data.get("username", ""))
|
self.ui.usernameEdit.setText(data.get("username", ""))
|
||||||
self.ui.passwordEdit.setText(data.get("password", ""))
|
self.ui.passwordEdit.setText(data.get("password", ""))
|
||||||
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
|
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
|
||||||
|
self.ui.timeoutSpinBox.setValue(data.get("timeout", 10))
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
"""Gibt die eingegebenen Daten zurück."""
|
"""Gibt die eingegebenen Daten zurück."""
|
||||||
@@ -126,4 +133,5 @@ class PostgreSqlConfigDialog(QDialog):
|
|||||||
"username": self.ui.usernameEdit.text().strip(),
|
"username": self.ui.usernameEdit.text().strip(),
|
||||||
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein
|
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein
|
||||||
"ssl_mode": self.ui.sslModeComboBox.currentText(),
|
"ssl_mode": self.ui.sslModeComboBox.currentText(),
|
||||||
|
"timeout": self.ui.timeoutSpinBox.value(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,27 @@
|
|||||||
</item>
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="timeoutLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Timeout (s):</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="7" column="1">
|
||||||
|
<widget class="QSpinBox" name="timeoutSpinBox">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>300</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>10</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="1">
|
||||||
<widget class="QPushButton" name="testConnectionButton">
|
<widget class="QPushButton" name="testConnectionButton">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Verbindung testen</string>
|
<string>Verbindung testen</string>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
|
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.9.1
|
## Created by: Qt User Interface Compiler version 6.10.1
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -110,10 +110,23 @@ class Ui_PostgreSqlConfigDialog(object):
|
|||||||
|
|
||||||
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
|
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
|
||||||
|
|
||||||
|
self.timeoutLabel = QLabel(PostgreSqlConfigDialog)
|
||||||
|
self.timeoutLabel.setObjectName(u"timeoutLabel")
|
||||||
|
|
||||||
|
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.timeoutLabel)
|
||||||
|
|
||||||
|
self.timeoutSpinBox = QSpinBox(PostgreSqlConfigDialog)
|
||||||
|
self.timeoutSpinBox.setObjectName(u"timeoutSpinBox")
|
||||||
|
self.timeoutSpinBox.setMinimum(1)
|
||||||
|
self.timeoutSpinBox.setMaximum(300)
|
||||||
|
self.timeoutSpinBox.setValue(10)
|
||||||
|
|
||||||
|
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.timeoutSpinBox)
|
||||||
|
|
||||||
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
|
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
|
||||||
self.testConnectionButton.setObjectName(u"testConnectionButton")
|
self.testConnectionButton.setObjectName(u"testConnectionButton")
|
||||||
|
|
||||||
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
|
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout.addLayout(self.formLayout)
|
self.verticalLayout.addLayout(self.formLayout)
|
||||||
@@ -151,6 +164,7 @@ class Ui_PostgreSqlConfigDialog(object):
|
|||||||
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
|
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
|
||||||
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
|
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
|
||||||
|
|
||||||
|
self.timeoutLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Timeout (s):", None))
|
||||||
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
|
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
|
||||||
# retranslateUi
|
# retranslateUi
|
||||||
|
|
||||||
|
|||||||
+86
-29
@@ -10,13 +10,35 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import polars as pl
|
import polars as pl
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtCore import QThread, Signal, Qt
|
||||||
|
from PySide6.QtWidgets import QMessageBox, QProgressDialog
|
||||||
|
|
||||||
from conf import app_settings, TreeNode, XslFile
|
from conf import app_settings, TreeNode, XslFile
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseQueryThread(QThread):
|
||||||
|
"""Thread für asynchrone Datenbankabfragen."""
|
||||||
|
|
||||||
|
query_completed = Signal(object) # pl.DataFrame
|
||||||
|
query_failed = Signal(str) # Fehlermeldung
|
||||||
|
|
||||||
|
def __init__(self, sql_query, connection_string):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_query = sql_query
|
||||||
|
self.connection_string = connection_string
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
df = pl.read_database_uri(self.sql_query, self.connection_string, engine="connectorx").sort(
|
||||||
|
["reporttyp_bez", "report_bez", "repfile_bez"]
|
||||||
|
)
|
||||||
|
self.query_completed.emit(df)
|
||||||
|
except Exception as e:
|
||||||
|
self.query_failed.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
class DatabaseMixin:
|
class DatabaseMixin:
|
||||||
"""
|
"""
|
||||||
Mixin für Datenbank-Operationen.
|
Mixin für Datenbank-Operationen.
|
||||||
@@ -31,7 +53,7 @@ class DatabaseMixin:
|
|||||||
def on_load_from_fn2_clicked(self):
|
def on_load_from_fn2_clicked(self):
|
||||||
"""
|
"""
|
||||||
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
|
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
|
||||||
Führt SQL-Abfrage aus und aktualisiert die Projekt-Nodes.
|
Startet SQL-Abfrage asynchron mit Fortschrittsdialog.
|
||||||
"""
|
"""
|
||||||
logger.debug("Button 'lade aus FN2' wurde geklickt!")
|
logger.debug("Button 'lade aus FN2' wurde geklickt!")
|
||||||
|
|
||||||
@@ -52,29 +74,68 @@ class DatabaseMixin:
|
|||||||
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
|
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Führe SQL-Abfrage aus
|
# SQL-Abfrage und Connection-String vorbereiten
|
||||||
df = self._execute_sql_query(db_config)
|
sql_query, connection_string = self._prepare_sql_query(db_config)
|
||||||
if df is None:
|
if sql_query is None:
|
||||||
return # Fehler bereits angezeigt
|
return # Fehler bereits angezeigt
|
||||||
|
|
||||||
# Verarbeite die Daten wie in readCsv.py
|
# Fortschrittsdialog erstellen
|
||||||
new_nodes = self._process_sql_data(df)
|
self._db_progress = QProgressDialog("Verbinde mit Datenbank...", "Abbrechen", 0, 0, self)
|
||||||
|
self._db_progress.setWindowTitle("Datenbank-Abfrage")
|
||||||
|
self._db_progress.setWindowModality(Qt.WindowModal)
|
||||||
|
self._db_progress.setMinimumDuration(0)
|
||||||
|
|
||||||
# Merge mit vorhandenen Nodes
|
# Query-Thread erstellen und starten
|
||||||
self._merge_nodes_with_existing(new_nodes)
|
self._db_query_thread = DatabaseQueryThread(sql_query, connection_string)
|
||||||
|
self._db_query_thread.query_completed.connect(self._on_db_query_completed)
|
||||||
# Speichere die aktualisierten Projekt-Einstellungen
|
self._db_query_thread.query_failed.connect(self._on_db_query_failed)
|
||||||
self._save_project_settings()
|
self._db_query_thread.finished.connect(self._cleanup_db_query)
|
||||||
|
self._db_progress.canceled.connect(self._on_db_query_canceled)
|
||||||
# Lade das Projekt neu
|
self._db_query_thread.start()
|
||||||
self._load_nodes_to_tree()
|
|
||||||
|
|
||||||
# QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Laden aus FN2: {e}")
|
logger.error(f"Fehler beim Laden aus FN2: {e}")
|
||||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
|
||||||
|
|
||||||
|
def _on_db_query_completed(self, df):
|
||||||
|
"""Wird aufgerufen, wenn die Datenbankabfrage erfolgreich war."""
|
||||||
|
# Ignoriere Ergebnis, falls der Benutzer abgebrochen hat
|
||||||
|
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_nodes = self._process_sql_data(df)
|
||||||
|
self._merge_nodes_with_existing(new_nodes)
|
||||||
|
self._save_project_settings()
|
||||||
|
self._load_nodes_to_tree()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Verarbeiten der DB-Daten: {e}")
|
||||||
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Verarbeiten der Daten:\n{str(e)}")
|
||||||
|
|
||||||
|
def _on_db_query_failed(self, error_msg):
|
||||||
|
"""Wird aufgerufen, wenn die Datenbankabfrage fehlgeschlagen ist."""
|
||||||
|
if hasattr(self, "_db_progress") and self._db_progress.wasCanceled():
|
||||||
|
return
|
||||||
|
|
||||||
|
error = f"Fehler beim Ausführen der SQL-Abfrage: {error_msg}"
|
||||||
|
logger.error(error)
|
||||||
|
QMessageBox.critical(self, "Fehler", error)
|
||||||
|
|
||||||
|
def _on_db_query_canceled(self):
|
||||||
|
"""Wird aufgerufen, wenn der Benutzer die Abfrage abbricht."""
|
||||||
|
logger.info("Datenbankabfrage vom Benutzer abgebrochen")
|
||||||
|
|
||||||
|
def _cleanup_db_query(self):
|
||||||
|
"""Räumt nach der Datenbankabfrage auf."""
|
||||||
|
if hasattr(self, "_db_progress"):
|
||||||
|
self._db_progress.canceled.disconnect(self._on_db_query_canceled)
|
||||||
|
self._db_progress.close()
|
||||||
|
self._db_progress.deleteLater()
|
||||||
|
del self._db_progress
|
||||||
|
if hasattr(self, "_db_query_thread"):
|
||||||
|
self._db_query_thread.deleteLater()
|
||||||
|
del self._db_query_thread
|
||||||
|
|
||||||
def _get_database_config(self, db_id):
|
def _get_database_config(self, db_id):
|
||||||
"""
|
"""
|
||||||
Holt die Datenbank-Konfiguration anhand der ID.
|
Holt die Datenbank-Konfiguration anhand der ID.
|
||||||
@@ -90,29 +151,27 @@ class DatabaseMixin:
|
|||||||
return db
|
return db
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _execute_sql_query(self, db_config):
|
def _prepare_sql_query(self, db_config):
|
||||||
"""
|
"""
|
||||||
Führt die SQL-Abfrage aus der data.sql Datei aus.
|
Bereitet SQL-Abfrage und Connection-String vor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_config: PostgreSQL-Datenbank-Konfiguration
|
db_config: PostgreSQL-Datenbank-Konfiguration
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
pl.DataFrame|None: Die Abfrageergebnisse oder None bei Fehler
|
tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Lade SQL-Abfrage aus Datei
|
|
||||||
sql_file_path = Path("src/res/data.sql")
|
sql_file_path = Path("src/res/data.sql")
|
||||||
if not sql_file_path.exists():
|
if not sql_file_path.exists():
|
||||||
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
|
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
with open(sql_file_path, "r", encoding="utf-8") as f:
|
with open(sql_file_path, "r", encoding="utf-8") as f:
|
||||||
sql_query = f.read()
|
sql_query = f.read()
|
||||||
|
|
||||||
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
||||||
|
|
||||||
# Verbindung zur PostgreSQL-Datenbank herstellen
|
|
||||||
connection_string = (
|
connection_string = (
|
||||||
"postgresql://"
|
"postgresql://"
|
||||||
f"{db_config.username}:"
|
f"{db_config.username}:"
|
||||||
@@ -121,19 +180,17 @@ class DatabaseMixin:
|
|||||||
f"{db_config.port}/"
|
f"{db_config.port}/"
|
||||||
f"{db_config.database}?"
|
f"{db_config.database}?"
|
||||||
f"sslmode={db_config.ssl_mode.value}"
|
f"sslmode={db_config.ssl_mode.value}"
|
||||||
|
f"&connect_timeout={db_config.timeout}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
|
logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
|
||||||
|
|
||||||
df = pl.read_database_uri(sql_query, connection_string, engine="connectorx").sort(
|
return sql_query, connection_string
|
||||||
["reporttyp_bez", "report_bez", "repfile_bez"]
|
|
||||||
)
|
|
||||||
return df
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}"
|
error_msg = f"Fehler beim Vorbereiten der SQL-Abfrage: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
QMessageBox.critical(self, "Fehler", error_msg)
|
QMessageBox.critical(self, "Fehler", error_msg)
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
def _process_sql_data(self, df):
|
def _process_sql_data(self, df):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user