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
|
||||
password: str
|
||||
ssl_mode: SSLMode = SSLMode.PREFER
|
||||
timeout: int = 10
|
||||
|
||||
|
||||
class Project(BaseModel):
|
||||
|
||||
@@ -19,7 +19,13 @@ class DatabaseTestThread(QThread):
|
||||
def run(self):
|
||||
"""Führt den Datenbanktest in einem separaten Thread aus."""
|
||||
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
|
||||
r = pl.read_database_uri(
|
||||
@@ -106,6 +112,7 @@ class PostgreSqlConfigDialog(QDialog):
|
||||
self.ui.usernameEdit.setText(data.get("username", ""))
|
||||
self.ui.passwordEdit.setText(data.get("password", ""))
|
||||
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
|
||||
self.ui.timeoutSpinBox.setValue(data.get("timeout", 10))
|
||||
|
||||
def get_data(self):
|
||||
"""Gibt die eingegebenen Daten zurück."""
|
||||
@@ -126,4 +133,5 @@ class PostgreSqlConfigDialog(QDialog):
|
||||
"username": self.ui.usernameEdit.text().strip(),
|
||||
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein
|
||||
"ssl_mode": self.ui.sslModeComboBox.currentText(),
|
||||
"timeout": self.ui.timeoutSpinBox.value(),
|
||||
}
|
||||
|
||||
@@ -138,7 +138,27 @@
|
||||
</item>
|
||||
</widget>
|
||||
</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">
|
||||
<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">
|
||||
<property name="text">
|
||||
<string>Verbindung testen</string>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
################################################################################
|
||||
## 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!
|
||||
################################################################################
|
||||
@@ -110,10 +110,23 @@ class Ui_PostgreSqlConfigDialog(object):
|
||||
|
||||
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.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)
|
||||
@@ -151,6 +164,7 @@ class Ui_PostgreSqlConfigDialog(object):
|
||||
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", 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))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
+86
-29
@@ -10,13 +10,35 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
"""
|
||||
Mixin für Datenbank-Operationen.
|
||||
@@ -31,7 +53,7 @@ class DatabaseMixin:
|
||||
def on_load_from_fn2_clicked(self):
|
||||
"""
|
||||
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!")
|
||||
|
||||
@@ -52,29 +74,68 @@ class DatabaseMixin:
|
||||
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
|
||||
return
|
||||
|
||||
# Führe SQL-Abfrage aus
|
||||
df = self._execute_sql_query(db_config)
|
||||
if df is None:
|
||||
# SQL-Abfrage und Connection-String vorbereiten
|
||||
sql_query, connection_string = self._prepare_sql_query(db_config)
|
||||
if sql_query is None:
|
||||
return # Fehler bereits angezeigt
|
||||
|
||||
# Verarbeite die Daten wie in readCsv.py
|
||||
new_nodes = self._process_sql_data(df)
|
||||
# Fortschrittsdialog erstellen
|
||||
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
|
||||
self._merge_nodes_with_existing(new_nodes)
|
||||
|
||||
# Speichere die aktualisierten Projekt-Einstellungen
|
||||
self._save_project_settings()
|
||||
|
||||
# Lade das Projekt neu
|
||||
self._load_nodes_to_tree()
|
||||
|
||||
# QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
|
||||
# Query-Thread erstellen und starten
|
||||
self._db_query_thread = DatabaseQueryThread(sql_query, connection_string)
|
||||
self._db_query_thread.query_completed.connect(self._on_db_query_completed)
|
||||
self._db_query_thread.query_failed.connect(self._on_db_query_failed)
|
||||
self._db_query_thread.finished.connect(self._cleanup_db_query)
|
||||
self._db_progress.canceled.connect(self._on_db_query_canceled)
|
||||
self._db_query_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden aus FN2: {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):
|
||||
"""
|
||||
Holt die Datenbank-Konfiguration anhand der ID.
|
||||
@@ -90,29 +151,27 @@ class DatabaseMixin:
|
||||
return db
|
||||
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:
|
||||
db_config: PostgreSQL-Datenbank-Konfiguration
|
||||
|
||||
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:
|
||||
# Lade SQL-Abfrage aus Datei
|
||||
sql_file_path = Path("src/res/data.sql")
|
||||
if not sql_file_path.exists():
|
||||
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:
|
||||
sql_query = f.read()
|
||||
|
||||
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
||||
|
||||
# Verbindung zur PostgreSQL-Datenbank herstellen
|
||||
connection_string = (
|
||||
"postgresql://"
|
||||
f"{db_config.username}:"
|
||||
@@ -121,19 +180,17 @@ class DatabaseMixin:
|
||||
f"{db_config.port}/"
|
||||
f"{db_config.database}?"
|
||||
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}")
|
||||
|
||||
df = pl.read_database_uri(sql_query, connection_string, engine="connectorx").sort(
|
||||
["reporttyp_bez", "report_bez", "repfile_bez"]
|
||||
)
|
||||
return df
|
||||
return sql_query, connection_string
|
||||
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)
|
||||
QMessageBox.critical(self, "Fehler", error_msg)
|
||||
return None
|
||||
return None, None
|
||||
|
||||
def _process_sql_data(self, df):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user