diff --git a/CHANGELOG b/CHANGELOG
index 3a32d2b..d3cd41a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,5 @@
-0.15.6
+0.16.0
+ - feat: check for updates when starting the DCOR-Aid GUI
- fix: when checking resource existence for a dataset, do not call
"package_show" for every single resource
- fix: use `verifiable_files` instead of `verified_files` in error message
diff --git a/dcoraid/gui/main.py b/dcoraid/gui/main.py
index 5c8b012..49a5f4c 100644
--- a/dcoraid/gui/main.py
+++ b/dcoraid/gui/main.py
@@ -17,11 +17,12 @@
from ..api import APIOutdatedError
from ..common import ConnectionTimeoutErrors
from ..dbmodel import APIInterrogator, DBExtract
-from .._version import version as __version__
+from .._version import __version__
from .api import get_ckan_api
from .preferences import PreferencesDialog
from .status_widget import StatusWidget
+from . import updater
from .wizard import SetupWizard
file_manager = ExitStack()
@@ -44,6 +45,9 @@ def __init__(self, *args, **kwargs):
application will print the version after initialization
and exit.
"""
+ self._update_thread = None
+ self._update_worker = None
+
# Settings are stored in the .ini file format. Even though
# `self.settings` may return integer/bool in the same session,
# in the next session, it will reliably return strings. Lists
@@ -115,6 +119,10 @@ def __init__(self, *args, **kwargs):
# User has not done anything yet
self.on_wizard()
+ # check for updates
+ do_update = int(self.settings.value("check for updates", 1))
+ self.on_action_check_update(do_update)
+
self.show()
self.raise_()
@@ -148,6 +156,50 @@ def on_action_about(self):
"DCOR-Aid {}".format(__version__),
about_text)
+ @QtCore.pyqtSlot(bool)
+ def on_action_check_update(self, b):
+ self.settings.setValue("check for updates", int(b))
+ if b and self._update_thread is None:
+ self._update_thread = QtCore.QThread()
+ self._update_worker = updater.UpdateWorker()
+ self._update_worker.moveToThread(self._update_thread)
+ self._update_worker.finished.connect(self._update_thread.quit)
+ self._update_worker.data_ready.connect(
+ self.on_action_check_update_finished)
+ self._update_thread.start()
+
+ ghrepo = "DCOR-dev/DCOR-Aid"
+
+ QtCore.QMetaObject.invokeMethod(
+ self._update_worker,
+ 'processUpdate',
+ QtCore.Qt.ConnectionType.QueuedConnection,
+ QtCore.Q_ARG(str, __version__),
+ QtCore.Q_ARG(str, ghrepo),
+ )
+
+ def on_action_check_update_finished(self, mdict):
+ # cleanup
+ self._update_thread.quit()
+ self._update_thread.wait()
+ self._update_worker = None
+ self._update_thread = None
+ # display message box
+ ver = mdict["version"]
+ web = mdict["releases url"]
+ dlb = mdict["binary url"]
+ msg = QtWidgets.QMessageBox()
+ msg.setWindowTitle(f"DCOR-Aid {ver} available!")
+ msg.setTextFormat(QtCore.Qt.TextFormat.RichText)
+ text = f"You can install DCOR-Aid {ver} "
+ if dlb is not None:
+ text += 'from a direct download. '.format(dlb)
+ else:
+ text += 'by running `pip install --upgrade dcoraid`. '
+ text += 'Visit the official release page!'.format(web)
+ msg.setText(text)
+ msg.exec()
+
@QtCore.pyqtSlot()
def on_action_software(self):
libs = [dclab,
diff --git a/dcoraid/gui/updater.py b/dcoraid/gui/updater.py
new file mode 100644
index 0000000..e0fd39c
--- /dev/null
+++ b/dcoraid/gui/updater.py
@@ -0,0 +1,91 @@
+import json
+import os
+import struct
+import sys
+import traceback
+import urllib.request
+
+from dclab.external.packaging import parse as parse_version
+from PyQt5 import QtCore
+
+
+class UpdateWorker(QtCore.QObject):
+ finished = QtCore.pyqtSignal()
+ data_ready = QtCore.pyqtSignal(dict)
+
+ @QtCore.pyqtSlot(str, str)
+ def processUpdate(self, version, ghrepo):
+ mdict = check_release(ghrepo, version)
+ if mdict["update available"]:
+ self.data_ready.emit(mdict)
+ self.finished.emit()
+
+
+def check_for_update(version, ghrepo):
+ thread = QtCore.QThread()
+ obj = UpdateWorker()
+ obj.moveToThread(thread)
+ obj.finished.connect(thread.quit)
+ thread.start()
+
+ QtCore.QMetaObject.invokeMethod(obj, 'processUpdate',
+ QtCore.Qt.ConnectionType.QueuedConnection,
+ QtCore.Q_ARG(str, version),
+ QtCore.Q_ARG(str, ghrepo),
+ )
+
+
+def check_release(ghrepo="user/repo", version=None, timeout=20):
+ """Check GitHub repository for latest release"""
+ url = "https://api.github.com/repos/{}/releases/latest".format(ghrepo)
+ if "GITHUB_TOKEN" in os.environ:
+ hdr = {'authorization': os.environ["GITHUB_TOKEN"]}
+ else:
+ hdr = {}
+ web = "https://github.com/{}/releases".format(ghrepo)
+ errors = None # error messages (str)
+ update = False # whether an update is available
+ binary = None # download link to binary file
+ new_version = None # string identifying new version
+
+ try:
+ req = urllib.request.Request(url, headers=hdr)
+ data = urllib.request.urlopen(req, timeout=timeout).read()
+ except BaseException:
+ errors = traceback.format_exc()
+ else:
+ j = json.loads(data)
+
+ newversion = j["tag_name"]
+
+ if version is not None:
+ new = parse_version(newversion)
+ old = parse_version(version)
+ if new > old:
+ update = True
+ new_version = newversion
+ if hasattr(sys, "frozen"):
+ # determine which binary URL we need
+ if sys.platform == "win32":
+ nbit = 8 * struct.calcsize("P")
+ if nbit == 32:
+ dlid = "win_32bit_setup.exe"
+ else:
+ dlid = "win_64bit_setup.exe"
+ elif sys.platform == "darwin":
+ dlid = ".pkg"
+ else:
+ dlid = False
+ # search for binary download file
+ if dlid:
+ for a in j["assets"]:
+ if a["browser_download_url"].count(dlid):
+ binary = a["browser_download_url"]
+ break
+ mdict = {"releases url": web,
+ "binary url": binary,
+ "version": new_version,
+ "update available": update,
+ "errors": errors,
+ }
+ return mdict
diff --git a/tests/conftest.py b/tests/conftest.py
index 33cfcfa..ea0fe4c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -50,7 +50,8 @@ def pytest_configure(config):
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
settings = QtCore.QSettings()
settings.setIniCodec("utf-8")
- settings.value("user scenario", "dcor-dev")
+ settings.setValue("check for updates", 0)
+ settings.setValue("user scenario", "dcor-dev")
settings.setValue("auth/server", "dcor-dev.mpl.mpg.de")
settings.setValue("auth/api key", common.get_api_key())
settings.setValue("debug/without timers", "1")
diff --git a/tests/test_gui_updater.py b/tests/test_gui_updater.py
new file mode 100644
index 0000000..dcc0848
--- /dev/null
+++ b/tests/test_gui_updater.py
@@ -0,0 +1,25 @@
+import socket
+
+import pytest
+from dcoraid.gui import updater
+
+
+with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ try:
+ s.connect(("www.python.org", 80))
+ NET_AVAILABLE = True
+ except socket.gaierror:
+ # no internet
+ NET_AVAILABLE = False
+
+
+@pytest.mark.skipif(not NET_AVAILABLE, reason="No network connection!")
+def test_update_basic():
+ mdict = updater.check_release(ghrepo="DCOR-dev/DCOR-Aid",
+ version="0.1.0a1")
+ assert mdict["errors"] is None
+ assert mdict["update available"]
+ mdict = updater.check_release(ghrepo="DCOR-dev/DCOR-Aid",
+ version="8472.0.0")
+ assert mdict["errors"] is None
+ assert not mdict["update available"]