Beispiel #1
0
def test_addAction():
    message = Message()
    message.addAction(action_id="blarg",
                      name="zomg",
                      icon="NO ICON",
                      description="SuperAwesomeMessage")

    assert len(message.getActions()) == 1
Beispiel #2
0
def test_addAction():
    message = Message()
    message.addAction(action_id = "blarg", name = "zomg", icon = "NO ICON", description="SuperAwesomeMessage")

    assert len(message.getActions()) == 1
Beispiel #3
0
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()