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 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 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")
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()