LocalPackageList.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from typing import Any, Dict, List, Optional, TYPE_CHECKING
  4. from operator import attrgetter
  5. from PyQt5.QtCore import pyqtSlot, QObject
  6. if TYPE_CHECKING:
  7. from PyQt5.QtCore import QObject
  8. from PyQt5.QtNetwork import QNetworkReply
  9. from UM.i18n import i18nCatalog
  10. from UM.TaskManagement.HttpRequestManager import HttpRequestManager
  11. from UM.Logger import Logger
  12. from .PackageList import PackageList
  13. from .PackageModel import PackageModel
  14. from .Constants import PACKAGE_UPDATES_URL
  15. catalog = i18nCatalog("cura")
  16. class LocalPackageList(PackageList):
  17. PACKAGE_CATEGORIES = {
  18. "installed":
  19. {
  20. "plugin": catalog.i18nc("@label", "Installed Plugins"),
  21. "material": catalog.i18nc("@label", "Installed Materials")
  22. },
  23. "bundled":
  24. {
  25. "plugin": catalog.i18nc("@label", "Bundled Plugins"),
  26. "material": catalog.i18nc("@label", "Bundled Materials")
  27. }
  28. } # The section headers to be used for the different package categories
  29. def __init__(self, parent: Optional["QObject"] = None) -> None:
  30. super().__init__(parent)
  31. self._has_footer = False
  32. self._ongoing_requests["check_updates"] = None
  33. @pyqtSlot()
  34. def updatePackages(self) -> None:
  35. """Update the list with local packages, these are materials or plugin, either bundled or user installed. The list
  36. will also contain **to be removed** or **to be installed** packages since the user might still want to interact
  37. with these.
  38. """
  39. self.setErrorMessage("") # Clear any previous errors.
  40. self.setIsLoading(True)
  41. # Obtain and sort the local packages
  42. self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._manager.local_packages]])
  43. self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True)
  44. self.checkForUpdates(self._manager.local_packages)
  45. self.setIsLoading(False)
  46. self.setHasMore(False) # All packages should have been loaded at this time
  47. def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel:
  48. """ Create a PackageModel from the package_info and determine its section_title"""
  49. package_id = package_info["package_id"]
  50. bundled_or_installed = "bundled" if self._manager.isBundledPackage(package_id) else "installed"
  51. package_type = package_info["package_type"]
  52. section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type]
  53. package = PackageModel(package_info, section_title = section_title, parent = self)
  54. self._connectManageButtonSignals(package)
  55. package.can_downgrade = self._manager.canDowngrade(package_id)
  56. if package_id in self._manager.getPackagesToRemove() or package_id in self._manager.getPackagesToInstall():
  57. package.is_recently_installed = True
  58. return package
  59. def checkForUpdates(self, packages: List[Dict[str, Any]]):
  60. installed_packages = "installed_packages=".join([f"{package['package_id']}:{package['package_version']}&" for package in packages])
  61. request_url = f"{PACKAGE_UPDATES_URL}?installed_packages={installed_packages[:-1]}"
  62. self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get(
  63. request_url,
  64. scope = self._scope,
  65. callback = self._parseResponse
  66. )
  67. def _parseResponse(self, reply: "QNetworkReply") -> None:
  68. """
  69. Parse the response from the package list API request which can update.
  70. :param reply: A reply containing information about a number of packages.
  71. """
  72. response_data = HttpRequestManager.readJSON(reply)
  73. if "data" not in response_data:
  74. Logger.error(
  75. f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
  76. return
  77. if len(response_data["data"]) == 0:
  78. return
  79. try:
  80. for package_data in response_data["data"]:
  81. package = self.getPackageModel(package_data["package_id"])
  82. package.download_url = package_data.get("download_url", "")
  83. package.can_update = True
  84. self.sort(attrgetter("sectionTitle", "can_update", "displayName"), key = "package", reverse = True)
  85. self._ongoing_requests["check_updates"] = None
  86. except RuntimeError:
  87. # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
  88. # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
  89. # was deleted when it was still parsing the response
  90. return