diff --git a/README.rst b/README.rst
index 4078f9c6..e7815836 100644
--- a/README.rst
+++ b/README.rst
@@ -199,7 +199,7 @@ Pinned requirements
.. code:: bash
pip install pip-tools
- pip-compile requirements.in > requirements.txt
+ pip-compile --resolver=backtracking requirements.in > requirements.txt
GTK debugging
^^^^^^^^^^^^^
diff --git a/dtool_lookup_gui/main.py b/dtool_lookup_gui/main.py
index 18964e65..78f325cb 100644
--- a/dtool_lookup_gui/main.py
+++ b/dtool_lookup_gui/main.py
@@ -34,6 +34,8 @@
import dtoolcore
import dtool_lookup_api.core.config
+from dtool_lookup_api.core.LookupClient import authenticate
+
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '4')
@@ -44,7 +46,9 @@
from .models.settings import settings
+
from .views.main_window import MainWindow
+from .views.login_window import LoginWindow
from .utils.logging import _log_nested
@@ -56,7 +60,6 @@
import dtool_lookup_gui.widgets.progress_chart
import dtool_lookup_gui.widgets.progress_popover_menu
-
logger = logging.getLogger(__name__)
from . import __version__
@@ -225,6 +228,11 @@ def do_startup(self):
export_config_action.connect("activate", self.do_export_config)
self.add_action(export_config_action)
+ # renew-token action
+ renew_token_action = Gio.SimpleAction.new("renew-token", GLib.VariantType.new("(sss)"))
+ renew_token_action.connect("activate", self.do_renew_token)
+ self.add_action(renew_token_action)
+
Gtk.Application.do_startup(self)
# custom application-scoped actions
@@ -308,6 +316,27 @@ def do_dtool_config_changed(self):
first when emitting dtool-config-changed signal."""
logger.debug("method handler for 'dtool-config-changed' called.")
+ def do_renew_token(self, action, value):
+ """Request new token."""
+
+ # Unpack the username, password, and auth_url from the tuple variant
+ username, password, auth_url = value.unpack()
+
+ async def retrieve_token(auth_url, username, password):
+ try:
+ token = await authenticate(auth_url, username, password)
+ except Exception as e:
+ logger.error(str(e))
+ return
+
+ dtool_lookup_api.core.config.Config.token = token
+ self.emit('dtool-config-changed')
+
+ asyncio.create_task(retrieve_token(
+ auth_url,
+ username,
+ password))
+
def run_gui():
GObject.type_register(GtkSource.View)
diff --git a/dtool_lookup_gui/models/datasets.py b/dtool_lookup_gui/models/datasets.py
index 80e2cf5e..e6b0c468 100644
--- a/dtool_lookup_gui/models/datasets.py
+++ b/dtool_lookup_gui/models/datasets.py
@@ -26,6 +26,7 @@
import asyncio
import logging
import os
+import json
import yaml
from concurrent.futures import ProcessPoolExecutor
@@ -37,6 +38,8 @@
from dtool_info.inventory import _dataset_info
from dtool_lookup_api.core.LookupClient import ConfigurationBasedLookupClient
+
+
from ..utils.logging import _log_nested
from ..utils.multiprocessing import StatusReportingChildProcessBuilder, process_initializer
from ..utils.progressbar import ProgressBar
@@ -280,12 +283,19 @@ async def query(cls, query_text, *args, **kwargs):
return [await cls.from_lookup(lookup_dict) for lookup_dict in datasets]
@classmethod
- async def query_all(cls, page_number=1, page_size=8, pagination={}):
+ async def query_all(cls, page_number=1, page_size=10, pagination={}):
"""Query all datasets from the lookup server."""
async with ConfigurationBasedLookupClient() as lookup:
datasets = await lookup.all(page_number=page_number, page_size=page_size, pagination=pagination)
return [await cls.from_lookup(lookup_dict) for lookup_dict in datasets]
+ @classmethod
+ async def versions(cls):
+ """To return version info from the server """
+ async with ConfigurationBasedLookupClient() as lookup:
+ version_info = await lookup.versions()
+ print(version_info)
+
def __str__(self):
return self._dataset_info['uri']
diff --git a/dtool_lookup_gui/views/login_window.py b/dtool_lookup_gui/views/login_window.py
new file mode 100644
index 00000000..3267f00e
--- /dev/null
+++ b/dtool_lookup_gui/views/login_window.py
@@ -0,0 +1,81 @@
+import logging
+import os
+import dtoolcore
+from gi.repository import Gio, GLib, Gtk, Gdk
+from dtool_lookup_api.core.config import Config
+from ..utils.about import pretty_version_text
+from ..utils.logging import _log_nested
+from .settings_dialog import SettingsDialog
+
+# Initialize logging
+logger = logging.getLogger(__name__)
+
+@Gtk.Template(filename=f'{os.path.dirname(__file__)}/login_window.ui')
+class LoginWindow(Gtk.Window):
+ __gtype_name__ = 'LoginWindow'
+
+ # Map GTK Template Child widgets to Python attributes
+ username_entry = Gtk.Template.Child()
+ password_entry = Gtk.Template.Child()
+ login_button = Gtk.Template.Child()
+ skip_button = Gtk.Template.Child()
+ settings_button = Gtk.Template.Child()
+
+ # Initialize the LoginWindow instance
+ def __init__(self, follow_up_action=None, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.application = self.get_application()
+ self.settings_dialog = SettingsDialog(application=self.application)
+ self._follow_up_action = follow_up_action
+
+ # Set the default values from the Config, ensuring they are not None
+ if Config.username is not None:
+ self.username_entry.set_text(Config.username)
+ if Config.password is not None: # Consider security implications
+ self.password_entry.set_text(Config.password)
+
+
+
+ # Handle the 'Login' button click event
+ @Gtk.Template.Callback()
+ def on_login_button_clicked(self, widget):
+
+
+
+ # Fetch entered username and password
+ username = self.username_entry.get_text()
+ password = self.password_entry.get_text()
+
+ # Create a GLib Variant tuple for authentication
+ user_pass_auth_variant = GLib.Variant.new_tuple(
+ GLib.Variant.new_string(username),
+ GLib.Variant.new_string(password),
+ GLib.Variant.new_string(Config.auth_url)
+ )
+
+ # Trigger the 'renew-token' action for authentication
+ self.get_action_group("app").activate_action('renew-token', user_pass_auth_variant)
+
+ if self._follow_up_action is not None:
+ self._follow_up_action()
+
+ self.close()
+
+ # Handle the 'Skip' button click event
+ @Gtk.Template.Callback()
+ def on_skip_button_clicked(self, widget):
+ # Close the login window
+ self.close()
+
+ @Gtk.Template.Callback()
+ def settings_button_clicked_cb(self, widget):
+ logger.info("Settings button clicked. Opening settings dialog.")
+ self.settings_dialog.show()
+
+ # Handle the window close event
+ @Gtk.Template.Callback()
+ def on_delete(self, widget, event):
+ # Hide the window instead of deleting it
+ return self.hide_on_delete()
+
+
diff --git a/dtool_lookup_gui/views/login_window.ui b/dtool_lookup_gui/views/login_window.ui
new file mode 100644
index 00000000..1f28ac71
--- /dev/null
+++ b/dtool_lookup_gui/views/login_window.ui
@@ -0,0 +1,193 @@
+
+
+
+
+
+ False
+ 400
+ 300
+
+
+
+
+
+
+
+
+
+
diff --git a/dtool_lookup_gui/views/main_window.py b/dtool_lookup_gui/views/main_window.py
index 52c2a660..5a7bf09b 100644
--- a/dtool_lookup_gui/views/main_window.py
+++ b/dtool_lookup_gui/views/main_window.py
@@ -31,7 +31,7 @@
import urllib.parse
from functools import reduce
-from gi.repository import Gio, GLib, Gtk, GtkSource
+from gi.repository import Gio, GLib, Gtk, GtkSource, Gdk
import dtoolcore.utils
from dtool_info.utils import sizeof_fmt
@@ -60,6 +60,8 @@
from .dataset_name_dialog import DatasetNameDialog
from .about_dialog import AboutDialog
from .settings_dialog import SettingsDialog
+from .server_versions_dialog import ServerVersionsDialog
+from .login_window import LoginWindow
from .log_window import LogWindow
_logger = logging.getLogger(__name__)
@@ -102,8 +104,6 @@ class MainWindow(Gtk.ApplicationWindow):
search_entry = Gtk.Template.Child()
- # copy_dataset_spinner = Gtk.Template.Child()
-
base_uri_list_box = Gtk.Template.Child()
dataset_list_box = Gtk.Template.Child()
@@ -152,11 +152,11 @@ class MainWindow(Gtk.ApplicationWindow):
manifest_view = Gtk.Template.Child()
settings_button = Gtk.Template.Child()
+ version_button = Gtk.Template.Child()
error_bar = Gtk.Template.Child()
error_label = Gtk.Template.Child()
-
first_page_button = Gtk.Template.Child()
prev_page_button = Gtk.Template.Child()
curr_page_button = Gtk.Template.Child()
@@ -166,9 +166,9 @@ class MainWindow(Gtk.ApplicationWindow):
last_page_button = Gtk.Template.Child()
main_statusbar = Gtk.Template.Child()
+ contents_per_page = Gtk.Template.Child()
-
-
+ test_login = Gtk.Template.Child()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -200,6 +200,7 @@ def __init__(self, *args, **kwargs):
self.log_window = LogWindow(application=self.application)
self.settings_dialog = SettingsDialog(application=self.application)
self.about_dialog = AboutDialog(application=self.application)
+ self.server_versions_dialog = ServerVersionsDialog(application=self.application)
# window-scoped actions
@@ -257,8 +258,7 @@ def __init__(self, *args, **kwargs):
_logger.debug(f"Constructed main window for app '{self.application.get_application_id()}'")
self.pagination = {}
-
-
+ self.contents_per_page_value = 10
# utility methods
def refresh(self):
@@ -316,51 +316,73 @@ def _update_search_summary(self, datasets):
def _update_main_statusbar(self):
total_number = self.pagination['total']
current_page = self.pagination['page']
- self.main_statusbar.push(0, f"Total Number of Dataset: {total_number}, Current page: {current_page}")
+ last_page = self.pagination['last_page']
+ self.main_statusbar.push(0,
+ f"Total Number of Datasets: {total_number}, Current Page: {current_page} of {last_page}")
- async def _fetch_search_results(self, keyword, on_show=None, page_number=1, page_size=8):
+ def contents_per_page_changed(self, widget):
+ self.contents_per_page_value = widget.get_active_text()
+ self.on_first_page_button_clicked(self.first_page_button) # Directly call the method
+
+ async def _fetch_search_results(self, keyword, on_show=None, page_number=1, page_size=10, widget=None):
row = self.base_uri_list_box.search_results_row
row.start_spinner()
- self.pagination = {} # Add pagination dictionary
-
-
-
+ self.main_spinner.start()
+ self.pagination = {} # Add pagination dictionary
try:
if keyword:
if is_valid_query(keyword):
_logger.debug("Valid query specified.")
- datasets = await DatasetModel.query(keyword, page_number=page_number, page_size=page_size,
- pagination=self.pagination) # Pass pagination dictionary and
- # page_number, page_size
+ datasets = await DatasetModel.query(
+ keyword,
+ page_number=page_number,
+ page_size=page_size,
+ pagination=self.pagination
+ )
else:
_logger.debug("Specified search text is not a valid query, just perform free text search.")
- # NOTE: server side allows a dict with the key-value pairs
- # "free_text", "creator_usernames", "base_uris", "uuids", "tags",
- # via route '/dataset/search', where all except "free_text"
- # can be lists and are translated to logical "and" or "or"
- # constructs on the server side. With the special treatment
- # of the 'uuid' keyword above, should we introduce similar
- # options for the other available keywords?
- datasets = await DatasetModel.search(keyword, page_number=page_number, page_size=page_size,
- pagination=self.pagination) # Pass pagination dictionary and
- # page_number, page_size
+ datasets = await DatasetModel.search(
+ keyword,
+ page_number=page_number,
+ page_size=page_size,
+ pagination=self.pagination
+ )
else:
_logger.debug("No keyword specified, list all datasets.")
- datasets = await DatasetModel.query_all(page_number=page_number, page_size=page_size,pagination=self.pagination) # Pass pagination dictionary and
-
+ datasets = await DatasetModel.query_all(
+ page_number=page_number,
+ page_size=page_size,
+ pagination=self.pagination
+ )
if len(datasets) > self._max_nb_datasets:
_logger.warning(
f"{len(datasets)} search results exceed allowed displayed maximum of {self._max_nb_datasets}. "
- f"Only the first {self._max_nb_datasets} results are shown. Narrow down your search.")
- datasets = datasets[:self._max_nb_datasets] # Limit number of datasets that are shown
+ f"Only the first {self._max_nb_datasets} results are shown. Narrow down your search."
+ )
+ datasets = datasets[:self._max_nb_datasets] # Limit number of datasets that are shown
+
row.search_results = datasets # Cache datasets
self._update_search_summary(datasets)
self._update_main_statusbar()
+ self.contents_per_page.connect("changed", self.contents_per_page_changed)
if self.base_uri_list_box.get_selected_row() == row:
# Only update if the row is still selected
self.dataset_list_box.fill(datasets, on_show=on_show)
+ except RuntimeError as e:
+ # TODO: There should probably be a more explicit test on authentication failure.
+ self.show_error(e)
+
+ async def retry():
+ await asyncio.sleep(1) # TODO: This is a dirty workaround for not having the login window pop up twice
+ await self._fetch_search_results(keyword, on_show, page_number, page_size, widget)
+
+ # What happens is that the LoginWindow evokes the renew-token action via Gtk framework.
+ # This happens asynchronously as well. This means _fetch_search_results called again
+ # within the retry() function would open another LoginWindow here as the token renewal does
+ # not happen "quick" enough. Hence there is the asybcio.sleep(1).
+ LoginWindow(application=self.application, follow_up_action=lambda: asyncio.create_task(retry())).show()
except Exception as e:
self.show_error(e)
@@ -368,7 +390,9 @@ async def _fetch_search_results(self, keyword, on_show=None, page_number=1, page
self.base_uri_list_box.select_search_results_row()
self.main_stack.set_visible_child(self.main_paned)
row.stop_spinner()
-
+ self.main_spinner.stop()
+ # If a widget was passed in, re-enable it
+ self.enable_pagination_buttons()
def _search_by_uuid(self, uuid):
@@ -458,10 +482,6 @@ def _select_base_uri_row_by_uri(self, uri):
index = self.base_uri_list_box.get_row_index_from_uri(uri)
self._select_base_uri_row_by_row_index(index)
- # def _show_base_uri(self, dataset):
- # asyncio.create_task(self._update_dataset_view(dataset))
- # self.dataset_stack.set_visible_child(self.dataset_box)
-
# actions
# dataset selection actions
@@ -540,8 +560,6 @@ async def _get_item(dataset, item_uuid):
asyncio.create_task(_get_item(dataset, item_uuid))
-
-
def do_refresh_view(self, action, value):
"""Refresh view by reloading base uri list, """
self.refresh()
@@ -551,11 +569,18 @@ def do_refresh_view(self, action, value):
def on_settings_clicked(self, widget):
self.settings_dialog.show()
+ @Gtk.Template.Callback()
+ def version_button_clicked(self, widget):
+ self.server_versions_dialog.show()
+
+ @Gtk.Template.Callback()
+ def on_test_login_clicked(self, widget):
+ self.login_window.show()
+
@Gtk.Template.Callback()
def on_logging_clicked(self, widget):
self.log_window.show()
-
@Gtk.Template.Callback()
def on_about_clicked(self, widget):
self.about_dialog.show()
@@ -726,7 +751,6 @@ def on_add_items_clicked(self, widget):
pass
dialog.destroy()
-
@Gtk.Template.Callback()
def on_manifest_row_activated(self, tree_view, path, column):
"""Handler for "row-activated" signal.
@@ -792,74 +816,103 @@ def on_error_bar_response(self, widget, response_id):
@Gtk.Template.Callback()
def on_first_page_button_clicked(self, widget):
- page_number = 1
- self.pagination['page'] = page_number
- asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
-
+ if not self.fetching_results:
+ page_number = 1
+ self.pagination['page'] = page_number
+ self.disable_pagination_buttons()
+ asyncio.create_task(
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
+ self.curr_page_button.set_label(str(page_number))
+ self.page_advancer_button.set_label(str(page_number - 1))
+ self.next_page_advancer_button.set_label(str(page_number + 1))
@Gtk.Template.Callback()
def on_nextoption_page_button_clicked(self, widget):
- if self.pagination['page'] < self.pagination['last_page']:
+ if not self.fetching_results and self.pagination['page'] < self.pagination['last_page']:
page_number = self.pagination['page'] + 1
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
@Gtk.Template.Callback()
def on_last_page_button_clicked(self, widget):
page_number = self.pagination['last_page']
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
@Gtk.Template.Callback()
def on_prev_page_button_clicked(self, widget):
- if self.pagination['page'] > 1:
+ if not self.fetching_results and self.pagination['page'] > 1:
self.pagination['page'] -= 1
page_number = self.pagination['page']
- self.curr_page_button.set_label(str(page_number)) # Update curr_page_button label
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
@Gtk.Template.Callback()
def curr_page_button_clicked(self, widget):
page_number = self.pagination['page']
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
@Gtk.Template.Callback()
def page_advancer_button_clicked(self, widget):
page_number = self.pagination['page']
- if page_number > 1:
+ if not self.fetching_results and page_number > 1:
page_number -= 1
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
@Gtk.Template.Callback()
def next_page_advancer_button_clicked(self, widget):
- if self.pagination['page'] < self.pagination['last_page']:
- page_number = self.pagination['page'] + 1
+ page_number = self.pagination['page']
+ if not self.fetching_results and page_number < self.pagination['last_page']:
+ page_number += 1
+ self.update_pagination_buttons(page_number, widget)
+ self.disable_pagination_buttons()
asyncio.create_task(
- self._fetch_search_results(keyword=None, on_show=None, page_number=page_number))
- self.curr_page_button.set_label(str(page_number))
- self.page_advancer_button.set_label(str(page_number - 1))
- self.next_page_advancer_button.set_label(str(page_number + 1))
+ self._fetch_search_results(keyword=None, on_show=None, page_number=page_number,
+ page_size=int(self.contents_per_page_value), widget=widget))
+
+ def update_pagination_buttons(self, page_number, widget):
+ self.pagination['page'] = page_number
+ self.curr_page_button.set_label(str(page_number))
+ self.page_advancer_button.set_label(str(page_number - 1))
+ self.next_page_advancer_button.set_label(str(page_number + 1))
+
+ def disable_pagination_buttons(self):
+ self.fetching_results = True
+ self.first_page_button.set_sensitive(False)
+ self.nextoption_page_button.set_sensitive(False)
+ self.last_page_button.set_sensitive(False)
+ self.prev_page_button.set_sensitive(False)
+ self.curr_page_button.set_sensitive(False)
+ self.page_advancer_button.set_sensitive(False)
+ self.next_page_advancer_button.set_sensitive(False)
+
+ def enable_pagination_buttons(self):
+ self.fetching_results = False
+ self.first_page_button.set_sensitive(True)
+ self.nextoption_page_button.set_sensitive(True)
+ self.last_page_button.set_sensitive(True)
+ self.prev_page_button.set_sensitive(True)
+ self.curr_page_button.set_sensitive(True)
+ self.page_advancer_button.set_sensitive(True)
+ self.next_page_advancer_button.set_sensitive(True)
# @Gtk.Template.Callback(), not in .ui
def on_copy_clicked(self, widget):
@@ -1005,4 +1058,4 @@ async def _compute_dependencies(self, dataset):
self.dependency_stack.set_visible_child(self.dependency_view)
def show_error(self, exception):
- _logger.error(traceback.format_exc())
+ _logger.error(traceback.format_exc())
\ No newline at end of file
diff --git a/dtool_lookup_gui/views/main_window.ui b/dtool_lookup_gui/views/main_window.ui
index 47aa6c07..8f7f232e 100644
--- a/dtool_lookup_gui/views/main_window.ui
+++ b/dtool_lookup_gui/views/main_window.ui
@@ -59,6 +59,34 @@
2
+
+
+ True
+ True
+ True
+ Version
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ True
+ True
+ True
+ Test Login
+
+
+
+ False
+ True
+ 4
+
+
submenu
@@ -183,10 +211,71 @@
True
False
-
+
True
False
-
+ vertical
+
+
+ True
+ False
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ True
+ False
+ 10
+ 10
+
+
+ True
+ False
+ start
+ False
+ Contents per Page:
+ 0
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ start
+ False
+ 0
+
+ - 10
+ - 25
+ - 50
+ - 75
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
@@ -259,10 +348,14 @@
First
+ 30
True
True
True
+
0
@@ -272,10 +365,15 @@
Prev
+ 100
+ 30
True
True
True
+
1
@@ -285,10 +383,15 @@
Last
+ 100
+ 30
True
True
True
+
6
@@ -298,10 +401,15 @@
Next
+ 100
+ 30
True
True
True
+
5
@@ -311,10 +419,15 @@
2
+ 100
+ 30
True
True
True
+
4
@@ -324,10 +437,15 @@
1
+ 100
+ 30
True
True
True
+
3
@@ -337,10 +455,15 @@
0
+ 100
+ 30
True
True
True
+
2
diff --git a/dtool_lookup_gui/views/server_versions_dialog.py b/dtool_lookup_gui/views/server_versions_dialog.py
new file mode 100644
index 00000000..835625a8
--- /dev/null
+++ b/dtool_lookup_gui/views/server_versions_dialog.py
@@ -0,0 +1,52 @@
+import asyncio
+import logging
+import os
+import gi
+
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+from dtool_lookup_api.asynchronous import versions
+from ..utils.logging import _log_nested
+
+# Set up logger for this module
+logger = logging.getLogger(__name__)
+
+
+@Gtk.Template(filename=f'{os.path.dirname(__file__)}/server_versions_dialog.ui')
+class ServerVersionsDialog(Gtk.Window):
+ __gtype_name__ = 'ServerVersionsDialog'
+ server_versions_label = Gtk.Template.Child()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Apply margins to the server versions label for better aesthetics
+ self.server_versions_label.set_margin_start(10)
+ self.server_versions_label.set_margin_end(10)
+ self.server_versions_label.set_margin_top(10)
+ self.server_versions_label.set_margin_bottom(10)
+
+ async def _retrieve_versions(self):
+ """Asynchronously fetch server versions and update the label with formatted information."""
+ server_versions = await versions()
+ version_info = self._format_server_versions(server_versions)
+ _log_nested(logger.info, version_info)
+ self.server_versions_label.set_markup(version_info)
+
+ def _format_server_versions(self, server_versions):
+ """Format server versions, sorting components by name length."""
+ sorted_components = sorted(server_versions.keys(), key=len)
+
+ # Use list comprehension for concise formatting of server versions
+ formatted_versions = [f"{component}: {server_versions[component]}" for component in sorted_components]
+ return "\n".join(formatted_versions)
+
+ @Gtk.Template.Callback()
+ def on_show(self, widget):
+ """Callback executed when the window is shown; fetches server versions."""
+ asyncio.create_task(self._retrieve_versions())
+
+ @Gtk.Template.Callback()
+ def on_delete(self, widget, event):
+ """Callback executed when window close event is triggered; hides the window instead of deleting."""
+ return self.hide_on_delete()
diff --git a/dtool_lookup_gui/views/server_versions_dialog.ui b/dtool_lookup_gui/views/server_versions_dialog.ui
new file mode 100644
index 00000000..18b93219
--- /dev/null
+++ b/dtool_lookup_gui/views/server_versions_dialog.ui
@@ -0,0 +1,82 @@
+
+
+
+
+
+ False
+ False
+ True
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ 5
+ 5
+ Server Versions
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ 5
+ 5
+ version info
+ True
+ True
+
+
+ False
+ True
+ 2
+
+
+
+
+ True
+ False
+
+
+ False
+ True
+ 3
+
+
+
+
+
+
+
+
+
diff --git a/dtool_lookup_gui/views/settings_dialog.py b/dtool_lookup_gui/views/settings_dialog.py
index fd945e80..6c54a38c 100644
--- a/dtool_lookup_gui/views/settings_dialog.py
+++ b/dtool_lookup_gui/views/settings_dialog.py
@@ -172,7 +172,7 @@ async def _refresh_list_of_endpoints(self):
logger.debug("Done refreshing settings dialog.")
def on_dtool_config_changed(self, widget):
- """Signal handler for dtool-method-changed."""
+ """Signal handler for dtool-config-changed."""
self._refresh_settings_dialog()
# signal handlers
@@ -205,14 +205,13 @@ def on_delete(self, widget, event):
@Gtk.Template.Callback()
def on_renew_token_clicked(self, widget):
+ # show authentication dialogue and get username and password
def authenticate(username, password):
- asyncio.create_task(self.retrieve_token(
- self.authenticator_url_entry.get_text(),
- username,
- password))
-
- # Reconnect since settings may have been changed
- #asyncio.create_task(self.main_application.lookup_tab.connect())
+ auth_url = self.authenticator_url_entry.get_text()
+ user_pass_auth_variant = GLib.Variant.new_tuple(GLib.Variant.new_string(username),
+ GLib.Variant.new_string(password),
+ GLib.Variant.new_string(auth_url))
+ self.get_action_group("app").activate_action('renew-token', user_pass_auth_variant)
AuthenticationDialog(authenticate, Config.username, Config.password).show()
@@ -275,17 +274,6 @@ def on_item_download_directory_file_chooser_button_file_set(self, widget):
logger.debug("Selected default item download directory '%s'.", item_download_directory)
settings.item_download_directory = item_download_directory
- async def retrieve_token(self, auth_url, username, password):
- #self.main_application.error_bar.hide()
- try:
- token = await authenticate(auth_url, username, password)
- except Exception as e:
- logger.error(str(e))
- return
- #self.builder.get_object('token-entry').set_text(token)
- self.token_entry.set_text(token)
- await self._refresh_list_of_endpoints()
-
_configuration_dialogs = {
's3': S3ConfigurationDialog,
'smb': SMBConfigurationDialog,
diff --git a/requirements.txt b/requirements.txt
index 99cbd7a2..ea5a9b1b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,9 +2,9 @@
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
-# pip-compile requirements.in
+# pip-compile --resolver=backtracking requirements.in
#
-aiohttp==3.8.4
+aiohttp==3.8.5
# via
# -r requirements.in
# dtool-lookup-api
@@ -12,17 +12,17 @@ aiosignal==1.3.1
# via aiohttp
asgiref==3.7.2
# via dtool-lookup-api
-async-timeout==4.0.2
+async-timeout==4.0.3
# via aiohttp
attrs==23.1.0
# via aiohttp
-boto3==1.28.2
+boto3==1.28.25
# via dtool-s3
-botocore==1.31.2
+botocore==1.31.25
# via
# boto3
# s3transfer
-certifi==2023.5.7
+certifi==2023.7.22
# via requests
cffi==1.15.1
# via cryptography
@@ -30,7 +30,7 @@ charset-normalizer==3.2.0
# via
# aiohttp
# requests
-click==8.1.4
+click==8.1.6
# via
# click-plugins
# dtool-cli
@@ -39,7 +39,7 @@ click==8.1.4
# dtool-s3
click-plugins==1.1.1
# via dtool-cli
-cryptography==41.0.2
+cryptography==41.0.3
# via jwt
dtool-cli==0.7.1
# via
@@ -52,7 +52,7 @@ dtool-http==0.5.1
# via dtool-create
dtool-info==0.16.2
# via -r requirements.in
-dtool-lookup-api==0.6.1
+dtool-lookup-api==0.7.0
# via -r requirements.in
dtool-s3==0.14.1
# via -r requirements.in
@@ -71,7 +71,7 @@ dtoolcore==3.18.2
# dtool-s3
# dtool-smb
# dtool-symlink
-frozenlist==1.3.3
+frozenlist==1.4.0
# via
# aiohttp
# aiosignal
@@ -107,7 +107,7 @@ pycairo==1.24.0
# via pygobject
pycparser==2.21
# via cffi
-pygments==2.15.1
+pygments==2.16.1
# via dtool-info
pygobject==3.44.1
# via
@@ -117,7 +117,7 @@ pysmb==1.2.9.1
# via dtool-smb
python-dateutil==2.8.2
# via botocore
-pyyaml==6.0
+pyyaml==6.0.1
# via
# -r requirements.in
# dtool-lookup-api
@@ -135,7 +135,7 @@ scipy==1.10.1
# via -r requirements.in
six==1.16.0
# via python-dateutil
-tqdm==4.65.0
+tqdm==4.66.1
# via pysmb
typing-extensions==4.7.1
# via asgiref
diff --git a/setup.py b/setup.py
index 66107dd3..c01b8a9c 100644
--- a/setup.py
+++ b/setup.py
@@ -70,7 +70,7 @@ def local_scheme(version):
'dtoolcore>=3.17',
'dtool-create>=0.23.4',
'dtool-info>=0.16.2',
- 'dtool-lookup-api>=0.5',
+ 'dtool-lookup-api>=0.7.0',
'aiohttp>=3.6',
'gbulb>=0.6',
'pyyaml>=5.3',
diff --git a/test/test_app.py b/test/test_app.py
index 7c3e4a64..9b7c74eb 100644
--- a/test/test_app.py
+++ b/test/test_app.py
@@ -5,6 +5,7 @@
from dtool_lookup_gui.views.about_dialog import AboutDialog
from dtool_lookup_gui.views.settings_dialog import SettingsDialog
from dtool_lookup_gui.views.log_window import LogWindow
+from dtool_lookup_gui.views.login_window import LoginWindow
from dtool_lookup_gui.views.main_window import MainWindow
logger = logging.getLogger(__name__)
@@ -34,7 +35,7 @@ async def test_app_id(app):
@pytest.mark.asyncio
async def test_app_window_types(app):
window_types = [type(win) for win in app.get_windows()]
- assert set(window_types) == set([AboutDialog, SettingsDialog, LogWindow, MainWindow])
+ assert set(window_types) == set([AboutDialog, SettingsDialog, LogWindow, LoginWindow, MainWindow])
@pytest.mark.asyncio