mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-10 00:07:51 -06:00
Refactor cloud sync out of Toolbox.py
CURA-6983
This commit is contained in:
parent
261ecfacc1
commit
71000180fc
4 changed files with 156 additions and 67 deletions
|
@ -2,6 +2,7 @@
|
||||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from .src import Toolbox
|
from .src import Toolbox
|
||||||
|
from .src.SubscriptionChecker import SubscriptionChecker
|
||||||
|
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
|
@ -9,4 +10,7 @@ def getMetaData():
|
||||||
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return {"extension": Toolbox.Toolbox(app)}
|
return {
|
||||||
|
"extension": Toolbox.Toolbox(app),
|
||||||
|
"subscription_checker": SubscriptionChecker(app)
|
||||||
|
}
|
||||||
|
|
18
plugins/Toolbox/src/CloudApiModel.py
Normal file
18
plugins/Toolbox/src/CloudApiModel.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
class CloudApiModel:
|
||||||
|
sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||||
|
cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str
|
||||||
|
cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||||
|
api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
||||||
|
cloud_api_root = cloud_api_root,
|
||||||
|
cloud_api_version = cloud_api_version,
|
||||||
|
sdk_version = sdk_version
|
||||||
|
) # type: str
|
||||||
|
|
||||||
|
# https://api.ultimaker.com/cura-packages/v1/user/packages
|
||||||
|
api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
|
||||||
|
cloud_api_root=cloud_api_root,
|
||||||
|
cloud_api_version=cloud_api_version,
|
||||||
|
)
|
124
plugins/Toolbox/src/SubscriptionChecker.py
Normal file
124
plugins/Toolbox/src/SubscriptionChecker.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||||
|
|
||||||
|
from UM.Extension import Extension
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from plugins.Toolbox.src.CloudApiModel import CloudApiModel
|
||||||
|
from plugins.Toolbox.src.SubscribedPackagesModel import SubscribedPackagesModel
|
||||||
|
from plugins.Toolbox.src.Toolbox import i18n_catalog
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionChecker(QObject, Extension):
|
||||||
|
|
||||||
|
def __init__(self, application: CuraApplication) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._application = application # type: CuraApplication
|
||||||
|
self._model = SubscribedPackagesModel()
|
||||||
|
|
||||||
|
self._application.initializationFinished.connect(self._onAppInitialized)
|
||||||
|
self._application.getCuraAPI().account.accessTokenChanged.connect(self._updateRequestHeader)
|
||||||
|
|
||||||
|
# This is a plugin, so most of the components required are not ready when
|
||||||
|
# this is initialized. Therefore, we wait until the application is ready.
|
||||||
|
def _onAppInitialized(self) -> None:
|
||||||
|
self._package_manager = self._application.getPackageManager()
|
||||||
|
|
||||||
|
# initial check
|
||||||
|
self._fetchUserSubscribedPackages()
|
||||||
|
# check again whenever the login state changes
|
||||||
|
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
|
||||||
|
|
||||||
|
def _fetchUserSubscribedPackages(self):
|
||||||
|
if self._application.getCuraAPI().account.isLoggedIn:
|
||||||
|
self._getUserPackages("subscribed_packages")
|
||||||
|
|
||||||
|
def _handleCompatibilityData(self, json_data) -> None:
|
||||||
|
user_subscribed_packages = [plugin["package_id"] for plugin in json_data]
|
||||||
|
user_installed_packages = self._package_manager.getUserInstalledPackages()
|
||||||
|
|
||||||
|
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace (discrepancy)
|
||||||
|
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
||||||
|
|
||||||
|
self._model.setMetadata(json_data)
|
||||||
|
self._model.addValue(package_discrepancy)
|
||||||
|
self._model.update()
|
||||||
|
|
||||||
|
if package_discrepancy:
|
||||||
|
self._handlePackageDiscrepancies(package_discrepancy)
|
||||||
|
|
||||||
|
def _handlePackageDiscrepancies(self, package_discrepancy):
|
||||||
|
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
|
||||||
|
sync_message = Message(i18n_catalog.i18nc(
|
||||||
|
"@info:generic",
|
||||||
|
"\nDo you want to sync material and software packages with your account?"),
|
||||||
|
lifetime=0,
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||||
|
sync_message.addAction("sync",
|
||||||
|
name=i18n_catalog.i18nc("@action:button", "Sync"),
|
||||||
|
icon="",
|
||||||
|
description="Sync your Cloud subscribed packages to your local environment.",
|
||||||
|
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
||||||
|
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
|
||||||
|
sync_message.show()
|
||||||
|
|
||||||
|
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
|
||||||
|
sync_message.hide()
|
||||||
|
compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
|
||||||
|
plugin_path_prefix = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
|
if plugin_path_prefix:
|
||||||
|
path = os.path.join(plugin_path_prefix, compatibility_dialog_path)
|
||||||
|
self.compatibility_dialog_view = self._application.getInstance().createQmlComponent(path, {"toolbox": self})
|
||||||
|
|
||||||
|
def _getUserPackages(self, request_type: str) -> None:
|
||||||
|
Logger.log("d", "Requesting [%s] metadata from server.", request_type)
|
||||||
|
self._updateRequestHeader()
|
||||||
|
url = CloudApiModel.api_url_user_packages
|
||||||
|
|
||||||
|
self._application.getHttpRequestManager().get(url,
|
||||||
|
headers_dict = self._request_headers,
|
||||||
|
callback = self._onUserPackagesRequestFinished,
|
||||||
|
error_callback = self._onUserPackagesRequestFinished)
|
||||||
|
|
||||||
|
def _onUserPackagesRequestFinished(self,
|
||||||
|
reply: "QNetworkReply",
|
||||||
|
error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||||
|
if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||||
|
Logger.log("w",
|
||||||
|
"Requesting user packages failed, response code %s while trying to connect to %s",
|
||||||
|
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||||
|
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
|
||||||
|
|
||||||
|
self._handleCompatibilityData(json_data["data"])
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
Logger.log("w", "Received invalid JSON for user packages")
|
||||||
|
|
||||||
|
def _updateRequestHeader(self):
|
||||||
|
# todo DRY, copied from Toolbox. To RequestManager?
|
||||||
|
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["Authorization"] = "Bearer {}".format(access_token)
|
||||||
|
|
|
@ -15,12 +15,12 @@ from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
from UM.Message import Message
|
|
||||||
|
|
||||||
from cura import ApplicationMetadata
|
from cura import ApplicationMetadata
|
||||||
from cura import UltimakerCloudAuthentication
|
from cura import UltimakerCloudAuthentication
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
|
from plugins.Toolbox.src.CloudApiModel import CloudApiModel
|
||||||
|
|
||||||
from .AuthorsModel import AuthorsModel
|
from .AuthorsModel import AuthorsModel
|
||||||
from .PackagesModel import PackagesModel
|
from .PackagesModel import PackagesModel
|
||||||
|
@ -32,8 +32,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
## Provides a marketplace for users to download plugins an materials
|
||||||
## The Toolbox class is responsible of communicating with the server through the API
|
|
||||||
class Toolbox(QObject, Extension):
|
class Toolbox(QObject, Extension):
|
||||||
def __init__(self, application: CuraApplication) -> None:
|
def __init__(self, application: CuraApplication) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -41,9 +40,6 @@ class Toolbox(QObject, Extension):
|
||||||
self._application = application # type: CuraApplication
|
self._application = application # type: CuraApplication
|
||||||
|
|
||||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||||
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str
|
|
||||||
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
|
||||||
self._api_url = None # type: Optional[str]
|
|
||||||
|
|
||||||
# Network:
|
# Network:
|
||||||
self._download_request_data = None # type: Optional[HttpRequestData]
|
self._download_request_data = None # type: Optional[HttpRequestData]
|
||||||
|
@ -61,17 +57,15 @@ class Toolbox(QObject, Extension):
|
||||||
self._server_response_data = {
|
self._server_response_data = {
|
||||||
"authors": [],
|
"authors": [],
|
||||||
"packages": [],
|
"packages": [],
|
||||||
"updates": [],
|
"updates": []
|
||||||
"subscribed_packages": [],
|
|
||||||
} # type: Dict[str, List[Any]]
|
} # type: Dict[str, List[Any]]
|
||||||
|
|
||||||
# Models:
|
# Models:
|
||||||
self._models = {
|
self._models = {
|
||||||
"authors": AuthorsModel(self),
|
"authors": AuthorsModel(self),
|
||||||
"packages": PackagesModel(self),
|
"packages": PackagesModel(self),
|
||||||
"updates": PackagesModel(self),
|
"updates": PackagesModel(self)
|
||||||
"subscribed_packages": SubscribedPackagesModel(self),
|
} # type: Dict[str, Union[AuthorsModel, PackagesModel]]
|
||||||
} # type: Dict[str, Union[AuthorsModel, PackagesModel, SubscribedPackagesModel]]
|
|
||||||
|
|
||||||
self._plugins_showcase_model = PackagesModel(self)
|
self._plugins_showcase_model = PackagesModel(self)
|
||||||
self._plugins_available_model = PackagesModel(self)
|
self._plugins_available_model = PackagesModel(self)
|
||||||
|
@ -159,7 +153,7 @@ 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 = "{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 = CloudApiModel.api_url, package_id = package_id)
|
||||||
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._application.getHttpRequestManager().put(url, headers_dict = self._request_headers,
|
self._application.getHttpRequestManager().put(url, headers_dict = self._request_headers,
|
||||||
|
@ -196,16 +190,6 @@ class Toolbox(QObject, Extension):
|
||||||
def _onAppInitialized(self) -> None:
|
def _onAppInitialized(self) -> None:
|
||||||
self._plugin_registry = self._application.getPluginRegistry()
|
self._plugin_registry = self._application.getPluginRegistry()
|
||||||
self._package_manager = self._application.getPackageManager()
|
self._package_manager = self._application.getPackageManager()
|
||||||
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
|
||||||
cloud_api_root = self._cloud_api_root,
|
|
||||||
cloud_api_version = self._cloud_api_version,
|
|
||||||
sdk_version = self._sdk_version
|
|
||||||
)
|
|
||||||
# https://api.ultimaker.com/cura-packages/v1/user/packages
|
|
||||||
self._api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
|
|
||||||
cloud_api_root = self._cloud_api_root,
|
|
||||||
cloud_api_version = self._cloud_api_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc.
|
# We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc.
|
||||||
installed_package_ids_with_versions = [":".join(items) for items in
|
installed_package_ids_with_versions = [":".join(items) for items in
|
||||||
|
@ -213,27 +197,20 @@ 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": "{base_url}/authors".format(base_url = self._api_url),
|
"authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url),
|
||||||
"packages": "{base_url}/packages".format(base_url = self._api_url),
|
"packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url),
|
||||||
"updates": "{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 = CloudApiModel.api_url, query = installed_packages_query)
|
||||||
"subscribed_packages": self._api_url_user_packages,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
|
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
|
||||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
|
|
||||||
|
|
||||||
# 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._makeRequestByType("updates")
|
self._makeRequestByType("updates")
|
||||||
self._fetchUserSubscribedPackages()
|
|
||||||
|
|
||||||
|
|
||||||
def _fetchUserSubscribedPackages(self):
|
|
||||||
if self._application.getCuraAPI().account.isLoggedIn:
|
|
||||||
self._makeRequestByType("subscribed_packages")
|
|
||||||
|
|
||||||
def _fetchPackageData(self) -> None:
|
def _fetchPackageData(self) -> None:
|
||||||
self._makeRequestByType("packages")
|
self._makeRequestByType("packages")
|
||||||
self._makeRequestByType("authors")
|
self._makeRequestByType("authors")
|
||||||
|
@ -652,46 +629,12 @@ class Toolbox(QObject, Extension):
|
||||||
# Tell the package manager that there's a new set of updates available.
|
# Tell the package manager that there's a new set of updates available.
|
||||||
packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]])
|
packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]])
|
||||||
self._package_manager.setPackagesWithUpdate(packages)
|
self._package_manager.setPackagesWithUpdate(packages)
|
||||||
elif request_type == "subscribed_packages":
|
|
||||||
self._checkCompatibilities(json_data["data"])
|
|
||||||
|
|
||||||
self.metadataChanged.emit()
|
self.metadataChanged.emit()
|
||||||
|
|
||||||
if self.isLoadingComplete():
|
if self.isLoadingComplete():
|
||||||
self.setViewPage("overview")
|
self.setViewPage("overview")
|
||||||
|
|
||||||
def _checkCompatibilities(self, json_data) -> None:
|
|
||||||
user_subscribed_packages = [plugin["package_id"] for plugin in json_data]
|
|
||||||
user_installed_packages = self._package_manager.getUserInstalledPackages()
|
|
||||||
|
|
||||||
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace (discrepancy)
|
|
||||||
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
|
||||||
if package_discrepancy:
|
|
||||||
self._models["subscribed_packages"].addValue(package_discrepancy)
|
|
||||||
self._models["subscribed_packages"].update()
|
|
||||||
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
|
|
||||||
sync_message = Message(i18n_catalog.i18nc(
|
|
||||||
"@info:generic",
|
|
||||||
"\nDo you want to sync material and software packages with your account?"),
|
|
||||||
lifetime=0,
|
|
||||||
title=i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
|
||||||
sync_message.addAction("sync",
|
|
||||||
name=i18n_catalog.i18nc("@action:button", "Sync"),
|
|
||||||
icon="",
|
|
||||||
description="Sync your Cloud subscribed packages to your local environment.",
|
|
||||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
|
||||||
|
|
||||||
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
|
|
||||||
sync_message.show()
|
|
||||||
|
|
||||||
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
|
|
||||||
sync_message.hide()
|
|
||||||
compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
|
|
||||||
plugin_path_prefix = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
|
||||||
if plugin_path_prefix:
|
|
||||||
path = os.path.join(plugin_path_prefix, compatibility_dialog_path)
|
|
||||||
self.compatibility_dialog_view = self._application.getInstance().createQmlComponent(path, {"toolbox": self})
|
|
||||||
|
|
||||||
# This function goes through all known remote versions of a package and notifies the package manager of this change
|
# This function goes through all known remote versions of a package and notifies the package manager of this change
|
||||||
def _notifyPackageManager(self):
|
def _notifyPackageManager(self):
|
||||||
for package in self._server_response_data["packages"]:
|
for package in self._server_response_data["packages"]:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue