From 4363f255fe264bebc608a1f307701f25ee6b287b Mon Sep 17 00:00:00 2001 From: Jeffrey Gill Date: Sun, 17 Jan 2021 00:24:48 -0500 Subject: [PATCH 1/3] Run the Google Drive authorization flow in a separate thread Also hides the metadata selector and replaces it with a message when the auth flow is triggered by the menu action. If the auth flow is triggered by a download instead, it will still run on the network thread, but the metadata selector won't be hidden. This is not ideal, but making the download-triggered case the same would be difficult. --- neurotic/gui/standalone.py | 84 ++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/neurotic/gui/standalone.py b/neurotic/gui/standalone.py index 8dbf336..bb643b6 100644 --- a/neurotic/gui/standalone.py +++ b/neurotic/gui/standalone.py @@ -64,8 +64,9 @@ class MainWindow(QT.QMainWindow): """ request_download = QT.pyqtSignal() - request_check_for_updates = QT.pyqtSignal(bool) request_load_dataset = QT.pyqtSignal() + request_gdrive_authorization = QT.pyqtSignal() + request_check_for_updates = QT.pyqtSignal(bool) def __init__(self, file=None, initial_selection=None, lazy=True, theme='light', ui_scale='medium', support_increased_line_width=False, show_datetime=False): """ @@ -118,23 +119,37 @@ def __init__(self, file=None, initial_selection=None, lazy=True, theme='light', self.loading_label.setFrameStyle(QT.QFrame.Panel | QT.QFrame.Sunken) self.loading_label.setAlignment(QT.Qt.AlignCenter) + # gdrive auth flow label + self.gdrive_auth_label = QT.QLabel('Continue Google Drive authorization\nin your web browser') + self.gdrive_auth_label.setFrameStyle(QT.QFrame.Panel | QT.QFrame.Sunken) + self.gdrive_auth_label.setAlignment(QT.Qt.AlignCenter) + # initially stack the metadata selector above the loading label self.stacked_layout = QT.QStackedLayout() self.stacked_layout.addWidget(self.metadata_selector) # index 0 self.stacked_layout.addWidget(self.loading_label) # index 1 + self.stacked_layout.addWidget(self.gdrive_auth_label) # index 2 central_widget = QT.QWidget() central_widget.setLayout(self.stacked_layout) self.setCentralWidget(central_widget) - # create a worker thread for downloading data and checking for updates + # create a worker thread for network activity (e.g., downloading data) self.network_thread = QT.QThread() self.network_worker = _NetworkWorker(self) self.network_worker.moveToThread(self.network_thread) + + # set up the network thread to perform data downloads self.request_download.connect(self.network_worker.download) self.network_worker.download_finished.connect(self.on_download_finished) + + # set up the network thread to perform checks for updates self.request_check_for_updates.connect(self.network_worker.get_latest_release_number) self.network_worker.version_check_finished.connect(self.on_version_check_finished) + # set up the network thread to run the Google Drive authorization flow + self.request_gdrive_authorization.connect(self.network_worker.authorize_gdrive) + self.network_worker.gdrive_authorization_finished.connect(self.on_gdrive_authorization_finished) + # create a worker thread for loading datasets self.load_dataset_thread = QT.QThread() self.load_dataset_worker = _LoadDatasetWorker(self) @@ -287,8 +302,8 @@ def create_menus(self): do_open_gdrive_creds_dir = help_menu.addAction('Open Google Drive credentials directory') do_open_gdrive_creds_dir.triggered.connect(self.open_gdrive_creds_dir) - do_authorize_gdrive = help_menu.addAction('Request Google Drive authorization now') - do_authorize_gdrive.triggered.connect(self.authorize_gdrive) + self.do_authorize_gdrive = help_menu.addAction('Request Google Drive authorization now') + self.do_authorize_gdrive.triggered.connect(self.authorize_gdrive) do_deauthorize_gdrive = help_menu.addAction('Purge Google Drive authorization token') do_deauthorize_gdrive.triggered.connect(self.deauthorize_gdrive) @@ -540,26 +555,36 @@ def authorize_gdrive(self): text = 'Completing this process will allow neurotic to ' \ 'download files from your Google Drive. A web browser ' \ 'will open so that you can log into your Google account ' \ - 'and accept the request for permissions. neurotic ' \ - 'will be unresponsive until this process finishes.' \ + 'and accept the request for permissions. You will not be ' \ + 'able to use neurotic until this process finishes.' \ '

