Introduce the SyncOrchestrator

CURA-6983
This commit is contained in:
Nino van Hooff 2020-01-07 14:34:15 +01:00
parent 6eab5e2492
commit 1a816ad010
8 changed files with 84 additions and 21 deletions

View 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")

View 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)

View 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

View 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)