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)
Exemple #3
0
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)
Exemple #4
0
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
Exemple #5
0
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()
Exemple #6
0
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)
Exemple #7
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_)
Exemple #8
0
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())
Exemple #9
0
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)