def test_addAction(): message = Message() message.addAction(action_id="blarg", name="zomg", icon="NO ICON", description="SuperAwesomeMessage") assert len(message.getActions()) == 1
def test_addAction(): message = Message() message.addAction(action_id = "blarg", name = "zomg", icon = "NO ICON", description="SuperAwesomeMessage") assert len(message.getActions()) == 1
class SmartSliceAPIClient(QObject): class ConnectionErrorCodes(Enum): genericInternetConnectionError = 1 loginCredentialsError = 2 def __init__(self, connector): super().__init__() self._client = None self.connector = connector self.extension = connector.extension self._token = None self._error_message = None self._number_of_timeouts = 20 self._timeout_sleep = 3 self._username_preference = "smartslice/username" self._app_preferences = Application.getInstance().getPreferences() #Login properties self._login_username = "" self._login_password = "" self._badCredentials = False self._plugin_metadata = connector.extension.metadata # If the user has logged in before, we will hold on to the email. If they log out, or # the login is unsuccessful, the email will not persist. def _usernamePreferenceExists(self): username = self._app_preferences.getValue(self._username_preference) if username is not None and username != "" and self._login_username == "": self._login_username = username else: self._app_preferences.addPreference(self._username_preference, "") @property def _token_file_path(self): config_path = os.path.join( QStandardPaths.writableLocation(QStandardPaths.GenericConfigLocation), "smartslice" ) if not os.path.exists(config_path): os.makedirs(config_path) return os.path.join(config_path, ".token") # Opening a connection is our main goal with the API client object. We get the address to connect to, # then we check to see if the user has a valid token, if they are already logged in. If not, we log # them in. def openConnection(self): url = urlparse(self._plugin_metadata.url) protocol = url.scheme hostname = url.hostname if url.port: port = url.port else: port = 443 self._usernamePreferenceExists() if type(port) is not int: port = int(port) self._client = pywim.http.thor.Client( protocol=protocol, hostname=hostname, port=port, cluster=self._plugin_metadata.cluster ) # To ensure that the user is tracked and has a proper subscription, we let them login and then use the token we recieve # to track them and their login status. self._getToken() #If there is a token, ensure it is a valid token self._checkToken() #If invalid token, attempt to Login. if not self.logged_in: self.loggedInChanged.emit() self._login() #If now a valid token, allow access if self.logged_in: self.loggedInChanged.emit() Logger.log("d", "SmartSlice HTTP Client: {}".format(self._client.address)) def _connectionCheck(self): try: self._client.info() except Exception as error: Logger.log("e", "An error has occured checking the internet connection: {}".format(error)) return (self.ConnectionErrorCodes.genericInternetConnectionError) return None # API calls need to be executed through this function using a lambda passed in, as well as a failure code. # This prevents a fatal crash of Cura in some circumstances, as well as allows for a timeout/retry system. # The failure codes give us better control over the messages that come from an internet disconnect issue. def executeApiCall(self, endpoint: Callable[[], Tuple[int, object]], failure_code): api_code = self._connectionCheck() timeout_counter = 0 self.clearErrorMessage() if api_code is not None: return api_code, None while api_code is None and timeout_counter < self._number_of_timeouts: try: api_code, api_result = endpoint() except Exception as error: # If this error occurs, there was a connection issue Logger.log("e", "An error has occured with an API call: {}".format(error)) timeout_counter += 1 time.sleep(self._timeout_sleep) if timeout_counter == self._number_of_timeouts: return failure_code, None self.clearErrorMessage() return api_code, api_result def clearErrorMessage(self): if self._error_message is not None: self._error_message.hide() self._error_message = None # Login is fairly simple, the email and password is pulled from the Login popup that is displayed # on the Cura stage, and then sent to the API. def _login(self): username = self._login_username password = self._login_password if self._token is None: self.loggedInChanged.emit() if password != "": api_code, user_auth = self.executeApiCall( lambda: self._client.basic_auth_login(username, password), self.ConnectionErrorCodes.loginCredentialsError ) if api_code != 200: # If we get bad login credentials, this will set the flag that alerts the user on the popup if api_code == 400: Logger.log("d", "API Code 400") self.badCredentials = True self._login_password = "" self.badCredentialsChanged.emit() self._token = None else: self._handleThorErrors(api_code, user_auth) # If all goes well, we will be able to store the login token for the user else: self.clearErrorMessage() self.badCredentials = False self._login_password = "" self._app_preferences.setValue(self._username_preference, username) self._token = self._client.get_token() self._createTokenFile() # Logout removes the current token, clears the last logged in username and signals the popup to reappear. def logout(self): self._token = None self._login_password = "" self._createTokenFile() self._app_preferences.setValue(self._username_preference, "") self.loggedInChanged.emit() # If our user has logged in before, their login token will be in the file. def _getToken(self): #TODO: If no token file, try to login and create one. For now, we will just create a token file. if not os.path.exists(self._token_file_path): self._token = None else: try: with open(self._token_file_path, "r") as token_file: self._token = json.load(token_file) except: Logger.log("d", "Unable to read Token JSON") self._token = None if self._token == "" or self._token is None: self._token = None # Once we have pulled the token, we want to check with the API to make sure the token is valid. def _checkToken(self): self._client.set_token(self._token) api_code, api_result = self.executeApiCall( lambda: self._client.whoami(), self.ConnectionErrorCodes.loginCredentialsError ) if api_code != 200: self._token = None self._createTokenFile() # If there is no token in the file, or the file does not exist, we create one. def _createTokenFile(self): with open(self._token_file_path, "w") as token_file: json.dump(self._token, token_file) def getSubscription(self): api_code, api_result = self.executeApiCall( lambda: self._client.smartslice_subscription(), self.ConnectionErrorCodes.genericInternetConnectionError ) if api_code != 200: self._handleThorErrors(api_code, api_result) return None return api_result def cancelJob(self, job_id): api_code, api_result = self.executeApiCall( lambda: self._client.smartslice_job_abort(job_id), self.ConnectionErrorCodes.genericInternetConnectionError ) if api_code != 200: self._handleThorErrors(api_code, api_result) # If the user is correctly logged in, and has a valid token, we can use the 3mf data from # the plugin to submit a job to the API, and the results will be handled when they are returned. def submitSmartSliceJob(self, cloud_job, threemf_data): thor_status_code, task = self.executeApiCall( lambda: self._client.new_smartslice_job(threemf_data), self.ConnectionErrorCodes.genericInternetConnectionError ) job_status_tracker = JobStatusTracker(self.connector, self.connector.status) Logger.log("d", "API Status after posting: {}".format(thor_status_code)) if thor_status_code != 200: self._handleThorErrors(thor_status_code, task) self.connector.cancelCurrentJob() if getattr(task, 'status', None): Logger.log("d", "Job status after posting: {}".format(task.status)) # While the task status is not finished/failed/crashed/aborted continue to # wait on the status using the API. thor_status_code = None while thor_status_code != self.ConnectionErrorCodes.genericInternetConnectionError and not cloud_job.canceled and task.status not in ( pywim.http.thor.JobInfo.Status.failed, pywim.http.thor.JobInfo.Status.crashed, pywim.http.thor.JobInfo.Status.aborted, pywim.http.thor.JobInfo.Status.finished ): self.job_status = task.status cloud_job.api_job_id = task.id thor_status_code, task = self.executeApiCall( lambda: self._client.smartslice_job_wait(task.id, callback=job_status_tracker), self.ConnectionErrorCodes.genericInternetConnectionError ) if thor_status_code == 200: thor_status_code, task = self.executeApiCall( lambda: self._client.smartslice_job_wait(task.id, callback=job_status_tracker), self.ConnectionErrorCodes.genericInternetConnectionError ) if thor_status_code not in (200, None): self._handleThorErrors(thor_status_code, task) self.connector.cancelCurrentJob() if not cloud_job.canceled: self.connector.propertyHandler._cancelChanges = False if task.status == pywim.http.thor.JobInfo.Status.failed: error_message = Message() error_message.setTitle("Smart Slice Solver") error_message.setText(i18n_catalog.i18nc( "@info:status", "Error while processing the job:\n{}".format(task.errors[0].message) )) error_message.show() self.connector.cancelCurrentJob() cloud_job.setError(SmartSliceCloudJob.JobException(error_message.getText())) Logger.log( "e", "An error occured while sending and receiving cloud job: {}".format(error_message.getText()) ) self.connector.propertyHandler._cancelChanges = False return None elif task.status == pywim.http.thor.JobInfo.Status.finished: return task elif len(task.errors) > 0: error_message = Message() error_message.setTitle("Smart Slice Solver") error_message.setText(i18n_catalog.i18nc( "@info:status", "Unexpected status occured:\n{}".format(task.errors[0].message) )) error_message.show() self.connector.cancelCurrentJob() cloud_job.setError(SmartSliceCloudJob.JobException(error_message.getText())) Logger.log( "e", "An unexpected status occured while sending and receiving cloud job: {}".format(error_message.getText()) ) self.connector.propertyHandler._cancelChanges = False return None # When something goes wrong with the API, the errors are sent here. The http_error_code is an int that indicates # the problem that has occurred. The returned object may hold additional information about the error, or it may be None. def _handleThorErrors(self, http_error_code, returned_object): if self._error_message is not None: self._error_message.hide() self._error_message = Message(lifetime= 180) self._error_message.setTitle("Smart Slice API") if http_error_code == 400: if returned_object.error.startswith('User\'s maximum job queue count reached'): print(self._error_message.getActions()) self._error_message.setTitle("") self._error_message.setText("You have exceeded the maximum allowable " "number of queued\n jobs. Please cancel a " "queued job or wait for your queue to clear.") self._error_message.addAction( "continue", i18n_catalog.i18nc("@action", "Ok"), "", "" ) self._error_message.actionTriggered.connect(self.errorMessageAction) else: self._error_message.setText(i18n_catalog.i18nc("@info:status", "SmartSlice Server Error (400: Bad Request):\n{}".format(returned_object.error))) elif http_error_code == 401: self._error_message.setText(i18n_catalog.i18nc("@info:status", "SmartSlice Server Error (401: Unauthorized):\nAre you logged in?")) elif http_error_code == 429: self._error_message.setText(i18n_catalog.i18nc("@info:status", "SmartSlice Server Error (429: Too Many Attempts)")) elif http_error_code == self.ConnectionErrorCodes.genericInternetConnectionError: self._error_message.setText(i18n_catalog.i18nc("@info:status", "Internet connection issue:\nPlease check your connection and try again.")) elif http_error_code == self.ConnectionErrorCodes.loginCredentialsError: self._error_message.setText(i18n_catalog.i18nc("@info:status", "Internet connection issue:\nCould not verify your login credentials.")) else: self._error_message.setText(i18n_catalog.i18nc("@info:status", "SmartSlice Server Error (HTTP Error: {})".format(http_error_code))) self._error_message.show() @staticmethod def errorMessageAction(msg, action): msg.hide() @pyqtSlot() def onLoginButtonClicked(self): self.openConnection() @pyqtProperty(str, constant=True) def smartSliceUrl(self): return self._plugin_metadata.url badCredentialsChanged = pyqtSignal() loggedInChanged = pyqtSignal() @pyqtProperty(bool, notify=loggedInChanged) def logged_in(self): return self._token is not None @pyqtProperty(str, constant=True) def loginUsername(self): return self._login_username @loginUsername.setter def loginUsername(self,value): self._login_username = value @pyqtProperty(str, constant=True) def loginPassword(self): return self._login_password @loginPassword.setter def loginPassword(self,value): self._login_password = value @pyqtProperty(bool, notify=badCredentialsChanged) def badCredentials(self): return self._badCredentials @badCredentials.setter def badCredentials(self, value): self._badCredentials = value self.badCredentialsChanged.emit()