mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Use network manager in Toolbox
CURA-6387
This commit is contained in:
parent
e98cf83cb3
commit
77511c2590
1 changed files with 116 additions and 101 deletions
|
@ -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.")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue