class Node(QObject): def __init__(self, node_id: str, parent=None): QObject.__init__(self, parent) self._temperature = 293 self._node_id = node_id self._server_url = "localhost" self._access_card = "" self.updateServerUrl(self._server_url) self._all_chart_data = {} self._network_manager = QNetworkAccessManager() self._network_manager.finished.connect(self._onNetworkFinished) self._data = None self._enabled = True self._incoming_connections = [] self._outgoing_connections = [] self._onFinishedCallbacks = {} # type: Dict[QNetworkReply, Callable[[QNetworkReply], None]] self._description = "" self._static_properties = {} self._performance = 1 self._target_performance = 1 self._min_performance = 0.5 self._max_performance = 1 self._max_safe_temperature = 500 self._heat_convection = 1.0 self._heat_emissivity = 1.0 self._modifiers = [] self._active = True self._update_timer = QTimer() self._update_timer.setInterval(30000) self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self.partialUpdate) # Timer that is used when the server could not be reached. self._failed_update_timer = QTimer() self._failed_update_timer.setInterval(30000) self._failed_update_timer.setSingleShot(True) self._failed_update_timer.timeout.connect(self.fullUpdate) self._additional_properties = [] self._converted_additional_properties = {} self.server_reachable = False self._optimal_temperature = 200 self._is_temperature_dependant = False self._resources_required = [] self._optional_resources_required = [] self._resources_received = [] self._resources_produced = [] self._resources_provided = [] self._health = 100 self._max_amount_stored = 0 self._amount_stored = 0 self._effectiveness_factor = 0 self._random_delay_timer = QTimer() self._random_delay_timer.setInterval(random.randint(0, 29000)) self._random_delay_timer.setSingleShot(True) self._random_delay_timer.timeout.connect(self.fullUpdate) self._random_delay_timer.start() temperatureChanged = Signal() historyPropertiesChanged = Signal() historyDataChanged = Signal() enabledChanged = Signal() incomingConnectionsChanged = Signal() outgoingConnectionsChanged = Signal() performanceChanged = Signal() staticPropertiesChanged = Signal() modifiersChanged = Signal() additionalPropertiesChanged = Signal() minPerformanceChanged = Signal() maxPerformanceChanged = Signal() maxSafeTemperatureChanged = Signal() heatConvectionChanged = Signal() heatEmissivityChanged = Signal() serverReachableChanged = Signal(bool) isTemperatureDependantChanged = Signal() optimalTemperatureChanged = Signal() targetPerformanceChanged = Signal() resourcesRequiredChanged = Signal() optionalResourcesRequiredChanged = Signal() resourcesReceivedChanged = Signal() resourcesProducedChanged = Signal() resourcesProvidedChanged = Signal() healthChanged = Signal() maxAmountStoredChanged = Signal() amountStoredChanged = Signal() effectivenessFactorChanged = Signal() activeChanged = Signal() def setAccessCard(self, access_card): self._access_card = access_card self._updateUrlsWithAuth(self._server_url, access_card) def _updateUrlsWithAuth(self, server_url, access_card): self._performance_url = f"{self._server_url}/node/{self._node_id}/performance/?accessCardID={self._access_card}" def updateServerUrl(self, server_url): if server_url == "": return self._server_url = f"http://{server_url}:5000" self._source_url = f"{self._server_url}/node/{self._node_id}/" self._incoming_connections_url = f"{self._server_url}/node/{self._node_id}/connections/incoming/" self._all_chart_data_url = f"{self._server_url}/node/{self._node_id}/all_property_chart_data/?showLast=50" self._outgoing_connections_url = f"{self._server_url}/node/{self._node_id}/connections/outgoing/" self._static_properties_url = f"{self._server_url}/node/{self._node_id}/static_properties/" self._modifiers_url = f"{self._server_url}/node/{self._node_id}/modifiers/" self._updateUrlsWithAuth(self._server_url, self._access_card) def get(self, url: str, callback: Callable[[QNetworkReply], None]) -> None: reply = self._network_manager.get(QNetworkRequest(QUrl(url))) self._onFinishedCallbacks[reply] = callback def fullUpdate(self) -> None: """ Request all data of this node from the server :return: """ self.get(self._incoming_connections_url, self._onIncomingConnectionsFinished) self.get(self._outgoing_connections_url, self._onOutgoingConnectionsFinished) self.get(self._static_properties_url, self._onStaticPropertiesFinished) self.partialUpdate() self._update_timer.start() @Slot() def partialUpdate(self) -> None: """ Request all the data that is dynamic :return: """ self.get(self._source_url, self._onSourceUrlFinished) self.get(self._all_chart_data_url, self._onChartDataFinished) self.get(self._modifiers_url, self._onModifiersChanged) def _setServerReachable(self, server_reachable: bool): if self.server_reachable != server_reachable: self.server_reachable = server_reachable self.serverReachableChanged.emit(self.server_reachable) def _readData(self, reply: QNetworkReply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 404: print("Node was not found!") return # For some magical reason, it segfaults if I convert the readAll() data directly to bytes. # So, yes, the extra .data() is needed. data = bytes(reply.readAll().data()) if not data or status_code == 503: self._failed_update_timer.start() self._update_timer.stop() self._setServerReachable(False) return None self._setServerReachable(True) try: return json.loads(data) except json.decoder.JSONDecodeError: return None def updateAdditionalProperties(self, data): if self._additional_properties != data: self._additional_properties = data self._converted_additional_properties = {} # Clear the list and convert them in a way that we can use them in a repeater. for additional_property in data: self._converted_additional_properties[additional_property["key"]] = { "value": additional_property["value"], "max_value": additional_property["max_value"]} self.additionalPropertiesChanged.emit() def _onModifiersChanged(self, reply: QNetworkReply): result = self._readData(reply) if result is None: result = [] if self._modifiers != result: self._modifiers = result self.modifiersChanged.emit() def _onPerformanceChanged(self, reply: QNetworkReply): print("CALLBAAACk") result = self._readData(reply) if not result: return if self._performance != result: self._performance = result self.performanceChanged.emit() @Slot(float) def setPerformance(self, performance): data = "{\"performance\": %s}" % performance self._target_performance = performance self.targetPerformanceChanged.emit() reply = self._network_manager.put(QNetworkRequest(QUrl(self._performance_url)), data.encode()) self._onFinishedCallbacks[reply] = self._onPerformanceChanged @Slot(str) def addModifier(self, modifier: str): data = "{\"modifier_name\": \"%s\"}" % modifier request = QNetworkRequest(QUrl(self._modifiers_url)) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") reply = self._network_manager.post(request, data.encode()) self._onFinishedCallbacks[reply] = self._onModifiersChanged @Property(float, notify=performanceChanged) def performance(self): return self._performance @Property(float, notify=targetPerformanceChanged) def targetPerformance(self): return self._target_performance @Property("QVariantList", notify=resourcesRequiredChanged) def resourcesRequired(self): return self._resources_required @Property("QVariantList", notify=resourcesProducedChanged) def resourcesProduced(self): return self._resources_produced @Property("QVariantList", notify=resourcesProvidedChanged) def resourcesProvided(self): return self._resources_provided @Property("QVariantList", notify=resourcesReceivedChanged) def resourcesReceived(self): return self._resources_received @Property("QVariantList", notify=optionalResourcesRequiredChanged) def optionalResourcesRequired(self): return self._optional_resources_required @Property("QVariantList", notify=modifiersChanged) def modifiers(self): return self._modifiers @Property(float, notify=minPerformanceChanged) def min_performance(self): return self._min_performance @Property(float, notify=maxPerformanceChanged) def max_performance(self): return self._max_performance @Property(float, notify=healthChanged) def health(self): return self._health def _onStaticPropertiesFinished(self, reply: QNetworkReply) -> None: result = self._readData(reply) if not result: return if self._static_properties != result: self._static_properties = result self.staticPropertiesChanged.emit() def _onIncomingConnectionsFinished(self, reply: QNetworkReply): result = self._readData(reply) if not result: return self._incoming_connections = result self.incomingConnectionsChanged.emit() def _onOutgoingConnectionsFinished(self, reply: QNetworkReply): result = self._readData(reply) if not result: return self._outgoing_connections = result self.outgoingConnectionsChanged.emit() @Property("QVariantList", notify=incomingConnectionsChanged) def incomingConnections(self): return self._incoming_connections @Property(str, notify=staticPropertiesChanged) def description(self): return self._static_properties.get("description", "") @Property(str, notify=staticPropertiesChanged) def node_type(self): return self._static_properties.get("node_type", "") @Property(str, notify=staticPropertiesChanged) def custom_description(self): return self._static_properties.get("custom_description", "") @Property("QStringList", notify=staticPropertiesChanged) def supported_modifiers(self): return self._static_properties.get("supported_modifiers", "") @Property(bool, notify=staticPropertiesChanged) def hasSettablePerformance(self): return self._static_properties.get("has_settable_performance", False) @Property(str, notify=staticPropertiesChanged) def label(self): return self._static_properties.get("label", self._node_id) @Property(float, notify=staticPropertiesChanged) def surface_area(self): return self._static_properties.get("surface_area", 0) @Property(float, notify=isTemperatureDependantChanged) def isTemperatureDependant(self): return self._is_temperature_dependant @Property(float, notify=activeChanged) def active(self): return self._active @Property(float, notify=optimalTemperatureChanged) def optimalTemperature(self): return self._optimal_temperature @Property(float, notify=maxSafeTemperatureChanged) def max_safe_temperature(self): return self._max_safe_temperature @Property(float, notify=heatConvectionChanged) def heat_convection(self): return self._heat_convection @Property(float, notify=heatEmissivityChanged) def heat_emissivity(self): return self._heat_emissivity @Property("QVariantList", notify=outgoingConnectionsChanged) def outgoingConnections(self): return self._outgoing_connections def _onSourceUrlFinished(self, reply: QNetworkReply): data = self._readData(reply) if not data: return self._updateProperty("temperature", data["temperature"] - 273.15 ) self._updateProperty("enabled", bool(data["enabled"])) self._updateProperty("active", bool(data["active"])) self._updateProperty("performance", data["performance"]) self._updateProperty("min_performance", data["min_performance"]) self._updateProperty("max_performance", data["max_performance"]) self._updateProperty("max_safe_temperature", data["max_safe_temperature"] - 273.15) self._updateProperty("heat_convection", data["heat_convection"]) self._updateProperty("heat_emissivity", data["heat_emissivity"]) self._updateProperty("is_temperature_dependant", data["is_temperature_dependant"]) self._updateProperty("optimal_temperature", data["optimal_temperature"] - 273.15) self._updateProperty("target_performance", data["target_performance"]) self._updateProperty("health", data["health"]) self._updateProperty("effectiveness_factor", data["effectiveness_factor"]) # We need to update the resources a bit different to prevent recreation of QML items. # As such we use tiny QObjects with their own getters and setters. # If an object is already in the list with the right type, don't recreate it (just update it's value) self.updateResourceList("optional_resources_required", data["optional_resources_required"]) self.updateResourceList("resources_received", data["resources_received"]) self.updateResourceList("resources_required", data["resources_required"]) self.updateResourceList("resources_produced", data["resources_produced"]) self.updateResourceList("resources_provided", data["resources_provided"]) self.updateAdditionalProperties(data["additional_properties"]) def updateResourceList(self, property_name, data): list_to_check = getattr(self, "_" + property_name) list_updated = False for item in data: item_found = False for resource in list_to_check: if item["resource_type"] == resource.type: item_found = True resource.value = item["value"] break if not item_found: list_updated = True list_to_check.append(NodeResource(item["resource_type"], item["value"])) if list_updated: signal_name = "".join(x.capitalize() for x in property_name.split("_")) signal_name = signal_name[0].lower() + signal_name[1:] + "Changed" getattr(self, signal_name).emit() def _updateProperty(self, property_name, property_value): if getattr(self, "_" + property_name) != property_value: setattr(self, "_" + property_name, property_value) signal_name = "".join(x.capitalize() for x in property_name.split("_")) signal_name = signal_name[0].lower() + signal_name[1:] + "Changed" getattr(self, signal_name).emit() def _onPutUpdateFinished(self, reply: QNetworkReply): pass def _onChartDataFinished(self, reply: QNetworkReply): data = self._readData(reply) if not data: return # Offset is given in the reply, but it's not a list of data. Remove it here. if "offset" in data: del data["offset"] all_keys = set(data.keys()) keys_changed = False data_changed = False if set(self._all_chart_data.keys()) != all_keys: keys_changed = True if self._all_chart_data != data: data_changed = True self._all_chart_data = data if data_changed: self.historyDataChanged.emit() if keys_changed: self.historyPropertiesChanged.emit() def _onNetworkFinished(self, reply: QNetworkReply): if reply in self._onFinishedCallbacks: self._onFinishedCallbacks[reply](reply) del self._onFinishedCallbacks[reply] else: print("GOT A RESPONSE WITH NO CALLBACK!", reply.readAll()) @Property(str, constant=True) def id(self): return self._node_id @Property(bool, notify = enabledChanged) def enabled(self): return self._enabled @Property(float, notify=amountStoredChanged) def amount_stored(self): return self._amount_stored @Property(float, notify=effectivenessFactorChanged) def effectiveness_factor(self): return self._effectiveness_factor @Property(float, notify=temperatureChanged) def temperature(self): return self._temperature @Property("QVariantList", notify=historyPropertiesChanged) def allHistoryProperties(self): return list(self._all_chart_data.keys()) @Property("QVariantMap", notify=historyDataChanged) def historyData(self): return self._all_chart_data @Property("QVariantMap", notify=additionalPropertiesChanged) def additionalProperties(self): return self._converted_additional_properties @Slot() def toggleEnabled(self): url = self._source_url + "enabled/" reply = self._network_manager.put(QNetworkRequest(url), QByteArray()) self._onFinishedCallbacks[reply] = self._onPutUpdateFinished # Already trigger an update, so the interface feels snappy self._enabled = not self._enabled self.enabledChanged.emit()
class CloudApiClient: # The cloud URL to use for this remote cluster. ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) ## Initializes a new cloud API client. # \param account: The user's account object # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: super().__init__() self._manager = QNetworkAccessManager() self._account = account self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] # In order to avoid garbage collection we keep the callbacks in this list. self._anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Gets the account used for the API. @property def account(self) -> Account: return self._account ## Retrieves all the clusters for the user that is currently logged in. # \param on_finished: The function to be called after the result is parsed. def getClusters( self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: url = "{}/clusters".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse) ## Retrieves the status of the given cluster. # \param cluster_id: The ID of the cluster. # \param on_finished: The function to be called after the result is parsed. def getClusterStatus( self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterStatus) ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. # \param on_finished: The function to be called after the result is parsed. def requestUpload( self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) self._addCallback(reply, on_finished, CloudPrintJobResponse) ## Uploads a print job tool path to the cloud. # \param print_job: The object received after requesting an upload with `self.requestUpload`. # \param mesh: The tool path data to be uploaded. # \param on_finished: The function to be called after the upload is successful. # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. # \param cluster_id: The ID of the cluster. # \param job_id: The ID of the print job. # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) reply = self._manager.post(self._createEmptyRequest(url), b"") self._addCallback(reply, on_finished, CloudPrintResponse) ## Send a print job action to the cluster for the given print job. # \param cluster_id: The ID of the cluster. # \param cluster_job_id: The ID of the print job within the cluster. # \param action: The name of the action to execute. def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None: body = b"" if data: try: body = json.dumps({"data": data}).encode() except JSONDecodeError as err: Logger.log("w", "Could not encode body: %s", err) return url = "{}/clusters/{}/print_jobs/{}/action/{}".format( self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) self._manager.post(self._createEmptyRequest(url), body) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) access_token = self._account.accessToken if access_token: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code), id=str(time()), http_status="500") Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: if "data" in response: data = response["data"] if isinstance(data, list): results = [model_class(**c) for c in data] # type: List[CloudApiClientModel] on_finished_list = cast( Callable[[List[CloudApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**data) # type: CloudApiClientModel on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished) on_finished_item(result) elif "errors" in response: self._on_error( [CloudError(**error) for error in response["errors"]]) else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either # a list or a single item. # \param model: The type of the model to convert the response to. def _addCallback( self, reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: def parse() -> None: # Don't try to parse the reply if we didn't get one if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) is None: return status_code, response = self._parseReply(reply) self._anti_gc_callbacks.remove(parse) self._parseModels(response, on_finished, model) return self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)
class ClusterApiClient: PRINTER_API_PREFIX = "/api/v1" CLUSTER_API_PREFIX = "/cluster-api/v1" # In order to avoid garbage collection we keep the callbacks in this list. _anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Initializes a new cluster API client. # \param address: The network address of the cluster to call. # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, address: str, on_error: Callable) -> None: super().__init__() self._manager = QNetworkAccessManager() self._address = address self._on_error = on_error ## Get printer system information. # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: url = "{}/system".format(self.PRINTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, PrinterSystemStatus) ## Get the printers in the cluster. # \param on_finished: The callback in case the response is successful. def getPrinters( self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: url = "{}/printers".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrinterStatus) ## Get the print jobs in the cluster. # \param on_finished: The callback in case the response is successful. def getPrintJobs( self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post( self._createEmptyRequest(url), json.dumps({ "to_position": 0, "list": "queued" }).encode()) ## Override print job configuration and force it to be printed. def forcePrintJob(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.put(self._createEmptyRequest(url), json.dumps({ "force": True }).encode()) ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.deleteResource(self._createEmptyRequest(url)) ## Set the state of a print job. def setPrintJobState(self, print_job_uuid: str, state: str) -> None: url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid) # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({ "action": action }).encode()) ## Get the preview image data of a print job. def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: url = QUrl("http://" + self._address + path) request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: Logger.logException("e", "Could not parse the cluster response: %s", err) return status_code, {"errors": [err]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None: try: if isinstance(response, list): results = [model_class(**c) for c in response ] # type: List[ClusterApiClientModel] on_finished_list = cast( Callable[[List[ClusterApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**response) # type: ClusterApiClientModel on_finished_item = cast(Callable[[ClusterApiClientModel], Any], on_finished) on_finished_item(result) except JSONDecodeError: Logger.log("e", "Could not parse response from network: %s", str(response)) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. def _addCallback( self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: def parse() -> None: self._anti_gc_callbacks.remove(parse) # Don't try to parse the reply if we didn't get one if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) is None: return if reply.error() > 0: self._on_error(reply.errorString()) return # If no parse model is given, simply return the raw data in the callback. if not model: on_finished(reply.readAll()) return # Otherwise parse the result and return the formatted data in the callback. status_code, response = self._parseReply(reply) self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)
class VAbstractNetworkClient(QObject): """Позволяет приложению отправлять сетевые запросы и получать на них ответы. Вся работа с сетью должна быть инкапсулирована в его наследниках. """ @staticmethod def contentTypeFrom(reply: QNetworkReply, default=None): """Определяет и возвращает MIME-тип содержимого (со всеми вспомогательными данными, напр., кодировкой) из http-заголовка `Content-type` в ответе `reply`. Если тип содержимого определить невозможно, возвращает `default`. """ assert reply contentType = reply.header(QNetworkRequest.ContentTypeHeader) if contentType: assert isinstance(contentType, str) # TODO: Delete me! return contentType return default @staticmethod def encodingFrom(reply: QNetworkReply, default: str = "utf-8") -> str: """Определяет и возвращает кодировку содержимого из http-заголовка `Content-type` в ответе `reply`. Если кодировку определить невозможно, возвращает `default`. """ missing = object() contentType = VAbstractNetworkClient.contentTypeFrom(reply, missing) if contentType is missing: return default try: charset = contentType.split(";")[1] assert "charset" in charset encoding = charset.split("=")[1] return encoding.strip() except: return default @staticmethod def waitForFinished(reply: QNetworkReply, timeout: int = -1): """Блокирует вызывающий метод на время, пока не будет завершен сетевой ответ `reply` (то есть пока не испустится сигнал `reply.finished`), или пока не истечет `timeout` миллисекунд. Если `timeout` меньше 0 (по умолчанию), то по данному таймеру блокировка отменяться не будет. """ if reply.isFinished(): return event_loop = QEventLoop() reply.finished.connect(event_loop.quit) if timeout >= 0: timer = QTimer() timer.setInterval(timeout) timer.setSingleShot(True) timer.timeout.connect(event_loop.quit) # Если блокировка отменится до истечения таймера, то при выходе из метода таймер остановится и уничтожится. timer.start() event_loop.exec() reply.finished.disconnect(event_loop.quit) networkAccessManagerChanged = pyqtSignal(QNetworkAccessManager, arguments=['manager']) """Сигнал об изменении менеджера доступа к сети. :param QNetworkAccessManager manager: Новый менеджер доступа к сети. """ baseUrlChanged = pyqtSignal(QUrl, arguments=['url']) """Сигнал об изменении базового url-а. :param QUrl url: Новый базовый url. """ replyFinished = pyqtSignal(QNetworkReply, arguments=['reply']) """Сигнал о завершении ответа на сетевой запрос. :param QNetworkReply reply: Завершенный сетевой запрос. """ def __init__(self, parent: QObject = None): super().__init__(parent) self.__networkAccessManager = QNetworkAccessManager(parent=self) self.__baseUrl = QUrl() def getNetworkAccessManager(self) -> QNetworkAccessManager: """Возвращает менеджер доступа к сети.""" return self.__networkAccessManager def setNetworkAccessManager(self, manager: QNetworkAccessManager): """Устанавливает менеджер доступа к сети.""" assert manager if manager is self.__networkAccessManager: return if self.__networkAccessManager.parent() is self: self.__networkAccessManager.deleteLater() self.__networkAccessManager = manager self.networkAccessManagerChanged.emit(manager) networkAccessManager = pyqtProperty(type=QNetworkAccessManager, fget=getNetworkAccessManager, fset=setNetworkAccessManager, notify=networkAccessManagerChanged, doc="Менеджер доступа к сети.") def getBaseUrl(self) -> QUrl: """Возвращает базовый url.""" return QUrl(self.__baseUrl) def setBaseUrl(self, url: QUrl): """Устанавливает базовый url.""" if url == self.__baseUrl: return self.__baseUrl = QUrl(url) self.baseUrlChanged.emit(url) baseUrl = pyqtProperty(type=QUrl, fget=getBaseUrl, fset=setBaseUrl, notify=baseUrlChanged, doc="Базовый url.") def _connectReplySignals(self, reply: QNetworkReply): """Соединяет сигналы ответа с сигналами клиента.""" reply.finished.connect(lambda: self.replyFinished.emit(reply)) # TODO: Добавить сюда подключение остальных сигналов. def _get(self, request: QNetworkRequest) -> QNetworkReply: """Запускает отправку GET-запроса и возвращает ответ :class:`QNetworkReply` на него.""" reply = self.__networkAccessManager.get(request) self._connectReplySignals(reply) return reply def _head(self, request: QNetworkRequest) -> QNetworkReply: """Запускает отправку HEAD-запроса и возвращает ответ :class:`QNetworkReply` на него.""" reply = self.__networkAccessManager.head(request) self._connectReplySignals(reply) return reply def _post(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку POST-запроса и возвращает ответ :class:`QNetworkReply` на него. _post(self, request: QNetworkRequest) -> QNetworkReply. _post(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _post(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _post(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.post(request, data) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"POST") self._connectReplySignals(reply) return reply def _put(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку PUT-запроса и возвращает ответ :class:`QNetworkReply` на него. _put(self, request: QNetworkRequest) -> QNetworkReply. _put(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _put(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _put(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.put(request, data) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"PUT") self._connectReplySignals(reply) return reply def _delete(self, request: QNetworkRequest, data=None) -> QNetworkReply: """Запускает отправку DELETE-запроса и возвращает ответ :class:`QNetworkReply` на него. _delete(self, request: QNetworkRequest) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: bytes) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: bytearray) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QByteArray) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QIODevice) -> QNetworkReply. _delete(self, request: QNetworkRequest, data: QHttpMultiPart) -> QNetworkReply. """ if data is not None: reply = self.__networkAccessManager.deleteResource(request) else: reply = self.__networkAccessManager.sendCustomRequest( request, b"DELETE") self._connectReplySignals(reply) return reply def _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data=None) -> QNetworkReply: """Запускает отправку пользовательского запроса и возвращает ответ :class:`QNetworkReply` на него. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytes) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: bytearray) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QByteArray) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QIODevice) -> QNetworkReply. _sendCustomRequest(self, request: QNetworkRequest, verb: bytes, data: QHttpMultiPart) -> QNetworkReply. """ reply = self.__networkAccessManager.sendCustomRequest( request, verb, data) self._connectReplySignals(reply) return reply
class HttpClient(QObject): sig_ended = pyqtSignal(bool) def __init__(self): super().__init__() self.network_manager = QNetworkAccessManager() self.request = QNetworkRequest() self.request.setRawHeader(b"accept", b"application/json") self.request.setRawHeader( b'user-agent', b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, ' b'like Gecko) Chrome/66.0.3359.139 Safari/537.36') self._ended = True self._reply = None self._text = b'' self._string = '' self._status_code = None self._json = None self._headers = None self._connect_to_slot() def reply(self): return self._reply def json(self): return self._json def status(self): return self._status_code def text(self): return self._string def headers(self): return self._headers def content_type(self): content_type = self._headers['content-type'] if 'text/html' in content_type: return 'html' elif 'test/plain' in content_type: return 'text' elif 'application/json' in content_type: return 'json' def _save_header(self, raw_headers): h = {} for t in raw_headers: h.update({str.lower(bytes(t[0]).decode()): bytes(t[1]).decode()}) self._headers = h def set_header(self, header): """ header must consist of strings of dict :param header: dict """ if isinstance(header, dict): for k in header: self.request.setRawHeader(k.encode(), header[k].encode()) def get(self, url: str, header=None): """ Get http request :param url: :param header: """ self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.get(self.request) def post(self, url: str, header: list(tuple()) = None, data: bytes = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.post(self.request, data) def put(self, url: str, header: list(tuple()) = None, data: bytes = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.put(self.request, data) def delete(self, url: str, header: list(tuple()) = None): self.request.setUrl(QUrl(url)) self.set_header(header) self.network_manager.deleteResource(self.request) def _connect_to_slot(self): self.network_manager.finished.connect(self.slot_reply_finished) def slot_reply_finished(self, data: QNetworkReply): self._reply = data self._text = data.readAll() self._string = bytes(self._text).decode() self._status_code = data.attribute( QNetworkRequest.HttpStatusCodeAttribute) self._save_header(data.rawHeaderPairs()) if self.content_type() == 'json': if len(self._string): self._json = json.loads(self._string) else: self._json = None if self._status_code >= 400: print(self._string) self.sig_ended.emit(True) data.deleteLater()
class MainWin2(QMainWindow): def __init__(self): super(self.__class__, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.ui.button_upload.clicked.connect(self.on_button_upload_clicked_1) self.ui.button_browse.clicked.connect(self.on_button_browse_clicked_1) self.ui.button_del.clicked.connect(self.on_button_del_clicked_1) self.ui.radio_width.toggled.connect(self.on_radio_width_toggled) self.ui.radio_height.toggled.connect(self.on_radio_height_toggled) self.ui.radio_both.toggled.connect(self.on_radio_both_toggled) self.ui.radio_noscale.toggled.connect(self.on_radio_dontscale_toggled) self.show() self.nam = 0 self.rep = 0 self.req = 0 self.f = 0 self.filecount = 0 def dragEnterEvent(self, e): if e.mimeData().hasUrls(): e.accept() def dropEvent(self, e): for item in e.mimeData().urls(): self.ui.listWidget.addItem(item.toLocalFile()) if self.ui.check_autostart.isChecked(): self.on_button_upload_clicked_1() def on_button_upload_clicked_1(self): self.filecount = self.ui.listWidget.count() self.processfile(0) def on_button_del_clicked_1(self): list = self.ui.listWidget.selectedItems() for item in list: self.ui.listWidget.takeItem(self.ui.listWidget.row(item)) def on_button_browse_clicked_1(self): list = QFileDialog.getOpenFileNames() for item in list[0]: self.ui.listWidget.addItem(QListWidgetItem(item)) def on_radio_width_toggled(self): self.ui.spin_width.setEnabled(True) self.ui.spin_height.setEnabled(False) def on_radio_height_toggled(self): self.ui.spin_width.setEnabled(False) self.ui.spin_height.setEnabled(True) def on_radio_both_toggled(self): self.ui.spin_width.setEnabled(True) self.ui.spin_height.setEnabled(True) def on_radio_dontscale_toggled(self): self.ui.spin_width.setEnabled(False) self.ui.spin_height.setEnabled(False) def processfile(self, i): print("processfile_start " + str(self.filecount - self.ui.listWidget.count())) if self.ui.listWidget.count() == 0: return file = str(self.ui.listWidget.item(i).text()) image = QImage(file) if self.ui.radio_noscale.isChecked(): pass elif self.ui.radio_both.isChecked(): image = image.scaled(self.ui.spin_width.value(), self.ui.spin_height.value(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) elif self.ui.radio_width.isChecked(): w = self.ui.spin_width.value() image = image.scaledToWidth(w, Qt.KeepAspectRatio, Qt.SmoothTransformation) elif self.ui.radio_height.isChecked(): h = self.ui.spin_height.value() image = image.scaledToHeight(h, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.f = QTemporaryFile() self.f.open() image.save(self.f, 'JPG') self.f.seek(0) url = QUrl("ftp://" + self.ui.line_host.text() + "/" + self.ui.line_dir.text() + "/" + self.ui.line_prefix.text() + str(self.ui.spin_start_num.value()) + self.ui.line_suffix.text()) url.setUserName(self.ui.line_user.text()) url.setPassword(self.ui.line_pass.text()) url.setPort(self.ui.spin_port.value()) try: self.ui.listWidget.takeItem(0) self.ui.spin_start_num.setValue(self.ui.spin_start_num.value() + 1) self.nam = QNetworkAccessManager() self.rep = self.nam.put(QNetworkRequest(url), self.f) self.rep.finished.connect(self.isfinished) self.rep.error.connect(self.getError) if self.filecount != 0: self.progress = int( (self.filecount - self.ui.listWidget.count()) / (0.01 * self.filecount)) self.ui.progressBar.setValue(self.progress) except Exception as e: print("Exception " + str(e)) print("end") def getError(self): print("error") def isfinished(self): print("finished") self.f.close() self.processfile(0)
class FLNetwork(QtCore.QObject): url = None request = None manager = None reply = None finished = QtCore.pyqtSignal() start = QtCore.pyqtSignal() data = QtCore.pyqtSignal(str) dataTransferProgress = QtCore.pyqtSignal(int, int) def __init__(self, url): super(FLNetwork, self).__init__() self.url = url from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager self.request = QNetworkRequest() self.manager = QNetworkAccessManager() # self.manager.readyRead.connect(self._slotNetworkStart) self.manager.finished['QNetworkReply*'].connect(self._slotNetworkFinished) # self.data.connect(self._slotNetWorkData) # self.dataTransferProgress.connect(self._slotNetworkProgress) @decorators.BetaImplementation def get(self, location): self.request.setUrl(QtCore.QUrl("%s%s" % (self.url, location))) self.reply = self.manager.get(self.request) try: self.reply.uploadProgress.disconnect(self._slotNetworkProgress) self.reply.downloadProgress.disconnect(self._slotNetworkProgress) except: pass self.reply.downloadProgress.connect(self._slotNetworkProgress) @decorators.BetaImplementation def put(self, data, location): self.request.setUrl(QtCore.QUrl("%s%s" % (self.url, localtion))) self.reply = self.manager.put(data, self.request) try: self.reply.uploadProgress.disconnect(self._slotNetworkProgress) self.reply.downloadProgress.disconnect(self._slotNetworkProgress) except: pass self.uploadProgress.connect(self.slotNetworkProgress) @decorators.BetaImplementation def copy(self, fromLocation, toLocation): self.request.setUrl("%s%s" % (self.url, fromLocaltion)) data = self.manager.get(self.request) self.put(data.readAll(), toLocation) @QtCore.pyqtSlot() def _slotNetworkStart(self): self.start.emit() @QtCore.pyqtSlot() def _slotNetworkFinished(self, reply=None): self.finished.emit() #@QtCore.pyqtSlot(QtCore.QByteArray) # def _slotNetWorkData(self, b): # buffer = b # self.data.emit(b) def _slotNetworkProgress(self, bDone, bTotal): self.dataTransferProgress.emit(bDone, bTotal) data_ = None reply_ = self.reply.readAll().data() try: data_ = str(reply_, encoding="iso-8859-15") except: data_ = str(reply_, encoding="utf-8") self.data.emit(data_)
class FLNetwork(object): url = None request = None manager = None self.reply = None finished = QtCore.pyqtSignal() start = QtCore.pyqtSignal() data = QtCore.pyqtSignal(str) dataTransferProgress = QtCore.pyqtSignal(int, int) def __init__(self, url): self.url = QUrl(url) self.request = QNetworkRequest() self.manager = QNetworkAccessManager() self.manager.readyRead.connect(self._slotNetworkStart) self.manager.finished.connect(self._slotNetworkFinished) # self.data.connect(self._slotNetWorkData) # self.dataTransferProgress.connect(self._slotNetworkProgress) @decorators.BetaImplementation def get(self, location): self.request.setUrl("%s%s" % (self.url, localtion)) self.reply = self.manager.get(self.request) try: self.reply.uploadProgress.disconnect(self._slotNetworkProgress) self.reply.downloadProgress.disconnect(self._slotNetworkProgress) except: pass self.reply.downloadProgress.connect(self.slotNetworkProgress) @decorators.BetaImplementation def put(self, data, location): self.request.setUrl("%s%s" % (self.url, localtion)) self.reply = self.manager.put(data, self.request) try: self.reply.uploadProgress.disconnect(self._slotNetworkProgress) self.reply.downloadProgress.disconnect(self._slotNetworkProgress) except: pass self.uploadProgress.connect(self.slotNetworkProgress) @decorators.BetaImplementation def copy(self, fromLocation, toLocation): self.request.setUrl("%s%s" % (self.url, fromLocaltion)) data = self.manager.get(self.request) self.put(data.readAll(), toLocation) @QtCore.pyqtSlot() def _slotNetworkStart(self): self.start.emit() @QtCore.pyqtSlot() def _slotNetworkFinished(self): self.finished.emit() @QtCore.pyqtSlot(QByteArray) def _slotNetWorkData(self, b): buffer = b self.data.emit(b) @QtCore.pyqtSlot(int, int) def _slotNetworkProgress(self, bDone, bTotal): self.dataTransferProgress(bDone, bTotal).emit() self.data.emit(self.reply.read())
class CloudApiClient: # The cloud URL to use for this remote cluster. ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) ## Initializes a new cloud API client. # \param account: The user's account object # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: super().__init__() self._manager = QNetworkAccessManager() self._account = account self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] # In order to avoid garbage collection we keep the callbacks in this list. self._anti_gc_callbacks = [] # type: List[Callable[[], None]] ## Gets the account used for the API. @property def account(self) -> Account: return self._account ## Retrieves all the clusters for the user that is currently logged in. # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: url = "{}/clusters".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse) ## Retrieves the status of the given cluster. # \param cluster_id: The ID of the cluster. # \param on_finished: The function to be called after the result is parsed. def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterStatus) ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. # \param on_finished: The function to be called after the result is parsed. def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any] ) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) self._addCallback(reply, on_finished, CloudPrintJobResponse) ## Uploads a print job tool path to the cloud. # \param print_job: The object received after requesting an upload with `self.requestUpload`. # \param mesh: The tool path data to be uploaded. # \param on_finished: The function to be called after the upload is successful. # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. # \param cluster_id: The ID of the cluster. # \param job_id: The ID of the print job. # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) reply = self._manager.post(self._createEmptyRequest(url), b"") self._addCallback(reply, on_finished, CloudPrintResponse) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) access_token = self._account.accessToken if access_token: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. # \param reply: The reply from the server. # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() return status_code, json.loads(response) except (UnicodeDecodeError, JSONDecodeError, ValueError) as err: error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code), id=str(time()), http_status="500") Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} ## Parses the given models and calls the correct callback depending on the result. # \param response: The response from the server, after being converted to a dict. # \param on_finished: The callback in case the response is successful. # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: if "data" in response: data = response["data"] if isinstance(data, list): results = [model_class(**c) for c in data] # type: List[CloudApiClientModel] on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished) on_finished_list(results) else: result = model_class(**data) # type: CloudApiClientModel on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished) on_finished_item(result) elif "errors" in response: self._on_error([CloudError(**error) for error in response["errors"]]) else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) ## Creates a callback function so that it includes the parsing of the response into the correct model. # The callback is added to the 'finished' signal of the reply. # \param reply: The reply that should be listened to. # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either # a list or a single item. # \param model: The type of the model to convert the response to. def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], ) -> None: def parse() -> None: status_code, response = self._parseReply(reply) self._anti_gc_callbacks.remove(parse) return self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) reply.finished.connect(parse)