mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-11 16:00:47 -07:00
Convert doxygen to rst for UM3NetworkPrinting
This commit is contained in:
parent
de82406782
commit
5eb5ffd916
38 changed files with 797 additions and 487 deletions
|
|
@ -20,13 +20,15 @@ from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
|||
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||
from ..Models.Http.CloudPrintResponse import CloudPrintResponse
|
||||
|
||||
## The generic type variable used to document the methods below.
|
||||
CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel)
|
||||
"""The generic type variable used to document the methods below."""
|
||||
|
||||
|
||||
## 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:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
# The cloud URL to use for this remote cluster.
|
||||
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||
|
|
@ -36,54 +38,70 @@ class CloudApiClient:
|
|||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
||||
|
||||
## 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:
|
||||
"""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.
|
||||
"""
|
||||
super().__init__()
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._account = account
|
||||
self._on_error = on_error
|
||||
self._upload = None # type: Optional[ToolPathUploader]
|
||||
|
||||
## Gets the account used for the API.
|
||||
@property
|
||||
def account(self) -> Account:
|
||||
"""Gets the account used for the API."""
|
||||
|
||||
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], failed: Callable) -> None:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, CloudClusterResponse, failed)
|
||||
|
||||
## 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:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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:
|
||||
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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]):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
|
||||
self._upload.start()
|
||||
|
||||
|
|
@ -96,20 +114,27 @@ class CloudApiClient:
|
|||
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:
|
||||
|
||||
"""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.
|
||||
"""
|
||||
|
||||
body = json.dumps({"data": data}).encode() if data else b""
|
||||
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:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
request = QNetworkRequest(QUrl(path))
|
||||
if content_type:
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||
|
|
@ -118,11 +143,14 @@ class CloudApiClient:
|
|||
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]]:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
try:
|
||||
response = bytes(reply.readAll()).decode()
|
||||
|
|
@ -133,14 +161,15 @@ class CloudApiClient:
|
|||
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:
|
||||
def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
if "data" in response:
|
||||
data = response["data"]
|
||||
if isinstance(data, list):
|
||||
|
|
@ -156,18 +185,21 @@ class CloudApiClient:
|
|||
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],
|
||||
on_error: Optional[Callable] = None) -> None:
|
||||
"""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 parse() -> None:
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,13 @@ from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
|
|||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The cloud output device is a network output device that works remotely but has limited functionality.
|
||||
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||
# As such, those methods have been implemented here.
|
||||
# Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||
"""The cloud output device is a network output device that works remotely but has limited functionality.
|
||||
|
||||
Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||
As such, those methods have been implemented here.
|
||||
Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||
"""
|
||||
|
||||
# The interval with which the remote cluster is checked.
|
||||
# We can do this relatively often as this API call is quite fast.
|
||||
|
|
@ -56,11 +58,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
# Therefore we create a private signal used to trigger the printersChanged signal.
|
||||
_cloudClusterPrintersChanged = pyqtSignal()
|
||||
|
||||
## Creates a new cloud output device
|
||||
# \param api_client: The client that will run the API calls
|
||||
# \param cluster: The device response received from the cloud API.
|
||||
# \param parent: The optional parent of this output device.
|
||||
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
|
||||
"""Creates a new cloud output device
|
||||
|
||||
:param api_client: The client that will run the API calls
|
||||
:param cluster: The device response received from the cloud API.
|
||||
:param parent: The optional parent of this output device.
|
||||
"""
|
||||
|
||||
# The following properties are expected on each networked output device.
|
||||
# Because the cloud connection does not off all of these, we manually construct this version here.
|
||||
|
|
@ -99,8 +103,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._tool_path = None # type: Optional[bytes]
|
||||
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
|
||||
|
||||
## Connects this device.
|
||||
def connect(self) -> None:
|
||||
"""Connects this device."""
|
||||
|
||||
if self.isConnected():
|
||||
return
|
||||
super().connect()
|
||||
|
|
@ -108,21 +113,24 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
||||
self._update()
|
||||
|
||||
## Disconnects the device
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnects the device"""
|
||||
|
||||
if not self.isConnected():
|
||||
return
|
||||
super().disconnect()
|
||||
Logger.log("i", "Disconnected from cluster %s", self.key)
|
||||
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
|
||||
|
||||
## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.
|
||||
def _onBackendStateChange(self, _: BackendState) -> None:
|
||||
"""Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices."""
|
||||
|
||||
self._tool_path = None
|
||||
self._uploaded_print_job = None
|
||||
|
||||
## Checks whether the given network key is found in the cloud's host name
|
||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||
"""Checks whether the given network key is found in the cloud's host name"""
|
||||
|
||||
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
||||
# the host name should then be "ultimakersystem-aabbccdd0011"
|
||||
if network_key.startswith(str(self.clusterData.host_name or "")):
|
||||
|
|
@ -133,15 +141,17 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
return True
|
||||
return False
|
||||
|
||||
## Set all the interface elements and texts for this output device.
|
||||
def _setInterfaceElements(self) -> None:
|
||||
"""Set all the interface elements and texts for this output device."""
|
||||
|
||||
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
|
||||
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
|
||||
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
|
||||
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
|
||||
|
||||
## Called when the network data should be updated.
|
||||
def _update(self) -> None:
|
||||
"""Called when the network data should be updated."""
|
||||
|
||||
super()._update()
|
||||
if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL:
|
||||
return # avoid calling the cloud too often
|
||||
|
|
@ -153,9 +163,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
else:
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
|
||||
## Method called when HTTP request to status endpoint is finished.
|
||||
# Contains both printers and print jobs statuses in a single response.
|
||||
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
|
||||
"""Method called when HTTP request to status endpoint is finished.
|
||||
|
||||
Contains both printers and print jobs statuses in a single response.
|
||||
"""
|
||||
self._responseReceived()
|
||||
if status.printers != self._received_printers:
|
||||
self._received_printers = status.printers
|
||||
|
|
@ -164,10 +176,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._received_print_jobs = status.print_jobs
|
||||
self._updatePrintJobs(status.print_jobs)
|
||||
|
||||
## Called when Cura requests an output device to receive a (G-code) file.
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
|
||||
"""Called when Cura requests an output device to receive a (G-code) file."""
|
||||
|
||||
# Show an error message if we're already sending a job.
|
||||
if self._progress.visible:
|
||||
PrintJobUploadBlockedMessage().show()
|
||||
|
|
@ -187,9 +200,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
job.finished.connect(self._onPrintJobCreated)
|
||||
job.start()
|
||||
|
||||
## Handler for when the print job was created locally.
|
||||
# It can now be sent over the cloud.
|
||||
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
|
||||
"""Handler for when the print job was created locally.
|
||||
|
||||
It can now be sent over the cloud.
|
||||
"""
|
||||
output = job.getOutput()
|
||||
self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again
|
||||
file_name = job.getFileName()
|
||||
|
|
@ -200,9 +215,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
)
|
||||
self._api.requestUpload(request, self._uploadPrintJob)
|
||||
|
||||
## Uploads the mesh when the print job was registered with the cloud API.
|
||||
# \param job_response: The response received from the cloud API.
|
||||
def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None:
|
||||
"""Uploads the mesh when the print job was registered with the cloud API.
|
||||
|
||||
:param job_response: The response received from the cloud API.
|
||||
"""
|
||||
if not self._tool_path:
|
||||
return self._onUploadError()
|
||||
self._progress.show()
|
||||
|
|
@ -210,38 +227,45 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
|
||||
self._onUploadError)
|
||||
|
||||
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
||||
def _onPrintJobUploaded(self) -> None:
|
||||
"""Requests the print to be sent to the printer when we finished uploading the mesh."""
|
||||
|
||||
self._progress.update(100)
|
||||
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
|
||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
|
||||
|
||||
## Shows a message when the upload has succeeded
|
||||
# \param response: The response from the cloud API.
|
||||
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
||||
"""Shows a message when the upload has succeeded
|
||||
|
||||
:param response: The response from the cloud API.
|
||||
"""
|
||||
self._progress.hide()
|
||||
PrintJobUploadSuccessMessage().show()
|
||||
self.writeFinished.emit()
|
||||
|
||||
## Displays the given message if uploading the mesh has failed
|
||||
# \param message: The message to display.
|
||||
def _onUploadError(self, message: str = None) -> None:
|
||||
"""Displays the given message if uploading the mesh has failed
|
||||
|
||||
:param message: The message to display.
|
||||
"""
|
||||
self._progress.hide()
|
||||
self._uploaded_print_job = None
|
||||
PrintJobUploadErrorMessage(message).show()
|
||||
self.writeError.emit()
|
||||
|
||||
## Whether the printer that this output device represents supports print job actions via the cloud.
|
||||
@pyqtProperty(bool, notify=_cloudClusterPrintersChanged)
|
||||
def supportsPrintJobActions(self) -> bool:
|
||||
"""Whether the printer that this output device represents supports print job actions via the cloud."""
|
||||
|
||||
if not self._printers:
|
||||
return False
|
||||
version_number = self.printers[0].firmwareVersion.split(".")
|
||||
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
||||
|
||||
## Set the remote print job state.
|
||||
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
||||
"""Set the remote print job state."""
|
||||
|
||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
|
||||
|
||||
@pyqtSlot(str, name="sendJobToTop")
|
||||
|
|
@ -265,18 +289,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
def openPrinterControlPanel(self) -> None:
|
||||
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl))
|
||||
|
||||
## Gets the cluster response from which this device was created.
|
||||
@property
|
||||
def clusterData(self) -> CloudClusterResponse:
|
||||
"""Gets the cluster response from which this device was created."""
|
||||
|
||||
return self._cluster
|
||||
|
||||
## Updates the cluster data from the cloud.
|
||||
@clusterData.setter
|
||||
def clusterData(self, value: CloudClusterResponse) -> None:
|
||||
"""Updates the cluster data from the cloud."""
|
||||
|
||||
self._cluster = value
|
||||
|
||||
## Gets the URL on which to monitor the cluster via the cloud.
|
||||
@property
|
||||
def clusterCloudUrl(self) -> str:
|
||||
"""Gets the URL on which to monitor the cluster via the cloud."""
|
||||
|
||||
root_url_prefix = "-staging" if self._account.is_staging else ""
|
||||
return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ from UM.Logger import Logger
|
|||
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||
|
||||
|
||||
## Class responsible for uploading meshes to the cloud in separate requests.
|
||||
class ToolPathUploader:
|
||||
"""Class responsible for uploading meshes to the cloud in separate requests."""
|
||||
|
||||
|
||||
# The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES
|
||||
MAX_RETRIES = 10
|
||||
|
|
@ -22,16 +23,19 @@ class ToolPathUploader:
|
|||
# The amount of bytes to send per request
|
||||
BYTES_PER_REQUEST = 256 * 1024
|
||||
|
||||
## Creates a mesh upload object.
|
||||
# \param manager: The network access manager that will handle the HTTP requests.
|
||||
# \param print_job: The print job response that was returned by the cloud after registering the upload.
|
||||
# \param data: The mesh bytes to be uploaded.
|
||||
# \param on_finished: The method to be called when done.
|
||||
# \param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
||||
# \param on_error: The method to be called when an error occurs.
|
||||
def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes,
|
||||
on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
|
||||
) -> None:
|
||||
"""Creates a mesh upload object.
|
||||
|
||||
:param manager: The network access manager that will handle the HTTP requests.
|
||||
:param print_job: The print job response that was returned by the cloud after registering the upload.
|
||||
:param data: The mesh bytes to be uploaded.
|
||||
:param on_finished: The method to be called when done.
|
||||
:param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
|
||||
:param on_error: The method to be called when an error occurs.
|
||||
"""
|
||||
|
||||
self._manager = manager
|
||||
self._print_job = print_job
|
||||
self._data = data
|
||||
|
|
@ -45,13 +49,15 @@ class ToolPathUploader:
|
|||
self._finished = False
|
||||
self._reply = None # type: Optional[QNetworkReply]
|
||||
|
||||
## Returns the print job for which this object was created.
|
||||
@property
|
||||
def printJob(self):
|
||||
"""Returns the print job for which this object was created."""
|
||||
|
||||
return self._print_job
|
||||
|
||||
## Creates a network request to the print job upload URL, adding the needed content range header.
|
||||
def _createRequest(self) -> QNetworkRequest:
|
||||
"""Creates a network request to the print job upload URL, adding the needed content range header."""
|
||||
|
||||
request = QNetworkRequest(QUrl(self._print_job.upload_url))
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type)
|
||||
|
||||
|
|
@ -62,14 +68,17 @@ class ToolPathUploader:
|
|||
|
||||
return request
|
||||
|
||||
## Determines the bytes that should be uploaded next.
|
||||
# \return: A tuple with the first and the last byte to upload.
|
||||
def _chunkRange(self) -> Tuple[int, int]:
|
||||
"""Determines the bytes that should be uploaded next.
|
||||
|
||||
:return: A tuple with the first and the last byte to upload.
|
||||
"""
|
||||
last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST)
|
||||
return self._sent_bytes, last_byte
|
||||
|
||||
## Starts uploading the mesh.
|
||||
def start(self) -> None:
|
||||
"""Starts uploading the mesh."""
|
||||
|
||||
if self._finished:
|
||||
# reset state.
|
||||
self._sent_bytes = 0
|
||||
|
|
@ -77,13 +86,15 @@ class ToolPathUploader:
|
|||
self._finished = False
|
||||
self._uploadChunk()
|
||||
|
||||
## Stops uploading the mesh, marking it as finished.
|
||||
def stop(self):
|
||||
"""Stops uploading the mesh, marking it as finished."""
|
||||
|
||||
Logger.log("i", "Stopped uploading")
|
||||
self._finished = True
|
||||
|
||||
## Uploads a chunk of the mesh to the cloud.
|
||||
def _uploadChunk(self) -> None:
|
||||
"""Uploads a chunk of the mesh to the cloud."""
|
||||
|
||||
if self._finished:
|
||||
raise ValueError("The upload is already finished")
|
||||
|
||||
|
|
@ -96,25 +107,29 @@ class ToolPathUploader:
|
|||
self._reply.uploadProgress.connect(self._progressCallback)
|
||||
self._reply.error.connect(self._errorCallback)
|
||||
|
||||
## Handles an update to the upload progress
|
||||
# \param bytes_sent: The amount of bytes sent in the current request.
|
||||
# \param bytes_total: The amount of bytes to send in the current request.
|
||||
def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None:
|
||||
"""Handles an update to the upload progress
|
||||
|
||||
:param bytes_sent: The amount of bytes sent in the current request.
|
||||
:param bytes_total: The amount of bytes to send in the current request.
|
||||
"""
|
||||
Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total)
|
||||
if bytes_total:
|
||||
total_sent = self._sent_bytes + bytes_sent
|
||||
self._on_progress(int(total_sent / len(self._data) * 100))
|
||||
|
||||
## Handles an error uploading.
|
||||
def _errorCallback(self) -> None:
|
||||
"""Handles an error uploading."""
|
||||
|
||||
reply = cast(QNetworkReply, self._reply)
|
||||
body = bytes(reply.readAll()).decode()
|
||||
Logger.log("e", "Received error while uploading: %s", body)
|
||||
self.stop()
|
||||
self._on_error()
|
||||
|
||||
## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
|
||||
def _finishedCallback(self) -> None:
|
||||
"""Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed."""
|
||||
|
||||
reply = cast(QNetworkReply, self._reply)
|
||||
Logger.log("i", "Finished callback %s %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
|
||||
|
|
@ -140,8 +155,9 @@ class ToolPathUploader:
|
|||
[bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode())
|
||||
self._chunkUploaded()
|
||||
|
||||
## Handles a chunk of data being uploaded, starting the next chunk if needed.
|
||||
def _chunkUploaded(self) -> None:
|
||||
"""Handles a chunk of data being uploaded, starting the next chunk if needed."""
|
||||
|
||||
# We got a successful response. Let's start the next chunk or report the upload is finished.
|
||||
first_byte, last_byte = self._chunkRange()
|
||||
self._sent_bytes += last_byte - first_byte
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue