mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-11-02 20:52:20 -07:00
Introduce the SyncOrchestrator
CURA-6983
This commit is contained in:
parent
6eab5e2492
commit
1a816ad010
8 changed files with 84 additions and 21 deletions
105
plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
Normal file
105
plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from UM.TaskManagement.HttpRequestScope import UltimakerCloudScope
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from plugins.Toolbox.src.CloudApiModel import CloudApiModel
|
||||
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
|
||||
from plugins.Toolbox.src.Toolbox import i18n_catalog
|
||||
|
||||
|
||||
class CloudPackageChecker(QObject):
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.discrepancies = Signal() # Emits SubscribedPackagesModel
|
||||
self._application = application # type: CuraApplication
|
||||
self._scope = UltimakerCloudScope(application)
|
||||
self._model = SubscribedPackagesModel()
|
||||
|
||||
self._application.initializationFinished.connect(self._onAppInitialized)
|
||||
|
||||
# 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()
|
||||
|
||||
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()
|
||||
|
||||
def _handlePackageDiscrepancies(self):
|
||||
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()
|
||||
self.discrepancies.emit(self._model)
|
||||
|
||||
def _getUserPackages(self) -> None:
|
||||
Logger.log("d", "Requesting subscribed packages metadata from server.")
|
||||
url = CloudApiModel.api_url_user_packages
|
||||
|
||||
self._application.getHttpRequestManager().get(url,
|
||||
callback = self._onUserPackagesRequestFinished,
|
||||
error_callback = self._onUserPackagesRequestFinished,
|
||||
scope = self._scope)
|
||||
|
||||
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")
|
||||
33
plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
Normal file
33
plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.Signal import Signal
|
||||
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
|
||||
# choices are emitted on the `packageMutations` Signal.
|
||||
class DiscrepanciesPresenter(QObject):
|
||||
|
||||
def __init__(self, app: QtApplication):
|
||||
super().__init__(app)
|
||||
|
||||
self.packageMutations = Signal() # {"SettingsGuide" : "install", "PrinterSettings" : "uninstall"}
|
||||
|
||||
self._app = app
|
||||
self._dialog = None # type: Optional[QObject]
|
||||
self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
|
||||
|
||||
def present(self, plugin_path: str, model: SubscribedPackagesModel):
|
||||
path = os.path.join(plugin_path, self._compatibility_dialog_path)
|
||||
self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model})
|
||||
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
|
||||
|
||||
def _onConfirmClicked(self, model: SubscribedPackagesModel):
|
||||
# For now, all packages presented to the user should be installed.
|
||||
# Later, we will support uninstall ?or ignoring? of a certain package
|
||||
choices = {item["package_id"]: "install" for item in model.items}
|
||||
self.packageMutations.emit(choices)
|
||||
61
plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
Normal file
61
plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
|
||||
class SubscribedPackagesModel(ListModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._items = []
|
||||
self._metadata = None
|
||||
self._discrepancies = None
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "name")
|
||||
self.addRoleName(Qt.UserRole + 2, "icon_url")
|
||||
self.addRoleName(Qt.UserRole + 3, "is_compatible")
|
||||
|
||||
def setMetadata(self, data):
|
||||
if self._metadata != data:
|
||||
self._metadata = data
|
||||
|
||||
def addValue(self, discrepancy):
|
||||
if self._discrepancies != discrepancy:
|
||||
self._discrepancies = discrepancy
|
||||
|
||||
def update(self):
|
||||
self._items.clear()
|
||||
|
||||
for item in self._metadata:
|
||||
if item["package_id"] not in self._discrepancies:
|
||||
continue
|
||||
package = {"package_id": item["package_id"], "name": item["display_name"], "sdk_versions": item["sdk_versions"]}
|
||||
if self._sdk_version not in item["sdk_versions"]:
|
||||
package.update({"is_compatible": False})
|
||||
else:
|
||||
package.update({"is_compatible": True})
|
||||
try:
|
||||
package.update({"icon_url": item["icon_url"]})
|
||||
except KeyError: # There is no 'icon_url" in the response payload for this package
|
||||
package.update({"icon_url": ""})
|
||||
|
||||
self._items.append(package)
|
||||
self.setItems(self._items)
|
||||
|
||||
def hasCompatiblePackages(self) -> bool:
|
||||
has_compatible_items = False
|
||||
for item in self._items:
|
||||
if item['is_compatible'] == True:
|
||||
has_compatible_items = True
|
||||
return has_compatible_items
|
||||
|
||||
def hasIncompatiblePackages(self) -> bool:
|
||||
has_incompatible_items = False
|
||||
for item in self._items:
|
||||
if item['is_compatible'] == False:
|
||||
has_incompatible_items = True
|
||||
return has_incompatible_items
|
||||
34
plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
Normal file
34
plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from UM.Extension import Extension
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from plugins.Toolbox import CloudPackageChecker
|
||||
from plugins.Toolbox.src.CloudSync.DiscrepanciesPresenter import DiscrepanciesPresenter
|
||||
from plugins.Toolbox.src.CloudSync.SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Orchestrates the synchronizing of packages from the user account to the installed packages
|
||||
# Example flow:
|
||||
# - CloudPackageChecker compares a list of packages the user `subscribed` to in their account
|
||||
# If there are `discrepancies` between the account and locally installed packages, they are emitted
|
||||
# - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
|
||||
# the user selected to be performed
|
||||
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
||||
# - The DownloadPresenter shows a download progress dialog
|
||||
# - The LicencePresenter extracts licences from the downloaded packages and presents a licence for each package to
|
||||
# - be installed. It emits the `licenceAnswers` {'packageId' : bool} for accept or declines
|
||||
# - The CloudPackageManager removes the declined packages from the account
|
||||
# - The SyncOrchestrator uses PackageManager to install the downloaded packages.
|
||||
# - Bliss / profit / done
|
||||
class SyncOrchestrator(Extension):
|
||||
|
||||
def __init__(self, app: CuraApplication):
|
||||
super().__init__()
|
||||
|
||||
self._checker = CloudPackageChecker(app)
|
||||
self._checker.discrepancies.connect(self._onDiscrepancies)
|
||||
|
||||
self._discrepanciesPresenter = DiscrepanciesPresenter(app)
|
||||
|
||||
def _onDiscrepancies(self, model: SubscribedPackagesModel):
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
self._discrepanciesPresenter.present(plugin_path, model)
|
||||
0
plugins/Toolbox/src/CloudSync/__init__.py
Normal file
0
plugins/Toolbox/src/CloudSync/__init__.py
Normal file
Loading…
Add table
Add a link
Reference in a new issue