Skip to content

Commit

Permalink
feat: check for updates when starting the DCOR-Aid GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmueller committed Nov 22, 2024
1 parent ce72693 commit 8943f16
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 3 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
54 changes: 53 additions & 1 deletion dcoraid/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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_()

Expand Down Expand Up @@ -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 <a href="{}">direct download</a>. '.format(dlb)
else:
text += 'by running `pip install --upgrade dcoraid`. '
text += 'Visit the <a href="{}">official release page</a>!'.format(web)
msg.setText(text)
msg.exec()

@QtCore.pyqtSlot()
def on_action_software(self):
libs = [dclab,
Expand Down
91 changes: 91 additions & 0 deletions dcoraid/gui/updater.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
25 changes: 25 additions & 0 deletions tests/test_gui_updater.py
Original file line number Diff line number Diff line change
@@ -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"]

0 comments on commit 8943f16

Please sign in to comment.