Exemple #1
0
def register(app):
    plugin_data = {}
    if Platform.isWindows():
        reader = SolidWorksReader.SolidWorksReader()
        if reader.isOperational():
            plugin_data["mesh_reader"] = reader
        else:
            no_valid_installation_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Dear customer,\nWe could not find a valid installation of SolidWorks on your system. That means that either SolidWorks is not installed or you don't own an valid license. Please make sure that running SolidWorks itself works without issues and/or contact your ICT.\n\nWith kind regards\n - Thomas Karl Pietrowski"
                ), 0)
            no_valid_installation_message.setTitle("SolidWorks plugin")
            no_valid_installation_message.show()

        plugin_data[
            "extension"] = SolidWorksDialogHandler.SolidWorksDialogHandler(
                reader)
    else:
        not_correct_os_message = Message(
            i18n_catalog.i18nc(
                "@info:status",
                "Dear customer,\nYou are currently running this plugin on an operating system other than Windows. This plugin will only work on Windows with SolidWorks installed, including an valid license. Please install this plugin on a Windows machine with SolidWorks installed.\n\nWith kind regards\n - Thomas Karl Pietrowski"
            ), 0)
        not_correct_os_message.setTitle("SolidWorks plugin")
        not_correct_os_message.show()

    return plugin_data
Exemple #2
0
    def run(self) -> None:
        if not self.job_type:
            error_message = Message()
            error_message.setTitle("Smart Slice")
            error_message.setText(i18n_catalog.i18nc("@info:status", "Job type not set for processing:\nDon't know what to do!"))
            error_message.show()
            self.connector.cancelCurrentJob()

        Job.yieldThread()  # Should allow the UI to update earlier

        try:
            job = self.prepareJob()
            Logger.log("i", "Smart Slice job prepared")
        except SmartSliceCloudJob.JobException as exc:
            Logger.log("w", "Smart Slice job cannot be prepared: {}".format(exc.problem))

            self.setError(exc)
            return

        task = self.processCloudJob(job)

        try:
            os.remove(job)
        except:
            Logger.log("w", "Unable to remove temporary 3MF {}".format(job))

        if task and task.result:
            self._result = task.result
Exemple #3
0
def test_gettersAndSetters():
    message = Message(text = "OMG", title="YAY", image_caption="DERP", image_source= "HERP", option_text="FOO", option_state= False)
    message.setMaxProgress(200)
    assert message.getText() == "OMG"
    assert message.getTitle() == "YAY"
    assert message.getImageCaption() == "DERP"
    assert message.getImageSource() == "HERP"
    assert message.getOptionText() == "FOO"
    assert message.getMaxProgress() == 200
    assert message.getOptionState() == False

    message.setTitle("whoo")
    assert message.getTitle() == "whoo"
Exemple #4
0
def test_gettersAndSetters():
    message = Message(text="OMG",
                      title="YAY",
                      image_caption="DERP",
                      image_source="HERP",
                      option_text="FOO",
                      option_state=False,
                      message_type=Message.MessageType.POSITIVE)
    message.setMaxProgress(200)
    assert message.getText() == "OMG"
    assert message.getTitle() == "YAY"
    assert message.getImageCaption() == "DERP"
    assert message.getImageSource() == "HERP"
    assert message.getOptionText() == "FOO"
    assert message.getMaxProgress() == 200
    assert message.getOptionState() == False

    message.setTitle("whoo")
    assert message.getTitle() == "whoo"
