PackageList.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import tempfile
  4. import json
  5. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
  6. from typing import Dict, Optional, Set, TYPE_CHECKING
  7. from UM.i18n import i18nCatalog
  8. from UM.Qt.ListModel import ListModel
  9. from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
  10. from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager
  11. from UM.Logger import Logger
  12. from cura.CuraApplication import CuraApplication
  13. from cura import CuraPackageManager
  14. from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
  15. from .PackageModel import PackageModel
  16. from .Constants import USER_PACKAGES_URL
  17. if TYPE_CHECKING:
  18. from PyQt5.QtCore import QObject
  19. catalog = i18nCatalog("cura")
  20. class PackageList(ListModel):
  21. """ A List model for Packages, this class serves as parent class for more detailed implementations.
  22. such as Packages obtained from Remote or Local source
  23. """
  24. PackageRole = Qt.UserRole + 1
  25. DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
  26. def __init__(self, parent: Optional["QObject"] = None) -> None:
  27. super().__init__(parent)
  28. self._manager: CuraPackageManager = CuraApplication.getInstance().getPackageManager()
  29. self._account = CuraApplication.getInstance().getCuraAPI().account
  30. self._error_message = ""
  31. self.addRoleName(self.PackageRole, "package")
  32. self._is_loading = False
  33. self._has_more = False
  34. self._has_footer = True
  35. self._to_install: Dict[str, str] = {}
  36. self.canInstallChanged.connect(self._install)
  37. self._local_packages: Set[str] = {p["package_id"] for p in self._manager.local_packages}
  38. self._ongoing_request: Optional[HttpRequestData] = None
  39. self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
  40. @pyqtSlot()
  41. def updatePackages(self) -> None:
  42. """ A Qt slot which will update the List from a source. Actual implementation should be done in the child class"""
  43. pass
  44. @pyqtSlot()
  45. def abortUpdating(self) -> None:
  46. """ A Qt slot which allows the update process to be aborted. Override this for child classes with async/callback
  47. updatePackges methods"""
  48. pass
  49. def reset(self) -> None:
  50. """ Resets and clears the list"""
  51. self.clear()
  52. isLoadingChanged = pyqtSignal()
  53. def setIsLoading(self, value: bool) -> None:
  54. if self._is_loading != value:
  55. self._is_loading = value
  56. self.isLoadingChanged.emit()
  57. @pyqtProperty(bool, fset = setIsLoading, notify = isLoadingChanged)
  58. def isLoading(self) -> bool:
  59. """ Indicating if the the packages are loading
  60. :return" ``True`` if the list is being obtained, otherwise ``False``
  61. """
  62. return self._is_loading
  63. hasMoreChanged = pyqtSignal()
  64. def setHasMore(self, value: bool) -> None:
  65. if self._has_more != value:
  66. self._has_more = value
  67. self.hasMoreChanged.emit()
  68. @pyqtProperty(bool, fset = setHasMore, notify = hasMoreChanged)
  69. def hasMore(self) -> bool:
  70. """ Indicating if there are more packages available to load.
  71. :return: ``True`` if there are more packages to load, or ``False``.
  72. """
  73. return self._has_more
  74. errorMessageChanged = pyqtSignal()
  75. def setErrorMessage(self, error_message: str) -> None:
  76. if self._error_message != error_message:
  77. self._error_message = error_message
  78. self.errorMessageChanged.emit()
  79. @pyqtProperty(str, notify = errorMessageChanged, fset = setErrorMessage)
  80. def errorMessage(self) -> str:
  81. """ If an error occurred getting the list of packages, an error message will be held here.
  82. If no error occurred (yet), this will be an empty string.
  83. :return: An error message, if any, or an empty string if everything went okay.
  84. """
  85. return self._error_message
  86. @pyqtProperty(bool, constant = True)
  87. def hasFooter(self) -> bool:
  88. """ Indicating if the PackageList should have a Footer visible. For paginated PackageLists
  89. :return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise"""
  90. return self._has_footer
  91. def getPackageModel(self, package_id: str) -> PackageModel:
  92. index = self.find("package", package_id)
  93. return self.getItem(index)["package"]
  94. canInstallChanged = pyqtSignal(str, bool)
  95. def _install(self, package_id: str, update: bool = False) -> None:
  96. package_path = self._to_install.pop(package_id)
  97. Logger.debug(f"Installing {package_id}")
  98. to_be_installed = self._manager.installPackage(package_path) is not None
  99. package = self.getPackageModel(package_id)
  100. if package.can_update and to_be_installed:
  101. package.can_update = False
  102. if update:
  103. package.is_updating = False
  104. else:
  105. Logger.debug(f"Setting recently installed for package: {package_id}")
  106. package.is_recently_managed = True
  107. package.is_installing = False
  108. self.subscribeUserToPackage(package_id, str(package.sdk_version))
  109. def download(self, package_id: str, url: str, update: bool = False) -> None:
  110. def downloadFinished(reply: "QNetworkReply") -> None:
  111. self._downloadFinished(package_id, reply, update)
  112. def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
  113. self._downloadError(package_id, update, reply, error)
  114. HttpRequestManager.getInstance().get(
  115. url,
  116. scope = self._scope,
  117. callback = downloadFinished,
  118. error_callback = downloadError
  119. )
  120. def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None:
  121. try:
  122. with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
  123. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  124. while bytes_read:
  125. temp_file.write(bytes_read)
  126. bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
  127. Logger.debug(f"Finished downloading {package_id} and stored it as {temp_file.name}")
  128. self._to_install[package_id] = temp_file.name
  129. self.canInstallChanged.emit(package_id, update)
  130. except IOError as e:
  131. Logger.error(f"Failed to write downloaded package to temp file {e}")
  132. temp_file.close()
  133. self._downloadError(package_id, update)
  134. def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
  135. if reply:
  136. reply_string = bytes(reply.readAll()).decode()
  137. Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
  138. package = self.getPackageModel(package_id)
  139. if update:
  140. package.is_updating = False
  141. else:
  142. package.is_installing = False
  143. def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None:
  144. if self._account.isLoggedIn:
  145. Logger.debug(f"Subscribing the user for package: {package_id}")
  146. HttpRequestManager.getInstance().put(
  147. url = USER_PACKAGES_URL,
  148. data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(),
  149. scope = self._scope
  150. )
  151. def unsunscribeUserFromPackage(self, package_id: str) -> None:
  152. if self._account.isLoggedIn:
  153. Logger.debug(f"Unsubscribing the user for package: {package_id}")
  154. HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope)
  155. # --- Handle the manage package buttons ---
  156. def _connectManageButtonSignals(self, package: PackageModel) -> None:
  157. package.installPackageTriggered.connect(self.installPackage)
  158. package.uninstallPackageTriggered.connect(self.uninstallPackage)
  159. package.updatePackageTriggered.connect(self.installPackage)
  160. package.enablePackageTriggered.connect(self.enablePackage)
  161. package.disablePackageTriggered.connect(self.disablePackage)
  162. @pyqtSlot(str)
  163. def installPackage(self, package_id: str) -> None:
  164. package = self.getPackageModel(package_id)
  165. package.is_installing = True
  166. url = package.download_url
  167. Logger.debug(f"Trying to download and install {package_id} from {url}")
  168. self.download(package_id, url, False)
  169. @pyqtSlot(str)
  170. def uninstallPackage(self, package_id: str) -> None:
  171. Logger.debug(f"Uninstalling {package_id}")
  172. package = self.getPackageModel(package_id)
  173. package.is_installing = True
  174. self._manager.removePackage(package_id)
  175. self.unsunscribeUserFromPackage(package_id)
  176. package.is_installing = False
  177. package.is_recently_managed = True
  178. @pyqtSlot(str)
  179. def updatePackage(self, package_id: str) -> None:
  180. package = self.getPackageModel(package_id)
  181. package.is_updating = True
  182. self._manager.removePackage(package_id, force_add = True)
  183. url = package.download_url
  184. Logger.debug(f"Trying to download and update {package_id} from {url}")
  185. self.download(package_id, url, True)
  186. @pyqtSlot(str)
  187. def enablePackage(self, package_id: str) -> None:
  188. package = self.getPackageModel(package_id)
  189. package.is_enabling = True
  190. Logger.debug(f"Enabling {package_id}")
  191. # TODO: implement enabling functionality
  192. package.is_enabling = False
  193. @pyqtSlot(str)
  194. def disablePackage(self, package_id: str) -> None:
  195. package = self.getPackageModel(package_id)
  196. package.is_enabling = True
  197. Logger.debug(f"Disabling {package_id}")
  198. # TODO: implement disabling functionality
  199. package.is_enabling = False