diff options
Diffstat (limited to 'cura/API/Account.py')
-rw-r--r-- | cura/API/Account.py | 213 |
1 files changed, 190 insertions, 23 deletions
diff --git a/cura/API/Account.py b/cura/API/Account.py index 0e3af0e6c1..15bccb71e1 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -1,15 +1,16 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict, TYPE_CHECKING +from datetime import datetime +from typing import Optional, Dict, TYPE_CHECKING, Callable -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS -from UM.i18n import i18nCatalog +from UM.Logger import Logger from UM.Message import Message -from cura import UltimakerCloudAuthentication - +from UM.i18n import i18nCatalog from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings +from cura.UltimakerCloud import UltimakerCloudConstants if TYPE_CHECKING: from cura.CuraApplication import CuraApplication @@ -17,29 +18,61 @@ if TYPE_CHECKING: i18n_catalog = i18nCatalog("cura") -## The account API provides a version-proof bridge to use Ultimaker Accounts -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.account.login() -# api.account.logout() -# api.account.userProfile # Who is logged in`` -# +class SyncState: + """QML: Cura.AccountSyncState""" + SYNCING = 0 + SUCCESS = 1 + ERROR = 2 + IDLE = 3 + class Account(QObject): - # Signal emitted when user logged in or out. + """The account API provides a version-proof bridge to use Ultimaker Accounts + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.account.login() + api.account.logout() + api.account.userProfile # Who is logged in + """ + + # The interval in which sync services are automatically triggered + SYNC_INTERVAL = 30.0 # seconds + Q_ENUMS(SyncState) + loginStateChanged = pyqtSignal(bool) + """Signal emitted when user logged in or out""" + accessTokenChanged = pyqtSignal() + syncRequested = pyqtSignal() + """Sync services may connect to this signal to receive sync triggers. + Services should be resilient to receiving a signal while they are still syncing, + either by ignoring subsequent signals or restarting a sync. + See setSyncState() for providing user feedback on the state of your service. + """ + lastSyncDateTimeChanged = pyqtSignal() + syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum + manualSyncEnabledChanged = pyqtSignal(bool) + updatePackagesEnabledChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) self._application = application + self._new_cloud_printers_detected = False self._error_message = None # type: Optional[Message] self._logged_in = False + self._sync_state = SyncState.IDLE + self._manual_sync_enabled = False + self._update_packages_enabled = False + self._update_packages_action = None # type: Optional[Callable] + self._last_sync_str = "-" self._callback_port = 32118 - self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot + self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= self._oauth_root, @@ -56,6 +89,16 @@ class Account(QObject): self._authorization_service = AuthorizationService(self._oauth_settings) + # Create a timer for automatic account sync + self._update_timer = QTimer() + self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000)) + # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self.sync) + + self._sync_services = {} # type: Dict[str, int] + """contains entries "service_name" : SyncState""" + def initialize(self) -> None: self._authorization_service.initialize(self._application.getPreferences()) self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged) @@ -63,12 +106,65 @@ class Account(QObject): self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged) self._authorization_service.loadAuthDataFromPreferences() + + @pyqtProperty(int, notify=syncStateChanged) + def syncState(self): + return self._sync_state + + def setSyncState(self, service_name: str, state: int) -> None: + """ Can be used to register sync services and update account sync states + + Contract: A sync service is expected exit syncing state in all cases, within reasonable time + + Example: `setSyncState("PluginSyncService", SyncState.SYNCING)` + :param service_name: A unique name for your service, such as `plugins` or `backups` + :param state: One of SyncState + """ + prev_state = self._sync_state + + self._sync_services[service_name] = state + + if any(val == SyncState.SYNCING for val in self._sync_services.values()): + self._sync_state = SyncState.SYNCING + self._setManualSyncEnabled(False) + elif any(val == SyncState.ERROR for val in self._sync_services.values()): + self._sync_state = SyncState.ERROR + self._setManualSyncEnabled(True) + else: + self._sync_state = SyncState.SUCCESS + self._setManualSyncEnabled(False) + + if self._sync_state != prev_state: + self.syncStateChanged.emit(self._sync_state) + + if self._sync_state == SyncState.SUCCESS: + self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M") + self.lastSyncDateTimeChanged.emit() + + if self._sync_state != SyncState.SYNCING: + # schedule new auto update after syncing completed (for whatever reason) + if not self._update_timer.isActive(): + self._update_timer.start() + + def setUpdatePackagesAction(self, action: Callable) -> None: + """ Set the callback which will be invoked when the user clicks the update packages button + + Should be invoked after your service sets the sync state to SYNCING and before setting the + sync state to SUCCESS. + + Action will be reset to None when the next sync starts + """ + self._update_packages_action = action + self._update_packages_enabled = True + self.updatePackagesEnabledChanged.emit(self._update_packages_enabled) + def _onAccessTokenChanged(self): self.accessTokenChanged.emit() - ## Returns a boolean indicating whether the given authentication is applied against staging or not. @property def is_staging(self) -> bool: + """Indication whether the given authentication is applied against staging or not.""" + return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) @@ -83,18 +179,60 @@ class Account(QObject): self._error_message.show() self._logged_in = False self.loginStateChanged.emit(False) + if self._update_timer.isActive(): + self._update_timer.stop() return if self._logged_in != logged_in: self._logged_in = logged_in self.loginStateChanged.emit(logged_in) + if logged_in: + self._setManualSyncEnabled(False) + self._sync() + else: + if self._update_timer.isActive(): + self._update_timer.stop() + + def _sync(self) -> None: + """Signals all sync services to start syncing + + This can be considered a forced sync: even when a + sync is currently running, a sync will be requested. + """ + + self._update_packages_action = None + self._update_packages_enabled = False + self.updatePackagesEnabledChanged.emit(self._update_packages_enabled) + if self._update_timer.isActive(): + self._update_timer.stop() + elif self._sync_state == SyncState.SYNCING: + Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) + + self.syncRequested.emit() + + def _setManualSyncEnabled(self, enabled: bool) -> None: + if self._manual_sync_enabled != enabled: + self._manual_sync_enabled = enabled + self.manualSyncEnabledChanged.emit(enabled) @pyqtSlot() - def login(self) -> None: + @pyqtSlot(bool) + def login(self, force_logout_before_login: bool = False) -> None: + """ + Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will + logout from the account before initiating the authorization flow. If the user is logged in and + force_logout_before_login is false, the function will return, as there is nothing to do. + + :param force_logout_before_login: Optional boolean parameter + :return: None + """ if self._logged_in: - # Nothing to do, user already logged in. - return - self._authorization_service.startAuthorizationFlow() + if force_logout_before_login: + self.logout() + else: + # Nothing to do, user already logged in. + return + self._authorization_service.startAuthorizationFlow(force_logout_before_login) @pyqtProperty(str, notify=loginStateChanged) def userName(self): @@ -114,15 +252,44 @@ class Account(QObject): def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() - # Get the profile of the logged in user - # @returns None if no user is logged in, a dict containing user_id, username and profile_image_url @pyqtProperty("QVariantMap", notify = loginStateChanged) def userProfile(self) -> Optional[Dict[str, Optional[str]]]: + """None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """ + user_profile = self._authorization_service.getUserProfile() if not user_profile: return None return user_profile.__dict__ + @pyqtProperty(str, notify=lastSyncDateTimeChanged) + def lastSyncDateTime(self) -> str: + return self._last_sync_str + + @pyqtProperty(bool, notify=manualSyncEnabledChanged) + def manualSyncEnabled(self) -> bool: + return self._manual_sync_enabled + + @pyqtProperty(bool, notify=updatePackagesEnabledChanged) + def updatePackagesEnabled(self) -> bool: + return self._update_packages_enabled + + @pyqtSlot() + @pyqtSlot(bool) + def sync(self, user_initiated: bool = False) -> None: + if user_initiated: + self._setManualSyncEnabled(False) + + self._sync() + + @pyqtSlot() + def onUpdatePackagesClicked(self) -> None: + if self._update_packages_action is not None: + self._update_packages_action() + + @pyqtSlot() + def popupOpened(self) -> None: + self._setManualSyncEnabled(True) + @pyqtSlot() def logout(self) -> None: if not self._logged_in: |