diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index af0a0748e7..4d8d6b12c6 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -7,7 +7,7 @@ import tempfile import platform from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union -from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from UM.Logger import Logger @@ -28,6 +28,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack + from cura.TaskManagement.HttpNetworkRequestManager import HttpNetworkRequestData i18n_catalog = i18nCatalog("cura") @@ -45,15 +46,13 @@ class Toolbox(QObject, Extension): self._api_url = None # type: Optional[str] # Network: - self._download_request = None # type: Optional[QNetworkRequest] - self._download_reply = None # type: Optional[QNetworkReply] + self._download_request_data = None # type: Optional[HttpNetworkRequestData] self._download_progress = 0 # type: float self._is_downloading = False # type: bool - self._network_manager = None # type: Optional[QNetworkAccessManager] - self._request_headers = [] # type: List[Tuple[bytes, bytes]] + self._request_headers = dict() # type: Dict[str, str] self._updateRequestHeader() - self._request_urls = {} # type: Dict[str, QUrl] + self._request_urls = {} # type: Dict[str, str] self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated self._old_plugin_ids = set() # type: Set[str] self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] @@ -142,20 +141,15 @@ class Toolbox(QObject, Extension): self._fetchPackageData() def _updateRequestHeader(self): - self._request_headers = [ - (b"User-Agent", - str.encode( - "%s/%s (%s %s)" % ( - self._application.getApplicationName(), - self._application.getVersion(), - platform.system(), - platform.machine(), - ) - )) - ] + self._request_headers = { + "User-Agent": "%s/%s (%s %s)" % (self._application.getApplicationName(), + self._application.getVersion(), + platform.system(), + platform.machine()) + } access_token = self._application.getCuraAPI().account.accessToken if access_token: - self._request_headers.append((b"Authorization", "Bearer {}".format(access_token).encode())) + self._request_headers["Authorization"] = "Bearer {}".format(access_token) def _resetUninstallVariables(self) -> None: self._package_id_to_uninstall = None # type: Optional[str] @@ -165,13 +159,11 @@ class Toolbox(QObject, Extension): @pyqtSlot(str, int) def ratePackage(self, package_id: str, rating: int) -> None: - url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url = self._api_url, package_id = package_id)) - - self._rate_request = QNetworkRequest(url) - for header_name, header_value in self._request_headers: - cast(QNetworkRequest, self._rate_request).setRawHeader(header_name, header_value) + url = "{base_url}/packages/{package_id}/ratings".format(base_url = self._api_url, package_id = package_id) data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating) - self._rate_reply = cast(QNetworkAccessManager, self._network_manager).put(self._rate_request, data.encode()) + + self._application.getHttpNetworkRequestManager().put(url, headers_dict = self._request_headers, + data = data.encode()) @pyqtSlot(result = str) def getLicenseDialogPluginName(self) -> str: @@ -213,11 +205,11 @@ class Toolbox(QObject, Extension): installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions) self._request_urls = { - "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), - "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), - "updates": QUrl("{base_url}/packages/package-updates?installed_packages={query}".format( - base_url = self._api_url, query = installed_packages_query)), - "subscribed_packages": QUrl(self._api_url_user_packages) + "authors": "{base_url}/authors".format(base_url = self._api_url), + "packages": "{base_url}/packages".format(base_url = self._api_url), + "updates": "{base_url}/packages/package-updates?installed_packages={query}".format( + base_url = self._api_url, query = installed_packages_query), + "subscribed_packages": self._api_url_user_packages, } self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) @@ -226,26 +218,13 @@ class Toolbox(QObject, Extension): # On boot we check which packages have updates. if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check") and len(installed_package_ids_with_versions) > 0: # Request the latest and greatest! - self._fetchPackageUpdates() + self._makeRequestByType("updates") self._fetchUserSubscribedPackages() - def _prepareNetworkManager(self): - if self._network_manager is not None: - self._network_manager.finished.disconnect(self._onRequestFinished) - self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged) - self._network_manager = QNetworkAccessManager() - self._network_manager.finished.connect(self._onRequestFinished) - self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged) - - def _fetchPackageUpdates(self): - self._prepareNetworkManager() - self._makeRequestByType("updates") - - def _fetchPackageData(self): - self._prepareNetworkManager() + @pyqtSlot() + def browsePackages(self) -> None: # Make remote requests: - self._makeRequestByType("packages") - self._makeRequestByType("authors") + self._fetchPackageData() # Gather installed packages: self._updateInstalledModels() @@ -254,10 +233,13 @@ class Toolbox(QObject, Extension): self._prepareNetworkManager() self._makeRequestByType("subscribed_packages") + def _fetchPackageData(self) -> None: + self._makeRequestByType("packages") + self._makeRequestByType("authors") + # Displays the toolbox @pyqtSlot() def launch(self) -> None: - if not self._dialog: self._dialog = self._createDialog("Toolbox.qml") @@ -268,7 +250,6 @@ class Toolbox(QObject, Extension): self._restart() self._dialog.show() - # Apply enabled/disabled state to installed plugins self.enabledChanged.emit() @@ -576,52 +557,85 @@ class Toolbox(QObject, Extension): # Make API Calls # -------------------------------------------------------------------------- def _makeRequestByType(self, request_type: str) -> None: - Logger.log("d", "Requesting '%s' metadata from server.", request_type) - request = QNetworkRequest(self._request_urls[request_type]) - for header_name, header_value in self._request_headers: - request.setRawHeader(header_name, header_value) + Logger.log("d", "Requesting [%s] metadata from server.", request_type) self._updateRequestHeader() - if self._network_manager: - self._network_manager.get(request) + url = self._request_urls[request_type] + + callback = lambda r, rt = request_type: self._onAuthorsDataRequestFinished(rt, r) + error_callback = lambda r, e, rt = request_type: self._onAuthorsDataRequestFinished(rt, r, e) + self._application.getHttpNetworkRequestManager().get(url, + headers_dict = self._request_headers, + callback = callback, + error_callback = error_callback) + + def _onAuthorsDataRequestFinished(self, request_type: str, + reply: "QNetworkReply", + error: Optional["QNetworkReply.NetworkError"] = None) -> None: + if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("w", + "Unable to connect with the server, we got a response code %s while trying to connect to %s", + reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url()) + self.setViewPage("errored") + return + + try: + json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) + + # Check for errors: + if "errors" in json_data: + for error in json_data["errors"]: + Logger.log("e", "%s", error["title"]) + return + + # Create model and apply metadata: + if not self._models[request_type]: + Logger.log("e", "Could not find the %s model.", request_type) + return + + self._server_response_data[request_type] = json_data["data"] + self._models[request_type].setMetadata(self._server_response_data[request_type]) + + if request_type == "packages": + self._models[request_type].setFilter({"type": "plugin"}) + self.reBuildMaterialsModels() + self.reBuildPluginsModels() + elif request_type == "authors": + self._models[request_type].setFilter({"package_types": "material"}) + self._models[request_type].setFilter({"tags": "generic"}) + + self.metadataChanged.emit() + + if self.isLoadingComplete(): + self.setViewPage("overview") + + except json.decoder.JSONDecodeError: + Logger.log("w", "Received invalid JSON for %s.", request_type) @pyqtSlot(str) def startDownload(self, url: str) -> None: Logger.log("i", "Attempting to download & install package from %s.", url) - url = QUrl(url) - self._download_request = QNetworkRequest(url) - if hasattr(QNetworkRequest, "FollowRedirectsAttribute"): - # Patch for Qt 5.6-5.8 - cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) - if hasattr(QNetworkRequest, "RedirectPolicyAttribute"): - # Patch for Qt 5.9+ - cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True) - for header_name, header_value in self._request_headers: - cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value) - self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request) + + callback = lambda r: self._onDownloadFinished(r) + error_callback = lambda r, e: self._onDownloadFailed(r, e) + download_progress_callback = self._onDownloadProgress + request_data = self._application.getHttpNetworkRequestManager().get(url, headers_dict = self._request_headers, + callback = callback, + error_callback = error_callback, + download_progress_callback = download_progress_callback) + + self._download_request_data = request_data self.setDownloadProgress(0) self.setIsDownloading(True) - cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress) @pyqtSlot() def cancelDownload(self) -> None: - Logger.log("i", "User cancelled the download of a package.") + Logger.log("i", "User cancelled the download of a package. request %s", self._download_request_data) + if self._download_request_data is not None: + self._application.getHttpNetworkRequestManager().abortRequest(self._download_request_data) + self._download_request_data = None self.resetDownload() def resetDownload(self) -> None: - if self._download_reply: - try: - self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) - except (TypeError, RuntimeError): # Raised when the method is not connected to the signal yet. - pass # Don't need to disconnect. - try: - self._download_reply.abort() - except RuntimeError: - # In some cases the garbage collector is a bit to agressive, which causes the dowload_reply - # to be deleted (especially if the machine has been put to sleep). As we don't know what exactly causes - # this (The issue probably lives in the bowels of (py)Qt somewhere), we can only catch and ignore it. - pass - self._download_reply = None - self._download_request = None self.setDownloadProgress(0) self.setIsDownloading(False) @@ -730,30 +744,31 @@ class Toolbox(QObject, Extension): for package in self._server_response_data["packages"]: self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) + def _onDownloadFinished(self, reply: "QNetworkReply") -> None: + self.resetDownload() + + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("w", "Failed to download package. The following error was returned: %s", + json.loads(reply.readAll().data().decode("utf-8"))) + return + # Must not delete the temporary file on Windows + self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) + file_path = self._temp_plugin_file.name + # Write first and close, otherwise on Windows, it cannot read the file + self._temp_plugin_file.write(reply.readAll()) + self._temp_plugin_file.close() + self._onDownloadComplete(file_path) + + def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None: + Logger.log("w", "Failed to download package. The following error was returned: %s", error) + + self.resetDownload() + def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None: if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 self.setDownloadProgress(new_progress) - if bytes_sent == bytes_total: - self.setIsDownloading(False) - self._download_reply = cast(QNetworkReply, self._download_reply) - self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) - - # Check if the download was sucessfull - if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - try: - Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8"))) - except json.decoder.JSONDecodeError: - Logger.logException("w", "Failed to download package and failed to parse a response from it") - finally: - return - # Must not delete the temporary file on Windows - self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) - file_path = self._temp_plugin_file.name - # Write first and close, otherwise on Windows, it cannot read the file - self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll()) - self._temp_plugin_file.close() - self._onDownloadComplete(file_path) + Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress) def _onDownloadComplete(self, file_path: str) -> None: Logger.log("i", "Download complete.")