Groundwork for installing/updating packages

Contributes to: CURA-8587
This commit is contained in:
Jelle Spijker 2021-12-02 18:02:49 +01:00
parent 08067432c6
commit 3b3d986058
No known key found for this signature in database
GPG key ID: 6662DC033BE6B99A
4 changed files with 73 additions and 14 deletions

View file

@ -20,12 +20,16 @@ class CuraPackageManager(PackageManager):
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None: def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent) super().__init__(application, parent)
self._locally_installed_packages = None self._locally_installed_packages = None
self.installedPackagesChanged.connect(self._updateLocallyInstalledPackages)
def _updateLocallyInstalledPackages(self):
self._locally_installed_packages = list(self.iterateAllLocalPackages())
@property @property
def locally_installed_packages(self): def locally_installed_packages(self):
"""locally installed packages, lazy execution""" """locally installed packages, lazy execution"""
if self._locally_installed_packages is None: if self._locally_installed_packages is None:
self._locally_installed_packages = list(self.iterateAllLocalPackages()) self._updateLocallyInstalledPackages()
return self._locally_installed_packages return self._locally_installed_packages
@locally_installed_packages.setter @locally_installed_packages.setter

View file

@ -89,7 +89,8 @@ class LocalPackageList(PackageList):
return return
for package_data in response_data["data"]: for package_data in response_data["data"]:
index = self.find("package", package_data["package_id"]) package = self._getPackageModel(package_data["package_id"])
self.getItem(index)["package"].canUpdate = True package.download_url = package_data.get("download_url", "")
package.canUpdate = True
self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True) self.sort(attrgetter("sectionTitle", "canUpdate", "displayName"), key = "package", reverse = True)

View file

@ -1,18 +1,21 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import tempfile
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from typing import Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.TaskManagement.HttpRequestManager import HttpRequestData # To request the package list from the API from UM.TaskManagement.HttpRequestManager import HttpRequestData , HttpRequestManager
from UM.Logger import Logger from UM.Logger import Logger
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
from .PackageModel import PackageModel
if TYPE_CHECKING: if TYPE_CHECKING:
from PyQt5.QtCore import QObject from PyQt5.QtCore import QObject
@ -24,6 +27,7 @@ class PackageList(ListModel):
such as Packages obtained from Remote or Local source such as Packages obtained from Remote or Local source
""" """
PackageRole = Qt.UserRole + 1 PackageRole = Qt.UserRole + 1
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
def __init__(self, parent: Optional["QObject"] = None) -> None: def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -33,6 +37,8 @@ class PackageList(ListModel):
self._is_loading = False self._is_loading = False
self._has_more = False self._has_more = False
self._has_footer = True self._has_footer = True
self._to_install: Dict[str, str] = {}
self.canInstallChanged.connect(self._install)
self._ongoing_request: Optional[HttpRequestData] = None self._ongoing_request: Optional[HttpRequestData] = None
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
@ -105,13 +111,60 @@ class PackageList(ListModel):
def _connectManageButtonSignals(self, package): def _connectManageButtonSignals(self, package):
package.installPackageTriggered.connect(self.installPackage) package.installPackageTriggered.connect(self.installPackage)
package.uninstallPackageTriggered.connect(self.uninstallPackage) package.uninstallPackageTriggered.connect(self.uninstallPackage)
package.updatePackageTriggered.connect(self.updatePackage) package.updatePackageTriggered.connect(self.installPackage)
package.enablePackageTriggered.connect(self.enablePackage) package.enablePackageTriggered.connect(self.enablePackage)
package.disablePackageTriggered.connect(self.disablePackage) package.disablePackageTriggered.connect(self.disablePackage)
def _getPackageModel(self, package_id: str) -> PackageModel:
index = self.find("package", package_id)
return self.getItem(index)["package"]
canInstallChanged = pyqtSignal(str, bool)
def download(self, package_id, url, update: bool = False):
def downloadFinished(reply: "QNetworkReply") -> None:
self._downloadFinished(package_id, reply, update)
HttpRequestManager.getInstance().get(
url,
scope = self._scope,
callback = downloadFinished
)
def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None:
try:
with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
temp_file.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
Logger.debug(f"Finished downloading {package_id} and stored it as {temp_file.name}")
self._to_install[package_id] = temp_file.name
self.canInstallChanged.emit(package_id, update)
except IOError as e:
Logger.logException("e", "Failed to write downloaded package to temp file", e)
temp_file.close()
@pyqtSlot(str) @pyqtSlot(str)
def installPackage(self, package_id): def installPackage(self, package_id: str) -> None:
package = self._getPackageModel(package_id)
url = package.download_url
Logger.debug(f"Trying to download and install {package_id} from {url}")
self.download(package_id, url)
def _install(self, package_id: str, update: bool = False) -> None:
package_path = self._to_install.pop(package_id)
Logger.debug(f"Installing {package_id}") Logger.debug(f"Installing {package_id}")
to_be_installed = self._manager.installPackage(package_path) != None
package = self._getPackageModel(package_id)
if package.canUpdate and to_be_installed:
package.canUpdate = False
package.setManageInstallState(to_be_installed)
if update:
package.setIsUpdating(False)
else:
package.setIsInstalling(False)
@pyqtSlot(str) @pyqtSlot(str)
def uninstallPackage(self, package_id): def uninstallPackage(self, package_id):
@ -119,7 +172,10 @@ class PackageList(ListModel):
@pyqtSlot(str) @pyqtSlot(str)
def updatePackage(self, package_id): def updatePackage(self, package_id):
Logger.debug(f"Updating {package_id}") package = self._getPackageModel(package_id)
url = package.download_url
Logger.debug(f"Trying to download and update {package_id} from {url}")
self.download(package_id, url, True)
@pyqtSlot(str) @pyqtSlot(str)
def enablePackage(self, package_id): def enablePackage(self, package_id):

View file

@ -17,11 +17,11 @@ RowLayout
property string busySecondaryText: busyMessageText.text property string busySecondaryText: busyMessageText.text
property string mainState: "primary" property string mainState: "primary"
property bool enabled: true property bool enabled: true
readonly property bool busy: state == "busy" property bool busy: false
signal clicked(bool primary_action) signal clicked(bool primary_action)
state: mainState state: busy ? "busy" : mainState
Cura.PrimaryButton Cura.PrimaryButton
{ {
@ -32,7 +32,6 @@ RowLayout
onClicked: onClicked:
{ {
manageButton.clicked(true) manageButton.clicked(true)
manageButton.state = "busy"
} }
} }
@ -45,7 +44,6 @@ RowLayout
onClicked: onClicked:
{ {
manageButton.clicked(false) manageButton.clicked(false)
manageButton.state = "busy"
} }
} }
@ -155,7 +153,7 @@ RowLayout
PropertyChanges PropertyChanges
{ {
target: busyMessage target: busyMessage
visible: true visible: manageButton.visible
} }
} }
] ]