Example #1
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)
Example #2
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
Example #3
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()