def install_packages(): """ installs base packages via pip, e.g. packaging package :return: True if packages installed successfully else False """ logger = QGISLogHandler("ModuleService") python = ModuleService.get_python() requirements = list("{}>={}".format(key, module_list[key]) for key in module_list.keys()) cmd = [python, "-m", "pip", "install", "--force-reinstall", "--user", "--upgrade"] + requirements try: logger.info("Installing packages", ", ".join(requirements)) logger.info("CMD: {}".format(" ".join(cmd))) result = run(cmd, stdout=PIPE, stderr=STDOUT, check=True) logger.info("installation result", result.stdout.decode()) return True except CalledProcessError as e: logger.error("Package installation failed!") logger.error("RETURN-CODE: {} - CMD: {}".format(e.returncode, e.cmd)) logger.error("OUTPUT: {}".format(e.output)) return False
def check_required_modules(): """ Check all module requirements :return: True is all modules with required versions were found, else false """ logger = QGISLogHandler("ModuleService") # don't display logging to QGIS safed_iface = logger.qgis_iface logger.qgis_iface = None logger.info("Checking required modules") python = ModuleService.get_python() cmd = [python, "-m", "pip", "list", "--format", "json", "--user"] try: logger.info("Checking package versions") logger.info("CMD: {}".format(" ".join(cmd))) result = run(cmd, stdout=PIPE, stderr=STDOUT, check=True) logger.info("run pip info successful") packages = json.loads(result.stdout.decode()) except CalledProcessError as e: # restore QGIS logging logger.qgis_iface = safed_iface logger.error("pip info request failed!") logger.error("RETURN-CODE: {} - CMD: {}".format(e.returncode, e.cmd)) logger.error("OUTPUT: {}".format(e.output)) return False ModuleService.modules = list() for module in module_list: module_found = False for package in packages: if package["name"] != module: continue module_found = True logger.debug("found module {} [{}]".format(package["name"], package["version"])) v1 = version.parse(package["version"]) v2 = version.parse(module_list[module]) if v1 < v2: logger.warn("Module version [{}] differs from required version [{}]".format(v1, v2)) ModuleService.modules.append("{}=={}".format(module, module_list[module])) if not module_found: logger.debug("Module not found, adding {}=={} to install list".format(module, module_list[module])) ModuleService.modules.append("{}=={}".format(module, module_list[module])) if len(ModuleService.modules) > 0: # restore QGIS logging logger.qgis_iface = safed_iface logger.info("Missing packages found: {}".format(ModuleService.modules)) return False logger.info("All packages up to date") # restore QGIS logging logger.qgis_iface = safed_iface return True
class DatabaseController(QObject): """ Controller class for database interaction """ def __init__(self, settings: SettingsDialog = None) -> None: """ Constructor :param settings: settings dialog :return: nothing """ QObject.__init__(self) self.logger = QGISLogHandler(DatabaseController.__name__) self.__db_service = DatabaseService.get_instance() self.__last_db_settings = dict() self.__settings = None self.__config = ConfigHandler() self.settings = settings db_type = self.__config.get("General", "db_type") if db_type != "" and db_type in ["SQLite", "PostgreSQL"]: self.settings.DB_type.setCurrentText(db_type) else: db_type = "SQLite" self.__config.set("General", "db_type", db_type) self.settings.DB_type.setCurrentText(db_type) self.__on_db_type_changed(db_type) # # slots # def __on_db_type_changed(self, db_type: str): """ Slot called, when the database type was changed to show / hide specific elements :param db_type: selected database type :return: nothing :raises ValueError: if type is unknown """ self.logger.debug("Selecting new database type: {}".format(db_type)) if db_type == "SQLite": self.settings.create_DB_button.show() self.settings.select_DB_button.show() self.settings.password_label.hide() self.settings.password.hide() self.settings.username_label.hide() self.settings.username.hide() self.settings.save_password.hide() tempdir = "/tmp" if platform.system( ) == "Darwin" else tempfile.gettempdir() filename = "geology.sqlite" self.settings.database_connection.setPlaceholderText( os.path.join(tempdir, filename)) elif db_type == "PostgreSQL": self.settings.create_DB_button.hide() self.settings.select_DB_button.hide() self.settings.password_label.show() self.settings.password.show() self.settings.username_label.show() self.settings.username.show() if found_keyring: self.settings.save_password.show() else: self.settings.save_password.hide() self.settings.database_connection.setPlaceholderText( "localhost:5432/geology") self.settings.username.setPlaceholderText("postgres") self.settings.password.setPlaceholderText("") else: self.settings.database_connection.setText("") self.settings.username.setText("") self.settings.password.setText("") self.settings.database_connection.setPlaceholderText("") self.settings.username.setPlaceholderText("") self.settings.password.setPlaceholderText("") raise ValueError("Unknown DB Format: {}".format(db_type)) if self.__config.has_section(db_type): self.settings.database_connection.setText( self.__config.get(db_type, "connection")) self.settings.username.setText( self.__config.get(db_type, "username")) self.settings.password.setText("") else: self.settings.database_connection.setText("") self.settings.username.setText("") self.settings.password.setText("") self.__update_db_service() def __on_create_db_clicked(self) -> None: """ slot for creating a new database :return: Nothing """ self.__validate() # noinspection PyCallByClass,PyArgumentList filename = get_file_name( QFileDialog.getSaveFileName( parent=self.settings, caption="Select database file", directory="", filter="Databases(*.db *.sqlite *.data);;Any File Type (*)")) if filename != "": # noinspection PyTypeChecker if os.path.splitext(filename)[-1].lower().lstrip('.') not in [ "db", "data", "sqlite" ]: filename += ".data" self.settings.database_connection.setText(filename) def __on_select_db(self) -> None: """ slot for selecting a sqlite database file and set the result to the related lineedit :return: Nothing """ self.__validate() # noinspection PyCallByClass,PyArgumentList filename = get_file_name( QFileDialog.getOpenFileName( self.settings, "Select database file", "", "Databases(*.db *.sqlite *.data);;Any File Type (*)")) if filename != "": # noinspection PyTypeChecker if os.path.splitext(filename)[-1].lower().lstrip('.') not in [ "db", "data", "sqlite" ]: filename += ".data" self.settings.database_connection.setText(filename) def __on_check_connection(self): """ Check the requested database connection :return: if the connection check was successful :raises ValueError: if database type is unknown """ result = self.__db_service.check_connection() if result == "": self.logger.info("Connection test successful") return True else: self.logger.error("connection test failed", result) return False def __on_save(self): self.__last_db_settings = self.__get_db_settings() self.__update_db_service() if self.__on_check_connection(): self.__update_config() self.settings.accept() else: self.__restore_db_settings(self.__last_db_settings) def __on_cancel(self): self.__on_db_type_changed(self.settings.DB_type.currentText()) self.settings.reject() # # private functions # def __update_db_service(self, _: object = None) -> None: """ Update the database service, if a GUI input element changed :param _: temporary parameter for QLineEdit update :return: Nothing """ self.__db_service.db_type = self.settings.DB_type.currentText() self.__db_service.connection = self.settings.database_connection.text() self.__db_service.username = self.settings.username.text() self.__db_service.password = self.settings.password.text() if self.__db_service.connection == "": self.__db_service.connection = self.settings.database_connection.placeholderText( ) if self.__db_service.username == "": self.__db_service.username = self.settings.username.placeholderText( ) if self.__db_service.password == "" and found_keyring: # empty password ? try request from system keystore self.__db_service.password = keyring.get_password( "Postgres {}".format(self.__db_service.connection), self.__db_service.username) # self.logger.debug("Connection settings:\ndatabase:\t{}\nconnection:\t{}\nusername:\t{}\npassword:\t{}".format( # self.__db_service.db_type, self.__db_service.connection, # self.__db_service.username, self.__db_service.password)) def __get_db_settings(self) -> Dict: """ Returns the current database connection settings as dictionary :return: current database connection settings """ return { "db": self.__db_service.db_type, "connection": self.__db_service.connection, "username": self.__db_service.username, "password": self.__db_service.password } def __restore_db_settings(self, values: Dict) -> None: """ Restores the database connection settings :param values: dictionary with connection settings :return: Nothing """ try: self.__db_service.db_type = values["db"] self.__db_service.connection = values["connection"] self.__db_service.username = values["username"] self.__db_service.password = values["password"] except KeyError as e: self.logger.error("Can't restore database settings: {}", str(e)) def __update_config(self): db_type = self.settings.DB_type.currentText() if db_type == "PostgreSQL": self.__config.set("PostgreSQL", "connection", self.__db_service.connection) self.__config.set("PostgreSQL", "username", self.__db_service.username) if self.settings.save_password.isChecked() and found_keyring: keyring.set_password( "Postgres {}".format(self.__db_service.connection), self.__db_service.username, self.__db_service.password) elif db_type == "SQLite": self.__config.set("SQLite", "connection", self.__db_service.connection) if db_type in ["PostgreSQL", "SQLite"]: self.__config.set("General", "db_type", db_type) self.__on_db_type_changed(self.__config.get("General", "db_type")) def __validate(self): """ Validates, if the service can be executed :return: Nothing :raises """ if self.settings is None: raise AttributeError("No settings dialog is set") if self.__config is None: raise AttributeError("No config is set") # # setter and getter # @property def settings(self) -> SettingsDialog: """ Returns the currently active settings dialog :return: returns the currently active settings dialog """ return self.__settings @settings.setter def settings(self, value: SettingsDialog) -> None: """ Sets the currently active settings dialog :return: returns the currently active settings dialog :raises TypeError: if value is not of type SettingsDialog """ if isinstance(value, SettingsDialog): if self.__settings is not None: self.__settings.create_DB_button.clicked.disconnect( self.__on_create_db_clicked) self.__settings.select_DB_button.clicked.disconnect( self.__on_select_db) self.__settings.DB_type.currentIndexChanged[str].disconnect( self.__on_db_type_changed) self.__settings.save_button.clicked.disconnect(self.__on_save) self.__settings.cancel_button.clicked.disconnect( self.__on_cancel) self.__settings = value self.__settings.create_DB_button.clicked.connect( self.__on_create_db_clicked) self.__settings.select_DB_button.clicked.connect( self.__on_select_db) self.__settings.DB_type.currentIndexChanged[str].connect( self.__on_db_type_changed) self.__settings.save_button.clicked.connect(self.__on_save) self.__settings.cancel_button.clicked.connect(self.__on_cancel) else: raise TypeError( "committed parameter is not of type SettingsDialog")
class ImportViewInterface(QObject): """ interface class defining signals and slots for all import views """ def __init__(self, dock_widget: GeologicalDataProcessingDockWidget) -> None: """ Initialize the view :param dock_widget: current GeologicalDataProcessingDockWidget instance """ self.logger = QGISLogHandler(self.__class__.__name__) self.__combos = dict() self._dwg = dock_widget # initialize user interface self._import_service = ImportService.get_instance() self._import_service.reset_import.connect(self.reset_import) self._import_service.import_columns_changed.connect(self._on_import_columns_changed) self._table_view: QTableView or None = None self._only_number_in_table_view: bool = False self._table_model: PropertyImportModel = PropertyImportModel() self._dwg.start_import_button.clicked.connect(self._on_start_import) self._controller_thread: ImportControllersInterface or None = None super().__init__() def _connect_combo_listener(self): """ connects all combobox elements to the on_selection_changed slot :return: Nothing """ [combo.currentIndexChanged.connect(self.on_selection_changed) for combo in self.__combos] @property def combobox_names(self) -> Dict: """ Returns a dictionary of comboboxes for the current view :return: returns a dictionary of comboboxes for the current view """ return self.__combos @combobox_names.setter def combobox_names(self, combo_dict: Dict) -> None: """ Sets a new dictionary to the combobox list. Disconnects the old ones and connects the new to the on_selection_changed slot :param combo_dict: dictionary with new combobox elements :return: Nothing :raises TypeError: if a dictionary value is not an instance of QComboBox or a key is not a str. Sets an empty dictionary instead """ self._disconnect_selection_changed() for key in combo_dict: if not isinstance(key, str): self.__combos = dict() raise TypeError("{} is not a string".format(str(key))) if not isinstance(combo_dict[key], QComboBox): self.__combos = dict() raise TypeError("{} is not an instance of QComboBox".format(str(key))) self.__combos = combo_dict self._connect_selection_changed() @property def table_view(self) -> QTableView or None: return self._table_view @table_view.setter def table_view(self, widget: QTableView) -> None: if not isinstance(widget, QTableView): raise TypeError("submitted object is not of type QTableView: {}", str(widget)) self._table_model.clear() self._table_view = widget self._table_view.setModel(self._table_model) if self._only_number_in_table_view: self._table_view.setItemDelegate(LogImportDelegate()) else: self._table_view.setItemDelegate(PropertyImportDelegate()) # self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) # self._table_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) @property def dockwidget(self) -> GeologicalDataProcessingDockWidget: """ Returns the current dockwidget :return: the current dockwidget """ return self._dwg # # signals # selection_changed = pyqtSignal(list) """data changed signal which gives the index or name of the changed column and the newly selected text""" start_import = pyqtSignal() """signal to start the data import through an ImportController Thread""" # # slots # def on_selection_changed(self, _=None) -> None: """ Emits the combobox_changed signal with a list of changed text :return: Nothing """ selection_list = [self.combobox_names[key].currentText() for key in self.combobox_names] self.selection_changed.emit(selection_list) if self._table_view is None: self.logger.debug("No list widget specified for additional columns") return if self._only_number_in_table_view: cols = diff(self._import_service.number_columns, selection_list) else: cols = diff(self._import_service.selectable_columns, selection_list) self.logger.debug("selectable_columns: " + str(self._import_service.selectable_columns)) self.logger.debug("selected_cols: " + str(selection_list)) self.logger.debug("additional cols: " + str(cols)) if self._table_view is not None: self._table_view.show() self._table_view.setEnabled(True) self._table_view.clearSelection() self._table_model.clear() for col in cols: property_type = PropertyTypes.FLOAT if col in self._import_service.number_columns else PropertyTypes.STRING self._table_model.add(PropertyImportData(name=col[0], unit=col[1], property_type=property_type)) def _on_import_columns_changed(self) -> None: """ change the import columns :return: Nothing """ self.logger.debug("(Interface) _on_import_columns_changed") self._connect_selection_changed() self.on_selection_changed() def _on_start_import(self) -> None: self.logger.debug("(Interface) _on_start_import") self._update_progress_bar(0) self._dwg.progress_bar_layout.setVisible(True) def _on_import_failed(self, msg: str) -> None: self.logger.debug("(Interface) _on_import_failed") self.__import_finished() self.logger.error("Import failed", msg, to_messagebar=True) def _on_import_finished_with_warnings(self, msg: str) -> None: self.logger.debug("(Interface) _on_import_finished_with_warnings") self.logger.warn("Import finished with warnings", msg, to_messagebar=True) self.__import_finished() def _on_import_successful(self): self.logger.debug("(Interface) _on_import_successful") self.logger.info("Import successful", to_messagebar=True) self.__import_finished() def _on_cancel_import(self): self._controller_thread.cancel_import("Import canceled by user") def __import_finished(self): self._dwg.progress_bar_layout.setVisible(False) self._disconnect_thread() self._controller_thread.wait(2000) self._controller_thread = None # # public functions # def combobox_data(self, index: int or str) -> str: """ Returns the currently selected item of the gui element with the given index :param index: index of the requested gui element as integer or string :return: Returns the data at the given index :raises IndexError: if index is not part in the available list """ if isinstance(index, int): index = [self.combobox_names.keys()][index] else: index = str(index) if index not in self.combobox_names: raise IndexError("{} is not available".format(index)) return self.combobox_names[index].currentText() def get_name(self, index: int) -> str or None: """ Returns the name of the combobox with the given index :param index: index of the requested combobox :return: Returns the name of the combobox with the given index :raises IndexError: if the requested index is not in the list :raises ValueError: if the index is not convertible to an integer """ index = int(index) if 0 <= index < len(self.combobox_names.keys()): return list(self.combobox_names.keys())[0] def get_names(self): """ Returns a list of the combobox names :return: Returns a list of the combobox names """ return list(self.combobox_names.keys()) def set_combobox_data(self, index: int or str, values: List[str], default_index: int = 0) -> None: """ Sets the committed values list to the gui combobox elements for the given index :param index: index of the requested gui element as integer or string :param values: new values for the combo boxes as a list of strings :param default_index: default selected index. If no default value is given, or the index is not part of the list, the first entry will be selected by default :return: Returns, if the data setting was successful :raises IndexError: if index is not part in the available list :raises TypeError: if default_index is not an instance of int """ if isinstance(index, int): index = [self.combobox_names.keys()][index] else: index = str(index) if index not in self.combobox_names: raise IndexError("{} is not available".format(index)) if not isinstance(default_index, int): raise TypeError("default_index({}) is not an instance of int!".format(default_index)) self.combobox_names[index].clear() for item in values: self.combobox_names[index].addItem(str(item)) if not (0 <= default_index <= len(values)): default_index = 0 self.combobox_names[index].setCurrentIndex(default_index) def reset_import(self) -> None: """ Clears all import combo boxes, in case of a failure :return: Nothing """ [self.set_combobox_data(name, []) for name in self.get_names()] def get_property_columns(self) -> List[PropertyImportData]: selection = set([x.row() for x in self._table_view.selectedIndexes()]) self.logger.debug("selected rows indices: {}".format(selection)) erg = [self._table_model.row(x) for x in selection] self.logger.debug("Selection:") [self.logger.debug("\t{}".format(x)) for x in erg] return erg # # protected functions # def _update_progress_bar(self, value): """ slot to set the current progress bar value :param value: value in percent :return: nothing """ self.logger.debug("Update progressbar with value {} called".format(value)) if value < 0: self.dockwidget.progress_bar.setValue(0) elif value > 100: self.dockwidget.progress_bar.setValue(100) else: self.dockwidget.progress_bar.setValue(int(value)) def _connect_selection_changed(self): self.logger.debug("_connect_selection_changed") [self.__combos[key].currentTextChanged.connect(self.on_selection_changed) for key in self.__combos] def _disconnect_selection_changed(self): for key in self.__combos: try: self.__combos[key].currentTextChanged.disconnect(self.on_selection_changed) except TypeError: # not connected pass def _connect_thread(self): self._controller_thread.import_finished.connect(self._on_import_successful) self._controller_thread.import_failed.connect(self._on_import_failed) self._controller_thread.import_finished_with_warnings.connect(self._on_import_finished_with_warnings) self._controller_thread.update_progress.connect(self._update_progress_bar) self._dwg.cancel_import.clicked.connect(self._on_cancel_import) def _disconnect_thread(self): self._controller_thread.import_finished.disconnect(self._on_import_successful) self._controller_thread.import_failed.disconnect(self._on_import_failed) self._controller_thread.import_finished_with_warnings.disconnect(self._on_import_finished_with_warnings) self._controller_thread.update_progress.disconnect(self._update_progress_bar) self._dwg.cancel_import.clicked.disconnect(self._on_cancel_import)
class ImportControllersInterface(QThread): """ Basic interface for all import_tests controller """ def __init__(self, data: Dict, selection: Dict, properties: List[PropertyImportData]) -> None: """ :param data: import data parsed from the file to import :param selection: dictionary of selected columns """ super().__init__() self._logger = QGISLogHandler(self.__class__.__name__) self._data: Dict = data self._selection: Dict = selection self._properties: List[PropertyImportData] = properties self._mutex = QMutex() self._cancel = False self._message = "" def run(self) -> None: """ Thread execution function to import data :return: Nothing """ pass # # signals # update_progress = pyqtSignal(int) """update progress bar signal. Committed value has to be between 0 and 100""" import_finished = pyqtSignal() """signal emitted, when the import process has finished""" import_finished_with_warnings = pyqtSignal(str) """signal emitted, when the import process has finished with warnings""" import_failed = pyqtSignal(str) """signal emitted, when the import process was canceled or failed through a call of the cancel_import slot""" # # slots # def cancel_import(self, msg: str = "") -> None: """ slot for canceling the import process. A trigger variable will hint the importer to stop at the next possibility. This should ensure the finalization of all started write processes and therefore the integrity of all database objects. The :func:`~GeologicalDataProcessing.controller.import_controller.ImportControllersInterface.import_cancelled` signal will be sent, if the import process was successfully cancelled. :return: Nothing """ self._cancel = True if msg == "": self._message = "Import canceled" else: self._message = msg def _import_done(self, future: Future = None) -> None: """ function called, when import is done or canceled :param future: import executing future object :return: nothing """ self._logger.debug("import done") self._view.dockwidget.progress_bar_layout.setVisible(False) self._view.dockwidget.cancel_import.clicked.disconnect(self._stop_import) if (future is not None) and future.cancelled(): self._logger.warn("future run finished, import cancelled!") elif future is not None: self._logger.info("future run finished, import successful") self._logger.info("QThread finished") if self.__thread is not None: self._logger.debug("waiting for the end...") self.__thread.wait() self._logger.debug("at the end...") self.__thread = None
def run(self) -> None: """Run method that loads and starts the plugin""" if not self.pluginIsActive: self.pluginIsActive = True try: # initialize logger logger = QGISLogHandler() logger.qgis_iface = self.iface logger.save_to_file = True if packages_found == "NO_PACKAGES" or not ModuleService.check_required_modules(): logger.info("installing or updating packages") if not ModuleService.install_packages(): logger.error("package installation failed, please restart QGIS to try again.") else: logger.info("package installation successful, please restart QGIS") msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText("packages installation successful") msg.setInformativeText("Please restart QGIS to use the GeologicalDataProcessing extension") msg.setWindowTitle("package update") msg.exec_() return return else: logger.debug("all required packages up2date") # dockwidget may not exist if: # first run of plugin # removed on close (see self.onClosePlugin method) if self.dockwidget is None: # Create the dockwidget (after translation) and keep reference self.dockwidget = GeologicalDataProcessingDockWidget() if self.settings_dialog is None: self.settings_dialog = SettingsDialog(parent=self.dockwidget) self.settings_dialog.setModal(True) self.dockwidget.settings_button.clicked.connect(self.settings_dialog.exec) # connect to provide cleanup on closing of dockwidget self.dockwidget.closingPlugin.connect(self.onClosePlugin) # show the dockwidget # TODO: fix to allow choice of dock location self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget) self.dockwidget.show() from GeologicalDataProcessing.controller.database_controller import DatabaseController from GeologicalDataProcessing.services.import_service import ImportService from GeologicalDataProcessing.views.import_views import LineImportView, PointImportView, \ WellImportView, PropertyImportView, WellLogImportView ImportService.get_instance(self.dockwidget) # initialize the gui and connect signals and slots self.dockwidget.import_type.currentChanged.connect(self.on_import_type_changed_event) # start tests button # -> only visible and active when the debug flag is True if config.debug: self.dockwidget.start_tests_button.clicked.connect(self.on_start_tests) else: self.dockwidget.start_tests_button.setVisible(False) self.dockwidget.start_tests_separator.setVisible(False) self.dockwidget.progress_bar_layout.setVisible(False) self.__views["import_points"] = PointImportView(self.dockwidget) self.__views["import_lines"] = LineImportView(self.dockwidget) self.__views["import_wells"] = WellImportView(self.dockwidget) self.__views["import_properties"] = PropertyImportView(self.dockwidget) self.__views["import_well_logs"] = WellLogImportView(self.dockwidget) self.__db_controller = DatabaseController(self.settings_dialog) if config.debug: self.dockwidget.import_file.setText( "/Users/stephan/Library/Application Support/QGIS/QGIS3/profiles/" + "default/python/plugins/GeologicalDataProcessing/tests/test_data/point_data.txt") except Exception as e: ExceptionHandler(e).log()