Exemple #5
0
    def openForeignFile(self, options):
        open_file_paths = self.getOpenDocumentPaths(options)

        # SolidWorks API: X
        options["sw_previous_active_file"] = options["app_instance"].ActiveDoc
        # If the file has not been loaded open it!
        if not os.path.normpath(options["foreignFile"]) in open_file_paths:
            Logger.log("d", "Opening the foreign file!")
            if options["foreignFormat"].upper() == self._extension_part:
                filetype = SolidWorksEnums.swDocumentTypes_e.swDocPART
            elif options["foreignFormat"].upper() == self._extension_assembly:
                filetype = SolidWorksEnums.swDocumentTypes_e.swDocASSEMBLY
            elif options["foreignFormat"].upper() == self._extension_drawing:
                filetype = SolidWorksEnums.swDocumentTypes_e.swDocDRAWING
            else:
                raise NotImplementedError(
                    "Unknown extension. Something went terribly wrong!")

            # SolidWorks API: 2008 FCS (Rev 16.0)
            documentSpecification = options["app_instance"].GetOpenDocSpec(
                options["foreignFile"])

            ## NOTE: SPEC: FileName
            #documentSpecification.FileName

            ## NOTE: SPEC: DocumentType
            ## TODO: Really needed here?!
            documentSpecification.DocumentType = filetype

            ## TODO: Test the impact of LightWeight = True
            #documentSpecification.LightWeight = True
            documentSpecification.Silent = True

            ## TODO: Double check, whether file was really opened read-only..
            documentSpecification.ReadOnly = True

            documentSpecificationObject = ComConnector.GetComObject(
                documentSpecification)
            # SolidWorks API: 2008 FCS (Rev 16.0)
            options["sw_model"] = options["app_instance"].OpenDoc7(
                documentSpecificationObject)

            if documentSpecification.Warning:
                Logger.log(
                    "w",
                    "Warnings happened while opening your SolidWorks file!")
            if documentSpecification.Error:
                Logger.log(
                    "e", "Errors happened while opening your SolidWorks file!")
                error_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "SolidWorks reported errors, while opening your file. We recommend to solve these issues inside SolidWorks itself."
                    ))
                error_message.setTitle("SolidWorks plugin")
                error_message.show()
            options["sw_opened_file"] = True
        else:
            Logger.log("d", "Foreign file has already been opened!")
            options["sw_model"] = self.getOpenDocumentFilepathDict(options)[
                os.path.normpath(options["foreignFile"])]
            options["sw_opened_file"] = False

        if options["foreignFormat"].upper() == self._extension_drawing:
            count_of_documents = self.countDocumentsInDrawing(options)
            if count_of_documents == 0:
                error_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "Found no models inside your drawing. Could you please check it's content again and make sure one part or assembly is inside?\n\n Thanks!."
                    ))
                error_message.setTitle("SolidWorks plugin")
                error_message.show()
            elif count_of_documents > 1:
                error_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "Found more then one part or assembly inside your drawing. We currently only support drawings with exactly one part or assembly inside.\n\nSorry!"
                    ))
                error_message.setTitle("SolidWorks plugin")
                error_message.show()
            else:
                options["sw_drawing"] = options["sw_model"]
                options["sw_drawing_opened"] = options["sw_opened_file"]
                options["foreignFile"] = self.getDocumentsInDrawing(options)[0]
                options["foreignFormat"] = os.path.splitext(
                    options["foreignFile"])[1]
                self.activatePreviousFile(options)

                options = self.openForeignFile(options)

        error = ComConnector.getByVarInt()
        # SolidWorks API: >= 20.0.x
        # SolidWorks API: 2001Plus FCS (Rev. 10.0) - GetTitle
        options["app_instance"].ActivateDoc3(
            options["sw_model"].GetTitle,
            True,
            SolidWorksEnums.swRebuildOnActivation_e.swDontRebuildActiveDoc,
            error,
        )

        # Might be useful in the future, but no need for this ATM
        #self.configuration = options["sw_model"].getActiveConfiguration
        #self.root_component = self.configuration.GetRootComponent

        ## EXPERIMENTAL: Browse single parts in assembly
        #if filetype == SolidWorksEnums.FileTypes.SWassembly:
        #    Logger.log("d", 'walkComponentsInAssembly: ' + repr(self.walkComponentsInAssembly()))

        return options
Exemple #6
0
    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
Exemple #7
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()