Do you want to continue?' button = QT.QMessageBox.question(self, title, text, defaultButton=QT.QMessageBox.Yes) if button == QT.QMessageBox.Yes: - logger.info(f'Initiating Google Drive authorization flow') - self.statusBar().showMessage('Check web browser to continue ' - 'authorization', msecs=5000) - try: - gdrive_downloader.authorize() - except Exception as e: - logger.error(f'Problem during authorization: {e}') - self.statusBar().showMessage('ERROR: Authorization failed ' - '(see console for details)', - msecs=5000) - else: - logger.info(f'Authorization complete') - self.statusBar().showMessage('Authorization complete', - msecs=5000) + self.do_authorize_gdrive.setText('Continue Google Drive authorization in your browser!') + self.do_authorize_gdrive.setEnabled(False) + self.metadata_selector.setEnabled(False) + self.stacked_layout.setCurrentIndex(2) # show gdrive auth label + self.network_thread.start() + self.request_gdrive_authorization.emit() + + def on_gdrive_authorization_finished(self, success): + """ + Cleanup network thread and display success or failute of Google Drive + authorization flow. + """ + self.network_thread.quit() + if success: + self.statusBar().showMessage('Authorization successful', + msecs=5000) + else: + self.statusBar().showMessage('ERROR: Authorization failed ' + '(see console for details)', + msecs=5000) + self.do_authorize_gdrive.setText('Request Google Drive authorization now') + self.do_authorize_gdrive.setEnabled(True) + self.metadata_selector.setEnabled(True) + self.stacked_layout.setCurrentIndex(0) # show metadata selector def deauthorize_gdrive(self): """ @@ -722,6 +747,10 @@ def set_ui_scale(self, size): font.setPointSize(font_size[size]+4) self.loading_label.setFont(font) + font = self.gdrive_auth_label.font() + font.setPointSize(font_size[size]+4) + self.gdrive_auth_label.setFont(font) + def set_theme(self, theme): self.theme = theme @@ -822,10 +851,11 @@ def load(self): class _NetworkWorker(QT.QObject): """ - A thread worker for downloading data files and checking for updates. + A thread worker for for network activity (e.g., downloading data) """ download_finished = QT.pyqtSignal(bool) + gdrive_authorization_finished = QT.pyqtSignal(bool) version_check_finished = QT.pyqtSignal(str, bool) def __init__(self, mainwindow): @@ -851,6 +881,18 @@ def download(self): finally: self.download_finished.emit(success) + def authorize_gdrive(self): + success = False + try: + logger.info('Initiating Google Drive authorization flow') + gdrive_downloader.authorize() + success = True + logger.info('Authorization successful') + except Exception as e: + logger.error(f'Problem during authorization: {e}') + finally: + self.gdrive_authorization_finished.emit(success) + def get_latest_release_number(self, show_new_only): """ Query GitHub for the version number of the latest release and emit a From b7e47c2ae033fd2957679e24de8b215abd8e10ae Mon Sep 17 00:00:00 2001 From: Jeffrey Gill Date: Sun, 17 Jan 2021 00:45:37 -0500 Subject: [PATCH 2/3] Suppress OAuth warning about missing token file neurotic already handles this. --- neurotic/gui/standalone.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neurotic/gui/standalone.py b/neurotic/gui/standalone.py index bb643b6..9442bc3 100644 --- a/neurotic/gui/standalone.py +++ b/neurotic/gui/standalone.py @@ -12,6 +12,7 @@ import requests import subprocess import pkg_resources +import warnings from packaging import version import quantities as pq @@ -26,6 +27,9 @@ import logging logger = logging.getLogger(__name__) +# suppress warning that gdrive token file does not exist +warnings.filterwarnings('ignore', message='Cannot access .*', module='oauth2client') + def open_path_with_default_program(path): """ From d22d3a6f05646c893c9e95a6bbef783b3c599d5c Mon Sep 17 00:00:00 2001 From: Jeffrey Gill Date: Sun, 17 Jan 2021 00:53:02 -0500 Subject: [PATCH 3/3] Disable menu bar when launching datasets or running gdrive auth flow The menu bar gets disabled only if the auth flow is triggered by the menu action. It does not get disabled if triggered by a download. This is not ideal, but making the download-triggered case the same would be difficult. --- neurotic/gui/standalone.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/neurotic/gui/standalone.py b/neurotic/gui/standalone.py index 9442bc3..3a9034b 100644 --- a/neurotic/gui/standalone.py +++ b/neurotic/gui/standalone.py @@ -200,10 +200,10 @@ def create_menus(self): Construct the menus of the app. """ - menu_bar = self.menuBar() - menu_bar.setNativeMenuBar(False) # disable for macOS, see GH-239 + self.menu_bar = self.menuBar() + self.menu_bar.setNativeMenuBar(False) # disable for macOS, see GH-239 - file_menu = menu_bar.addMenu(self.tr('&File')) + file_menu = self.menu_bar.addMenu(self.tr('&File')) do_open_metadata = file_menu.addAction('&Open metadata') do_open_metadata.setShortcut('Ctrl+O') @@ -238,7 +238,7 @@ def create_menus(self): self.do_launch.setShortcut('Return') self.do_launch.triggered.connect(self.start_launch) - options_menu = menu_bar.addMenu(self.tr('&Options')) + options_menu = self.menu_bar.addMenu(self.tr('&Options')) do_toggle_lazy = options_menu.addAction('&Fast loading') do_toggle_lazy.setStatusTip('Reduces load time and memory usage, disables expensive features like spike detection') @@ -257,7 +257,7 @@ def create_menus(self): do_view_global_config_file = options_menu.addAction('View global &config file') do_view_global_config_file.triggered.connect(self.view_global_config_file) - appearance_menu = menu_bar.addMenu(self.tr('&Appearance')) + appearance_menu = self.menu_bar.addMenu(self.tr('&Appearance')) ui_scale_group = QT.QActionGroup(appearance_menu) ui_scale_actions = {} @@ -288,7 +288,7 @@ def create_menus(self): do_toggle_support_increased_line_width.setChecked(self.support_increased_line_width) do_toggle_support_increased_line_width.triggered.connect(self.toggle_support_increased_line_width) - help_menu = menu_bar.addMenu(self.tr('&Help')) + help_menu = self.menu_bar.addMenu(self.tr('&Help')) self.do_toggle_debug_logging = help_menu.addAction('Show and log &debug messages') self.do_toggle_debug_logging.setCheckable(True) @@ -420,8 +420,7 @@ def start_launch(self): Load data for the selected dataset in a separate thread. """ - self.do_launch.setText('&Launch in progress!') - self.do_launch.setEnabled(False) + self.menu_bar.setEnabled(False) self.metadata_selector.setEnabled(False) self.stacked_layout.setCurrentIndex(1) # show loading label self.load_dataset_thread.start() @@ -470,8 +469,7 @@ def on_load_dataset_finished(self): finally: - self.do_launch.setText('&Launch') - self.do_launch.setEnabled(True) + self.menu_bar.setEnabled(True) self.metadata_selector.setEnabled(True) self.stacked_layout.setCurrentIndex(0) # show metadata selector @@ -565,8 +563,7 @@ def authorize_gdrive(self): button = QT.QMessageBox.question(self, title, text, defaultButton=QT.QMessageBox.Yes) if button == QT.QMessageBox.Yes: - self.do_authorize_gdrive.setText('Continue Google Drive authorization in your browser!') - self.do_authorize_gdrive.setEnabled(False) + self.menu_bar.setEnabled(False) self.metadata_selector.setEnabled(False) self.stacked_layout.setCurrentIndex(2) # show gdrive auth label self.network_thread.start() @@ -585,8 +582,7 @@ def on_gdrive_authorization_finished(self, success): self.statusBar().showMessage('ERROR: Authorization failed ' '(see console for details)', msecs=5000) - self.do_authorize_gdrive.setText('Request Google Drive authorization now') - self.do_authorize_gdrive.setEnabled(True) + self.menu_bar.setEnabled(True) self.metadata_selector.setEnabled(True) self.stacked_layout.setCurrentIndex(0) # show metadata selector