diff --git a/.pylintrc b/.pylintrc index 21d73562..ba45d3b4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -12,7 +12,7 @@ max-module-lines=2000 #ignored-argument-names=_.*|^ignored_|^unused_|rule_args|callback|rei # Catching too general exception Exception -disable=W0718 +disable=W0718,fixme # C++ modules of PyQt6 not visible to pylint extension-pkg-whitelist=PyQt6 diff --git a/ibridgesgui/IrodsInfo.py b/ibridgesgui/IrodsInfo.py deleted file mode 100644 index cbe922a8..00000000 --- a/ibridgesgui/IrodsInfo.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provide the GUI with iRODS information - -""" -import sys - -import PyQt6 -import PyQt6.QtWidgets -import PyQt6.uic -from ibridges.resources import Resources - -import ibridgesgui as gui -from ibridgesgui.gui_utils import UI_FILE_DIR, populate_table - - -class IrodsInfo(PyQt6.QtWidgets.QWidget, - gui.ui_files.tabInfo.Ui_tabInfo): - """Set iRODS information in the GUI - - """ - - def __init__(self, session): - super().__init__() - if getattr(sys, 'frozen', False): - super().setupUi(self) - else: - PyQt6.uic.loadUi(UI_FILE_DIR / "tabInfo.ui", self) - self.session = session - - self.refreshButton.clicked.connect(self.refresh_info) - self.refresh_info() - - def refresh_info(self): - """Find and set the information of the connected iRODS system - including the availble top-level resources. - """ - self.rescTable.setRowCount(0) - self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.WaitCursor)) - # irods Zone - self.zoneLabel.setText(self.session.zone) - # irods user - self.userLabel.setText(self.session.username) - # irods user type and groups - user_type, user_groups = self.session.get_user_info() - self.typeLabel.setText(user_type) - self.groupsLabel.setText('\n'.join(user_groups)) - # default resource - self.rescLabel.setText(self.session.default_resc) - # irods server and version - self.serverLabel.setText(self.session.host) - self.versionLabel.setText( - '.'.join((str(num) for num in self.session.server_version))) - # irods resources - resc_info = Resources(self.session).root_resources - populate_table(self.rescTable, len(resc_info[0]), resc_info) - self.rescTable.resizeColumnsToContents() - self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.ArrowCursor)) diff --git a/ibridgesgui/IrodsLogin.py b/ibridgesgui/IrodsLogin.py deleted file mode 100644 index 83a5cfdc..00000000 --- a/ibridgesgui/IrodsLogin.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Pop up Widget for Login. -""" -import sys -from pathlib import Path - -from ibridges import Session -from ibridges.session import LoginError, PasswordError -from PyQt6.QtWidgets import QDialog, QLineEdit -from PyQt6.uic import loadUi - -from ibridgesgui.gui_utils import UI_FILE_DIR -from ibridgesgui.ui_files.irodsLogin import Ui_irodsLogin - - -class IrodsLogin(QDialog, Ui_irodsLogin): - """Definition and initialization of the iRODS login window. - - """ - - def __init__(self, session_dict): - super().__init__() - if getattr(sys, 'frozen', False): - super().setupUi(self) - else: - loadUi(UI_FILE_DIR / "irodsLogin.ui", self) - - self.session_dict = session_dict - self.irods_path = Path('~', '.irods').expanduser() - self._init_envbox() - self.cached_pw = self._init_password() - self._load_gui() - self.setWindowTitle("Connect to iRODS server") - - def _load_gui(self): - """ - - """ - self.connectButton.clicked.connect(self.login_function) - self.cancelButton.clicked.connect(self.close) - self.passwordField.setEchoMode(QLineEdit.EchoMode.Password) - - def _init_envbox(self): - env_jsons = [ - path.name for path in - self.irods_path.glob('irods_environment*json')] - if len(env_jsons) == 0: - self.envError.setText(f'ERROR: no "irods_environment*json" files found in {self.irods_path}') - self.envbox.clear() - self.envbox.addItems(env_jsons) - self.envbox.setCurrentIndex(0) - - def _init_password(self): - #Check if there is a cached password - passwdFile = self.irods_path.joinpath('.irodsA') - if passwdFile.is_file(): - self.passwordField.setText("***********") - return True - return False - - def close(self): - self.done(0) - - def login_function(self): - self.passError.clear() - env_file = self.irods_path.joinpath(self.envbox.currentText()) - try: - if self.cached_pw is True and self.passwordField.text() == "***********": - self.session = Session(irods_env=env_file) - else: - self.session = Session(irods_env=env_file, password=self.passwordField.text()) - self.session_dict['session'] = self.session - self.session.write_pam_password() - self.close() - except LoginError as e: - self.passError.setText("irods_environment.json not setup correctly.") - except PasswordError as e: - self.passError.setText("Wrong password!") - except ConnectionError: - self.passError.setText("Cannot connect to server. Check Internet, host name and port.") diff --git a/ibridgesgui/__main__.py b/ibridgesgui/__main__.py index f69742ba..1f7d7bdc 100755 --- a/ibridgesgui/__main__.py +++ b/ibridgesgui/__main__.py @@ -1,74 +1,79 @@ #!/usr/bin/env python3 """iBridges GUI startup script.""" -import sys import logging +import sys +from pathlib import Path import PyQt6.QtWidgets import PyQt6.uic import setproctitle -from ibridgesgui import ui_files from ibridgesgui.browser import Browser +from ibridgesgui.config import get_log_level, init_logger, set_log_level from ibridgesgui.gui_utils import UI_FILE_DIR from ibridgesgui.info import Info from ibridgesgui.login import Login -from ibridgesgui.config import get_log_level, set_log_level, init_logger +from ibridgesgui.popup_widgets import CheckConfig +from ibridgesgui.ui_files.MainMenu import Ui_MainWindow # Global constants -THIS_APPLICATION = 'ibridges-gui' +THIS_APPLICATION = "ibridges-gui" # Application globals app = PyQt6.QtWidgets.QApplication(sys.argv) -widget = PyQt6.QtWidgets.QStackedWidget() -class MainMenu(PyQt6.QtWidgets.QMainWindow, ui_files.MainMenu.Ui_MainWindow): - """GUI Main Menu""" +class MainMenu(PyQt6.QtWidgets.QMainWindow, Ui_MainWindow): + """Set up the GUI Main Menu.""" - def __init__(self, widget, app_name): + def __init__(self, parent_widget, app_name): + """Initialise the main window.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: - PyQt6.uic.loadUi(UI_FILE_DIR/'MainMenu.ui', self) + PyQt6.uic.loadUi(UI_FILE_DIR/"MainMenu.ui", self) self.logger = logging.getLogger(app_name) + self.irods_path = Path("~", ".irods").expanduser() self.app_name = app_name self.ui_tabs_lookup = { - 'tabBrowser': self.init_browser_tab, + "tabBrowser": self.init_browser_tab, #'tabUpDownload': self.setupTabUpDownload, #'tabDataBundle': self.setupTabDataBundle, #'tabCreateTicket': self.setupTabCreateTicket, #'tabELNData': self.setupTabELNData, #'tabAmberWorkflow': self.setupTabAmberWorkflow, - 'tabInfo': self.init_info_tab + "tabInfo": self.init_info_tab #'tabExample': self.setupTabExample, } - self.widget = widget + self.parent_widget = parent_widget self.session = None self.session_dict = {} self.actionConnect.triggered.connect(self.connect) self.actionExit.triggered.connect(self.exit) self.actionCloseSession.triggered.connect(self.disconnect) + self.actionAdd_configuration.triggered.connect(self.create_env_file) + self.actionCheck_configuration.triggered.connect(self.inspect_env_file) self.tabWidget.setCurrentIndex(0) def disconnect(self): - """Close iRODS session""" - if 'session' in self.session_dict: - session = self.session_dict['session'] - self.logger.info('Disconnecting %s from %s', session.username, session.host) - self.session_dict['session'].close() + """Close iRODS session.""" + if "session" in self.session_dict: + session = self.session_dict["session"] + self.logger.info("Disconnecting %s from %s", session.username, session.host) + self.session_dict["session"].close() self.session_dict.clear() self.tabWidget.clear() def connect(self): - """Create iRODS session""" + """Create iRODS session.""" # Trick to get the session object from the QDialog login_window = Login(self.session_dict, self.app_name) login_window.exec() - if 'session' in self.session_dict: - self.session = self.session_dict['session'] + if "session" in self.session_dict: + self.session = self.session_dict["session"] try: self.setup_tabs() except: @@ -76,10 +81,10 @@ def connect(self): raise def exit(self): - """Quit program""" + """Quit program.""" quit_msg = "Are you sure you want to exit the program?" reply = PyQt6.QtWidgets.QMessageBox.question( - self, 'Message', quit_msg, + self, "Message", quit_msg, PyQt6.QtWidgets.QMessageBox.StandardButton.Yes, PyQt6.QtWidgets.QMessageBox.StandardButton.No) if reply == PyQt6.QtWidgets.QMessageBox.StandardButton.Yes: @@ -88,35 +93,45 @@ def exit(self): pass def setup_tabs(self): - """Init tab view""" + """Init tab view.""" for tab_fun in self.ui_tabs_lookup.values(): tab_fun() #self.ui_tabs_lookup[tab_name]() def init_info_tab(self): - """Create info""" + """Create info.""" irods_info = Info(self.session) self.tabWidget.addTab(irods_info, "Info") def init_browser_tab(self): - """Create browser""" + """Create browser.""" irods_browser = Browser(self.session, self.app_name) self.tabWidget.addTab(irods_browser, "Browser") + def create_env_file(self): + """Populate drop down menu to create a new environment.json.""" + create_widget = CheckConfig(self.logger, self.irods_path) + create_widget.exec() + + def inspect_env_file(self): + """Init drop down menu to inspect an environment.json.""" + create_widget = CheckConfig(self.logger, self.irods_path) + create_widget.exec() + def main(): - """Main function""" + """Call main function.""" setproctitle.setproctitle(THIS_APPLICATION) log_level = get_log_level() if log_level is not None: init_logger(THIS_APPLICATION, log_level) else: - set_log_level('debug') - init_logger(THIS_APPLICATION, 'debug') - - main_app = MainMenu(widget, THIS_APPLICATION) - widget.addWidget(main_app) - widget.show() + set_log_level("debug") + init_logger(THIS_APPLICATION, "debug") + main_widget = PyQt6.QtWidgets.QStackedWidget() + main_app = MainMenu(main_widget, THIS_APPLICATION) + main_widget.addWidget(main_app) + main_widget.show() app.exec() if __name__ == "__main__": diff --git a/ibridgesgui/browser.py b/ibridgesgui/browser.py index cfa79dbc..e0fe0a70 100644 --- a/ibridgesgui/browser.py +++ b/ibridgesgui/browser.py @@ -1,6 +1,6 @@ """Browser tab.""" -import sys import logging +import sys from pathlib import Path import irods.exception @@ -13,29 +13,26 @@ from ibridges.meta import MetaData from ibridges.permissions import Permissions -import ibridgesgui as gui -from ibridgesgui.popup_widgets import CreateCollection from ibridgesgui.gui_utils import ( UI_FILE_DIR, get_coll_dict, get_downloads_dir, get_irods_item, populate_table, + populate_textfield, ) +from ibridgesgui.popup_widgets import CreateCollection +from ibridgesgui.ui_files.tabBrowser import Ui_tabBrowser class Browser(PyQt6.QtWidgets.QWidget, - gui.ui_files.tabBrowser.Ui_tabBrowser): - """Browser view for iRODS session. - - """ + Ui_tabBrowser): + """Browser view for iRODS session.""" def __init__(self, session, app_name): - """Initialize an iRODS browser view. - - """ + """Initialize an iRODS browser view.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: PyQt6.uic.loadUi(UI_FILE_DIR / "tabBrowser.ui", self) @@ -47,33 +44,32 @@ def __init__(self, session, app_name): if self.session.home is not None: root_path = self.session.home else: - root_path = f'/{self.session.zone}/home/{self.session.username}' + root_path = f"/{self.session.zone}/home/{self.session.username}" try: self.root_coll = get_collection(self.session, root_path) except irods.exception.CollectionDoesNotExist: - self.root_coll = get_collection(self.session, f'/{self.session.zone}/home') + self.root_coll = get_collection(self.session, f"/{self.session.zone}/home") except irods.exception.NetworkException: self.errorLabel.setText( - 'iRODS NETWORK ERROR: No Connection, please check network') + "iRODS NETWORK ERROR: No Connection, please check network") except Exception as err: - self.errorLabel.setText('Cannot set root collection. Set "irods_home" in your environment.json') - self.logger.exception('Failed to set iRODS home: %s', err) + self.errorLabel.setText( + 'Cannot set root collection. Set "irods_home" in your environment.json') + self.logger.exception("Failed to set iRODS home: %s", err) self.reset_path() self.browse() def browse(self): - """Initialize browser view GUI elements. - Defines the signals and slots. - """ + """Initialize browser view GUI elements. Define the signals and slots.""" # Main navigation elements self.inputPath.returnPressed.connect(self.load_browser_table) self.refreshButton.clicked.connect(self.load_browser_table) - self.refreshButton.setToolTip('Refresh') + self.refreshButton.setToolTip("Refresh") self.homeButton.clicked.connect(self.reset_path) - self.homeButton.setToolTip('Home') + self.homeButton.setToolTip("Home") self.parentButton.clicked.connect(self.set_parent) - self.parentButton.setToolTip('Parent Coll') + self.parentButton.setToolTip("Parent Coll") # Main manipulation buttons Upload/Download create collection self.UploadButton.clicked.connect(self.file_upload) @@ -100,13 +96,13 @@ def browse(self): def reset_path(self): - """Reset browser table to root path""" + """Reset browser table to root path.""" self.inputPath.setText(self.root_coll.path) self.load_browser_table() def set_parent(self): - """Set browser path to parent of current collection and update browser table""" + """Set browser path to parent of current collection and update browser table.""" current_path = IrodsPath(self.session, self.inputPath.text()) self.inputPath.setText(str(current_path.parent)) self.load_browser_table() @@ -114,7 +110,7 @@ def set_parent(self): # @PyQt6.QtCore.pyqtSlot(PyQt6.QtCore.QModelIndex) def update_path(self, index): - """Takes path from inputPath and loads browser table""" + """Take path from inputPath and loads browser table.""" self.errorLabel.clear() row = index.row() irods_path = self._get_item_path(row) @@ -123,14 +119,14 @@ def update_path(self, index): self.load_browser_table() def create_collection(self): - """Create a new collection in current collection""" + """Create a new collection in current collection.""" parent = IrodsPath(self.session, "/"+self.inputPath.text().strip("/")) coll_widget = CreateCollection(parent, self.logger) coll_widget.exec() self.load_browser_table() def folder_upload(self): - """Select a folder and upload""" + """Select a folder and upload.""" self.errorLabel.clear() select_dir = PyQt6.QtWidgets.QFileDialog.getExistingDirectory(self, "Select Directory") path = self._fs_select(select_dir) @@ -138,7 +134,7 @@ def folder_upload(self): self._upload(path) def file_upload(self): - """Select a file and upload""" + """Select a file and upload.""" self.errorLabel.clear() select_file = PyQt6.QtWidgets.QFileDialog.getOpenFileName(self, "Open Filie") path = self._fs_select(select_file) @@ -147,46 +143,47 @@ def file_upload(self): def download(self): - """Download collection or data object""" + """Download collection or data object.""" self.errorLabel.clear() if self.browserTable.currentRow() == -1: - self.errorLabel.setText('Please select a row from the table first!') + self.errorLabel.setText("Please select a row from the table first!") return if self.browserTable.item(self.browserTable.currentRow(), 1) is not None: item_name = self.browserTable.item(self.browserTable.currentRow(), 1).text() - path = IrodsPath(self.session, '/', *self.inputPath.text().split('/'), item_name) + path = IrodsPath(self.session, "/", *self.inputPath.text().split("/"), item_name) overwrite = self.overwrite.isChecked() download_dir = get_downloads_dir() if overwrite: write = "All data will be updated." else: write = "Only new data will be added." - info = f'Download data:\n{path}\n\nto\n\n{download_dir}\n\n{write}' + info = f"Download data:\n{path}\n\nto\n\n{download_dir}\n\n{write}" + try: if path.exists(): - button_reply = PyQt6.QtWidgets.QMessageBox.question(self, '', info) + button_reply = PyQt6.QtWidgets.QMessageBox.question(self, "", info) if button_reply == PyQt6.QtWidgets.QMessageBox.StandardButton.Yes: if Path(download_dir).joinpath(item_name).exists() and not overwrite: raise FileExistsError self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.BusyCursor)) - self.logger.info('Downloading %s to %s, overwrite %s', path, download_dir, + self.logger.info("Downloading %s to %s, overwrite %s", path, download_dir, str(overwrite)) download(self.session, path, download_dir, overwrite=overwrite) self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.ArrowCursor)) self.errorLabel.setText("Data downloaded to: "+str(download_dir)) else: self.errorLabel.setText( - f'Data {path.parent} does not exist.') + f"Data {path.parent} does not exist.") except FileExistsError: - self.errorLabel.setText(f'Data already exists in {download_dir}.'+\ + self.errorLabel.setText(f"Data already exists in {download_dir}."+\ ' Check "overwrite" to overwrite the data.') except Exception as err: - self.logger.exception('Downloading %s failed: %s', path, err) - self.errorLabel.setText(f'Could not download {path}. Consult the logs.') + self.logger.exception("Downloading %s failed: %s", path, err) + self.errorLabel.setText(f"Could not download {path}. Consult the logs.") def load_browser_table(self): - """Loads main browser table""" + """Load main browser table.""" self.errorLabel.clear() self._clear_view_tabs() obj_path = IrodsPath(self.session, self.inputPath.text()) @@ -194,31 +191,31 @@ def load_browser_table(self): try: coll = get_collection(self.session, obj_path) - coll_data = [('C-', subcoll.name, '', '', - subcoll.create_time.strftime('%d-%m-%Y'), - subcoll.modify_time.strftime('%d-%m-%Y %H:%m')) + coll_data = [("C-", subcoll.name, "", "", + subcoll.create_time.strftime("%d-%m-%Y"), + subcoll.modify_time.strftime("%d-%m-%Y %H:%m")) for subcoll in coll.subcollections] obj_data = [(max(repl[4] for repl in obj_replicas(obj)), obj.name, str(obj.size), - obj.checksum, obj.create_time.strftime('%d-%m-%Y'), - obj.modify_time.strftime('%d-%m-%Y %H:%m')) + obj.checksum, obj.create_time.strftime("%d-%m-%Y"), + obj.modify_time.strftime("%d-%m-%Y %H:%m")) for obj in coll.data_objects] populate_table(self.browserTable, len(coll.data_objects)+len(coll.subcollections), coll_data+obj_data) self.browserTable.resizeColumnsToContents() - except Exception as exception: + except Exception: self.browserTable.setRowCount(0) self.logger.exception("Cannot load browser.") - self.errorLabel.setText(f"Cannot load browser table. Consult the logs.") + self.errorLabel.setText("Cannot load browser table. Consult the logs.") else: self.browserTable.setRowCount(0) self.errorLabel.setText("Collection does not exist.") def fill_info(self): - """Fill lower tabs with info""" + """Fill lower tabs with info.""" self.errorLabel.clear() self._clear_view_tabs() self.deleteSelectionBrowser.clear() @@ -232,27 +229,27 @@ def fill_info(self): self._fill_metadata_tab(irods_path) self._fill_acls_tab(irods_path) self._fill_replicas_tab(irods_path) - except Exception as error: + except Exception: self.logger.exception("Cannot load info tabs.") - self.errorLabel.setText(f"Cannot load info tabs. Consult the logs.") + self.errorLabel.setText("Cannot load info tabs. Consult the logs.") def set_icat_meta(self): - """Button metadata set""" + """Button metadata set.""" try: self._metadata_edits("set") except Exception as error: self.errorLabel.setText(repr(error)) def add_icat_meta(self): - """Button metadata add""" + """Button metadata add.""" try: self._metadata_edits("add") except Exception as error: self.errorLabel.setText(repr(error)) def delete_icat_meta(self): - """Button metadata delete""" + """Button metadata delete.""" try: self._metadata_edits("delete") except Exception as error: @@ -261,7 +258,7 @@ def delete_icat_meta(self): # @PyQt6.QtCore.pyqtSlot(PyQt6.QtCore.QModelIndex) def edit_metadata(self, index): - """Load selected metadata into edit fields""" + """Load selected metadata into edit fields.""" self.errorLabel.clear() self.metaValueField.clear() self.metaUnitsField.clear() @@ -278,11 +275,11 @@ def edit_metadata(self, index): # @PyQt6.QtCore.pyqtSlot(PyQt6.QtCore.QModelIndex) def edit_acl(self, index): - """Load selected acl into editing fields""" + """Load selected acl into editing fields.""" self.errorLabel.clear() self.aclUserField.clear() self.aclZoneField.clear() - self.aclBox.setCurrentText('') + self.aclBox.setCurrentText("") row = index.row() user_name = self.aclTable.item(row, 0).text() user_zone = self.aclTable.item(row, 1).text() @@ -292,20 +289,20 @@ def edit_acl(self, index): self.aclBox.setCurrentText(acc_name) def update_icat_acl(self): - """Send acls to iRODS server""" + """Send acls to iRODS server.""" self.errorLabel.clear() if self.browserTable.currentRow() == -1: - self.errorLabel.setText('Please select a row from the table first!') + self.errorLabel.setText("Please select a row from the table first!") return irods_path = self._get_item_path(self.browserTable.currentRow()) user_name = self.aclUserField.text() user_zone = self.aclZoneField.text() acc_name = self.aclBox.currentText() - if acc_name in ('inherit', 'noinherit'): + if acc_name in ("inherit", "noinherit"): if irods_path.dataobject_exists(): self.errorLabel.setText( - 'WARNING: (no)inherit is not applicable to data objects') + "WARNING: (no)inherit is not applicable to data objects") return elif user_name == "": self.errorLabel.setText("Please provide a user.") @@ -313,25 +310,24 @@ def update_icat_acl(self): elif acc_name == "": self.errorLabel.setText("Please provide an access level from the menu.") return - recursive = self.recurseBox.currentText() == 'True' + recursive = self.recurseBox.currentText() == "True" try: item = get_irods_item(irods_path) perm = Permissions(self.session, item) perm.set(perm=acc_name, user=user_name, zone=user_zone, recursive=recursive) if acc_name == "null": - self.logger.info('Delete access (%s, %s, %s, %s) for %s', + self.logger.info("Delete access (%s, %s, %s, %s) for %s", acc_name, user_name, user_zone, str(recursive), item.path) else: - self.logger.info('Add/change access of %s to (%s, %s, %s, %s)', + self.logger.info("Add/change access of %s to (%s, %s, %s, %s)", item.path, acc_name, user_name, user_zone, str(recursive)) self._fill_acls_tab(irods_path) - except Exception as error: + except Exception: self.logger.exception("Cannot update ACLs.") - self.errorLabel.setText(f"Cannot update ACLs. Consult the logs.") - + self.errorLabel.setText("Cannot update ACLs. Consult the logs.") def load_selection(self): - """loads selection from main table into delete tab""" + """Load selection from main table into delete tab.""" self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.WaitCursor)) self.deleteSelectionBrowser.clear() row = self.browserTable.currentRow() @@ -339,43 +335,45 @@ def load_selection(self): self.errorLabel.setText("Please select a row from the table first.") self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.ArrowCursor)) return + content = [] item_path = self._get_item_path(row) if item_path.exists(): item = get_irods_item(item_path) if item_path.collection_exists(): data_dict = get_coll_dict(item) for key in list(data_dict.keys())[:20]: - self.deleteSelectionBrowser.append(key) + content.append(key) if len(data_dict[key]) > 0: for item in data_dict[key]: - self.deleteSelectionBrowser.append('\t'+item) - self.deleteSelectionBrowser.append('...') + content.append("\t"+item) + content.append("...") else: - self.deleteSelectionBrowser.append(str(item_path)) + content.append(str(item_path)) + populate_textfield(self.deleteSelectionBrowser, content) self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.ArrowCursor)) def delete_data(self): - """Deletes all data in the deleteSelectionBrowser""" + """Delete all data in the deleteSelectionBrowser.""" self.errorLabel.clear() - data = self.deleteSelectionBrowser.toPlainText().split('\n') - if data[0] != '': + data = self.deleteSelectionBrowser.toPlainText().split("\n") + if data[0] != "": item = data[0].strip() - quit_msg = "Delete all data in \n\n"+item+'\n' + quit_msg = "Delete all data in \n\n"+item+"\n" reply = PyQt6.QtWidgets.QMessageBox.question( - self, 'Message', quit_msg, + self, "Message", quit_msg, PyQt6.QtWidgets.QMessageBox.StandardButton.Yes, PyQt6.QtWidgets.QMessageBox.StandardButton.No) if reply == PyQt6.QtWidgets.QMessageBox.StandardButton.Yes: try: IrodsPath(self.session, item).remove() - self.logger.info('Delete data %s', item) + self.logger.info("Delete data %s", item) self.deleteSelectionBrowser.clear() self.load_browser_table() self.errorLabel.clear() except (irods.exception.CAT_NO_ACCESS_PERMISSION, PermissionError): self.errorLabel.setText(f"No permissions to delete {item}") - except Exception as error: - self.logger.exception('FAILED: Delete data %s', item) + except Exception: + self.logger.exception("FAILED: Delete data %s", item) self.errorLabel.setText(f"FAILED: Delete data {item}. Consult the logs.") # Internal functions @@ -387,12 +385,11 @@ def _clear_view_tabs(self): self.previewBrowser.clear() def _fill_replicas_tab(self, irods_path): - """Populate the table in the Replicas tab with the details of - the replicas of the selected data object. + """Populate the table in the Replicas tab. Parameters ---------- - obj_path : str + irods_path : str Path of iRODS collection or data object selected. """ @@ -408,34 +405,34 @@ def _fill_acls_tab(self, irods_path): Parameters ---------- - obj_path : str + irods_path : str Path of iRODS collection or data object selected. """ self.aclTable.setRowCount(0) self.aclUserField.clear() self.aclZoneField.clear() - self.aclBox.setCurrentText('') + self.aclBox.setCurrentText("") obj = None if irods_path.collection_exists(): obj = get_collection(self.session, irods_path) - inheritance = f'{obj.inheritance}' + inheritance = f"{obj.inheritance}" elif irods_path.dataobject_exists(): obj = get_dataobject(self.session, irods_path) - inheritance = '' + inheritance = "" if obj is not None: acls = Permissions(self.session, obj) acl_data = [(p.user_name, p.user_zone, p.access_name, inheritance) for p in acls] populate_table(self.aclTable, len(list(acls)), acl_data) self.aclTable.resizeColumnsToContents() - self.owner_label.setText(f'{obj.owner_name}') + self.owner_label.setText(f"{obj.owner_name}") def _fill_metadata_tab(self, irods_path): """Populate the table in the metadata tab. Parameters ---------- - obj_path : str + irods_path : str Full name of iRODS collection or data object selected. """ @@ -457,46 +454,43 @@ def _fill_preview_tab(self, irods_path): Parameters ---------- - obj_path : str + irods_path : str Full name of iRODS collection or data object selected. """ if irods_path.collection_exists(): obj = get_collection(self.session, irods_path) - content = ['Collections:', '-----------------'] + content = ["Collections:", "-----------------"] content.extend([sc.name for sc in obj.subcollections]) - content.extend(['\n', 'DataObjects:', '-----------------']) + content.extend(["\n", "DataObjects:", "-----------------"]) content.extend([do.name for do in obj.data_objects]) - preview_string = '\n'.join(content) - self.previewBrowser.append(preview_string) elif irods_path.dataobject_exists(): - file_type = '' + file_type = "" obj = get_dataobject(self.session, irods_path) - if '.' in irods_path.parts[-1]: - file_type = irods_path.parts[-1].split('.')[1] - if file_type in ['txt', 'json', 'csv']: + if "." in irods_path.parts[-1]: + file_type = irods_path.parts[-1].split(".")[1] + if file_type in ["txt", "json", "csv"]: try: - with obj.open('r') as objfd: - preview_string = objfd.read(1024).decode('utf-8') - self.previewBrowser.append(preview_string) + with obj.open("r") as objfd: + content = [objfd.read(1024).decode("utf-8")] + #self.previewBrowser.append(preview_string) except Exception as error: - self.previewBrowser.append( - f'No Preview for: {irods_path}') - self.previewBrowser.append(repr(error)) - self.previewBrowser.append( - "Storage resource might be down.") + content = [f"No Preview for: {irods_path}", + repr(error), + "Storage resource might be down." + ] else: - self.previewBrowser.append( - f'No Preview for: {irods_path}') + content = [f"No Preview for: {irods_path}"] + populate_textfield(self.previewBrowser, content) def _get_item_path(self, row): item_name = self.browserTable.item(row, 1).text() - return IrodsPath(self.session, '/', *self.inputPath.text().split('/'), item_name) + return IrodsPath(self.session, "/", *self.inputPath.text().split("/"), item_name) def _metadata_edits(self, operation): self.errorLabel.clear() if self.browserTable.currentRow() == -1: - self.errorLabel.setText('Please select an object first!') + self.errorLabel.setText("Please select an object first!") else: irods_path = self._get_item_path(self.browserTable.currentRow()) new_key = self.metaKeyField.text() @@ -508,22 +502,25 @@ def _metadata_edits(self, operation): meta = MetaData(item) if operation == "add": meta.add(new_key, new_val, new_units) - self.logger.info('Add metadata (%s, %s, %s) to %s', + self.logger.info("Add metadata (%s, %s, %s) to %s", new_key, new_val, new_units, irods_path) elif operation == "set": meta.set(new_key, new_val, new_units) - self.logger.info('Set all metadata with key %s to (%s, %s, %s) for %s', + self.logger.info("Set all metadata with key %s to (%s, %s, %s) for %s", new_key, new_key, new_val, new_units, irods_path) elif operation == "delete": meta.delete(new_key, new_val, new_units) - self.logger.info('Delete metadata (%s, %s, %s) from %s', + self.logger.info("Delete metadata (%s, %s, %s) from %s", new_key, new_val, new_units, irods_path) self._fill_metadata_tab(irods_path) def _fs_select(self, path_select): - """Retrieve the path (file or folder) from a QFileDialog - path_select: PyQt6.QtWidgets.QFileDialog.getExistingDirectory - PyQt6.QtWidgets.QFileDialog.getOpenFileName + """Retrieve the path (file or folder) from a QFileDialog. + + Parameters + ---------- + path_select: PyQt6.QtWidgets.QFileDialog.getExistingDirectory + PyQt6.QtWidgets.QFileDialog.getOpenFileName """ yes_button = PyQt6.QtWidgets.QMessageBox.StandardButton.Yes @@ -532,35 +529,36 @@ def _fs_select(self, path_select): else: path = path_select - if path != '': + if path != "": if self.overwrite.isChecked(): write = "All data will be updated." else: write = "Only new data will be added." - info = f'Upload data:\n{path}\n\nto\n{self.inputPath.text()}\n\n{write}' + info = f"Upload data:\n{path}\n\nto\n{self.inputPath.text()}\n\n{write}" reply = PyQt6.QtWidgets.QMessageBox.question(self, "", info) if reply == yes_button: return Path(path) return None def _upload(self, source): - """Uploads data to path in inputPath""" + """Upload data to path in inputPath.""" overwrite = self.overwrite.isChecked() - parent_path = IrodsPath(self.session, '/', *self.inputPath.text().split('/')) + parent_path = IrodsPath(self.session, "/", *self.inputPath.text().split("/")) try: if parent_path.joinpath(source.name).exists() and not overwrite: raise FileExistsError - self.logger.info('Uploading %s to %s, overwrite %s', source, parent_path, str(overwrite)) + self.logger.info("Uploading %s to %s, overwrite %s", + source, parent_path, str(overwrite)) upload(self.session, source, parent_path, overwrite=overwrite) self.load_browser_table() except FileExistsError: self.errorLabel.setText(f'Data already exists in {parent_path}.'+\ ' Check "overwrite" to overwrite the data.') except irods.exception.CAT_NO_ACCESS_PERMISSION: - self.errorLabel.setText(f'No permission to upload data to {parent_path}.') - self.logger.info('Uploading %s to %s, overwrite %s failed. No permissions.', + self.errorLabel.setText(f"No permission to upload data to {parent_path}.") + self.logger.info("Uploading %s to %s, overwrite %s failed. No permissions.", source, parent_path, str(overwrite)) except Exception as err: - self.logger.exception('Failed to upload %s to %s: %s', source, parent_path, err) - self.errorLabel.setText(f'Failed to upload {source}. Consult the logs.') + self.logger.exception("Failed to upload %s to %s: %s", source, parent_path, err) + self.errorLabel.setText(f"Failed to upload {source}. Consult the logs.") diff --git a/ibridgesgui/config.py b/ibridgesgui/config.py index 37080ab2..dbb6091e 100644 --- a/ibridgesgui/config.py +++ b/ibridgesgui/config.py @@ -1,32 +1,37 @@ -"""Setting up logger and configuration files""" -from pathlib import Path -from typing import Union - -import logging -import logging.handlers +"""Setting up logger and configuration files.""" import datetime import json +import logging +import logging.handlers import sys +from json import JSONDecodeError +from pathlib import Path +from typing import Union + +from ibridges.session import Session +from irods.exception import CAT_INVALID_USER, PAM_AUTH_PASSWORD_FAILED, NetworkException +from irods.session import iRODSSession LOG_LEVEL = { - 'fulldebug': logging.DEBUG - 5, - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warn': logging.WARNING, - 'error': logging.ERROR, - 'critical': logging.CRITICAL, + "fulldebug": logging.DEBUG - 5, + "debug": logging.DEBUG, + "info": logging.INFO, + "warn": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, } -CONFIG_DIR = Path("~/.ibridges").expanduser() +CONFIG_DIR = Path("~", ".ibridges").expanduser() CONFIG_FILE = CONFIG_DIR.joinpath("ibridges_gui.json") + def ensure_log_config_location(): - """The location for logs and config files""" + """Ensure the location for logs and config files.""" CONFIG_DIR.mkdir(parents=True, exist_ok=True) +# logging functions def init_logger(app_name: str, log_level: str) -> logging.Logger: - """ - Create a logger for an app + """Create a logger for the app. app_name : str Name of the app, will be used as file name @@ -34,76 +39,150 @@ def init_logger(app_name: str, log_level: str) -> logging.Logger: String that will be mapped to python's log levels, default is info """ logger = logging.getLogger(app_name) - logfile = CONFIG_DIR.joinpath(f'{app_name}.log') + logfile = CONFIG_DIR.joinpath(f"{app_name}.log") # Direct logging to logfile - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler = logging.handlers.RotatingFileHandler(logfile, 'a', 100000, 1) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + file_handler = logging.handlers.RotatingFileHandler(logfile, "a", 100000, 1) file_handler.setFormatter(formatter) logger.addHandler(file_handler) - logger.setLevel(LOG_LEVEL.get(log_level, LOG_LEVEL['info'])) + logger.setLevel(LOG_LEVEL.get(log_level, LOG_LEVEL["info"])) # Logger greeting when app is started - with open(logfile, 'a', encoding='utf-8') as logfd: - logfd.write('\n\n') - underscores = f'{"_" * 50}\n' + with open(logfile, "a", encoding="utf-8") as logfd: + logfd.write("\n\n") + underscores = f"{'_' * 50}\n" logfd.write(underscores * 2) - logfd.write(f'\t Starting iBridges-GUI \n\t{datetime.datetime.now().isoformat()}\n') + logfd.write(f"\t Starting iBridges-GUI \n\t{datetime.datetime.now().isoformat()}\n") logfd.write(underscores * 2) return logger -def _get_config() -> Union[None, dict]: - try: - with open(CONFIG_FILE, "r", encoding="utf-8") as handle: - return json.load(handle) - except FileNotFoundError: - return None - except json.decoder.JSONDecodeError as err: - # empty file - if err.msg == "Expecting value": - return None - print(f"CANNOT START APP: {CONFIG_FILE} incorrectly formatted.") - sys.exit(1) - +# ibridges config functions def get_last_ienv_path() -> Union[None, str]: - """Retrieve last used environment path from the config file""" + """Retrieve last used environment path from the config file.""" config = _get_config() if config is not None: return config.get("gui_last_env") return None def set_last_ienv_path(ienv_path: Path): - """ - Save the last used environment path to the config file + """Save the last used environment path to the config file. ienv_path : Path Path to last environment """ config = _get_config() if config is not None: - config['gui_last_env'] = ienv_path + config["gui_last_env"] = ienv_path else: - config = {'gui_last_env': ienv_path} + config = {"gui_last_env": ienv_path} _save_config(config) def get_log_level() -> Union[None, str]: - """Retrieve log level from config""" + """Retrieve log level from config.""" config = _get_config() if config is not None: return config.get("log_level") return None def set_log_level(level: str): - """Save log level to config""" + """Save log level to config.""" config = _get_config() if config is not None: - config['log_level'] = level + config["log_level"] = level else: - config = {'log_level': level} + config = {"log_level": level} _save_config(config) def _save_config(conf: dict): ensure_log_config_location() - with open(CONFIG_FILE, "w", encoding="utf-8") as handle: - json.dump(conf, handle) + _write_json(CONFIG_FILE, conf) + +def _get_config() -> Union[None, dict]: + try: + return _read_json(CONFIG_FILE) + except FileNotFoundError: + return None + except JSONDecodeError as err: + # empty file + if err.msg == "Expecting value": + return None + print(f"CANNOT START APP: {CONFIG_FILE} incorrectly formatted.") + sys.exit(1) + +# irods config functions +def check_irods_config(ienv: Union[Path, dict]) -> str: + """Check whether an iRODS configuration file is correct. + + Parameters + ---------- + ienv : Path or dict + Path to the irods_environment.json or the dictionary conatining the json. + + Returns + ------- + str : + Error message why login with the settings would fail. + "All checks passed successfully" in case all seems to be fine. + + """ + if isinstance(ienv, Path): + try: + env = _read_json(ienv) + except FileNotFoundError: + return f"{ienv} not found." + except JSONDecodeError as err: + return f"{ienv} not well formatted.\n{err.msg}" + else: + env = ienv + # check host and port and connectivity + if "irods_host" not in env: + return '"irods_host" is missing in environment.' + if "irods_port" not in env: + return '"irods_port" is missing in environment.' + if not isinstance(env["irods_port"], int): + return '"irods_port" needs to be an integer, remove quotes.' + if not Session.network_check(env["irods_host"], env["irods_port"]): + return f'No connection: Network might be down or\n \ + server name {env["irods_host"]} is incorrect or\n \ + port {env["irods_port"]} is incorrect.' + # check authentication scheme + try: + sess = iRODSSession(password="bogus", **env) + _ = sess.server_version + except TypeError as err: + return repr(err) + except NetworkException as err: + return repr(err) + except AttributeError as err: + return repr(err) + + # password incorrect but rest is fine + except (ValueError, CAT_INVALID_USER, PAM_AUTH_PASSWORD_FAILED): + return "All checks passed successfully." + + # all tests passed + return "All checks passed successfully." + +def save_irods_config(env_path: Union[Path, str], conf: dict): + """Save an irods environment as json in ~/.irods. + + Parmeters + --------- + env_name : str + file name + """ + env_path = Path(env_path) + if env_path.suffix == ".json": + _write_json(env_path, conf) + else: + raise ValueError("Filetype needs to be '.json'.") + +def _read_json(file_path: Path) -> dict: + with open(file_path, "r", encoding="utf-8") as handle: + return json.load(handle) + +def _write_json(file_path: Path, content: dict): + with open(file_path, "w", encoding="utf-8") as handle: + json.dump(content, handle) diff --git a/ibridgesgui/gui_utils.py b/ibridgesgui/gui_utils.py index 649d8db6..b08555fa 100644 --- a/ibridgesgui/gui_utils.py +++ b/ibridgesgui/gui_utils.py @@ -1,11 +1,10 @@ -"""Handy and reusable functions for the GUI""" +"""Handy and reusable functions for the GUI.""" import pathlib from importlib.resources import files +from typing import Union import irods import PyQt6 -from typing import Union - from ibridges import get_collection, get_dataobject from ibridges.path import IrodsPath @@ -13,16 +12,25 @@ # Widget utils def populate_table(table_widget, rows: int, data_by_row: list): - + """Populate a table-like pyqt widget with data.""" table_widget.setRowCount(rows) for row, data in enumerate(data_by_row): for col, item in enumerate(data): table_widget.setItem(row, col, PyQt6.QtWidgets.QTableWidgetItem(str(item))) table_widget.resizeColumnsToContents() +def populate_textfield(text_widget, text_by_row: Union[str, list]): + """Populate a text viewer or editor with text.""" + text_widget.clear() + if isinstance(text_by_row, str): + text_widget.append(text_by_row) + else: + for row in text_by_row: + text_widget.append(row) # iBridges/iRODS utils def get_irods_item(irods_path: IrodsPath): + """Get the item behind an iRODS path.""" try: item = get_collection(irods_path.session, irods_path) except ValueError: @@ -56,11 +64,10 @@ def get_downloads_dir() -> pathlib.Path: Absolute path to 'Downloads' directory. """ - if isinstance(pathlib.Path('~'), pathlib.PosixPath): - return pathlib.Path('~', 'Downloads').expanduser() - else: - import winreg - sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' - downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}' - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: - return pathlib.Path(winreg.QueryValueEx(key, downloads_guid)[0]) + # Linux and Mac Download folders + if pathlib.Path("~", "Downloads").expanduser().is_dir(): + return pathlib.Path("~", "Downloads").expanduser() + + # Try to create Downloads + pathlib.Path("~", "Downloads").expanduser().mkdir(parents=True) + return pathlib.Path("~", "Downloads").expanduser() diff --git a/ibridgesgui/info.py b/ibridgesgui/info.py index 85b360a5..781625fc 100644 --- a/ibridgesgui/info.py +++ b/ibridgesgui/info.py @@ -1,6 +1,4 @@ -"""Provide the GUI with iRODS information - -""" +"""Provide the GUI with iRODS information.""" import sys import PyQt6 @@ -8,16 +6,18 @@ import PyQt6.uic from ibridges.resources import Resources -import ibridgesgui as gui -from ibridgesgui.gui_utils import populate_table, UI_FILE_DIR +from ibridgesgui.config import CONFIG_DIR +from ibridgesgui.gui_utils import UI_FILE_DIR, populate_table, populate_textfield +from ibridgesgui.ui_files.tabInfo import Ui_tabInfo -class Info(PyQt6.QtWidgets.QWidget, gui.ui_files.tabInfo.Ui_tabInfo): - """Set iRODS information in the GUI""" +class Info(PyQt6.QtWidgets.QWidget, Ui_tabInfo): + """Set iRODS information in the GUI.""" def __init__(self, session): + """Initialise the tab.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: PyQt6.uic.loadUi(UI_FILE_DIR / "tabInfo.ui", self) @@ -27,9 +27,7 @@ def __init__(self, session): self.refresh_info() def refresh_info(self): - """Find and set the information of the connected iRODS system - including the availble top-level resources. - """ + """Find and set the information of the connected iRODS system.""" self.rescTable.setRowCount(0) self.setCursor(PyQt6.QtGui.QCursor(PyQt6.QtCore.Qt.CursorShape.WaitCursor)) # irods Zone @@ -39,13 +37,15 @@ def refresh_info(self): # irods user type and groups user_type, user_groups = self.session.get_user_info() self.typeLabel.setText(user_type) - self.groupsLabel.setText('\n'.join(user_groups)) + populate_textfield(self.groupsBrowser, user_groups) + # ibridges log location + self.log_loc.setText(str(CONFIG_DIR)) # default resource self.rescLabel.setText(self.session.default_resc) # irods server and version self.serverLabel.setText(self.session.host) self.versionLabel.setText( - '.'.join((str(num) for num in self.session.server_version))) + ".".join((str(num) for num in self.session.server_version))) # irods resources resc_info = Resources(self.session).root_resources populate_table(self.rescTable, len(resc_info[0]), resc_info) diff --git a/ibridgesgui/login.py b/ibridgesgui/login.py index 3adfbb9c..2d4786b3 100644 --- a/ibridgesgui/login.py +++ b/ibridgesgui/login.py @@ -1,31 +1,33 @@ """Pop up Widget for Login.""" +import logging import sys from pathlib import Path -import logging from ibridges import Session from ibridges.session import LoginError, PasswordError from PyQt6.QtWidgets import QDialog, QLineEdit from PyQt6.uic import loadUi +from ibridgesgui.config import get_last_ienv_path, set_last_ienv_path from ibridgesgui.gui_utils import UI_FILE_DIR from ibridgesgui.ui_files.irodsLogin import Ui_irodsLogin -from ibridgesgui.config import get_last_ienv_path, set_last_ienv_path + class Login(QDialog, Ui_irodsLogin): """Definition and initialization of the iRODS login window.""" def __init__(self, session_dict, app_name): + """Initialise tab.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: loadUi(UI_FILE_DIR / "irodsLogin.ui", self) - + self.logger = logging.getLogger(app_name) + self.irods_config_dir = Path("~", ".irods").expanduser() self.session_dict = session_dict - self.irods_path = Path('~', '.irods').expanduser() self._init_envbox() self.cached_pw = self._init_password() self._load_gui() @@ -39,43 +41,45 @@ def _load_gui(self): def _init_envbox(self): env_jsons = [ path.name for path in - self.irods_path.glob('irods_environment*json')] + self.irods_config_dir.glob("irods_environment*json")] if len(env_jsons) == 0: - self.envError.setText(f'ERROR: no "irods_environment*json" files found in {self.irods_path}') + self.envError.setText( + f"ERROR: no irods_environment*json files found in {self.irods_config_dir}") self.envbox.clear() self.envbox.addItems(env_jsons) last_env = get_last_ienv_path() - if last_env is not None: + if last_env is not None and last_env in env_jsons: self.envbox.setCurrentIndex(env_jsons.index(last_env)) else: self.envbox.setCurrentIndex(0) def _init_password(self): #Check if there is a cached password - passwd_file = self.irods_path.joinpath('.irodsA') + passwd_file = self.irods_config_dir.joinpath(".irodsA") if passwd_file.is_file(): self.passwordField.setText("***********") return True return False def close(self): - """Abort login""" + """Abort login.""" self.done(0) def login_function(self): - """Connect to iRODS server with gathered info""" + """Connect to iRODS server with gathered info.""" self.passError.clear() - env_file = self.irods_path.joinpath(self.envbox.currentText()) + env_file = self.irods_config_dir.joinpath(self.envbox.currentText()) try: if self.cached_pw is True and self.passwordField.text() == "***********": - self.logger.debug(f"Login with {env_file} and cached password.") + self.logger.debug("Login with %s and cached password.", env_file) session = Session(irods_env=env_file) else: session = Session(irods_env=env_file, password=self.passwordField.text()) - self.logger.debug(f"Login with {env_file} and password from prompt.") - self.session_dict['session'] = session - self.logger.info(f"Logged in as {session.username} to {session.host}; working coll {session.home}") + self.logger.debug("Login with %s and password from prompt.", env_file) + self.session_dict["session"] = session + self.logger.info("Logged in as %s to %s; working coll %s", + session.username, session.host, session.home) session.write_pam_password() set_last_ienv_path(env_file.name) self.close() @@ -86,6 +90,6 @@ def login_function(self): except ConnectionError: self.passError.setText("Cannot connect to server. Check Internet, host name and port.") except Exception as err: - log_path = Path('~/.ibridges') - self.logger.exception(f'Failed to login: {err}') - self.passError.setText(f'Login failed, consult the log file(s) in {log_path}') + log_path = Path("~/.ibridges") + self.logger.exception("Failed to login: %s", repr(err)) + self.passError.setText(f"Login failed, consult the log file(s) in {log_path}") diff --git a/ibridgesgui/popup_widgets.py b/ibridgesgui/popup_widgets.py index bf721a4b..d8a32e75 100644 --- a/ibridgesgui/popup_widgets.py +++ b/ibridgesgui/popup_widgets.py @@ -1,22 +1,27 @@ """Pop-up widget definitions.""" +import json import os import sys import irods from ibridges import IrodsPath from PyQt6 import QtCore -from PyQt6.QtWidgets import QDialog +from PyQt6.QtWidgets import QDialog, QFileDialog from PyQt6.uic import loadUi -from ibridgesgui.gui_utils import UI_FILE_DIR +from ibridgesgui.config import _read_json, check_irods_config, save_irods_config +from ibridgesgui.gui_utils import UI_FILE_DIR, populate_textfield +from ibridgesgui.ui_files.configCheck import Ui_configCheck from ibridgesgui.ui_files.createCollection import Ui_createCollection class CreateCollection(QDialog, Ui_createCollection): - """Popup window to create a new collection""" + """Popup window to create a new collection.""" + def __init__(self, parent, logger): + """Initialise window.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: loadUi(UI_FILE_DIR / "createCollection.ui", self) @@ -29,29 +34,31 @@ def __init__(self, parent, logger): self.buttonBox.accepted.connect(self.accept) def accept(self): - """Create""" + """Create new collection.""" if self.collPathLine.text() != "": new_coll_path = IrodsPath(self.parent.session, self.parent, self.collPathLine.text()) if new_coll_path.exists(): - self.errorLabel.setText(f'{new_coll_path} already exists.') + self.errorLabel.setText(f"{new_coll_path} already exists.") else: try: IrodsPath.create_collection(new_coll_path.session, new_coll_path) - self.logger.info(f'Created collection {new_coll_path}') + self.logger.info(f"Created collection {new_coll_path}") self.done(0) except irods.exception.CAT_NO_ACCESS_PERMISSION: - self.errorLabel.setText(f'No access rights to {new_coll_path.parent}.'+\ - f' Cannot create {self.collPathLine.text()}.') + self.errorLabel.setText(f"No access rights to {new_coll_path.parent}."+\ + f" Cannot create {self.collPathLine.text()}.") except Exception as err: - self.logger.exception(f'Could not create {new_coll_path}: {err}') - self.errorLabel.setText(f'Could not create {new_coll_path}, consult the logs.') + self.logger.exception(f"Could not create {new_coll_path}: {err}") + self.errorLabel.setText(f"Could not create {new_coll_path}, consult the logs.") class CreateDirectory(QDialog, Ui_createCollection): - """Popup window to create a new directory""" + """Popup window to create a new directory.""" + def __init__(self, parent): + """Initialise window.""" super().__init__() - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): super().setupUi(self) else: loadUi("gui/ui_files/createCollection.ui", self) @@ -62,14 +69,124 @@ def __init__(self, parent): self.buttonBox.accepted.connect(self.accept) def accept(self): - """Create""" + """Create folder.""" if self.collPathLine.text() != "": new_dir_path = self.parent + os.sep + self.collPathLine.text() try: os.makedirs(new_dir_path) self.done(1) except Exception as error: - if hasattr(error, 'message'): + if hasattr(error, "message"): self.errorLabel.setText(error.message) else: self.errorLabel.setText("ERROR: insufficient rights.") + +class CheckConfig(QDialog, Ui_configCheck): + """Popup window to edit, create and check an environment.json.""" + + def __init__(self, logger, env_path): + """Initialise window.""" + super().__init__() + if getattr(sys, "frozen", False): + super().setupUi(self) + else: + loadUi(UI_FILE_DIR / "configCheck.ui", self) + + self.logger = logger + self.env_path = env_path + self.setWindowTitle("Create, edit and inspect iRODS environment") + self._init_env_box() + + self.envbox.activated.connect(self.load_env) + self.createButton.clicked.connect(self.create_env) + self.checkButton.clicked.connect(self.check_env) + self.saveButton.clicked.connect(self.save_env) + self.saveasButton.clicked.connect(self.save_env_as) + self.closeButton.clicked.connect(self.close) + + + def _init_env_box(self): + self.envbox.clear() + env_jsons = [""]+[ + path.name for path in + self.env_path.glob("irods_environment*json")] + if len(env_jsons) != 0: + self.envbox.addItems(env_jsons) + self.envbox.setCurrentIndex(0) + + def load_env(self): + """Load json into text field.""" + self.errorLabel.clear() + env_file = self.env_path.joinpath(self.envbox.currentText()) + try: + content = json.dumps(_read_json(env_file), + sort_keys=True, indent=4, separators=(",", ": ")) + populate_textfield(self.envEdit, content) + except IsADirectoryError: + self.errorLabel.setText("Choose and environment or create a new one.") + except FileNotFoundError: + self.errorLabel.setText(f"File does not exist {env_file}") + except Exception as err: + self.errorLabel.setText(f"{repr(err)}") + + def create_env(self): + """Load standard environment into text field.""" + self.errorLabel.clear() + self.envbox.setCurrentIndex(0) + env = { + "irods_host": "", + "irods_port": 1247, + "irods_home": "", + "irods_user_name": "", + "irods_zone_name": "", + "irods_authentication_scheme": "pam", + "irods_encryption_algorithm": "AES-256-CBC", + "irods_encryption_key_size": 32, + "irods_encryption_num_hash_rounds": 16, + "irods_encryption_salt_size": 8, + "irods_client_server_policy": "CS_NEG_REQUIRE", + "irods_client_server_negotiation": "request_server_negotiation" + } + populate_textfield(self.envEdit, + json.dumps(env, sort_keys=True, indent=4, separators=(",", ": "))) + + def check_env(self): + """Check formatting, parameters and connectivity of information in text field.""" + self.errorLabel.clear() + try: + msg = check_irods_config(json.loads(self.envEdit.toPlainText())) + except json.decoder.JSONDecodeError as err: + msg = "JSON decoding error: "+err.msg + self.errorLabel.setText(msg) + + def save_env(self): + """Overwrite file from combobox with information from text field.""" + self.errorLabel.clear() + env_file = self.env_path.joinpath(self.envbox.currentText()) + if env_file.exists(): + try: + save_irods_config(env_file, json.loads(self.envEdit.toPlainText())) + self.errorLabel.setText(f"Configuration saved as {env_file}") + except json.decoder.JSONDecodeError: + self.errorLabel.setText( + "Incorrectly formatted. Click 'Check' for more information.") + else: + self.errorLabel.setText("Choose 'Save as' to save") + + def save_env_as(self): + """Choose file to save text field as json.""" + self.errorLabel.clear() + dialog = QFileDialog(self) + dialog.setFileMode(QFileDialog.FileMode.AnyFile) + dialog.setNameFilter("(*.json)") + create_file = QFileDialog.getSaveFileName(self, "Save as File", + str(self.env_path), "(*.json)") + if create_file[0] != "": + try: + save_irods_config(create_file[0], json.loads(self.envEdit.toPlainText())) + self.errorLabel.setText(f"Configuration saved as {create_file[0]}") + except json.decoder.JSONDecodeError: + self.errorLabel.setText( + "Incorrectly formatted. Click 'Check' for more information.") + except TypeError: + self.errorLabel.setText("File type needs to be .json") diff --git a/ibridgesgui/ui_files/MainMenu.ui b/ibridgesgui/ui_files/MainMenu.ui index 8dd340c6..cec2c559 100644 --- a/ibridgesgui/ui_files/MainMenu.ui +++ b/ibridgesgui/ui_files/MainMenu.ui @@ -55,6 +55,7 @@ QTabBar::tab:top:selected { 16 + 50 false false @@ -88,6 +89,7 @@ QTabBar::tab:top:selected { 16 + 50 false false @@ -96,6 +98,7 @@ QTabBar::tab:top:selected { 16 + 50 false false @@ -112,7 +115,7 @@ QTabBar::tab:top:selected { Configure - + @@ -160,7 +163,7 @@ QTabBar::tab:top:selected { Check Configuration - + Add Configuration diff --git a/ibridgesgui/ui_files/__init__.py b/ibridgesgui/ui_files/__init__.py index fa988eea..c0f6f287 100644 --- a/ibridgesgui/ui_files/__init__.py +++ b/ibridgesgui/ui_files/__init__.py @@ -1,12 +1,2 @@ """ - """ -from . import createCollection -from . import dataTransferState -from . import irodsLogin -from . import MainMenu -from . import searchDialog -from . import tabBrowser -from . import tabInfo -from . import tabUpDownload -from . import ExampleTab diff --git a/ibridgesgui/ui_files/configCheck.py b/ibridgesgui/ui_files/configCheck.py new file mode 100644 index 00000000..8b0f345f --- /dev/null +++ b/ibridgesgui/ui_files/configCheck.py @@ -0,0 +1,87 @@ +# Form implementation generated from reading ui file 'ibridgesgui/ui_files/configCheck.ui' +# +# Created by: PyQt6 UI code generator 6.4.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_configCheck(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(1034, 513) + Dialog.setStyleSheet("QWidget\n" +"{\n" +" color: rgb(86, 184, 139);\n" +" background-color: rgb(54, 54, 54);\n" +" font: 14pt\n" +"}\n" +"\n" +"QLabel#errorLabel\n" +"{\n" +" color: rgb(217, 174, 23);\n" +"}") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.envbox = QtWidgets.QComboBox(parent=Dialog) + self.envbox.setObjectName("envbox") + self.horizontalLayout.addWidget(self.envbox) + self.loadButton = QtWidgets.QPushButton(parent=Dialog) + self.loadButton.setObjectName("loadButton") + self.horizontalLayout.addWidget(self.loadButton) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.createButton = QtWidgets.QPushButton(parent=Dialog) + self.createButton.setObjectName("createButton") + self.horizontalLayout.addWidget(self.createButton) + self.verticalLayout_2.addLayout(self.horizontalLayout) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.envEdit = QtWidgets.QTextEdit(parent=Dialog) + self.envEdit.setObjectName("envEdit") + self.verticalLayout.addWidget(self.envEdit) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.checkButton = QtWidgets.QPushButton(parent=Dialog) + self.checkButton.setObjectName("checkButton") + self.horizontalLayout_4.addWidget(self.checkButton) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_4.addItem(spacerItem1) + self.errorLabel = QtWidgets.QLabel(parent=Dialog) + self.errorLabel.setText("") + self.errorLabel.setObjectName("errorLabel") + self.horizontalLayout_4.addWidget(self.errorLabel) + self.verticalLayout.addLayout(self.horizontalLayout_4) + self.verticalLayout_2.addLayout(self.verticalLayout) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.saveButton = QtWidgets.QPushButton(parent=Dialog) + self.saveButton.setObjectName("saveButton") + self.horizontalLayout_2.addWidget(self.saveButton) + self.saveasButton = QtWidgets.QPushButton(parent=Dialog) + self.saveasButton.setObjectName("saveasButton") + self.horizontalLayout_2.addWidget(self.saveasButton) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_2.addItem(spacerItem2) + self.closeButton = QtWidgets.QPushButton(parent=Dialog) + self.closeButton.setObjectName("closeButton") + self.horizontalLayout_2.addWidget(self.closeButton) + self.verticalLayout_2.addLayout(self.horizontalLayout_2) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.loadButton.setText(_translate("Dialog", "Load")) + self.createButton.setText(_translate("Dialog", "Create new")) + self.checkButton.setText(_translate("Dialog", "Check")) + self.saveButton.setText(_translate("Dialog", "Save")) + self.saveasButton.setText(_translate("Dialog", "Save as")) + self.closeButton.setText(_translate("Dialog", "Close")) diff --git a/ibridgesgui/ui_files/configCheck.ui b/ibridgesgui/ui_files/configCheck.ui new file mode 100644 index 00000000..30125055 --- /dev/null +++ b/ibridgesgui/ui_files/configCheck.ui @@ -0,0 +1,142 @@ + + + Dialog + + + + 0 + 0 + 1034 + 429 + + + + Dialog + + + QWidget +{ + color: rgb(86, 184, 139); + background-color: rgb(54, 54, 54); + font: 14pt +} + +QTextEdit +{ + background-color: rgb(85, 87, 83); +} + +QLabel#errorLabel +{ + color: rgb(217, 174, 23); +} + + + + + + + + + + + New Config + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + Check + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + Save + + + + + + + Save as + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + diff --git a/ibridgesgui/ui_files/tabInfo.ui b/ibridgesgui/ui_files/tabInfo.ui index 155b9e84..3d79859b 100644 --- a/ibridgesgui/ui_files/tabInfo.ui +++ b/ibridgesgui/ui_files/tabInfo.ui @@ -7,7 +7,7 @@ 0 0 640 - 547 + 572 @@ -19,6 +19,7 @@ color: rgb(86, 184, 139); background-color: rgb(54, 54, 54); selection-background-color: rgb(58, 152, 112); + font:16 } QTableWidget @@ -31,41 +32,32 @@ QTextBrowser background-color: rgb(85, 87, 83); } +QLabel#client, QLabel#server +{ + font-weight: bold; +} + - - + + - - + + - - - - - 18 - false - false - - - - Server Information - - - - - + + Qt::Vertical @@ -77,8 +69,53 @@ QTextBrowser - - + + + + + + + + + + + Server + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 2 + false + false + + + + Refresh + + + + + + + Qt::Vertical @@ -90,65 +127,38 @@ QTextBrowser - - - - Server - - - - + Version - - - - - - - - - - - - - - - - + + - Qt::Horizontal + Qt::Vertical - 40 - 20 + 20 + 40 - - - - Username - - - - - + + 18 + 75 false - false + true - Client Information + Server Information @@ -165,79 +175,42 @@ QTextBrowser - - - - User's groups - - - - - - - - - - - - - - - - - - - + + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 40 + 20 - - + + + + + 18 + 75 + false + true + + - + Client Information - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 18 - false - false - - - - Refresh - - - - + + + + Username + + - + Qt::Vertical @@ -250,28 +223,35 @@ QTextBrowser - - + + - Resources + User's groups - - + + - Zone + Usertype - - + + - Usertype + + + + + + + + Default resource - + QAbstractItemView::NoEditTriggers @@ -299,20 +279,64 @@ QTextBrowser - - + + + + + - Default resource + Zone - - + + + + Resources + + + + + + + + + + + + + + + + iBridges Logfiles + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + diff --git a/pyproject.toml b/pyproject.toml index d7001d9e..a86eb8a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ dependencies = [ "PyQt6>=6.4.2", - "ibridges", + "ibridges>=0.1.6", # "pyinstaller==5.8.0", "setproctitle==1.3.3", ]