Use network manager in Toolbox

CURA-6387
This commit is contained in:
Lipu Fei 2019-03-15 09:56:02 +01:00
parent e98cf83cb3
commit 77511c2590

View file

@ -7,7 +7,7 @@ import tempfile
import platform import platform
from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union 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 PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Logger import Logger from UM.Logger import Logger
@ -28,6 +28,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.TaskManagement.HttpNetworkRequestManager import HttpNetworkRequestData
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -45,15 +46,13 @@ class Toolbox(QObject, Extension):
self._api_url = None # type: Optional[str] self._api_url = None # type: Optional[str]
# Network: # Network:
self._download_request = None # type: Optional[QNetworkRequest] self._download_request_data = None # type: Optional[HttpNetworkRequestData]
self._download_reply = None # type: Optional[QNetworkReply]
self._download_progress = 0 # type: float self._download_progress = 0 # type: float
self._is_downloading = False # type: bool self._is_downloading = False # type: bool
self._network_manager = None # type: Optional[QNetworkAccessManager] self._request_headers = dict() # type: Dict[str, str]
self._request_headers = [] # type: List[Tuple[bytes, bytes]]
self._updateRequestHeader() 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._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = set() # type: Set[str] self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]] self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
@ -142,20 +141,15 @@ class Toolbox(QObject, Extension):
self._fetchPackageData() self._fetchPackageData()
def _updateRequestHeader(self): def _updateRequestHeader(self):
self._request_headers = [ self._request_headers = {
(b"User-Agent", "User-Agent": "%s/%s (%s %s)" % (self._application.getApplicationName(),
str.encode( self._application.getVersion(),
"%s/%s (%s %s)" % ( platform.system(),
self._application.getApplicationName(), platform.machine())
self._application.getVersion(), }
platform.system(),
platform.machine(),
)
))
]
access_token = self._application.getCuraAPI().account.accessToken access_token = self._application.getCuraAPI().account.accessToken
if access_token: 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: def _resetUninstallVariables(self) -> None:
self._package_id_to_uninstall = None # type: Optional[str] self._package_id_to_uninstall = None # type: Optional[str]
@ -165,13 +159,11 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, int) @pyqtSlot(str, int)
def ratePackage(self, package_id: str, rating: int) -> None: 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)) url = "{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)
data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating) 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) @pyqtSlot(result = str)
def getLicenseDialogPluginName(self) -> str: def getLicenseDialogPluginName(self) -> str:
@ -213,11 +205,11 @@ class Toolbox(QObject, Extension):
installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions) installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions)
self._request_urls = { self._request_urls = {
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), "authors": "{base_url}/authors".format(base_url = self._api_url),
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), "packages": "{base_url}/packages".format(base_url = self._api_url),
"updates": QUrl("{base_url}/packages/package-updates?installed_packages={query}".format( "updates": "{base_url}/packages/package-updates?installed_packages={query}".format(
base_url = self._api_url, query = installed_packages_query)), base_url = self._api_url, query = installed_packages_query),
"subscribed_packages": QUrl(self._api_url_user_packages) "subscribed_packages": self._api_url_user_packages,
} }
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart) self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
@ -226,26 +218,13 @@ class Toolbox(QObject, Extension):
# On boot we check which packages have updates. # 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: if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check") and len(installed_package_ids_with_versions) > 0:
# Request the latest and greatest! # Request the latest and greatest!
self._fetchPackageUpdates() self._makeRequestByType("updates")
self._fetchUserSubscribedPackages() self._fetchUserSubscribedPackages()
def _prepareNetworkManager(self): @pyqtSlot()
if self._network_manager is not None: def browsePackages(self) -> 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()
# Make remote requests: # Make remote requests:
self._makeRequestByType("packages") self._fetchPackageData()
self._makeRequestByType("authors")
# Gather installed packages: # Gather installed packages:
self._updateInstalledModels() self._updateInstalledModels()
@ -254,10 +233,13 @@ class Toolbox(QObject, Extension):
self._prepareNetworkManager() self._prepareNetworkManager()
self._makeRequestByType("subscribed_packages") self._makeRequestByType("subscribed_packages")
def _fetchPackageData(self) -> None:
self._makeRequestByType("packages")
self._makeRequestByType("authors")
# Displays the toolbox # Displays the toolbox
@pyqtSlot() @pyqtSlot()
def launch(self) -> None: def launch(self) -> None:
if not self._dialog: if not self._dialog:
self._dialog = self._createDialog("Toolbox.qml") self._dialog = self._createDialog("Toolbox.qml")
@ -268,7 +250,6 @@ class Toolbox(QObject, Extension):
self._restart() self._restart()
self._dialog.show() self._dialog.show()
# Apply enabled/disabled state to installed plugins # Apply enabled/disabled state to installed plugins
self.enabledChanged.emit() self.enabledChanged.emit()
@ -576,52 +557,85 @@ class Toolbox(QObject, Extension):
# Make API Calls # Make API Calls
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def _makeRequestByType(self, request_type: str) -> None: def _makeRequestByType(self, request_type: str) -> None:
Logger.log("d", "Requesting '%s' metadata from server.", request_type) 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)
self._updateRequestHeader() self._updateRequestHeader()
if self._network_manager: url = self._request_urls[request_type]
self._network_manager.get(request)
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) @pyqtSlot(str)
def startDownload(self, url: str) -> None: def startDownload(self, url: str) -> None:
Logger.log("i", "Attempting to download & install package from %s.", url) Logger.log("i", "Attempting to download & install package from %s.", url)
url = QUrl(url)
self._download_request = QNetworkRequest(url) callback = lambda r: self._onDownloadFinished(r)
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"): error_callback = lambda r, e: self._onDownloadFailed(r, e)
# Patch for Qt 5.6-5.8 download_progress_callback = self._onDownloadProgress
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) request_data = self._application.getHttpNetworkRequestManager().get(url, headers_dict = self._request_headers,
if hasattr(QNetworkRequest, "RedirectPolicyAttribute"): callback = callback,
# Patch for Qt 5.9+ error_callback = error_callback,
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True) download_progress_callback = download_progress_callback)
for header_name, header_value in self._request_headers:
cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value) self._download_request_data = request_data
self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
self.setDownloadProgress(0) self.setDownloadProgress(0)
self.setIsDownloading(True) self.setIsDownloading(True)
cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)
@pyqtSlot() @pyqtSlot()
def cancelDownload(self) -> None: 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() self.resetDownload()
def resetDownload(self) -> None: 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.setDownloadProgress(0)
self.setIsDownloading(False) self.setIsDownloading(False)
@ -730,30 +744,31 @@ class Toolbox(QObject, Extension):
for package in self._server_response_data["packages"]: for package in self._server_response_data["packages"]:
self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"])) 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: def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
if bytes_total > 0: if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100 new_progress = bytes_sent / bytes_total * 100
self.setDownloadProgress(new_progress) self.setDownloadProgress(new_progress)
if bytes_sent == bytes_total: Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress)
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)
def _onDownloadComplete(self, file_path: str) -> None: def _onDownloadComplete(self, file_path: str) -> None:
Logger.log("i", "Download complete.") Logger.log("i", "Download complete.")