RemotePackageList.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # Copyright (c) 2021 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
  4. from PyQt5.QtNetwork import QNetworkReply
  5. from typing import Optional, TYPE_CHECKING
  6. from cura.CuraApplication import CuraApplication
  7. from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
  8. from UM.i18n import i18nCatalog
  9. from UM.Logger import Logger
  10. from UM.TaskManagement.HttpRequestManager import HttpRequestManager, HttpRequestData # To request the package list from the API.
  11. from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API.
  12. from . import Marketplace # To get the list of packages. Imported this way to prevent circular imports.
  13. from .PackageList import PackageList
  14. from .PackageModel import PackageModel # The contents of this list.
  15. if TYPE_CHECKING:
  16. from PyQt5.QtCore import QObject
  17. from PyQt5.QtNetwork import QNetworkReply
  18. catalog = i18nCatalog("cura")
  19. class RemotePackageList(PackageList):
  20. ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
  21. def __init__(self, parent: "QObject" = None) -> None:
  22. super().__init__(parent)
  23. self._ongoing_request: Optional[HttpRequestData] = None
  24. self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
  25. self._package_type_filter = ""
  26. self._request_url = self._initialRequestUrl()
  27. def __del__(self) -> None:
  28. """
  29. When deleting this object, abort the request so that we don't get a callback from it later on a deleted C++
  30. object.
  31. """
  32. self.abortUpdating()
  33. @pyqtSlot()
  34. def updatePackages(self) -> None:
  35. """
  36. Make a request for the first paginated page of packages.
  37. When the request is done, the list will get updated with the new package models.
  38. """
  39. self.setErrorMessage("") # Clear any previous errors.
  40. self.setIsLoading(True)
  41. self._ongoing_request = HttpRequestManager.getInstance().get(
  42. self._request_url,
  43. scope = self._scope,
  44. callback = self._parseResponse,
  45. error_callback = self._onError
  46. )
  47. @pyqtSlot()
  48. def abortUpdating(self) -> None:
  49. HttpRequestManager.getInstance().abortRequest(self._ongoing_request)
  50. self._ongoing_request = None
  51. def reset(self) -> None:
  52. self.clear()
  53. self._request_url = self._initialRequestUrl()
  54. packageTypeFilterChanged = pyqtSignal()
  55. def setPackageTypeFilter(self, new_filter: str) -> None:
  56. if new_filter != self._package_type_filter:
  57. self._package_type_filter = new_filter
  58. self.reset()
  59. self.packageTypeFilterChanged.emit()
  60. @pyqtProperty(str, fset = setPackageTypeFilter, notify = packageTypeFilterChanged)
  61. def packageTypeFilter(self) -> str:
  62. """
  63. Get the package type this package list is filtering on, like ``plugin`` or ``material``.
  64. :return: The package type this list is filtering on.
  65. """
  66. return self._package_type_filter
  67. def _initialRequestUrl(self) -> str:
  68. """
  69. Get the URL to request the first paginated page with.
  70. :return: A URL to request.
  71. """
  72. if self._package_type_filter != "":
  73. return f"{Marketplace.PACKAGES_URL}?package_type={self._package_type_filter}&limit={self.ITEMS_PER_PAGE}"
  74. return f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
  75. def _parseResponse(self, reply: "QNetworkReply") -> None:
  76. """
  77. Parse the response from the package list API request.
  78. This converts that response into PackageModels, and triggers the ListModel to update.
  79. :param reply: A reply containing information about a number of packages.
  80. """
  81. response_data = HttpRequestManager.readJSON(reply)
  82. if "data" not in response_data or "links" not in response_data:
  83. Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}")
  84. self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response."))
  85. return
  86. for package_data in response_data["data"]:
  87. package = PackageModel(package_data, parent = self)
  88. self.appendItem({"package": package}) # Add it to this list model.
  89. self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page.
  90. self._ongoing_request = None
  91. self.setIsLoading(False)
  92. self.setHasMore(self._request_url != "")
  93. def _onError(self, reply: "QNetworkReply", error: Optional[QNetworkReply.NetworkError]) -> None:
  94. """
  95. Handles networking and server errors when requesting the list of packages.
  96. :param reply: The reply with packages. This will most likely be incomplete and should be ignored.
  97. :param error: The error status of the request.
  98. """
  99. if error == QNetworkReply.NetworkError.OperationCanceledError:
  100. Logger.debug("Cancelled request for packages.")
  101. self._ongoing_request = None
  102. return # Don't show an error about this to the user.
  103. Logger.error("Could not reach Marketplace server.")
  104. self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))
  105. self._ongoing_request = None
  106. self.setIsLoading(False)