STAR-322: Using QNetworkReply.finished signal instead of QNetworkAccessManager.finished

This commit is contained in:
Daniel Schiavini 2018-12-14 14:50:15 +01:00
parent 4dc8edb996
commit 2f08854097
9 changed files with 152 additions and 164 deletions

View file

@ -4,11 +4,11 @@ import json
from json import JSONDecodeError
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from UM.Logger import Logger
from cura.API import Account
from cura.NetworkClient import NetworkClient
from .ResumableUpload import ResumableUpload
from ..Models import BaseModel
from .Models.CloudClusterResponse import CloudClusterResponse
@ -21,7 +21,7 @@ from .Models.CloudPrintJobResponse import CloudPrintJobResponse
## The cloud API client is responsible for handling the requests and responses from the cloud.
# Each method should only handle models instead of exposing Any HTTP details.
class CloudApiClient(NetworkClient):
class CloudApiClient:
# The cloud URL to use for this remote cluster.
# TODO: Make sure that this URL goes to the live api before release
@ -34,6 +34,7 @@ class CloudApiClient(NetworkClient):
# \param on_error: The callback to be called whenever we receive errors from the server.
def __init__(self, account: Account, on_error: Callable[[List[CloudErrorObject]], None]) -> None:
super().__init__()
self._manager = QNetworkAccessManager()
self._account = account
self._on_error = on_error
@ -46,14 +47,18 @@ class CloudApiClient(NetworkClient):
# \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)
self.get(url, on_finished=self._wrapCallback(on_finished, CloudClusterResponse))
reply = self._manager.get(self._createEmptyRequest(url))
callback = self._wrapCallback(reply, on_finished, CloudClusterResponse)
reply.finished.connect(callback)
## 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)
self.get(url, on_finished=self._wrapCallback(on_finished, CloudClusterStatus))
reply = self._manager.get(self._createEmptyRequest(url))
callback = self._wrapCallback(reply, on_finished, CloudClusterStatus)
reply.finished.connect(callback)
## Requests the cloud to register the upload of a print job mesh.
# \param request: The request object.
@ -62,7 +67,9 @@ class CloudApiClient(NetworkClient):
) -> None:
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
body = json.dumps({"data": request.toDict()})
self.put(url, body, on_finished=self._wrapCallback(on_finished, CloudPrintJobResponse))
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
callback = self._wrapCallback(reply, on_finished, CloudPrintJobResponse)
reply.finished.connect(callback)
## Requests the cloud to register the upload of a print job mesh.
# \param upload_response: The object received after requesting an upload with `self.requestUpload`.
@ -72,7 +79,7 @@ class CloudApiClient(NetworkClient):
# \param on_error: A function to be called if the upload fails. It receives a dict with the error.
def uploadMesh(self, upload_response: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
ResumableUpload(upload_response.upload_url, upload_response.content_type, mesh, on_finished,
ResumableUpload(self._manager, upload_response.upload_url, upload_response.content_type, mesh, on_finished,
on_progress, on_error).start()
# Requests a cluster to print the given print job.
@ -81,13 +88,17 @@ class CloudApiClient(NetworkClient):
# \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)
self.post(url, data = "", on_finished=self._wrapCallback(on_finished, CloudPrintResponse))
reply = self._manager.post(self._createEmptyRequest(url), b"")
callback = self._wrapCallback(reply, on_finished, CloudPrintResponse)
reply.finished.connect(callback)
## 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 = super()._createEmptyRequest(path, content_type)
request = QNetworkRequest(QUrl(path))
if content_type:
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
if self._account.isLoggedIn:
request.setRawHeader(b"Authorization", "Bearer {}".format(self._account.accessToken).encode())
Logger.log("i", "Created request for URL %s. Logged in = %s", path, self._account.isLoggedIn)
@ -132,10 +143,11 @@ class CloudApiClient(NetworkClient):
# \param model: The type of the model to convert the response to. It may either be a single record or a list.
# \return: A function that can be passed to the
def _wrapCallback(self,
reply: QNetworkReply,
on_finished: Callable[[Union[Model, List[Model]]], Any],
model: Type[Model],
) -> Callable[[QNetworkReply], None]:
def parse(reply: QNetworkReply) -> None:
def parse() -> None:
status_code, response = self._parseReply(reply)
return self._parseModels(response, on_finished, model)
return parse

View file

@ -172,8 +172,6 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
if self._last_response_time and time() - self._last_response_time < self.CHECK_CLUSTER_INTERVAL:
return # avoid calling the cloud too often
Logger.log("i", "Requesting update for %s after %s", self._device_id,
self._last_response_time and time() - self._last_response_time)
if self._account.isLoggedIn:
self.setAuthenticationState(AuthState.Authenticated)
self._api.getClusterStatus(self._device_id, self._onStatusCallFinished)

View file

@ -125,13 +125,14 @@ class CloudOutputDeviceManager:
## Handles an API error received from the cloud.
# \param errors: The errors received
def _onApiError(self, errors: List[CloudErrorObject]) -> None:
message = ". ".join(e.title for e in errors) # TODO: translate errors
Message(
text = message,
text = ". ".join(e.title for e in errors) # TODO: translate errors
message = Message(
text = text,
title = self.I18N_CATALOG.i18nc("@info:title", "Error"),
lifetime = 10,
dismissable = True
).show()
)
message.show()
def start(self):
if self._running:
@ -141,7 +142,6 @@ class CloudOutputDeviceManager:
# When switching machines we check if we have to activate a remote cluster.
application.globalContainerStackChanged.connect(self._connectToActiveMachine)
self._update_timer.timeout.connect(self._getRemoteClusters)
self._api.start()
self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn)
def stop(self):
@ -152,5 +152,4 @@ class CloudOutputDeviceManager:
# When switching machines we check if we have to activate a remote cluster.
application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
self._update_timer.timeout.disconnect(self._getRemoteClusters)
self._api.stop()
self._onLoginStateChanged(is_logged_in = False)

View file

@ -1,14 +1,14 @@
# Copyright (c) 2018 Ultimaker B.V.
# !/usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from typing import Optional, Callable, Any, Tuple
from UM.Logger import Logger
from cura.NetworkClient import NetworkClient
class ResumableUpload(NetworkClient):
class ResumableUpload:
MAX_RETRIES = 10
BYTES_PER_REQUEST = 256 * 1024
RETRY_HTTP_CODES = {500, 502, 503, 504}
@ -18,9 +18,9 @@ class ResumableUpload(NetworkClient):
# \param content_length: The total content length of the file, in bytes.
# \param http_method: The HTTP method to be used, e.g. "POST" or "PUT".
# \param timeout: The timeout for each chunk upload. Important: If None, no timeout is applied at all.
def __init__(self, url: str, content_type: str, data: bytes,
def __init__(self, manager: QNetworkAccessManager, url: str, content_type: str, data: bytes,
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
super().__init__()
self._manager = manager
self._url = url
self._content_type = content_type
self._data = data
@ -32,13 +32,15 @@ class ResumableUpload(NetworkClient):
self._sent_bytes = 0
self._retries = 0
self._finished = False
self._reply = None # type: Optional[QNetworkReply]
## We override _createEmptyRequest in order to add the user credentials.
## We override _createRequest 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 = super()._createEmptyRequest(path, content_type = self._content_type)
def _createRequest(self) -> QNetworkRequest:
request = QNetworkRequest(QUrl(self._url))
request.setHeader(QNetworkRequest.ContentTypeHeader, self._content_type)
first_byte, last_byte = self._chunkRange()
content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data))
request.setRawHeader(b"Content-Range", content_range.encode())
@ -51,7 +53,6 @@ class ResumableUpload(NetworkClient):
return self._sent_bytes, last_byte
def start(self) -> None:
super().start()
if self._finished:
self._sent_bytes = 0
self._retries = 0
@ -59,7 +60,6 @@ class ResumableUpload(NetworkClient):
self._uploadChunk()
def stop(self):
super().stop()
Logger.log("i", "Stopped uploading")
self._finished = True
@ -68,47 +68,43 @@ class ResumableUpload(NetworkClient):
raise ValueError("The upload is already finished")
first_byte, last_byte = self._chunkRange()
# self.put(self._url, data = self._data[first_byte:last_byte], content_type = self._content_type,
# on_finished = self.finishedCallback, on_progress = self._progressCallback)
request = self._createEmptyRequest(self._url, content_type=self._content_type)
request = self._createRequest()
reply = self._manager.put(request, self._data[first_byte:last_byte])
reply.finished.connect(lambda: self._finishedCallback(reply))
reply.uploadProgress.connect(self._progressCallback)
reply.error.connect(self._errorCallback)
if reply.isFinished():
self._finishedCallback(reply)
self._reply = self._manager.put(request, self._data[first_byte:last_byte])
self._reply.finished.connect(self._finishedCallback)
self._reply.uploadProgress.connect(self._progressCallback)
self._reply.error.connect(self._errorCallback)
def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None:
Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total)
if bytes_total:
self._on_progress(int((self._sent_bytes + bytes_sent) / len(self._data) * 100))
def _errorCallback(self, reply: QNetworkReply) -> None:
body = bytes(reply.readAll()).decode()
def _errorCallback(self) -> None:
body = bytes(self._reply.readAll()).decode()
Logger.log("e", "Received error while uploading: %s", body)
self.stop()
self._on_error()
def _finishedCallback(self, reply: QNetworkReply) -> None:
def _finishedCallback(self) -> None:
Logger.log("i", "Finished callback %s %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
self._reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), self._reply.url().toString())
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
status_code = self._reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if self._retries < self.MAX_RETRIES and status_code in self.RETRY_HTTP_CODES:
self._retries += 1
Logger.log("i", "Retrying %s/%s request %s", self._retries, self.MAX_RETRIES, reply.url().toString())
Logger.log("i", "Retrying %s/%s request %s", self._retries, self.MAX_RETRIES, self._reply.url().toString())
self._uploadChunk()
return
if status_code > 308:
self._errorCallback(reply)
self._errorCallback()
return
body = bytes(reply.readAll()).decode()
body = bytes(self._reply.readAll()).decode()
Logger.log("w", "status_code: %s, Headers: %s, body: %s", status_code,
[bytes(header).decode() for header in reply.rawHeaderList()], body)
[bytes(header).decode() for header in self._reply.rawHeaderList()], body)
first_byte, last_byte = self._chunkRange()
self._sent_bytes += last_byte - first_byte