Added initial implementation to WXUpdater class

This commit is contained in:
Manuel Cortez 2022-02-18 16:56:54 -06:00
parent ec214bc387
commit 42b60d19a1
No known key found for this signature in database
GPG Key ID: 9E0735CA15EFE790

143
updater/wxupdater.py Normal file
View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
""" Updater implementation that supports WXPython phoenix.
This module allows you to perform authomatic updates via the updater package by instantiating the :py:class:`WXUpdater` class and calling to the :py:func:`WXUpdater.check_for_updates` function, which retrieves update information from the provided server, ask the user to authorize the update if needed, and displays a progress bar while the update downloads.
:note:
You need to have the WXPython library installed in your system. If you are not using this graphical user interface, probably you might use another update implementation.
Automatic updates can be implemented, within a WXPython application, in the following way:
>>> import wx
>>> from updater.wxupdater import WXUpdater
>>> app = wx.App()
>>> updater = WXUpdater(app_name="My app", current_version="0.1", endpoint="https://some_url.com")
>>> updater.check_for_updates()
>>> app.MainLoop()
Also, you can customize messages in the update dialogs via the parameters :py:data:`WXUpdater.new_update_title`, :py:data:`WXUpdater.new_update_msg`, :py:data:`WXUpdater.update_progress_title`, :py:data:`WXUpdater.update_progress_msg`, :py:data:`WXUpdater.update_almost_complete_title` and :py:data:`WXUpdater.update_almost_complete_msg`,
>>> import wx
>>> from updater.wxupdater import WXUpdater
>>> app = wx.App()
>>> updater = WXUpdater(app_name="My app", current_version="0.1", endpoint="https://some_url.com")
>>> updater.new_update_title = "New version of my awesome app is available"
>>> updater.new_update_msg = "Do you want to get it right now?"
>>> updater.check_for_updates()
>>> app.MainLoop()
"""
import os
import tempfile
import wx # type: ignore
import logging
from typing import Optional, Any, cast
from pubsub import pub # type: ignore
from platform_utils import paths # type: ignore
from . import core, utils
log = logging.getLogger("updater.WXUpdater")
class WXUpdater(core.UpdaterCore):
""" Class to implement updates via a WXPython interface.
:ivar new_update_title: Title to display in the dialog when a new update is available.
:ivar new_update_msg: Text to display to users when an update is available. This text is displayed in a message box. It supports the following variables: {app_name}, {update_version} and {description} which are provided by the update information.
:ivar update_progress_title: Title to display when the update is in progress. Available variables are {total_downloaded} and {total_size}, which are human readable strings of data downloaded.
:ivar update_progress_msg: Text to display while update is downloading. Available variables are {total_downloaded} and {total_size}, which are human readable strings of data downloaded.
:ivar update_almost_complete_title: Title of the message to display to users when the update is about to be installed.
:ivar update_almost_complete_msg: Message to explain to users about the application restart, after updates are applied.
"""
new_update_title: str = "New version for {app_name}"
new_update_msg: str = "There's a new {app_name} version available. Would you like to download it now?\n\n {app_name} version: {update_version}\n\nChanges:\n{update_description}"
update_progress_title: str = "Downloading update..."
update_progress_msg: str = "Updating... {total_downloaded} of {total_size}"
update_almost_complete_title: str = "Done"
update_almost_complete_msg: str = "The update is about to be installed in your system. After being installed, the application will restart. Press OK to continue."
def __init__(self, new_update_title: Optional[str] = None, new_update_msg: Optional[str] = None, update_progress_title: Optional[str] = None, update_progress_msg: Optional[str] = None, update_almost_complete_title: Optional[str] = None, update_almost_complete_msg: Optional[str] = None, *args, **kwargs):
""" class constructor.
It accepts all parameters required by :py:class:`updater.core.UpdaterCore`, plus the following:
:param new_update_title: Title to display in the dialog when a new update is available.
:type new_update_title: str
:param new_update_msg: Text to display to users when an update is available. This text is displayed in a message box. It supports the following variables: {app_name}, {update_version} and {description} which are provided by the update information.
:type new_update_msg: str
:param update_progress_title: Title to display when the update is in progress. Available variables are {total_downloaded} and {total_size}, which are human readable strings of data downloaded.
:type update_progress_title: str
:param update_progress_msg: Text to display while update is downloading. Available variables are {total_downloaded} and {total_size}, which are human readable strings of data downloaded.
:type update_progress_msg: str
:param update_almost_complete_title: Title of the message to display to users when the update is about to be installed.
:type update_almost_complete_title: str
:param update_almost_complete_msg: Message to explain to users about the application restart, after updates are applied.
:type update_almost_complete_msg: str
"""
super(WXUpdater, self).__init__(*args, **kwargs)
if new_update_title:
self.new_update_title = new_update_title
if new_update_msg:
self.new_update_msg = new_update_msg
if update_progress_title:
self.update_progress_title = update_progress_title
if update_progress_msg:
self.update_progress_msg
if update_almost_complete_title:
self.update_almost_complete_title = update_almost_complete_title
if update_almost_complete_msg:
self.update_almost_complete_msg = update_almost_complete_msg
self.progress_dialog: Any = None
def initialize(self) -> None:
pub.subscribe(self.on_update_progress, "updater.update-progress")
def create_progress_dialog(self) -> None:
self.progress_dialog = wx.ProgressDialog(self.update_progress_msg.format(total_downloaded="0", total_size="0"), self.update_progress_title, parent=None, maximum=100)
def on_new_update_available(self) -> bool:
dialog = wx.MessageDialog(None, self.new_update_msg, self.new_update_title, style=wx.YES|wx.NO|wx.ICON_WARNING)
response = dialog.ShowModal()
dialog.Destroy()
if response == wx.ID_YES:
return True
else:
return False
def on_update_progress(self, total_downloaded: int, total_size: int) -> None:
if self.progress_dialog == None:
self.create_progress_dialog()
self.progress_dialog.Show()
if total_downloaded == total_size:
self.progress_dialog.Destroy()
else:
self.progress_dialog.Update((total_downloaded*100)/total_size, self.update_progress_msg.format(total_downloaded=utils.convert_bytes(total_downloaded), total_size=utils.convert_bytes(total_size)))
self.progress_dialog.SetTitle(self.update_progress_msg.format(total_downloaded=utils.convert_bytes(total_downloaded), total_size=utils.convert_bytes(total_size)))
def on_update_almost_complete(self) -> None:
ms = wx.MessageDialog(None, self.update_almost_complete_msg, self.update_almost_complete_title)
return ms.ShowModal()
def check_for_updates(self) -> None:
self.create_session()
self.initialize()
update_info = self.get_update_information()
version_data = self.get_version_data(update_info)
if version_data[0] == False:
return None
response = self.on_new_update_available()
if response == False:
return None
base_path = tempfile.mkdtemp()
print(base_path)
download_path = os.path.join(base_path, 'update.zip')
downloaded = self.download_update(cast(str, version_data[2]), download_path)
update_path = os.path.join(base_path, 'update')
extraction_path = self.extract_update(downloaded, destination=update_path)
bootstrap_exe = self.move_bootstrap(extraction_path)
source_path = os.path.join(paths.app_path(), "sandbox")
self.on_update_almost_complete()
self.execute_bootstrap(bootstrap_exe, source_path)
def __del__(self) -> None:
pub.unsubscribe(self.on_update_progress, "updater.update-progress")