LocalPackageList.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. # Copyright (c) 2022 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 PyQt6.QtCore import pyqtSlot, QObject
  5. from UM.Version import Version
  6. from UM.i18n import i18nCatalog
  7. from UM.TaskManagement.HttpRequestManager import HttpRequestManager
  8. from UM.Logger import Logger
  9. from .PackageList import PackageList
  10. from .PackageModel import PackageModel
  11. from .Constants import PACKAGE_UPDATES_URL
  12. if TYPE_CHECKING:
  13. from PyQt6.QtCore import QObject
  14. from PyQt6.QtNetwork import QNetworkReply
  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. self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate)
  34. self._package_manager.packageUninstalled.connect(self._removePackageModel)
  35. def _sortSectionsOnUpdate(self) -> None:
  36. section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"]))
  37. self.sort(lambda model: (section_order[model.sectionTitle], not model.canUpdate, model.displayName.lower()), key = "package")
  38. def _removePackageModel(self, package_id: str) -> None:
  39. """
  40. Cleanup function to remove the package model from the list. Note that this is only done if the package can't
  41. be updated, it is in the to remove list and isn't in the to be installed list
  42. """
  43. package = self.getPackageModel(package_id)
  44. if package and not package.canUpdate and \
  45. package_id in self._package_manager.getToRemovePackageIDs() and \
  46. package_id not in self._package_manager.getPackagesToInstall():
  47. index = self.find("package", package_id)
  48. if index < 0:
  49. Logger.error(f"Could not find card in Listview corresponding with {package_id}")
  50. self.updatePackages()
  51. return
  52. self.removeItem(index)
  53. @pyqtSlot()
  54. def updatePackages(self) -> None:
  55. """Update the list with local packages, these are materials or plugin, either bundled or user installed. The list
  56. will also contain **to be removed** or **to be installed** packages since the user might still want to interact
  57. with these.
  58. """
  59. self.setErrorMessage("") # Clear any previous errors.
  60. self.setIsLoading(True)
  61. # Obtain and sort the local packages
  62. self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._package_manager.local_packages]])
  63. self._sortSectionsOnUpdate()
  64. self.checkForUpdates(self._package_manager.local_packages)
  65. self.setIsLoading(False)
  66. self.setHasMore(False) # All packages should have been loaded at this time
  67. def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel:
  68. """ Create a PackageModel from the package_info and determine its section_title"""
  69. package_id = package_info["package_id"]
  70. bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed"
  71. package_type = package_info["package_type"]
  72. section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type]
  73. package = PackageModel(package_info, section_title = section_title, parent = self)
  74. self._connectManageButtonSignals(package)
  75. return package
  76. def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None:
  77. installed_packages = "&".join([f"installed_packages={package['package_id']}:{package['package_version']}" for package in packages])
  78. request_url = f"{PACKAGE_UPDATES_URL}?{installed_packages}"
  79. self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get(
  80. request_url,
  81. scope = self._scope,
  82. callback = self._parseResponse
  83. )
  84. def _parseResponse(self, reply: "QNetworkReply") -> None:
  85. """
  86. Parse the response from the package list API request which can update.
  87. :param reply: A reply containing information about a number of packages.
  88. """
  89. response_data = HttpRequestManager.readJSON(reply)
  90. if response_data is None or "data" not in response_data:
  91. Logger.error(
  92. f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
  93. return
  94. if len(response_data["data"]) == 0:
  95. return
  96. packages = response_data["data"]
  97. for package in packages:
  98. self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))
  99. package_model = self.getPackageModel(package["package_id"])
  100. if package_model:
  101. # Also make sure that the local list knows where to get an update
  102. package_model.setDownloadUrl(package["download_url"])
  103. self._ongoing_requests["check_updates"] = None