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()