예제 #1
0
class Fetch():
    data = QtCore.pyqtSignal(dict)

    def __init__(self, parent):
        self.session = QNetworkAccessManager(parent)
        self.cookies = QNetworkCookieJar()
        self.parent = parent
        self.session.setCookieJar(self.cookies)

    def base_handler(self, reply: QNetworkReply):
        try:
            response = json.loads(str(reply.readAll(), encoding='utf-8'))
            status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        except:
            self.parent.warn.add_warn("Http解析错误")
            return
        if reply.error() != QNetworkReply.NoError:
            self.handler_error(response, status_code)
        else:
            self.data.emit(response)

    def get(self, url, param=None):
        url = QtCore.QUrl(parse_url(url, param))
        request = QNetworkRequest(url)
        reply = self.session.get(request)
        return reply

    def post(self, url, param=None, data=None, json=True):
        if isinstance(data, dict):
            f = ''
            for i in data:
                f += '{}={}&'.format(i, data[i])
            data = f[:-1]
        byte_data = QtCore.QByteArray()
        byte_data.append(data)
        url = QtCore.QUrl(parse_url(url, param))
        request = QNetworkRequest(url)
        if json:
            request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json')
        reply = self.session.post(request, byte_data)
        return reply

    def handler_error(self, response, status_code):
        if isinstance(response, dict):
            message = response.get('error', 'unknown')
            self.parent.warn.add_warn('网络请求错误,错误码为{},原因为{}'.format(status_code, message))
예제 #2
0
파일: app.py 프로젝트: sethkontny/cutecoin
class Application(QObject):

    """
    Managing core application datas :
    Accounts list and general configuration
    Saving and loading the application state
    """

    loading_progressed = pyqtSignal(int, int)
    version_requested = pyqtSignal()

    def __init__(self, argv, qapp):
        """
        Create a new "cutecoin" application

        :param argv: The argv parameters of the call
        """
        super().__init__()
        self.qapp = qapp
        self.accounts = {}
        self.current_account = None
        self.monitor = None
        self.available_version = (True, __version__, "")
        config.parse_arguments(argv)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self.read_available_version)
        self.preferences = {"account": "", "lang": "en_GB", "ref": 0}

        self.load()
        self.switch_language()

    def switch_language(self):
        translator = QTranslator(self.qapp)
        logging.debug("Loading translations")
        locale = self.preferences["lang"]
        QLocale.setDefault(QLocale(locale))
        if translator.load(":/i18n/{0}".format(locale)):
            if QCoreApplication.installTranslator(translator):
                logging.debug("Loaded i18n/{0}".format(locale))
        else:
            logging.debug("Couldn't load translation")

    def get_account(self, name):
        """
        Load an account then return it

        :param str name: The account name
        :return: The loaded account if it's a success, else return None
        """
        self.load_account(name)
        if name in self.accounts.keys():
            return self.accounts[name]
        else:
            return None

    def create_account(self, name):
        """
        Create a new account from its name

        :param str name: The account name
        :return: The new account
        :raise: NameAlreadyExists if the account name is already used locally
        """
        for a in self.accounts:
            if a == name:
                raise NameAlreadyExists(a)

        account = Account.create(name)

        return account

    def add_account(self, account):
        self.accounts[account.name] = account

    def delete_account(self, account):
        """
        Delete an account.
        Current account changes to None if it is deleted.
        """
        self.accounts.pop(account.name)
        if self.current_account == account:
            self.current_account = None
        with open(config.parameters["data"], "w") as outfile:
            json.dump(self.jsonify(), outfile, indent=4, sort_keys=True)
        if self.preferences["account"] == account.name:
            self.preferences["account"] = ""
            self.save_preferences(self.preferences)

    def change_current_account(self, account):
        """
        Change current account displayed and refresh its cache.

        :param account: The account object to display
        .. note:: Emits the application pyqtSignal loading_progressed
        during cache refresh
        """

        def progressing(value, maximum):
            self.loading_progressed.emit(value, maximum)

        if self.current_account is not None:
            if self.monitor:
                self.monitor.stop_watching()
            self.save_cache(self.current_account)
        account.loading_progressed.connect(progressing)
        account.refresh_cache()
        self.monitor = Monitor(account)
        self.monitor.prepare_watching()
        self.current_account = account

    def load(self):
        """
        Load a saved application state from the data file.
        Loads only jsonified objects but not their cache.

        If the standard application state file can't be found,
        no error is raised.
        """
        self.load_persons()
        self.load_preferences()
        try:
            logging.debug("Loading data...")
            with open(config.parameters["data"], "r") as json_data:
                data = json.load(json_data)
                for account_name in data["local_accounts"]:
                    self.accounts[account_name] = None
        except FileNotFoundError:
            pass

    def load_persons(self):
        """
        Load the Person instances of the person module.
        Each instance is unique, and can be find by its public key.
        """
        try:
            persons_path = os.path.join(config.parameters["home"], "__persons__")
            with open(persons_path, "r") as persons_path:
                data = json.load(persons_path)
                person.load_cache(data)
        except FileNotFoundError:
            pass

    def load_account(self, account_name):
        """
        Load an account from its name

        :param str account_name: The account name
        """
        account_path = os.path.join(config.parameters["home"], account_name, "properties")
        with open(account_path, "r") as json_data:
            data = json.load(json_data)
            account = Account.load(data)
            self.load_cache(account)
            self.accounts[account_name] = account

    def load_cache(self, account):
        """
        Load an account cache

        :param account: The account object to load the cache
        """
        for community in account.communities:
            community_path = os.path.join(config.parameters["home"], account.name, "__cache__", community.currency)

            network_path = os.path.join(
                config.parameters["home"], account.name, "__cache__", community.currency + "_network"
            )

            if os.path.exists(network_path):
                with open(network_path, "r") as json_data:
                    data = json.load(json_data)
                if "version" in data and data["version"] == __version__:
                    logging.debug("Merging network : {0}".format(data))
                    community.load_merge_network(data["network"])
                else:
                    os.remove(network_path)

            if os.path.exists(community_path):
                with open(community_path, "r") as json_data:
                    data = json.load(json_data)
                if "version" in data and data["version"] == __version__:
                    community.load_cache(data)
                else:
                    os.remove(community_path)

        for wallet in account.wallets:
            wallet_path = os.path.join(config.parameters["home"], account.name, "__cache__", wallet.pubkey)
            if os.path.exists(wallet_path):
                with open(wallet_path, "r") as json_data:
                    data = json.load(json_data)
                if "version" in data and data["version"] == __version__:
                    wallet.load_caches(data)
                else:
                    os.remove(wallet_path)

    def load_preferences(self):
        """
        Load the preferences.
        """

        try:
            preferences_path = os.path.join(config.parameters["home"], "preferences")
            with open(preferences_path, "r") as json_data:
                data = json.load(json_data)
                self.preferences = data
        except FileNotFoundError:
            pass

    def save_preferences(self, preferences):
        """
        Save the preferences.

        :param preferences: A dict containing the keys/values of the preferences
        """
        assert "lang" in preferences
        assert "account" in preferences
        assert "ref" in preferences

        self.preferences = preferences
        preferences_path = os.path.join(config.parameters["home"], "preferences")
        with open(preferences_path, "w") as outfile:
            json.dump(preferences, outfile, indent=4)

    def save(self, account):
        """
        Save an account

        :param account: The account object to save
        """
        with open(config.parameters["data"], "w") as outfile:
            json.dump(self.jsonify(), outfile, indent=4, sort_keys=True)
        account_path = os.path.join(config.parameters["home"], account.name)
        if account.name in self.accounts:
            properties_path = os.path.join(account_path, "properties")
            if not os.path.exists(account_path):
                logging.info("Creating account directory")
                os.makedirs(account_path)
            with open(properties_path, "w") as outfile:
                json.dump(account.jsonify(), outfile, indent=4, sort_keys=True)
        else:
            account_path = os.path.join(config.parameters["home"], account.name)
            shutil.rmtree(account_path)

    def save_persons(self):
        """
        Save the person module cache
        """
        persons_path = os.path.join(config.parameters["home"], "__persons__")
        with open(persons_path, "w") as outfile:
            data = person.jsonify_cache()
            data["version"] = __version__
            json.dump(data, outfile, indent=4, sort_keys=True)

    def save_wallet(self, account, wallet):
        """
        Save wallet of account in cache

        :param cutecoin.core.account.Account account: Account instance
        :param cutecoin.core.wallet.Wallet wallet: Wallet instance
        """
        if not os.path.exists(os.path.join(config.parameters["home"], account.name, "__cache__")):
            os.makedirs(os.path.join(config.parameters["home"], account.name, "__cache__"))
        wallet_path = os.path.join(config.parameters["home"], account.name, "__cache__", wallet.pubkey)
        with open(wallet_path, "w") as outfile:
            data = wallet.jsonify_caches()
            data["version"] = __version__
            json.dump(data, outfile, indent=4, sort_keys=True)

    def save_cache(self, account):
        """
        Save the cache of an account

        :param account: The account object to save the cache
        """
        if not os.path.exists(os.path.join(config.parameters["home"], account.name, "__cache__")):
            os.makedirs(os.path.join(config.parameters["home"], account.name, "__cache__"))
        for wallet in account.wallets:
            self.save_wallet(account, wallet)

        for community in account.communities:
            community_path = os.path.join(config.parameters["home"], account.name, "__cache__", community.currency)

            network_path = os.path.join(
                config.parameters["home"], account.name, "__cache__", community.currency + "_network"
            )

            with open(network_path, "w") as outfile:
                data = dict()
                data["network"] = community.jsonify_network()
                data["version"] = __version__
                json.dump(data, outfile, indent=4, sort_keys=True)

            with open(community_path, "w") as outfile:
                data = community.jsonify_cache()
                data["version"] = __version__
                json.dump(data, outfile, indent=4, sort_keys=True)

    def import_account(self, file, name):
        """
        Import an account from a tar file and open it

        :param str file: The file path of the tar file
        :param str name: The account name
        """
        with tarfile.open(file, "r") as tar:
            path = os.path.join(config.parameters["home"], name)
            for obj in ["properties"]:
                try:
                    tar.getmember(obj)
                except KeyError:
                    raise BadAccountFile(file)
            tar.extractall(path)

        account_path = os.path.join(config.parameters["home"], name, "properties")
        json_data = open(account_path, "r")
        data = json.load(json_data)
        account = Account.load(data)
        account.name = name
        self.add_account(account)
        self.save(account)
        self.change_current_account(account)

    def export_account(self, file, account):
        """
        Export an account to a tar file

        :param str file: The filepath of the tar file
        :param account: The account object to export
        """
        with tarfile.open(file, "w") as tar:
            for file in ["properties"]:
                path = os.path.join(config.parameters["home"], account.name, file)
                tar.add(path, file)

    def jsonify_accounts(self):
        """
        Jsonify an account

        :return: The account as a dict to format as json
        """
        data = []
        logging.debug("{0}".format(self.accounts))
        for account in self.accounts:
            data.append(account)
        return data

    def jsonify(self):
        """
        Jsonify the app datas

        :return: The accounts of the app to format as json
        """
        data = {"local_accounts": self.jsonify_accounts()}
        return data

    def get_last_version(self):
        url = QUrl("https://api.github.com/repos/ucoin-io/cutecoin/releases")
        request = QNetworkRequest(url)
        self._network_manager.get(request)

    @pyqtSlot(QNetworkReply)
    def read_available_version(self, reply):
        latest = None
        releases = reply.readAll().data().decode("utf-8")
        logging.debug(releases)
        if reply.error() == QNetworkReply.NoError:
            for r in json.loads(releases):
                if not latest:
                    latest = r
                else:
                    latest_date = datetime.datetime.strptime(latest["published_at"], "%Y-%m-%dT%H:%M:%SZ")
                    date = datetime.datetime.strptime(r["published_at"], "%Y-%m-%dT%H:%M:%SZ")
                    if latest_date < date:
                        latest = r
            latest_version = latest["tag_name"]
            version = (__version__ == latest_version, latest_version, latest["html_url"])
            logging.debug("Found version : {0}".format(latest_version))
            logging.debug("Current version : {0}".format(__version__))
            self.available_version = version
        self.version_requested.emit()
예제 #3
0
class PACFetcher(QObject):

    """Asynchronous fetcher of PAC files."""

    finished = pyqtSignal()

    def __init__(self, url, parent=None):
        """Resolve a PAC proxy from URL.

        Args:
            url: QUrl of a PAC proxy.
        """
        super().__init__(parent)

        pac_prefix = "pac+"

        assert url.scheme().startswith(pac_prefix)
        url.setScheme(url.scheme()[len(pac_prefix):])

        self._pac_url = url
        self._manager = QNetworkAccessManager()
        self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
        self._reply = self._manager.get(QNetworkRequest(url))
        self._reply.finished.connect(self._finish)
        self._pac = None
        self._error_message = None

    @pyqtSlot()
    def _finish(self):
        if self._reply.error() != QNetworkReply.NoError:
            error = "Can't fetch PAC file from URL, error code {}: {}"
            self._error_message = error.format(
                self._reply.error(), self._reply.errorString())
            log.network.error(self._error_message)
        else:
            try:
                pacscript = bytes(self._reply.readAll()).decode("utf-8")
            except UnicodeError as e:
                error = "Invalid encoding of a PAC file: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
            try:
                self._pac = PACResolver(pacscript)
                log.network.debug("Successfully evaluated PAC file.")
            except EvalProxyError as e:
                error = "Error in PAC evaluation: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
        self._manager = None
        self._reply = None
        self.finished.emit()

    def _wait(self):
        """Wait until a reply from the remote server is received."""
        if self._manager is not None:
            loop = qtutils.EventLoop()
            self.finished.connect(loop.quit)
            loop.exec_()

    def fetch_error(self):
        """Check if PAC script is successfully fetched.

        Return None iff PAC script is downloaded and evaluated successfully,
        error string otherwise.
        """
        self._wait()
        return self._error_message

    def resolve(self, query):
        """Resolve a query via PAC.

        Args: QNetworkProxyQuery.

        Return a list of QNetworkProxy objects in order of preference.
        """
        self._wait()
        from_file = self._pac_url.scheme() == 'file'
        try:
            return self._pac.resolve(query, from_file=from_file)
        except (EvalProxyError, ParseProxyError) as e:
            log.network.exception("Error in PAC resolution: {}.".format(e))
            # .invalid is guaranteed to be inaccessible in RFC 6761.
            # Port 9 is for DISCARD protocol -- DISCARD servers act like
            # /dev/null.
            # Later NetworkManager.createRequest will detect this and display
            # an error message.
            error_host = "pac-resolve-error.qutebrowser.invalid"
            return [QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)]
예제 #4
0
class GridWidget(QWidget):
    Page = 0
    loadStarted = pyqtSignal(bool)

    def __init__(self, *args, **kwargs):
        super(GridWidget, self).__init__(*args, **kwargs)
        self._layout = FlowLayout(self)  # 使用自定义流式布局
        # 异步网络下载管理器
        self._manager = QNetworkAccessManager(self)
        self._manager.finished.connect(self.onFinished)

    def load(self):
        if self.Page == -1:
            return
        self.loadStarted.emit(True)
        # 延迟一秒后调用目的在于显示进度条
        QTimer.singleShot(1000, self._load)

    def _load(self):
        print("load url:", Url.format(self.Page * 30))
        url = QUrl(Url.format(self.Page * 30))
        self._manager.get(QNetworkRequest(url))

    def onFinished(self, reply):
        # 请求完成后会调用该函数
        req = reply.request()  # 获取请求
        iwidget = req.attribute(QNetworkRequest.User + 1, None)
        path = req.attribute(QNetworkRequest.User + 2, None)
        html = reply.readAll().data()
        reply.deleteLater()
        del reply
        if iwidget and path and html:
            # 这里是图片下载完毕
            open(path, "wb").write(html)
            iwidget.setCover(path)
            return
        # 解析网页
        self._parseHtml(html)
        self.loadStarted.emit(False)

    def _parseHtml(self, html):
        #         encoding = chardet.detect(html) or {}
        #         html = html.decode(encoding.get("encoding","utf-8"))
        html = HTML(html)
        # 查找所有的li list_item
        lis = html.xpath("//li[@class='list_item']")
        if not lis:
            self.Page = -1  # 后面没有页面了
            return
        self.Page += 1
        self._makeItem(lis)

    def _makeItem(self, lis):
        for li in lis:
            a = li.find("a")
            video_url = a.get("href")  # 视频播放地址
            img = a.find("img")
            cover_url = "http:" + img.get("r-lazyload")  # 封面图片
            figure_title = img.get("alt")  # 电影名
            figure_info = a.find("div/span")
            figure_info = "" if figure_info is None else figure_info.text  # 影片信息
            figure_score = "".join(li.xpath(".//em/text()"))  # 评分
            # 主演
            figure_desc = "<span style=\"font-size: 12px;\">主演:</span>" + \
                          "".join([Actor.format(**dict(fd.items()))
                                   for fd in li.xpath(".//div[@class='figure_desc']/a")])
            # 播放数
            figure_count = (
                li.xpath(".//div[@class='figure_count']/span/text()")
                or [""])[0]
            path = "cache/{0}.jpg".format(
                os.path.splitext(os.path.basename(video_url))[0])
            cover_path = "Data/pic_v.png"
            if os.path.isfile(path):
                cover_path = path
            iwidget = ItemWidget(cover_path, figure_info, figure_title,
                                 figure_score, figure_desc, figure_count,
                                 video_url, cover_url, path, self)
            self._layout.addWidget(iwidget)
예제 #5
0
파일: Toolbox.py 프로젝트: qin2512/Cura
class Toolbox(QObject, Extension):
    DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str
    DEFAULT_CLOUD_API_VERSION = 1 #type: int

    def __init__(self, application: CuraApplication) -> None:
        super().__init__()

        self._application = application #type: CuraApplication

        self._sdk_version = None    # type: Optional[int]
        self._cloud_api_version = None # type: Optional[int]
        self._cloud_api_root = None # type: Optional[str]
        self._api_url = None    # type: Optional[str]

        # Network:
        self._download_request = None #type: Optional[QNetworkRequest]
        self._download_reply = None #type: Optional[QNetworkReply]
        self._download_progress = 0 #type: float
        self._is_downloading = False #type: bool
        self._network_manager = None #type: Optional[QNetworkAccessManager]
        self._request_header = [
            b"User-Agent",
            str.encode(
                "%s/%s (%s %s)" % (
                    self._application.getApplicationName(),
                    self._application.getVersion(),
                    platform.system(),
                    platform.machine(),
                )
            )
        ]
        self._request_urls = {} # type: Dict[str, QUrl]
        self._to_update = []  # type: List[str] # Package_ids that are waiting to be updated
        self._old_plugin_ids = [] # type: List[str]

        # Data:
        self._metadata = {
            "authors":             [],
            "packages":            [],
            "plugins_showcase":    [],
            "plugins_available":   [],
            "plugins_installed":   [],
            "materials_showcase":  [],
            "materials_available": [],
            "materials_installed": [],
            "materials_generic":   []
        } # type: Dict[str, List[Any]]

        # Models:
        self._models = {
            "authors":             AuthorsModel(self),
            "packages":            PackagesModel(self),
            "plugins_showcase":    PackagesModel(self),
            "plugins_available":   PackagesModel(self),
            "plugins_installed":   PackagesModel(self),
            "materials_showcase":  AuthorsModel(self),
            "materials_available": PackagesModel(self),
            "materials_installed": PackagesModel(self),
            "materials_generic":   PackagesModel(self)
        } # type: Dict[str, ListModel]

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin" #type: str

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "loading" #type: str

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None # type: Optional[Dict[str, Any]]

        self._dialog = None #type: Optional[QObject]
        self._confirm_reset_dialog = None #type: Optional[QObject]
        self._resetUninstallVariables()

        self._restart_required = False #type: bool

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = "" #type: str
        self._license_dialog_license_content = "" #type: str
        self._license_dialog_plugin_file_location = "" #type: str
        self._restart_dialog_message = "" #type: str

        self._application.initializationFinished.connect(self._onAppInitialized)



    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()
    uninstallVariablesChanged = pyqtSignal()

    def _resetUninstallVariables(self):
        self._package_id_to_uninstall = None
        self._package_name_to_uninstall = ""
        self._package_used_materials = []
        self._package_used_qualities = []

    @pyqtSlot(result = str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result = str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result = str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._plugin_registry = self._application.getPluginRegistry()
        self._package_manager = self._application.getPackageManager()
        self._sdk_version = self._getSDKVersion()
        self._cloud_api_version = self._getCloudAPIVersion()
        self._cloud_api_root = self._getCloudAPIRoot()
        self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
            cloud_api_root=self._cloud_api_root,
            cloud_api_version=self._cloud_api_version,
            sdk_version=self._sdk_version
        )
        self._request_urls = {
            "authors": QUrl("{base_url}/authors".format(base_url=self._api_url)),
            "packages": QUrl("{base_url}/packages".format(base_url=self._api_url)),
            "plugins_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
            "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url=self._api_url)),
            "materials_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
            "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url=self._api_url)),
            "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url=self._api_url))
        }

    # Get the API root for the packages API depending on Cura version settings.
    def _getCloudAPIRoot(self) -> str:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_ROOT
        if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        return cura.CuraVersion.CuraCloudAPIRoot # type: ignore

    # Get the cloud API version from CuraVersion
    def _getCloudAPIVersion(self) -> int:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_VERSION
        if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        return cura.CuraVersion.CuraCloudAPIVersion # type: ignore

    # Get the packages version depending on Cura version settings.
    def _getSDKVersion(self) -> int:
        if not hasattr(cura, "CuraVersion"):
            return self._plugin_registry.APIVersion
        if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
            return self._plugin_registry.APIVersion
        if not cura.CuraVersion.CuraSDKVersion: # type: ignore
            return self._plugin_registry.APIVersion
        return cura.CuraVersion.CuraSDKVersion # type: ignore

    @pyqtSlot()
    def browsePackages(self) -> None:
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")
        self._makeRequestByType("plugins_showcase")
        self._makeRequestByType("materials_showcase")
        self._makeRequestByType("materials_available")
        self._makeRequestByType("materials_generic")

        # Gather installed packages:
        self._updateInstalledModels()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")

        if not self._dialog:
            Logger.log("e", "Unexpected error trying to create the 'Toolbox' dialog.")
            return

        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Toolbox: Creating dialog [%s].", qml_name)
        plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
        if not plugin_path:
            return None
        path = os.path.join(plugin_path, "resources", "qml", qml_name)
        
        dialog = self._application.createQmlComponent(path, {"toolbox": self})
        if not dialog:
            raise Exception("Failed to create toolbox dialog")
        return dialog

    def _convertPluginMetadata(self, plugin: Dict[str, Any]) -> Dict[str, Any]:
        formatted = {
            "package_id": plugin["id"],
            "package_type": "plugin",
            "display_name": plugin["plugin"]["name"],
            "package_version": plugin["plugin"]["version"],
            "sdk_version": plugin["plugin"]["api"],
            "author": {
                "author_id": plugin["plugin"]["author"],
                "display_name": plugin["plugin"]["author"]
            },
            "is_installed": True,
            "description": plugin["plugin"]["description"]
        }
        return formatted

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        # This is moved here to avoid code duplication and so that after installing plugins they get removed from the
        # list of old plugins
        old_plugin_ids = self._plugin_registry.getInstalledPlugins()
        installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
        scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()

        self._old_plugin_ids = []
        self._old_plugin_metadata = [] # type: List[Dict[str, Any]]

        for plugin_id in old_plugin_ids:
            # Neither the installed packages nor the packages that are scheduled to remove are old plugins
            if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
                Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id)

                old_metadata = self._plugin_registry.getMetaData(plugin_id)
                new_metadata = self._convertPluginMetadata(old_metadata)

                self._old_plugin_ids.append(plugin_id)
                self._old_plugin_metadata.append(new_metadata)

        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata
            self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._metadata["materials_installed"] = all_packages["material"]
            # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE
            self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Check package usage and uninstall
    #   If the package is in use, you'll get a confirmation dialog to set everything to default
    @pyqtSlot(str)
    def checkPackageUsageAndUninstall(self, package_id: str) -> None:
        package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id)
        if package_used_materials or package_used_qualities:
            # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
            self._package_id_to_uninstall = package_id
            package_info = self._package_manager.getInstalledPackageInfo(package_id)
            self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id"))
            self._package_used_materials = package_used_materials
            self._package_used_qualities = package_used_qualities
            # Ask change to default material / profile
            if self._confirm_reset_dialog is None:
                self._confirm_reset_dialog = self._createDialog("ToolboxConfirmUninstallResetDialog.qml")
            self.uninstallVariablesChanged.emit()
            if self._confirm_reset_dialog is None:
                Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.")
            else:
                self._confirm_reset_dialog.show()
        else:
            # Plain uninstall
            self.uninstall(package_id)

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def pluginToUninstall(self):
        return self._package_name_to_uninstall

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedMaterials(self):
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedQualities(self):
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])

    @pyqtSlot()
    def closeConfirmResetDialog(self):
        if self._confirm_reset_dialog is not None:
            self._confirm_reset_dialog.close()

    ##  Uses "uninstall variables" to reset qualities and materials, then uninstall
    #   It's used as an action on Confirm reset on Uninstall
    @pyqtSlot()
    def resetMaterialsQualitiesAndUninstall(self):
        application = CuraApplication.getInstance()
        material_manager = application.getMaterialManager()
        quality_manager = application.getQualityManager()
        machine_manager = application.getMachineManager()

        for global_stack, extruder_nr, container_id in self._package_used_materials:
            default_material_node = material_manager.getDefaultMaterial(global_stack, extruder_nr, global_stack.extruders[extruder_nr].variant.getName())
            machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack)
        for global_stack, extruder_nr, container_id in self._package_used_qualities:
            default_quality_group = quality_manager.getDefaultQualityType(global_stack)
            machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)

        self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)

        self.uninstall(self._package_id_to_uninstall)
        self._resetUninstallVariables()
        self.closeConfirmResetDialog()

    def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
        container_registry = self._application.getContainerRegistry()

        all_containers = self._package_manager.getPackageContainerIds(package_id)
        for container_id in all_containers:
            containers = container_registry.findInstanceContainers(id = container_id)
            if not containers:
                continue
            container = containers[0]
            if container.getMetaDataEntry("type") != "material":
                continue
            root_material_id = container.getMetaDataEntry("base_file")
            root_material_containers = container_registry.findInstanceContainers(id = root_material_id)
            if not root_material_containers:
                continue
            root_material_container = root_material_containers[0]
            root_material_container.setMetaDataEntry("removed", True)

    @pyqtSlot(str)
    def uninstall(self, package_id: str) -> None:
        self._package_manager.removePackage(package_id, force_add = True)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Actual update packages that are in self._to_update
    def _update(self) -> None:
        if self._to_update:
            plugin_id = self._to_update.pop(0)
            remote_package = self.getRemotePackage(plugin_id)
            if remote_package:
                download_url = remote_package["download_url"]
                Logger.log("d", "Updating package [%s]..." % plugin_id)
                self.startDownload(download_url)
            else:
                Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id)

        if self._to_update:
            self._application.callLater(self._update)

    ##  Update a plugin by plugin_id
    @pyqtSlot(str)
    def update(self, plugin_id: str) -> None:
        self._to_update.append(plugin_id)
        self._application.callLater(self._update)

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify = metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify = restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self) -> None:
        self._application.windowClosed()

    def getRemotePackage(self, package_id: str) -> Optional[Dict]:
        # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
        remote_package = None
        for package in self._metadata["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
                break
        return remote_package

    # Checks
    # --------------------------------------------------------------------------
    @pyqtSlot(str, result = bool)
    def canUpdate(self, package_id: str) -> bool:
        if self.isOldPlugin(package_id):
            return True

        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        remote_package = self.getRemotePackage(package_id)
        if remote_package is None:
            return False

        local_version = Version(local_package["package_version"])
        remote_version = Version(remote_package["package_version"])
        return remote_version > local_version

    @pyqtSlot(str, result = bool)
    def canDowngrade(self, package_id: str) -> bool:
        # If the currently installed version is higher than the bundled version (if present), the we can downgrade
        # this package.
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        bundled_package = self._package_manager.getBundledPackageInfo(package_id)
        if bundled_package is None:
            return False

        local_version = Version(local_package["package_version"])
        bundled_version = Version(bundled_package["package_version"])
        return bundled_version < local_version

    @pyqtSlot(str, result = bool)
    def isInstalled(self, package_id: str) -> bool:
        return self._package_manager.isPackageInstalled(package_id)

    @pyqtSlot(str, result = int)
    def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._metadata["materials_installed"]:
            if package["author"]["author_id"] == author_id:
                count += 1
        return count

    @pyqtSlot(str, result = int)
    def getTotalNumberOfPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._metadata["materials_available"]:
            if package["author"]["author_id"] == author_id:
                count += 1
        return count

    @pyqtSlot(str, result = bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    # Check for plugins that were installed with the old plugin browser
    @pyqtSlot(str, result = bool)
    def isOldPlugin(self, plugin_id: str) -> bool:
        if plugin_id in self._old_plugin_ids:
            return True
        return False

    def loadingComplete(self) -> bool:
        populated = 0
        for list in self._metadata.items():
            if len(list) > 0:
                populated += 1
        if populated == len(self._metadata.items()):
            return True
        return False

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, type: str) -> None:
        Logger.log("i", "Toolbox: Requesting %s metadata from server.", type)
        request = QNetworkRequest(self._request_urls[type])
        request.setRawHeader(*self._request_header)
        if self._network_manager:
            self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Toolbox: Attempting to download & install package from %s.", url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
        cast(QNetworkRequest, self._download_request).setRawHeader(*self._request_header)
        self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "Toolbox: User cancelled the download of a plugin.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            try:
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
            except TypeError: #Raised when the method is not connected to the signal yet.
                pass #Don't need to disconnect.
            self._download_reply.abort()
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(self, network_accessibility: QNetworkAccessManager.NetworkAccessibility) -> None:
        if network_accessibility == QNetworkAccessManager.NotAccessible:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for type, url in self._request_urls.items():
                if reply.url() == url:
                    if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
                        try:
                            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                            # Check for errors:
                            if "errors" in json_data:
                                for error in json_data["errors"]:
                                    Logger.log("e", "%s", error["title"])
                                return

                            # Create model and apply metadata:
                            if not self._models[type]:
                                Logger.log("e", "Could not find the %s model.", type)
                                break

                            # HACK: Eventually get rid of the code from here...
                            if type is "plugins_showcase" or type is "materials_showcase":
                                self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
                                self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
                                self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
                                self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
                            else:
                                # ...until here.
                                # This hack arises for multiple reasons but the main
                                # one is because there are not separate API calls
                                # for different kinds of showcases.
                                self._metadata[type] = json_data["data"]
                                self._models[type].setMetadata(self._metadata[type])

                            # Do some auto filtering
                            # TODO: Make multiple API calls in the future to handle this
                            if type is "packages":
                                self._models[type].setFilter({"type": "plugin"})
                            if type is "authors":
                                self._models[type].setFilter({"package_types": "material"})
                            if type is "materials_generic":
                                self._models[type].setFilter({"tags": "generic"})

                            self.metadataChanged.emit()

                            if self.loadingComplete() is True:
                                self.setViewPage("overview")

                            return
                        except json.decoder.JSONDecodeError:
                            Logger.log("w", "Toolbox: Received invalid JSON for %s.", type)
                            break
                    else:
                        self.setViewPage("errored")
                        self.resetDownload()
                        return

        else:
            # Ignore any operation that is not a get operation
            pass

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str):
        Logger.log("i", "Toolbox: Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Toolbox: Package file [%s] was not a valid CuraPackage.", file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content, file_path)
            return

        self.install(file_path)
        return

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: float) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
    def downloadProgress(self) -> float:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        self._active_package = package
        self.activePackageChanged.emit()

    @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        self._view_category = category
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        self._view_page = page
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewPage, notify = viewChanged)
    def viewPage(self) -> str:
        return self._view_page



    # Expose Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, notify = metadataChanged)
    def authorsModel(self) -> AuthorsModel:
        return cast(AuthorsModel, self._models["authors"])

    @pyqtProperty(QObject, notify = metadataChanged)
    def packagesModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["packages"])

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["plugins_showcase"])

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsInstalledModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["plugins_installed"])

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsShowcaseModel(self) -> AuthorsModel:
        return cast(AuthorsModel, self._models["materials_showcase"])

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsInstalledModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["materials_installed"])

    @pyqtProperty(QObject, notify=metadataChanged)
    def materialsGenericModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["materials_generic"])



    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({filter_type: parameter})
        self.filterChanged.emit()

    @pyqtSlot(str, "QVariantMap")
    def setFilters(self, model_type: str, filter_dict: dict) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter(filter_dict)
        self.filterChanged.emit()

    @pyqtSlot(str)
    def removeFilters(self, model_type: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Toolbox: Couldn't remove filters on %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({})
        self.filterChanged.emit()
예제 #6
0
class MyWindowClass(QtWidgets.QMainWindow, form_auto.Ui_MainWindow):
    def __init__(self, parent=None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.setupNet()
        self.login_status = False
        self.Username = '******'
        self.Usercart = []
        self.Shoplist = []
        self.last_error_msg = None

        actRefresh = QtWidgets.QAction(QIcon('icon_refresh.png'), 'Refresh',
                                       self)
        actRefresh.triggered.connect(self.refresh)
        actRegister = QtWidgets.QAction(QIcon('icon_reg.png'), 'Register',
                                        self)
        actRegister.triggered.connect(self.regDialog)
        actLogin = QtWidgets.QAction(QIcon('icon_login.png'), 'Log in', self)
        actLogin.triggered.connect(self.loginDialog)
        actLogout = QtWidgets.QAction(QIcon('icon_logout.png'), 'Log out',
                                      self)
        actLogout.triggered.connect(self.logoutDialog)
        actQuit = QtWidgets.QAction(QIcon('icon_quit.png'), 'Quit', self)
        actQuit.triggered.connect(self.clExit)
        self.actUsername = QtWidgets.QAction(self.Username, self)
        self.actCart = QtWidgets.QAction(QIcon('icon_cart.png'), 'Cart', self)
        self.actCart.setEnabled(False)
        self.actCart.triggered.connect(self.cartPrepare)

        self.toolbar = self.addToolBar('Tools')
        self.toolbar.addAction(actRefresh)
        self.toolbar.addAction(actLogin)
        self.toolbar.addAction(self.actUsername)
        self.toolbar.addAction(self.actCart)

        self.menuUser.addAction(actLogin)
        self.menuUser.addAction(self.actCart)
        self.menuUser.addSeparator()
        self.menuUser.addAction(actLogout)
        self.menuQuit.addAction(actRegister)
        self.menuQuit.addSeparator()
        self.menuQuit.addAction(actQuit)
        self.menubar.addAction(self.menuQuit.menuAction())
        self.menubar.addAction(self.menuUser.menuAction())

        self.getContent()
        #self.populateTable()
        self.statusBar().showMessage('Ready.')

    def setupNet(self):
        self.manager = QNetworkAccessManager()
        self.url_list = {
            "login": "******",
            "register": "http://127.0.0.1:8000/shop_site/cl_reg/",
            "content": "http://127.0.0.1:8000/shop_site/cl_index/",
            "logout": "http://127.0.0.1:8000/shop_site/cl_logout/",
            "cart": "http://127.0.0.1:8000/shop_site/cl_cart/",
            "checkout": "http://127.0.0.1:8000/shop_site/cl_check/"
        }

    def regDialog(self):
        dr = QtWidgets.QDialog(parent=self)
        dr.setWindowTitle("Register in Nothing Shop")
        dr.setWindowModality(QtCore.Qt.ApplicationModal)
        dr_layoutV = QtWidgets.QVBoxLayout()
        label_name = QtWidgets.QLabel(dr)
        label_name.setText('Enter your name:')
        dr_layoutV.addWidget(label_name)
        text_name = QtWidgets.QLineEdit(dr)
        dr_layoutV.addWidget(text_name)
        label_pass1 = QtWidgets.QLabel(dr)
        label_pass1.setText('Enter your password:'******'Enter your password again:')
        dr_layoutV.addWidget(label_pass2)
        text_pass2 = QtWidgets.QLineEdit(dr)
        text_pass2.setEchoMode(2)
        dr_layoutV.addWidget(text_pass2)
        dr_layoutH = QtWidgets.QHBoxLayout()
        submit = QtWidgets.QPushButton("Register", dr)
        dr_layoutH.addWidget(submit)
        cancel = QtWidgets.QPushButton("Cancel", dr)
        dr_layoutH.addWidget(cancel)
        dr_layoutV.addLayout(dr_layoutH)
        submit.clicked.connect(
            partial(self.register, text_name, text_pass1, text_pass2))
        cancel.clicked.connect(dr.close)
        dr.setLayout(dr_layoutV)
        dr.exec_()

    def register(self, name, pass1, pass2):
        print("Registering...")
        if not name.text() or not pass1.text() or not pass2.text():
            msg = QtWidgets.QMessageBox.warning(
                self, 'Unfilled fields',
                'You didn\'t fill all required fields!',
                QtWidgets.QMessageBox.Ok)
            return
        if pass1.text() != pass2.text():
            msg = QtWidgets.QMessageBox.warning(
                self, 'Passwords mismatch',
                'Passwords entered in both fields are not same!',
                QtWidgets.QMessageBox.Ok)
            return
        self.statusBar().showMessage('Registering...')
        url = QtCore.QUrl(self.url_list['register'])
        request = QNetworkRequest()
        request.setUrl(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          "application/x-www-form-urlencoded")
        data = QtCore.QByteArray()
        data.append(''.join(['user='******'&']))
        data.append(''.join(['password='******'Registration failed', 'Registration failed due to:\n' +
                bytes(self.replyObjectReg.readAll()).decode("utf-8"),
                QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('Registration failed.')
            return
        elif self.replyObjectReg.error() != QNetworkReply.NoError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Registration failed', ''.join([
                    'An error was encountered during connecting to shop server.\nError code: ',
                    str(self.replyObjectReg.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('An error was encountered.')
            return
        msg = QtWidgets.QMessageBox.information(
            self, 'Registration successful',
            'You are now registered in Nothing Shop!\n You can now use entered username and password to log in!',
            QtWidgets.QMessageBox.Ok)
        self.findChild(QtWidgets.QDialog).close()

    def loginDialog(self):
        if self.login_status:
            msg = QtWidgets.QMessageBox.information(
                self, 'Information',
                ''.join(['You are already logged in as:\n',
                         self.Username]), QtWidgets.QMessageBox.Ok)
        else:
            dl = QtWidgets.QDialog(parent=self)
            dl.setWindowTitle("Log in Nothing Shop")
            dl.setWindowModality(QtCore.Qt.ApplicationModal)
            dl.setFixedSize(220, 110)
            dl_layoutV = QtWidgets.QVBoxLayout()
            text_log = QtWidgets.QLineEdit(dl)
            text_log.setPlaceholderText('Enter your username here...')
            dl_layoutV.addWidget(text_log)
            text_pass = QtWidgets.QLineEdit(dl)
            text_pass.setEchoMode(2)
            text_pass.setPlaceholderText('Enter your password here...')
            dl_layoutV.addWidget(text_pass)
            dl_layoutH = QtWidgets.QHBoxLayout()
            submit = QtWidgets.QPushButton("Login", dl)
            dl_layoutH.addWidget(submit)
            cancel = QtWidgets.QPushButton("Cancel", dl)
            dl_layoutH.addWidget(cancel)
            dl_layoutV.addLayout(dl_layoutH)
            submit.clicked.connect(partial(self.login, text_log, text_pass))
            cancel.clicked.connect(dl.close)
            dl.setLayout(dl_layoutV)
            dl.exec_()

    def login(self, name_field, pass_field):
        print("Logging in...")
        self.statusBar().showMessage('Logging in...')
        self.Username = name_field.text()
        url = QtCore.QUrl(self.url_list['login'])
        request = QNetworkRequest()
        request.setUrl(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          "application/x-www-form-urlencoded")
        data = QtCore.QByteArray()
        data.append(''.join(['user='******'&']))
        data.append(''.join(['password='******'Login failed',
                'Wrong username or password.\nPlease try again.',
                QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('Login attempt failed.')
            return
        elif self.replyObjectLogin.error() != QNetworkReply.NoError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Login failed', ''.join([
                    'An error was encountered during connecting to shop server.\n Error code: ',
                    str(self.replyObjectReg.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('An error was encountered.')
            return
        self.user_token = bytes(
            self.replyObjectLogin.readAll()).decode("utf-8")
        self.login_status = True
        self.findChild(QtWidgets.QDialog).close()
        self.actUsername.setText(''.join(['Logged in as ', self.Username]))
        self.actCart.setEnabled(True)
        self.statusBar().showMessage('Logged in.')

    def logoutDialog(self):
        if self.login_status:
            msg = QtWidgets.QMessageBox.question(
                self, 'Confirmation', 'Are you sure you want to log out?',
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                QtWidgets.QMessageBox.No)
            if msg == QtWidgets.QMessageBox.Yes:
                self.user_token = None
                self.login_status = False
                self.Username = '******'
                self.Usercart = []
                self.actUsername.setText(self.Username)
                self.actCart.setEnabled(False)
                self.statusBar().showMessage('Logged out.')
        else:
            msg = QtWidgets.QMessageBox.question(
                self, 'Information', 'You are already logged out!',
                QtWidgets.QMessageBox.Ok)

    def refresh(self):
        try:
            self.scrollAreaWidgetContents.setParent(None)
        except AttributeError:
            pass
        self.getContent()

    def getContent(self):
        print("Getting content...")
        url = QtCore.QUrl(self.url_list['content'])
        request = QNetworkRequest()
        request.setUrl(url)
        self.replyObject = self.manager.get(request)
        self.replyObject.finished.connect(self.populateShopList)

    def populateShopList(self):
        print('Populating list...')
        if self.replyObject.error() == QNetworkReply.ConnectionRefusedError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Shoplist fetching failed',
                'Shop server is currently refusing connections.\nCheck your internet connection.',
                QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage("Couldn't connect to shop server.")
            return
        elif self.replyObject.error() != QNetworkReply.NoError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Shoplist fetching failed', ''.join([
                    'An error was encountered during connecting to shop server.\n Error code: ',
                    str(self.replyObject.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('An error was encountered.')
            return
        answerAsJson = bytes(self.replyObject.readAll()).decode("utf-8")
        try:
            answerAsText = json.loads(answerAsJson)
        except json.decoder.JSONDecodeError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Shoplist fetching failed',
                'An error was encountered while parsing shoplist data.\nPlease try again.',
                QtWidgets.QMessageBox.Ok)
            return
        self.Shoplist.clear()
        for item in answerAsText:
            self.Shoplist.append(
                Merch(item['id'], item['name'], item['desc'], item['quantity'],
                      item['price']))
            data = urllib.request.urlopen(''.join(
                ['http://127.0.0.1:8000/static/', item['image']]))
            self.Shoplist[-1].image = QtGui.QPixmap()
            self.Shoplist[-1].image.loadFromData(data.read())
        self.populateTable()

    def populateTable(self):
        print("Making tables...")
        self.scrollAreaWidgetContents = QtWidgets.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 620, 419))
        self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
        self.merchList.setWidget(self.scrollAreaWidgetContents)

        self.mainVerticalLayer = QtWidgets.QVBoxLayout(
            self.scrollAreaWidgetContents)
        self.mainVerticalLayer.setObjectName("mainVerticalLayer")

        MerchBoxList = []
        ImageLabelList = []
        NameLabelList = []
        DescBoxList = []
        RemainLabelList = []
        PriceLabelList = []
        MerchNumList = []
        BuyButtonList = []
        MerchBoxHWidgetList = []
        MerchBoxH1LayoutList = []
        MerchBoxH2LayoutList = []
        MerchBoxV1LayoutList = []
        MerchBoxV2LayoutList = []
        for merch in self.Shoplist:
            MerchBoxList.append(
                QtWidgets.QGroupBox(self.scrollAreaWidgetContents))
            MerchBoxList[-1].setTitle(merch.name)

            MerchBoxHWidgetList.append(QtWidgets.QWidget(MerchBoxList[-1]))
            # Columnt 1 - Image
            MerchBoxH1LayoutList.append(
                QtWidgets.QHBoxLayout(MerchBoxHWidgetList[-1]))
            MerchBoxH1LayoutList[-1].setSizeConstraint(
                QtWidgets.QLayout.SetDefaultConstraint)
            MerchBoxH1LayoutList[-1].setContentsMargins(0, 0, 10, 6)
            MerchBoxH1LayoutList[-1].setSpacing(10)

            ImageLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1]))
            ImageLabelList[-1].setMinimumSize(QtCore.QSize(150, 150))
            ImageLabelList[-1].setAlignment(QtCore.Qt.AlignCenter)
            ImageLabelList[-1].setPixmap(
                merch.image.scaled(ImageLabelList[-1].size(), 1))
            MerchBoxH1LayoutList[-1].addWidget(ImageLabelList[-1])

            # Column 2 - Name, description
            MerchBoxV1LayoutList.append(QtWidgets.QVBoxLayout())
            MerchBoxV1LayoutList[-1].setSizeConstraint(
                QtWidgets.QLayout.SetDefaultConstraint)
            MerchBoxV1LayoutList[-1].setContentsMargins(0, 0, 10, 10)
            MerchBoxV1LayoutList[-1].setSpacing(10)

            NameLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1]))
            NameLabelList[-1].setText(merch.name)
            MerchBoxV1LayoutList[-1].addWidget(NameLabelList[-1])

            DescBoxList.append(QtWidgets.QTextEdit(MerchBoxHWidgetList[-1]))
            DescBoxList[-1].setPlainText(merch.desc)
            DescBoxList[-1].setReadOnly(True)
            MerchBoxV1LayoutList[-1].addWidget(DescBoxList[-1])
            MerchBoxH1LayoutList[-1].addLayout(MerchBoxV1LayoutList[-1])

            # Column 3 - Remaining, Price, Put in cart controls
            MerchBoxV2LayoutList.append(QtWidgets.QVBoxLayout())
            MerchBoxV2LayoutList[-1].setSizeConstraint(
                QtWidgets.QLayout.SetDefaultConstraint)
            MerchBoxV2LayoutList[-1].setContentsMargins(0, 0, 10, 10)
            MerchBoxV2LayoutList[-1].setSpacing(10)

            RemainLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1]))
            RemainLabelList[-1].setAlignment(QtCore.Qt.AlignCenter)
            RemainLabelList[-1].setText('Remaining: ' + str(merch.quant))
            MerchBoxV2LayoutList[-1].addWidget(RemainLabelList[-1])

            PriceLabelList.append(QtWidgets.QLabel(MerchBoxHWidgetList[-1]))
            PriceLabelList[-1].setAlignment(QtCore.Qt.AlignCenter)
            PriceLabelList[-1].setText('Price: $' + str(merch.price))
            MerchBoxV2LayoutList[-1].addWidget(PriceLabelList[-1])

            MerchBoxH2LayoutList.append(QtWidgets.QHBoxLayout())
            MerchBoxH2LayoutList[-1].setSizeConstraint(
                QtWidgets.QLayout.SetDefaultConstraint)
            MerchBoxH2LayoutList[-1].setContentsMargins(0, 0, 0, 0)
            MerchBoxH2LayoutList[-1].setSpacing(10)

            MerchNumList.append(QtWidgets.QSpinBox(MerchBoxHWidgetList[-1]))
            MerchNumList[-1].setMinimum(1)
            MerchNumList[-1].setMaximum(merch.quant)
            MerchBoxH2LayoutList[-1].addWidget(MerchNumList[-1])

            BuyButtonList.append(QtWidgets.QPushButton(
                MerchBoxHWidgetList[-1]))
            BuyButtonList[-1].setText("In cart")
            BuyButtonList[-1].clicked.connect(
                partial(self.putCart, merch, MerchNumList[-1], True))
            if merch.quant == 0:
                BuyButtonList[-1].setEnabled(False)
            MerchBoxH2LayoutList[-1].addWidget(BuyButtonList[-1])
            MerchBoxV2LayoutList[-1].addLayout(MerchBoxH2LayoutList[-1])
            MerchBoxH1LayoutList[-1].addLayout(MerchBoxV2LayoutList[-1])

            MerchBoxV1LayoutList[-1].addStretch(1)
            MerchBoxH2LayoutList[-1].addStretch(1)
            MerchBoxH1LayoutList[-1].setStretch(0, 3)
            MerchBoxH1LayoutList[-1].setStretch(1, 8)
            MerchBoxH1LayoutList[-1].setStretch(2, 2)
            MerchBoxList[-1].setLayout(MerchBoxH1LayoutList[-1])
            self.mainVerticalLayer.addWidget(MerchBoxList[-1])
        self.merchList.setWidget(self.scrollAreaWidgetContents)
        self.horizontalLayout_3.addWidget(self.merchList)

    def cartPrepare(self):
        print("Fetching user cart...")
        self.statusBar().showMessage('Fetching user cart...')
        url = QtCore.QUrl(self.url_list['cart'])
        request = QNetworkRequest()
        request.setUrl(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          "application/x-www-form-urlencoded")
        data = QtCore.QByteArray()
        data.append(''.join(['token=', self.user_token, '&']))
        data.append(''.join(['submethod=', 'get']))
        self.replyObjectCart = self.manager.post(request, data)
        self.replyObjectCart.finished.connect(self.cartDialog)

    def cartDialog(self):
        if self.replyObjectCart.error(
        ) == QNetworkReply.AuthenticationRequiredError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Login error', ''.join([
                    'There was a problem with your credentials.\nPlease try logging in again.\n Error code: ',
                    str(self.replyObjectCart.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage("Couldn't fetch user cart.")
            return
        answerAsJson = bytes(self.replyObjectCart.readAll()).decode("utf-8")
        answerAsText = json.loads(answerAsJson)
        self.Usercart.clear()
        for item in answerAsText:
            self.Usercart.append({
                "merch":
                next(x for x in self.Shoplist if x.id == item['id']),
                "num":
                item['num']
            })
        dc = QtWidgets.QDialog(parent=self)
        dc.setWindowTitle(''.join(["Shop Cart - ", self.Username]))
        dc.setWindowModality(QtCore.Qt.ApplicationModal)
        #dc.resize(600, 420)
        dc_layoutV = QtWidgets.QVBoxLayout()
        lazylist = []
        BoxList = []
        BoxWidgetList = []
        BoxLayoutList = []
        ImgList = []
        NameList = []
        NumList = []
        NumEditList = []
        RemButtonList = []
        if self.Usercart:
            for item in self.Usercart:
                BoxList.append(QtWidgets.QGroupBox(dc))
                BoxList[-1].setObjectName(''.join(
                    ['Box', str(item['merch'].id)]))
                BoxWidgetList.append(QtWidgets.QWidget(BoxList[-1]))
                BoxLayoutList.append(QtWidgets.QHBoxLayout(BoxWidgetList[-1]))
                BoxLayoutList[-1].setSizeConstraint(
                    QtWidgets.QLayout.SetDefaultConstraint)
                BoxLayoutList[-1].setContentsMargins(5, 5, 5, 5)
                BoxLayoutList[-1].setSpacing(10)

                ImgList.append(QtWidgets.QLabel(BoxWidgetList[-1]))
                ImgList[-1].setMinimumSize(QtCore.QSize(50, 50))
                ImgList[-1].setAlignment(QtCore.Qt.AlignCenter)
                ImgList[-1].setPixmap(item['merch'].image.scaled(
                    ImgList[-1].size(), 1))
                BoxLayoutList[-1].addWidget(ImgList[-1])

                NameList.append(QtWidgets.QLabel(BoxWidgetList[-1]))
                NameList[-1].setAlignment(QtCore.Qt.AlignCenter)
                NameList[-1].setText(item['merch'].name)
                BoxLayoutList[-1].addWidget(NameList[-1])

                NumList.append(QtWidgets.QLabel(BoxWidgetList[-1]))
                NumList[-1].setAlignment(QtCore.Qt.AlignCenter)
                NumList[-1].setText(''.join(
                    ['In cart: ', str(item['num']), ' units']))
                NumList[-1].setObjectName(''.join(
                    ['NumLabel', str(item['merch'].id)]))
                BoxLayoutList[-1].addWidget(NumList[-1])

                NumEditList.append(QtWidgets.QSpinBox(BoxWidgetList[-1]))
                NumEditList[-1].setMinimum(1)
                NumEditList[-1].setMaximum(item['num'])
                BoxLayoutList[-1].addWidget(NumEditList[-1])

                RemButtonList.append(QtWidgets.QPushButton(BoxWidgetList[-1]))
                RemButtonList[-1].setText('Remove from cart')
                RemButtonList[-1].clicked.connect(
                    partial(self.putCart, item['merch'], NumEditList[-1],
                            False))
                BoxLayoutList[-1].addWidget(RemButtonList[-1])

                BoxList[-1].setLayout(BoxLayoutList[-1])
                dc_layoutV.addWidget(BoxList[-1])
        else:
            nothinglabel = QtWidgets.QLabel(dc)
            nothinglabel.setText('Your cart is empty!')
            dc_layoutV.addWidget(nothinglabel)
        submitbtn = QtWidgets.QPushButton("Purchase!", dc)
        submitbtn.clicked.connect(self.purchaseDialog)
        submitbtn.setObjectName('submitbtn')
        if not self.Usercart:
            submitbtn.setEnabled(False)
        dc_layoutV.addWidget(submitbtn)
        cancelbtn = QtWidgets.QPushButton("Close cart...", dc)
        dc_layoutV.addWidget(cancelbtn)
        cancelbtn.clicked.connect(dc.close)
        dc.setLayout(dc_layoutV)
        dc.exec_()

    def putCart(self, merch, num_field, add):
        print("Putting merchandize in cart...")
        self.statusBar().showMessage('Putting merchandize in cart...')
        if self.login_status:
            url = QtCore.QUrl(self.url_list['cart'])
            request = QNetworkRequest()
            request.setUrl(url)
            request.setHeader(QNetworkRequest.ContentTypeHeader,
                              "application/x-www-form-urlencoded")
            data = QtCore.QByteArray()
            data.append(''.join(['token=', self.user_token, '&']))
            data.append(''.join(['submethod=', 'change', '&']))
            data.append(''.join(['merchid=', str(merch.id), '&']))
            if add:
                data.append(''.join(['merchnum=', str(num_field.value())]))
            else:
                data.append(''.join(['merchnum=-', str(num_field.value())]))
            self.replyObjectPutCart = self.manager.post(request, data)
            self.replyObjectPutCart.finished.connect(self.putCartResult)
        else:
            msg = QtWidgets.QMessageBox.information(
                self, 'Information',
                'You need to log in first\nto use shopping cart!',
                QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('Not logged in yet.')

    def putCartResult(self):
        print("A thing is happening!")
        if self.replyObjectPutCart.error(
        ) == QNetworkReply.AuthenticationRequiredError:
            self.statusBar().showMessage("Authentication failed.")
            msg = QtWidgets.QMessageBox.warning(
                self, 'Authentication failed',
                'Your login session has expired.\nPlease relog into shop and try again.',
                QtWidgets.QMessageBox.Ok)
            return
        elif self.replyObjectPutCart.error() != QNetworkReply.NoError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Authentication failed', ''.join([
                    'An error was encountered during connecting to shop server.\n Error code: ',
                    str(self.replyObjectPutCart.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('An error was encountered.')
            return
        answerAsJson = bytes(self.replyObjectPutCart.readAll()).decode("utf-8")
        answerAsText = json.loads(answerAsJson)
        print(answerAsText)
        if 'merch+' in answerAsText:
            print("A merch+ is happening!")
            next(x for x in self.Shoplist if x.id ==
                 answerAsText['merch+']).quant = answerAsText['quantity']
            self.scrollAreaWidgetContents.setParent(None)
            self.populateTable()
            self.statusBar().showMessage('Placed merchandize in cart.')
        elif 'merch-' in answerAsText:
            print("A merch- is happening!")
            item = next(x for x in self.Usercart
                        if x['merch'].id == answerAsText['merch-'])
            item['num'] -= answerAsText['quantity']
            item['merch'].quant += answerAsText['quantity']
            dc = self.findChild(QtWidgets.QDialog)
            if item['num'] < 1:
                dc.findChild(QtWidgets.QGroupBox,
                             ''.join(['Box', str(item['merch'].id)])).hide()
                self.Usercart.remove(item)
                if not self.Usercart:
                    dc.findChild(QtWidgets.QPushButton,
                                 'submitbtn').setEnabled(False)
            else:
                label = dc.findChild(
                    QtWidgets.QLabel,
                    ''.join(['NumLabel', str(item['merch'].id)]))
                label.setText(''.join(
                    ['In cart: ', str(item['num']), ' units']))
            self.scrollAreaWidgetContents.setParent(None)
            self.populateTable()
            self.statusBar().showMessage('Removed merchandize from cart.')

    def purchaseDialog(self):
        dp = QtWidgets.QDialog(parent=self)
        dp.setWindowTitle("Checkout")
        dp.setWindowModality(QtCore.Qt.ApplicationModal)
        dp_layoutV = QtWidgets.QVBoxLayout()
        label_address = QtWidgets.QLabel(dp)
        label_address.setText('Enter your address:')
        dp_layoutV.addWidget(label_address)
        text_address = QtWidgets.QTextEdit(dp)
        dp_layoutV.addWidget(text_address)
        label_date = QtWidgets.QLabel(dp)
        label_date.setText(
            'Enter date when you want to receive your purchase:')
        dp_layoutV.addWidget(label_date)
        text_date = QtWidgets.QLineEdit(dp)
        dp_layoutV.addWidget(text_date)
        label_mail = QtWidgets.QLabel(dp)
        label_mail.setText('Enter your mail to receive confirmation letter:')
        dp_layoutV.addWidget(label_mail)
        text_mail = QtWidgets.QLineEdit(dp)
        dp_layoutV.addWidget(text_mail)
        dp_layoutH = QtWidgets.QHBoxLayout()
        submit = QtWidgets.QPushButton("Purchase!", dp)
        dp_layoutH.addWidget(submit)
        cancel = QtWidgets.QPushButton("Cancel", dp)
        dp_layoutH.addWidget(cancel)
        dp_layoutV.addLayout(dp_layoutH)
        submit.clicked.connect(self.purchase)
        cancel.clicked.connect(dp.close)
        dp.setLayout(dp_layoutV)
        dp.exec_()

    def purchase(self):
        print("Confirming purchase...")
        self.statusBar().showMessage('Confirming purchase...')
        url = QtCore.QUrl(self.url_list['checkout'])
        request = QNetworkRequest()
        request.setUrl(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          "application/x-www-form-urlencoded")
        data = QtCore.QByteArray()
        data.append(''.join(['token=', self.user_token]))
        self.replyObjectPur = self.manager.post(request, data)
        self.replyObjectPur.finished.connect(self.purchaseFinalize)

    def purchaseFinalize(self):
        if self.replyObjectPur.error(
        ) == QNetworkReply.AuthenticationRequiredError:
            self.statusBar().showMessage("Authentication failed.")
            msg = QtWidgets.QMessageBox.warning(
                self, 'Authentication failed',
                'Your login session has expired.\nPlease relog into shop and try again.',
                QtWidgets.QMessageBox.Ok)
            return
        elif self.replyObjectPur.error() != QNetworkReply.NoError:
            msg = QtWidgets.QMessageBox.warning(
                self, 'Confirmation failed', ''.join([
                    'An error was encountered during connecting to shop server.\n Error code: ',
                    str(self.replyObjectPutCart.error())
                ]), QtWidgets.QMessageBox.Ok)
            self.statusBar().showMessage('An error was encountered.')
            return
        msg = QtWidgets.QMessageBox.information(
            self, 'Purchase completed!',
            'Congratulations! Your merchandize is already on it\'s way!',
            QtWidgets.QMessageBox.Ok)
        for window in self.findChildren(QtWidgets.QDialog):
            window.close()

    def clExit(self):
        app.quit()
예제 #7
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal(
    )  # Called '...Signal' to avoid confusion with function-names.
    removeDeviceSignal = Signal()  # Ditto ^^^.
    discoveredDevicesChanged = Signal()
    cloudFlowIsPossible = Signal()

    def __init__(self):
        super().__init__()

        self._zero_conf = None
        self._zero_conf_browser = None

        self._application = CuraApplication.getInstance()

        # Create a cloud output device manager that abstracts all cloud connection logic away.
        self._cloud_output_device_manager = CloudOutputDeviceManager()

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        self._application.globalContainerStackChanged.connect(
            self.refreshConnections)

        self._discovered_devices = {}

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")
        self._min_cloud_version = Version("5.2.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = CuraApplication.getInstance().getPreferences()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  # A comma-separated list of ip adresses or hostnames

        manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")
        self._manual_instances = {
            address: ManualPrinterRequest(address)
            for address in manual_instances
        }  # type: Dict[str, ManualPrinterRequest]

        # Store the last manual entry key
        self._last_manual_entry_key = ""  # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(
            target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._account = self._application.getCuraAPI().account

        # Check if cloud flow is possible when user logs in
        self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)

        # Check if cloud flow is possible when user switches machines
        self._application.globalContainerStackChanged.connect(
            self._onMachineSwitched)

        # Listen for when cloud flow is possible
        self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)

        # Listen if cloud cluster was added
        self._cloud_output_device_manager.addedCloudCluster.connect(
            self._onCloudPrintingConfigured)

        # Listen if cloud cluster was removed
        self._cloud_output_device_manager.removedCloudCluster.connect(
            self.checkCloudFlowIsPossible)

        self._start_cloud_flow_message = None  # type: Optional[Message]
        self._cloud_flow_complete_message = None  # type: Optional[Message]

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()
        self._cloud_output_device_manager.start()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(
            self._zero_conf, u'_ultimaker._tcp.local.',
            [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    # TODO: CHANGE TO HOSTNAME
    def refreshConnections(self):
        active_machine = CuraApplication.getInstance().getGlobalContainerStack(
        )
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    # It should already be set, but if it actually connects we know for sure it's supported!
                    active_machine.addConfiguredConnectionType(
                        self._discovered_devices[key].connectionType.value)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[
                        key].connectionStateChanged.connect(
                            self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log(
                        "d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[
                        key].connectionStateChanged.disconnect(
                            self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = CuraApplication.getInstance(
            ).getGlobalContainerStack().getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(
                    self._discovered_devices[key])
                self.checkCloudFlowIsPossible(None)
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()
        self._cloud_output_device_manager.stop()

    def canAddManualDevice(self,
                           address: str = "") -> ManualDeviceAdditionAttempt:
        # This plugin should always be the fallback option (at least try it):
        return ManualDeviceAdditionAttempt.POSSIBLE

    def removeManualDevice(self,
                           key: str,
                           address: Optional[str] = None) -> None:
        if key not in self._discovered_devices and address is not None:
            key = "manual:%s" % address

        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            manual_printer_request = self._manual_instances.pop(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances.keys()))

            if manual_printer_request.network_reply is not None:
                manual_printer_request.network_reply.abort()

            if manual_printer_request.callback is not None:
                self._application.callLater(manual_printer_request.callback,
                                            False, address)

    def addManualDevice(
            self,
            address: str,
            callback: Optional[Callable[[bool, str], None]] = None) -> None:
        self._manual_instances[address] = ManualPrinterRequest(
            address, callback=callback)
        self._preferences.setValue("um3networkprinting/manual_instances",
                                   ",".join(self._manual_instances.keys()))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary":
            b"true"  # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        reply = self._checkManualDevice(address)
        self._manual_instances[address].network_reply = reply

    def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
        discovered_device = self._discovered_devices.get(key)
        if discovered_device is None:
            Logger.log("e", "Could not find discovered device with key [%s]",
                       key)
            return

        group_name = discovered_device.getProperty("name")
        machine_type_id = discovered_device.getProperty("printer_type")

        Logger.log(
            "i",
            "Creating machine from network device with key = [%s], group name = [%s],  printer type = [%s]",
            key, group_name, machine_type_id)

        self._application.getMachineManager().addMachine(
            machine_type_id, group_name)
        # connect the new machine to that network printer
        self.associateActiveMachineWithPrinterDevice(discovered_device)
        # ensure that the connection states are refreshed.
        self.refreshConnections()

    def associateActiveMachineWithPrinterDevice(
            self, printer_device: Optional["PrinterOutputDevice"]) -> None:
        if not printer_device:
            return

        Logger.log(
            "d",
            "Attempting to set the network key of the active machine to %s",
            printer_device.key)

        machine_manager = CuraApplication.getInstance().getMachineManager()
        global_container_stack = machine_manager.activeMachine
        if not global_container_stack:
            return

        for machine in machine_manager.getMachinesInGroup(
                global_container_stack.getMetaDataEntry("group_id")):
            machine.setMetaDataEntry("um_network_key", printer_device.key)
            machine.setMetaDataEntry("group_name", printer_device.name)

            # Delete old authentication data.
            Logger.log(
                "d", "Removing old authentication id %s for device %s",
                global_container_stack.getMetaDataEntry(
                    "network_authentication_id", None), printer_device.key)

            machine.removeMetaDataEntry("network_authentication_id")
            machine.removeMetaDataEntry("network_authentication_key")

            # Ensure that these containers do know that they are configured for network connection
            machine.addConfiguredConnectionType(
                printer_device.connectionType.value)

        self.refreshConnections()

    def _checkManualDevice(self, address: str) -> "QNetworkReply":
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        return self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply: "QNetworkReply") -> None:
        reply_url = reply.url().toString()

        address = reply.url().host()
        device = None
        properties = {}  # type: Dict[bytes, bytes]

        if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
            # Either:
            #  - Something went wrong with checking the firmware version!
            #  - Something went wrong with checking the amount of printers the cluster has!
            #  - Couldn't find printer at the address when trying to add it manually.
            if address in self._manual_instances:
                key = "manual:" + address
                self.removeManualDevice(key, address)
            return

        if "system" in reply_url:
            try:
                system_info = json.loads(
                    bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            if address in self._manual_instances:
                manual_printer_request = self._manual_instances[address]
                manual_printer_request.network_reply = None
                if manual_printer_request.callback is not None:
                    self._application.callLater(
                        manual_printer_request.callback, True, address)

            has_cluster_capable_firmware = Version(
                system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": (system_info["name"] + " (manual)").encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine":
                str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address +
                                   self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(
                    bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b"cluster_size"] = str(
                    len(cluster_printers_list)).encode("utf-8")
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id: str) -> None:
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(
                        self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass
            self._application.getDiscoveredPrintersModel(
            ).removeDiscoveredPrinter(device.address)
            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        printer_type = properties.get(b"machine", b"").decode("utf-8")
        printer_type_identifiers = {
            "9066": "ultimaker3",
            "9511": "ultimaker3_extended",
            "9051": "ultimaker_s5"
        }

        for key, value in printer_type_identifiers.items():
            if printer_type.startswith(key):
                properties[b"printer_type"] = bytes(value, encoding="utf8")
                break
        else:
            properties[b"printer_type"] = b"Unknown"
        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(
                name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(
                name, address, properties)
        self._application.getDiscoveredPrintersModel().addDiscoveredPrinter(
            address, device.getId(), properties[b"name"].decode("utf-8"),
            self._createMachineFromDiscoveredPrinter,
            properties[b"printer_type"].decode("utf-8"), device)
        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = CuraApplication.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack and device.getId(
        ) == global_container_stack.getMetaDataEntry("um_network_key"):
            # Ensure that the configured connection type is set.
            global_container_stack.addConfiguredConnectionType(
                device.connectionType.value)
            device.connect()
            device.connectionStateChanged.connect(
                self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name,
                                     state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout=5.0)

            # Stop if the application is shutting down
            if CuraApplication.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = [
            ]  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type,
                                                    name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException(
                        "e",
                        "Failed to get service info for [%s] [%s], the request will be rescheduled",
                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be careful
    #   calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address,
                                                  info.properties)
                    else:
                        Logger.log(
                            "w",
                            "The type of the found device is '%s', not 'printer'! Ignoring.."
                            % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True

    ## Check if the prerequsites are in place to start the cloud flow
    def checkCloudFlowIsPossible(self,
                                 cluster: Optional[CloudOutputDevice]) -> None:
        Logger.log("d", "Checking if cloud connection is possible...")

        # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
        active_machine = self._application.getMachineManager(
        ).activeMachine  # type: Optional[GlobalStack]
        if active_machine:
            # Check 1A: Printer isn't already configured for cloud
            if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
                Logger.log("d",
                           "Active machine was already configured for cloud.")
                return

            # Check 1B: Printer isn't already configured for cloud
            if active_machine.getMetaDataEntry("cloud_flow_complete", False):
                Logger.log("d",
                           "Active machine was already configured for cloud.")
                return

            # Check 2: User did not already say "Don't ask me again"
            if active_machine.getMetaDataEntry("do_not_show_cloud_message",
                                               False):
                Logger.log(
                    "d", "Active machine shouldn't ask about cloud anymore.")
                return

            # Check 3: User is logged in with an Ultimaker account
            if not self._account.isLoggedIn:
                Logger.log("d", "Cloud Flow not possible: User not logged in!")
                return

            # Check 4: Machine is configured for network connectivity
            if not self._application.getMachineManager(
            ).activeMachineHasNetworkConnection:
                Logger.log(
                    "d", "Cloud Flow not possible: Machine is not connected!")
                return

            # Check 5: Machine has correct firmware version
            firmware_version = self._application.getMachineManager(
            ).activeMachineFirmwareVersion  # type: str
            if not Version(firmware_version) > self._min_cloud_version:
                Logger.log(
                    "d",
                    "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)",
                    firmware_version, self._min_cloud_version)
                return

            Logger.log("d", "Cloud flow is possible!")
            self.cloudFlowIsPossible.emit()

    def _onCloudFlowPossible(self) -> None:
        # Cloud flow is possible, so show the message
        if not self._start_cloud_flow_message:
            self._createCloudFlowStartMessage()
        if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.show()

    def _onCloudPrintingConfigured(self, device) -> None:
        # Hide the cloud flow start message if it was hanging around already
        # For example: if the user already had the browser openen and made the association themselves
        if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()

        # Cloud flow is complete, so show the message
        if not self._cloud_flow_complete_message:
            self._createCloudFlowCompleteMessage()
        if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.show()

        # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
        active_machine = self._application.getMachineManager().activeMachine
        if active_machine:

            # The active machine _might_ not be the machine that was in the added cloud cluster and
            # then this will hide the cloud message for the wrong machine. So we only set it if the
            # host names match between the active machine and the newly added cluster
            saved_host_name = active_machine.getMetaDataEntry(
                "um_network_key", "").split('.')[0]
            added_host_name = device.toDict()["host_name"]

            if added_host_name == saved_host_name:
                active_machine.setMetaDataEntry("do_not_show_cloud_message",
                                                True)

        return

    def _onDontAskMeAgain(self, checked: bool) -> None:
        active_machine = self._application.getMachineManager(
        ).activeMachine  # type: Optional[GlobalStack]
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message",
                                            checked)
            if checked:
                Logger.log(
                    "d",
                    "Will not ask the user again to cloud connect for current printer."
                )
        return

    def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager(
        ).activeMachineAddress  # type: str
        if address:
            QDesktopServices.openUrl(
                QUrl("http://" + address + "/cloud_connect"))
            if self._start_cloud_flow_message:
                self._start_cloud_flow_message.hide()
                self._start_cloud_flow_message = None
        return

    def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager(
        ).activeMachineAddress  # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/settings"))
        return

    def _onMachineSwitched(self) -> None:
        # Hide any left over messages
        if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.hide()

        # Check for cloud flow again with newly selected machine
        self.checkCloudFlowIsPossible(None)

    def _createCloudFlowStartMessage(self):
        self._start_cloud_flow_message = Message(
            text=i18n_catalog.i18nc(
                "@info:status",
                "Send and monitor print jobs from anywhere using your Ultimaker account."
            ),
            lifetime=0,
            image_source=QUrl.fromLocalFile(
                os.path.join(
                    PluginRegistry.getInstance().getPluginPath(
                        "UM3NetworkPrinting"), "resources", "svg",
                    "cloud-flow-start.svg")),
            image_caption=i18n_catalog.i18nc(
                "@info:status Ultimaker Cloud is a brand name and shouldn't be translated.",
                "Connect to Ultimaker Cloud"),
            option_text=i18n_catalog.i18nc(
                "@action", "Don't ask me again for this printer."),
            option_state=False)
        self._start_cloud_flow_message.addAction(
            "", i18n_catalog.i18nc("@action", "Get started"), "", "")
        self._start_cloud_flow_message.optionToggled.connect(
            self._onDontAskMeAgain)
        self._start_cloud_flow_message.actionTriggered.connect(
            self._onCloudFlowStarted)

    def _createCloudFlowCompleteMessage(self):
        self._cloud_flow_complete_message = Message(
            text=i18n_catalog.i18nc(
                "@info:status",
                "You can now send and monitor print jobs from anywhere using your Ultimaker account."
            ),
            lifetime=30,
            image_source=QUrl.fromLocalFile(
                os.path.join(
                    PluginRegistry.getInstance().getPluginPath(
                        "UM3NetworkPrinting"), "resources", "svg",
                    "cloud-flow-completed.svg")),
            image_caption=i18n_catalog.i18nc("@info:status", "Connected!"))
        self._cloud_flow_complete_message.addAction(
            "", i18n_catalog.i18nc("@action", "Review your connection"), "",
            "", 1)  # TODO: Icon
        self._cloud_flow_complete_message.actionTriggered.connect(
            self._onReviewCloudConnection)
예제 #8
0
파일: apps_table.py 프로젝트: lavkeshg/bauh
class AppsTable(QTableWidget):

    COL_NUMBER = 8

    def __init__(self, parent: QWidget, icon_cache: MemoryCache,
                 download_icons: bool):
        super(AppsTable, self).__init__()
        self.setParent(parent)
        self.window = parent
        self.download_icons = download_icons
        self.setColumnCount(self.COL_NUMBER)
        self.setFocusPolicy(Qt.NoFocus)
        self.setShowGrid(False)
        self.verticalHeader().setVisible(False)
        self.horizontalHeader().setVisible(False)
        self.horizontalHeader().setSizePolicy(QSizePolicy.Minimum,
                                              QSizePolicy.Preferred)
        self.setSelectionBehavior(QTableView.SelectRows)
        self.setHorizontalHeaderLabels(['' for _ in range(self.columnCount())])
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.icon_logo = QIcon(resource.get_path('img/logo.svg'))
        self.pixmap_verified = QIcon(
            resource.get_path('img/verified.svg')).pixmap(QSize(10, 10))

        self.network_man = QNetworkAccessManager()
        self.network_man.finished.connect(self._load_icon_and_cache)

        self.icon_cache = icon_cache
        self.lock_async_data = Lock()
        self.setRowHeight(80, 80)
        self.cache_type_icon = {}
        self.i18n = self.window.i18n

    def has_any_settings(self, pkg: PackageView):
        return pkg.model.has_history() or \
               pkg.model.can_be_downgraded() or \
               bool(pkg.model.get_custom_supported_actions())

    def show_pkg_actions(self, pkg: PackageView):
        menu_row = QMenu()
        menu_row.setCursor(QCursor(Qt.PointingHandCursor))

        if pkg.model.installed:
            if pkg.model.has_history():
                action_history = QAction(
                    self.i18n["manage_window.apps_table.row.actions.history"])
                action_history.setIcon(
                    QIcon(resource.get_path('img/history.svg')))

                def show_history():
                    self.window.get_app_history(pkg)

                action_history.triggered.connect(show_history)
                menu_row.addAction(action_history)

            if pkg.model.can_be_downgraded():
                action_downgrade = QAction(
                    self.i18n["manage_window.apps_table.row.actions.downgrade"]
                )

                def downgrade():
                    if dialog.ask_confirmation(
                            title=self.i18n[
                                'manage_window.apps_table.row.actions.downgrade'],
                            body=self._parag(self.i18n[
                                'manage_window.apps_table.row.actions.downgrade.popup.body']
                                             .format(self._bold(str(pkg)))),
                            i18n=self.i18n):
                        self.window.downgrade(pkg)

                action_downgrade.triggered.connect(downgrade)
                action_downgrade.setIcon(
                    QIcon(resource.get_path('img/downgrade.svg')))
                menu_row.addAction(action_downgrade)

        if bool(pkg.model.get_custom_supported_actions()):
            for action in pkg.model.get_custom_supported_actions():
                item = QAction(self.i18n[action.i18_label_key])

                if action.icon_path:
                    item.setIcon(QIcon(action.icon_path))

                def custom_action():
                    if dialog.ask_confirmation(
                            title=self.i18n[action.i18_label_key],
                            body=self._parag('{} {} ?'.format(
                                self.i18n[action.i18_label_key],
                                self._bold(str(pkg)))),
                            i18n=self.i18n):
                        self.window.execute_custom_action(pkg, action)

                item.triggered.connect(custom_action)
                menu_row.addAction(item)

        menu_row.adjustSize()
        menu_row.popup(QCursor.pos())
        menu_row.exec_()

    def refresh(self, pkg: PackageView):
        self._update_row(pkg,
                         update_check_enabled=False,
                         change_update_col=False)

    def update_package(self, pkg: PackageView):
        if self.download_icons and pkg.model.icon_url:
            icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
            icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                      True)
            self.network_man.get(icon_request)

        self._update_row(pkg, change_update_col=False)

    def _uninstall_app(self, app_v: PackageView):
        if dialog.ask_confirmation(
                title=self.i18n[
                    'manage_window.apps_table.row.actions.uninstall.popup.title'],
                body=self._parag(self.i18n[
                    'manage_window.apps_table.row.actions.uninstall.popup.body']
                                 .format(self._bold(str(app_v)))),
                i18n=self.i18n):
            self.window.uninstall_app(app_v)

    def _bold(self, text: str) -> str:
        return '<span style="font-weight: bold">{}</span>'.format(text)

    def _parag(self, text: str) -> str:
        return '<p>{}</p>'.format(text)

    def _install_app(self, pkgv: PackageView):

        body = self.i18n[
            'manage_window.apps_table.row.actions.install.popup.body'].format(
                self._bold(str(pkgv)))

        warning = self.i18n.get('gem.{}.install.warning'.format(
            pkgv.model.get_type().lower()))

        if warning:
            body += '<br/><br/> {}'.format('<br/>'.join(
                ('{}.'.format(phrase) for phrase in warning.split('.')
                 if phrase)))

        if dialog.ask_confirmation(title=self.i18n[
                'manage_window.apps_table.row.actions.install.popup.title'],
                                   body=self._parag(body),
                                   i18n=self.i18n):

            self.window.install(pkgv)

    def _load_icon_and_cache(self, http_response: QNetworkReply):
        icon_url = http_response.request().url().toString()

        icon_data = self.icon_cache.get(icon_url)
        icon_was_cached = True

        if not icon_data:
            icon_bytes = http_response.readAll()

            if not icon_bytes:
                return

            icon_was_cached = False
            pixmap = QPixmap()
            pixmap.loadFromData(icon_bytes)

            if not pixmap.isNull():
                icon = QIcon(pixmap)
                icon_data = {'icon': icon, 'bytes': icon_bytes}
                self.icon_cache.add(icon_url, icon_data)

        if icon_data:
            for idx, app in enumerate(self.window.pkgs):
                if app.model.icon_url == icon_url:
                    col_name = self.item(idx, 0)
                    col_name.setIcon(icon_data['icon'])

                    if app.model.supports_disk_cache(
                    ) and app.model.get_disk_icon_path():
                        if not icon_was_cached or not os.path.exists(
                                app.model.get_disk_icon_path()):
                            self.window.manager.cache_to_disk(
                                pkg=app.model,
                                icon_bytes=icon_data['bytes'],
                                only_icon=True)

    def update_packages(self,
                        pkgs: List[PackageView],
                        update_check_enabled: bool = True):
        self.setRowCount(len(pkgs) if pkgs else 0)
        self.setEnabled(True)

        if pkgs:
            for idx, pkg in enumerate(pkgs):
                pkg.table_index = idx

                if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url:
                    icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
                    icon_request.setAttribute(
                        QNetworkRequest.FollowRedirectsAttribute, True)
                    self.network_man.get(icon_request)

                self._update_row(pkg, update_check_enabled)

    def _update_row(self,
                    pkg: PackageView,
                    update_check_enabled: bool = True,
                    change_update_col: bool = True):
        self._set_col_name(0, pkg)
        self._set_col_version(1, pkg)
        self._set_col_description(2, pkg)
        self._set_col_publisher(3, pkg)
        self._set_col_type(4, pkg)
        self._set_col_installed(5, pkg)
        self._set_col_actions(6, pkg)

        if change_update_col:
            col_update = None

            if update_check_enabled and pkg.model.update:
                col_update = QToolBar()
                col_update.setSizePolicy(QSizePolicy.Minimum,
                                         QSizePolicy.Preferred)
                col_update.addWidget(
                    UpdateToggleButton(pkg=pkg,
                                       root=self.window,
                                       i18n=self.i18n,
                                       checked=pkg.update_checked if
                                       pkg.model.can_be_updated() else False,
                                       clickable=pkg.model.can_be_updated()))

            self.setCellWidget(pkg.table_index, 7, col_update)

    def _gen_row_button(self, text: str, style: str, callback) -> QWidget:
        col = QWidget()
        col.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

        col_bt = QToolButton()
        col_bt.setCursor(QCursor(Qt.PointingHandCursor))
        col_bt.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        col_bt.setText(text)
        col_bt.setStyleSheet('QToolButton { ' + style + '}')
        col_bt.setMinimumWidth(80)
        col_bt.clicked.connect(callback)

        layout = QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setAlignment(Qt.AlignCenter)

        layout.addWidget(col_bt)

        col.setLayout(layout)
        return col

    def _set_col_installed(self, col: int, pkg: PackageView):
        toolbar = QToolBar()
        toolbar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)

        if pkg.model.installed:
            if pkg.model.can_be_uninstalled():

                def uninstall():
                    self._uninstall_app(pkg)

                style = 'color: {c}; font-size: 10px; font-weight: bold;'.format(
                    c=BROWN)
                item = self._gen_row_button(
                    self.i18n['uninstall'].capitalize(), style, uninstall)
            else:
                item = QLabel()
                item.setPixmap((QPixmap(resource.get_path('img/checked.svg'))))
                item.setAlignment(Qt.AlignCenter)
                item.setToolTip(self.i18n['installed'])
        elif pkg.model.can_be_installed():

            def install():
                self._install_app(pkg)

            style = 'background: {b}; color: white; font-size: 10px; font-weight: bold'.format(
                b=GREEN)
            item = self._gen_row_button(self.i18n['install'].capitalize(),
                                        style, install)
        else:
            item = None

        toolbar.addWidget(item)
        self.setCellWidget(pkg.table_index, col, toolbar)

    def _set_col_type(self, col: int, pkg: PackageView):
        icon_data = self.cache_type_icon.get(pkg.model.get_type())

        if icon_data is None:
            pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(QSize(
                16, 16))
            icon_data = {
                'px': pixmap,
                'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())
            }
            self.cache_type_icon[pkg.model.get_type()] = icon_data

        item = QLabel()
        item.setPixmap(icon_data['px'])
        item.setAlignment(Qt.AlignCenter)

        item.setToolTip(icon_data['tip'])
        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_version(self, col: int, pkg: PackageView):
        label_version = QLabel(
            str(pkg.model.version if pkg.model.version else '?'))
        label_version.setAlignment(Qt.AlignCenter)

        item = QWidget()
        item.setLayout(QHBoxLayout())
        item.layout().addWidget(label_version)

        if pkg.model.version:
            tooltip = self.i18n[
                'version.installed'] if pkg.model.installed else self.i18n[
                    'version']
        else:
            tooltip = self.i18n['version.unknown']

        if pkg.model.update:
            label_version.setStyleSheet(
                "color: {}; font-weight: bold".format(GREEN))
            tooltip = self.i18n['version.installed_outdated']

        if pkg.model.installed and pkg.model.update and pkg.model.version and pkg.model.latest_version and pkg.model.version != pkg.model.latest_version:
            tooltip = '{}. {}: {}'.format(tooltip, self.i18n['version.latest'],
                                          pkg.model.latest_version)
            label_version.setText(label_version.text() +
                                  '  >  {}'.format(pkg.model.latest_version))

        item.setToolTip(tooltip)
        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_name(self, col: int, pkg: PackageView):
        item = QTableWidgetItem()
        item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        name = pkg.model.get_display_name()
        if name:
            item.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(),
                                            pkg.model.get_name_tooltip()))
        else:
            name = '...'
            item.setToolTip(self.i18n['app.name'].lower())

        if len(name) > NAME_MAX_SIZE:
            name = name[0:NAME_MAX_SIZE - 3] + '...'

        if len(name) < NAME_MAX_SIZE:
            name = name + ' ' * (NAME_MAX_SIZE - len(name))

        item.setText(name)

        icon_path = pkg.model.get_disk_icon_path()
        if pkg.model.supports_disk_cache() and icon_path and os.path.isfile(
                icon_path):
            with open(icon_path, 'rb') as f:
                icon_bytes = f.read()
                pixmap = QPixmap()
                pixmap.loadFromData(icon_bytes)
                icon = QIcon(pixmap)
                self.icon_cache.add_non_existing(pkg.model.icon_url, {
                    'icon': icon,
                    'bytes': icon_bytes
                })

        elif not pkg.model.icon_url:
            icon = QIcon(pkg.model.get_default_icon_path())
        else:
            icon_data = self.icon_cache.get(pkg.model.icon_url)
            icon = icon_data['icon'] if icon_data else QIcon(
                pkg.model.get_default_icon_path())

        item.setIcon(icon)
        self.setItem(pkg.table_index, col, item)

    def _set_col_description(self, col: int, pkg: PackageView):
        item = QLabel()

        if pkg.model.description is not None or not pkg.model.is_application(
        ) or pkg.model.status == PackageStatus.READY:
            desc = pkg.model.description.split(
                '\n')[0] if pkg.model.description else pkg.model.description
        else:
            desc = '...'

        if desc and desc != '...' and len(desc) > DESC_MAX_SIZE:
            desc = strip_html(desc[0:DESC_MAX_SIZE - 1]) + '...'

        item.setText(desc)

        if pkg.model.description:
            item.setToolTip(pkg.model.description)

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_publisher(self, col: int, pkg: PackageView):
        item = QToolBar()

        publisher = pkg.model.get_publisher()
        full_publisher = None

        if publisher:
            publisher = publisher.strip()
            full_publisher = publisher

            if len(publisher) > PUBLISHER_MAX_SIZE:
                publisher = full_publisher[0:PUBLISHER_MAX_SIZE - 3] + '...'

        if not publisher:
            if not pkg.model.installed:
                item.setStyleSheet('QLabel { color: red; }')

            publisher = self.i18n['unknown']

        lb_name = QLabel('  {}'.format(publisher))
        item.addWidget(lb_name)

        if publisher and full_publisher:
            lb_name.setToolTip(self.i18n['publisher'].capitalize() + (
                (': ' + full_publisher) if full_publisher else ''))

            if pkg.model.is_trustable():
                lb_verified = QLabel()
                lb_verified.setPixmap(self.pixmap_verified)
                lb_verified.setToolTip(
                    self.i18n['publisher.verified'].capitalize())
                item.addWidget(lb_verified)
            else:
                lb_name.setText(lb_name.text() + "   ")

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_actions(self, col: int, pkg: PackageView):
        item = QToolBar()
        item.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)

        if pkg.model.installed:

            def run():
                self.window.run_app(pkg)

            bt = IconButton(QIcon(resource.get_path('img/app_play.svg')),
                            i18n=self.i18n,
                            action=run,
                            tooltip=self.i18n['action.run.tooltip'])
            bt.setEnabled(pkg.model.can_be_run())
            item.addWidget(bt)

        def handle_click():
            self.show_pkg_actions(pkg)

        settings = self.has_any_settings(pkg)
        if pkg.model.installed:
            bt = IconButton(QIcon(resource.get_path('img/app_actions.svg')),
                            i18n=self.i18n,
                            action=handle_click,
                            tooltip=self.i18n['action.settings.tooltip'])
            bt.setEnabled(bool(settings))
            item.addWidget(bt)

        if not pkg.model.installed:

            def get_screenshots():
                self.window.get_screenshots(pkg)

            bt = IconButton(QIcon(resource.get_path('img/camera.svg')),
                            i18n=self.i18n,
                            action=get_screenshots,
                            tooltip=self.i18n['action.screenshots.tooltip'])
            bt.setEnabled(bool(pkg.model.has_screenshots()))
            item.addWidget(bt)

        def get_info():
            self.window.get_app_info(pkg)

        bt = IconButton(QIcon(resource.get_path('img/app_info.svg')),
                        i18n=self.i18n,
                        action=get_info,
                        tooltip=self.i18n['action.info.tooltip'])
        bt.setEnabled(bool(pkg.model.has_info()))
        item.addWidget(bt)

        self.setCellWidget(pkg.table_index, col, item)

    def change_headers_policy(
            self,
            policy: QHeaderView = QHeaderView.ResizeToContents,
            maximized: bool = False):
        header_horizontal = self.horizontalHeader()
        for i in range(self.columnCount()):
            if maximized:
                if i in (1, 2):
                    header_horizontal.setSectionResizeMode(
                        i, QHeaderView.ResizeToContents)
                else:
                    header_horizontal.setSectionResizeMode(
                        i, QHeaderView.Stretch)
            else:
                header_horizontal.setSectionResizeMode(i, policy)

    def get_width(self):
        return reduce(operator.add,
                      [self.columnWidth(i) for i in range(self.columnCount())])
예제 #9
0
파일: NetworkCamera.py 프로젝트: CPS-3/Cura
class NetworkCamera(QObject):
    newImage = pyqtSignal()

    def __init__(self, target = None, parent = None):
        super().__init__(parent)
        self._stream_buffer = b""
        self._stream_buffer_start_index = -1
        self._manager = None
        self._image_request = None
        self._image_reply = None
        self._image = QImage()
        self._image_id = 0

        self._target = target
        self._started = False

    @pyqtSlot(str)
    def setTarget(self, target):
        restart_required = False
        if self._started:
            self.stop()
            restart_required = True

        self._target = target

        if restart_required:
            self.start()

    @pyqtProperty(QUrl, notify=newImage)
    def latestImage(self):
        self._image_id += 1
        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://camera/" + str(self._image_id)

        return QUrl(temp, QUrl.TolerantMode)

    @pyqtSlot()
    def start(self):
        # Ensure that previous requests (if any) are stopped.
        self.stop()
        if self._target is None:
            Logger.log("w", "Unable to start camera stream without target!")
            return
        self._started = True
        url = QUrl(self._target)
        self._image_request = QNetworkRequest(url)
        if self._manager is None:
            self._manager = QNetworkAccessManager()

        self._image_reply = self._manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)

    @pyqtSlot()
    def stop(self):
        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        if self._image_reply:
            try:
                # disconnect the signal
                try:
                    self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
                except Exception:
                    pass
                # abort the request if it's not finished
                if not self._image_reply.isFinished():
                    self._image_reply.close()
            except Exception as e:  # RuntimeError
                pass  # It can happen that the wrapped c++ object is already deleted.

            self._image_reply = None
            self._image_request = None

        self._manager = None

        self._started = False

    def getImage(self):
        return self._image

    ##  Ensure that close gets called when object is destroyed
    def __del__(self):
        self.stop()

    def _onStreamDownloadProgress(self, bytes_received, bytes_total):
        # An MJPG stream is (for our purpose) a stream of concatenated JPG images.
        # JPG images start with the marker 0xFFD8, and end with 0xFFD9
        if self._image_reply is None:
            return
        self._stream_buffer += self._image_reply.readAll()

        if len(self._stream_buffer) > 2000000:  # No single camera frame should be 2 Mb or larger
            Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
            self.stop()  # resets stream buffer and start index
            self.start()
            return

        if self._stream_buffer_start_index == -1:
            self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
        stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
        # If this happens to be more than a single frame, then so be it; the JPG decoder will
        # ignore the extra data. We do it like this in order not to get a buildup of frames

        if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
            jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
            self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
            self._stream_buffer_start_index = -1
            self._image.loadFromData(jpg_data)

        self.newImage.emit()
예제 #10
0
파일: daemon.py 프로젝트: hydrargyrum/eye
class Ycm(QObject, CategoryMixin):
	"""YCMD instance control"""

	YCMD_CMD = ['ycmd']
	"""Base ycmd command.

	Useful if ycmd is not in `PATH` or set permanent arguments
	"""

	IDLE_SUICIDE = 120
	"""Maximum time after which ycmd should quit if it has received no requests.

	A periodic ping is sent by `Ycm` objects.
	"""

	CHECK_REPLY_SIGNATURE = True
	TIMEOUT = 10

	def __init__(self, **kwargs):
		super(Ycm, self).__init__(**kwargs)

		self.addr = None
		"""Address of the ycmd server."""

		self.port = 0
		"""TCP port of the ycmd server."""

		self._ready = False
		self.secret = ''
		self.config = {}

		self.proc = QProcess()
		self.proc.started.connect(self.procStarted)
		self.proc.errorOccurred.connect(self.procError)
		self.proc.finished.connect(self.procFinished)

		self.pingTimer = QTimer(self)
		self.pingTimer.timeout.connect(self.ping)

		self.network = QNetworkAccessManager()

		qApp().aboutToQuit.connect(self.stop)

		self.addCategory('ycm_control')

	def makeConfig(self):
		self.secret = generate_key()
		self.config['hmac_secret'] = b64encode(self.secret).decode('ascii')

		fd, path = tempfile.mkstemp()
		with open(path, 'w') as fd:
			fd.write(json.dumps(self.config))
			fd.flush()
		return path

	def checkReply(self, reply):
		"""Check the ycmd reply is a success.

		Checks the `reply` has a HTTP 200 status code and the signature is valid.
		In case of error, raises a :any:`ServerError`.

		:type reply: QNetworkReply
		"""
		reply.content = bytes(reply.readAll())

		if reply.error():
			raise ServerError(reply.error() + 1000, reply.errorString(), reply.content)
		status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
		if status_code != 200:
			data = reply.content.decode('utf-8')
			try:
				data = json.loads(data)
			except (ValueError, JSONDecodeError):
				LOGGER.info('ycmd replied non-json body: %r', data)

			raise ServerError(status_code, data)

		if not self.CHECK_REPLY_SIGNATURE:
			return
		actual = b64decode(bytes(reply.rawHeader(HMAC_HEADER)))
		expected = self._hmacDigest(reply.content)

		if not hmac.compare_digest(expected, actual):
			raise RuntimeError('Server signature did not match')

	def _jsonReply(self, reply):
		body = reply.content.decode('utf-8')
		return json.loads(body)

	def _hmacDigest(self, msg):
		return hmac.new(self.secret, msg, hashlib.sha256).digest()

	def _sign(self, verb, path, body=b''):
		digests = [self._hmacDigest(part) for part in [verb, path, body]]
		return self._hmacDigest(b''.join(digests))

	def _doGet(self, path):
		url = urlunsplit(('http', self.addr, path, '', ''))
		sig = self._sign(b'GET', path.encode('utf-8'), b'')
		headers = {
			HMAC_HEADER: b64encode(sig)
		}

		request = QNetworkRequest(QUrl(url))
		for hname in headers:
			request.setRawHeader(hname, headers[hname])

		reply = self.network.get(request)
		return reply

	def _doPost(self, path, **params):
		url = urlunsplit(('http', self.addr, path, '', ''))
		body = json.dumps(params)
		sig = self._sign(b'POST', path.encode('utf-8'), body.encode('utf-8'))
		headers = {
			HMAC_HEADER: b64encode(sig),
			'Content-Type': 'application/json'
		}

		request = QNetworkRequest(QUrl(url))
		for hname in headers:
			request.setRawHeader(hname, headers[hname])
		reply = self.network.post(request, body)
		return reply

	def ping(self):
		def handleReply():
			self.checkReply(reply)
			if not self._ready:
				self._ready = True
				self.pingTimer.start(60000)
				self.ready.emit()

		reply = self._doGet('/healthy')
		reply.finished.connect(handleReply)
		reply.finished.connect(reply.deleteLater)

	def start(self):
		if not self.port:
			self.port = generate_port()
		self.addr = 'localhost:%s' % self.port
		path = self.makeConfig()

		_, outlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.out.log')
		_, errlogpath = tempfile.mkstemp(prefix='eye-ycm', suffix='.err.log')
		LOGGER.info('ycmd will log to %r and %r', outlogpath, errlogpath)

		cmd = (self.YCMD_CMD +
			[
				'--idle_suicide_seconds', str(self.IDLE_SUICIDE),
				'--port', str(self.port),
				'--options_file', path,
				'--stdout', outlogpath,
				'--stderr', errlogpath,
			])

		LOGGER.debug('will run %r', cmd)

		self.proc.start(cmd[0], cmd[1:])

		self._ready = False

	@Slot()
	def stop(self, wait=0.2):
		if self.proc.state() == QProcess.NotRunning:
			return
		self.proc.terminate()
		if self.proc.state() == QProcess.NotRunning:
			return
		time.sleep(wait)
		self.proc.kill()

	def isRunning(self):
		return self.proc.state() == QProcess.Running

	def connectTo(self, addr):
		self.addr = addr
		self._ready = False
		self.pingTimer.start(1000)

	@Slot()
	def procStarted(self):
		LOGGER.debug('daemon has started')
		self.pingTimer.start(1000)

	@Slot(int, QProcess.ExitStatus)
	def procFinished(self, code, status):
		LOGGER.info('daemon has exited with status %r and code %r', status, code)
		self.pingTimer.stop()
		self._ready = False

	@Slot(QProcess.ProcessError)
	def procError(self, error):
		LOGGER.warning('daemon failed to start (%r): %s', error, self.errorString())

	ready = Signal()

	def _commonPostDict(self, filepath, filetype, contents):
		d = {
			'filepath': filepath,
			'filetype': filetype,
			'file_data': {
				filepath: {
					'filetypes': [filetype],
					'contents': contents
				}
			},
			'line_num': 1, # useless but required
			'column_num': 1,
		}
		return d

	def _postSimpleRequest(self, urlpath, filepath, filetype, contents, **kwargs):
		d = self._commonPostDict(filepath, filetype, contents)
		d.update(**kwargs)

		return self._doPost(urlpath, **d)

	def acceptExtraConf(self, filepath, filetype, contents):
		reply = self._postSimpleRequest('/load_extra_conf_file', filepath, filetype, contents)
		reply.finished.connect(reply.deleteLater)

	def rejectExtraConf(self, filepath, filetype, contents):
		reply = self._postSimpleRequest('/ignore_extra_conf_file', filepath, filetype, contents,
		                                _ignore_body=True)
		reply.finished.connect(reply.deleteLater)

	def sendParse(self, filepath, filetype, contents, retry_extra=True):
		d = {
			'event_name': 'FileReadyToParse'
		}
		reply = self._postSimpleRequest('/event_notification', filepath, filetype, contents, **d)

		def handleReply():
			try:
				self.checkReply(reply)
			except ServerError as exc:
				excdata = exc.args[1]
				if (isinstance(excdata, dict) and 'exception' in excdata and
					excdata['exception']['TYPE'] == 'UnknownExtraConf' and
					retry_extra):
					confpath = excdata['exception']['extra_conf_file']
					LOGGER.info('ycmd encountered %r and wonders if it should be loaded', confpath)

					accepted = sendIntent(None, 'queryExtraConf', conf=confpath)
					if accepted:
						LOGGER.info('extra conf %r will be loaded', confpath)
						self.acceptExtraConf(confpath, filetype, contents)
					else:
						LOGGER.info('extra conf %r will be rejected', confpath)
						self.rejectExtraConf(confpath, filetype, contents)

					return self.sendParse(filepath, filetype, contents, retry_extra=False)
				raise

		reply.finished.connect(handleReply)
		reply.finished.connect(reply.deleteLater)

	if 0:
		def querySubcommandsList(self, filepath, filetype, contents, line, col):
			return self._postSimpleRequest('/defined_subcommands', filepath, filetype, contents)

		def _querySubcommand(self, filepath, filetype, contents, line, col, *args):
			d = {
				'command_arguments': list(args)
			}
			return self._postSimpleRequest('/run_completer_command', filepath, filetype, contents, **d)

		def queryGoTo(self, *args):
			res = self._querySubcommand(*args)
			if res.get('filepath'):
				return {
					'filepath': res['filepath'],
					'line': res['line_num'],
					'column': res['column_num'],
				}

		def queryInfo(self, *args):
			res = self._querySubcommand(*args)
			return res.get('message', '') or res.get('detailed_info', '')

	def queryCompletions(self, filepath, filetype, contents, line, col):
		d = {
			'line_num': line,
			'column_num': col,
		}
		return self._postSimpleRequest('/completions', filepath, filetype, contents, **d)

	if 0:
		def queryDiagnostic(self, filepath, filetype, contents, line, col):
			return self._postSimpleRequest('/detailed_diagnostic', filepath, filetype, contents)

		def queryDebug(self, filepath, filetype, contents, line, col):
			return self._postSimpleRequest('/debug_info', filepath, filetype, contents)
예제 #11
0
class FLNetwork(QtCore.QObject):

    url = None
    request = None
    manager = None

    reply = None

    finished = QtCore.pyqtSignal()
    start = QtCore.pyqtSignal()
    data = QtCore.pyqtSignal(str)
    dataTransferProgress = QtCore.pyqtSignal(int, int)

    def __init__(self, url):
        super(FLNetwork, self).__init__()
        self.url = url
        from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
        self.request = QNetworkRequest()

        self.manager = QNetworkAccessManager()
        # self.manager.readyRead.connect(self._slotNetworkStart)
        self.manager.finished['QNetworkReply*'].connect(self._slotNetworkFinished)
        # self.data.connect(self._slotNetWorkData)
        # self.dataTransferProgress.connect(self._slotNetworkProgress)

    @decorators.BetaImplementation
    def get(self, location):
        self.request.setUrl(QtCore.QUrl("%s%s" % (self.url, location)))
        self.reply = self.manager.get(self.request)
        try:
            self.reply.uploadProgress.disconnect(self._slotNetworkProgress)
            self.reply.downloadProgress.disconnect(self._slotNetworkProgress)
        except:
            pass

        self.reply.downloadProgress.connect(self._slotNetworkProgress)

    @decorators.BetaImplementation
    def put(self, data, location):
        self.request.setUrl(QtCore.QUrl("%s%s" % (self.url, localtion)))
        self.reply = self.manager.put(data, self.request)
        try:
            self.reply.uploadProgress.disconnect(self._slotNetworkProgress)
            self.reply.downloadProgress.disconnect(self._slotNetworkProgress)
        except:
            pass
        self.uploadProgress.connect(self.slotNetworkProgress)

    @decorators.BetaImplementation
    def copy(self, fromLocation, toLocation):
        self.request.setUrl("%s%s" % (self.url, fromLocaltion))
        data = self.manager.get(self.request)
        self.put(data.readAll(), toLocation)

    @QtCore.pyqtSlot()
    def _slotNetworkStart(self):
        self.start.emit()

    @QtCore.pyqtSlot()
    def _slotNetworkFinished(self, reply=None):
        self.finished.emit()

    #@QtCore.pyqtSlot(QtCore.QByteArray)
    # def _slotNetWorkData(self, b):
    #    buffer = b
    #    self.data.emit(b)

    def _slotNetworkProgress(self, bDone, bTotal):
        self.dataTransferProgress.emit(bDone, bTotal)
        data_ = None
        reply_ = self.reply.readAll().data()
        try:
            data_ = str(reply_, encoding="iso-8859-15")
        except:
            data_ = str(reply_, encoding="utf-8")

        self.data.emit(data_)
예제 #12
0
파일: qpdfview.py 프로젝트: akkana/scripts
class PDFWidget(QLabel):

    '''
    A widget showing one page of a PDF.
    If you want to show multiple pages of the same PDF,
    make sure you share the document (let the first PDFWidget
    create the document, then pass thatPDFwidget.document to any
    subsequent widgets you create) or use a ScrolledPDFWidget.
    '''

    def __init__(self, url, document=None, pageno=1, dpi=72, parent=None,
                 load_cb=None):
        '''
           load_cb: will be called when the document is loaded.
        '''
        super(PDFWidget, self).__init__(parent)

        self.filename = url

        self.load_cb = load_cb

        self.network_manager = None

        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        if not document:
            self.document = None
            if url:
                self.start_load(url)
        else:
            self.document = document
        self.page = None
        self.pagesize = QSize(600, 800)

        self.dpi = dpi

        # Poppler page numbering starts from 0 but that's not what
        # most PDF users will expect, so subtract:
        if pageno > 0:
            pageno -= 1
        self.pageno = pageno

        self.render()

    def sizeHint(self):
        if not self.page:
            if not self.document:
                return QSize(600, 800)
            self.page = self.document.page(self.pageno)

        if not self.pagesize:
            self.pagesize = self.page.pageSize()

        return self.pagesize

    def render(self):
        '''Render to a pixmap at the current DPI setting.
        '''
        if not self.document:
            return

        if not self.page:
            self.page = self.document.page(self.pageno)
            self.pagesize = self.page.pageSize()

            self.document.setRenderHint(Poppler.Document.TextAntialiasing)
            # self.document.setRenderHint(Poppler.Document.TextHinting)

        # Most Qt5 programs seem to use setGeometry(x, y, w, h)
        # to set initial window size. resize() is the only method I've
        # found that doesn't force initial position as well as size.
        # self.resize(self.pagesize.width() * self.dpi/POINTS_PER_INCH,
        #             self.pagesize.height() * self.dpi/POINTS_PER_INCH)

        self.setWindowTitle('PDF Viewer')

        img = self.page.renderToImage(self.dpi, self.dpi)
        self.pixmap = QPixmap.fromImage(img)
        self.setPixmap(self.pixmap)

    def start_load(self, url):
        '''Create a Poppler.Document from the given URL, QUrl or filename.
           Return, then asynchronously call self.load_cb.
        '''

        # If it's not a local file, we'll need to load it.
        # http://doc.qt.io/qt-5/qnetworkaccessmanager.html
        qurl = QUrl(url)
        if not qurl.scheme():
            qurl = QUrl.fromLocalFile(url)
        if not self.network_manager:
            self.network_manager = QNetworkAccessManager();
        self.network_manager.finished.connect(self.download_finished)
        self.network_manager.get(QNetworkRequest(qurl))

    def download_finished(self, network_reply):
        qbytes = network_reply.readAll()
        self.document = Poppler.Document.loadFromData(qbytes)

        self.render()

        if self.load_cb:
            self.load_cb()
예제 #13
0
class MostWatchedItems(QWidget):
    def __init__(self, parent=None):
        super(MostWatchedItems, self).__init__(parent)
        
        self.ui = Ui_MostWatchedItems()
        self.ui.setupUi(self)
        self.ui.tableWidget.setColumnCount(5)
        self.ui.tableWidget.setColumnWidth(0, 160)
        self.ui.tableWidget.setColumnWidth(1, 480)
        
        self.categoryID = ''
        
        self.manager = QNetworkAccessManager(self)
        diskCache = QNetworkDiskCache(self)
        diskCache.setCacheDirectory("cache")
        self.manager.setCache(diskCache)
        self.manager.finished.connect(self.on_finished)
        self.manager.sslErrors.connect(self.on_sslErrors)
        
        self.replyMap = dict()
        
    @pyqtSlot(bool)
    def on_selectPushButton_clicked(self, checked):
        dialog = CategoryInfoDialog.instance()
        res = dialog.exec()
        if res == QDialog.Accepted:
            self.ui.categoryLineEdit.setText('%s -> %s' % (dialog.categoryID(), dialog.categoryNamePath()))
            self.categoryID = dialog.categoryID()
    
    @pyqtSlot(bool)    
    def on_clearPushButton_clicked(self, checked):
        self.ui.categoryLineEdit.setText('')
        self.categoryID = ''
        
    @pyqtSlot(bool)    
    def on_wipePushButton_clicked(self, checked):
        while self.ui.tableWidget.rowCount() > 0:
            self.ui.tableWidget.removeRow(0)
        
    @pyqtSlot(bool)    
    def on_searchPushButton_clicked(self, checked):
        if self.categoryID != '' and self.categoryID != '-1':
            merchandising = Merchandising(warnings = False)
            response = merchandising.execute(
                'getMostWatchedItems',
                {
                    'categoryId': self.categoryID,
                    'maxResults': self.ui.spinBox.value()
                }
            )
            reply = response.reply
            itemRecommendations = reply.itemRecommendations.item
            row = self.ui.tableWidget.rowCount()
            for item in itemRecommendations:
                self.ui.tableWidget.insertRow(row)
                imageUrl = 'http://thumbs3.ebaystatic.com/pict/%s4040.jpg' % item.itemId
                request = QNetworkRequest(QUrl(imageUrl))
                request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.PreferCache)
                reply = self.manager.get(request)
                self.replyMap[reply] = row
                viewItemURL = QLabel()
                viewItemURL.setOpenExternalLinks(True)
                #viewItemURL.setTextInteractionFlags(Qt.TextBrowserInteraction)
                title = '<a href="%s">%s</a>' % (item.viewItemURL, item.title)
                viewItemURL.setText(title)
                self.ui.tableWidget.setCellWidget(row, 1, viewItemURL)
                self.ui.tableWidget.setItem(row, 2, QTableWidgetItem(item.primaryCategoryName))
                self.ui.tableWidget.setItem(row, 3, QTableWidgetItem(item.buyItNowPrice.value))
                self.ui.tableWidget.setItem(row, 4, QTableWidgetItem(item.watchCount))
                row += 1
                
    @pyqtSlot(QNetworkReply)
    def on_finished(self, reply):
        if reply in self.replyMap:
            row = self.replyMap.get(reply)
            del self.replyMap[reply]
            pixmap = QPixmap()
            pixmap.loadFromData(reply.readAll())
            image = QLabel()
            image.setPixmap(pixmap)
            self.ui.tableWidget.setCellWidget(row, 0, image)
            self.ui.tableWidget.setRowHeight(row, 160)
    
    @pyqtSlot(QNetworkReply, list)
    def on_sslErrors(self, reply, errors):
        if reply in self.replyMap:
            del self.replyMap[reply]
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, properties):
        super().__init__(key)
        self._address = address
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None

        ##  Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint.
        self._num_extruders = 1

        self._hotend_temperatures = [0] * self._num_extruders
        self._target_hotend_temperatures = [0] * self._num_extruders

        self._api_version = "1"
        self._api_prefix = "/api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        self.setName(key)
        self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._progress_message = None
        self._error_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_timer = QTimer()
        self._camera_timer.setInterval(500)  # Todo: Add preference for camera update interval
        self._camera_timer.setSingleShot(False)
        self._camera_timer.timeout.connect(self._update_camera)

        self._camera_image_id = 0

        self._camera_image = QImage()

    def getProperties(self):
        return self._properties

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result = str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the printer (as returned from the zeroConf properties)
    @pyqtProperty(str, constant = True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this printer
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    def _update_camera(self):
        ## Request new image
        url = QUrl("http://" + self._address + ":8080/?action=snapshot")
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)

    def _update(self):
        ## Request 'general' printer data
        url = QUrl("http://" + self._address + self._api_prefix + "printer")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def close(self):
        self.setConnectionState(ConnectionState.closed)
        self._update_timer.stop()
        self._camera_timer.stop()

    def requestWrite(self, node, file_name = None, filter_by_machine = False):
        self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from printer
    def connect(self):
        self.setConnectionState(ConnectionState.connecting)
        self._update()  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        self._update_camera()
        Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address)
        self._update_timer.start()
        self._camera_timer.start()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify = newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._job_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"
        data = "{\"command\": \"%s\"}" % command
        self._job_reply = self._manager.post(self._job_request, data.encode())
        Logger.log("d", "Sent command to OctoPrint instance: %s", data)

    def startPrint(self):
        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            for line in self._gcode:
                single_string_file_data += line

            ##  TODO: Use correct file name (we use placeholder now)
            file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            url = QUrl("http://" + self._address + self._api_prefix + "files/local")

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request, self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to printer. Is another job still active?"))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log("e", "An exception occurred in network connection: %s" % str(e))

    ##  Handler for all requests that have finished.
    def _onFinished(self, reply):
        http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "printer" in reply.url().toString():  # Status update from /printer.
                if http_status_code == 200:
                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    # Check for hotend temperatures
                    for index in range(0, self._num_extruders):
                        temperature = json_data["temperature"]["tool%d" % index]["actual"]
                        self._setHotendTemperature(index, temperature)

                    bed_temperature = json_data["temperature"]["bed"]["actual"]
                    self._setBedTemperature(bed_temperature)

                    printer_state = "offline"
                    if json_data["state"]["flags"]["error"]:
                        printer_state = "error"
                    elif json_data["state"]["flags"]["paused"]:
                        printer_state = "paused"
                    elif json_data["state"]["flags"]["printing"]:
                        printer_state = "printing"
                    elif json_data["state"]["flags"]["ready"]:
                        printer_state = "ready"
                    self._updateJobState(printer_state)
                else:
                    pass  # TODO: Handle errors

            elif "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(json_data["job"]["estimatedPrintTime"])
                        elif progress > 0:
                            self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

            elif "snapshot" in reply.url().toString():  # Update from camera:
                if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
                    self._camera_image.loadFromData(reply.readAll())
                    self.newImage.emit()
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "files" in reply.url().toString():  # Result from /files command:
                if http_status_code == 201:
                    Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()

            elif "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation())

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            self._progress_message.setProgress(bytes_sent / bytes_total * 100)
        else:
            self._progress_message.setProgress(0)
예제 #15
0
class downloadManager(QObject):
    manager = None
    currentDownload = None
    reply = None
    url = None
    result = None
    filename = None
    dir_ = None
    url_ = None

    def __init__(self):
        super(downloadManager, self).__init__()
        self.manager = QNetworkAccessManager()
        self.currentDownload = []
        self.manager.finished.connect(self.downloadFinished)

    def setLE(self, filename, dir_, urllineedit):
        self.filename = filename
        self.dir_ = dir_
        self.url_ = urllineedit

    def doDownload(self):
        request = QNetworkRequest(
            QUrl("%s/%s/%s" % (self.url_.text(), self.dir_, self.filename)))
        self.reply = self.manager.get(request)
        # self.reply.sslErrors.connect(self.sslErrors)
        self.currentDownload.append(self.reply)

    def saveFileName(self, url):
        path = url.path()
        basename = QFileInfo(path).fileName()

        if not basename:
            basename = "download"

        if QFile.exists(basename):
            i = 0
            basename = basename + "."
            while QFile.exists("%s%s" % (basename, i)):
                i = i + 1

            basename = "%s%s" % (basename, i)

        return basename

    def saveToDisk(self, filename, data):
        fi = "%s/%s" % (self.dir_, filename)
        if not os.path.exists(self.dir_):
            os.makedirs(self.dir_)
        file = QFile(fi)
        if not file.open(QIODevice.WriteOnly):
            return False

        file.write(data.readAll())
        file.close()

        return True

    def isHttpRedirect(self, reply):
        statusCode = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        return statusCode in [301, 302, 303, 305, 307, 308]

    @QtCore.pyqtSlot(QNetworkReply)
    def downloadFinished(self, reply):
        url = reply.url()
        if not reply.error():
            if not self.isHttpRedirect(reply):
                filename = self.saveFileName(url)
                filename = filename.replace(":", "")
                self.saveToDisk(filename, reply)
                self.result = "%s ---> %s/%s" % (url, self.dir_, filename)
            else:
                self.result = "Redireccionado ... :("
        else:
            self.result = reply.errorString()
class DiscoverOctoPrintAction(MachineAction):
    def __init__(self, parent: QObject = None) -> None:
        super().__init__("DiscoverOctoPrintAction", catalog.i18nc("@action", "Connect OctoPrint"))

        self._qml_url = "DiscoverOctoPrintAction.qml"

        self._application = CuraApplication.getInstance()
        self._network_plugin = None # type: Optional[OctoPrintOutputDevicePlugin]

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)

        self._settings_reply = None # type: Optional[QNetworkReply]

        self._appkeys_supported = False
        self._appkey_reply = None # type: Optional[QNetworkReply]
        self._appkey_request = None # type: Optional[QNetworkRequest]
        self._appkey_instance_id = ""

        self._appkey_poll_timer = QTimer()
        self._appkey_poll_timer.setInterval(500)
        self._appkey_poll_timer.setSingleShot(True)
        self._appkey_poll_timer.timeout.connect(self._pollApiKey)

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                self._plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            self._plugin_version = "0.0"
            Logger.logException("w", "Could not get version information for the plugin")

        self._user_agent = ("%s/%s %s/%s" % (
            self._application.getApplicationName(),
            self._application.getVersion(),
            "OctoPrintPlugin",
            self._plugin_version
        )).encode()

        self._instance_responded = False
        self._instance_in_error = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False

        # Load keys cache from preferences
        self._preferences = self._application.getPreferences()
        self._preferences.addPreference("octoprint/keys_cache", "")

        try:
            self._keys_cache = json.loads(self._deobfuscateString(self._preferences.getValue("octoprint/keys_cache")))
        except ValueError:
            self._keys_cache = {} # type: Dict[str, Any]
        if not isinstance(self._keys_cache, dict):
            self._keys_cache = {} # type: Dict[str, Any]

        self._additional_components = None # type:Optional[QObject]

        ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
        self._application.engineCreatedSignal.connect(self._createAdditionalComponentsView)

    @pyqtProperty(str, constant=True)
    def pluginVersion(self) -> str:
        return self._plugin_version

    @pyqtSlot()
    def startDiscovery(self) -> None:
        if not self._network_plugin:
            self._network_plugin = self._application.getOutputDeviceManager().getOutputDevicePlugin(self._plugin_id)
            if not self._network_plugin:
                return
            self._network_plugin.addInstanceSignal.connect(self._onInstanceDiscovery)
            self._network_plugin.removeInstanceSignal.connect(self._onInstanceDiscovery)
            self._network_plugin.instanceListChanged.connect(self._onInstanceDiscovery)
            self.instancesChanged.emit()
        else:
            # Restart bonjour discovery
            self._network_plugin.startDiscovery()

    def _onInstanceDiscovery(self, *args) -> None:
        self.instancesChanged.emit()

    @pyqtSlot(str)
    def removeManualInstance(self, name: str) -> None:
        if not self._network_plugin:
            return

        self._network_plugin.removeManualInstance(name)

    @pyqtSlot(str, str, int, str, bool, str, str)
    def setManualInstance(self, name: str, address: str, port: int, path: str, useHttps: bool, userName: str = "", password: str = "") -> None:
        if not self._network_plugin:
            return

        # This manual printer could replace a current manual printer
        self._network_plugin.removeManualInstance(name)

        self._network_plugin.addManualInstance(name, address, port, path, useHttps, userName, password)

    def _onContainerAdded(self, container: "ContainerInterface") -> None:
        # Add this action as a supported action to all machine definitions
        if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
            self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())

    instancesChanged = pyqtSignal()
    appKeysSupportedChanged = pyqtSignal()
    appKeyReceived = pyqtSignal()

    @pyqtProperty("QVariantList", notify = instancesChanged)
    def discoveredInstances(self) -> List[Any]:
        if self._network_plugin:
            instances = list(self._network_plugin.getInstances().values())
            instances.sort(key = lambda k: k.name)
            return instances
        else:
            return []

    @pyqtSlot(str)
    def setInstanceId(self, key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if global_container_stack:
            global_container_stack.setMetaDataEntry("octoprint_id", key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    @pyqtSlot(result = str)
    def getInstanceId(self) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        return global_container_stack.getMetaDataEntry("octoprint_id", "")

    @pyqtSlot(str, str, str, str)
    def requestApiKey(self, instance_id: str, base_url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None:
        ## Request appkey
        self._appkey_instance_id = instance_id
        self._appkey_request = self._createRequest(QUrl(base_url + "plugin/appkeys/request"), basic_auth_username, basic_auth_password)
        self._appkey_request.setRawHeader(b"Content-Type", b"application/json")
        data = json.dumps({"app": "Cura"})
        self._appkey_reply = self._network_manager.post(self._appkey_request, data.encode())

    @pyqtSlot()
    def cancelApiKeyRequest(self) -> None:
        if self._appkey_reply:
            self._appkey_reply.abort()
            self._appkey_reply = None

        self._appkey_request = None # type: Optional[QNetworkRequest]

        self._appkey_poll_timer.stop()

    def _pollApiKey(self) -> None:
        if not self._appkey_request:
            return
        self._appkey_reply = self._network_manager.get(self._appkey_request)

    @pyqtSlot(str, str, str)
    def probeAppKeySupport(self, base_url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None:
        self._appkeys_supported = False
        self.appKeysSupportedChanged.emit()

        appkey_probe_request = self._createRequest(QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username, basic_auth_password)
        self._appkey_request = self._network_manager.get(appkey_probe_request)

    @pyqtSlot(str, str, str, str)
    def testApiKey(self, base_url: str, api_key: str, basic_auth_username: str = "", basic_auth_password: str = "") -> None:
        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self.selectedInstanceSettingsChanged.emit()

        if api_key != "":
            Logger.log("d", "Trying to access OctoPrint instance at %s with the provided API key." % base_url)

            ## Request 'settings' dump
            settings_request = self._createRequest(QUrl(base_url + "api/settings"), basic_auth_username, basic_auth_password)
            settings_request.setRawHeader("X-Api-Key".encode(), api_key.encode())
            self._settings_reply = self._network_manager.get(settings_request)
        else:
            if self._settings_reply:
                self._settings_reply.abort()
                self._settings_reply = None

    @pyqtSlot(str)
    def setApiKey(self, api_key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        global_container_stack.setMetaDataEntry("octoprint_api_key", base64.b64encode(api_key.encode("ascii")).decode("ascii"))

        self._keys_cache[self.getInstanceId()] = api_key
        keys_cache = base64.b64encode(json.dumps(self._keys_cache).encode("ascii")).decode("ascii")
        self._preferences.setValue("octoprint/keys_cache", keys_cache)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    ##  Get the stored API key of an instance, or the one stored in the machine instance
    #   \return key String containing the key of the machine.
    @pyqtSlot(str, result=str)
    def getApiKey(self, instance_id: str) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        if instance_id == self.getInstanceId():
            api_key = self._deobfuscateString(global_container_stack.getMetaDataEntry("octoprint_api_key", ""))
        else:
            api_key = self._keys_cache.get(instance_id, "")

        return api_key

    selectedInstanceSettingsChanged = pyqtSignal()

    @pyqtProperty(bool, notify = selectedInstanceSettingsChanged)
    def instanceResponded(self) -> bool:
        return self._instance_responded

    @pyqtProperty(bool, notify = selectedInstanceSettingsChanged)
    def instanceInError(self) -> bool:
        return self._instance_in_error

    @pyqtProperty(bool, notify = selectedInstanceSettingsChanged)
    def instanceApiKeyAccepted(self) -> bool:
        return self._instance_api_key_accepted

    @pyqtProperty(bool, notify = selectedInstanceSettingsChanged)
    def instanceSupportsSd(self) -> bool:
        return self._instance_supports_sd

    @pyqtProperty(bool, notify = selectedInstanceSettingsChanged)
    def instanceSupportsCamera(self) -> bool:
        return self._instance_supports_camera

    @pyqtProperty(bool, notify = appKeysSupportedChanged)
    def instanceSupportsAppKeys(self) -> bool:
        return self._appkeys_supported

    @pyqtSlot(str, str, str)
    def setContainerMetaDataEntry(self, container_id: str, key: str, value: str) -> None:
        containers = ContainerRegistry.getInstance().findContainers(id = container_id)
        if not containers:
            Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
            return

        containers[0].setMetaDataEntry(key, value)

    @pyqtSlot(bool)
    def applyGcodeFlavorFix(self, apply_fix: bool) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode"
        if global_container_stack.getProperty("machine_gcode_flavor", "value") == gcode_flavor:
            # No need to add a definition_changes container if the setting is not going to be changed
            return

        # Make sure there is a definition_changes container to store the machine settings
        definition_changes_container = global_container_stack.definitionChanges
        if definition_changes_container == ContainerRegistry.getInstance().getEmptyInstanceContainer():
            definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
                global_container_stack, global_container_stack.getId() + "_settings")

        definition_changes_container.setProperty("machine_gcode_flavor", "value", gcode_flavor)

        # Update the has_materials metadata flag after switching gcode flavor
        definition = global_container_stack.getBottom()
        if not definition or definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False):
            # In other words: only continue for the UM2 (extended), but not for the UM2+
            return

        has_materials = global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"

        material_container = global_container_stack.material

        if has_materials:
            global_container_stack.setMetaDataEntry("has_materials", True)

            # Set the material container to a sane default
            if material_container == ContainerRegistry.getInstance().getEmptyInstanceContainer():
                search_criteria = { "type": "material", "definition": "fdmprinter", "id": global_container_stack.getMetaDataEntry("preferred_material")}
                materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
                if materials:
                    global_container_stack.material = materials[0]
        else:
            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.removeMetaDataEntry("has_materials")

            global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()

        self._application.globalContainerStackChanged.emit()


    @pyqtSlot(str)
    def openWebPage(self, url: str) -> None:
        QDesktopServices.openUrl(QUrl(url))

    def _createAdditionalComponentsView(self) -> None:
        Logger.log("d", "Creating additional ui components for OctoPrint-connected printers.")

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "OctoPrintComponents.qml")
        self._additional_components = self._application.createQmlComponent(path, {"manager": self})
        if not self._additional_components:
            Logger.log("w", "Could not create additional components for OctoPrint-connected printers.")
            return

        self._application.addAdditionalComponent("monitorButtons", self._additional_components.findChild(QObject, "openOctoPrintButton"))

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply: QNetworkReply) -> None:

        http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.PostOperation:
            if "/plugin/appkeys/request" in reply.url().toString():  # Initial AppKey request
                if http_status_code == 201 or http_status_code == 202:
                    Logger.log("w", "Start polling for AppKeys decision")
                    if not self._appkey_request:
                        return
                    self._appkey_request.setUrl(reply.header(QNetworkRequest.LocationHeader))
                    self._appkey_request.setRawHeader(b"Content-Type", b"")
                    self._appkey_poll_timer.start()
                elif http_status_code == 404:
                    Logger.log("w", "This instance of OctoPrint does not support AppKeys")
                    self._appkey_request = None # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log("w", "Unknown response when requesting an AppKey: %d. OctoPrint said %s" % (http_status_code, response))
                    self._appkey_request = None # type: Optional[QNetworkRequest]

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "/plugin/appkeys/probe" in reply.url().toString():  # Probe for AppKey support
                if http_status_code == 204:
                    self._appkeys_supported = True
                else:
                    self._appkeys_supported = False
                self.appKeysSupportedChanged.emit()
            if "/plugin/appkeys/request" in reply.url().toString():  # Periodic AppKey request poll
                if http_status_code == 202:
                    self._appkey_poll_timer.start()
                elif http_status_code == 200:
                    Logger.log("d", "AppKey granted")
                    self._appkey_request = None # type: Optional[QNetworkRequest]
                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Received invalid JSON from octoprint instance.")
                        return

                    self._keys_cache[self._appkey_instance_id] = json_data["api_key"]
                    self.appKeyReceived.emit()
                elif http_status_code == 404:
                    Logger.log("d", "AppKey denied")
                    self._appkey_request = None # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log("w", "Unknown response when waiting for an AppKey: %d. OctoPrint said %s" % (http_status_code, response))
                    self._appkey_request = None # type: Optional[QNetworkRequest]

            if "api/settings" in reply.url().toString():  # OctoPrint settings dump from /settings:
                self._instance_in_error = False

                if http_status_code == 200:
                    Logger.log("d", "API key accepted by OctoPrint.")
                    self._instance_api_key_accepted = True

                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data["feature"]:
                        self._instance_supports_sd = json_data["feature"]["sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data["webcam"]:
                        stream_url = json_data["webcam"]["streamUrl"]
                        if stream_url: #not empty string or None
                            self._instance_supports_camera = True

                elif http_status_code == 401:
                    Logger.log("d", "Invalid API key for OctoPrint.")
                    self._instance_api_key_accepted = False

                elif http_status_code == 502 or http_status_code == 503:
                    Logger.log("d", "OctoPrint is not running.")
                    self._instance_api_key_accepted = False
                    self._instance_in_error = True

                self._instance_responded = True
                self.selectedInstanceSettingsChanged.emit()

    def _createRequest(self, url: str, basic_auth_username: str = "", basic_auth_password: str = "") -> QNetworkRequest:
        request = QNetworkRequest(url)
        request.setRawHeader("User-Agent".encode(), self._user_agent)
        if basic_auth_username and basic_auth_password:
            data = base64.b64encode(("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8")
            request.setRawHeader("Authorization".encode(), ("Basic %s" % data).encode())
        return request

    ##  Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before
    def _deobfuscateString(self, source: str) -> str:
        try:
            return base64.b64decode(source.encode("ascii")).decode("ascii")
        except UnicodeDecodeError:
            return source
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, properties):
        super().__init__(key)
        self._address = address
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None

        ##  Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint.
        self._num_extruders = 1

        self._hotend_temperatures = [0] * self._num_extruders
        self._target_hotend_temperatures = [0] * self._num_extruders

        self._api_version = "1"
        self._api_prefix = "/api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        self.setName(key)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._progress_message = None
        self._error_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(
            2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_timer = QTimer()
        self._camera_timer.setInterval(
            500)  # Todo: Add preference for camera update interval
        self._camera_timer.setSingleShot(False)
        self._camera_timer.timeout.connect(self._update_camera)

        self._camera_image_id = 0

        self._camera_image = QImage()

    def getProperties(self):
        return self._properties

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result=str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the printer (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this printer
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    def _update_camera(self):
        ## Request new image
        url = QUrl("http://" + self._address + ":8080/?action=snapshot")
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)

    def _update(self):
        ## Request 'general' printer data
        url = QUrl("http://" + self._address + self._api_prefix + "printer")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(),
                                       self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def close(self):
        self.setConnectionState(ConnectionState.closed)
        self._update_timer.stop()
        self._camera_timer.stop()

    def requestWrite(self, node, file_name=None, filter_by_machine=False):
        self._gcode = getattr(
            Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from printer
    def connect(self):
        self.setConnectionState(ConnectionState.connecting)
        self._update(
        )  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        self._update_camera()
        Logger.log("d", "Connection with printer %s with ip %s started",
                   self._key, self._address)
        self._update_timer.start()
        self._camera_timer.start()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify=newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(),
                                       self._api_key.encode())
        self._job_request.setHeader(QNetworkRequest.ContentTypeHeader,
                                    "application/json")
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"
        data = "{\"command\": \"%s\"}" % command
        self._job_reply = self._manager.post(self._job_request, data.encode())
        Logger.log("d", "Sent command to OctoPrint instance: %s", data)

    def startPrint(self):
        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Printer is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(
                i18n_catalog.i18nc("@info:status", "Sending data to printer"),
                0, False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            for line in self._gcode:
                single_string_file_data += line

            ##  TODO: Use correct file name (we use placeholder now)
            file_name = "%s.gcode" % Application.getInstance(
            ).getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                      "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                      "form-data; name=\"print\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(
                QNetworkRequest.ContentDispositionHeader,
                "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            url = QUrl("http://" + self._address + self._api_prefix +
                       "files/local")

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(),
                                            self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request,
                                                  self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Unable to send data to printer. Is another job still active?"
                ))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log(
                "e",
                "An exception occurred in network connection: %s" % str(e))

    ##  Handler for all requests that have finished.
    def _onFinished(self, reply):
        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "printer" in reply.url().toString(
            ):  # Status update from /printer.
                if http_status_code == 200:
                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    # Check for hotend temperatures
                    for index in range(0, self._num_extruders):
                        temperature = json_data["temperature"]["tool%d" %
                                                               index]["actual"]
                        self._setHotendTemperature(index, temperature)

                    bed_temperature = json_data["temperature"]["bed"]["actual"]
                    self._setBedTemperature(bed_temperature)

                    printer_state = "offline"
                    if json_data["state"]["flags"]["error"]:
                        printer_state = "error"
                    elif json_data["state"]["flags"]["paused"]:
                        printer_state = "paused"
                    elif json_data["state"]["flags"]["printing"]:
                        printer_state = "printing"
                    elif json_data["state"]["flags"]["ready"]:
                        printer_state = "ready"
                    self._updateJobState(printer_state)
                else:
                    pass  # TODO: Handle errors

            elif "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(
                                json_data["progress"]["printTime"] +
                                json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(
                                json_data["job"]["estimatedPrintTime"])
                        elif progress > 0:
                            self.setTimeTotal(
                                json_data["progress"]["printTime"] /
                                (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

            elif "snapshot" in reply.url().toString():  # Update from camera:
                if reply.attribute(
                        QNetworkRequest.HttpStatusCodeAttribute) == 200:
                    self._camera_image.loadFromData(reply.readAll())
                    self.newImage.emit()
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "files" in reply.url().toString(
            ):  # Result from /files command:
                if http_status_code == 201:
                    Logger.log(
                        "d", "Resource created on OctoPrint instance: %s",
                        reply.header(
                            QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()

            elif "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d",
                       "OctoPrintOutputDevice got an unhandled operation %s",
                       reply.operation())

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            self._progress_message.setProgress(bytes_sent / bytes_total * 100)
        else:
            self._progress_message.setProgress(0)
예제 #18
0
class PyPIVersionClient(QObject):

    """A client for the PyPI API using QNetworkAccessManager.

    It gets the latest version of qutebrowser from PyPI.

    Attributes:
        _nam: The QNetworkAccessManager used.

    Class attributes:
        API_URL: The base API URL.

    Signals:
        success: Emitted when getting the version info succeeded.
                 arg: The newest version.
        error: Emitted when getting the version info failed.
               arg: The error message, as string.
    """

    API_URL = 'https://pypi.python.org/pypi/{}/json'
    success = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._nam = QNetworkAccessManager(self)

    def get_version(self, package='qutebrowser'):
        """Get the newest version of a given package.

        Emits success/error when done.

        Args:
            package: The name of the package to check.
        """
        url = QUrl(self.API_URL.format(package))
        request = QNetworkRequest(url)
        reply = self._nam.get(request)
        if reply.isFinished():
            self.on_reply_finished(reply)
        else:
            reply.finished.connect(functools.partial(
                self.on_reply_finished, reply))

    def on_reply_finished(self, reply):
        """When the reply finished, load and parse the json data.

        Then emits error/success.

        Args:
            reply: The QNetworkReply which finished.
        """
        if reply.error() != QNetworkReply.NoError:
            self.error.emit(reply.errorString())
            return
        try:
            data = bytes(reply.readAll()).decode('utf-8')
        except UnicodeDecodeError as e:
            self.error.emit("Invalid UTF-8 data received in reply: "
                            "{}!".format(e))
            return
        try:
            json_data = json.loads(data)
        except ValueError as e:
            self.error.emit("Invalid JSON received in reply: {}!".format(e))
            return
        try:
            self.success.emit(json_data['info']['version'])
        except KeyError as e:
            self.error.emit("Malformed data recieved in reply "
                            "({!r} not found)!".format(e))
            return
예제 #19
0
class BasemapsWidget(BASE, WIDGET):
    def __init__(self, parent):
        super(BasemapsWidget, self).__init__(parent)

        self.parent = parent

        self.p_client = PlanetClient.getInstance()

        self._series = None
        self._initialized = False
        self._quads = None

        self.oneoff = None

        self.setupUi(self)

        self.mosaicsList = BasemapsListWidget()
        self.frameResults.layout().addWidget(self.mosaicsList)
        self.mosaicsList.setVisible(False)
        self.mosaicsList.basemapsSelectionChanged.connect(
            self.selection_changed)

        self.quadsTree = QuadsTreeWidget()
        self.quadsTree.quadsSelectionChanged.connect(
            self.quads_selection_changed)
        self.grpBoxQuads.layout().addWidget(self.quadsTree)

        self.renderingOptions = BasemapRenderingOptionsWidget()
        layout = QVBoxLayout()
        layout.setMargin(0)
        layout.addWidget(self.renderingOptions)
        self.frameRenderingOptions.setLayout(layout)

        self.aoi_filter = PlanetAOIFilter(self, self.parent, QUADS_AOI_COLOR)
        self.grpBoxAOI.layout().addWidget(self.aoi_filter)

        self.radioDownloadComplete.setChecked(True)

        self.buttons = [self.btnOneOff, self.btnSeries, self.btnAll]

        self.btnOneOff.clicked.connect(
            lambda: self.btn_filter_clicked(self.btnOneOff))
        self.btnSeries.clicked.connect(
            lambda: self.btn_filter_clicked(self.btnSeries))
        self.btnAll.clicked.connect(
            lambda: self.btn_filter_clicked(self.btnAll))

        self.btnOrder.clicked.connect(self.order)
        self.btnExplore.clicked.connect(self.explore)
        self.btnCancelQuadSearch.clicked.connect(self.cancel_quad_search)

        self.btnNextOrderMethodPage.clicked.connect(
            self.next_order_method_page_clicked)
        self.btnBackOrderMethodPage.clicked.connect(
            lambda: self.stackedWidget.setCurrentWidget(self.searchPage))
        self.btnBackAOIPage.clicked.connect(self.back_aoi_page_clicked)
        self.btnBackNamePage.clicked.connect(self.back_name_page_clicked)
        self.btnBackStreamingPage.clicked.connect(
            self.back_streaming_page_clicked)
        self.btnBackQuadsPage.clicked.connect(self.back_quads_page_clicked)
        self.btnNextQuadsPage.clicked.connect(self.next_quads_page_clicked)
        self.btnFindQuads.clicked.connect(self.find_quads_clicked)
        self.btnSubmitOrder.clicked.connect(self.submit_button_clicked)
        self.btnCloseConfirmation.clicked.connect(self.close_aoi_page)
        self.btnSubmitOrderStreaming.clicked.connect(
            self.submit_streaming_button_clicked)
        self.chkMinZoomLevel.stateChanged.connect(self.min_zoom_level_checked)
        self.chkMaxZoomLevel.stateChanged.connect(self.max_zoom_level_checked)

        levels = [str(x) for x in range(19)]
        self.comboMinZoomLevel.addItems(levels)
        self.comboMaxZoomLevel.addItems(levels)
        self.comboMaxZoomLevel.setCurrentIndex(len(levels) - 1)

        self.textBrowserOrderConfirmation.setOpenExternalLinks(False)
        self.textBrowserOrderConfirmation.anchorClicked.connect(
            show_orders_monitor)
        self.comboSeriesName.currentIndexChanged.connect(self.serie_selected)
        self.grpBoxFilter.collapsedStateChanged.connect(
            self.collapse_state_changed)
        self.lblSelectAllMosaics.linkActivated.connect(
            self.batch_select_mosaics_clicked)
        self.lblSelectAllQuads.linkActivated.connect(
            self.batch_select_quads_clicked)
        self.chkGroupByQuad.stateChanged.connect(self._populate_quads)
        self.chkOnlySRBasemaps.stateChanged.connect(
            self._only_sr_basemaps_changed)
        self.btnBasemapsFilter.clicked.connect(self._apply_filter)

        self.comboCadence.currentIndexChanged.connect(self.cadence_selected)

        self.textBrowserNoAccess.setOpenLinks(False)
        self.textBrowserNoAccess.setOpenExternalLinks(False)
        self.textBrowserNoAccess.anchorClicked.connect(
            self._open_basemaps_website)

    def _open_basemaps_website(self):
        open_link_with_browser("https://www.planet.com/purchase")

    def init(self):
        if not self._initialized:
            if self.series():
                self.stackedWidget.setCurrentWidget(self.searchPage)
                self.btnSeries.setChecked(True)
                self.btn_filter_clicked(self.btnSeries)
                self._initialized = True
            else:
                self.stackedWidget.setCurrentWidget(self.noBasemapsAccessPage)
                self._initialized = True

    def reset(self):
        self._initialized = False

    def batch_select_mosaics_clicked(self, url="all"):
        checked = url == "all"
        self.mosaicsList.setAllChecked(checked)

    def batch_select_quads_clicked(self, url="all"):
        checked = url == "all"
        self.quadsTree.setAllChecked(checked)

    def collapse_state_changed(self, collapsed):
        if not collapsed:
            self.set_filter_visibility()

    def set_filter_visibility(self):
        is_one_off = self.btnOneOff.isChecked()
        if is_one_off:
            self.grpBoxFilter.setVisible(False)
            mosaics = self.one_off_mosaics()
            self.mosaicsList.populate(mosaics)
            self.mosaicsList.setVisible(True)
            self.toggle_select_basemap_panel(False)
        else:
            is_all = self.btnAll.isChecked()
            self.grpBoxFilter.setVisible(True)
            self.labelBasemapsFilter.setVisible(is_all)
            self.textBasemapsFilter.setVisible(is_all)
            self.btnBasemapsFilter.setVisible(is_all)
            self.labelCadence.setVisible(not is_all)
            self.comboCadence.setVisible(not is_all)
            self.comboSeriesName.clear()
            if is_all:
                self.textBasemapsFilter.setText("")
                self.comboSeriesName.addItem(
                    "Apply a filter to populate this list of series", None)
            else:
                cadences = list(set([s[INTERVAL] for s in self.series()]))
                self.comboCadence.blockSignals(True)
                self.comboCadence.clear()
                self.comboCadence.addItem("All", None)

                def cadenceKey(c):
                    tokens = c.split(" ")
                    return f"{tokens[-1]}_{tokens[0]}"

                cadences.sort(key=cadenceKey)
                for cadence in cadences:
                    self.comboCadence.addItem(cadence, cadence)
                self.comboCadence.blockSignals(False)
                self.cadence_selected()

    def btn_filter_clicked(self, selectedbtn):
        for btn in self.buttons:
            if btn != selectedbtn:
                btn.blockSignals(True)
                btn.setChecked(False)
                btn.setEnabled(True)
                btn.blockSignals(False)
            selectedbtn.setEnabled(False)
        self.populate(selectedbtn)

    @waitcursor
    def series(self):
        if self._series is None:
            self._series = []
            response = self.p_client.list_mosaic_series()
            for page in response.iter():
                self._series.extend(page.get().get(SERIES))
        return self._series

    def _apply_filter(self):
        text = self.textBasemapsFilter.text()
        mosaics = self._get_filtered_mosaics(text)
        series = self._get_filtered_series(text)
        if len(mosaics) == 0 and len(series) == 0:
            self.parent.show_message("No results for current filter",
                                     level=Qgis.Warning,
                                     duration=10)
            return
        self.comboSeriesName.clear()
        self.comboSeriesName.addItem("Select a series or mosaic", None)
        for m in mosaics:
            self.comboSeriesName.addItem(m[NAME], (m, False))
        self.comboSeriesName.insertSeparator(len(mosaics))
        for s in series:
            self.comboSeriesName.addItem(s[NAME], (s, True))

    def _get_filtered_mosaics(self, text):
        mosaics = []
        response = self.p_client.get_mosaics(text)
        for page in response.iter():
            mosaics.extend(page.get().get(Mosaics.ITEM_KEY))
        mosaics = [m for m in mosaics if m[PRODUCT_TYPE] != TIMELAPSE]
        return mosaics

    def _get_filtered_series(self, text):
        return self.p_client.list_mosaic_series(text).get()[SERIES]

    def _only_sr_basemaps_changed(self):
        self.mosaicsList.set_only_sr_basemaps(
            self.chkOnlySRBasemaps.isChecked())

    def cadence_selected(self):
        cadence = self.comboCadence.currentData()
        self.comboSeriesName.blockSignals(True)
        self.comboSeriesName.clear()
        self.comboSeriesName.addItem("Select a series", None)
        series = self.series_for_interval(cadence)
        for s in series:
            self.comboSeriesName.addItem(s[NAME], (s, True))
        self.comboSeriesName.blockSignals(False)
        self.toggle_select_basemap_panel(True)

    def populate(self, category_btn=None):
        category_btn = category_btn or self.btnAll

        self.mosaicsList.clear()
        self.btnOrder.setText("Order (0 instances)")
        self.set_filter_visibility()

        self.batch_select_mosaics_clicked("none")

    def toggle_select_basemap_panel(self, show):
        self.lblSelectBasemapName.setVisible(show)
        self.lblSelectAllMosaics.setVisible(not show)
        self.lblCheckInstances.setVisible(not show)
        self.mosaicsList.setVisible(not show)

    def min_zoom_level_checked(self):
        self.comboMinZoomLevel.setEnabled(self.chkMinZoomLevel.isChecked())

    def max_zoom_level_checked(self):
        self.comboMaxZoomLevel.setEnabled(self.chkMaxZoomLevel.isChecked())

    @waitcursor
    def one_off_mosaics(self):
        if self.oneoff is None:
            all_mosaics = []
            response = self.p_client.get_mosaics()
            for page in response.iter():
                all_mosaics.extend(page.get().get(Mosaics.ITEM_KEY))
            self.oneoff = [
                m for m in all_mosaics if m[PRODUCT_TYPE] != TIMELAPSE
            ]

        return self.oneoff

    def series_for_interval(self, interval):
        series = []
        for s in self.series():
            interv = s.get(INTERVAL)
            if interv == interval or interval is None:
                series.append(s)
        return series

    @waitcursor
    def mosaics_for_serie(self, serie):
        mosaics = self.p_client.get_mosaics_for_series(serie[ID])
        all_mosaics = []
        for page in mosaics.iter():
            all_mosaics.extend(page.get().get(Mosaics.ITEM_KEY))
        return all_mosaics

    def serie_selected(self):
        self.mosaicsList.clear()
        data = self.comboSeriesName.currentData()
        self.toggle_select_basemap_panel(data is None)
        self.mosaicsList.setVisible(data is not None)
        if data:
            if data[1]:  # it is a series, not a single mosaic
                try:
                    mosaics = self.mosaics_for_serie(data[0])
                except InvalidAPIKey:
                    self.parent.show_message(
                        "Insufficient privileges. Cannot show mosaics of the selected"
                        " series",
                        level=Qgis.Warning,
                        duration=10,
                    )
                    return
            else:
                mosaics = [data[0]]
            self.mosaicsList.populate(mosaics)

    def selection_changed(self):
        selected = self.mosaicsList.selected_mosaics()
        n = len(selected)
        self.btnOrder.setText(f"Order ({n} items)")

    def quads_selection_changed(self):
        selected = self.quadsTree.selected_quads()
        n = len(selected)
        total = self.quadsTree.quads_count()
        self.labelQuadsSelected.setText(f"{n}/{total} quads selected")

    def _check_has_items_checked(self):
        selected = self.mosaicsList.selected_mosaics()
        if selected:
            if self.btnOneOff.isChecked() and len(selected) > 1:
                self.parent.show_message(
                    'Only one single serie can be selected in "one off" mode.',
                    level=Qgis.Warning,
                    duration=10,
                )
                return False
            else:
                return True
        else:
            self.parent.show_message("No checked items to order",
                                     level=Qgis.Warning,
                                     duration=10)
            return False

    def explore(self):
        if self._check_has_items_checked():
            selected = self.mosaicsList.selected_mosaics()

            analytics_track(BASEMAP_SERVICE_ADDED_TO_MAP)

            add_mosaics_to_qgis_project(
                selected,
                self.comboSeriesName.currentText() or selected[0][NAME])

    def order(self):
        if self._check_has_items_checked():
            self.stackedWidget.setCurrentWidget(self.orderMethodPage)

    def next_order_method_page_clicked(self):
        if self.radioDownloadComplete.isChecked():
            mosaics = self.mosaicsList.selected_mosaics()
            quad = self.p_client.get_one_quad(mosaics[0])
            quadarea = self._area_from_bbox_coords(quad[BBOX])
            mosaicarea = self._area_from_bbox_coords(mosaics[0][BBOX])
            if mosaicarea > MAX_AREA_TO_DOWNLOAD:
                QMessageBox.warning(
                    self,
                    "Complete Download",
                    "This area is too big to download from the QGIS Plugin.<br>To"
                    " download a large Basemap area, you may want to consult our <a"
                    " href='https://developers.planet.com/docs/basemaps/'>developer"
                    " resources</a>",
                )
                return
            numquads = int(mosaicarea / quadarea)
            if numquads > MAX_QUADS_TO_DOWNLOAD:
                ret = QMessageBox.question(
                    self,
                    "Complete Download",
                    f"The download will contain more than {MAX_QUADS_TO_DOWNLOAD}"
                    " quads.\nAre your sure you want to proceed?",
                )
                if ret != QMessageBox.Yes:
                    return
            self.show_order_name_page()
        elif self.radioDownloadAOI.isChecked():
            self.labelWarningQuads.setText("")
            self.widgetProgressFindQuads.setVisible(False)
            self.stackedWidget.setCurrentWidget(self.orderAOIPage)
        elif self.radioStreaming.isChecked():
            self.show_order_streaming_page()

    def find_quads_clicked(self):
        self.labelWarningQuads.setText("")
        selected = self.mosaicsList.selected_mosaics()
        if not self.aoi_filter.leAOI.text():
            self.labelWarningQuads.setText(
                "⚠️ No area of interest (AOI) defined")
            return
        geom = self.aoi_filter.aoi_as_4326_geom()
        if geom is None:
            self.parent.show_message("Wrong AOI definition",
                                     level=Qgis.Warning,
                                     duration=10)
            return
        mosaic_extent = QgsRectangle(*selected[0][BBOX])
        if not geom.intersects(mosaic_extent):
            self.parent.show_message("No mosaics in the selected area",
                                     level=Qgis.Warning,
                                     duration=10)
            return
        qgsarea = QgsDistanceArea()
        area = qgsarea.convertAreaMeasurement(
            qgsarea.measureArea(geom), QgsUnitTypes.AreaSquareKilometers)
        if area > MAX_AREA_TO_DOWNLOAD:
            QMessageBox.warning(
                self,
                "Quad Download",
                "This area is too big to download from the QGIS Plugin.<br>To download"
                " a large Basemap area, you may want to consult our <a"
                " href='https://developers.planet.com/docs/basemaps/'>developer"
                " resources</a>",
            )
            return
        self.find_quads()

    @waitcursor
    def find_quads(self):
        selected = self.mosaicsList.selected_mosaics()
        geom = self.aoi_filter.aoi_as_4326_geom()
        qgsarea = QgsDistanceArea()
        area = qgsarea.convertAreaMeasurement(
            qgsarea.measureArea(geom), QgsUnitTypes.AreaSquareKilometers)
        quad = self.p_client.get_one_quad(selected[0])
        quadarea = self._area_from_bbox_coords(quad[BBOX])
        numpages = math.ceil(area / quadarea / QUADS_PER_PAGE)

        self.widgetProgressFindQuads.setVisible(True)
        self.progressBarInstances.setMaximum(len(selected))
        self.progressBarQuads.setMaximum(numpages)
        self.finder = QuadFinder()
        self.finder.setup(self.p_client, selected, geom)

        self.objThread = QThread()
        self.finder.moveToThread(self.objThread)
        self.finder.finished.connect(self.objThread.quit)
        self.finder.finished.connect(self._update_quads)
        self.finder.mosaicStarted.connect(self._mosaic_started)
        self.finder.pageRead.connect(self._page_read)
        self.objThread.started.connect(self.finder.find_quads)
        self.objThread.start()

    def cancel_quad_search(self):
        self.finder.cancel()
        self.objThread.quit()
        self.widgetProgressFindQuads.setVisible(False)

    def _mosaic_started(self, i, name):
        self.labelProgressInstances.setText(
            f"Processing basemap '{name}' ({i}/{self.progressBarInstances.maximum()})"
        )
        self.progressBarInstances.setValue(i)
        QApplication.processEvents()

    def _page_read(self, i):
        total = self.progressBarQuads.maximum()
        self.labelProgressQuads.setText(
            f"Downloading quad footprints (page {i} of (estimated) {total})")
        self.progressBarQuads.setValue(i)
        QApplication.processEvents()

    def _update_quads(self, quads):
        self._quads = quads
        self._populate_quads()

    def _populate_quads(self):
        selected = self.mosaicsList.selected_mosaics()
        if self.chkGroupByQuad.isChecked():
            self.quadsTree.populate_by_quad(selected, self._quads)
        else:
            self.quadsTree.populate_by_basemap(selected, self._quads)
        total_quads = self.quadsTree.quads_count()
        self.labelQuadsSummary.setText(
            f"{total_quads} quads from {len(selected)} basemap instances "
            "intersect your AOI for this basemap")
        self.batch_select_quads_clicked("all")
        self.quads_selection_changed()
        self.widgetProgressFindQuads.setVisible(False)
        self.stackedWidget.setCurrentWidget(self.orderQuadsPage)

    def next_quads_page_clicked(self):
        selected = self.quadsTree.selected_quads()
        if len(selected) > MAX_QUADS_TO_DOWNLOAD:
            ret = QMessageBox.question(
                self,
                "Quad Download",
                f"The download will contain more than {MAX_QUADS_TO_DOWNLOAD} quads.\n"
                "Are your sure you want to proceed?",
            )
            if ret != QMessageBox.Yes:
                return
        if selected:
            self.show_order_name_page()
        else:
            self.parent.show_message("No checked quads to order",
                                     level=Qgis.Warning,
                                     duration=10)

    def back_quads_page_clicked(self):
        self.quadsTree.clear()
        self.stackedWidget.setCurrentWidget(self.orderAOIPage)

    def show_order_streaming_page(self):
        selected = self.mosaicsList.selected_mosaics()
        name = selected[0][NAME]
        dates = date_interval_from_mosaics(selected)
        description = (
            f'<span style="color:black;"><b>{name}</b></span><br>'
            f'<span style="color:grey;">{len(selected)} instances | {dates}</span>'
        )
        self.labelStreamingOrderDescription.setText(description)
        pixmap = QPixmap(PLACEHOLDER_THUMB, "SVG")
        thumb = pixmap.scaled(48, 48, QtCore.Qt.KeepAspectRatio,
                              QtCore.Qt.SmoothTransformation)
        self.labelStreamingOrderIcon.setPixmap(thumb)
        if THUMB in selected[0][LINKS]:
            self.set_summary_icon(selected[0][LINKS][THUMB])
        self.chkMinZoomLevel.setChecked(False)
        self.chkMaxZoomLevel.setChecked(False)
        self.comboMinZoomLevel.setEnabled(False)
        self.comboMaxZoomLevel.setEnabled(False)
        self.renderingOptions.set_datatype(selected[0][DATATYPE])
        self.stackedWidget.setCurrentWidget(self.orderStreamingPage)

    def _quads_summary(self):
        selected = self.mosaicsList.selected_mosaics()
        dates = date_interval_from_mosaics(selected)
        selected_quads = self.quadsTree.selected_quads()
        return f"{len(selected_quads)} quads | {dates}"

    def _quads_quota(self):
        selected_quads = self.quadsTree.selected_quads()
        total_area = 0
        for quad in selected_quads:
            total_area += self._area_from_bbox_coords(quad[BBOX])
        return total_area

    def _area_from_bbox_coords(self, bbox):
        qgsarea = QgsDistanceArea()
        extent = QgsRectangle(*bbox)
        geom = QgsGeometry.fromRect(extent)
        area = qgsarea.convertAreaMeasurement(
            qgsarea.measureArea(geom), QgsUnitTypes.AreaSquareKilometers)
        return area

    def show_order_name_page(self):
        QUAD_SIZE = 1
        selected = self.mosaicsList.selected_mosaics()
        if not self.btnOneOff.isChecked():
            name = self.comboSeriesName.currentText()
        else:
            name = selected[0][NAME]
        dates = date_interval_from_mosaics(selected)
        if self.radioDownloadComplete.isChecked():
            description = (
                f'<span style="color:black;"><b>{name}</b></span><br>'
                f'<span style="color:grey;">{len(selected)} instances | {dates}</span>'
            )

            title = "Order Complete Basemap"
            total_area = self._area_from_bbox_coords(
                selected[0][BBOX]) * len(selected)
            quad = self.p_client.get_one_quad(selected[0])
            quadarea = self._area_from_bbox_coords(quad[BBOX])
            numquads = total_area / quadarea
        elif self.radioDownloadAOI.isChecked():
            selected_quads = self.quadsTree.selected_quads()
            numquads = len(selected_quads)
            title = "Order Partial Basemap"
            description = (
                f'<span style="color:black;"><b>{name}</b></span><br>'
                f'<span style="color:grey;">{self._quads_summary()}</span>')
            total_area = self._quads_quota()

        pixmap = QPixmap(PLACEHOLDER_THUMB, "SVG")
        thumb = pixmap.scaled(48, 48, QtCore.Qt.KeepAspectRatio,
                              QtCore.Qt.SmoothTransformation)
        self.labelOrderIcon.setPixmap(thumb)
        if THUMB in selected[0][LINKS]:
            self.set_summary_icon(selected[0][LINKS][THUMB])
        self.labelOrderDescription.setText(description)
        self.grpBoxNamePage.setTitle(title)
        self.stackedWidget.setCurrentWidget(self.orderNamePage)
        self.txtOrderName.setText("")
        quota = self.p_client.user_quota_remaining()
        size = numquads * QUAD_SIZE
        if quota is not None:
            self.labelOrderInfo.setText(
                f"This Order will use {total_area:.2f} square km"
                f" of your remaining {quota} quota.\n\n"
                f"This Order's download size will be approximately {size} GB.")
            self.labelOrderInfo.setVisible(True)
        else:
            self.labelOrderInfo.setVisible(False)

    def set_summary_icon(self, iconurl):
        self.nam = QNetworkAccessManager()
        self.nam.finished.connect(self.iconDownloaded)
        self.nam.get(QNetworkRequest(QUrl(iconurl)))

    def iconDownloaded(self, reply):
        img = QImage()
        img.loadFromData(reply.readAll())
        pixmap = QPixmap(img)
        thumb = pixmap.scaled(48, 48, QtCore.Qt.KeepAspectRatio,
                              QtCore.Qt.SmoothTransformation)
        if self.radioStreaming.isChecked():
            self.labelStreamingOrderIcon.setPixmap(thumb)
        else:
            self.labelOrderIcon.setPixmap(thumb)

    def back_streaming_page_clicked(self):
        self.stackedWidget.setCurrentWidget(self.orderMethodPage)

    def back_name_page_clicked(self):
        if self.radioDownloadComplete.isChecked():
            self.stackedWidget.setCurrentWidget(self.orderMethodPage)
        elif self.radioDownloadAOI.isChecked():
            self.quadsTree.show_footprints()
            self.stackedWidget.setCurrentWidget(self.orderQuadsPage)

    def back_aoi_page_clicked(self):
        self.stackedWidget.setCurrentWidget(self.orderMethodPage)
        self.aoi_filter.reset_aoi_box()

    def submit_streaming_button_clicked(self):
        selected = self.mosaicsList.selected_mosaics()
        zmin = (self.comboMinZoomLevel.currentText()
                if self.chkMinZoomLevel.isChecked() else 0)
        zmax = (self.comboMaxZoomLevel.currentText()
                if self.chkMaxZoomLevel.isChecked() else 18)
        mosaicname = self.comboSeriesName.currentText() or selected[0][NAME]
        proc = self.renderingOptions.process()
        ramp = self.renderingOptions.ramp()

        analytics_track(BASEMAP_SERVICE_CONNECTION_ESTABLISHED)

        for mosaic in selected:
            name = f"{mosaicname} - {mosaic_title(mosaic)}"
            add_mosaics_to_qgis_project(
                [mosaic],
                name,
                proc=proc,
                ramp=ramp,
                zmin=zmin,
                zmax=zmax,
                add_xyz_server=True,
            )
        selected = self.mosaicsList.selected_mosaics()
        base_html = "<p>Your Connection(s) have been established</p>"
        self.grpBoxOrderConfirmation.setTitle("Order Streaming Download")
        dates = date_interval_from_mosaics(selected)
        description = f"{len(selected)} | {dates}"
        values = {"Series Name": mosaicname, "Series Instances": description}
        self.set_order_confirmation_summary(values, base_html)
        self.stackedWidget.setCurrentWidget(self.orderConfirmationPage)

    def submit_button_clicked(self):
        name = self.txtOrderName.text()
        if not bool(name.strip()):
            self.parent.show_message("Enter a name for the order",
                                     level=Qgis.Warning,
                                     duration=10)
            return
        if self.radioDownloadComplete.isChecked():
            self.order_complete_submit()
        elif self.radioDownloadAOI.isChecked():
            self.order_partial_submit()

    def set_order_confirmation_summary(self, values, base_html=None):
        html = base_html or (
            "<p>Your order has been successfully submitted for processing.You may"
            " monitor its progress and availability in the <a href='#'>Order Status"
            " panel</a>.</p>")
        html += "<p><table>"
        for k, v in values.items():
            html += f"<tr><td>{k}</td><td><b>{v}</b></td></tr>"
        html += "</table>"
        self.textBrowserOrderConfirmation.setHtml(html)

    @waitcursor
    def order_complete_submit(self):
        selected = self.mosaicsList.selected_mosaics()

        analytics_track(BASEMAP_COMPLETE_ORDER)

        name = self.txtOrderName.text()
        load_as_virtual = self.chkLoadAsVirtualLayer.isChecked()

        self.grpBoxOrderConfirmation.setTitle("Order Complete Download")
        dates = date_interval_from_mosaics(selected)
        description = f"{len(selected)} complete mosaics | {dates}"
        create_quad_order_from_mosaics(name, description, selected,
                                       load_as_virtual)
        refresh_orders()
        values = {
            "Order Name": self.txtOrderName.text(),
            "Series Name": self.comboSeriesName.currentText()
            or selected[0][NAME],
            "Series Instances": description,
        }
        self.set_order_confirmation_summary(values)
        self.stackedWidget.setCurrentWidget(self.orderConfirmationPage)

    def order_partial_submit(self):
        self.grpBoxOrderConfirmation.setTitle("Order Partial Download")
        mosaics = self.mosaicsList.selected_mosaics()
        quads_count = len(self.quadsTree.selected_quads())

        analytics_track(BASEMAP_PARTIAL_ORDER, {"count": quads_count})

        dates = date_interval_from_mosaics(mosaics)
        quads = self.quadsTree.selected_quads_classified()
        name = self.txtOrderName.text()
        load_as_virtual = self.chkLoadAsVirtualLayer.isChecked()
        description = f"{quads_count} quads | {dates}"
        create_quad_order_from_quads(name, description, quads, load_as_virtual)
        refresh_orders()
        values = {
            "Order Name": self.txtOrderName.text(),
            "Series Name": self.comboSeriesName.currentText()
            or mosaics[0][NAME],
            "Quads": self._quads_summary(),
            "Quota": self._quads_quota(),
        }
        self.set_order_confirmation_summary(values)
        self.quadsTree.clear()
        self.stackedWidget.setCurrentWidget(self.orderConfirmationPage)

    def close_aoi_page(self):
        self.aoi_filter.reset_aoi_box()
        self.quadsTree.clear()
        self.stackedWidget.setCurrentWidget(self.searchPage)
예제 #20
0
class WeatherController(QObject):
    """
    Class to request the weather data and control the view
    """

    # noinspection PyUnresolvedReferences
    def __init__(self, parent=None):
        super(WeatherController, self).__init__(parent)

        self._dataReceived = False
        self._forecastDataReceived = False

        self._weather_data = CurrentWeatherData()
        self._weather_forecast_data = []
        self._data_model = ForecastDataModel()

        self._network_manager = QNetworkAccessManager(self)
        self._timer = QTimer(self)

        self._timer.timeout.connect(self.update_weather)

        self._current_weather = None
        self._forecast_weather = None
        self._api_key = ''

        self._last_update_time = ''

        self._requested_location = 'Dachau'

        try:
            with open('resources/api.txt') as f:
                self._api_key = f.readline()
        except FileNotFoundError:
            print('The api key is not found')

    weather_changed = pyqtSignal()

    @pyqtProperty('QString', notify=weather_changed)
    def location(self):
        """
        The location in the weather data.
        :return: string location name
        """
        return self._weather_data.location_name

    @pyqtProperty('QString', notify=weather_changed)
    def description(self):
        """
        The current weather description.
        :return: string description
        """
        return self._weather_data.description

    @pyqtProperty('QString', notify=weather_changed)
    def icon(self):
        """
        Icon for the current weather condition.
        :return: string the path to the icon
        """
        return self._weather_data.icon

    @pyqtProperty('QString', notify=weather_changed)
    def temp(self):
        """
        The current temperature.
        :return: string the formatted temperature
        """
        return str(self._weather_data.temperature)

    model_changed = pyqtSignal()

    @pyqtProperty(QAbstractListModel, notify=model_changed)
    def data_model(self):
        return self._data_model

    last_update_time_changed = pyqtSignal()

    @pyqtProperty('QString', notify=last_update_time_changed)
    def last_update_time(self):
        return self._last_update_time

    @pyqtSlot()
    def view_is_ready(self):
        """
        Request the weather data and start the timer when the view is ready,
        called from the view onCompleted.

        :rtype: none
        """
        self._request_weather_data()
        self._timer.start(3600000)

    @pyqtSlot()
    def stop_timer(self):
        self._timer.stop()
        qDebug('Timer stopped')

    @pyqtProperty('QString')
    def requested_location(self):
        return self._requested_location

    @requested_location.setter
    def requested_location(self, value):
        self._requested_location = value
        self._request_weather_data()

    def weather_data_received(self):
        json_str = self._current_weather.readAll()
        json_doc = QJsonDocument.fromJson(json_str)
        self._read_current_weather_data(json_doc.object())

    def forecast_data_received(self):
        json_str = self._forecast_weather.readAll()
        json_doc = QJsonDocument.fromJson(json_str)
        self._read_forecast_data(json_doc.object())

    def update_weather(self):
        self._request_weather_data()

    def _read_current_weather_data(self, json_object):
        # location
        """
        Read the current weather data from the json object.

        :param json_object: The Json Object
        :return nothing
        """
        if 'name' not in json_object:
            self._weather_data.location_name = 'No data available!'
        else:
            name = json_object['name'].toString()
            qDebug(name)
            if name == '':
                message = json_object['message'].toString()
                self._weather_data.location_name = message
            else:
                self._weather_data.location_name = name

                # temperature
                tDo = json_object['main'].toObject()['temp'].toDouble()
                temp = int(round(tDo))
                self._weather_data.temperature = temp

                # description
                json_weather = json_object['weather'].toArray()[0].toObject()
                desc = json_weather['description'].toString()
                self._weather_data.description = desc

                # icon
                icon = json_weather['icon'].toString()
                icon_path = '../../resources/weather_img/{0}.png'.format(icon)
                self._weather_data.icon = icon_path

            self.weather_changed.emit()

            self._last_update_time = QDateTime.currentDateTime().toString(
                        'h:mm')
            self.last_update_time_changed.emit()

    def _read_forecast_data(self, json_object):
        json_list = json_object['list'].toArray()
        self._weather_forecast_data = []
        for obj in json_list:
            json_list_object = obj.toObject()
            forecast_data = WeatherForecastData()

            # time
            time = json_list_object['dt'].toInt()
            forecast_data.time = time

            weather_array = json_list_object['weather'].toArray()
            weather_object = weather_array[0].toObject()

            # description
            desc = weather_object['description'].toString()
            forecast_data.description = desc

            # icon
            icon = weather_object['icon'].toString()
            icon_path = '../../resources/weather_img/{0}.png'.format(icon)
            forecast_data.icon = icon_path

            # temperature max / min
            temp_object = json_list_object['temp'].toObject()
            temp_min_double = temp_object['min'].toDouble()
            temp_max_double = temp_object['max'].toDouble()
            temp_min = int(round(temp_min_double))
            forecast_data.temp_min = temp_min
            temp_max = int(round(temp_max_double))
            forecast_data.temp_max = temp_max

            self._weather_forecast_data.append(forecast_data)

        self._data_model.set_all_data(self._weather_forecast_data)
        self.model_changed.emit()

    def _request_weather_data(self):
        """
        Request the weather over Http request at openweathermap.org, you need
        an API key according to use the services.
        If the call is finished will call according function over Qt's
        signaling System.

        :return: nothing

        """

        # request current weather
        api_call = QUrl(
            'http://api.openweathermap.org/data/2.5/weather?q'
            '={0},de&units=metric&APPID={1}'.format(self._requested_location,
                                                    self._api_key))
        request_current_weather = QNetworkRequest(api_call)
        self._current_weather = self._network_manager.get(
            request_current_weather)
        self._current_weather.finished.connect(self.weather_data_received)

        # forecast data
        api_call_forecast = QUrl(
            'http://api.openweathermap.org/data/2.5/forecast/daily?q={0},'
            'de&cnt=4&units=metric&APPID={1}'.format(self._requested_location,
                                                     self._api_key))
        request_forecast = QNetworkRequest(api_call_forecast)
        self._forecast_weather = self._network_manager.get(request_forecast)
        self._forecast_weather.finished.connect(self.forecast_data_received)
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}
        self._cluster_printers_seen = {}  # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference("um3networkprinting/manual_instances", "") #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        self._network_requests_buffer = {}  # store api responses until data is complete

        # The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target = self._handleOnServiceChangedRequests,
                                                      daemon = True)
        self._service_changed_request_thread.start()

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [printer_name for printer_name in self._printers]
            self._printers = {}
            self.printerListChanged.emit()
        # After network switching, one must make a new instance of Zeroconf
        # On windows, the instance creation is very fast (unnoticable). Other platforms?
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true"
        }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        self.checkManualPrinter(address)
        self.checkClusterPrinter(address)

    def removeManualPrinter(self, key, address = None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def checkManualPrinter(self, address):
        # Check if a printer exists at this address
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def checkClusterPrinter(self, address):
        cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster")
        cluster_request = QNetworkRequest(cluster_url)
        self._network_manager.get(cluster_request)

    ##  Handler for all requests that have finished.
    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if reply.operation() == QNetworkAccessManager.GetOperation:
            address = reply.url().host()
            if "origin=manual_name" in reply_url:  # Name returned from printer.
                if status_code == 200:

                    try:
                        system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.JSONDecodeError:
                        Logger.log("e", "Printer returned invalid JSON.")
                        return
                    except UnicodeDecodeError:
                        Logger.log("e", "Printer returned incorrect UTF-8.")
                        return

                    if address not in self._network_requests_buffer:
                        self._network_requests_buffer[address] = {}
                    self._network_requests_buffer[address]["system"] = system_info
            elif "origin=check_cluster" in reply_url:
                if address not in self._network_requests_buffer:
                    self._network_requests_buffer[address] = {}
                if status_code == 200:
                    # We know it's a cluster printer
                    Logger.log("d", "Cluster printer detected: [%s]", reply.url())

                    try:
                        cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.JSONDecodeError:
                        Logger.log("e", "Printer returned invalid JSON.")
                        return
                    except UnicodeDecodeError:
                        Logger.log("e", "Printer returned incorrect UTF-8.")
                        return

                    self._network_requests_buffer[address]["cluster"] = True
                    self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list)
                else:
                    Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url())
                    self._network_requests_buffer[address]["cluster"] = False

            # Both the system call and cluster call are finished
            if (address in self._network_requests_buffer and
                "system" in self._network_requests_buffer[address] and
                "cluster" in self._network_requests_buffer[address]):

                instance_name = "manual:%s" % address
                system_info = self._network_requests_buffer[address]["system"]
                machine = "unknown"
                if "variant" in system_info:
                    variant = system_info["variant"]
                    if variant == "Ultimaker 3":
                        machine = "9066"
                    elif variant == "Ultimaker 3 Extended":
                        machine = "9511"

                properties = {
                    b"name": system_info["name"].encode("utf-8"),
                    b"address": address.encode("utf-8"),
                    b"firmware_version": system_info["firmware"].encode("utf-8"),
                    b"manual": b"true",
                    b"machine": machine.encode("utf-8")
                }

                if self._network_requests_buffer[address]["cluster"]:
                    properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"]

                if instance_name in self._printers:
                    # Only replace the printer if it is still in the list of (manual) printers
                    self.removePrinter(instance_name)
                    self.addPrinter(instance_name, address, properties)

                del self._network_requests_buffer[address]

    ##  Stop looking for devices on network.
    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("um_network_key"):
                if not self._printers[key].isConnected():
                    Logger.log("d", "Connecting [%s]..." % key)
                    self._printers[key].connect()
                    self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    Logger.log("d", "Closing connection [%s]..." % key)
                    self._printers[key].close()
                    self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)

    ##  Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
    def addPrinter(self, name, address, properties):
        cluster_size = int(properties.get(b"cluster_size", -1))
        if cluster_size >= 0:
            printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
                name, address, properties, self._api_prefix)
        else:
            printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        self._cluster_printers_seen[printer.getKey()] = name  # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
            if printer.getKey() not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey())
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.disconnect()
                printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
                Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
        self.printerListChanged.emit()

    ##  Handler for when the connection state of one of the detected printers changes
    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        if self._printers[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties = {})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addPrinterSignal.emit(str(name), address, info.properties)
                    else:
                        Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))

        return True

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)
            # stop if the application is shutting down
            if Application.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # handle all pending requests
            reschedule_requests = []  # a list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type, name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
                                        service_type, name)
                    reschedule_requests.append(request)

            # re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    @pyqtSlot()
    def openControlPanel(self):
        Logger.log("d", "Opening print jobs web UI...")
        selected_device = self.getOutputDeviceManager().getActiveDevice()
        if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice):
            QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
예제 #22
0
 def get(self, url):
     return QNetworkAccessManager.get(self, self._getRequest(url))
예제 #23
0
class DevicesListWidget(QWidget):
    def __init__(self, parent, *args, **kwargs):
        super(DevicesListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.mdi = parent.mdi
        self.idx = None

        self.nam = QNetworkAccessManager()
        self.backup = bytes()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()),
                                  QSettings.IniFormat)
        self.hidden_columns = self.settings.value("hidden_columns", [1, 2])

        self.tb = Toolbar(Qt.Horizontal, 16, Qt.ToolButtonTextBesideIcon)
        self.tb.addAction(QIcon("GUI/icons/add.png"), "Add", self.device_add)

        self.layout().addWidget(self.tb)

        self.device_list = TableView()
        self.model = parent.device_model
        self.telemetry_model = parent.telemetry_model
        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupColumns(columns, self.hidden_columns)
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(DevMdl.TOPIC, Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.layout().addWidget(self.device_list)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.doubleClicked.connect(self.device_config)
        self.device_list.customContextMenuRequested.connect(
            self.show_list_ctx_menu)

        self.device_list.horizontalHeader().setContextMenuPolicy(
            Qt.CustomContextMenu)
        self.device_list.horizontalHeader().customContextMenuRequested.connect(
            self.show_header_ctx_menu)

        self.ctx_menu = QMenu()
        self.ctx_menu_relays = None
        self.create_actions()

        self.build_header_ctx_menu()

    def create_actions(self):
        self.ctx_menu.addAction(QIcon("GUI/icons/configure.png"), "Configure",
                                self.device_config)
        self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Remove",
                                self.device_delete)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh",
                                self.ctx_menu_refresh)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/on.png"), "Power ON",
                                lambda: self.ctx_menu_power(state="ON"))
        self.ctx_menu.addAction(QIcon("GUI/icons/off.png"), "Power OFF",
                                lambda: self.ctx_menu_power(state="OFF"))

        self.ctx_menu_relays = QMenu("Relays")
        self.ctx_menu_relays.setIcon(QIcon("GUI/icons/switch.png"))
        relays_btn = self.ctx_menu.addMenu(self.ctx_menu_relays)
        self.ctx_menu_relays.setEnabled(False)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained",
                                self.ctx_menu_clean_retained)
        self.ctx_menu.addSeparator()

        self.ctx_menu_copy = QMenu("Copy")
        self.ctx_menu_copy.setIcon(QIcon("GUI/icons/copy.png"))
        copy_btn = self.ctx_menu.addMenu(self.ctx_menu_copy)

        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction("Set teleperiod", self.ctx_menu_teleperiod)
        self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart",
                                self.ctx_menu_restart)
        self.ctx_menu.addAction(QIcon("GUI/icons/web.png"), "Open WebUI",
                                self.ctx_menu_webui)
        self.ctx_menu.addSeparator()

        self.ctx_menu_ota = QMenu("OTA upgrade")
        self.ctx_menu_ota.addAction("Set OTA URL", self.ctx_menu_ota_set_url)
        self.ctx_menu_ota.addAction("Upgrade", self.ctx_menu_ota_set_upgrade)
        ota_btn = self.ctx_menu.addMenu(self.ctx_menu_ota)

        self.ctx_menu.addAction("Config backup", self.ctx_menu_config_backup)

        self.ctx_menu_copy.addAction(
            "IP", lambda: self.ctx_menu_copy_value(DevMdl.IP))
        self.ctx_menu_copy.addAction(
            "MAC", lambda: self.ctx_menu_copy_value(DevMdl.MAC))
        self.ctx_menu_copy.addAction("BSSID", self.ctx_menu_copy_bssid)
        self.ctx_menu_copy.addSeparator()
        self.ctx_menu_copy.addAction(
            "Topic", lambda: self.ctx_menu_copy_value(DevMdl.TOPIC))
        self.ctx_menu_copy.addAction(
            "FullTopic", lambda: self.ctx_menu_copy_value(DevMdl.FULL_TOPIC))
        self.ctx_menu_copy.addAction(
            "STAT topic", lambda: self.ctx_menu_copy_prefix_topic("STAT"))
        self.ctx_menu_copy.addAction(
            "CMND topic", lambda: self.ctx_menu_copy_prefix_topic("CMND"))
        self.ctx_menu_copy.addAction(
            "TELE topic", lambda: self.ctx_menu_copy_prefix_topic("TELE"))

        self.tb.addActions(self.ctx_menu.actions())
        self.tb.widgetForAction(ota_btn).setPopupMode(QToolButton.InstantPopup)
        self.tb.widgetForAction(relays_btn).setPopupMode(
            QToolButton.InstantPopup)
        self.tb.widgetForAction(copy_btn).setPopupMode(
            QToolButton.InstantPopup)

    def ctx_menu_copy_value(self, column):
        if self.idx:
            row = self.idx.row()
            value = self.model.data(self.model.index(row, column))
            QApplication.clipboard().setText(value)

    def ctx_menu_copy_bssid(self):
        if self.idx:
            QApplication.clipboard().setText(self.model.bssid(self.idx))

    def ctx_menu_copy_prefix_topic(self, prefix):
        if self.idx:
            if prefix == "STAT":
                topic = self.model.statTopic(self.idx)
            elif prefix == "CMND":
                topic = self.model.commandTopic(self.idx)
            elif prefix == "TELE":
                topic = self.model.teleTopic(self.idx)
            QApplication.clipboard().setText(topic)

    def ctx_menu_clean_retained(self):
        if self.idx:
            relays = self.model.data(
                self.model.index(self.idx.row(), DevMdl.POWER))
            if relays and len(relays.keys()) > 0:
                cmnd_topic = self.model.commandTopic(self.idx)

                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic + r, retain=True)
                QMessageBox.information(self, "Clear retained",
                                        "Cleared retained messages.")

    def ctx_menu_power(self, relay=None, state=None):
        if self.idx:
            relays = self.model.data(
                self.model.index(self.idx.row(), DevMdl.POWER))
            cmnd_topic = self.model.commandTopic(self.idx)
            if relay:
                self.mqtt.publish(cmnd_topic + relay, payload=state)

            elif relays:
                for r in relays.keys():
                    self.mqtt.publish(cmnd_topic + r, payload=state)

    def ctx_menu_restart(self):
        if self.idx:
            self.mqtt.publish("{}/restart".format(
                self.model.commandTopic(self.idx)),
                              payload="1")

    def ctx_menu_refresh(self):
        if self.idx:
            for q in initial_queries:
                self.mqtt.publish("{}/status".format(
                    self.model.commandTopic(self.idx)),
                                  payload=q)

    def ctx_menu_teleperiod(self):
        if self.idx:
            teleperiod, ok = QInputDialog.getInt(
                self, "Set telemetry period",
                "Input 1 to reset to default\n[Min: 10, Max: 3600]",
                int(
                    self.model.data(
                        self.model.index(self.idx.row(),
                                         DevMdl.TELEPERIOD))), 1, 3600)
            if ok:
                if teleperiod != 1 and teleperiod < 10:
                    teleperiod = 10
            self.mqtt.publish("{}/teleperiod".format(
                self.model.commandTopic(self.idx)),
                              payload=teleperiod)

    def ctx_menu_telemetry(self):
        if self.idx:
            self.mqtt.publish("{}/status".format(
                self.model.commandTopic(self.idx)),
                              payload=8)

    def ctx_menu_webui(self):
        if self.idx:
            QDesktopServices.openUrl(
                QUrl("http://{}".format(self.model.ip(self.idx))))

    def ctx_menu_config_backup(self):
        if self.idx:
            self.backup = bytes()
            ip = self.model.data(self.model.index(self.idx.row(), DevMdl.IP))
            self.dl = self.nam.get(
                QNetworkRequest(QUrl("http://{}/dl".format(ip))))
            self.dl.readyRead.connect(self.get_dump)
            self.dl.finished.connect(self.save_dump)

    def ctx_menu_ota_set_url(self):
        if self.idx:
            current_url = self.model.data(
                self.model.index(self.idx.row(), DevMdl.OTA_URL))
            url, ok = QInputDialog.getText(
                self,
                "Set OTA URL",
                '100 chars max. Set to "1" to reset to default.',
                text=current_url)
            if ok:
                self.mqtt.publish("{}/otaurl".format(
                    self.model.commandTopic(self.idx)),
                                  payload=url)

    def ctx_menu_ota_set_upgrade(self):
        if self.idx:
            current_url = self.model.data(
                self.model.index(self.idx.row(), DevMdl.OTA_URL))
            if QMessageBox.question(
                    self, "OTA Upgrade",
                    "Are you sure to OTA upgrade from\n{}".format(current_url),
                    QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
                self.model.setData(
                    self.model.index(self.idx.row(), DevMdl.FIRMWARE),
                    "Upgrade in progress")
                self.mqtt.publish("{}/upgrade".format(
                    self.model.commandTopic(self.idx)),
                                  payload="1")

    def show_list_ctx_menu(self, at):
        self.select_device(self.device_list.indexAt(at))
        self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at))

    def build_header_ctx_menu(self):
        self.hdr_ctx_menu = QMenu()
        for c in columns.keys():
            a = self.hdr_ctx_menu.addAction(columns[c][0])
            a.setData(c)
            a.setCheckable(True)
            a.setChecked(not self.device_list.isColumnHidden(c))
            a.toggled.connect(self.header_ctx_menu_toggle_col)

    def show_header_ctx_menu(self, at):
        self.hdr_ctx_menu.popup(
            self.device_list.horizontalHeader().viewport().mapToGlobal(at))

    def header_ctx_menu_toggle_col(self, state):
        self.device_list.setColumnHidden(self.sender().data(), not state)
        hidden_columns = [
            int(c) for c in columns.keys()
            if self.device_list.isColumnHidden(c)
        ]
        self.settings.setValue("hidden_columns", hidden_columns)
        self.settings.sync()

    def select_device(self, idx):
        self.idx = self.sorted_device_model.mapToSource(idx)
        self.device = self.model.data(self.model.index(idx.row(),
                                                       DevMdl.TOPIC))

        relays = self.model.data(self.model.index(self.idx.row(),
                                                  DevMdl.POWER))
        if relays and len(relays.keys()) > 1:
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.setEnabled(True)
            self.ctx_menu_relays.clear()

            for r in relays.keys():
                actR = self.ctx_menu_relays.addAction("{} ON".format(r))
                actR.triggered.connect(
                    lambda st, x=r: self.ctx_menu_power(x, "ON"))
                actR = self.ctx_menu_relays.addAction("{} OFF".format(r))
                actR.triggered.connect(
                    lambda st, x=r: self.ctx_menu_power(x, "OFF"))
                self.ctx_menu_relays.addSeparator()
        else:
            self.ctx_menu_relays.setEnabled(False)
            self.ctx_menu_relays.clear()

    def device_config(self, idx=None):
        if self.idx:
            dev_cfg = DevicesConfigWidget(self, self.model.topic(self.idx))
            self.mdi.addSubWindow(dev_cfg)
            dev_cfg.setWindowState(Qt.WindowMaximized)

    def device_add(self):
        rc = self.model.rowCount()
        self.model.insertRow(rc)
        dlg = DeviceEditDialog(self.model, rc)
        dlg.full_topic.setText("%prefix%/%topic%/")

        if dlg.exec_() == QDialog.Accepted:
            self.model.setData(
                self.model.index(rc, DevMdl.FRIENDLY_NAME),
                self.model.data(self.model.index(rc, DevMdl.TOPIC)))
            topic = dlg.topic.text()
            tele_dev = self.telemetry_model.addDevice(TasmotaDevice, topic)
            self.telemetry_model.devices[topic] = tele_dev
        else:
            self.model.removeRow(rc)

    def device_delete(self):
        if self.idx:
            topic = self.model.topic(self.idx)
            if QMessageBox.question(
                    self, "Confirm",
                    "Do you want to remove '{}' from devices list?".format(
                        topic)) == QMessageBox.Yes:
                self.model.removeRows(self.idx.row(), 1)
                tele_idx = self.telemetry_model.devices.get(topic)
                if tele_idx:
                    self.telemetry_model.removeRows(tele_idx.row(), 1)

    def get_dump(self):
        self.backup += self.dl.readAll()

    def save_dump(self):
        fname = self.dl.header(QNetworkRequest.ContentDispositionHeader)
        if fname:
            fname = fname.split('=')[1]
            save_file = QFileDialog.getSaveFileName(
                self, "Save config backup",
                "{}/TDM/{}".format(QDir.homePath(), fname))[0]
            if save_file:
                with open(save_file, "wb") as f:
                    f.write(self.backup)

    def closeEvent(self, event):
        event.ignore()
예제 #24
0
파일: Toolbox.py 프로젝트: CPS-3/Cura
class Toolbox(QObject, Extension):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)

        self._application = Application.getInstance()
        self._package_manager = None
        self._plugin_registry = Application.getInstance().getPluginRegistry()
        self._packages_version = self._plugin_registry.APIVersion
        self._api_version = 1
        self._api_url = "https://api-staging.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version)

        # Network:
        self._get_packages_request = None
        self._get_showcase_request = None
        self._download_request = None
        self._download_reply = None
        self._download_progress = 0
        self._is_downloading = False
        self._network_manager = None
        self._request_header = [
            b"User-Agent",
            str.encode(
                "%s/%s (%s %s)" % (
                    Application.getInstance().getApplicationName(),
                    Application.getInstance().getVersion(),
                    platform.system(),
                    platform.machine(),
                )
            )
        ]
        self._request_urls = {
            "authors":            QUrl("{base_url}/authors".format(base_url = self._api_url)),
            "packages":           QUrl("{base_url}/packages".format(base_url = self._api_url)),
            "plugins_showcase":   QUrl("{base_url}/showcase".format(base_url = self._api_url)),
            "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url))
        }

        # Data:
        self._metadata = {
            "authors": [],
            "packages": [],
            "plugins_showcase": [],
            "plugins_installed": [],
            "materials_showcase": [],
            "materials_installed": []
        }

        # Models:
        self._models = {
            "authors": AuthorsModel(self),
            "packages": PackagesModel(self),
            "plugins_showcase": PackagesModel(self),
            "plugins_available": PackagesModel(self),
            "plugins_installed": PackagesModel(self),
            "materials_showcase": AuthorsModel(self),
            "materials_available": PackagesModel(self),
            "materials_installed": PackagesModel(self)
        }

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin"

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "loading"

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None

        self._dialog = None
        self._restart_required = False

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = ""
        self._license_dialog_license_content = ""
        self._license_dialog_plugin_file_location = ""
        self._restart_dialog_message = ""

        Application.getInstance().initializationFinished.connect(self._onAppInitialized)



    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()

    @pyqtSlot(result = str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result = str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result = str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._package_manager = Application.getInstance().getCuraPackageManager()

    @pyqtSlot()
    def browsePackages(self) -> None:
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")
        self._makeRequestByType("plugins_showcase")
        self._makeRequestByType("materials_showcase")

        # Gather installed packages:
        self._updateInstalledModels()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")
        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Toolbox: Creating dialog [%s].", qml_name)
        path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name)
        dialog = Application.getInstance().createQmlComponent(path, {"toolbox": self})
        return dialog

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            self._metadata["plugins_installed"] = all_packages["plugin"]
            self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._metadata["materials_installed"] = all_packages["material"]
            self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def uninstall(self, plugin_id: str) -> None:
        self._package_manager.removePackage(plugin_id)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify = metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify = restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self):
        self._package_manager._removeAllScheduledPackages()
        CuraApplication.getInstance().windowClosed()

    # Checks
    # --------------------------------------------------------------------------
    @pyqtSlot(str, result = bool)
    def canUpdate(self, package_id: str) -> bool:
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        remote_package = None
        for package in self._metadata["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
        if remote_package is None:
            return False

        local_version = local_package["package_version"]
        remote_version = remote_package["package_version"]
        return Version(remote_version) > Version(local_version)

    @pyqtSlot(str, result = bool)
    def isInstalled(self, package_id: str) -> bool:
        return self._package_manager.isPackageInstalled(package_id)

    @pyqtSlot(str, result = bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    def loadingComplete(self) -> bool:
        populated = 0
        for list in self._metadata.items():
            if len(list) > 0:
                populated += 1
        if populated == len(self._metadata.items()):
            return True
        return False

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, type: str) -> None:
        Logger.log("i", "Toolbox: Requesting %s metadata from server.", type)
        request = QNetworkRequest(self._request_urls[type])
        request.setRawHeader(*self._request_header)
        self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Toolbox: Attempting to download & install package from %s.", url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            self._download_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            self._download_request.setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
        self._download_request.setRawHeader(*self._request_header)
        self._download_reply = self._network_manager.get(self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        self._download_reply.downloadProgress.connect(self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "Toolbox: User cancelled the download of a plugin.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            self._download_reply.abort()
            self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(self, accessible: int) -> None:
        if accessible == 0:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:

        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for type, url in self._request_urls.items():
                if reply.url() == url:
                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                        # Check for errors:
                        if "errors" in json_data:
                            for error in json_data["errors"]:
                                Logger.log("e", "%s", error["title"])
                            return

                        # Create model and apply metadata:
                        if not self._models[type]:
                            Logger.log("e", "Could not find the %s model.", type)
                            break

                        # HACK: Eventually get rid of the code from here...
                        if type is "plugins_showcase" or type is "materials_showcase":
                            self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
                            self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
                            self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
                            self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
                        else:
                            # ...until here.
                            # This hack arises for multiple reasons but the main
                            # one is because there are not separate API calls
                            # for different kinds of showcases.
                            self._metadata[type] = json_data["data"]
                            self._models[type].setMetadata(self._metadata[type])

                        # Do some auto filtering
                        # TODO: Make multiple API calls in the future to handle this
                        if type is "packages":
                            self._models[type].setFilter({"type": "plugin"})
                        if type is "authors":
                            self._models[type].setFilter({"package_types": "material"})

                        self.metadataChanged.emit()

                        if self.loadingComplete() is True:
                            self.setViewPage("overview")

                        return
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Toolbox: Received invalid JSON for %s.", type)
                        break

        else:
            # Ignore any operation that is not a get operation
            pass

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(self._download_reply.readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str):
        Logger.log("i", "Toolbox: Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Toolbox: Package file [%s] was not a valid CuraPackage.", file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content, file_path)
            return

        self.install(file_path)
        return

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: Union[int, float]) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
    def downloadProgress(self) -> int:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        self._active_package = package
        self.activePackageChanged.emit()

    @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        self._view_category = category
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        self._view_page = page
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewPage, notify = viewChanged)
    def viewPage(self) -> str:
        return self._view_page



    # Expose Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, notify = metadataChanged)
    def authorsModel(self) -> AuthorsModel:
        return self._models["authors"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def packagesModel(self) -> PackagesModel:
        return self._models["packages"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return self._models["plugins_showcase"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsInstalledModel(self) -> PackagesModel:
        return self._models["plugins_installed"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsShowcaseModel(self) -> PackagesModel:
        return self._models["materials_showcase"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsInstalledModel(self) -> PackagesModel:
        return self._models["materials_installed"]



    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, modelType: str, filterType: str, parameter: str):
        if not self._models[modelType]:
            Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", modelType)
            return
        self._models[modelType].setFilter({ filterType: parameter })
        self.filterChanged.emit()

    @pyqtSlot()
    def removeFilters(self, modelType: str):
        if not self._models[modelType]:
            Logger.log("w", "Toolbox: Couldn't remove filters on %s model because it doesn't exist.", modelType)
            return
        self._models[modelType].setFilter({})
        self.filterChanged.emit()
예제 #25
0
파일: qpdfview.py 프로젝트: snuffop/scripts
class PDFWidget(QLabel):
    """
    A widget showing one page of a PDF.
    If you want to show multiple pages of the same PDF,
    make sure you share the document (let the first PDFWidget
    create the document, then pass thatPDFwidget.document to any
    subsequent widgets you create) or use a ScrolledPDFWidget.
    Will try to resize to a reasonable size to fit inside the
    geometry passed in (typically the screen size, a PyQt5.QtCore.QRect);
    or specify dpi explicitly.
    """
    def __init__(self,
                 url,
                 document=None,
                 pageno=1,
                 dpi=None,
                 geometry=None,
                 parent=None,
                 load_cb=None):
        """
           load_cb: will be called when the document is loaded.
        """
        super(PDFWidget, self).__init__(parent)

        self.geometry = geometry

        # Guess at initial size: will be overridden later.
        if geometry:
            self.winwidth = geometry.height() * .75
            self.winheight = geometry.height()
        else:
            self.geometry = PyQt5.QtCore.QSize(600, 800)
            self.winwidth = 600
            self.winheight = 800

        self.filename = url

        self.load_cb = load_cb

        self.network_manager = None

        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        if not document:
            self.document = None
            if url:
                self.start_load(url)
        else:
            self.document = document
        self.page = None
        self.pagesize = QSize(self.winwidth, self.winheight)

        self.dpi = dpi

        # Poppler page numbering starts from 0 but that's not what
        # most PDF users will expect, so subtract:
        if pageno > 0:
            pageno -= 1
        self.pageno = pageno

        self.render()

    def sizeHint(self):
        if not self.page:
            if not self.document:
                return QSize(self.winwidth, self.winheight)
            self.page = self.document.page(self.pageno)

        if not self.pagesize:
            self.pagesize = self.page.pageSize()

        return self.pagesize

    def render(self):
        """Render to a pixmap at the current DPI setting.
        """
        if not self.document:
            return

        if not self.page:
            self.page = self.document.page(self.pageno)
            self.pagesize = self.page.pageSize()

            self.document.setRenderHint(Poppler.Document.TextAntialiasing)
            # self.document.setRenderHint(Poppler.Document.TextHinting)

        if not self.dpi:
            # Probably first time here.
            # self.pagesize is sized in pixels assuming POINTS_PER_INCH;
            # adjust that so the page barely fits on in self.geometry.

            # First assume that it's portrait aspect ratio and that
            # vertical size will be the limiting factor.
            self.dpi = POINTS_PER_INCH * \
                self.geometry.height() / self.pagesize.height()

            # Was that too much: will it overflow in width?
            if self.pagesize.width() * self.dpi / POINTS_PER_INCH \
               > self.geometry.width():
                self.dpi = POINTS_PER_INCH * \
                    self.geometry.width() / self.pagesize.width()

            self.winwidth = self.pagesize.width() * self.dpi / POINTS_PER_INCH
            self.winheight = self.pagesize.height(
            ) * self.dpi / POINTS_PER_INCH

            # Most Qt5 programs seem to use setGeometry(x, y, w, h)
            # to set initial window size. resize() is the only method I've
            # found that doesn't force initial position as well as size.
            self.resize(self.winwidth, self.winheight)

        self.setWindowTitle('PDF Viewer')

        img = self.page.renderToImage(self.dpi, self.dpi)
        self.pixmap = QPixmap.fromImage(img)
        self.setPixmap(self.pixmap)

    def start_load(self, url):
        """Create a Poppler.Document from the given URL, QUrl or filename.
           Return, then asynchronously call self.load_cb.
        """

        # If it's not a local file, we'll need to load it.
        # http://doc.qt.io/qt-5/qnetworkaccessmanager.html
        qurl = QUrl(url)
        if not qurl.scheme():
            qurl = QUrl.fromLocalFile(url)
        if not self.network_manager:
            self.network_manager = QNetworkAccessManager()
        self.network_manager.finished.connect(self.download_finished)
        self.network_manager.get(QNetworkRequest(qurl))

    def download_finished(self, network_reply):
        qbytes = network_reply.readAll()
        self.document = Poppler.Document.loadFromData(qbytes)

        self.render()

        if self.load_cb:
            self.load_cb()
예제 #26
0
class SearchThread(QThread):
    domain = 'kyfw.12306.cn' #请求域名(真实连接地址)
    host='kyfw.12306.cn' #请求的域名(host)
    http = requests.session()
    stopSignal=False
    threadId=1
    leftTicketUrl="leftTicket/query"
    requests.packages.urllib3.disable_warnings()

    searchThreadCallback= pyqtSignal(list)

    def __init__(self,from_station,to_station,train_date,threadId,leftTicketUrl,interval=2,domain=''):
        super(SearchThread,self).__init__()
        if domain!='':
            self.domain=domain

        self.threadId=threadId
        self.from_station=from_station
        self.to_station=to_station
        self.train_date=train_date
        self.interval=interval
        self.leftTicketUrl=leftTicketUrl


    def run(self):
        time.sleep(self.threadId)

        userAgent="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36"
        headers={'Referer':'https://kyfw.12306.cn/otn/leftTicket/init',"host":self.host\
            ,'Cache-Control':'no-cache','Pragma':"no-cache","User-Agent":userAgent,"X-Requested-With":"XMLHttpRequest"}

        t=str(random.random())

        dataUrl='?leftTicketDTO.train_date='+self.train_date\
            +"&leftTicketDTO.from_station="+self.stationCode[self.from_station]+"&leftTicketDTO.to_station="+\
            self.stationCode[self.to_station]+"&purpose_codes=ADULT"

        logUrl='https://' + self.domain + '/otn/leftTicket/log'+dataUrl
        url='https://' + self.domain + '/otn/'+self.leftTicketUrl+dataUrl


        self.http.get(logUrl,verify=False,headers=headers)
        jc_fromStation=xxtea.unicodeStr(self.from_station+","+self.stationCode[self.from_station])
        jc_toStation=xxtea.unicodeStr(self.to_station+","+self.stationCode[self.to_station])
        self.http.cookies.set("_jc_save_fromStation",jc_fromStation)
        self.http.cookies.set("_jc_save_toStation",jc_toStation)
        self.http.cookies.set('_jc_save_fromDate',self.train_date)
        self.http.cookies.set('_jc_save_toDate',"2014-01-01")
        self.http.cookies.set('_jc_save_wfdc_flag','dc')

        ret=self.http.get(url,verify=False,headers=headers)
        ticketInfo=ret.json()

        if ticketInfo['status']!=True :
            print(ticketInfo)

        cookies=self.http.cookies.get_dict()
        cookieStr=";".join('%s=%s' % (key, value) for (key, value) in cookies.items())

        self.http.get(logUrl,verify=False,headers=headers)
        self.req=QNetworkRequest()
        self.req.setUrl(QUrl(url))
        self.req.setRawHeader("Referer","https://kyfw.12306.cn/otn/leftTicket/init")
        self.req.setRawHeader("host",self.host)
        self.req.setRawHeader("Cache-Control","no-cache")
        self.req.setRawHeader("Pragma","no-cache")
        self.req.setRawHeader("User-Agent",userAgent)
        self.req.setRawHeader("Cookie",cookieStr)

        while not self.stopSignal:
            mutex.acquire(1)
            self.search_ticket(self.from_station,self.to_station,self.train_date)
            mutex.release(1)
            time.sleep(self.interval)


    def search_ticket(self, fromStation, toStation, date):
        try:
            self.netWorkManager=QNetworkAccessManager()
            self.reply=self.netWorkManager.get(self.req)
            self.reply.ignoreSslErrors()
            self.reply.finished.connect(self.search_finished)
            self.exec()

        except Exception as e:
            print("ip:"+self.domain+"查询发生错误:"+e.__str__())
            return False



    def search_finished(self):
        try:
            ret=self.reply.readAll()
            ret=str(ret,'utf8')
            ticketInfo=json.loads(ret)
            self.reply=None
            self.netWorkManager=None
            self.exit()
            if ticketInfo['status']!=True or ticketInfo['messages']!=[] :
                print(self.domain)
                print(ticketInfo)
                return False

            if len(ticketInfo['data'])<=0:
                return False

            data=ticketInfo['data']
            ret=None
            ticketInfo=None

            self.searchThreadCallback.emit(data)
        except Exception as e:
            print(e.__str__())

    def load_station_code(self,stationCode):
        self.stationCode = stationCode
        return True

    def stop(self):
        self.stopSignal=True
예제 #27
0
class DiscoverOctoPrintAction(MachineAction):
    def __init__(self, parent: QObject = None) -> None:
        super().__init__("DiscoverOctoPrintAction",
                         catalog.i18nc("@action", "Connect OctoPrint"))

        self._qml_url = os.path.join("qml", "DiscoverOctoPrintAction.qml")

        self._application = CuraApplication.getInstance()
        self._network_plugin = None  # type: Optional[OctoPrintOutputDevicePlugin]

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)

        self._settings_reply = None  # type: Optional[QNetworkReply]
        self._settings_reply_timeout = None  # type: Optional[NetworkReplyTimeout]

        self._instance_supports_appkeys = False
        self._appkey_reply = None  # type: Optional[QNetworkReply]
        self._appkey_request = None  # type: Optional[QNetworkRequest]
        self._appkey_instance_id = ""

        self._appkey_poll_timer = QTimer()
        self._appkey_poll_timer.setInterval(500)
        self._appkey_poll_timer.setSingleShot(True)
        self._appkey_poll_timer.timeout.connect(self._pollApiKey)

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                self._plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            self._plugin_version = "0.0"
            Logger.logException(
                "w", "Could not get version information for the plugin")

        self._user_agent = ("%s/%s %s/%s" %
                            (self._application.getApplicationName(),
                             self._application.getVersion(), "OctoPrintPlugin",
                             self._plugin_version)).encode()

        self._settings_instance = None  # type: Optional[OctoPrintOutputDevice]

        self._instance_responded = False
        self._instance_in_error = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self._instance_installed_plugins = []  # type: List[str]

        self._power_plugins_manager = PowerPlugins()

        # Load keys cache from preferences
        self._preferences = self._application.getPreferences()
        self._preferences.addPreference("octoprint/keys_cache", "")

        try:
            self._keys_cache = json.loads(
                self._deobfuscateString(
                    self._preferences.getValue("octoprint/keys_cache")))
        except ValueError:
            self._keys_cache = {}  # type: Dict[str, Any]
        if not isinstance(self._keys_cache, dict):
            self._keys_cache = {}  # type: Dict[str, Any]

        self._additional_components = None  # type:Optional[QObject]

        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerAdded)
        self._application.engineCreatedSignal.connect(
            self._createAdditionalComponentsView)

    @pyqtProperty(str, constant=True)
    def pluginVersion(self) -> str:
        return self._plugin_version

    @pyqtSlot()
    def startDiscovery(self) -> None:
        if not self._plugin_id:
            return
        if not self._network_plugin:
            self._network_plugin = cast(
                OctoPrintOutputDevicePlugin,
                self._application.getOutputDeviceManager().
                getOutputDevicePlugin(self._plugin_id))
            if not self._network_plugin:
                return
            self._network_plugin.addInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.removeInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.instanceListChanged.connect(
                self._onInstanceDiscovery)
            self.instancesChanged.emit()
        else:
            # Restart bonjour discovery
            self._network_plugin.startDiscovery()

    def _onInstanceDiscovery(self, *args) -> None:
        self.instancesChanged.emit()

    @pyqtSlot(str)
    def removeManualInstance(self, name: str) -> None:
        if not self._network_plugin:
            return

        self._network_plugin.removeManualInstance(name)

    @pyqtSlot(str, str, int, str, bool, str, str)
    def setManualInstance(self,
                          name: str,
                          address: str,
                          port: int,
                          path: str,
                          useHttps: bool,
                          userName: str = "",
                          password: str = "") -> None:
        if not self._network_plugin:
            return

        # This manual printer could replace a current manual printer
        self._network_plugin.removeManualInstance(name)

        self._network_plugin.addManualInstance(name, address, port, path,
                                               useHttps, userName, password)

    def _onContainerAdded(self, container: "ContainerInterface") -> None:
        # Add this action as a supported action to all machine definitions
        if (isinstance(container, DefinitionContainer)
                and container.getMetaDataEntry("type") == "machine"
                and container.getMetaDataEntry("supports_usb_connection")):

            self._application.getMachineActionManager().addSupportedAction(
                container.getId(), self.getKey())

    instancesChanged = pyqtSignal()
    appKeysSupportedChanged = pyqtSignal()
    appKeyReceived = pyqtSignal()
    instanceIdChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=instancesChanged)
    def discoveredInstances(self) -> List[Any]:
        if self._network_plugin:
            instances = list(self._network_plugin.getInstances().values())
            instances.sort(key=lambda k: k.name)
            return instances
        else:
            return []

    @pyqtSlot(str)
    def setInstanceId(self, key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if global_container_stack:
            global_container_stack.setMetaDataEntry("octoprint_id", key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

        self.instanceIdChanged.emit()

    @pyqtProperty(str, notify=instanceIdChanged)
    def instanceId(self) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        return global_container_stack.getMetaDataEntry("octoprint_id", "")

    @pyqtSlot(str)
    def requestApiKey(self, instance_id: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url:
            return

        ## Request appkey
        self._appkey_instance_id = instance_id
        self._appkey_request = self._createRequest(
            QUrl(base_url + "plugin/appkeys/request"), basic_auth_username,
            basic_auth_password)
        self._appkey_request.setRawHeader(b"Content-Type", b"application/json")
        data = json.dumps({"app": "Cura"})
        self._appkey_reply = self._network_manager.post(
            self._appkey_request, data.encode())

    @pyqtSlot()
    def cancelApiKeyRequest(self) -> None:
        if self._appkey_reply:
            if self._appkey_reply.isRunning():
                self._appkey_reply.abort()
            self._appkey_reply = None

        self._appkey_request = None  # type: Optional[QNetworkRequest]

        self._appkey_poll_timer.stop()

    def _pollApiKey(self) -> None:
        if not self._appkey_request:
            return
        self._appkey_reply = self._network_manager.get(self._appkey_request)

    @pyqtSlot(str)
    def probeAppKeySupport(self, instance_id: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url or not instance:
            return

        instance.getAdditionalData()

        self._instance_supports_appkeys = False
        self.appKeysSupportedChanged.emit()

        appkey_probe_request = self._createRequest(
            QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username,
            basic_auth_password)
        self._appkey_reply = self._network_manager.get(appkey_probe_request)

    @pyqtSlot(str, str)
    def testApiKey(self, instance_id: str, api_key: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url:
            return

        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self._instance_installed_plugins = []  # type: List[str]
        self.selectedInstanceSettingsChanged.emit()

        if self._settings_reply:
            if self._settings_reply.isRunning():
                self._settings_reply.abort()
            self._settings_reply = None
        if self._settings_reply_timeout:
            self._settings_reply_timeout = None

        if api_key != "":
            Logger.log(
                "d",
                "Trying to access OctoPrint instance at %s with the provided API key."
                % base_url)

            ## Request 'settings' dump
            settings_request = self._createRequest(
                QUrl(base_url + "api/settings"), basic_auth_username,
                basic_auth_password)
            settings_request.setRawHeader(b"X-Api-Key", api_key.encode())
            self._settings_reply = self._network_manager.get(settings_request)
            self._settings_reply_timeout = NetworkReplyTimeout(
                self._settings_reply, 5000, self._onRequestFailed)

            self._settings_instance = instance

    @pyqtSlot(str)
    def setApiKey(self, api_key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        global_container_stack.setMetaDataEntry(
            "octoprint_api_key",
            base64.b64encode(api_key.encode("ascii")).decode("ascii"))

        self._keys_cache[self.instanceId] = api_key
        keys_cache = base64.b64encode(
            json.dumps(self._keys_cache).encode("ascii")).decode("ascii")
        self._preferences.setValue("octoprint/keys_cache", keys_cache)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    ##  Get the stored API key of an instance, or the one stored in the machine instance
    #   \return key String containing the key of the machine.
    @pyqtSlot(str, result=str)
    def getApiKey(self, instance_id: str) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        if instance_id == self.instanceId:
            api_key = self._deobfuscateString(
                global_container_stack.getMetaDataEntry(
                    "octoprint_api_key", ""))
        else:
            api_key = self._keys_cache.get(instance_id, "")

        return api_key

    selectedInstanceSettingsChanged = pyqtSignal()

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceResponded(self) -> bool:
        return self._instance_responded

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceInError(self) -> bool:
        return self._instance_in_error

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceApiKeyAccepted(self) -> bool:
        return self._instance_api_key_accepted

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsSd(self) -> bool:
        return self._instance_supports_sd

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsCamera(self) -> bool:
        return self._instance_supports_camera

    @pyqtProperty("QStringList", notify=selectedInstanceSettingsChanged)
    def instanceInstalledPlugins(self) -> List[str]:
        return self._instance_installed_plugins

    @pyqtProperty("QVariantList", notify=selectedInstanceSettingsChanged)
    def instanceAvailablePowerPlugins(self) -> List[Dict[str, str]]:
        available_plugins = self._power_plugins_manager.getAvailablePowerPlugs(
        )
        return [{
            "key": plug_id,
            "text": plug_data["name"]
        } for (plug_id, plug_data) in available_plugins.items()]

    @pyqtProperty(bool, notify=appKeysSupportedChanged)
    def instanceSupportsAppKeys(self) -> bool:
        return self._instance_supports_appkeys

    @pyqtSlot(str, str, str)
    def setContainerMetaDataEntry(self, container_id: str, key: str,
                                  value: str) -> None:
        containers = ContainerRegistry.getInstance().findContainers(
            id=container_id)
        if not containers:
            Logger.log(
                "w",
                "Could not set metadata of container %s because it was not found.",
                container_id)
            return

        containers[0].setMetaDataEntry(key, value)

    @pyqtSlot(bool)
    def applyGcodeFlavorFix(self, apply_fix: bool) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode"
        if global_container_stack.getProperty("machine_gcode_flavor",
                                              "value") == gcode_flavor:
            # No need to add a definition_changes container if the setting is not going to be changed
            return

        # Make sure there is a definition_changes container to store the machine settings
        definition_changes_container = global_container_stack.definitionChanges
        if definition_changes_container == ContainerRegistry.getInstance(
        ).getEmptyInstanceContainer():
            definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
                global_container_stack,
                global_container_stack.getId() + "_settings")

        definition_changes_container.setProperty("machine_gcode_flavor",
                                                 "value", gcode_flavor)

        # Update the has_materials metadata flag after switching gcode flavor
        definition = global_container_stack.getBottom()
        if (not definition or definition.getProperty("machine_gcode_flavor",
                                                     "value") != "UltiGCode"
                or definition.getMetaDataEntry("has_materials", False)):

            # In other words: only continue for the UM2 (extended), but not for the UM2+
            return

        has_materials = global_container_stack.getProperty(
            "machine_gcode_flavor", "value") != "UltiGCode"

        material_container = global_container_stack.material

        if has_materials:
            global_container_stack.setMetaDataEntry("has_materials", True)

            # Set the material container to a sane default
            if material_container == ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer():
                search_criteria = {
                    "type":
                    "material",
                    "definition":
                    "fdmprinter",
                    "id":
                    global_container_stack.getMetaDataEntry(
                        "preferred_material")
                }
                materials = ContainerRegistry.getInstance(
                ).findInstanceContainers(**search_criteria)
                if materials:
                    global_container_stack.material = materials[0]
        else:
            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.removeMetaDataEntry("has_materials")

            global_container_stack.material = ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer()

        self._application.globalContainerStackChanged.emit()

    @pyqtSlot(str)
    def openWebPage(self, url: str) -> None:
        QDesktopServices.openUrl(QUrl(url))

    def _createAdditionalComponentsView(self) -> None:
        Logger.log(
            "d",
            "Creating additional ui components for OctoPrint-connected printers."
        )

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qml",
                            "OctoPrintComponents.qml")
        self._additional_components = self._application.createQmlComponent(
            path, {"manager": self})
        if not self._additional_components:
            Logger.log(
                "w",
                "Could not create additional components for OctoPrint-connected printers."
            )
            return

        self._application.addAdditionalComponent(
            "monitorButtons",
            self._additional_components.findChild(QObject,
                                                  "openOctoPrintButton"))

    def _onRequestFailed(self, reply: QNetworkReply) -> None:
        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                Logger.log(
                    "w",
                    "Connection refused or timeout when trying to access OctoPrint at %s"
                    % reply.url().toString())
                self._instance_in_error = True
                self.selectedInstanceSettingsChanged.emit()

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply: QNetworkReply) -> None:

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            self._onRequestFailed(reply)
            return

        if reply.operation() == QNetworkAccessManager.PostOperation:
            if "/plugin/appkeys/request" in reply.url().toString(
            ):  # Initial AppKey request
                if http_status_code == 201 or http_status_code == 202:
                    Logger.log("w", "Start polling for AppKeys decision")
                    if not self._appkey_request:
                        return
                    self._appkey_request.setUrl(
                        reply.header(QNetworkRequest.LocationHeader))
                    self._appkey_request.setRawHeader(b"Content-Type", b"")
                    self._appkey_poll_timer.start()
                elif http_status_code == 404:
                    Logger.log(
                        "w",
                        "This instance of OctoPrint does not support AppKeys")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log(
                        "w",
                        "Unknown response when requesting an AppKey: %d. OctoPrint said %s"
                        % (http_status_code, response))
                    self._appkey_request = None  # type: Optional[QNetworkRequest]

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "/plugin/appkeys/probe" in reply.url().toString(
            ):  # Probe for AppKey support
                if http_status_code == 204:
                    self._instance_supports_appkeys = True
                else:
                    self._instance_supports_appkeys = False
                self.appKeysSupportedChanged.emit()

            if "/plugin/appkeys/request" in reply.url().toString(
            ):  # Periodic AppKey request poll
                if http_status_code == 202:
                    self._appkey_poll_timer.start()
                elif http_status_code == 200:
                    Logger.log("d", "AppKey granted")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        return

                    api_key = json_data["api_key"]
                    self._keys_cache[self._appkey_instance_id] = api_key

                    global_container_stack = self._application.getGlobalContainerStack(
                    )
                    if global_container_stack:
                        global_container_stack.setMetaDataEntry(
                            "octoprint_api_key",
                            base64.b64encode(
                                api_key.encode("ascii")).decode("ascii"))

                    self.appKeyReceived.emit()
                elif http_status_code == 404:
                    Logger.log("d", "AppKey denied")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log(
                        "w",
                        "Unknown response when waiting for an AppKey: %d. OctoPrint said %s"
                        % (http_status_code, response))
                    self._appkey_request = None  # type: Optional[QNetworkRequest]

            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                self._instance_in_error = False

                if http_status_code == 200:
                    Logger.log("d", "API key accepted by OctoPrint.")
                    self._instance_api_key_accepted = True

                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data[
                            "feature"]:
                        self._instance_supports_sd = json_data["feature"][
                            "sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data[
                            "webcam"]:
                        stream_url = json_data["webcam"]["streamUrl"]
                        if stream_url:  #not empty string or None
                            self._instance_supports_camera = True

                    if "plugins" in json_data:
                        self._power_plugins_manager.parsePluginData(
                            json_data["plugins"])
                        self._instance_installed_plugins = list(
                            json_data["plugins"].keys())

                    api_key = bytes(reply.request().rawHeader(
                        b"X-Api-Key")).decode("utf-8")
                    self.setApiKey(api_key)  # store api key in key cache
                    if self._settings_instance:
                        self._settings_instance.setApiKey(api_key)
                        self._settings_instance.resetOctoPrintUserName()
                        self._settings_instance.getAdditionalData()
                        self._settings_instance.parseSettingsData(json_data)

                    self._settings_instance = None

                elif http_status_code == 401:
                    Logger.log("d", "Invalid API key for OctoPrint.")
                    self._instance_api_key_accepted = False

                elif http_status_code == 502 or http_status_code == 503:
                    Logger.log("d", "OctoPrint is not running.")
                    self._instance_api_key_accepted = False
                    self._instance_in_error = True

                self._instance_responded = True
                self.selectedInstanceSettingsChanged.emit()

    def _createRequest(self,
                       url: str,
                       basic_auth_username: str = "",
                       basic_auth_password: str = "") -> QNetworkRequest:
        request = QNetworkRequest(url)
        request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        request.setRawHeader(b"User-Agent", self._user_agent)

        if basic_auth_username and basic_auth_password:
            data = base64.b64encode(
                ("%s:%s" % (basic_auth_username,
                            basic_auth_password)).encode()).decode("utf-8")
            request.setRawHeader(b"Authorization",
                                 ("Basic %s" % data).encode())

        # ignore SSL errors (eg for self-signed certificates)
        ssl_configuration = QSslConfiguration.defaultConfiguration()
        ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone)
        request.setSslConfiguration(ssl_configuration)

        return request

    ##  Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before
    def _deobfuscateString(self, source: str) -> str:
        try:
            return base64.b64decode(source.encode("ascii")).decode("ascii")
        except UnicodeDecodeError:
            return source

    def _getInstanceInfo(
        self, instance_id: str
    ) -> Tuple[Optional[OctoPrintOutputDevice], str, str, str]:
        if not self._network_plugin:
            return (None, "", "", "")
        instance = self._network_plugin.getInstanceById(instance_id)
        if not instance:
            return (None, "", "", "")

        return (instance, instance.baseURL, instance.getProperty("userName"),
                instance.getProperty("password"))
예제 #28
0
class PluginRepositoryWidget(QWidget, Ui_PluginRepositoryDialog):
    """
    Class implementing a dialog showing the available plugins.
    
    @signal closeAndInstall() emitted when the Close & Install button is
        pressed
    """
    closeAndInstall = pyqtSignal()
    
    DescrRole = Qt.UserRole
    UrlRole = Qt.UserRole + 1
    FilenameRole = Qt.UserRole + 2
    AuthorRole = Qt.UserRole + 3

    PluginStatusUpToDate = 0
    PluginStatusNew = 1
    PluginStatusLocalUpdate = 2
    PluginStatusRemoteUpdate = 3
    
    def __init__(self, parent=None, external=False):
        """
        Constructor
        
        @param parent parent of this dialog (QWidget)
        @param external flag indicating an instatiation as a main
            window (boolean)
        """
        super(PluginRepositoryWidget, self).__init__(parent)
        self.setupUi(self)
        
        self.__updateButton = self.buttonBox.addButton(
            self.tr("Update"), QDialogButtonBox.ActionRole)
        self.__downloadButton = self.buttonBox.addButton(
            self.tr("Download"), QDialogButtonBox.ActionRole)
        self.__downloadButton.setEnabled(False)
        self.__downloadInstallButton = self.buttonBox.addButton(
            self.tr("Download && Install"),
            QDialogButtonBox.ActionRole)
        self.__downloadInstallButton.setEnabled(False)
        self.__downloadCancelButton = self.buttonBox.addButton(
            self.tr("Cancel"), QDialogButtonBox.ActionRole)
        self.__installButton = \
            self.buttonBox.addButton(self.tr("Close && Install"),
                                     QDialogButtonBox.ActionRole)
        self.__downloadCancelButton.setEnabled(False)
        self.__installButton.setEnabled(False)
        
        self.repositoryUrlEdit.setText(
            Preferences.getUI("PluginRepositoryUrl6"))
        
        self.repositoryList.headerItem().setText(
            self.repositoryList.columnCount(), "")
        self.repositoryList.header().setSortIndicator(0, Qt.AscendingOrder)
        
        self.__pluginContextMenu = QMenu(self)
        self.__hideAct = self.__pluginContextMenu.addAction(
            self.tr("Hide"), self.__hidePlugin)
        self.__hideSelectedAct = self.__pluginContextMenu.addAction(
            self.tr("Hide Selected"), self.__hideSelectedPlugins)
        self.__pluginContextMenu.addSeparator()
        self.__showAllAct = self.__pluginContextMenu.addAction(
            self.tr("Show All"), self.__showAllPlugins)
        self.__pluginContextMenu.addSeparator()
        self.__pluginContextMenu.addAction(
            self.tr("Cleanup Downloads"), self.__cleanupDownloads)
        
        self.pluginRepositoryFile = \
            os.path.join(Utilities.getConfigDir(), "PluginRepository")
        
        self.__external = external
        
        # attributes for the network objects
        self.__networkManager = QNetworkAccessManager(self)
        self.__networkManager.proxyAuthenticationRequired.connect(
            proxyAuthenticationRequired)
        if SSL_AVAILABLE:
            self.__sslErrorHandler = E5SslErrorHandler(self)
            self.__networkManager.sslErrors.connect(self.__sslErrors)
        self.__replies = []
        
        self.__networkConfigurationManager = QNetworkConfigurationManager(self)
        self.__onlineStateChanged(
            self.__networkConfigurationManager.isOnline())
        
        self.__networkConfigurationManager.onlineStateChanged.connect(
            self.__onlineStateChanged)
        
        self.__doneMethod = None
        self.__inDownload = False
        self.__pluginsToDownload = []
        self.__pluginsDownloaded = []
        self.__isDownloadInstall = False
        self.__allDownloadedOk = False
        
        self.__hiddenPlugins = Preferences.getPluginManager("HiddenPlugins")
        
        self.__populateList()
    
    @pyqtSlot(bool)
    def __onlineStateChanged(self, online):
        """
        Private slot handling online state changes.
        
        @param online flag indicating the online status
        @type bool
        """
        self.__updateButton.setEnabled(online)
        self.on_repositoryList_itemSelectionChanged()
        if online:
            msg = self.tr("Network Status: online")
        else:
            msg = self.tr("Network Status: offline")
        self.statusLabel.setText(msg)
    
    @pyqtSlot(QAbstractButton)
    def on_buttonBox_clicked(self, button):
        """
        Private slot to handle the click of a button of the button box.
        
        @param button reference to the button pressed (QAbstractButton)
        """
        if button == self.__updateButton:
            self.__updateList()
        elif button == self.__downloadButton:
            self.__isDownloadInstall = False
            self.__downloadPlugins()
        elif button == self.__downloadInstallButton:
            self.__isDownloadInstall = True
            self.__allDownloadedOk = True
            self.__downloadPlugins()
        elif button == self.__downloadCancelButton:
            self.__downloadCancel()
        elif button == self.__installButton:
            self.__closeAndInstall()
    
    def __formatDescription(self, lines):
        """
        Private method to format the description.
        
        @param lines lines of the description (list of strings)
        @return formatted description (string)
        """
        # remove empty line at start and end
        newlines = lines[:]
        if len(newlines) and newlines[0] == '':
            del newlines[0]
        if len(newlines) and newlines[-1] == '':
            del newlines[-1]
        
        # replace empty lines by newline character
        index = 0
        while index < len(newlines):
            if newlines[index] == '':
                newlines[index] = '\n'
            index += 1
        
        # join lines by a blank
        return ' '.join(newlines)
    
    @pyqtSlot(QPoint)
    def on_repositoryList_customContextMenuRequested(self, pos):
        """
        Private slot to show the context menu.
        
        @param pos position to show the menu (QPoint)
        """
        self.__hideAct.setEnabled(
            self.repositoryList.currentItem() is not None and
            len(self.__selectedItems()) == 1)
        self.__hideSelectedAct.setEnabled(
            len(self.__selectedItems()) > 1)
        self.__showAllAct.setEnabled(bool(self.__hasHiddenPlugins()))
        self.__pluginContextMenu.popup(self.repositoryList.mapToGlobal(pos))
    
    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
    def on_repositoryList_currentItemChanged(self, current, previous):
        """
        Private slot to handle the change of the current item.
        
        @param current reference to the new current item (QTreeWidgetItem)
        @param previous reference to the old current item (QTreeWidgetItem)
        """
        if self.__repositoryMissing or current is None:
            return
        
        self.urlEdit.setText(
            current.data(0, PluginRepositoryWidget.UrlRole) or "")
        self.descriptionEdit.setPlainText(
            current.data(0, PluginRepositoryWidget.DescrRole) and
            self.__formatDescription(
                current.data(0, PluginRepositoryWidget.DescrRole)) or "")
        self.authorEdit.setText(
            current.data(0, PluginRepositoryWidget.AuthorRole) or "")
    
    def __selectedItems(self):
        """
        Private method to get all selected items without the toplevel ones.
        
        @return list of selected items (list)
        """
        ql = self.repositoryList.selectedItems()
        for index in range(self.repositoryList.topLevelItemCount()):
            ti = self.repositoryList.topLevelItem(index)
            if ti in ql:
                ql.remove(ti)
        return ql
    
    @pyqtSlot()
    def on_repositoryList_itemSelectionChanged(self):
        """
        Private slot to handle a change of the selection.
        """
        self.__downloadButton.setEnabled(
            len(self.__selectedItems()) and
            self.__networkConfigurationManager.isOnline())
        self.__downloadInstallButton.setEnabled(
            len(self.__selectedItems()) and
            self.__networkConfigurationManager.isOnline())
        self.__installButton.setEnabled(len(self.__selectedItems()))
    
    def __updateList(self):
        """
        Private slot to download a new list and display the contents.
        """
        url = self.repositoryUrlEdit.text()
        self.__downloadFile(url,
                            self.pluginRepositoryFile,
                            self.__downloadRepositoryFileDone)
    
    def __downloadRepositoryFileDone(self, status, filename):
        """
        Private method called after the repository file was downloaded.
        
        @param status flaging indicating a successful download (boolean)
        @param filename full path of the downloaded file (string)
        """
        self.__populateList()
    
    def __downloadPluginDone(self, status, filename):
        """
        Private method called, when the download of a plugin is finished.
        
        @param status flag indicating a successful download (boolean)
        @param filename full path of the downloaded file (string)
        """
        if status:
            self.__pluginsDownloaded.append(filename)
        if self.__isDownloadInstall:
            self.__allDownloadedOk &= status
        
        del self.__pluginsToDownload[0]
        if len(self.__pluginsToDownload):
            self.__downloadPlugin()
        else:
            self.__downloadPluginsDone()
    
    def __downloadPlugin(self):
        """
        Private method to download the next plugin.
        """
        self.__downloadFile(self.__pluginsToDownload[0][0],
                            self.__pluginsToDownload[0][1],
                            self.__downloadPluginDone)
    
    def __downloadPlugins(self):
        """
        Private slot to download the selected plugins.
        """
        self.__pluginsDownloaded = []
        self.__pluginsToDownload = []
        self.__downloadButton.setEnabled(False)
        self.__downloadInstallButton.setEnabled(False)
        self.__installButton.setEnabled(False)
        for itm in self.repositoryList.selectedItems():
            if itm not in [self.__stableItem, self.__unstableItem,
                           self.__unknownItem]:
                url = itm.data(0, PluginRepositoryWidget.UrlRole)
                filename = os.path.join(
                    Preferences.getPluginManager("DownloadPath"),
                    itm.data(0, PluginRepositoryWidget.FilenameRole))
                self.__pluginsToDownload.append((url, filename))
        self.__downloadPlugin()
    
    def __downloadPluginsDone(self):
        """
        Private method called, when the download of the plugins is finished.
        """
        self.__downloadButton.setEnabled(len(self.__selectedItems()))
        self.__downloadInstallButton.setEnabled(len(self.__selectedItems()))
        self.__installButton.setEnabled(True)
        self.__doneMethod = None
        if not self.__external:
            ui = e5App().getObject("UserInterface")
        else:
            ui = None
        if ui and ui.notificationsEnabled():
            ui.showNotification(
                UI.PixmapCache.getPixmap("plugin48.png"),
                self.tr("Download Plugin Files"),
                self.tr("""The requested plugins were downloaded."""))
        
        if self.__isDownloadInstall:
            self.closeAndInstall.emit()
        else:
            if ui is None or not ui.notificationsEnabled():
                E5MessageBox.information(
                    self,
                    self.tr("Download Plugin Files"),
                    self.tr("""The requested plugins were downloaded."""))
            self.downloadProgress.setValue(0)
            
            # repopulate the list to update the refresh icons
            self.__populateList()
    
    def __resortRepositoryList(self):
        """
        Private method to resort the tree.
        """
        self.repositoryList.sortItems(
            self.repositoryList.sortColumn(),
            self.repositoryList.header().sortIndicatorOrder())
    
    def __populateList(self):
        """
        Private method to populate the list of available plugins.
        """
        self.repositoryList.clear()
        self.__stableItem = None
        self.__unstableItem = None
        self.__unknownItem = None
        
        self.downloadProgress.setValue(0)
        self.__doneMethod = None
        
        if os.path.exists(self.pluginRepositoryFile):
            self.__repositoryMissing = False
            f = QFile(self.pluginRepositoryFile)
            if f.open(QIODevice.ReadOnly):
                from E5XML.PluginRepositoryReader import PluginRepositoryReader
                reader = PluginRepositoryReader(f, self.addEntry)
                reader.readXML()
                self.repositoryList.resizeColumnToContents(0)
                self.repositoryList.resizeColumnToContents(1)
                self.repositoryList.resizeColumnToContents(2)
                self.__resortRepositoryList()
                url = Preferences.getUI("PluginRepositoryUrl6")
                if url != self.repositoryUrlEdit.text():
                    self.repositoryUrlEdit.setText(url)
                    E5MessageBox.warning(
                        self,
                        self.tr("Plugins Repository URL Changed"),
                        self.tr(
                            """The URL of the Plugins Repository has"""
                            """ changed. Select the "Update" button to get"""
                            """ the new repository file."""))
            else:
                E5MessageBox.critical(
                    self,
                    self.tr("Read plugins repository file"),
                    self.tr("<p>The plugins repository file <b>{0}</b> "
                            "could not be read. Select Update</p>")
                    .format(self.pluginRepositoryFile))
        else:
            self.__repositoryMissing = True
            QTreeWidgetItem(
                self.repositoryList,
                ["", self.tr(
                    "No plugin repository file available.\nSelect Update.")
                 ])
            self.repositoryList.resizeColumnToContents(1)
    
    def __downloadFile(self, url, filename, doneMethod=None):
        """
        Private slot to download the given file.
        
        @param url URL for the download (string)
        @param filename local name of the file (string)
        @param doneMethod method to be called when done
        """
        if self.__networkConfigurationManager.isOnline():
            self.__updateButton.setEnabled(False)
            self.__downloadButton.setEnabled(False)
            self.__downloadInstallButton.setEnabled(False)
            self.__downloadCancelButton.setEnabled(True)
            
            self.statusLabel.setText(url)
            
            self.__doneMethod = doneMethod
            self.__downloadURL = url
            self.__downloadFileName = filename
            self.__downloadIODevice = QFile(self.__downloadFileName + ".tmp")
            self.__downloadCancelled = False
            
            request = QNetworkRequest(QUrl(url))
            request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
                                 QNetworkRequest.AlwaysNetwork)
            reply = self.__networkManager.get(request)
            reply.finished.connect(self.__downloadFileDone)
            reply.downloadProgress.connect(self.__downloadProgress)
            self.__replies.append(reply)
        else:
            E5MessageBox.warning(
                self,
                self.tr("Error downloading file"),
                self.tr(
                    """<p>Could not download the requested file"""
                    """ from {0}.</p><p>Error: {1}</p>"""
                ).format(url, self.tr("Computer is offline.")))
    
    def __downloadFileDone(self):
        """
        Private method called, after the file has been downloaded
        from the internet.
        """
        self.__updateButton.setEnabled(True)
        self.__downloadCancelButton.setEnabled(False)
        self.__onlineStateChanged(
            self.__networkConfigurationManager.isOnline())
        
        ok = True
        reply = self.sender()
        if reply in self.__replies:
            self.__replies.remove(reply)
        if reply.error() != QNetworkReply.NoError:
            ok = False
            if not self.__downloadCancelled:
                E5MessageBox.warning(
                    self,
                    self.tr("Error downloading file"),
                    self.tr(
                        """<p>Could not download the requested file"""
                        """ from {0}.</p><p>Error: {1}</p>"""
                    ).format(self.__downloadURL, reply.errorString())
                )
            self.downloadProgress.setValue(0)
            self.__downloadURL = None
            self.__downloadIODevice.remove()
            self.__downloadIODevice = None
            if self.repositoryList.topLevelItemCount():
                if self.repositoryList.currentItem() is None:
                    self.repositoryList.setCurrentItem(
                        self.repositoryList.topLevelItem(0))
                else:
                    self.__downloadButton.setEnabled(
                        len(self.__selectedItems()))
                    self.__downloadInstallButton.setEnabled(
                        len(self.__selectedItems()))
            reply.deleteLater()
            return
        
        self.__downloadIODevice.open(QIODevice.WriteOnly)
        self.__downloadIODevice.write(reply.readAll())
        self.__downloadIODevice.close()
        if QFile.exists(self.__downloadFileName):
            QFile.remove(self.__downloadFileName)
        self.__downloadIODevice.rename(self.__downloadFileName)
        self.__downloadIODevice = None
        self.__downloadURL = None
        reply.deleteLater()
        
        if self.__doneMethod is not None:
            self.__doneMethod(ok, self.__downloadFileName)
    
    def __downloadCancel(self):
        """
        Private slot to cancel the current download.
        """
        if self.__replies:
            reply = self.__replies[0]
            self.__downloadCancelled = True
            self.__pluginsToDownload = []
            reply.abort()
    
    def __downloadProgress(self, done, total):
        """
        Private slot to show the download progress.
        
        @param done number of bytes downloaded so far (integer)
        @param total total bytes to be downloaded (integer)
        """
        if total:
            self.downloadProgress.setMaximum(total)
            self.downloadProgress.setValue(done)
    
    def addEntry(self, name, short, description, url, author, version,
                 filename, status):
        """
        Public method to add an entry to the list.
        
        @param name data for the name field (string)
        @param short data for the short field (string)
        @param description data for the description field (list of strings)
        @param url data for the url field (string)
        @param author data for the author field (string)
        @param version data for the version field (string)
        @param filename data for the filename field (string)
        @param status status of the plugin (string [stable, unstable, unknown])
        """
        pluginName = filename.rsplit("-", 1)[0]
        if pluginName in self.__hiddenPlugins:
            return
        
        if status == "stable":
            if self.__stableItem is None:
                self.__stableItem = \
                    QTreeWidgetItem(self.repositoryList,
                                    [self.tr("Stable")])
                self.__stableItem.setExpanded(True)
            parent = self.__stableItem
        elif status == "unstable":
            if self.__unstableItem is None:
                self.__unstableItem = \
                    QTreeWidgetItem(self.repositoryList,
                                    [self.tr("Unstable")])
                self.__unstableItem.setExpanded(True)
            parent = self.__unstableItem
        else:
            if self.__unknownItem is None:
                self.__unknownItem = \
                    QTreeWidgetItem(self.repositoryList,
                                    [self.tr("Unknown")])
                self.__unknownItem.setExpanded(True)
            parent = self.__unknownItem
        itm = QTreeWidgetItem(parent, [name, version, short])
        
        itm.setData(0, PluginRepositoryWidget.UrlRole, url)
        itm.setData(0, PluginRepositoryWidget.FilenameRole, filename)
        itm.setData(0, PluginRepositoryWidget.AuthorRole, author)
        itm.setData(0, PluginRepositoryWidget.DescrRole, description)
        
        updateStatus = self.__updateStatus(filename, version)
        if updateStatus == PluginRepositoryWidget.PluginStatusUpToDate:
            itm.setIcon(1, UI.PixmapCache.getIcon("empty.png"))
            itm.setToolTip(1, self.tr("up-to-date"))
        elif updateStatus == PluginRepositoryWidget.PluginStatusNew:
            itm.setIcon(1, UI.PixmapCache.getIcon("download.png"))
            itm.setToolTip(1, self.tr("new download available"))
        elif updateStatus == PluginRepositoryWidget.PluginStatusLocalUpdate:
            itm.setIcon(1, UI.PixmapCache.getIcon("updateLocal.png"))
            itm.setToolTip(1, self.tr("update installable"))
        elif updateStatus == PluginRepositoryWidget.PluginStatusRemoteUpdate:
            itm.setIcon(1, UI.PixmapCache.getIcon("updateRemote.png"))
            itm.setToolTip(1, self.tr("updated download available"))
    
    def __updateStatus(self, filename, version):
        """
        Private method to check, if the given archive update status.
        
        @param filename data for the filename field (string)
        @param version data for the version field (string)
        @return plug-in update status (integer, one of PluginStatusNew,
            PluginStatusUpToDate, PluginStatusLocalUpdate,
            PluginStatusRemoteUpdate)
        """
        archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
                               filename)
        
        # check, if it is an update (i.e. we already have archives
        # with the same pattern)
        archivesPattern = archive.rsplit('-', 1)[0] + "-*.zip"
        if len(glob.glob(archivesPattern)) == 0:
            return PluginRepositoryWidget.PluginStatusNew
        
        # check, if the archive exists
        if not os.path.exists(archive):
            return PluginRepositoryWidget.PluginStatusRemoteUpdate
        
        # check, if the archive is a valid zip file
        if not zipfile.is_zipfile(archive):
            return PluginRepositoryWidget.PluginStatusRemoteUpdate
        
        zip = zipfile.ZipFile(archive, "r")
        try:
            aversion = zip.read("VERSION").decode("utf-8")
        except KeyError:
            aversion = ""
        zip.close()
        
        if aversion == version:
            if not self.__external:
                # Check against installed/loaded plug-ins
                pluginManager = e5App().getObject("PluginManager")
                pluginName = filename.rsplit('-', 1)[0]
                pluginDetails = pluginManager.getPluginDetails(pluginName)
                if pluginDetails is None or pluginDetails["version"] < version:
                    return PluginRepositoryWidget.PluginStatusLocalUpdate
            
            return PluginRepositoryWidget.PluginStatusUpToDate
        else:
            return PluginRepositoryWidget.PluginStatusRemoteUpdate
    
    def __sslErrors(self, reply, errors):
        """
        Private slot to handle SSL errors.
        
        @param reply reference to the reply object (QNetworkReply)
        @param errors list of SSL errors (list of QSslError)
        """
        ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0]
        if ignored == E5SslErrorHandler.NotIgnored:
            self.__downloadCancelled = True
    
    def getDownloadedPlugins(self):
        """
        Public method to get the list of recently downloaded plugin files.
        
        @return list of plugin filenames (list of strings)
        """
        return self.__pluginsDownloaded
    
    @pyqtSlot(bool)
    def on_repositoryUrlEditButton_toggled(self, checked):
        """
        Private slot to set the read only status of the repository URL line
        edit.
        
        @param checked state of the push button (boolean)
        """
        self.repositoryUrlEdit.setReadOnly(not checked)
    
    def __closeAndInstall(self):
        """
        Private method to close the dialog and invoke the install dialog.
        """
        if not self.__pluginsDownloaded and self.__selectedItems():
            for itm in self.__selectedItems():
                filename = os.path.join(
                    Preferences.getPluginManager("DownloadPath"),
                    itm.data(0, PluginRepositoryWidget.FilenameRole))
                self.__pluginsDownloaded.append(filename)
        self.closeAndInstall.emit()
    
    def __hidePlugin(self):
        """
        Private slot to hide the current plug-in.
        """
        itm = self.__selectedItems()[0]
        pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
                      .rsplit("-", 1)[0])
        self.__updateHiddenPluginsList([pluginName])
    
    def __hideSelectedPlugins(self):
        """
        Private slot to hide all selected plug-ins.
        """
        hideList = []
        for itm in self.__selectedItems():
            pluginName = (itm.data(0, PluginRepositoryWidget.FilenameRole)
                          .rsplit("-", 1)[0])
            hideList.append(pluginName)
        self.__updateHiddenPluginsList(hideList)
    
    def __showAllPlugins(self):
        """
        Private slot to show all plug-ins.
        """
        self.__hiddenPlugins = []
        self.__updateHiddenPluginsList([])
    
    def __hasHiddenPlugins(self):
        """
        Private method to check, if there are any hidden plug-ins.
        
        @return flag indicating the presence of hidden plug-ins (boolean)
        """
        return bool(self.__hiddenPlugins)
    
    def __updateHiddenPluginsList(self, hideList):
        """
        Private method to store the list of hidden plug-ins to the settings.
        
        @param hideList list of plug-ins to add to the list of hidden ones
            (list of string)
        """
        if hideList:
            self.__hiddenPlugins.extend(
                [p for p in hideList if p not in self.__hiddenPlugins])
        Preferences.setPluginManager("HiddenPlugins", self.__hiddenPlugins)
        self.__populateList()
    
    def __cleanupDownloads(self):
        """
        Private slot to cleanup the plug-in downloads area.
        """
        downloadPath = Preferences.getPluginManager("DownloadPath")
        downloads = {}  # plug-in name as key, file name as value
        
        # step 1: extract plug-ins and downloaded files
        for pluginFile in os.listdir(downloadPath):
            if not os.path.isfile(os.path.join(downloadPath, pluginFile)):
                continue
            
            pluginName = pluginFile.rsplit("-", 1)[0]
            if pluginName not in downloads:
                downloads[pluginName] = []
            downloads[pluginName].append(pluginFile)
        
        # step 2: delete old entries
        for pluginName in downloads:
            downloads[pluginName].sort()
        
            if pluginName in self.__hiddenPlugins and \
                    not Preferences.getPluginManager("KeepHidden"):
                removeFiles = downloads[pluginName]
            else:
                removeFiles = downloads[pluginName][
                    :-Preferences.getPluginManager("KeepGenerations")]
            for removeFile in removeFiles:
                try:
                    os.remove(os.path.join(downloadPath, removeFile))
                except (IOError, OSError) as err:
                    E5MessageBox.critical(
                        self,
                        self.tr("Cleanup of Plugin Downloads"),
                        self.tr("""<p>The plugin download <b>{0}</b> could"""
                                """ not be deleted.</p><p>Reason: {1}</p>""")
                        .format(removeFile, str(err)))
예제 #29
0
class downloadManager(QObject):
    manager = None
    currentDownload = None
    reply = None
    url = None
    result = None
    filename = None
    dir_ = None
    url_ = None

    def __init__(self):
        super(downloadManager, self).__init__()
        self.manager = QNetworkAccessManager()
        self.currentDownload = []
        self.manager.finished.connect(self.downloadFinished)

    def setLE(self, filename, dir_, urllineedit):
        self.filename = filename
        self.dir_ = dir_
        self.url_ = urllineedit

    def doDownload(self):
        request = QNetworkRequest(QUrl("%s/%s/%s" % (self.url_.text(), self.dir_, self.filename)))
        self.reply = self.manager.get(request)
        # self.reply.sslErrors.connect(self.sslErrors)
        self.currentDownload.append(self.reply)

    def saveFileName(self, url):
        path = url.path()
        basename = QFileInfo(path).fileName()

        if not basename:
            basename = "download"

        if QFile.exists(basename):
            i = 0
            basename = basename + "."
            while QFile.exists("%s%s" % (basename, i)):
                i = i + 1

            basename = "%s%s" % (basename, i)

        return basename

    def saveToDisk(self, filename, data):
        fi = "%s/%s" % (self.dir_, filename)
        if not os.path.exists(self.dir_):
            os.makedirs(self.dir_)
        file = QFile(fi)
        if not file.open(QIODevice.WriteOnly):
            return False

        file.write(data.readAll())
        file.close()

        return True

    def isHttpRedirect(self, reply):
        statusCode = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        return statusCode in [301, 302, 303, 305, 307, 308]

    @QtCore.pyqtSlot(QNetworkReply)
    def downloadFinished(self, reply):
        url = reply.url()
        if not reply.error():
            if not self.isHttpRedirect(reply):
                filename = self.saveFileName(url)
                filename = filename.replace(":", "")
                self.saveToDisk(filename, reply)
                self.result = "%s ---> %s/%s" % (url, self.dir_, filename)
            else:
                self.result = "Redireccionado ... :("
        else:
            self.result = reply.errorString()
예제 #30
0
class PluginManager(QObject):
    """
    Class implementing the Plugin Manager.
    
    @signal shutdown() emitted at shutdown of the IDE
    @signal pluginAboutToBeActivated(modulName, pluginObject) emitted just
        before a plugin is activated
    @signal pluginActivated(moduleName, pluginObject) emitted just after
        a plugin was activated
    @signal allPlugginsActivated() emitted at startup after all plugins have
        been activated
    @signal pluginAboutToBeDeactivated(moduleName, pluginObject) emitted just
        before a plugin is deactivated
    @signal pluginDeactivated(moduleName, pluginObject) emitted just after
        a plugin was deactivated
    """
    shutdown = pyqtSignal()
    pluginAboutToBeActivated = pyqtSignal(str, object)
    pluginActivated = pyqtSignal(str, object)
    allPlugginsActivated = pyqtSignal()
    pluginAboutToBeDeactivated = pyqtSignal(str, object)
    pluginDeactivated = pyqtSignal(str, object)
    
    def __init__(self, parent=None, doLoadPlugins=True, develPlugin=None):
        """
        Constructor
        
        The Plugin Manager deals with three different plugin directories.
        The first is the one, that is part of eric6 (eric6/Plugins). The
        second one is the global plugin directory called 'eric6plugins',
        which is located inside the site-packages directory. The last one
        is the user plugin directory located inside the .eric6 directory
        of the users home directory.
        
        @param parent reference to the parent object (QObject)
        @keyparam doLoadPlugins flag indicating, that plugins should
            be loaded (boolean)
        @keyparam develPlugin filename of a plugin to be loaded for
            development (string)
        @exception PluginPathError raised to indicate an invalid plug-in path
        """
        super(PluginManager, self).__init__(parent)
        
        self.__ui = parent
        self.__develPluginFile = develPlugin
        self.__develPluginName = None
        
        self.__inactivePluginsKey = "PluginManager/InactivePlugins"
        
        self.pluginDirs = {
            "eric6": os.path.join(getConfig('ericDir'), "Plugins"),
            "global": os.path.join(Utilities.getPythonModulesDirectory(),
                                   "eric6plugins"),
            "user": os.path.join(Utilities.getConfigDir(), "eric6plugins"),
        }
        self.__priorityOrder = ["eric6", "global", "user"]
        
        self.__defaultDownloadDir = os.path.join(
            Utilities.getConfigDir(), "Downloads")
        
        self.__activePlugins = {}
        self.__inactivePlugins = {}
        self.__onDemandActivePlugins = {}
        self.__onDemandInactivePlugins = {}
        self.__activeModules = {}
        self.__inactiveModules = {}
        self.__onDemandActiveModules = {}
        self.__onDemandInactiveModules = {}
        self.__failedModules = {}
        
        self.__foundCoreModules = []
        self.__foundGlobalModules = []
        self.__foundUserModules = []
        
        self.__modulesCount = 0
        
        pdirsExist, msg = self.__pluginDirectoriesExist()
        if not pdirsExist:
            raise PluginPathError(msg)
        
        if doLoadPlugins:
            if not self.__pluginModulesExist():
                raise PluginModulesError
            
            self.__insertPluginsPaths()
            
            self.__loadPlugins()
        
        self.__checkPluginsDownloadDirectory()
        
        self.pluginRepositoryFile = \
            os.path.join(Utilities.getConfigDir(), "PluginRepository")
        
        # attributes for the network objects
        self.__networkManager = QNetworkAccessManager(self)
        self.__networkManager.proxyAuthenticationRequired.connect(
            proxyAuthenticationRequired)
        if SSL_AVAILABLE:
            self.__sslErrorHandler = E5SslErrorHandler(self)
            self.__networkManager.sslErrors.connect(self.__sslErrors)
        self.__replies = []
    
    def finalizeSetup(self):
        """
        Public method to finalize the setup of the plugin manager.
        """
        for module in list(self.__onDemandInactiveModules.values()) + \
                list(self.__onDemandActiveModules.values()):
            if hasattr(module, "moduleSetup"):
                module.moduleSetup()
        
    def getPluginDir(self, key):
        """
        Public method to get the path of a plugin directory.
        
        @param key key of the plug-in directory (string)
        @return path of the requested plugin directory (string)
        """
        if key not in ["global", "user"]:
            return None
        else:
            try:
                return self.pluginDirs[key]
            except KeyError:
                return None
    
    def __pluginDirectoriesExist(self):
        """
        Private method to check, if the plugin folders exist.
        
        If the plugin folders don't exist, they are created (if possible).
        
        @return tuple of a flag indicating existence of any of the plugin
            directories (boolean) and a message (string)
        """
        if self.__develPluginFile:
            path = Utilities.splitPath(self.__develPluginFile)[0]
            fname = os.path.join(path, "__init__.py")
            if not os.path.exists(fname):
                try:
                    f = open(fname, "w")
                    f.close()
                except IOError:
                    return (
                        False,
                        self.tr("Could not create a package for {0}.")
                            .format(self.__develPluginFile))
        
        if Preferences.getPluginManager("ActivateExternal"):
            fname = os.path.join(self.pluginDirs["user"], "__init__.py")
            if not os.path.exists(fname):
                if not os.path.exists(self.pluginDirs["user"]):
                    os.mkdir(self.pluginDirs["user"], 0o755)
                try:
                    f = open(fname, "w")
                    f.close()
                except IOError:
                    del self.pluginDirs["user"]
            
            if not os.path.exists(self.pluginDirs["global"]) and \
               os.access(Utilities.getPythonModulesDirectory(), os.W_OK):
                # create the global plugins directory
                os.mkdir(self.pluginDirs["global"], 0o755)
                fname = os.path.join(self.pluginDirs["global"], "__init__.py")
                f = open(fname, "w", encoding="utf-8")
                f.write('# -*- coding: utf-8 -*-' + "\n")
                f.write("\n")
                f.write('"""' + "\n")
                f.write('Package containing the global plugins.' + "\n")
                f.write('"""' + "\n")
                f.close()
            if not os.path.exists(self.pluginDirs["global"]):
                del self.pluginDirs["global"]
        else:
            del self.pluginDirs["user"]
            del self.pluginDirs["global"]
        
        if not os.path.exists(self.pluginDirs["eric6"]):
            return (
                False,
                self.tr(
                    "The internal plugin directory <b>{0}</b>"
                    " does not exits.").format(self.pluginDirs["eric6"]))
        
        return (True, "")
    
    def __pluginModulesExist(self):
        """
        Private method to check, if there are plugins available.
        
        @return flag indicating the availability of plugins (boolean)
        """
        if self.__develPluginFile and \
                not os.path.exists(self.__develPluginFile):
            return False
        
        self.__foundCoreModules = self.getPluginModules(
            self.pluginDirs["eric6"])
        if "global" in self.pluginDirs:
            self.__foundGlobalModules = \
                self.getPluginModules(self.pluginDirs["global"])
        if "user" in self.pluginDirs:
            self.__foundUserModules = \
                self.getPluginModules(self.pluginDirs["user"])
        
        return len(self.__foundCoreModules + self.__foundGlobalModules +
                   self.__foundUserModules) > 0
    
    def getPluginModules(self, pluginPath):
        """
        Public method to get a list of plugin modules.
        
        @param pluginPath name of the path to search (string)
        @return list of plugin module names (list of string)
        """
        pluginFiles = [f[:-3] for f in os.listdir(pluginPath)
                       if self.isValidPluginName(f)]
        return pluginFiles[:]
    
    def isValidPluginName(self, pluginName):
        """
        Public methode to check, if a file name is a valid plugin name.
        
        Plugin modules must start with "Plugin" and have the extension ".py".
        
        @param pluginName name of the file to be checked (string)
        @return flag indicating a valid plugin name (boolean)
        """
        return pluginName.startswith("Plugin") and pluginName.endswith(".py")
    
    def __insertPluginsPaths(self):
        """
        Private method to insert the valid plugin paths intos the search path.
        """
        for key in self.__priorityOrder:
            if key in self.pluginDirs:
                if not self.pluginDirs[key] in sys.path:
                    sys.path.insert(2, self.pluginDirs[key])
                UI.PixmapCache.addSearchPath(self.pluginDirs[key])
        
        if self.__develPluginFile:
            path = Utilities.splitPath(self.__develPluginFile)[0]
            if path not in sys.path:
                sys.path.insert(2, path)
            UI.PixmapCache.addSearchPath(path)
    
    def __loadPlugins(self):
        """
        Private method to load the plugins found.
        """
        develPluginName = ""
        if self.__develPluginFile:
            develPluginPath, develPluginName = \
                Utilities.splitPath(self.__develPluginFile)
            if self.isValidPluginName(develPluginName):
                develPluginName = develPluginName[:-3]
        
        for pluginName in self.__foundCoreModules:
            # global and user plugins have priority
            if pluginName not in self.__foundGlobalModules and \
               pluginName not in self.__foundUserModules and \
               pluginName != develPluginName:
                self.loadPlugin(pluginName, self.pluginDirs["eric6"])
        
        for pluginName in self.__foundGlobalModules:
            # user plugins have priority
            if pluginName not in self.__foundUserModules and \
               pluginName != develPluginName:
                self.loadPlugin(pluginName, self.pluginDirs["global"])
        
        for pluginName in self.__foundUserModules:
            if pluginName != develPluginName:
                self.loadPlugin(pluginName, self.pluginDirs["user"])
        
        if develPluginName:
            self.loadPlugin(develPluginName, develPluginPath)
            self.__develPluginName = develPluginName
    
    def loadPlugin(self, name, directory, reload_=False):
        """
        Public method to load a plugin module.
        
        Initially all modules are inactive. Modules that are requested on
        demand are sorted out and are added to the on demand list. Some
        basic validity checks are performed as well. Modules failing these
        checks are added to the failed modules list.
        
        @param name name of the module to be loaded (string)
        @param directory name of the plugin directory (string)
        @param reload_ flag indicating to reload the module (boolean)
        @exception PluginLoadError raised to indicate an issue loading
            the plug-in
        """
        try:
            fname = "{0}.py".format(os.path.join(directory, name))
            module = imp.load_source(name, fname)
            if not hasattr(module, "autoactivate"):
                module.error = self.tr(
                    "Module is missing the 'autoactivate' attribute.")
                self.__failedModules[name] = module
                raise PluginLoadError(name)
            if sys.version_info[0] < 3:
                if not hasattr(module, "python2Compatible"):
                    module.error = self.tr(
                        "Module is missing the Python2 compatibility flag."
                        " Please update.")
                    compatible = False
                elif not getattr(module, "python2Compatible"):
                    module.error = self.tr(
                        "Module is not Python2 compatible.")
                    compatible = False
                else:
                    compatible = True
                if not compatible:
                    self.__failedModules[name] = module
                    raise PluginPy2IncompatibleError(name)
            if getattr(module, "autoactivate"):
                self.__inactiveModules[name] = module
            else:
                if not hasattr(module, "pluginType") or \
                   not hasattr(module, "pluginTypename"):
                    module.error = \
                        self.tr("Module is missing the 'pluginType' "
                                "and/or 'pluginTypename' attributes.")
                    self.__failedModules[name] = module
                    raise PluginLoadError(name)
                else:
                    self.__onDemandInactiveModules[name] = module
            module.eric6PluginModuleName = name
            module.eric6PluginModuleFilename = fname
            self.__modulesCount += 1
            if reload_:
                imp.reload(module)
        except PluginLoadError:
            print("Error loading plug-in module:", name)
        except PluginPy2IncompatibleError:
            print("Error loading plug-in module:", name)
            print("The plug-in is not Python2 compatible.")
        except Exception as err:
            module = imp.new_module(name)
            module.error = self.tr(
                "Module failed to load. Error: {0}").format(str(err))
            self.__failedModules[name] = module
            print("Error loading plug-in module:", name)
            print(str(err))
    
    def unloadPlugin(self, name):
        """
        Public method to unload a plugin module.
        
        @param name name of the module to be unloaded (string)
        @return flag indicating success (boolean)
        """
        if name in self.__onDemandActiveModules:
            # cannot unload an ondemand plugin, that is in use
            return False
        
        if name in self.__activeModules:
            self.deactivatePlugin(name)
        
        if name in self.__inactiveModules:
            try:
                del self.__inactivePlugins[name]
            except KeyError:
                pass
            del self.__inactiveModules[name]
        elif name in self.__onDemandInactiveModules:
            try:
                del self.__onDemandInactivePlugins[name]
            except KeyError:
                pass
            del self.__onDemandInactiveModules[name]
        elif name in self.__failedModules:
            del self.__failedModules[name]
        
        self.__modulesCount -= 1
        return True
    
    def removePluginFromSysModules(self, pluginName, package,
                                   internalPackages):
        """
        Public method to remove a plugin and all related modules from
        sys.modules.
        
        @param pluginName name of the plugin module (string)
        @param package name of the plugin package (string)
        @param internalPackages list of intenal packages (list of string)
        @return flag indicating the plugin module was found in sys.modules
            (boolean)
        """
        packages = [package] + internalPackages
        found = False
        if not package:
            package = "__None__"
        for moduleName in list(sys.modules.keys())[:]:
            if moduleName == pluginName or \
                    moduleName.split(".")[0] in packages:
                found = True
                del sys.modules[moduleName]
        return found
    
    def initOnDemandPlugins(self):
        """
        Public method to create plugin objects for all on demand plugins.
        
        Note: The plugins are not activated.
        """
        names = sorted(self.__onDemandInactiveModules.keys())
        for name in names:
            self.initOnDemandPlugin(name)
    
    def initOnDemandPlugin(self, name):
        """
        Public method to create a plugin object for the named on demand plugin.
        
        Note: The plug-in is not activated.
        
        @param name name of the plug-in (string)
        @exception PluginActivationError raised to indicate an issue during the
            plug-in activation
        """
        try:
            try:
                module = self.__onDemandInactiveModules[name]
            except KeyError:
                return
            
            if not self.__canActivatePlugin(module):
                raise PluginActivationError(module.eric6PluginModuleName)
            version = getattr(module, "version")
            className = getattr(module, "className")
            pluginClass = getattr(module, className)
            pluginObject = None
            if name not in self.__onDemandInactivePlugins:
                pluginObject = pluginClass(self.__ui)
                pluginObject.eric6PluginModule = module
                pluginObject.eric6PluginName = className
                pluginObject.eric6PluginVersion = version
                self.__onDemandInactivePlugins[name] = pluginObject
        except PluginActivationError:
            return
    
    def activatePlugins(self):
        """
        Public method to activate all plugins having the "autoactivate"
        attribute set to True.
        """
        savedInactiveList = Preferences.Prefs.settings.value(
            self.__inactivePluginsKey)
        if self.__develPluginName is not None and \
           savedInactiveList is not None and \
           self.__develPluginName in savedInactiveList:
            savedInactiveList.remove(self.__develPluginName)
        names = sorted(self.__inactiveModules.keys())
        for name in names:
            if savedInactiveList is None or name not in savedInactiveList:
                self.activatePlugin(name)
        self.allPlugginsActivated.emit()
    
    def activatePlugin(self, name, onDemand=False):
        """
        Public method to activate a plugin.
        
        @param name name of the module to be activated
        @keyparam onDemand flag indicating activation of an
            on demand plugin (boolean)
        @return reference to the initialized plugin object
        @exception PluginActivationError raised to indicate an issue during the
            plug-in activation
        """
        try:
            try:
                if onDemand:
                    module = self.__onDemandInactiveModules[name]
                else:
                    module = self.__inactiveModules[name]
            except KeyError:
                return None
            
            if not self.__canActivatePlugin(module):
                raise PluginActivationError(module.eric6PluginModuleName)
            version = getattr(module, "version")
            className = getattr(module, "className")
            pluginClass = getattr(module, className)
            pluginObject = None
            if onDemand and name in self.__onDemandInactivePlugins:
                pluginObject = self.__onDemandInactivePlugins[name]
            elif not onDemand and name in self.__inactivePlugins:
                pluginObject = self.__inactivePlugins[name]
            else:
                pluginObject = pluginClass(self.__ui)
            self.pluginAboutToBeActivated.emit(name, pluginObject)
            try:
                obj, ok = pluginObject.activate()
            except TypeError:
                module.error = self.tr(
                    "Incompatible plugin activation method.")
                obj = None
                ok = True
            except Exception as err:
                module.error = str(err)
                obj = None
                ok = False
            if not ok:
                return None
            
            self.pluginActivated.emit(name, pluginObject)
            pluginObject.eric6PluginModule = module
            pluginObject.eric6PluginName = className
            pluginObject.eric6PluginVersion = version
            
            if onDemand:
                self.__onDemandInactiveModules.pop(name)
                try:
                    self.__onDemandInactivePlugins.pop(name)
                except KeyError:
                    pass
                self.__onDemandActivePlugins[name] = pluginObject
                self.__onDemandActiveModules[name] = module
            else:
                self.__inactiveModules.pop(name)
                try:
                    self.__inactivePlugins.pop(name)
                except KeyError:
                    pass
                self.__activePlugins[name] = pluginObject
                self.__activeModules[name] = module
            return obj
        except PluginActivationError:
            return None
    
    def __canActivatePlugin(self, module):
        """
        Private method to check, if a plugin can be activated.
        
        @param module reference to the module to be activated
        @return flag indicating, if the module satisfies all requirements
            for being activated (boolean)
        @exception PluginModuleFormatError raised to indicate an invalid
            plug-in module format
        @exception PluginClassFormatError raised to indicate an invalid
            plug-in class format
        """
        try:
            if not hasattr(module, "version"):
                raise PluginModuleFormatError(
                    module.eric6PluginModuleName, "version")
            if not hasattr(module, "className"):
                raise PluginModuleFormatError(
                    module.eric6PluginModuleName, "className")
            className = getattr(module, "className")
            if not hasattr(module, className):
                raise PluginModuleFormatError(
                    module.eric6PluginModuleName, className)
            pluginClass = getattr(module, className)
            if not hasattr(pluginClass, "__init__"):
                raise PluginClassFormatError(
                    module.eric6PluginModuleName,
                    className, "__init__")
            if not hasattr(pluginClass, "activate"):
                raise PluginClassFormatError(
                    module.eric6PluginModuleName,
                    className, "activate")
            if not hasattr(pluginClass, "deactivate"):
                raise PluginClassFormatError(
                    module.eric6PluginModuleName,
                    className, "deactivate")
            return True
        except PluginModuleFormatError as e:
            print(repr(e))
            return False
        except PluginClassFormatError as e:
            print(repr(e))
            return False
    
    def deactivatePlugin(self, name, onDemand=False):
        """
        Public method to deactivate a plugin.
        
        @param name name of the module to be deactivated
        @keyparam onDemand flag indicating deactivation of an
            on demand plugin (boolean)
        """
        try:
            if onDemand:
                module = self.__onDemandActiveModules[name]
            else:
                module = self.__activeModules[name]
        except KeyError:
            return
        
        if self.__canDeactivatePlugin(module):
            pluginObject = None
            if onDemand and name in self.__onDemandActivePlugins:
                pluginObject = self.__onDemandActivePlugins[name]
            elif not onDemand and name in self.__activePlugins:
                pluginObject = self.__activePlugins[name]
            if pluginObject:
                self.pluginAboutToBeDeactivated.emit(name, pluginObject)
                pluginObject.deactivate()
                self.pluginDeactivated.emit(name, pluginObject)
                
                if onDemand:
                    self.__onDemandActiveModules.pop(name)
                    self.__onDemandActivePlugins.pop(name)
                    self.__onDemandInactivePlugins[name] = pluginObject
                    self.__onDemandInactiveModules[name] = module
                else:
                    self.__activeModules.pop(name)
                    try:
                        self.__activePlugins.pop(name)
                    except KeyError:
                        pass
                    self.__inactivePlugins[name] = pluginObject
                    self.__inactiveModules[name] = module
    
    def __canDeactivatePlugin(self, module):
        """
        Private method to check, if a plugin can be deactivated.
        
        @param module reference to the module to be deactivated
        @return flag indicating, if the module satisfies all requirements
            for being deactivated (boolean)
        """
        return getattr(module, "deactivateable", True)
    
    def getPluginObject(self, type_, typename, maybeActive=False):
        """
        Public method to activate an ondemand plugin given by type and
        typename.
        
        @param type_ type of the plugin to be activated (string)
        @param typename name of the plugin within the type category (string)
        @keyparam maybeActive flag indicating, that the plugin may be active
            already (boolean)
        @return reference to the initialized plugin object
        """
        for name, module in list(self.__onDemandInactiveModules.items()):
            if getattr(module, "pluginType") == type_ and \
               getattr(module, "pluginTypename") == typename:
                return self.activatePlugin(name, onDemand=True)
        
        if maybeActive:
            for name, module in list(self.__onDemandActiveModules.items()):
                if getattr(module, "pluginType") == type_ and \
                   getattr(module, "pluginTypename") == typename:
                    self.deactivatePlugin(name, onDemand=True)
                    return self.activatePlugin(name, onDemand=True)
        
        return None
    
    def getPluginInfos(self):
        """
        Public method to get infos about all loaded plugins.
        
        @return list of tuples giving module name (string), plugin name
            (string), version (string), autoactivate (boolean), active
            (boolean), short description (string), error flag (boolean)
        """
        infos = []
        
        for name in list(self.__activeModules.keys()):
            pname, shortDesc, error, version = \
                self.__getShortInfo(self.__activeModules[name])
            infos.append((name, pname, version, True, True, shortDesc, error))
        for name in list(self.__inactiveModules.keys()):
            pname, shortDesc, error, version = \
                self.__getShortInfo(self.__inactiveModules[name])
            infos.append(
                (name, pname, version, True, False, shortDesc, error))
        for name in list(self.__onDemandActiveModules.keys()):
            pname, shortDesc, error, version = \
                self.__getShortInfo(self.__onDemandActiveModules[name])
            infos.append(
                (name, pname, version, False, True, shortDesc, error))
        for name in list(self.__onDemandInactiveModules.keys()):
            pname, shortDesc, error, version = \
                self.__getShortInfo(self.__onDemandInactiveModules[name])
            infos.append(
                (name, pname, version, False, False, shortDesc, error))
        for name in list(self.__failedModules.keys()):
            pname, shortDesc, error, version = \
                self.__getShortInfo(self.__failedModules[name])
            infos.append(
                (name, pname, version, False, False, shortDesc, error))
        return infos
    
    def __getShortInfo(self, module):
        """
        Private method to extract the short info from a module.
        
        @param module module to extract short info from
        @return short info as a tuple giving plugin name (string),
            short description (string), error flag (boolean) and
            version (string)
        """
        name = getattr(module, "name", "")
        shortDesc = getattr(module, "shortDescription", "")
        version = getattr(module, "version", "")
        error = getattr(module, "error", "") != ""
        return name, shortDesc, error, version
    
    def getPluginDetails(self, name):
        """
        Public method to get detailed information about a plugin.
        
        @param name name of the module to get detailed infos about (string)
        @return details of the plugin as a dictionary
        """
        details = {}
        
        autoactivate = True
        active = True
        
        if name in self.__activeModules:
            module = self.__activeModules[name]
        elif name in self.__inactiveModules:
            module = self.__inactiveModules[name]
            active = False
        elif name in self.__onDemandActiveModules:
            module = self.__onDemandActiveModules[name]
            autoactivate = False
        elif name in self.__onDemandInactiveModules:
            module = self.__onDemandInactiveModules[name]
            autoactivate = False
            active = False
        elif name in self.__failedModules:
            module = self.__failedModules[name]
            autoactivate = False
            active = False
        else:
            # should not happen
            return None
        
        details["moduleName"] = name
        details["moduleFileName"] = getattr(
            module, "eric6PluginModuleFilename", "")
        details["pluginName"] = getattr(module, "name", "")
        details["version"] = getattr(module, "version", "")
        details["author"] = getattr(module, "author", "")
        details["description"] = getattr(module, "longDescription", "")
        details["autoactivate"] = autoactivate
        details["active"] = active
        details["error"] = getattr(module, "error", "")
        
        return details
    
    def doShutdown(self):
        """
        Public method called to perform actions upon shutdown of the IDE.
        """
        names = []
        for name in list(self.__inactiveModules.keys()):
            names.append(name)
        Preferences.Prefs.settings.setValue(self.__inactivePluginsKey, names)
        
        self.shutdown.emit()

    def getPluginDisplayStrings(self, type_):
        """
        Public method to get the display strings of all plugins of a specific
        type.
        
        @param type_ type of the plugins (string)
        @return dictionary with name as key and display string as value
            (dictionary of string)
        """
        pluginDict = {}
        
        for name, module in \
            list(self.__onDemandActiveModules.items()) + \
                list(self.__onDemandInactiveModules.items()):
            if getattr(module, "pluginType") == type_ and \
               getattr(module, "error", "") == "":
                plugin_name = getattr(module, "pluginTypename")
                if hasattr(module, "displayString"):
                    try:
                        disp = module.displayString()
                    except TypeError:
                        disp = getattr(module, "displayString")
                    if disp != "":
                        pluginDict[plugin_name] = disp
                else:
                    pluginDict[plugin_name] = plugin_name
        
        return pluginDict
        
    def getPluginPreviewPixmap(self, type_, name):
        """
        Public method to get a preview pixmap of a plugin of a specific type.
        
        @param type_ type of the plugin (string)
        @param name name of the plugin type (string)
        @return preview pixmap (QPixmap)
        """
        for modname, module in \
            list(self.__onDemandActiveModules.items()) + \
                list(self.__onDemandInactiveModules.items()):
            if getattr(module, "pluginType") == type_ and \
               getattr(module, "pluginTypename") == name:
                if hasattr(module, "previewPix"):
                    return module.previewPix()
                else:
                    return QPixmap()
        
        return QPixmap()
        
    def getPluginApiFiles(self, language):
        """
        Public method to get the list of API files installed by a plugin.
        
        @param language language of the requested API files (string)
        @return list of API filenames (list of string)
        """
        apis = []
        
        for module in list(self.__activeModules.values()) + \
                list(self.__onDemandActiveModules.values()):
            if hasattr(module, "apiFiles"):
                apis.extend(module.apiFiles(language))
        
        return apis
        
    def getPluginExeDisplayData(self):
        """
        Public method to get data to display information about a plugins
        external tool.
        
        @return list of dictionaries containing the data. Each dictionary must
            either contain data for the determination or the data to be
            displayed.<br />
            A dictionary of the first form must have the following entries:
            <ul>
                <li>programEntry - indicator for this dictionary form
                   (boolean), always True</li>
                <li>header - string to be diplayed as a header (string)</li>
                <li>exe - the executable (string)</li>
                <li>versionCommand - commandline parameter for the exe
                    (string)</li>
                <li>versionStartsWith - indicator for the output line
                    containing the version (string)</li>
                <li>versionPosition - number of element containing the
                    version (integer)</li>
                <li>version - version to be used as default (string)</li>
                <li>versionCleanup - tuple of two integers giving string
                    positions start and stop for the version string
                    (tuple of integers)</li>
            </ul>
            A dictionary of the second form must have the following entries:
            <ul>
                <li>programEntry - indicator for this dictionary form
                    (boolean), always False</li>
                <li>header - string to be diplayed as a header (string)</li>
                <li>text - entry text to be shown (string)</li>
                <li>version - version text to be shown (string)</li>
            </ul>
        """
        infos = []
        
        for module in list(self.__activeModules.values()) + \
                list(self.__inactiveModules.values()):
            if hasattr(module, "exeDisplayDataList"):
                infos.extend(module.exeDisplayDataList())
            elif hasattr(module, "exeDisplayData"):
                infos.append(module.exeDisplayData())
        for module in list(self.__onDemandActiveModules.values()) + \
                list(self.__onDemandInactiveModules.values()):
            if hasattr(module, "exeDisplayDataList"):
                infos.extend(module.exeDisplayDataList())
            elif hasattr(module, "exeDisplayData"):
                infos.append(module.exeDisplayData())
        
        return infos
        
    def getPluginConfigData(self):
        """
        Public method to get the config data of all active, non on-demand
        plugins used by the configuration dialog.
        
        Plugins supporting this functionality must provide the plugin module
        function 'getConfigData' returning a dictionary with unique keys
        of lists with the following list contents:
        <dl>
          <dt>display string</dt>
          <dd>string shown in the selection area of the configuration page.
              This should be a localized string</dd>
          <dt>pixmap name</dt>
          <dd>filename of the pixmap to be shown next to the display
              string</dd>
          <dt>page creation function</dt>
          <dd>plugin module function to be called to create the configuration
              page. The page must be subclasses from
              Preferences.ConfigurationPages.ConfigurationPageBase and must
              implement a method called 'save' to save the settings. A parent
              entry will be created in the selection list, if this value is
              None.</dd>
          <dt>parent key</dt>
          <dd>dictionary key of the parent entry or None, if this defines a
              toplevel entry.</dd>
          <dt>reference to configuration page</dt>
          <dd>This will be used by the configuration dialog and must always
              be None</dd>
        </dl>
        
        @return plug-in configuration data
        """
        configData = {}
        for module in list(self.__activeModules.values()) + \
            list(self.__onDemandActiveModules.values()) + \
                list(self.__onDemandInactiveModules.values()):
            if hasattr(module, 'getConfigData'):
                configData.update(module.getConfigData())
        return configData
        
    def isPluginLoaded(self, pluginName):
        """
        Public method to check, if a certain plugin is loaded.
        
        @param pluginName name of the plugin to check for (string)
        @return flag indicating, if the plugin is loaded (boolean)
        """
        return pluginName in self.__activeModules or \
            pluginName in self.__inactiveModules or \
            pluginName in self.__onDemandActiveModules or \
            pluginName in self.__onDemandInactiveModules
        
    def isPluginActive(self, pluginName):
        """
        Public method to check, if a certain plugin is active.
        
        @param pluginName name of the plugin to check for (string)
        @return flag indicating, if the plugin is active (boolean)
        """
        return pluginName in self.__activeModules or \
            pluginName in self.__onDemandActiveModules
    
    ###########################################################################
    ## Specialized plugin module handling methods below
    ###########################################################################
    
    ###########################################################################
    ## VCS related methods below
    ###########################################################################
    
    def getVcsSystemIndicators(self):
        """
        Public method to get the Vcs System indicators.
        
        Plugins supporting this functionality must support the module function
        getVcsSystemIndicator returning a dictionary with indicator as key and
        a tuple with the vcs name (string) and vcs display string (string).
        
        @return dictionary with indicator as key and a list of tuples as
            values. Each tuple contains the vcs name (string) and vcs display
            string (string).
        """
        vcsDict = {}
        
        for name, module in \
            list(self.__onDemandActiveModules.items()) + \
                list(self.__onDemandInactiveModules.items()):
            if getattr(module, "pluginType") == "version_control":
                if hasattr(module, "getVcsSystemIndicator"):
                    res = module.getVcsSystemIndicator()
                    for indicator, vcsData in list(res.items()):
                        if indicator in vcsDict:
                            vcsDict[indicator].append(vcsData)
                        else:
                            vcsDict[indicator] = [vcsData]
        
        return vcsDict
    
    def deactivateVcsPlugins(self):
        """
        Public method to deactivated all activated VCS plugins.
        """
        for name, module in list(self.__onDemandActiveModules.items()):
            if getattr(module, "pluginType") == "version_control":
                self.deactivatePlugin(name, True)
    
    ########################################################################
    ## Methods creation of the plug-ins download directory
    ########################################################################
    
    def __checkPluginsDownloadDirectory(self):
        """
        Private slot to check for the existence of the plugins download
        directory.
        """
        downloadDir = Preferences.getPluginManager("DownloadPath")
        if not downloadDir:
            downloadDir = self.__defaultDownloadDir
        
        if not os.path.exists(downloadDir):
            try:
                os.mkdir(downloadDir, 0o755)
            except (OSError, IOError) as err:
                # try again with (possibly) new default
                downloadDir = self.__defaultDownloadDir
                if not os.path.exists(downloadDir):
                    try:
                        os.mkdir(downloadDir, 0o755)
                    except (OSError, IOError) as err:
                        E5MessageBox.critical(
                            self.__ui,
                            self.tr("Plugin Manager Error"),
                            self.tr(
                                """<p>The plugin download directory"""
                                """ <b>{0}</b> could not be created. Please"""
                                """ configure it via the configuration"""
                                """ dialog.</p><p>Reason: {1}</p>""")
                            .format(downloadDir, str(err)))
                        downloadDir = ""
        
        Preferences.setPluginManager("DownloadPath", downloadDir)
    
    def preferencesChanged(self):
        """
        Public slot to react to changes in configuration.
        """
        self.__checkPluginsDownloadDirectory()
    
    ########################################################################
    ## Methods for automatic plug-in update check below
    ########################################################################
    
    def checkPluginUpdatesAvailable(self):
        """
        Public method to check the availability of updates of plug-ins.
        """
        period = Preferences.getPluginManager("UpdatesCheckInterval")
        if period == 0:
            return
        elif period in [1, 2, 3]:
            lastModified = QFileInfo(self.pluginRepositoryFile).lastModified()
            if lastModified.isValid() and lastModified.date().isValid():
                lastModifiedDate = lastModified.date()
                now = QDate.currentDate()
                if period == 1 and lastModifiedDate.day() == now.day():
                    # daily
                    return
                elif period == 2 and lastModifiedDate.daysTo(now) < 7:
                    # weekly
                    return
                elif period == 3 and \
                    (lastModifiedDate.daysTo(now) <
                     lastModifiedDate.daysInMonth()):
                    # monthly
                    return
        
        self.__updateAvailable = False
        
        request = QNetworkRequest(
            QUrl(Preferences.getUI("PluginRepositoryUrl6")))
        request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
                             QNetworkRequest.AlwaysNetwork)
        reply = self.__networkManager.get(request)
        reply.finished.connect(self.__downloadRepositoryFileDone)
        self.__replies.append(reply)
    
    def __downloadRepositoryFileDone(self):
        """
        Private method called after the repository file was downloaded.
        """
        reply = self.sender()
        if reply in self.__replies:
            self.__replies.remove(reply)
        if reply.error() != QNetworkReply.NoError:
            E5MessageBox.warning(
                None,
                self.tr("Error downloading file"),
                self.tr(
                    """<p>Could not download the requested file"""
                    """ from {0}.</p><p>Error: {1}</p>"""
                ).format(Preferences.getUI("PluginRepositoryUrl6"),
                         reply.errorString())
            )
            return
        
        ioDevice = QFile(self.pluginRepositoryFile + ".tmp")
        ioDevice.open(QIODevice.WriteOnly)
        ioDevice.write(reply.readAll())
        ioDevice.close()
        if QFile.exists(self.pluginRepositoryFile):
            QFile.remove(self.pluginRepositoryFile)
        ioDevice.rename(self.pluginRepositoryFile)
        
        if os.path.exists(self.pluginRepositoryFile):
            f = QFile(self.pluginRepositoryFile)
            if f.open(QIODevice.ReadOnly):
                # save current URL
                url = Preferences.getUI("PluginRepositoryUrl6")
                
                # read the repository file
                from E5XML.PluginRepositoryReader import PluginRepositoryReader
                reader = PluginRepositoryReader(f, self.checkPluginEntry)
                reader.readXML()
                if url != Preferences.getUI("PluginRepositoryUrl6"):
                    # redo if it is a redirect
                    self.checkPluginUpdatesAvailable()
                    return
                
                if self.__updateAvailable:
                    res = E5MessageBox.information(
                        None,
                        self.tr("New plugin versions available"),
                        self.tr("<p>There are new plug-ins or plug-in"
                                " updates available. Use the plug-in"
                                " repository dialog to get them.</p>"),
                        E5MessageBox.StandardButtons(
                            E5MessageBox.Ignore |
                            E5MessageBox.Open),
                        E5MessageBox.Open)
                    if res == E5MessageBox.Open:
                        self.__ui.showPluginsAvailable()
    
    def checkPluginEntry(self, name, short, description, url, author, version,
                         filename, status):
        """
        Public method to check a plug-in's data for an update.
        
        @param name data for the name field (string)
        @param short data for the short field (string)
        @param description data for the description field (list of strings)
        @param url data for the url field (string)
        @param author data for the author field (string)
        @param version data for the version field (string)
        @param filename data for the filename field (string)
        @param status status of the plugin (string [stable, unstable, unknown])
        """
        # ignore hidden plug-ins
        pluginName = os.path.splitext(url.rsplit("/", 1)[1])[0]
        if pluginName in Preferences.getPluginManager("HiddenPlugins"):
            return
        
        archive = os.path.join(Preferences.getPluginManager("DownloadPath"),
                               filename)
        
        # Check against installed/loaded plug-ins
        pluginDetails = self.getPluginDetails(pluginName)
        if pluginDetails is None:
            if not Preferences.getPluginManager("CheckInstalledOnly"):
                self.__updateAvailable = True
            return
        
        if pluginDetails["version"] < version:
            self.__updateAvailable = True
            return
        
        if not Preferences.getPluginManager("CheckInstalledOnly"):
            # Check against downloaded plugin archives
            # 1. Check, if the archive file exists
            if not os.path.exists(archive):
                self.__updateAvailable = True
                return
            
            # 2. Check, if the archive is a valid zip file
            if not zipfile.is_zipfile(archive):
                self.__updateAvailable = True
                return
            
            # 3. Check the version of the archive file
            zip = zipfile.ZipFile(archive, "r")
            try:
                aversion = zip.read("VERSION").decode("utf-8")
            except KeyError:
                aversion = ""
            zip.close()
            
            if aversion != version:
                self.__updateAvailable = True
    
    def __sslErrors(self, reply, errors):
        """
        Private slot to handle SSL errors.
        
        @param reply reference to the reply object (QNetworkReply)
        @param errors list of SSL errors (list of QSslError)
        """
        ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0]
        if ignored == E5SslErrorHandler.NotIgnored:
            self.__downloadCancelled = True
예제 #31
0
파일: pac.py 프로젝트: RvstFyth/qutebrowser
class PACFetcher(QObject):
    """Asynchronous fetcher of PAC files."""

    finished = pyqtSignal()

    def __init__(self, url, parent=None):
        """Resolve a PAC proxy from URL.

        Args:
            url: QUrl of a PAC proxy.
        """
        super().__init__(parent)

        pac_prefix = "pac+"

        assert url.scheme().startswith(pac_prefix)
        url.setScheme(url.scheme()[len(pac_prefix):])

        self._pac_url = url
        self._manager = QNetworkAccessManager()
        self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
        self._pac = None
        self._error_message = None
        self._reply = None

    def __eq__(self, other):
        return self._pac_url == other._pac_url

    def __repr__(self):
        return utils.get_repr(self, url=self._pac_url, constructor=True)

    def fetch(self):
        """Fetch the proxy from the remote URL."""
        self._reply = self._manager.get(QNetworkRequest(self._pac_url))
        self._reply.finished.connect(self._finish)

    @pyqtSlot()
    def _finish(self):
        if self._reply.error() != QNetworkReply.NoError:
            error = "Can't fetch PAC file from URL, error code {}: {}"
            self._error_message = error.format(self._reply.error(),
                                               self._reply.errorString())
            log.network.error(self._error_message)
        else:
            try:
                pacscript = bytes(self._reply.readAll()).decode("utf-8")
            except UnicodeError as e:
                error = "Invalid encoding of a PAC file: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
            try:
                self._pac = PACResolver(pacscript)
                log.network.debug("Successfully evaluated PAC file.")
            except EvalProxyError as e:
                error = "Error in PAC evaluation: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
        self._manager = None
        self._reply = None
        self.finished.emit()

    def _wait(self):
        """Wait until a reply from the remote server is received."""
        if self._manager is not None:
            loop = qtutils.EventLoop()
            self.finished.connect(loop.quit)
            loop.exec_()

    def fetch_error(self):
        """Check if PAC script is successfully fetched.

        Return None iff PAC script is downloaded and evaluated successfully,
        error string otherwise.
        """
        self._wait()
        return self._error_message

    def resolve(self, query):
        """Resolve a query via PAC.

        Args: QNetworkProxyQuery.

        Return a list of QNetworkProxy objects in order of preference.
        """
        self._wait()
        from_file = self._pac_url.scheme() == 'file'
        try:
            return self._pac.resolve(query, from_file=from_file)
        except (EvalProxyError, ParseProxyError) as e:
            log.network.exception("Error in PAC resolution: {}.".format(e))
            # .invalid is guaranteed to be inaccessible in RFC 6761.
            # Port 9 is for DISCARD protocol -- DISCARD servers act like
            # /dev/null.
            # Later NetworkManager.createRequest will detect this and display
            # an error message.
            error_host = "pac-resolve-error.qutebrowser.invalid"
            return [QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)]
예제 #32
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal()
    removeDeviceSignal = Signal()
    discoveredDevicesChanged = Signal()
    cloudFlowIsPossible = Signal()

    def __init__(self):
        super().__init__()
        
        self._zero_conf = None
        self._zero_conf_browser = None

        self._application = CuraApplication.getInstance()

        # Create a cloud output device manager that abstracts all cloud connection logic away.
        self._cloud_output_device_manager = CloudOutputDeviceManager()

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        self._application.globalContainerStackChanged.connect(self.reCheckConnections)

        self._discovered_devices = {}
        
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")
        self._min_cloud_version = Version("5.2.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = CuraApplication.getInstance().getPreferences()
        self._preferences.addPreference("um3networkprinting/manual_instances",
                                        "")  # A comma-separated list of ip adresses or hostnames

        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        # Store the last manual entry key
        self._last_manual_entry_key = "" # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._account = self._application.getCuraAPI().account

        # Check if cloud flow is possible when user logs in
        self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)

        # Check if cloud flow is possible when user switches machines
        self._application.globalContainerStackChanged.connect(self._onMachineSwitched)

        # Listen for when cloud flow is possible 
        self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)

        # Listen if cloud cluster was added
        self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured)

        # Listen if cloud cluster was removed
        self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible)

        self._start_cloud_flow_message = None # type: Optional[Message]
        self._cloud_flow_complete_message = None # type: Optional[Message]

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()
        self._cloud_output_device_manager.start()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
                                                 [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    def reCheckConnections(self):
        active_machine = CuraApplication.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    # It should already be set, but if it actually connects we know for sure it's supported!
                    active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
                self.checkCloudFlowIsPossible()
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()
        self._cloud_output_device_manager.stop()

    def removeManualDevice(self, key, address = None):
        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def addManualDevice(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary": b"true"   # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        self._checkManualDevice(address)

    def _checkManualDevice(self, address):
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()

        if "system" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the firmware version!
                return

            try:
                system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            address = reply.url().host()
            has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": (system_info["name"] + " (manual)").encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine": str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the amount of printers the cluster has!
                return
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            address = reply.url().host()
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b"cluster_size"] = len(cluster_printers_list)
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id):
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass

            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        printer_type = properties.get(b"machine", b"").decode("utf-8")
        printer_type_identifiers = {
            "9066": "ultimaker3",
            "9511": "ultimaker3_extended",
            "9051": "ultimaker_s5"
        }

        for key, value in printer_type_identifiers.items():
            if printer_type.startswith(key):
                properties[b"printer_type"] = bytes(value, encoding="utf8")
                break
        else:
            properties[b"printer_type"] = b"Unknown"
        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)

        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
            # Ensure that the configured connection type is set.
            global_container_stack.addConfiguredConnectionType(device.connectionType.value)
            device.connect()
            device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)

            # Stop if the application is shutting down
            if CuraApplication.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = []  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type, name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
                                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be careful
    #   calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties = {})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address, info.properties)
                    else:
                        Logger.log("w",
                                   "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True

    ## Check if the prerequsites are in place to start the cloud flow
    def checkCloudFlowIsPossible(self) -> None:
        Logger.log("d", "Checking if cloud connection is possible...")

        # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
        active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
        if active_machine:
            
            # Check 1A: Printer isn't already configured for cloud
            if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
                Logger.log("d", "Active machine was already configured for cloud.")
                return
            
            # Check 1B: Printer isn't already configured for cloud
            if active_machine.getMetaDataEntry("cloud_flow_complete", False):
                Logger.log("d", "Active machine was already configured for cloud.")
                return

            # Check 2: User did not already say "Don't ask me again"
            if active_machine.getMetaDataEntry("do_not_show_cloud_message", False):
                Logger.log("d", "Active machine shouldn't ask about cloud anymore.")
                return
        
            # Check 3: User is logged in with an Ultimaker account
            if not self._account.isLoggedIn:
                Logger.log("d", "Cloud Flow not possible: User not logged in!")
                return

            # Check 4: Machine is configured for network connectivity
            if not self._application.getMachineManager().activeMachineHasNetworkConnection:
                Logger.log("d", "Cloud Flow not possible: Machine is not connected!")
                return
            
            # Check 5: Machine has correct firmware version
            firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str
            if not Version(firmware_version) > self._min_cloud_version:
                Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)",
                                firmware_version,
                                self._min_cloud_version)
                return
            
            Logger.log("d", "Cloud flow is possible!")
            self.cloudFlowIsPossible.emit()

    def _onCloudFlowPossible(self) -> None:
        # Cloud flow is possible, so show the message
        if not self._start_cloud_flow_message:
            self._createCloudFlowStartMessage()
        if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.show()        

    def _onCloudPrintingConfigured(self) -> None:
        # Hide the cloud flow start message if it was hanging around already
        # For example: if the user already had the browser openen and made the association themselves
        if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        
        # Cloud flow is complete, so show the message
        if not self._cloud_flow_complete_message:
            self._createCloudFlowCompleteMessage()
        if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.show()
        
        # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
        active_machine = self._application.getMachineManager().activeMachine
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
        return

    def _onDontAskMeAgain(self, checked: bool) -> None:
        active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message", checked)
            if checked:
                Logger.log("d", "Will not ask the user again to cloud connect for current printer.")
        return

    def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager().activeMachineAddress # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect"))
            if self._start_cloud_flow_message:
                self._start_cloud_flow_message.hide()
                self._start_cloud_flow_message = None
        return
    
    def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager().activeMachineAddress # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/settings"))
        return

    def _onMachineSwitched(self) -> None:
        # Hide any left over messages
        if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.hide()

        # Check for cloud flow again with newly selected machine
        self.checkCloudFlowIsPossible()

    def _createCloudFlowStartMessage(self):
        self._start_cloud_flow_message = Message(
            text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."),
            lifetime = 0,
            image_source = QUrl.fromLocalFile(os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "svg", "cloud-flow-start.svg"
            )),
            image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"),
            option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
            option_state = False
        )
        self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "")
        self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain)
        self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted)

    def _createCloudFlowCompleteMessage(self):
        self._cloud_flow_complete_message = Message(
            text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."),
            lifetime = 30,
            image_source = QUrl.fromLocalFile(os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "svg", "cloud-flow-completed.svg"
            )),
            image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
        )
        self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
        self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection)
예제 #33
0
파일: Toolbox.py 프로젝트: sfxphil/Cura
class Toolbox(QObject, Extension):
    def __init__(self, application: CuraApplication) -> None:
        super().__init__()

        self._application = application  # type: CuraApplication

        self._sdk_version = ApplicationMetadata.CuraSDKVersion  # type: Union[str, int]
        self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion  # type: str
        self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot  # type: str
        self._api_url = None  # type: Optional[str]

        # Network:
        self._download_request = None  # type: Optional[QNetworkRequest]
        self._download_reply = None  # type: Optional[QNetworkReply]
        self._download_progress = 0  # type: float
        self._is_downloading = False  # type: bool
        self._network_manager = None  # type: Optional[QNetworkAccessManager]
        self._request_headers = []  # type: List[Tuple[bytes, bytes]]
        self._updateRequestHeader()

        self._request_urls = {}  # type: Dict[str, QUrl]
        self._to_update = [
        ]  # type: List[str] # Package_ids that are waiting to be updated
        self._old_plugin_ids = set()  # type: Set[str]
        self._old_plugin_metadata = dict()  # type: Dict[str, Dict[str, Any]]

        # The responses as given by the server parsed to a list.
        self._server_response_data = {
            "authors": [],
            "packages": []
        }  # type: Dict[str, List[Any]]

        # Models:
        self._models = {
            "authors": AuthorsModel(self),
            "packages": PackagesModel(self),
        }  # type: Dict[str, Union[AuthorsModel, PackagesModel]]

        self._plugins_showcase_model = PackagesModel(self)
        self._plugins_available_model = PackagesModel(self)
        self._plugins_installed_model = PackagesModel(self)

        self._materials_showcase_model = AuthorsModel(self)
        self._materials_available_model = AuthorsModel(self)
        self._materials_installed_model = PackagesModel(self)
        self._materials_generic_model = PackagesModel(self)

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin"  # type: str

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "loading"  # type: str

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None  # type: Optional[Dict[str, Any]]

        self._dialog = None  # type: Optional[QObject]
        self._confirm_reset_dialog = None  # type: Optional[QObject]
        self._resetUninstallVariables()

        self._restart_required = False  # type: bool

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = ""  # type: str
        self._license_dialog_license_content = ""  # type: str
        self._license_dialog_plugin_file_location = ""  # type: str
        self._restart_dialog_message = ""  # type: str

        self._application.initializationFinished.connect(
            self._onAppInitialized)
        self._application.getCuraAPI().account.loginStateChanged.connect(
            self._updateRequestHeader)

    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()
    uninstallVariablesChanged = pyqtSignal()

    def _updateRequestHeader(self):
        self._request_headers = [(b"User-Agent",
                                  str.encode("%s/%s (%s %s)" % (
                                      self._application.getApplicationName(),
                                      self._application.getVersion(),
                                      platform.system(),
                                      platform.machine(),
                                  )))]
        access_token = self._application.getCuraAPI().account.accessToken
        if access_token:
            self._request_headers.append(
                (b"Authorization", "Bearer {}".format(access_token).encode()))

    def _resetUninstallVariables(self) -> None:
        self._package_id_to_uninstall = None  # type: Optional[str]
        self._package_name_to_uninstall = ""
        self._package_used_materials = [
        ]  # type: List[Tuple[GlobalStack, str, str]]
        self._package_used_qualities = [
        ]  # type: List[Tuple[GlobalStack, str, str]]

    @pyqtSlot(str, int)
    def ratePackage(self, package_id: str, rating: int) -> None:
        url = QUrl("{base_url}/packages/{package_id}/ratings".format(
            base_url=self._api_url, package_id=package_id))

        self._rate_request = QNetworkRequest(url)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest,
                 self._rate_request).setRawHeader(header_name, header_value)
        data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (
            Version(self._application.getVersion()), rating)
        self._rate_reply = cast(QNetworkAccessManager,
                                self._network_manager).put(
                                    self._rate_request, data.encode())

    @pyqtSlot(result=str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result=str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result=str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str,
                          plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._plugin_registry = self._application.getPluginRegistry()
        self._package_manager = self._application.getPackageManager()
        self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
            cloud_api_root=self._cloud_api_root,
            cloud_api_version=self._cloud_api_version,
            sdk_version=self._sdk_version)
        self._request_urls = {
            "authors":
            QUrl("{base_url}/authors".format(base_url=self._api_url)),
            "packages":
            QUrl("{base_url}/packages".format(base_url=self._api_url))
        }

        # Request the latest and greatest!
        self._fetchPackageData()

    def _fetchPackageData(self):
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(
                self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(
            self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")

        # Gather installed packages:
        self._updateInstalledModels()

    @pyqtSlot()
    def browsePackages(self) -> None:
        self._fetchPackageData()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")

        if not self._dialog:
            Logger.log(
                "e",
                "Unexpected error trying to create the 'Marketplace' dialog.")
            return

        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name)
        plugin_path = PluginRegistry.getInstance().getPluginPath(
            self.getPluginId())
        if not plugin_path:
            return None
        path = os.path.join(plugin_path, "resources", "qml", qml_name)

        dialog = self._application.createQmlComponent(path, {"toolbox": self})
        if not dialog:
            raise Exception("Failed to create Marketplace dialog")
        return dialog

    def _convertPluginMetadata(
            self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        try:
            highest_sdk_version_supported = Version(0)
            for supported_version in plugin_data["plugin"][
                    "supported_sdk_versions"]:
                if supported_version > highest_sdk_version_supported:
                    highest_sdk_version_supported = supported_version

            formatted = {
                "package_id": plugin_data["id"],
                "package_type": "plugin",
                "display_name": plugin_data["plugin"]["name"],
                "package_version": plugin_data["plugin"]["version"],
                "sdk_version": highest_sdk_version_supported,
                "author": {
                    "author_id": plugin_data["plugin"]["author"],
                    "display_name": plugin_data["plugin"]["author"]
                },
                "is_installed": True,
                "description": plugin_data["plugin"]["description"]
            }
            return formatted
        except KeyError:
            Logger.log("w", "Unable to convert plugin meta data %s",
                       str(plugin_data))
            return None

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        # This is moved here to avoid code duplication and so that after installing plugins they get removed from the
        # list of old plugins
        old_plugin_ids = self._plugin_registry.getInstalledPlugins()
        installed_package_ids = self._package_manager.getAllInstalledPackageIDs(
        )
        scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs(
        )

        self._old_plugin_ids = set()
        self._old_plugin_metadata = dict()

        for plugin_id in old_plugin_ids:
            # Neither the installed packages nor the packages that are scheduled to remove are old plugins
            if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
                Logger.log(
                    "i",
                    "Found a plugin that was installed with the old plugin browser: %s",
                    plugin_id)

                old_metadata = self._plugin_registry.getMetaData(plugin_id)
                new_metadata = self._convertPluginMetadata(old_metadata)
                if new_metadata is None:
                    # Something went wrong converting it.
                    continue
                self._old_plugin_ids.add(plugin_id)
                self._old_plugin_metadata[
                    new_metadata["package_id"]] = new_metadata

        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
            # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
            # plugins should be excluded from the old plugins list/dict.
            all_plugin_package_ids = set(package["package_id"]
                                         for package in all_packages["plugin"])
            self._old_plugin_ids = set(
                plugin_id for plugin_id in self._old_plugin_ids
                if plugin_id not in all_plugin_package_ids)
            self._old_plugin_metadata = {
                k: v
                for k, v in self._old_plugin_metadata.items()
                if k in self._old_plugin_ids
            }

            self._plugins_installed_model.setMetadata(
                all_packages["plugin"] +
                list(self._old_plugin_metadata.values()))
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._materials_installed_model.setMetadata(
                all_packages["material"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Check package usage and uninstall
    #   If the package is in use, you'll get a confirmation dialog to set everything to default
    @pyqtSlot(str)
    def checkPackageUsageAndUninstall(self, package_id: str) -> None:
        package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(
            package_id)
        if package_used_materials or package_used_qualities:
            # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
            self._package_id_to_uninstall = package_id
            package_info = self._package_manager.getInstalledPackageInfo(
                package_id)
            self._package_name_to_uninstall = package_info.get(
                "display_name", package_info.get("package_id"))
            self._package_used_materials = package_used_materials
            self._package_used_qualities = package_used_qualities
            # Ask change to default material / profile
            if self._confirm_reset_dialog is None:
                self._confirm_reset_dialog = self._createDialog(
                    "ToolboxConfirmUninstallResetDialog.qml")
            self.uninstallVariablesChanged.emit()
            if self._confirm_reset_dialog is None:
                Logger.log(
                    "e",
                    "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package."
                )
            else:
                self._confirm_reset_dialog.show()
        else:
            # Plain uninstall
            self.uninstall(package_id)

    @pyqtProperty(str, notify=uninstallVariablesChanged)
    def pluginToUninstall(self) -> str:
        return self._package_name_to_uninstall

    @pyqtProperty(str, notify=uninstallVariablesChanged)
    def uninstallUsedMaterials(self) -> str:
        return "\n".join([
            "%s (%s)" % (str(global_stack.getName()), material) for
            global_stack, extruder_nr, material in self._package_used_materials
        ])

    @pyqtProperty(str, notify=uninstallVariablesChanged)
    def uninstallUsedQualities(self) -> str:
        return "\n".join([
            "%s (%s)" % (str(global_stack.getName()), quality) for
            global_stack, extruder_nr, quality in self._package_used_qualities
        ])

    @pyqtSlot()
    def closeConfirmResetDialog(self) -> None:
        if self._confirm_reset_dialog is not None:
            self._confirm_reset_dialog.close()

    ##  Uses "uninstall variables" to reset qualities and materials, then uninstall
    #   It's used as an action on Confirm reset on Uninstall
    @pyqtSlot()
    def resetMaterialsQualitiesAndUninstall(self) -> None:
        application = CuraApplication.getInstance()
        material_manager = application.getMaterialManager()
        quality_manager = application.getQualityManager()
        machine_manager = application.getMachineManager()

        for global_stack, extruder_nr, container_id in self._package_used_materials:
            default_material_node = material_manager.getDefaultMaterial(
                global_stack, extruder_nr,
                global_stack.extruders[extruder_nr].variant.getName())
            machine_manager.setMaterial(extruder_nr,
                                        default_material_node,
                                        global_stack=global_stack)
        for global_stack, extruder_nr, container_id in self._package_used_qualities:
            default_quality_group = quality_manager.getDefaultQualityType(
                global_stack)
            machine_manager.setQualityGroup(default_quality_group,
                                            global_stack=global_stack)

        if self._package_id_to_uninstall is not None:
            self._markPackageMaterialsAsToBeUninstalled(
                self._package_id_to_uninstall)
            self.uninstall(self._package_id_to_uninstall)
        self._resetUninstallVariables()
        self.closeConfirmResetDialog()

    def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
        container_registry = self._application.getContainerRegistry()

        all_containers = self._package_manager.getPackageContainerIds(
            package_id)
        for container_id in all_containers:
            containers = container_registry.findInstanceContainers(
                id=container_id)
            if not containers:
                continue
            container = containers[0]
            if container.getMetaDataEntry("type") != "material":
                continue
            root_material_id = container.getMetaDataEntry("base_file")
            root_material_containers = container_registry.findInstanceContainers(
                id=root_material_id)
            if not root_material_containers:
                continue
            root_material_container = root_material_containers[0]
            root_material_container.setMetaDataEntry("removed", True)

    @pyqtSlot(str)
    def uninstall(self, package_id: str) -> None:
        self._package_manager.removePackage(package_id, force_add=True)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Actual update packages that are in self._to_update
    def _update(self) -> None:
        if self._to_update:
            plugin_id = self._to_update.pop(0)
            remote_package = self.getRemotePackage(plugin_id)
            if remote_package:
                download_url = remote_package["download_url"]
                Logger.log("d", "Updating package [%s]..." % plugin_id)
                self.startDownload(download_url)
            else:
                Logger.log(
                    "e",
                    "Could not update package [%s] because there is no remote package info available.",
                    plugin_id)

        if self._to_update:
            self._application.callLater(self._update)

    ##  Update a plugin by plugin_id
    @pyqtSlot(str)
    def update(self, plugin_id: str) -> None:
        self._to_update.append(plugin_id)
        self._application.callLater(self._update)

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify=metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify=restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self) -> None:
        self._application.windowClosed()

    def getRemotePackage(self, package_id: str) -> Optional[Dict]:
        # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
        remote_package = None
        for package in self._server_response_data["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
                break
        return remote_package

    @pyqtSlot(str, result=bool)
    def canDowngrade(self, package_id: str) -> bool:
        # If the currently installed version is higher than the bundled version (if present), the we can downgrade
        # this package.
        local_package = self._package_manager.getInstalledPackageInfo(
            package_id)
        if local_package is None:
            return False

        bundled_package = self._package_manager.getBundledPackageInfo(
            package_id)
        if bundled_package is None:
            return False

        local_version = Version(local_package["package_version"])
        bundled_version = Version(bundled_package["package_version"])
        return bundled_version < local_version

    @pyqtSlot(str, result=bool)
    def isInstalled(self, package_id: str) -> bool:
        result = self._package_manager.isPackageInstalled(package_id)
        # Also check the old plugins list if it's not found in the package manager.
        if not result:
            result = self.isOldPlugin(package_id)
        return result

    @pyqtSlot(str, result=int)
    def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._materials_installed_model.items:
            if package["author_id"] == author_id:
                count += 1
        return count

    # This slot is only used to get the number of material packages by author, not any other type of packages.
    @pyqtSlot(str, result=int)
    def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._server_response_data["packages"]:
            if package["package_type"] == "material":
                if package["author"]["author_id"] == author_id:
                    count += 1
        return count

    @pyqtSlot(str, result=bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    # Check for plugins that were installed with the old plugin browser
    def isOldPlugin(self, plugin_id: str) -> bool:
        return plugin_id in self._old_plugin_ids

    def getOldPluginPackageMetadata(
            self, plugin_id: str) -> Optional[Dict[str, Any]]:
        return self._old_plugin_metadata.get(plugin_id)

    def isLoadingComplete(self) -> bool:
        populated = 0
        for metadata_list in self._server_response_data.items():
            if metadata_list:
                populated += 1
        return populated == len(self._server_response_data.items())

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, request_type: str) -> None:
        Logger.log("i", "Requesting %s metadata from server.", request_type)
        request = QNetworkRequest(self._request_urls[request_type])
        for header_name, header_value in self._request_headers:
            request.setRawHeader(header_name, header_value)
        if self._network_manager:
            self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Attempting to download & install package from %s.",
                   url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            cast(QNetworkRequest, self._download_request).setAttribute(
                QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            cast(QNetworkRequest, self._download_request).setAttribute(
                QNetworkRequest.RedirectPolicyAttribute, True)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest,
                 self._download_request).setRawHeader(header_name,
                                                      header_value)
        self._download_reply = cast(QNetworkAccessManager,
                                    self._network_manager).get(
                                        self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        cast(QNetworkReply, self._download_reply).downloadProgress.connect(
            self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "User cancelled the download of a package.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            try:
                self._download_reply.downloadProgress.disconnect(
                    self._onDownloadProgress)
            except (
                    TypeError, RuntimeError
            ):  # Raised when the method is not connected to the signal yet.
                pass  # Don't need to disconnect.
            try:
                self._download_reply.abort()
            except RuntimeError:
                # In some cases the garbage collector is a bit to agressive, which causes the dowload_reply
                # to be deleted (especially if the machine has been put to sleep). As we don't know what exactly causes
                # this (The issue probably lives in the bowels of (py)Qt somewhere), we can only catch and ignore it.
                pass
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(
        self, network_accessibility: QNetworkAccessManager.NetworkAccessibility
    ) -> None:
        if network_accessibility == QNetworkAccessManager.NotAccessible:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for response_type, url in self._request_urls.items():
                if reply.url() == url:
                    if reply.attribute(
                            QNetworkRequest.HttpStatusCodeAttribute) == 200:
                        try:
                            json_data = json.loads(
                                bytes(reply.readAll()).decode("utf-8"))

                            # Check for errors:
                            if "errors" in json_data:
                                for error in json_data["errors"]:
                                    Logger.log("e", "%s", error["title"])
                                return

                            # Create model and apply metadata:
                            if not self._models[response_type]:
                                Logger.log("e", "Could not find the %s model.",
                                           response_type)
                                break

                            self._server_response_data[
                                response_type] = json_data["data"]
                            self._models[response_type].setMetadata(
                                self._server_response_data[response_type])

                            if response_type is "packages":
                                self._models[response_type].setFilter(
                                    {"type": "plugin"})
                                self.reBuildMaterialsModels()
                                self.reBuildPluginsModels()
                                self._notifyPackageManager()
                            elif response_type is "authors":
                                self._models[response_type].setFilter(
                                    {"package_types": "material"})
                                self._models[response_type].setFilter(
                                    {"tags": "generic"})

                            self.metadataChanged.emit()

                            if self.isLoadingComplete():
                                self.setViewPage("overview")

                        except json.decoder.JSONDecodeError:
                            Logger.log("w", "Received invalid JSON for %s.",
                                       response_type)
                            break
                    else:
                        Logger.log(
                            "w",
                            "Unable to connect with the server, we got a response code %s while trying to connect to %s",
                            reply.attribute(
                                QNetworkRequest.HttpStatusCodeAttribute),
                            reply.url())
                        self.setViewPage("errored")
                        self.resetDownload()
        elif reply.operation() == QNetworkAccessManager.PutOperation:
            # Ignore any operation that is not a get operation
            pass

    # This function goes through all known remote versions of a package and notifies the package manager of this change
    def _notifyPackageManager(self):
        for package in self._server_response_data["packages"]:
            self._package_manager.addAvailablePackageVersion(
                package["package_id"], Version(package["package_version"]))

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                self._download_reply = cast(QNetworkReply,
                                            self._download_reply)
                self._download_reply.downloadProgress.disconnect(
                    self._onDownloadProgress)

                # Check if the download was sucessfull
                if self._download_reply.attribute(
                        QNetworkRequest.HttpStatusCodeAttribute) != 200:
                    Logger.log(
                        "w",
                        "Failed to download package. The following error was returned: %s",
                        json.loads(
                            bytes(self._download_reply.readAll()).decode(
                                "utf-8")))
                    return
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(
                    mode="w+b", suffix=".curapackage", delete=False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(
                    cast(QNetworkReply, self._download_reply).readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str) -> None:
        Logger.log("i", "Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Package file [%s] was not a valid CuraPackage.",
                       file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content,
                                   file_path)
            return

        self.install(file_path)

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: float) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int,
                  fset=setDownloadProgress,
                  notify=onDownloadProgressChanged)
    def downloadProgress(self) -> float:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset=setIsDownloading, notify=onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        if self._active_package != package:
            self._active_package = package
            self.activePackageChanged.emit()

    ##  The active package is the package that is currently being downloaded
    @pyqtProperty(QObject, fset=setActivePackage, notify=activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        if self._view_category != category:
            self._view_category = category
            self.viewChanged.emit()

    @pyqtProperty(str, fset=setViewCategory, notify=viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        if self._view_page != page:
            self._view_page = page
            self.viewChanged.emit()

    @pyqtProperty(str, fset=setViewPage, notify=viewChanged)
    def viewPage(self) -> str:
        return self._view_page

    # Exposed Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, constant=True)
    def authorsModel(self) -> AuthorsModel:
        return cast(AuthorsModel, self._models["authors"])

    @pyqtProperty(QObject, constant=True)
    def packagesModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["packages"])

    @pyqtProperty(QObject, constant=True)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return self._plugins_showcase_model

    @pyqtProperty(QObject, constant=True)
    def pluginsAvailableModel(self) -> PackagesModel:
        return self._plugins_available_model

    @pyqtProperty(QObject, constant=True)
    def pluginsInstalledModel(self) -> PackagesModel:
        return self._plugins_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsShowcaseModel(self) -> AuthorsModel:
        return self._materials_showcase_model

    @pyqtProperty(QObject, constant=True)
    def materialsAvailableModel(self) -> AuthorsModel:
        return self._materials_available_model

    @pyqtProperty(QObject, constant=True)
    def materialsInstalledModel(self) -> PackagesModel:
        return self._materials_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsGenericModel(self) -> PackagesModel:
        return self._materials_generic_model

    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, model_type: str, filter_type: str,
                          parameter: str) -> None:
        if not self._models[model_type]:
            Logger.log("w",
                       "Couldn't filter %s model because it doesn't exist.",
                       model_type)
            return
        self._models[model_type].setFilter({filter_type: parameter})
        self.filterChanged.emit()

    @pyqtSlot(str, "QVariantMap")
    def setFilters(self, model_type: str, filter_dict: dict) -> None:
        if not self._models[model_type]:
            Logger.log("w",
                       "Couldn't filter %s model because it doesn't exist.",
                       model_type)
            return
        self._models[model_type].setFilter(filter_dict)
        self.filterChanged.emit()

    @pyqtSlot(str)
    def removeFilters(self, model_type: str) -> None:
        if not self._models[model_type]:
            Logger.log(
                "w",
                "Couldn't remove filters on %s model because it doesn't exist.",
                model_type)
            return
        self._models[model_type].setFilter({})
        self.filterChanged.emit()

    # HACK(S):
    # --------------------------------------------------------------------------
    def reBuildMaterialsModels(self) -> None:
        materials_showcase_metadata = []
        materials_available_metadata = []
        materials_generic_metadata = []

        processed_authors = []  # type: List[str]

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "material":

                author = item["author"]
                if author["author_id"] in processed_authors:
                    continue

                # Generic materials to be in the same section
                if "generic" in item["tags"]:
                    materials_generic_metadata.append(item)
                else:
                    if "showcase" in item["tags"]:
                        materials_showcase_metadata.append(author)
                    else:
                        materials_available_metadata.append(author)

                    processed_authors.append(author["author_id"])

        self._materials_showcase_model.setMetadata(materials_showcase_metadata)
        self._materials_available_model.setMetadata(
            materials_available_metadata)
        self._materials_generic_model.setMetadata(materials_generic_metadata)

    def reBuildPluginsModels(self) -> None:
        plugins_showcase_metadata = []
        plugins_available_metadata = []

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "plugin":
                if "showcase" in item["tags"]:
                    plugins_showcase_metadata.append(item)
                else:
                    plugins_available_metadata.append(item)

        self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
        self._plugins_available_model.setMetadata(plugins_available_metadata)
예제 #34
0
class HttpWindow(QDialog):
    def __init__(self, parent=None):
        super(HttpWindow, self).__init__(parent)

        self.url = QUrl()
        self.qnam = QNetworkAccessManager()
        self.reply = None
        self.outFile = None
        self.httpGetId = 0
        self.httpRequestAborted = False

        self.urlLineEdit = QLineEdit('https://www.qt.io')

        urlLabel = QLabel("&URL:")
        urlLabel.setBuddy(self.urlLineEdit)
        self.statusLabel = QLabel(
                "Please enter the URL of a file you want to download.")
        self.statusLabel.setWordWrap(True)

        self.downloadButton = QPushButton("Download")
        self.downloadButton.setDefault(True)
        self.quitButton = QPushButton("Quit")
        self.quitButton.setAutoDefault(False)

        buttonBox = QDialogButtonBox()
        buttonBox.addButton(self.downloadButton, QDialogButtonBox.ActionRole)
        buttonBox.addButton(self.quitButton, QDialogButtonBox.RejectRole)

        self.progressDialog = QProgressDialog(self)

        self.urlLineEdit.textChanged.connect(self.enableDownloadButton)
        self.qnam.authenticationRequired.connect(
                self.slotAuthenticationRequired)
        self.qnam.sslErrors.connect(self.sslErrors)
        self.progressDialog.canceled.connect(self.cancelDownload)
        self.downloadButton.clicked.connect(self.downloadFile)
        self.quitButton.clicked.connect(self.close)

        topLayout = QHBoxLayout()
        topLayout.addWidget(urlLabel)
        topLayout.addWidget(self.urlLineEdit)

        mainLayout = QVBoxLayout()
        mainLayout.addLayout(topLayout)
        mainLayout.addWidget(self.statusLabel)
        mainLayout.addWidget(buttonBox)
        self.setLayout(mainLayout)

        self.setWindowTitle("HTTP")
        self.urlLineEdit.setFocus()

    def startRequest(self, url):
        self.reply = self.qnam.get(QNetworkRequest(url))
        self.reply.finished.connect(self.httpFinished)
        self.reply.readyRead.connect(self.httpReadyRead)
        self.reply.downloadProgress.connect(self.updateDataReadProgress)

    def downloadFile(self):
        self.url = QUrl(self.urlLineEdit.text())
        fileInfo = QFileInfo(self.url.path())
        fileName = fileInfo.fileName()

        if not fileName:
            fileName = 'index.html'

        if QFile.exists(fileName):
            ret = QMessageBox.question(self, "HTTP",
                    "There already exists a file called %s in the current "
                    "directory. Overwrite?" % fileName,
                    QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

            if ret == QMessageBox.No:
                return

            QFile.remove(fileName)

        self.outFile = QFile(fileName)
        if not self.outFile.open(QIODevice.WriteOnly):
            QMessageBox.information(self, "HTTP",
                    "Unable to save the file %s: %s." % (fileName, self.outFile.errorString()))
            self.outFile = None
            return

        self.progressDialog.setWindowTitle("HTTP")
        self.progressDialog.setLabelText("Downloading %s." % fileName)
        self.downloadButton.setEnabled(False)

        self.httpRequestAborted = False
        self.startRequest(self.url)

    def cancelDownload(self):
        self.statusLabel.setText("Download canceled.")
        self.httpRequestAborted = True
        if self.reply is not None:
            self.reply.abort()
        self.downloadButton.setEnabled(True)

    def httpFinished(self):
        if self.httpRequestAborted:
            if self.outFile is not None:
                self.outFile.close()
                self.outFile.remove()
                self.outFile = None

            self.reply.deleteLater()
            self.reply = None
            self.progressDialog.hide()
            return

        self.progressDialog.hide()
        self.outFile.flush()
        self.outFile.close()

        redirectionTarget = self.reply.attribute(QNetworkRequest.RedirectionTargetAttribute)

        if self.reply.error():
            self.outFile.remove()
            QMessageBox.information(self, "HTTP",
                    "Download failed: %s." % self.reply.errorString())
            self.downloadButton.setEnabled(True)
        elif redirectionTarget is not None:
            newUrl = self.url.resolved(redirectionTarget)

            ret = QMessageBox.question(self, "HTTP",
                    "Redirect to %s?" % newUrl.toString(),
                    QMessageBox.Yes | QMessageBox.No)

            if ret == QMessageBox.Yes:
                self.url = newUrl
                self.reply.deleteLater()
                self.reply = None
                self.outFile.open(QIODevice.WriteOnly)
                self.outFile.resize(0)
                self.startRequest(self.url)
                return
        else:
            fileName = QFileInfo(QUrl(self.urlLineEdit.text()).path()).fileName()
            self.statusLabel.setText("Downloaded %s to %s." % (fileName, QDir.currentPath()))

            self.downloadButton.setEnabled(True)

        self.reply.deleteLater()
        self.reply = None
        self.outFile = None

    def httpReadyRead(self):
        if self.outFile is not None:
            self.outFile.write(self.reply.readAll())

    def updateDataReadProgress(self, bytesRead, totalBytes):
        if self.httpRequestAborted:
            return

        self.progressDialog.setMaximum(totalBytes)
        self.progressDialog.setValue(bytesRead)

    def enableDownloadButton(self):
        self.downloadButton.setEnabled(self.urlLineEdit.text() != '')

    def slotAuthenticationRequired(self, authenticator):
        import os
        from PyQt5 import uic

        ui = os.path.join(os.path.dirname(__file__), 'authenticationdialog.ui')
        dlg = uic.loadUi(ui)
        dlg.adjustSize()
        dlg.siteDescription.setText("%s at %s" % (authenticator.realm(), self.url.host()))

        dlg.userEdit.setText(self.url.userName())
        dlg.passwordEdit.setText(self.url.password())

        if dlg.exec_() == QDialog.Accepted:
            authenticator.setUser(dlg.userEdit.text())
            authenticator.setPassword(dlg.passwordEdit.text())

    def sslErrors(self, reply, errors):
        errorString = ", ".join([str(error.errorString()) for error in errors])

        ret = QMessageBox.warning(self, "HTTP Example",
                "One or more SSL errors has occurred: %s" % errorString,
                QMessageBox.Ignore | QMessageBox.Abort)

        if ret == QMessageBox.Ignore:
            self.reply.ignoreSslErrors()
예제 #35
0
파일: apps_table.py 프로젝트: vinifmor/bauh
class PackagesTable(QTableWidget):
    COL_NUMBER = 9
    DEFAULT_ICON_SIZE = QSize(16, 16)

    def __init__(self, parent: QWidget, icon_cache: MemoryCache,
                 download_icons: bool, screen_width: int):
        super(PackagesTable, self).__init__()
        self.screen_width = screen_width
        self.setObjectName('table_packages')
        self.setParent(parent)
        self.window = parent
        self.download_icons = download_icons
        self.setColumnCount(self.COL_NUMBER)
        self.setFocusPolicy(Qt.NoFocus)
        self.setShowGrid(False)
        self.verticalHeader().setVisible(False)
        self.horizontalHeader().setVisible(False)
        self.horizontalHeader().setSizePolicy(QSizePolicy.Minimum,
                                              QSizePolicy.Preferred)
        self.setSelectionBehavior(QTableView.SelectRows)
        self.setHorizontalHeaderLabels(('' for _ in range(self.columnCount())))
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
        self.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))

        self.network_man = QNetworkAccessManager()
        self.network_man.finished.connect(self._load_icon_and_cache)

        self.icon_cache = icon_cache
        self.lock_async_data = Lock()
        self.setRowHeight(80, 80)
        self.cache_type_icon = {}
        self.i18n = self.window.i18n

    def has_any_settings(self, pkg: PackageView):
        return pkg.model.has_history() or \
               pkg.model.can_be_downgraded() or \
               pkg.model.supports_ignored_updates() or \
               bool(pkg.model.get_custom_actions())

    def show_pkg_actions(self, pkg: PackageView):
        menu_row = QMenu()
        menu_row.setSizePolicy(QSizePolicy.MinimumExpanding,
                               QSizePolicy.Preferred)
        menu_row.setObjectName('app_actions')
        menu_row.setCursor(QCursor(Qt.PointingHandCursor))

        if pkg.model.installed:

            if pkg.model.has_history():

                def show_history():
                    self.window.begin_show_history(pkg)

                menu_row.addAction(
                    QCustomMenuAction(
                        parent=menu_row,
                        label=self.
                        i18n["manage_window.apps_table.row.actions.history"],
                        action=show_history,
                        button_name='app_history'))

            if pkg.model.can_be_downgraded():

                def downgrade():
                    if ConfirmationDialog(
                            title=self.i18n[
                                'manage_window.apps_table.row.actions.downgrade'],
                            body=self._parag(self.i18n[
                                'manage_window.apps_table.row.actions.downgrade.popup.body']
                                             .format(self._bold(str(pkg)))),
                            i18n=self.i18n).ask():
                        self.window.begin_downgrade(pkg)

                menu_row.addAction(
                    QCustomMenuAction(
                        parent=menu_row,
                        label=self.
                        i18n["manage_window.apps_table.row.actions.downgrade"],
                        action=downgrade,
                        button_name='app_downgrade'))

            if pkg.model.supports_ignored_updates():
                if pkg.model.is_update_ignored():
                    action_label = self.i18n[
                        "manage_window.apps_table.row.actions.ignore_updates_reverse"]
                    button_name = 'revert_ignore_updates'
                else:
                    action_label = self.i18n[
                        "manage_window.apps_table.row.actions.ignore_updates"]
                    button_name = 'ignore_updates'

                def ignore_updates():
                    self.window.begin_ignore_updates(pkg)

                menu_row.addAction(
                    QCustomMenuAction(parent=menu_row,
                                      label=action_label,
                                      button_name=button_name,
                                      action=ignore_updates))

        custom_actions = pkg.model.get_custom_actions()
        if custom_actions:
            menu_row.addActions((self._map_custom_action(pkg, a, menu_row)
                                 for a in custom_actions))

        menu_row.adjustSize()
        menu_row.popup(QCursor.pos())
        menu_row.exec_()

    def _map_custom_action(self, pkg: PackageView,
                           action: CustomSoftwareAction,
                           parent: QWidget) -> QCustomMenuAction:
        def custom_action():
            if action.i18n_confirm_key:
                body = self.i18n[action.i18n_confirm_key].format(
                    bold(pkg.model.name))
            else:
                body = '{} ?'.format(self.i18n[action.i18n_label_key])

            if not action.requires_confirmation or ConfirmationDialog(
                    icon=QIcon(pkg.model.get_type_icon_path()),
                    title=self.i18n[action.i18n_label_key],
                    body=self._parag(body),
                    i18n=self.i18n).ask():
                self.window.begin_execute_custom_action(pkg, action)

        tip = self.i18n[
            action.
            i18n_description_key] if action.i18n_description_key else None
        return QCustomMenuAction(
            parent=parent,
            label=self.i18n[action.i18n_label_key],
            icon=QIcon(action.icon_path) if action.icon_path else None,
            tooltip=tip,
            action=custom_action)

    def refresh(self, pkg: PackageView):
        self._update_row(pkg,
                         update_check_enabled=False,
                         change_update_col=False)

    def update_package(self,
                       pkg: PackageView,
                       change_update_col: bool = False):
        if self.download_icons and pkg.model.icon_url:
            icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
            icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                      True)
            self.network_man.get(icon_request)

        self._update_row(pkg, change_update_col=change_update_col)

    def _uninstall(self, pkg: PackageView):
        if ConfirmationDialog(
                title=self.i18n[
                    'manage_window.apps_table.row.actions.uninstall.popup.title'],
                body=self._parag(self.i18n[
                    'manage_window.apps_table.row.actions.uninstall.popup.body']
                                 .format(self._bold(str(pkg)))),
                i18n=self.i18n).ask():
            self.window.begin_uninstall(pkg)

    def _bold(self, text: str) -> str:
        return '<span style="font-weight: bold">{}</span>'.format(text)

    def _parag(self, text: str) -> str:
        return '<p>{}</p>'.format(text)

    def _install_app(self, pkgv: PackageView):

        body = self.i18n[
            'manage_window.apps_table.row.actions.install.popup.body'].format(
                self._bold(str(pkgv)))

        warning = self.i18n.get('gem.{}.install.warning'.format(
            pkgv.model.get_type().lower()))

        if warning:
            body += '<br/><br/> {}'.format('<br/>'.join(
                ('{}.'.format(phrase) for phrase in warning.split('.')
                 if phrase)))

        if ConfirmationDialog(title=self.i18n[
                'manage_window.apps_table.row.actions.install.popup.title'],
                              body=self._parag(body),
                              i18n=self.i18n).ask():
            self.window.install(pkgv)

    def _load_icon_and_cache(self, http_response: QNetworkReply):
        icon_url = http_response.request().url().toString()

        icon_data = self.icon_cache.get(icon_url)
        icon_was_cached = True

        if not icon_data:
            icon_bytes = http_response.readAll()

            if not icon_bytes:
                return

            icon_was_cached = False
            pixmap = QPixmap()
            pixmap.loadFromData(icon_bytes)

            if not pixmap.isNull():
                icon = QIcon(pixmap)
                icon_data = {'icon': icon, 'bytes': icon_bytes}
                self.icon_cache.add(icon_url, icon_data)

        if icon_data:
            for idx, app in enumerate(self.window.pkgs):
                if app.model.icon_url == icon_url:
                    self._update_icon(self.cellWidget(idx, 0),
                                      icon_data['icon'])

                    if app.model.supports_disk_cache(
                    ) and app.model.get_disk_icon_path(
                    ) and icon_data['bytes']:
                        if not icon_was_cached or not os.path.exists(
                                app.model.get_disk_icon_path()):
                            self.window.manager.cache_to_disk(
                                pkg=app.model,
                                icon_bytes=icon_data['bytes'],
                                only_icon=True)

    def update_packages(self,
                        pkgs: List[PackageView],
                        update_check_enabled: bool = True):
        self.setRowCount(
            0)  # removes the overwrite effect when updates the table
        self.setEnabled(True)

        if pkgs:
            self.setColumnCount(
                self.COL_NUMBER if update_check_enabled else self.COL_NUMBER -
                1)
            self.setRowCount(len(pkgs))

            for idx, pkg in enumerate(pkgs):
                pkg.table_index = idx

                if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url:
                    icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
                    icon_request.setAttribute(
                        QNetworkRequest.FollowRedirectsAttribute, True)
                    self.network_man.get(icon_request)

                self._update_row(pkg, update_check_enabled)

            self.scrollToTop()

    def _update_row(self,
                    pkg: PackageView,
                    update_check_enabled: bool = True,
                    change_update_col: bool = True):
        self._set_col_icon(0, pkg)
        self._set_col_name(1, pkg)
        self._set_col_version(2, pkg)
        self._set_col_description(3, pkg)
        self._set_col_publisher(4, pkg)
        self._set_col_type(5, pkg)
        self._set_col_installed(6, pkg)
        self._set_col_actions(7, pkg)

        if change_update_col and update_check_enabled:
            if pkg.model.installed and not pkg.model.is_update_ignored(
            ) and pkg.model.update:
                col_update = QCustomToolbar()
                col_update.add_space()
                col_update.add_widget(
                    UpgradeToggleButton(pkg=pkg,
                                        root=self.window,
                                        i18n=self.i18n,
                                        checked=pkg.update_checked if
                                        pkg.model.can_be_updated() else False,
                                        clickable=pkg.model.can_be_updated()))
                col_update.add_space()
            else:
                col_update = QLabel()

            self.setCellWidget(pkg.table_index, 8, col_update)

    def _gen_row_button(self,
                        text: str,
                        name: str,
                        callback,
                        tip: Optional[str] = None) -> QToolButton:
        col_bt = QToolButton()
        col_bt.setProperty('text_only', 'true')
        col_bt.setObjectName(name)
        col_bt.setCursor(QCursor(Qt.PointingHandCursor))
        col_bt.setText(text)
        col_bt.clicked.connect(callback)

        if tip:
            col_bt.setToolTip(tip)

        return col_bt

    def _set_col_installed(self, col: int, pkg: PackageView):
        toolbar = QCustomToolbar()
        toolbar.add_space()

        if pkg.model.installed:
            if pkg.model.can_be_uninstalled():

                def uninstall():
                    self._uninstall(pkg)

                item = self._gen_row_button(
                    text=self.i18n['uninstall'].capitalize(),
                    name='bt_uninstall',
                    callback=uninstall,
                    tip=self.i18n['manage_window.bt_uninstall.tip'])
            else:
                item = None

        elif pkg.model.can_be_installed():

            def install():
                self._install_app(pkg)

            item = self._gen_row_button(
                text=self.i18n['install'].capitalize(),
                name='bt_install',
                callback=install,
                tip=self.i18n['manage_window.bt_install.tip'])
        else:
            item = None

        toolbar.add_widget(item)
        toolbar.add_space()
        self.setCellWidget(pkg.table_index, col, toolbar)

    def _set_col_type(self, col: int, pkg: PackageView):
        icon_data = self.cache_type_icon.get(pkg.model.get_type())

        if icon_data is None:
            icon = QIcon(pkg.model.get_type_icon_path())
            pixmap = icon.pixmap(self._get_icon_size(icon))
            icon_data = {
                'px': pixmap,
                'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())
            }
            self.cache_type_icon[pkg.model.get_type()] = icon_data

        col_type_icon = QLabel()
        col_type_icon.setProperty('icon', 'true')
        col_type_icon.setSizePolicy(QSizePolicy.MinimumExpanding,
                                    QSizePolicy.Preferred)
        col_type_icon.setPixmap(icon_data['px'])
        col_type_icon.setToolTip(icon_data['tip'])
        self.setCellWidget(pkg.table_index, col, col_type_icon)

    def _set_col_version(self, col: int, pkg: PackageView):
        label_version = QLabel(
            str(pkg.model.version if pkg.model.version else '?'))
        label_version.setObjectName('app_version')
        label_version.setAlignment(Qt.AlignCenter)

        item = QWidget()
        item.setProperty('container', 'true')
        item.setLayout(QHBoxLayout())
        item.layout().addWidget(label_version)

        if pkg.model.version:
            tooltip = self.i18n[
                'version.installed'] if pkg.model.installed else self.i18n[
                    'version']
        else:
            tooltip = self.i18n['version.unknown']

        if pkg.model.installed and pkg.model.update and not pkg.model.is_update_ignored(
        ):
            label_version.setProperty('update', 'true')
            tooltip = pkg.model.get_update_tip(
            ) or self.i18n['version.installed_outdated']

        if pkg.model.installed and pkg.model.is_update_ignored():
            label_version.setProperty('ignored', 'true')
            tooltip = self.i18n['version.updates_ignored']

        if pkg.model.installed and pkg.model.update and not pkg.model.is_update_ignored(
        ) and pkg.model.version and pkg.model.latest_version and pkg.model.version != pkg.model.latest_version:
            tooltip = f"{tooltip} ({self.i18n['version.installed']}: {pkg.model.version}  |  " \
                      f"{self.i18n['version.latest']}: {pkg.model.latest_version})"
            label_version.setText(
                f"{label_version.text()} > {pkg.model.latest_version}")

            if label_version.sizeHint().width() / self.screen_width > 0.22:
                label_version.setText(pkg.model.latest_version)

        item.setToolTip(tooltip)
        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_icon(self, col: int, pkg: PackageView):
        icon_path = pkg.model.get_disk_icon_path()
        if pkg.model.installed and pkg.model.supports_disk_cache(
        ) and icon_path:
            if icon_path.startswith('/'):
                if os.path.isfile(icon_path):
                    with open(icon_path, 'rb') as f:
                        icon_bytes = f.read()
                        pixmap = QPixmap()
                        pixmap.loadFromData(icon_bytes)
                        icon = QIcon(pixmap)
                        self.icon_cache.add_non_existing(
                            pkg.model.icon_url, {
                                'icon': icon,
                                'bytes': icon_bytes
                            })
                else:
                    icon = QIcon(pkg.model.get_default_icon_path())
            else:
                try:
                    icon = QIcon.fromTheme(icon_path)

                    if icon.isNull():
                        icon = QIcon(pkg.model.get_default_icon_path())
                    elif pkg.model.icon_url:
                        self.icon_cache.add_non_existing(
                            pkg.model.icon_url, {
                                'icon': icon,
                                'bytes': None
                            })

                except:
                    icon = QIcon(pkg.model.get_default_icon_path())

        elif not pkg.model.icon_url:
            icon = QIcon(pkg.model.get_default_icon_path())
        else:
            icon_data = self.icon_cache.get(pkg.model.icon_url)
            icon = icon_data['icon'] if icon_data else QIcon(
                pkg.model.get_default_icon_path())

        col_icon = QLabel()
        col_icon.setProperty('icon', 'true')
        col_icon.setSizePolicy(QSizePolicy.MinimumExpanding,
                               QSizePolicy.Preferred)
        self._update_icon(col_icon, icon)
        self.setCellWidget(pkg.table_index, col, col_icon)

    def _set_col_name(self, col: int, pkg: PackageView):
        col_name = QLabel()
        col_name.setObjectName('app_name')
        col_name.setSizePolicy(QSizePolicy.MinimumExpanding,
                               QSizePolicy.Preferred)

        name = pkg.model.get_display_name().strip()
        if name:
            col_name.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(),
                                                pkg.model.get_name_tooltip()))
        else:
            name = '...'
            col_name.setToolTip(self.i18n['app.name'].lower())

        col_name.setText(name)
        screen_perc = col_name.sizeHint().width() / self.screen_width

        if screen_perc > 0.15:
            max_chars = int(len(name) * 0.15 / screen_perc) - 3
            col_name.setText(name[0:max_chars] + '...')

        self.setCellWidget(pkg.table_index, col, col_name)

    def _update_icon(self, label: QLabel, icon: QIcon):
        label.setPixmap(icon.pixmap(self._get_icon_size(icon)))

    def _get_icon_size(self, icon: QIcon) -> QSize:
        sizes = icon.availableSizes()
        return sizes[-1] if sizes else self.DEFAULT_ICON_SIZE

    def _set_col_description(self, col: int, pkg: PackageView):
        item = QLabel()
        item.setObjectName('app_description')

        if pkg.model.description is not None or not pkg.model.is_application(
        ) or pkg.model.status == PackageStatus.READY:
            desc = pkg.model.description.split(
                '\n')[0] if pkg.model.description else pkg.model.description
        else:
            desc = '...'

        if desc and desc != '...':
            desc = strip_html(desc)

        item.setText(desc)

        current_width_perc = item.sizeHint().width() / self.screen_width
        if current_width_perc > 0.18:
            max_width = int(len(desc) * 0.18 / current_width_perc) - 3
            desc = desc[0:max_width] + '...'
            item.setText(desc)

        if pkg.model.description:
            item.setToolTip(pkg.model.description)

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_publisher(self, col: int, pkg: PackageView):
        item = QToolBar()

        publisher = pkg.model.get_publisher()
        full_publisher = None

        lb_name = QLabel()
        lb_name.setObjectName('app_publisher')

        if publisher:
            publisher = publisher.strip()
            full_publisher = publisher

            if publisher:
                lb_name.setText(publisher)
                screen_perc = lb_name.sizeHint().width() / self.screen_width

                if screen_perc > 0.12:
                    max_chars = int(len(publisher) * 0.12 / screen_perc) - 3
                    publisher = publisher[0:max_chars] + '...'
                    lb_name.setText(publisher)

        if not publisher:
            if not pkg.model.installed:
                lb_name.setProperty('publisher_known', 'false')

            publisher = self.i18n['unknown']

        lb_name.setText(f'  {publisher}')
        item.addWidget(lb_name)

        if publisher and full_publisher:
            lb_name.setToolTip(self.i18n['publisher'].capitalize() + (
                (': ' + full_publisher) if full_publisher else ''))

            if pkg.model.is_trustable():
                lb_verified = QLabel()
                lb_verified.setObjectName('icon_publisher_verified')
                lb_verified.setCursor(QCursor(Qt.WhatsThisCursor))
                lb_verified.setToolTip(
                    self.i18n['publisher.verified'].capitalize())
                item.addWidget(lb_verified)
            else:
                lb_name.setText(lb_name.text() + "   ")

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_actions(self, col: int, pkg: PackageView):
        toolbar = QCustomToolbar()
        toolbar.setObjectName('app_actions')
        toolbar.add_space()

        if pkg.model.installed:

            def run():
                self.window.begin_launch_package(pkg)

            bt = IconButton(i18n=self.i18n,
                            action=run,
                            tooltip=self.i18n['action.run.tooltip'])
            bt.setObjectName('app_run')

            if not pkg.model.can_be_run():
                bt.setEnabled(False)
                bt.setProperty('_enabled', 'false')

            toolbar.layout().addWidget(bt)

        settings = self.has_any_settings(pkg)

        if pkg.model.installed:

            def handle_custom_actions():
                self.show_pkg_actions(pkg)

            bt = IconButton(i18n=self.i18n,
                            action=handle_custom_actions,
                            tooltip=self.i18n['action.settings.tooltip'])
            bt.setObjectName('app_actions')
            bt.setEnabled(bool(settings))
            toolbar.layout().addWidget(bt)

        if not pkg.model.installed:

            def show_screenshots():
                self.window.begin_show_screenshots(pkg)

            bt = IconButton(i18n=self.i18n,
                            action=show_screenshots,
                            tooltip=self.i18n['action.screenshots.tooltip'])
            bt.setObjectName('app_screenshots')

            if not pkg.model.has_screenshots():
                bt.setEnabled(False)
                bt.setProperty('_enabled', 'false')

            toolbar.layout().addWidget(bt)

        def show_info():
            self.window.begin_show_info(pkg)

        bt = IconButton(i18n=self.i18n,
                        action=show_info,
                        tooltip=self.i18n['action.info.tooltip'])
        bt.setObjectName('app_info')
        bt.setEnabled(bool(pkg.model.has_info()))
        toolbar.layout().addWidget(bt)

        self.setCellWidget(pkg.table_index, col, toolbar)

    def change_headers_policy(
            self,
            policy: QHeaderView = QHeaderView.ResizeToContents,
            maximized: bool = False):
        header_horizontal = self.horizontalHeader()
        for i in range(self.columnCount()):
            if maximized:
                if i in (2, 3):
                    header_horizontal.setSectionResizeMode(
                        i, QHeaderView.Stretch)
                else:
                    header_horizontal.setSectionResizeMode(
                        i, QHeaderView.ResizeToContents)
            else:
                header_horizontal.setSectionResizeMode(i, policy)

    def get_width(self):
        return reduce(operator.add,
                      [self.columnWidth(i) for i in range(self.columnCount())])
예제 #36
0
class SlippyMap(QObject):

    updated = pyqtSignal(QRect)

    def __init__(self, parent=None):
        super(SlippyMap, self).__init__(parent)

        self._offset = QPoint()
        self._tilesRect = QRect()
        self._tilePixmaps = {} # Point(x, y) to QPixmap mapping
        self._manager = QNetworkAccessManager()
        self._url = QUrl()
        # public vars
        self.width = 400
        self.height = 300
        self.zoom = 15
        self.latitude = 59.9138204
        self.longitude = 10.7387413

        self._emptyTile = QPixmap(TDIM, TDIM)
        self._emptyTile.fill(Qt.lightGray)

        cache = QNetworkDiskCache()
        cache.setCacheDirectory(
            QStandardPaths.writableLocation(QStandardPaths.CacheLocation))
        self._manager.setCache(cache)
        self._manager.finished.connect(self.handleNetworkData)

    def invalidate(self):
        if self.width <= 0 or self.height <= 0:
            return

        ct = tileForCoordinate(self.latitude, self.longitude, self.zoom)
        tx = ct.x()
        ty = ct.y()

        # top-left corner of the center tile
        xp = int(self.width / 2 - (tx - math.floor(tx)) * TDIM)
        yp = int(self.height / 2 - (ty - math.floor(ty)) * TDIM)

        # first tile vertical and horizontal
        xa = (xp + TDIM - 1) / TDIM
        ya = (yp + TDIM - 1) / TDIM
        xs = int(tx) - xa
        ys = int(ty) - ya

        # offset for top-left tile
        self._offset = QPoint(xp - xa * TDIM, yp - ya * TDIM)

        # last tile vertical and horizontal
        xe = int(tx) + (self.width - xp - 1) / TDIM
        ye = int(ty) + (self.height - yp - 1) / TDIM

        # build a rect
        self._tilesRect = QRect(xs, ys, xe - xs + 1, ye - ys + 1)

        if self._url.isEmpty():
            self.download()

        self.updated.emit(QRect(0, 0, self.width, self.height))

    def render(self, p, rect):
        for x in range(self._tilesRect.width()):
            for y in range(self._tilesRect.height()):
                tp = Point(x + self._tilesRect.left(), y + self._tilesRect.top())
                box = self.tileRect(tp)
                if rect.intersects(box):
                    p.drawPixmap(box, self._tilePixmaps.get(tp, self._emptyTile))
   
    def pan(self, delta):
        dx = QPointF(delta) / float(TDIM)
        center = tileForCoordinate(self.latitude, self.longitude, self.zoom) - dx
        self.latitude = latitudeFromTile(center.y(), self.zoom)
        self.longitude = longitudeFromTile(center.x(), self.zoom)
        self.invalidate()

    # slots
    def handleNetworkData(self, reply):
        img = QImage()
        tp = Point(reply.request().attribute(QNetworkRequest.User))
        url = reply.url()
        if not reply.error():
            if img.load(reply, None):
                self._tilePixmaps[tp] = QPixmap.fromImage(img)
        reply.deleteLater()
        self.updated.emit(self.tileRect(tp))

        # purge unused tiles
        bound = self._tilesRect.adjusted(-2, -2, 2, 2)
        for tp in list(self._tilePixmaps.keys()):
            if not bound.contains(tp):
                del self._tilePixmaps[tp]
        self.download()

    def download(self):
        grab = None
        for x in range(self._tilesRect.width()):
            for y in range(self._tilesRect.height()):
                tp = Point(self._tilesRect.topLeft() + QPoint(x, y))
                if tp not in self._tilePixmaps:
                    grab = QPoint(tp)
                    break

        if grab is None:
            self._url = QUrl()
            return

        path = 'http://tile.openstreetmap.org/%d/%d/%d.png' % (self.zoom, grab.x(), grab.y())
        self._url = QUrl(path)
        request = QNetworkRequest()
        request.setUrl(self._url)
        request.setRawHeader(b'User-Agent', b'Nokia (PyQt) Graphics Dojo 1.0')
        request.setAttribute(QNetworkRequest.User, grab)
        self._manager.get(request)

    def tileRect(self, tp):
        t = tp - self._tilesRect.topLeft()
        x = t.x() * TDIM + self._offset.x()
        y = t.y() * TDIM + self._offset.y()

        return QRect(x, y, TDIM, TDIM)
예제 #37
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal()
    removeDeviceSignal = Signal()
    discoveredDevicesChanged = Signal()

    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._zero_conf_browser = None

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        self._discovered_devices = {}
        
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = Application.getInstance().getPreferences()
        self._preferences.addPreference("um3networkprinting/manual_instances",
                                        "")  # A comma-separated list of ip adresses or hostnames

        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        # Store the last manual entry key
        self._last_manual_entry_key = "" # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
                                                 [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def removeManualDevice(self, key, address = None):
        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def addManualDevice(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary": b"true"   # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        self._checkManualDevice(address)

    def _checkManualDevice(self, address):
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()

        if "system" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the firmware version!
                return

            try:
                system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            address = reply.url().host()
            has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": (system_info["name"] + " (manual)").encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine": str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the amount of printers the cluster has!
                return
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            address = reply.url().host()
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b'cluster_size'] = len(cluster_printers_list)
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id):
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass

            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)

        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
            device.connect()
            device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)

            # Stop if the application is shutting down
            if Application.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = []  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type, name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
                                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address, info.properties)
                    else:
                        Logger.log("w",
                                   "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}
        self._cluster_printers_seen = {
        }  # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(
            self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")

        self._network_requests_buffer = {
        }  # store api responses until data is complete

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [
                printer_name for printer_name in self._printers
            ]
            self._printers = {}
            self.printerListChanged.emit()
        # After network switching, one must make a new instance of Zeroconf
        # On windows, the instance creation is very fast (unnoticable). Other platforms?
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf,
                                       u'_ultimaker._tcp.local.',
                                       [self._onServiceChanged])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true"
        }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        self.checkManualPrinter(address)
        self.checkClusterPrinter(address)

    def removeManualPrinter(self, key, address=None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

    def checkManualPrinter(self, address):
        # Check if a printer exists at this address
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix +
                   "system?origin=manual_name")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def checkClusterPrinter(self, address):
        cluster_url = QUrl("http://" + address + self._cluster_api_prefix +
                           "printers/?origin=check_cluster")
        cluster_request = QNetworkRequest(cluster_url)
        self._network_manager.get(cluster_request)

    ##  Handler for all requests that have finished.
    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if reply.operation() == QNetworkAccessManager.GetOperation:
            address = reply.url().host()
            if "origin=manual_name" in reply_url:  # Name returned from printer.
                if status_code == 200:

                    try:
                        system_info = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.JSONDecodeError:
                        Logger.log("e", "Printer returned invalid JSON.")
                        return
                    except UnicodeDecodeError:
                        Logger.log("e", "Printer returned incorrect UTF-8.")
                        return

                    if address not in self._network_requests_buffer:
                        self._network_requests_buffer[address] = {}
                    self._network_requests_buffer[address][
                        "system"] = system_info
            elif "origin=check_cluster" in reply_url:
                if address not in self._network_requests_buffer:
                    self._network_requests_buffer[address] = {}
                if status_code == 200:
                    # We know it's a cluster printer
                    Logger.log("d", "Cluster printer detected: [%s]",
                               reply.url())
                    self._network_requests_buffer[address]["cluster"] = True
                else:
                    Logger.log("d",
                               "This url is not from a cluster printer: [%s]",
                               reply.url())
                    self._network_requests_buffer[address]["cluster"] = False

            # Both the system call and cluster call are finished
            if (address in self._network_requests_buffer
                    and "system" in self._network_requests_buffer[address]
                    and "cluster" in self._network_requests_buffer[address]):

                instance_name = "manual:%s" % address
                system_info = self._network_requests_buffer[address]["system"]
                is_cluster = self._network_requests_buffer[address]["cluster"]
                machine = "unknown"
                if "variant" in system_info:
                    variant = system_info["variant"]
                    if variant == "Ultimaker 3":
                        machine = "9066"
                    elif variant == "Ultimaker 3 Extended":
                        machine = "9511"

                properties = {
                    b"name": system_info["name"].encode("utf-8"),
                    b"address": address.encode("utf-8"),
                    b"firmware_version":
                    system_info["firmware"].encode("utf-8"),
                    b"manual": b"true",
                    b"machine": machine.encode("utf-8")
                }
                if instance_name in self._printers:
                    # Only replace the printer if it is still in the list of (manual) printers
                    self.removePrinter(instance_name)
                    self.addPrinter(instance_name,
                                    address,
                                    properties,
                                    force_cluster=is_cluster)

                del self._network_requests_buffer[address]

    ##  Stop looking for devices on network.
    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("um_network_key"):
                if not self._printers[key].isConnected():
                    Logger.log("d", "Connecting [%s]..." % key)
                    self._printers[key].connect()
                    self._printers[key].connectionStateChanged.connect(
                        self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    Logger.log("d", "Closing connection [%s]..." % key)
                    self._printers[key].close()
                    self._printers[key].connectionStateChanged.disconnect(
                        self._onPrinterConnectionStateChanged)

    ##  Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
    def addPrinter(self, name, address, properties, force_cluster=False):
        cluster_size = int(properties.get(b"cluster_size", -1))
        was_cluster_before = name in self._cluster_printers_seen
        if was_cluster_before:
            Logger.log(
                "d",
                "Printer [%s] had Cura Connect before, so assume it's still equipped with Cura Connect.",
                name)
        if force_cluster or cluster_size >= 0 or was_cluster_before:
            printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
                name, address, properties, self._api_prefix, self._plugin_path)
        else:
            printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(
                name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        self._cluster_printers_seen[printer.getKey(
        )] = name  # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack and printer.getKey(
        ) == global_container_stack.getMetaDataEntry("um_network_key"):
            if printer.getKey(
            ) not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                Logger.log("d",
                           "addPrinter, connecting [%s]..." % printer.getKey())
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(
                    self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.disconnect()
                printer.connectionStateChanged.disconnect(
                    self._onPrinterConnectionStateChanged)
                Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
        self.printerListChanged.emit()

    ##  Handler for when the connection state of one of the detected printers changes
    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        if self._printers[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    ##  Handler for zeroConf detection
    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addPrinterSignal.emit(str(name), address,
                                                   info.properties)
                    else:
                        Logger.log(
                            "w",
                            "The type of the found device is '%s', not 'printer'! Ignoring.."
                            % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))

    ##  For cluster below
    def _get_plugin_directory_name(self):
        current_file_absolute_path = os.path.realpath(__file__)
        directory_path = os.path.dirname(current_file_absolute_path)
        _, directory_name = os.path.split(directory_path)
        return directory_name

    @property
    def _plugin_path(self):
        return PluginRegistry.getInstance().getPluginPath(
            self._get_plugin_directory_name())

    @pyqtSlot()
    def openControlPanel(self):
        Logger.log("d", "Opening print jobs web UI...")
        selected_device = self.getOutputDeviceManager().getActiveDevice()
        if isinstance(
                selected_device, NetworkClusterPrinterOutputDevice.
                NetworkClusterPrinterOutputDevice):
            QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))
예제 #39
0
class CloudApiClient:

    # The cloud URL to use for this remote cluster.
    ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
    CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
    CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)

    # 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:
        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:
        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]) -> None:
        url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT)
        reply = self._manager.get(self._createEmptyRequest(url))
        self._addCallback(reply, on_finished, CloudClusterResponse)

    ## 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)
        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:
        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]):
        self._upload = ToolPathUploader(self._manager, print_job, mesh,
                                        on_finished, on_progress, on_error)
        self._upload.start()

    # Requests a cluster to print the given print job.
    #  \param cluster_id: The ID of the cluster.
    #  \param job_id: The ID of the print job.
    #  \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)
        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:
        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:
        request = QNetworkRequest(QUrl(path))
        if content_type:
            request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
        access_token = self._account.accessToken
        if access_token:
            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]]:
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        try:
            response = bytes(reply.readAll()).decode()
            return status_code, json.loads(response)
        except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
            error = CloudError(code=type(err).__name__,
                               title=str(err),
                               http_code=str(status_code),
                               id=str(time()),
                               http_status="500")
            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:
        if "data" in response:
            data = response["data"]
            if isinstance(data, list):
                results = [model_class(**c)
                           for c in data]  # type: List[CloudApiClientModel]
                on_finished_list = cast(
                    Callable[[List[CloudApiClientModel]], Any], on_finished)
                on_finished_list(results)
            else:
                result = model_class(**data)  # type: CloudApiClientModel
                on_finished_item = cast(Callable[[CloudApiClientModel], Any],
                                        on_finished)
                on_finished_item(result)
        elif "errors" in response:
            self._on_error(
                [CloudError(**error) for error in response["errors"]])
        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]) -> None:
        def parse() -> None:
            self._anti_gc_callbacks.remove(parse)

            # Don't try to parse the reply if we didn't get one
            if reply.attribute(
                    QNetworkRequest.HttpStatusCodeAttribute) is None:
                return

            status_code, response = self._parseReply(reply)
            self._parseModels(response, on_finished, model)

        self._anti_gc_callbacks.append(parse)
        reply.finished.connect(parse)
예제 #40
0
파일: Toolbox.py 프로젝트: rwreynolds/Cura
class Toolbox(QObject, Extension):
    DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com"  # type: str
    DEFAULT_CLOUD_API_VERSION = 1  # type: int

    def __init__(self, application: CuraApplication) -> None:
        super().__init__()

        self._application = application  # type: CuraApplication

        self._sdk_version = None  # type: Optional[Union[str, int]]
        self._cloud_api_version = None  # type: Optional[int]
        self._cloud_api_root = None  # type: Optional[str]
        self._api_url = None  # type: Optional[str]

        # Network:
        self._download_request = None  # type: Optional[QNetworkRequest]
        self._download_reply = None  # type: Optional[QNetworkReply]
        self._download_progress = 0  # type: float
        self._is_downloading = False  # type: bool
        self._network_manager = None  # type: Optional[QNetworkAccessManager]
        self._request_headers = [] # type: List[Tuple[bytes, bytes]]
        self._updateRequestHeader()


        self._request_urls = {}  # type: Dict[str, QUrl]
        self._to_update = []  # type: List[str] # Package_ids that are waiting to be updated
        self._old_plugin_ids = set()  # type: Set[str]
        self._old_plugin_metadata = dict()  # type: Dict[str, Dict[str, Any]]

        # The responses as given by the server parsed to a list.
        self._server_response_data = {
            "authors":             [],
            "packages":            []
        }  # type: Dict[str, List[Any]]

        # Models:
        self._models = {
            "authors":             AuthorsModel(self),
            "packages":            PackagesModel(self),
        }  # type: Dict[str, Union[AuthorsModel, PackagesModel]]

        self._plugins_showcase_model = PackagesModel(self)
        self._plugins_available_model = PackagesModel(self)
        self._plugins_installed_model = PackagesModel(self)

        self._materials_showcase_model = AuthorsModel(self)
        self._materials_available_model = AuthorsModel(self)
        self._materials_installed_model = PackagesModel(self)
        self._materials_generic_model = PackagesModel(self)

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin"  # type: str

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "loading"  # type: str

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None  # type: Optional[Dict[str, Any]]

        self._dialog = None  # type: Optional[QObject]
        self._confirm_reset_dialog = None  # type: Optional[QObject]
        self._resetUninstallVariables()

        self._restart_required = False  # type: bool

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = ""  # type: str
        self._license_dialog_license_content = ""  # type: str
        self._license_dialog_plugin_file_location = ""  # type: str
        self._restart_dialog_message = ""  # type: str

        self._application.initializationFinished.connect(self._onAppInitialized)
        self._application.getCuraAPI().account.loginStateChanged.connect(self._updateRequestHeader)

    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()
    uninstallVariablesChanged = pyqtSignal()

    def _updateRequestHeader(self):
        self._request_headers = [
            (b"User-Agent",
            str.encode(
                "%s/%s (%s %s)" % (
                    self._application.getApplicationName(),
                    self._application.getVersion(),
                    platform.system(),
                    platform.machine(),
                )
            ))
        ]
        access_token = self._application.getCuraAPI().account.accessToken
        if access_token:
            self._request_headers.append((b"Authorization", "Bearer {}".format(access_token).encode()))

    def _resetUninstallVariables(self) -> None:
        self._package_id_to_uninstall = None  # type: Optional[str]
        self._package_name_to_uninstall = ""
        self._package_used_materials = []  # type: List[Tuple[GlobalStack, str, str]]
        self._package_used_qualities = []  # type: List[Tuple[GlobalStack, str, str]]

    @pyqtSlot(str, int)
    def ratePackage(self, package_id: str, rating: int) -> None:
        url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url=self._api_url, package_id = package_id))

        self._rate_request = QNetworkRequest(url)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest, self._rate_request).setRawHeader(header_name, header_value)
        data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating)
        self._rate_reply = cast(QNetworkAccessManager, self._network_manager).put(self._rate_request, data.encode())

    @pyqtSlot(result = str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result = str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result = str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._plugin_registry = self._application.getPluginRegistry()
        self._package_manager = self._application.getPackageManager()
        self._sdk_version = self._getSDKVersion()
        self._cloud_api_version = self._getCloudAPIVersion()
        self._cloud_api_root = self._getCloudAPIRoot()
        self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
            cloud_api_root = self._cloud_api_root,
            cloud_api_version = self._cloud_api_version,
            sdk_version = self._sdk_version
        )
        self._request_urls = {
            "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
            "packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
        }

    # Get the API root for the packages API depending on Cura version settings.
    def _getCloudAPIRoot(self) -> str:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_ROOT
        if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        if not cura.CuraVersion.CuraCloudAPIRoot:  # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        return cura.CuraVersion.CuraCloudAPIRoot  # type: ignore

    # Get the cloud API version from CuraVersion
    def _getCloudAPIVersion(self) -> int:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_VERSION
        if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        if not cura.CuraVersion.CuraCloudAPIVersion:  # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        return cura.CuraVersion.CuraCloudAPIVersion  # type: ignore

    # Get the packages version depending on Cura version settings.
    def _getSDKVersion(self) -> Union[int, str]:
        if not hasattr(cura, "CuraVersion"):
            return self._application.getAPIVersion().getMajor()
        if not hasattr(cura.CuraVersion, "CuraSDKVersion"):  # type: ignore
            return self._application.getAPIVersion().getMajor()
        if not cura.CuraVersion.CuraSDKVersion:  # type: ignore
            return self._application.getAPIVersion().getMajor()
        return cura.CuraVersion.CuraSDKVersion  # type: ignore

    @pyqtSlot()
    def browsePackages(self) -> None:
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")

        # Gather installed packages:
        self._updateInstalledModels()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")

        if not self._dialog:
            Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.")
            return

        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name)
        plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
        if not plugin_path:
            return None
        path = os.path.join(plugin_path, "resources", "qml", qml_name)
        
        dialog = self._application.createQmlComponent(path, {"toolbox": self})
        if not dialog:
            raise Exception("Failed to create Marketplace dialog")
        return dialog

    def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        try:
            formatted = {
                "package_id": plugin_data["id"],
                "package_type": "plugin",
                "display_name": plugin_data["plugin"]["name"],
                "package_version": plugin_data["plugin"]["version"],
                "sdk_version": plugin_data["plugin"]["api"],
                "author": {
                    "author_id": plugin_data["plugin"]["author"],
                    "display_name": plugin_data["plugin"]["author"]
                },
                "is_installed": True,
                "description": plugin_data["plugin"]["description"]
            }
            return formatted
        except KeyError:
            Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
            return None

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        # This is moved here to avoid code duplication and so that after installing plugins they get removed from the
        # list of old plugins
        old_plugin_ids = self._plugin_registry.getInstalledPlugins()
        installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
        scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()

        self._old_plugin_ids = set()
        self._old_plugin_metadata = dict()

        for plugin_id in old_plugin_ids:
            # Neither the installed packages nor the packages that are scheduled to remove are old plugins
            if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
                Logger.log("i", "Found a plugin that was installed with the old plugin browser: %s", plugin_id)

                old_metadata = self._plugin_registry.getMetaData(plugin_id)
                new_metadata = self._convertPluginMetadata(old_metadata)
                if new_metadata is None:
                    # Something went wrong converting it.
                    continue
                self._old_plugin_ids.add(plugin_id)
                self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata

        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
            # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
            # plugins should be excluded from the old plugins list/dict.
            all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
            self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
                                    if plugin_id not in all_plugin_package_ids)
            self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}

            self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._materials_installed_model.setMetadata(all_packages["material"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Check package usage and uninstall
    #   If the package is in use, you'll get a confirmation dialog to set everything to default
    @pyqtSlot(str)
    def checkPackageUsageAndUninstall(self, package_id: str) -> None:
        package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id)
        if package_used_materials or package_used_qualities:
            # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
            self._package_id_to_uninstall = package_id
            package_info = self._package_manager.getInstalledPackageInfo(package_id)
            self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id"))
            self._package_used_materials = package_used_materials
            self._package_used_qualities = package_used_qualities
            # Ask change to default material / profile
            if self._confirm_reset_dialog is None:
                self._confirm_reset_dialog = self._createDialog("ToolboxConfirmUninstallResetDialog.qml")
            self.uninstallVariablesChanged.emit()
            if self._confirm_reset_dialog is None:
                Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.")
            else:
                self._confirm_reset_dialog.show()
        else:
            # Plain uninstall
            self.uninstall(package_id)

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def pluginToUninstall(self) -> str:
        return self._package_name_to_uninstall

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedMaterials(self) -> str:
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedQualities(self) -> str:
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])

    @pyqtSlot()
    def closeConfirmResetDialog(self) -> None:
        if self._confirm_reset_dialog is not None:
            self._confirm_reset_dialog.close()

    ##  Uses "uninstall variables" to reset qualities and materials, then uninstall
    #   It's used as an action on Confirm reset on Uninstall
    @pyqtSlot()
    def resetMaterialsQualitiesAndUninstall(self) -> None:
        application = CuraApplication.getInstance()
        material_manager = application.getMaterialManager()
        quality_manager = application.getQualityManager()
        machine_manager = application.getMachineManager()

        for global_stack, extruder_nr, container_id in self._package_used_materials:
            default_material_node = material_manager.getDefaultMaterial(global_stack, extruder_nr, global_stack.extruders[extruder_nr].variant.getName())
            machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack)
        for global_stack, extruder_nr, container_id in self._package_used_qualities:
            default_quality_group = quality_manager.getDefaultQualityType(global_stack)
            machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)

        if self._package_id_to_uninstall is not None:
            self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
            self.uninstall(self._package_id_to_uninstall)
        self._resetUninstallVariables()
        self.closeConfirmResetDialog()

    def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
        container_registry = self._application.getContainerRegistry()

        all_containers = self._package_manager.getPackageContainerIds(package_id)
        for container_id in all_containers:
            containers = container_registry.findInstanceContainers(id = container_id)
            if not containers:
                continue
            container = containers[0]
            if container.getMetaDataEntry("type") != "material":
                continue
            root_material_id = container.getMetaDataEntry("base_file")
            root_material_containers = container_registry.findInstanceContainers(id = root_material_id)
            if not root_material_containers:
                continue
            root_material_container = root_material_containers[0]
            root_material_container.setMetaDataEntry("removed", True)

    @pyqtSlot(str)
    def uninstall(self, package_id: str) -> None:
        self._package_manager.removePackage(package_id, force_add = True)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Actual update packages that are in self._to_update
    def _update(self) -> None:
        if self._to_update:
            plugin_id = self._to_update.pop(0)
            remote_package = self.getRemotePackage(plugin_id)
            if remote_package:
                download_url = remote_package["download_url"]
                Logger.log("d", "Updating package [%s]..." % plugin_id)
                self.startDownload(download_url)
            else:
                Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id)

        if self._to_update:
            self._application.callLater(self._update)

    ##  Update a plugin by plugin_id
    @pyqtSlot(str)
    def update(self, plugin_id: str) -> None:
        self._to_update.append(plugin_id)
        self._application.callLater(self._update)

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify = metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify = restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self) -> None:
        self._application.windowClosed()

    def getRemotePackage(self, package_id: str) -> Optional[Dict]:
        # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
        remote_package = None
        for package in self._server_response_data["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
                break
        return remote_package

    # Checks
    # --------------------------------------------------------------------------
    @pyqtSlot(str, result = bool)
    def canUpdate(self, package_id: str) -> bool:
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            local_package = self.getOldPluginPackageMetadata(package_id)
            if local_package is None:
                return False

        remote_package = self.getRemotePackage(package_id)
        if remote_package is None:
            return False

        local_version = Version(local_package["package_version"])
        remote_version = Version(remote_package["package_version"])
        can_upgrade = False
        if remote_version > local_version:
            can_upgrade = True
        # A package with the same version can be built to have different SDK versions. So, for a package with the same
        # version, we also need to check if the current one has a lower SDK version. If so, this package should also
        # be upgradable.
        elif remote_version == local_version:
            # First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system).
            remote_sdk_version = Version(remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0)))
            local_sdk_version = Version(local_package.get("sdk_version_semver", local_package.get("sdk_version", 0)))
            can_upgrade = local_sdk_version < remote_sdk_version

        return can_upgrade

    @pyqtSlot(str, result = bool)
    def canDowngrade(self, package_id: str) -> bool:
        # If the currently installed version is higher than the bundled version (if present), the we can downgrade
        # this package.
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        bundled_package = self._package_manager.getBundledPackageInfo(package_id)
        if bundled_package is None:
            return False

        local_version = Version(local_package["package_version"])
        bundled_version = Version(bundled_package["package_version"])
        return bundled_version < local_version

    @pyqtSlot(str, result = bool)
    def isInstalled(self, package_id: str) -> bool:
        result = self._package_manager.isPackageInstalled(package_id)
        # Also check the old plugins list if it's not found in the package manager.
        if not result:
            result = self.isOldPlugin(package_id)
        return result

    @pyqtSlot(str, result = int)
    def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._materials_installed_model.items:
            if package["author_id"] == author_id:
                count += 1
        return count

    # This slot is only used to get the number of material packages by author, not any other type of packages.
    @pyqtSlot(str, result = int)
    def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._server_response_data["packages"]:
            if package["package_type"] == "material":
                if package["author"]["author_id"] == author_id:
                    count += 1
        return count

    @pyqtSlot(str, result = bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    # Check for plugins that were installed with the old plugin browser
    def isOldPlugin(self, plugin_id: str) -> bool:
        return plugin_id in self._old_plugin_ids

    def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
        return self._old_plugin_metadata.get(plugin_id)

    def isLoadingComplete(self) -> bool:
        populated = 0
        for metadata_list in self._server_response_data.items():
            if metadata_list:
                populated += 1
        return populated == len(self._server_response_data.items())

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, request_type: str) -> None:
        Logger.log("i", "Requesting %s metadata from server.", request_type)
        request = QNetworkRequest(self._request_urls[request_type])
        for header_name, header_value in self._request_headers:
            request.setRawHeader(header_name, header_value)
        if self._network_manager:
            self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Attempting to download & install package from %s.", url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value)
        self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "User cancelled the download of a package.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            try:
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
            except TypeError:  # Raised when the method is not connected to the signal yet.
                pass  # Don't need to disconnect.
            self._download_reply.abort()
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(self, network_accessibility: QNetworkAccessManager.NetworkAccessibility) -> None:
        if network_accessibility == QNetworkAccessManager.NotAccessible:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for response_type, url in self._request_urls.items():
                if reply.url() == url:
                    if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
                        try:
                            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                            # Check for errors:
                            if "errors" in json_data:
                                for error in json_data["errors"]:
                                    Logger.log("e", "%s", error["title"])
                                return

                            # Create model and apply metadata:
                            if not self._models[response_type]:
                                Logger.log("e", "Could not find the %s model.", response_type)
                                break
                            
                            self._server_response_data[response_type] = json_data["data"]
                            self._models[response_type].setMetadata(self._server_response_data[response_type])

                            if response_type is "packages":
                                self._models[response_type].setFilter({"type": "plugin"})
                                self.reBuildMaterialsModels()
                                self.reBuildPluginsModels()
                            elif response_type is "authors":
                                self._models[response_type].setFilter({"package_types": "material"})
                                self._models[response_type].setFilter({"tags": "generic"})

                            self.metadataChanged.emit()

                            if self.isLoadingComplete():
                                self.setViewPage("overview")

                        except json.decoder.JSONDecodeError:
                            Logger.log("w", "Received invalid JSON for %s.", response_type)
                            break
                    else:
                        self.setViewPage("errored")
                        self.resetDownload()
        elif reply.operation() == QNetworkAccessManager.PutOperation:
            # Ignore any operation that is not a get operation
            pass

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                self._download_reply = cast(QNetworkReply, self._download_reply)
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
                
                # Check if the download was sucessfull
                if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                    Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
                    return
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str) -> None:
        Logger.log("i", "Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content, file_path)
            return

        self.install(file_path)

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: float) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
    def downloadProgress(self) -> float:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        if self._active_package != package:
            self._active_package = package
            self.activePackageChanged.emit()

    ##  The active package is the package that is currently being downloaded
    @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        if self._view_category != category:
            self._view_category = category
            self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        if self._view_page != page:
            self._view_page = page
            self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewPage, notify = viewChanged)
    def viewPage(self) -> str:
        return self._view_page

    # Exposed Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, constant=True)
    def authorsModel(self) -> AuthorsModel:
        return cast(AuthorsModel, self._models["authors"])

    @pyqtProperty(QObject, constant=True)
    def packagesModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["packages"])

    @pyqtProperty(QObject, constant=True)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return self._plugins_showcase_model

    @pyqtProperty(QObject, constant=True)
    def pluginsAvailableModel(self) -> PackagesModel:
        return self._plugins_available_model

    @pyqtProperty(QObject, constant=True)
    def pluginsInstalledModel(self) -> PackagesModel:
        return self._plugins_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsShowcaseModel(self) -> AuthorsModel:
        return self._materials_showcase_model

    @pyqtProperty(QObject, constant=True)
    def materialsAvailableModel(self) -> AuthorsModel:
        return self._materials_available_model

    @pyqtProperty(QObject, constant=True)
    def materialsInstalledModel(self) -> PackagesModel:
        return self._materials_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsGenericModel(self) -> PackagesModel:
        return self._materials_generic_model

    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({filter_type: parameter})
        self.filterChanged.emit()

    @pyqtSlot(str, "QVariantMap")
    def setFilters(self, model_type: str, filter_dict: dict) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter(filter_dict)
        self.filterChanged.emit()

    @pyqtSlot(str)
    def removeFilters(self, model_type: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({})
        self.filterChanged.emit()

    # HACK(S):
    # --------------------------------------------------------------------------
    def reBuildMaterialsModels(self) -> None:
        materials_showcase_metadata = []
        materials_available_metadata = []
        materials_generic_metadata = []

        processed_authors = []  # type: List[str]

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "material":

                author = item["author"]
                if author["author_id"] in processed_authors:
                    continue

                # Generic materials to be in the same section
                if "generic" in item["tags"]:
                    materials_generic_metadata.append(item)
                else:
                    if "showcase" in item["tags"]:
                        materials_showcase_metadata.append(author)
                    else:
                        materials_available_metadata.append(author)

                    processed_authors.append(author["author_id"])

        self._materials_showcase_model.setMetadata(materials_showcase_metadata)
        self._materials_available_model.setMetadata(materials_available_metadata)
        self._materials_generic_model.setMetadata(materials_generic_metadata)

    def reBuildPluginsModels(self) -> None:
        plugins_showcase_metadata = []
        plugins_available_metadata = []

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "plugin":
                if "showcase" in item["tags"]:
                    plugins_showcase_metadata.append(item)
                else:
                    plugins_available_metadata.append(item)

        self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
        self._plugins_available_model.setMetadata(plugins_available_metadata)
예제 #41
0
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, port, properties):
        super().__init__(key)

        self._address = address
        self._port = port
        self._path = properties.get(b"path", b"/").decode("utf-8")
        if self._path[-1:] != "/":
            self._path += "/"
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None
        self._auto_print = True

        ##  We start with a single extruder, but update this when we get data from octoprint
        self._num_extruders_set = False
        self._num_extruders = 1

        self._api_version = "1"
        self._api_prefix = "api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        protocol = "https" if properties.get(b'useHttps') == b"true" else "http"
        self._base_url = "%s://%s:%d%s" % (protocol, self._address, self._port, self._path)
        self._api_url = self._base_url + self._api_prefix
        self._camera_url = "%s://%s:8080/?action=stream" % (protocol, self._address)

        self.setPriority(2) # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")
        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None
        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._command_request = None
        self._command_reply = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_image_id = 0
        self._camera_image = QImage()

        self._connection_state_before_timeout = None

        self._last_response_time = None
        self._last_request_time = None
        self._response_timeout_time = 5
        self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec.
        self._recreate_network_manager_count = 1

        self._preheat_timer = QTimer()
        self._preheat_timer.setSingleShot(True)
        self._preheat_timer.timeout.connect(self.cancelPreheatBed)

    def getProperties(self):
        return self._properties

    @pyqtSlot(str, result = str)
    def getProperty(self, key):
        key = key.encode("utf-8")
        if key in self._properties:
            return self._properties.get(key, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result = str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant = True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self):
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self):
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self):
        return self._base_url

    def _startCamera(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if not global_container_stack or not parseBool(global_container_stack.getMetaDataEntry("octoprint_show_camera", False)):
            return

        # Start streaming mjpg stream
        url = QUrl(self._camera_url)
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)

    def _stopCamera(self):
        if self._image_reply:
            self._image_reply.abort()
            self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
            self._image_reply = None
        self._image_request = None

        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._camera_image = QImage()
        self.newImage.emit()

    def _update(self):
        if self._last_response_time:
            time_since_last_response = time() - self._last_response_time
        else:
            time_since_last_response = 0
        if self._last_request_time:
            time_since_last_request = time() - self._last_request_time
        else:
            time_since_last_request = float("inf") # An irrelevantly large number of seconds

        # Connection is in timeout, check if we need to re-start the connection.
        # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
        # Re-creating the QNetworkManager seems to fix this issue.
        if self._last_response_time and self._connection_state_before_timeout:
            if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
                self._recreate_network_manager_count += 1
                # It can happen that we had a very long timeout (multiple times the recreate time).
                # In that case we should jump through the point that the next update won't be right away.
                while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
                    self._recreate_network_manager_count += 1
                Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response)
                self._createNetworkManager()
                return

        # Check if we have an connection in the first place.
        if not self._manager.networkAccessible():
            if not self._connection_state_before_timeout:
                Logger.log("d", "The network connection seems to be disabled. Going into timeout mode")
                self._connection_state_before_timeout = self._connection_state
                self.setConnectionState(ConnectionState.error)
                self._connection_message = Message(i18n_catalog.i18nc("@info:status",
                                                                      "The connection with the network was lost."))
                self._connection_message.show()
                # Check if we were uploading something. Abort if this is the case.
                # Some operating systems handle this themselves, others give weird issues.
                try:
                    if self._post_reply:
                        Logger.log("d", "Stopping post upload because the connection was lost.")
                        try:
                            self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
                        except TypeError:
                            pass  # The disconnection can fail on mac in some cases. Ignore that.

                        self._post_reply.abort()
                        self._progress_message.hide()
                except RuntimeError:
                    self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
            return
        else:
            if not self._connection_state_before_timeout:
                self._recreate_network_manager_count = 1

        # Check that we aren't in a timeout state
        if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
            if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
                # Go into timeout state.
                Logger.log("d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections."))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        url = QUrl(self._api_url + "printer")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl(self._api_url + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def _createNetworkManager(self):
        if self._manager:
            self._manager.finished.disconnect(self._onRequestFinished)

        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()

        self._stopCamera()

    def requestWrite(self, node, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
        self.writeStarted.emit(self)
        self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        self._createNetworkManager()

        self.setConnectionState(ConnectionState.connecting)
        self._update()  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url)
        self._update_timer.start()

        self._last_response_time = None
        self.setAcceptsCommands(False)
        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format(self._key))

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url)
        self.close()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify = newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"

        if command:
            self._sendJobCommand(command)

    def startPrint(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if not global_container_stack:
            return

        if self.jobState not in ["ready", ""]:
            if self.jobState == "offline":
                self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is offline. Unable to start a new job."))
            else:
                self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job."))
            self._error_message.show()
            return

        self._preheat_timer.stop()

        self._auto_print = parseBool(global_container_stack.getMetaDataEntry("octoprint_auto_print", True))
        if self._auto_print:
            Application.getInstance().showPrintMonitor.emit(True)

        try:
            self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1)
            self._progress_message.addAction("Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
            self._progress_message.actionTriggered.connect(self._cancelSendGcode)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            last_process_events = time()
            for line in self._gcode:
                single_string_file_data += line
                if time() > last_process_events + 0.05:
                    # Ensure that the GUI keeps updated at least 20 times per second.
                    QCoreApplication.processEvents()
                    last_process_events = time()

            job_name = Application.getInstance().getPrintInformation().jobName.strip()
            if job_name is "":
                job_name = "untitled_print"
            file_name = "%s.gcode" % job_name

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            if self._auto_print:
                self._post_part = QHttpPart()
                self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
                self._post_part.setBody(b"true")
                self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            destination = "local"
            if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)):
                destination = "sdcard"

            url = QUrl(self._api_url + "files/" + destination)

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request, self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log("e", "An exception occurred in network connection: %s" % str(e))

    def _cancelSendGcode(self, message_id, action_id):
        if self._post_reply:
            Logger.log("d", "Stopping upload because the user pressed cancel.")
            try:
                self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
            except TypeError:
                pass  # The disconnection can fail on mac in some cases. Ignore that.

            self._post_reply.abort()
            self._post_reply = None
        self._progress_message.hide()

    def _sendCommand(self, command):
        self._sendCommandToApi("printer/command", command)
        Logger.log("d", "Sent gcode command to OctoPrint instance: %s", command)

    def _sendJobCommand(self, command):
        self._sendCommandToApi("job", command)
        Logger.log("d", "Sent job command to OctoPrint instance: %s", command)

    def _sendCommandToApi(self, endpoint, command):
        url = QUrl(self._api_url + endpoint)
        self._command_request = QNetworkRequest(url)
        self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")

        data = "{\"command\": \"%s\"}" % command
        self._command_reply = self._manager.post(self._command_request, data.encode())

    ##  Pre-heats the heated bed of the printer.
    #
    #   \param temperature The temperature to heat the bed to, in degrees
    #   Celsius.
    #   \param duration How long the bed should stay warm, in seconds.
    @pyqtSlot(float, float)
    def preheatBed(self, temperature, duration):
        self._setTargetBedTemperature(temperature)
        if duration > 0:
            self._preheat_timer.setInterval(duration * 1000)
            self._preheat_timer.start()
        else:
            self._preheat_timer.stop()

    ##  Cancels pre-heating the heated bed of the printer.
    #
    #   If the bed is not pre-heated, nothing happens.
    @pyqtSlot()
    def cancelPreheatBed(self):
        self._setTargetBedTemperature(0)
        self._preheat_timer.stop()

    def _setTargetBedTemperature(self, temperature):
        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    def _setTargetHotendTemperature(self, index, temperature):
        Logger.log("d", "Setting hotend %s temperature to %s", index, temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    def _setHeadPosition(self, x, y , z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Y%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
        self._sendCommand("G90")

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if self._api_prefix + "printer" in reply.url().toString():  # Status update from /printer.
                if http_status_code == 200:
                    if not self.acceptsCommands:
                        self.setAcceptsCommands(True)
                        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    if "temperature" in json_data:
                        if not self._num_extruders_set:
                            self._num_extruders = 0
                            while "tool%d" % self._num_extruders in json_data["temperature"]:
                                self._num_extruders = self._num_extruders + 1

                            # Reinitialise from PrinterOutputDevice to match the new _num_extruders
                            self._hotend_temperatures = [0] * self._num_extruders
                            self._target_hotend_temperatures = [0] * self._num_extruders

                            self._num_extruders_set = True

                        # Check for hotend temperatures
                        for index in range(0, self._num_extruders):
                            temperature = json_data["temperature"]["tool%d" % index]["actual"] if ("tool%d" % index) in json_data["temperature"] else 0
                            self._setHotendTemperature(index, temperature)

                        bed_temperature = json_data["temperature"]["bed"]["actual"] if "bed" in json_data["temperature"] else 0
                        self._setBedTemperature(bed_temperature)

                    job_state = "offline"
                    if "state" in json_data:
                        if json_data["state"]["flags"]["error"]:
                            job_state = "error"
                        elif json_data["state"]["flags"]["paused"]:
                            job_state = "paused"
                        elif json_data["state"]["flags"]["printing"]:
                            job_state = "printing"
                        elif json_data["state"]["flags"]["ready"]:
                            job_state = "ready"
                    self._updateJobState(job_state)

                elif http_status_code == 401:
                    self._updateJobState("offline")
                    self.setConnectionText(i18n_catalog.i18nc("@info:status", "OctoPrint on {0} does not allow access to print").format(self._key))
                elif http_status_code == 409:
                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)

                    self._updateJobState("offline")
                    self.setConnectionText(i18n_catalog.i18nc("@info:status", "The printer connected to OctoPrint on {0} is not operational").format(self._key))
                else:
                    self._updateJobState("offline")
                    Logger.log("w", "Received an unexpected returncode: %d", http_status_code)

            elif self._api_prefix + "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
                        elif progress > 0:
                            self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if self._api_prefix + "files" in reply.url().toString():  # Result from /files command:
                if http_status_code == 201:
                    Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()
                global_container_stack = Application.getInstance().getGlobalContainerStack()
                if not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(reply.header(QNetworkRequest.LocationHeader).toString()).fileName()
                        message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint as {0}").format(file_name))
                    else:
                        message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint"))
                    message.addAction("open_browser", i18n_catalog.i18nc("@action:button", "Open OctoPrint..."), "globe",
                                        i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface"))
                    message.actionTriggered.connect(self._onMessageActionTriggered)
                    message.show()

            elif self._api_prefix + "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation())

    def _onStreamDownloadProgress(self, bytes_received, bytes_total):
        self._stream_buffer += self._image_reply.readAll()

        if self._stream_buffer_start_index == -1:
            self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
        stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')

        if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
            jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
            self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
            self._stream_buffer_start_index = -1

            self._camera_image.loadFromData(jpg_data)
            self.newImage.emit()

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                if progress > self._progress_message.getProgress():
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._base_url))
예제 #42
0
class PDFWidget(QLabel):
    '''
    A widget showing one page of a PDF.
    If you want to show multiple pages of the same PDF,
    make sure you share the document (let the first PDFWidget
    create the document, then pass thatPDFwidget.document to any
    subsequent widgets you create) or use a ScrolledPDFWidget.
    '''
    def __init__(self,
                 url,
                 document=None,
                 pageno=1,
                 dpi=72,
                 parent=None,
                 load_cb=None):
        '''
           load_cb: will be called when the document is loaded.
        '''
        super(PDFWidget, self).__init__(parent)

        self.filename = url

        self.load_cb = load_cb

        self.network_manager = None

        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        if not document:
            self.document = None
            if url:
                self.start_load(url)
        else:
            self.document = document
        self.page = None
        self.pagesize = QSize(600, 800)

        self.dpi = dpi

        # Poppler page numbering starts from 0 but that's not what
        # most PDF users will expect, so subtract:
        if pageno > 0:
            pageno -= 1
        self.pageno = pageno

        self.render()

    def sizeHint(self):
        if not self.page:
            if not self.document:
                return QSize(600, 800)
            self.page = self.document.page(self.pageno)

        if not self.pagesize:
            self.pagesize = self.page.pageSize()

        return self.pagesize

    def render(self):
        '''Render to a pixmap at the current DPI setting.
        '''
        if not self.document:
            return

        if not self.page:
            self.page = self.document.page(self.pageno)
            self.pagesize = self.page.pageSize()

            self.document.setRenderHint(Poppler.Document.TextAntialiasing)
            # self.document.setRenderHint(Poppler.Document.TextHinting)

        # Most Qt5 programs seem to use setGeometry(x, y, w, h)
        # to set initial window size. resize() is the only method I've
        # found that doesn't force initial position as well as size.
        # self.resize(self.pagesize.width() * self.dpi/POINTS_PER_INCH,
        #             self.pagesize.height() * self.dpi/POINTS_PER_INCH)

        self.setWindowTitle('PDF Viewer')

        img = self.page.renderToImage(self.dpi, self.dpi)
        self.pixmap = QPixmap.fromImage(img)
        self.setPixmap(self.pixmap)

    def start_load(self, url):
        '''Create a Poppler.Document from the given URL, QUrl or filename.
           Return, then asynchronously call self.load_cb.
        '''

        # If it's not a local file, we'll need to load it.
        # http://doc.qt.io/qt-5/qnetworkaccessmanager.html
        qurl = QUrl(url)
        if not qurl.scheme():
            qurl = QUrl.fromLocalFile(url)
        if not self.network_manager:
            self.network_manager = QNetworkAccessManager()
        self.network_manager.finished.connect(self.download_finished)
        self.network_manager.get(QNetworkRequest(qurl))

    def download_finished(self, network_reply):
        qbytes = network_reply.readAll()
        self.document = Poppler.Document.loadFromData(qbytes)

        self.render()

        if self.load_cb:
            self.load_cb()
예제 #43
0
class DiscoverOctoPrintAction(MachineAction):
    def __init__(self, parent=None):
        super().__init__("DiscoverOctoPrintAction",
                         catalog.i18nc("@action", "Connect OctoPrint"))

        self._qml_url = "DiscoverOctoPrintAction.qml"

        self._network_plugin = None

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        self._settings_reply = None

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            plugin_version = "Unknown"
            Logger.logException(
                "w", "Could not get version information for the plugin")

        self._user_agent = (
            "%s/%s %s/%s" %
            (Application.getInstance().getApplicationName(),
             Application.getInstance().getVersion(), "OctoPrintPlugin",
             Application.getInstance().getVersion())).encode()

        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False

        self._additional_components = None

        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerAdded)
        Application.getInstance().engineCreatedSignal.connect(
            self._createAdditionalComponentsView)

    @pyqtSlot()
    def startDiscovery(self):
        if not self._network_plugin:
            self._network_plugin = Application.getInstance(
            ).getOutputDeviceManager().getOutputDevicePlugin("OctoPrintPlugin")
            self._network_plugin.addInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.removeInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.instanceListChanged.connect(
                self._onInstanceDiscovery)
            self.instancesChanged.emit()
        else:
            # Restart bonjour discovery
            self._network_plugin.startDiscovery()

    def _onInstanceDiscovery(self, *args):
        self.instancesChanged.emit()

    @pyqtSlot(str)
    def removeManualInstance(self, name):
        if not self._network_plugin:
            return

        self._network_plugin.removeManualInstance(name)

    @pyqtSlot(str, str, int, str, bool, str, str)
    def setManualInstance(self, name, address, port, path, useHttps, userName,
                          password):
        # This manual printer could replace a current manual printer
        self._network_plugin.removeManualInstance(name)

        self._network_plugin.addManualInstance(name, address, port, path,
                                               useHttps, userName, password)

    def _onContainerAdded(self, container):
        # Add this action as a supported action to all machine definitions
        if isinstance(container,
                      DefinitionContainer) and container.getMetaDataEntry(
                          "type") == "machine" and container.getMetaDataEntry(
                              "supports_usb_connection"):
            Application.getInstance().getMachineActionManager(
            ).addSupportedAction(container.getId(), self.getKey())

    instancesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=instancesChanged)
    def discoveredInstances(self):
        if self._network_plugin:
            instances = list(self._network_plugin.getInstances().values())
            instances.sort(key=lambda k: k.name)
            return instances
        else:
            return []

    @pyqtSlot(str)
    def setKey(self, key):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if "octoprint_id" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry("octoprint_id", key)
            else:
                global_container_stack.addMetaDataEntry("octoprint_id", key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    @pyqtSlot(result=str)
    def getStoredKey(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            meta_data = global_container_stack.getMetaData()
            if "octoprint_id" in meta_data:
                return global_container_stack.getMetaDataEntry("octoprint_id")

        return ""

    @pyqtSlot(str, str, str, str)
    def testApiKey(self,
                   base_url,
                   api_key,
                   basic_auth_username="",
                   basic_auth_password=""):
        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self.selectedInstanceSettingsChanged.emit()

        if api_key != "":
            Logger.log(
                "d",
                "Trying to access OctoPrint instance at %s with the provided API key."
                % base_url)

            ## Request 'settings' dump
            url = QUrl(base_url + "api/settings")
            settings_request = QNetworkRequest(url)
            settings_request.setRawHeader("X-Api-Key".encode(),
                                          api_key.encode())
            settings_request.setRawHeader("User-Agent".encode(),
                                          self._user_agent)
            if basic_auth_username and basic_auth_password:
                data = base64.b64encode(
                    ("%s:%s" % (basic_auth_username,
                                basic_auth_password)).encode()).decode("utf-8")
                settings_request.setRawHeader("Authorization".encode(),
                                              ("Basic %s" % data).encode())
            self._settings_reply = self._manager.get(settings_request)
        else:
            if self._settings_reply:
                self._settings_reply.abort()
                self._settings_reply = None

    @pyqtSlot(str)
    def setApiKey(self, api_key):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if "octoprint_api_key" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry(
                    "octoprint_api_key", api_key)
            else:
                global_container_stack.addMetaDataEntry(
                    "octoprint_api_key", api_key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    apiKeyChanged = pyqtSignal()

    ##  Get the stored API key of this machine
    #   \return key String containing the key of the machine.
    @pyqtProperty(str, notify=apiKeyChanged)
    def apiKey(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            return global_container_stack.getMetaDataEntry("octoprint_api_key")
        else:
            return ""

    selectedInstanceSettingsChanged = pyqtSignal()

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceResponded(self):
        return self._instance_responded

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceApiKeyAccepted(self):
        return self._instance_api_key_accepted

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsSd(self):
        return self._instance_supports_sd

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsCamera(self):
        return self._instance_supports_camera

    @pyqtSlot(str, str, str)
    def setContainerMetaDataEntry(self, container_id, key, value):
        containers = ContainerRegistry.getInstance().findContainers(
            id=container_id)
        if not containers:
            UM.Logger.log(
                "w",
                "Could not set metadata of container %s because it was not found.",
                container_id)
            return False

        container = containers[0]
        if key in container.getMetaData():
            container.setMetaDataEntry(key, value)
        else:
            container.addMetaDataEntry(key, value)

    @pyqtSlot(bool)
    def applyGcodeFlavorFix(self, apply_fix):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode"
        if global_container_stack.getProperty("machine_gcode_flavor",
                                              "value") == gcode_flavor:
            # No need to add a definition_changes container if the setting is not going to be changed
            return

        # Make sure there is a definition_changes container to store the machine settings
        definition_changes_container = global_container_stack.definitionChanges
        if definition_changes_container == ContainerRegistry.getInstance(
        ).getEmptyInstanceContainer():
            definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
                global_container_stack,
                global_container_stack.getId() + "_settings")

        definition_changes_container.setProperty("machine_gcode_flavor",
                                                 "value", gcode_flavor)

        # Update the has_materials metadata flag after switching gcode flavor
        definition = global_container_stack.getBottom()
        if definition.getProperty(
                "machine_gcode_flavor",
                "value") != "UltiGCode" or definition.getMetaDataEntry(
                    "has_materials", False):
            # In other words: only continue for the UM2 (extended), but not for the UM2+
            return

        has_materials = global_container_stack.getProperty(
            "machine_gcode_flavor", "value") != "UltiGCode"

        material_container = global_container_stack.material

        if has_materials:
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry("has_materials", True)
            else:
                global_container_stack.addMetaDataEntry("has_materials", True)

            # Set the material container to a sane default
            if material_container == ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer():
                search_criteria = {
                    "type":
                    "material",
                    "definition":
                    "fdmprinter",
                    "id":
                    global_container_stack.getMetaDataEntry(
                        "preferred_material")
                }
                materials = ContainerRegistry.getInstance(
                ).findInstanceContainers(**search_criteria)
                if materials:
                    global_container_stack.material = materials[0]
        else:
            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.removeMetaDataEntry("has_materials")

            global_container_stack.material = ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer()

        Application.getInstance().globalContainerStackChanged.emit()

    @pyqtSlot(str)
    def openWebPage(self, url):
        QDesktopServices.openUrl(QUrl(url))

    def _createAdditionalComponentsView(self):
        Logger.log(
            "d",
            "Creating additional ui components for OctoPrint-connected printers."
        )

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            "OctoPrintComponents.qml")
        self._additional_components = Application.getInstance(
        ).createQmlComponent(path, {"manager": self})
        if not self._additional_components:
            Logger.log(
                "w",
                "Could not create additional components for OctoPrint-connected printers."
            )
            return

        Application.getInstance().addAdditionalComponent(
            "monitorButtons",
            self._additional_components.findChild(QObject,
                                                  "openOctoPrintButton"))

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                if http_status_code == 200:
                    Logger.log("d", "API key accepted by OctoPrint.")
                    self._instance_api_key_accepted = True

                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data[
                            "feature"]:
                        self._instance_supports_sd = json_data["feature"][
                            "sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data[
                            "webcam"]:
                        stream_url = json_data["webcam"]["streamUrl"]
                        if stream_url:  #not empty string or None
                            self._instance_supports_camera = True

                elif http_status_code == 401:
                    Logger.log("d", "Invalid API key for OctoPrint.")
                    self._instance_api_key_accepted = False

                self._instance_responded = True
                self.selectedInstanceSettingsChanged.emit()
예제 #44
0
class TabbedWindow(QMainWindow):
    def __init__(self, title):
        super(TabbedWindow, self).__init__()

        self.setMinimumSize(440, 540)

        self.create_status_bar()
        self.create_central_widget()
        self.create_menu()

        self.shown = False
        self.qnam = QNetworkAccessManager()
        self.http_reply = None
        self.in_manual_update_check = False

        self.faq_dialog = None
        self.about_dialog = None

        geometry = get_config_value('window_geometry')
        if geometry is not None:
            qt_geometry = QByteArray.fromBase64(geometry.encode('utf8'))
            self.restoreGeometry(qt_geometry)

        self.setWindowTitle(title)

        if not config_true(
                get_config_value('allow_multiple_instances', 'False')):
            self.init_named_pipe()

    def set_text(self):
        self.file_menu.setTitle(_('&File'))
        self.exit_action.setText(_('E&xit'))
        self.help_menu.setTitle(_('&Help'))
        self.faq_action.setText(_('&Frequently asked questions (FAQ)'))
        if getattr(sys, 'frozen', False):
            self.update_action.setText(_('&Check for update'))
        self.about_action.setText(_('&About CDDA Game Launcher'))

        if self.about_dialog is not None:
            self.about_dialog.set_text()
        self.central_widget.set_text()

    def create_status_bar(self):
        status_bar = self.statusBar()
        status_bar.busy = 0

        status_bar.showMessage(_('Ready'))

    def create_central_widget(self):
        central_widget = CentralWidget()
        self.setCentralWidget(central_widget)
        self.central_widget = central_widget

    def create_menu(self):
        file_menu = QMenu(_('&File'))
        self.menuBar().addMenu(file_menu)
        self.file_menu = file_menu

        exit_action = QAction(_('E&xit'), self, triggered=self.close)
        file_menu.addAction(exit_action)
        self.exit_action = exit_action

        help_menu = QMenu(_('&Help'))
        self.menuBar().addMenu(help_menu)
        self.help_menu = help_menu

        faq_action = QAction(_('&Frequently asked questions (FAQ)'),
                             self,
                             triggered=self.show_faq_dialog)
        self.faq_action = faq_action
        self.help_menu.addAction(faq_action)

        self.help_menu.addSeparator()

        if getattr(sys, 'frozen', False):
            update_action = QAction(_('&Check for update'),
                                    self,
                                    triggered=self.manual_update_check)
            self.update_action = update_action
            self.help_menu.addAction(update_action)
            self.help_menu.addSeparator()

        about_action = QAction(_('&About CDDA Game Launcher'),
                               self,
                               triggered=self.show_about_dialog)
        self.about_action = about_action
        self.help_menu.addAction(about_action)

    def show_faq_dialog(self):
        if self.faq_dialog is None:
            faq_dialog = FaqDialog(
                self, Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
            self.faq_dialog = faq_dialog

        self.faq_dialog.exec()

    def show_about_dialog(self):
        if self.about_dialog is None:
            about_dialog = AboutDialog(
                self, Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
            self.about_dialog = about_dialog

        self.about_dialog.exec()

    def check_new_launcher_version(self):
        self.lv_html = BytesIO()

        url = cons.GITHUB_REST_API_URL + cons.CDDAGL_LATEST_RELEASE

        request = QNetworkRequest(QUrl(url))
        request.setRawHeader(b'User-Agent',
                             b'CDDA-Game-Launcher/' + version.encode('utf8'))
        request.setRawHeader(b'Accept', cons.GITHUB_API_VERSION)

        self.http_reply = self.qnam.get(request)
        self.http_reply.finished.connect(self.lv_http_finished)
        self.http_reply.readyRead.connect(self.lv_http_ready_read)

    def lv_http_finished(self):
        redirect = self.http_reply.attribute(
            QNetworkRequest.RedirectionTargetAttribute)
        if redirect is not None:
            redirected_url = urljoin(
                self.http_reply.request().url().toString(),
                redirect.toString())

            self.lv_html = BytesIO()

            request = QNetworkRequest(QUrl(redirected_url))
            request.setRawHeader(
                b'User-Agent', b'CDDA-Game-Launcher/' + version.encode('utf8'))
            request.setRawHeader(b'Accept', cons.GITHUB_API_VERSION)

            self.http_reply = self.qnam.get(request)
            self.http_reply.finished.connect(self.lv_http_finished)
            self.http_reply.readyRead.connect(self.lv_http_ready_read)
            return

        status_code = self.http_reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if status_code != 200:
            reason = self.http_reply.attribute(
                QNetworkRequest.HttpReasonPhraseAttribute)
            url = self.http_reply.request().url().toString()
            logger.warning(
                _('Could not find launcher latest release when requesting {url}. Error: {error}'
                  ).format(url=url, error=f'[HTTP {status_code}] ({reason})'))

            if self.in_manual_update_check:
                self.in_manual_update_check = False

            self.lv_html = None
            return

        self.lv_html.seek(0)

        try:
            latest_release = json.loads(
                TextIOWrapper(self.lv_html, encoding='utf8').read())
        except json.decoder.JSONDecodeError:
            latest_release = {'cannot_decode': True}

        self.lv_html = None

        if 'name' not in latest_release:
            return
        if 'html_url' not in latest_release:
            return
        if 'tag_name' not in latest_release:
            return
        if 'assets' not in latest_release:
            return
        if 'body' not in latest_release:
            return

        version_text = latest_release['tag_name']
        if version_text.startswith('v'):
            version_text = version_text[1:]
        latest_version = LooseVersion(version_text)

        if latest_version is None:
            return

        executable_url = None
        for file_asset in latest_release['assets']:
            if not 'name' in file_asset:
                continue
            if 'browser_download_url' not in file_asset:
                continue
            if file_asset['name'].endswith('.exe'):
                executable_url = file_asset['browser_download_url']
                break

        if executable_url is None:
            return

        current_version = LooseVersion(version)

        if latest_version > current_version:

            markdown_desc = latest_release['body']

            # Replace number signs with issue links
            number_pattern = ' \\#(?P<id>\\d+)'
            replacement_pattern = (' [#\\g<id>](' +
                                   cons.CDDAGL_ISSUE_URL_ROOT + '\\g<id>)')
            markdown_desc = re.sub(number_pattern, replacement_pattern,
                                   markdown_desc)

            html_desc = markdown.markdown(markdown_desc)

            release_html = ('''
<h2><a href="{release_url}">{release_name}</a></h2>{description}
                ''').format(release_url=html.escape(
                latest_release['html_url']),
                            release_name=html.escape(latest_release['name']),
                            description=html_desc)

            no_launcher_version_check_checkbox = QCheckBox()
            no_launcher_version_check_checkbox.setText(
                _('Do not check '
                  'for new version of the CDDA Game Launcher on launch'))
            check_state = (Qt.Checked if config_true(
                get_config_value('prevent_version_check_launch', 'False')) else
                           Qt.Unchecked)
            no_launcher_version_check_checkbox.stateChanged.connect(
                self.nlvcc_changed)
            no_launcher_version_check_checkbox.setCheckState(check_state)

            launcher_update_msgbox = QMessageBox()
            launcher_update_msgbox.setWindowTitle(_('Launcher update'))
            launcher_update_msgbox.setText(
                _('You are using version '
                  '{version} but there is a new update for CDDA Game '
                  'Launcher. Would you like to update?').format(
                      version=version))
            launcher_update_msgbox.setInformativeText(release_html)
            launcher_update_msgbox.addButton(_('Update the launcher'),
                                             QMessageBox.YesRole)
            launcher_update_msgbox.addButton(_('Not right now'),
                                             QMessageBox.NoRole)
            launcher_update_msgbox.setCheckBox(
                no_launcher_version_check_checkbox)
            launcher_update_msgbox.setIcon(QMessageBox.Question)

            if launcher_update_msgbox.exec() == 0:
                flags = Qt.WindowTitleHint | Qt.WindowCloseButtonHint

                launcher_update_dialog = (LauncherUpdateDialog(
                    executable_url, version_text, self, flags))
                launcher_update_dialog.exec()

                if launcher_update_dialog.updated:
                    self.close()

        else:
            self.no_launcher_update_found()

    def nlvcc_changed(self, state):
        no_launcher_version_check_checkbox = (
            self.central_widget.settings_tab.launcher_settings_group_box.
            no_launcher_version_check_checkbox)
        no_launcher_version_check_checkbox.setCheckState(state)

    def manual_update_check(self):
        self.in_manual_update_check = True
        self.check_new_launcher_version()

    def no_launcher_update_found(self):
        if self.in_manual_update_check:
            up_to_date_msgbox = QMessageBox()
            up_to_date_msgbox.setWindowTitle(_('Up to date'))
            up_to_date_msgbox.setText(
                _('The CDDA Game Launcher is up to date.'))
            up_to_date_msgbox.setIcon(QMessageBox.Information)

            up_to_date_msgbox.exec()

            self.in_manual_update_check = False

    def lv_http_ready_read(self):
        self.lv_html.write(self.http_reply.readAll())

    def init_named_pipe(self):
        class PipeReadWaitThread(QThread):
            read = pyqtSignal(bytes)

            def __init__(self):
                super(PipeReadWaitThread, self).__init__()

                try:
                    self.pipe = SimpleNamedPipe('cddagl_instance')
                except (OSError, PyWinError):
                    self.pipe = None

            def __del__(self):
                self.wait()

            def run(self):
                if self.pipe is None:
                    return

                while self.pipe is not None:
                    if self.pipe.connect() and self.pipe is not None:
                        try:
                            value = self.pipe.read(1024)
                            self.read.emit(value)
                        except (PyWinError, IOError):
                            pass

        def instance_read(value):
            if value == b'dupe':
                self.showNormal()
                self.raise_()
                self.activateWindow()

        pipe_read_wait_thread = PipeReadWaitThread()
        pipe_read_wait_thread.read.connect(instance_read)
        pipe_read_wait_thread.start()

        self.pipe_read_wait_thread = pipe_read_wait_thread

    def showEvent(self, event):
        if not self.shown:
            if not config_true(
                    get_config_value('prevent_version_check_launch', 'False')):
                if getattr(sys, 'frozen', False):
                    self.in_manual_update_check = False
                    self.check_new_launcher_version()

        self.shown = True

    def save_geometry(self):
        geometry = self.saveGeometry().toBase64().data().decode('utf8')
        set_config_value('window_geometry', geometry)

        backups_tab = self.central_widget.backups_tab
        backups_tab.save_geometry()

    def closeEvent(self, event):
        update_group_box = self.central_widget.main_tab.update_group_box
        soundpacks_tab = self.central_widget.soundpacks_tab

        if update_group_box.updating:
            update_group_box.close_after_update = True
            update_group_box.update_game()

            if not update_group_box.updating:
                self.save_geometry()
                event.accept()
            else:
                event.ignore()
        elif soundpacks_tab.installing_new_soundpack:
            soundpacks_tab.close_after_install = True
            soundpacks_tab.install_new()

            if not soundpacks_tab.installing_new_soundpack:
                self.save_geometry()
                event.accept()
            else:
                event.ignore()
        else:
            self.save_geometry()
            event.accept()
예제 #45
0
class Dialogo(QDialog):
    def __init__(self):
        QDialog.__init__(self)
        uic.loadUi("progressbar.ui", self)
        #Almacena la url del archivo a descargar
        self.url = None
        #Almacena el manejador del fichero a crear
        self.file = None
        #almacena el nombre del archivo a descargar
        self.filename = None
        #almacena en su caso el error en un string
        self.errorString = None
        #almacena en su caso el número de error
        self.errorCode = None
        #Objeto para establecer la conexión y crear el objeto QNetworkReply
        self.http = QNetworkAccessManager(self)
        #Desactivar los botones de descargar y cancelar en un principio
        self.btn_download.setEnabled(False)
        self.btn_cancel.setEnabled(False)
        #Establece si los botones están activos o no
        self.btn_active = False

        #Inicia todo el proceso de descarga
        self.btn_download.clicked.connect(self.download)
        #Cancela la descarga
        self.btn_cancel.clicked.connect(self.cancel_download)
        #Detectar el cambio de texto en el campo de texto para activar el botón de descarga
        self.ruta.textChanged.connect(self.btn_enabled)

    #Detectar el cambio de texto en el campo de texto para activar el botón de descarga
    def btn_enabled(self):
        if self.ruta.text() != "":
            self.btn_active = True
            self.btn_download.setEnabled(True)
        else:
            self.btn_active = False

    #Inicia todo el proceso de descarga
    def download(self):
        if self.btn_active == True:
            #Ruta indicada por el usuario
            ruta = self.ruta.text()
            self.url = QUrl(ruta)
            fileinfo = QFileInfo(self.url.path())
            self.filename = fileinfo.fileName()
            #Manejador del fichero
            self.file = QFile(self.filename)
            #Si no es posible crear el fichero
            if not self.file.open(QIODevice.WriteOnly):
                self.labelState.setText("No se pudo crear el archivo")
                self.file.close()
            else:  #Entonces llamar al método que inicia la descarga del archivo
                self.start_download()

    #Inicia el proceso de descarga y controla las diferente señales (eventos) durante la misma
    def start_download(self):
        #Objeto QNetworkReply
        self.reply = self.http.get(QNetworkRequest(self.url))
        self.labelState.setText("Iniciando la descarga ...")
        #Empieza la lectura del archivo remoto y escritura local
        self.reply.readyRead.connect(self.ready_read)
        #Señal predefinida para obtener los bytes en el proceso descarga y asignarlos al progressBar
        self.reply.downloadProgress.connect(self.updateDataReadProgress)
        #Señal para capturar posibles errores durante la descarga
        self.reply.error.connect(self.error_download)
        #Finalización de la descarga
        self.reply.finished.connect(self.finished_download)

    #Ocurre durante la escritura del archivo
    def ready_read(self):
        #Escritura del archivo local
        self.file.write(self.reply.readAll())
        self.labelState.setText("Descargando ...")
        #Activación del botón de cancelar
        self.btn_cancel.setEnabled(True)

    #Método predefinido en la clase QNetworkReply para leer el progreso de descarga
    def updateDataReadProgress(self, bytesRead, totalBytes):
        self.progressBar.setMaximum(totalBytes)
        self.progressBar.setValue(bytesRead)

    #Si ha ocurrido algún error durante el proceso de descarga
    def error_download(self, error):
        #Si ha ocurrido un error, mostrar el error e eliminar el archivo en el método finished_download
        self.errorString = self.reply.errorString()
        self.errorCode = error

    #Ocurre cuando la descarga ha finalizado
    def finished_download(self):
        #Si existe un error
        if self.errorCode is not None:
            #Poner a 0 el progressBar
            self.progressBar.setValue(0)
            self.labelState.setText(
                str(self.errorCode) + ": " + self.errorString)
            #Eliminar el archivo
            self.file.remove()
        else:
            self.labelState.setText("Descarga completada")

        #Cerrar el fichero
        self.file.close()
        #Desactivar el botón de cancelar ya que la descarga ha finalizado
        self.btn_cancel.setEnabled(False)
        #Restaurar a None los valores de los atributos de error
        self.errorString = None
        self.errorCode = None

    #Cancelar la descargar durante su ejecución
    def cancel_download(self):
        #Abortar la descarga
        self.reply.abort()
        #Desconectar del servidor
        self.reply.close()
        #Eliminar el fichero
        self.file.remove()
        #Cerrar el fichero
        self.file.close()
예제 #46
0
class LauncherUpdateDialog(QDialog):
    def __init__(self, url, version, parent=0, f=0):
        super(LauncherUpdateDialog, self).__init__(parent, f)

        self.updated = False
        self.url = url

        layout = QGridLayout()

        self.shown = False
        self.qnam = QNetworkAccessManager()
        self.http_reply = None

        progress_label = QLabel()
        progress_label.setText(_('Progress:'))
        layout.addWidget(progress_label, 0, 0, Qt.AlignRight)
        self.progress_label = progress_label

        progress_bar = QProgressBar()
        layout.addWidget(progress_bar, 0, 1)
        self.progress_bar = progress_bar

        url_label = QLabel()
        url_label.setText(_('Url:'))
        layout.addWidget(url_label, 1, 0, Qt.AlignRight)
        self.url_label = url_label

        url_lineedit = QLineEdit()
        url_lineedit.setText(url)
        url_lineedit.setReadOnly(True)
        layout.addWidget(url_lineedit, 1, 1)
        self.url_lineedit = url_lineedit

        size_label = QLabel()
        size_label.setText(_('Size:'))
        layout.addWidget(size_label, 2, 0, Qt.AlignRight)
        self.size_label = size_label

        size_value_label = QLabel()
        layout.addWidget(size_value_label, 2, 1)
        self.size_value_label = size_value_label

        speed_label = QLabel()
        speed_label.setText(_('Speed:'))
        layout.addWidget(speed_label, 3, 0, Qt.AlignRight)
        self.speed_label = speed_label

        speed_value_label = QLabel()
        layout.addWidget(speed_value_label, 3, 1)
        self.speed_value_label = speed_value_label

        cancel_button = QPushButton()
        cancel_button.setText(_('Cancel update'))
        cancel_button.setStyleSheet('font-size: 15px;')
        cancel_button.clicked.connect(self.cancel_update)
        layout.addWidget(cancel_button, 4, 0, 1, 2)
        self.cancel_button = cancel_button

        layout.setColumnStretch(1, 100)

        self.setLayout(layout)
        self.setMinimumSize(300, 0)
        self.setWindowTitle(_('CDDA Game Launcher self-update'))

    def showEvent(self, event):
        if not self.shown:
            temp_dl_dir = tempfile.mkdtemp(prefix=cons.TEMP_PREFIX)

            exe_name = os.path.basename(sys.executable)

            self.downloaded_file = os.path.join(temp_dl_dir, exe_name)
            self.downloading_file = open(self.downloaded_file, 'wb')

            self.download_last_read = datetime.utcnow()
            self.download_last_bytes_read = 0
            self.download_speed_count = 0
            self.download_aborted = False

            self.http_reply = self.qnam.get(QNetworkRequest(QUrl(self.url)))
            self.http_reply.finished.connect(self.http_finished)
            self.http_reply.readyRead.connect(self.http_ready_read)
            self.http_reply.downloadProgress.connect(self.dl_progress)

        self.shown = True

    def closeEvent(self, event):
        self.cancel_update(True)

    def http_finished(self):
        self.downloading_file.close()

        if self.download_aborted:
            download_dir = os.path.dirname(self.downloaded_file)
            delete_path(download_dir)
        else:
            redirect = self.http_reply.attribute(
                QNetworkRequest.RedirectionTargetAttribute)
            if redirect is not None:
                download_dir = os.path.dirname(self.downloaded_file)
                delete_path(download_dir)
                os.makedirs(download_dir)

                redirected_url = urljoin(
                    self.http_reply.request().url().toString(),
                    redirect.toString())

                self.downloading_file = open(self.downloaded_file, 'wb')

                self.download_last_read = datetime.utcnow()
                self.download_last_bytes_read = 0
                self.download_speed_count = 0
                self.download_aborted = False

                self.progress_bar.setValue(0)

                self.http_reply = self.qnam.get(
                    QNetworkRequest(QUrl(redirected_url)))
                self.http_reply.finished.connect(self.http_finished)
                self.http_reply.readyRead.connect(self.http_ready_read)
                self.http_reply.downloadProgress.connect(self.dl_progress)
            else:
                # Download completed
                if getattr(sys, 'frozen', False):
                    # Launch self.downloaded_file and close

                    subprocess.Popen([self.downloaded_file])

                    self.updated = True
                    self.done(0)

    def http_ready_read(self):
        self.downloading_file.write(self.http_reply.readAll())

    def dl_progress(self, bytes_read, total_bytes):
        self.progress_bar.setMaximum(total_bytes)
        self.progress_bar.setValue(bytes_read)

        self.download_speed_count += 1

        self.size_value_label.setText('{bytes_read}/{total_bytes}'.format(
            bytes_read=sizeof_fmt(bytes_read),
            total_bytes=sizeof_fmt(total_bytes)))

        if self.download_speed_count % 5 == 0:
            delta_bytes = bytes_read - self.download_last_bytes_read
            delta_time = datetime.utcnow() - self.download_last_read

            bytes_secs = delta_bytes / delta_time.total_seconds()
            self.speed_value_label.setText(
                _('{bytes_sec}/s').format(bytes_sec=sizeof_fmt(bytes_secs)))

            self.download_last_bytes_read = bytes_read
            self.download_last_read = datetime.utcnow()

    def cancel_update(self, from_close=False):
        if self.http_reply.isRunning():
            self.download_aborted = True
            self.http_reply.abort()

        if not from_close:
            self.close()
예제 #47
0
class Downloader(QProgressDialog):

    def __init__(self, parent=None):
        super(Downloader, self).__init__(parent)
        self.setWindowTitle(__doc__)
        if not os.path.isfile(__file__) or not __source__:
            return
        if not os.access(__file__, os.W_OK):
            error_msg = ("Destination file permission denied (not Writable)! "
                         "Try again to Update but as root or administrator.")
            log.critical(error_msg)
            QMessageBox.warning(self, __doc__.title(), error_msg)
            return
        self._time, self._date = time.time(), datetime.now().isoformat()[:-7]
        self._url, self._dst = __source__, __file__
        log.debug("Downloading from {} to {}.".format(self._url, self._dst))
        if not self._url.lower().startswith("https:"):
            log.warning("Unsecure Download over plain text without SSL.")
        self.template = """<h3>Downloading</h3><hr><table>
        <tr><td><b>From:</b></td>      <td>{}</td>
        <tr><td><b>To:  </b></td>      <td>{}</td> <tr>
        <tr><td><b>Started:</b></td>   <td>{}</td>
        <tr><td><b>Actual:</b></td>    <td>{}</td> <tr>
        <tr><td><b>Elapsed:</b></td>   <td>{}</td>
        <tr><td><b>Remaining:</b></td> <td>{}</td> <tr>
        <tr><td><b>Received:</b></td>  <td>{} MegaBytes</td>
        <tr><td><b>Total:</b></td>     <td>{} MegaBytes</td> <tr>
        <tr><td><b>Speed:</b></td>     <td>{}</td>
        <tr><td><b>Percent:</b></td>     <td>{}%</td></table><hr>"""
        self.manager = QNetworkAccessManager(self)
        self.manager.finished.connect(self.save_downloaded_data)
        self.manager.sslErrors.connect(self.download_failed)
        self.progreso = self.manager.get(QNetworkRequest(QUrl(self._url)))
        self.progreso.downloadProgress.connect(self.update_download_progress)
        self.show()
        self.exec_()

    def save_downloaded_data(self, data):

        log.debug("Download done. Update Done.")
        with open(os.path.join(self._dst), "wb") as output_file:
            output_file.write(data.readAll())
        data.close()
        QMessageBox.information(self, __doc__.title(),
                                "<b>You got the latest version of this App!")
        del self.manager, data
        return self.close()

    def download_failed(self, download_error):

        log.error(download_error)
        QMessageBox.warning(self, __doc__.title(), str(download_error))

    def seconds_time_to_human_string(self, time_on_seconds=0):

        minutes, seconds = divmod(int(time_on_seconds), 60)
        hours, minutes = divmod(minutes, 60)
        days, hours = divmod(hours, 24)
        human_time_string = ""
        if days:
            human_time_string += "%02d Days " % days
        if hours:
            human_time_string += "%02d Hours " % hours
        if minutes:
            human_time_string += "%02d Minutes " % minutes
        human_time_string += "%02d Seconds" % seconds
        return human_time_string

    def update_download_progress(self, bytesReceived, bytesTotal):

        downloaded_MB = round(((bytesReceived / 1024) / 1024), 2)
        total_data_MB = round(((bytesTotal / 1024) / 1024), 2)
        downloaded_KB, total_data_KB = bytesReceived / 1024, bytesTotal / 1024
        elapsed = time.clock()
        if elapsed > 0:
            speed = round((downloaded_KB / elapsed), 2)
            if speed > 1024000:  # Gigabyte speeds
                download_speed = "{} GigaByte/Second".format(speed // 1024000)
            if speed > 1024:  # MegaByte speeds
                download_speed = "{} MegaBytes/Second".format(speed // 1024)
            else:  # KiloByte speeds
                download_speed = "{} KiloBytes/Second".format(int(speed))
        if speed > 0:
            missing = abs((total_data_KB - downloaded_KB) // speed)
        percentage = int(100.0 * bytesReceived // bytesTotal)
        self.setLabelText(self.template.format(
            self._url.lower()[:99], self._dst.lower()[:99],
            self._date, datetime.now().isoformat()[:-7],
            self.seconds_time_to_human_string(time.time() - self._time),
            self.seconds_time_to_human_string(missing),
            downloaded_MB, total_data_MB, download_speed, percentage))
        self.setValue(percentage)
예제 #48
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal()
    removeDeviceSignal = Signal()
    discoveredDevicesChanged = Signal()

    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._zero_conf_browser = None

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        self._discovered_devices = {}
        
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = Application.getInstance().getPreferences()
        self._preferences.addPreference("um3networkprinting/manual_instances",
                                        "")  # A comma-separated list of ip adresses or hostnames

        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        # Store the last manual entry key
        self._last_manual_entry_key = "" # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
                                                 [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def removeManualDevice(self, key, address = None):
        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def addManualDevice(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary": b"true"   # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        self._checkManualDevice(address)

    def _checkManualDevice(self, address):
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()

        if "system" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the firmware version!
                return

            try:
                system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            address = reply.url().host()
            has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": system_info["name"].encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine": str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the amount of printers the cluster has!
                return
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            address = reply.url().host()
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b'cluster_size'] = len(cluster_printers_list)
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id):
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass

            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)

        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
            device.connect()
            device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)

            # Stop if the application is shutting down
            if Application.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = []  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type, name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
                                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address, info.properties)
                    else:
                        Logger.log("w",
                                   "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True
예제 #49
0
class ComicVineTalker(QObject):

    logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
    api_key = ""

    @staticmethod
    def getRateLimitMessage():
        if ComicVineTalker.api_key == "":
            return "Comic Vine rate limit exceeded.  You should configue your own Comic Vine API key."
        else:
            return "Comic Vine rate limit exceeded.  Please wait a bit."

    def __init__(self):
        QObject.__init__(self)

        self.api_base_url = "https://comicvine.gamespot.com/api"
        self.wait_for_rate_limit = False

        # key that is registered to comictagger
        default_api_key = '27431e6787042105bd3e47e169a624521f89f3a4'

        if ComicVineTalker.api_key == "":
            self.api_key = default_api_key
        else:
            self.api_key = ComicVineTalker.api_key

        self.log_func = None

        # always use a tls context for urlopen
        self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLS)

    def setLogFunc(self, log_func):
        self.log_func = log_func

    def writeLog(self, text):
        if self.log_func is None:
            # sys.stdout.write(text.encode(errors='replace'))
            # sys.stdout.flush()
            print(text, file=sys.stderr)
        else:
            self.log_func(text)

    def parseDateStr(self, date_str):
        day = None
        month = None
        year = None
        if date_str is not None:
            parts = date_str.split('-')
            year = parts[0]
            if len(parts) > 1:
                month = parts[1]
                if len(parts) > 2:
                    day = parts[2]
        return day, month, year

    def testKey(self, key):

        try:
            test_url = self.api_base_url + "/issue/1/?api_key=" + \
                key + "&format=json&field_list=name"
            resp = urllib.request.urlopen(test_url, context=self.ssl)
            content = resp.read()

            cv_response = json.loads(content.decode('utf-8'))

            # Bogus request, but if the key is wrong, you get error 100: "Invalid
            # API Key"
            return cv_response['status_code'] != 100
        except:
            return False

    """
    Get the contect from the CV server.  If we're in "wait mode" and status code is a rate limit error
    sleep for a bit and retry.
    """

    def getCVContent(self, url):
        total_time_waited = 0
        limit_wait_time = 1
        counter = 0
        wait_times = [1, 2, 3, 4]
        while True:
            content = self.getUrlContent(url)
            cv_response = json.loads(content.decode('utf-8'))
            if self.wait_for_rate_limit and cv_response[
                    'status_code'] == ComicVineTalkerException.RateLimit:
                self.writeLog(
                    "Rate limit encountered.  Waiting for {0} minutes\n".
                    format(limit_wait_time))
                time.sleep(limit_wait_time * 60)
                total_time_waited += limit_wait_time
                limit_wait_time = wait_times[counter]
                if counter < 3:
                    counter += 1
                # don't wait much more than 20 minutes
                if total_time_waited < 20:
                    continue
            if cv_response['status_code'] != 1:
                self.writeLog(
                    "Comic Vine query failed with error #{0}:  [{1}]. \n".
                    format(cv_response['status_code'], cv_response['error']))
                raise ComicVineTalkerException(cv_response['status_code'],
                                               cv_response['error'])
            else:
                # it's all good
                break
        return cv_response

    def getUrlContent(self, url):
        # connect to server:
        #  if there is a 500 error, try a few more times before giving up
        #  any other error, just bail
        #print("---", url)
        for tries in range(3):
            try:
                resp = urllib.request.urlopen(url, context=self.ssl)
                return resp.read()
            except urllib.error.HTTPError as e:
                if e.getcode() == 500:
                    self.writeLog("Try #{0}: ".format(tries + 1))
                    time.sleep(1)
                self.writeLog(str(e) + "\n")

                if e.getcode() != 500:
                    break

            except Exception as e:
                self.writeLog(str(e) + "\n")
                raise ComicVineTalkerException(
                    ComicVineTalkerException.Network, "Network Error!")

        raise ComicVineTalkerException(ComicVineTalkerException.Unknown,
                                       "Error on Comic Vine server")

    def searchForSeries(self, series_name, callback=None, refresh_cache=False):

        # remove cruft from the search string
        series_name = utils.removearticles(series_name).lower().strip()

        # before we search online, look in our cache, since we might have
        # done this same search recently
        cvc = ComicVineCacher()
        if not refresh_cache:
            cached_search_results = cvc.get_search_results(series_name)

            if len(cached_search_results) > 0:
                return cached_search_results

        original_series_name = series_name

        # Split and rejoin to remove extra internal spaces
        query_word_list = series_name.split()
        query_string = " ".join(query_word_list).strip()
        #print ("Query string = ", query_string)

        query_string = urllib.parse.quote_plus(query_string.encode("utf-8"))

        search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + \
            query_string + \
            "&field_list=name,id,start_year,publisher,image,description,count_of_issues&limit=100"
        cv_response = self.getCVContent(search_url + "&page=1")

        search_results = list()

        # see http://api.comicvine.com/documentation/#handling_responses

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']

        # 8 Dec 2018 - Comic Vine changed query results again. Terms are now
        # ORed together, and we get thousands of results.  Good news is the
        # results are sorted by relevance, so we can be smart about halting
        # the search.
        # 1. Don't fetch more than some sane amount of pages.
        max_results = 500
        # 2. Halt when not all of our search terms are present in a result
        # 3. Halt when the results contain more (plus threshold) words than
        #    our search
        result_word_count_max = len(query_word_list) + 3

        total_result_count = min(total_result_count, max_results)

        if callback is None:
            self.writeLog("Found {0} of {1} results\n".format(
                cv_response['number_of_page_results'],
                cv_response['number_of_total_results']))
        search_results.extend(cv_response['results'])
        page = 1

        if callback is not None:
            callback(current_result_count, total_result_count)

        # see if we need to keep asking for more pages...
        stop_searching = False
        while (current_result_count < total_result_count):

            last_result = search_results[-1]['name']

            # See if the last result's name has all the of the search terms.
            # if not, break out of this, loop, we're done.
            #print("Searching for {} in '{}'".format(query_word_list, last_result))
            for term in query_word_list:
                if term not in last_result.lower():
                    #print("Term '{}' not in last result. Halting search result fetching".format(term))
                    stop_searching = True
                    break

            # Also, stop searching when the word count of last results is too much longer
            # than our search terms list
            if len(utils.removearticles(
                    last_result).split()) > result_word_count_max:
                #print("Last result '{}' is too long. Halting search result fetching".format(last_result))
                stop_searching = True

            if stop_searching:
                break

            if callback is None:
                self.writeLog(
                    "getting another page of results {0} of {1}...\n".format(
                        current_result_count, total_result_count))
            page += 1

            cv_response = self.getCVContent(search_url + "&page=" + str(page))

            search_results.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

            if callback is not None:
                callback(current_result_count, total_result_count)

        # Remove any search results that don't contain all the search terms
        # (iterate backwards for easy removal)
        for i in range(len(search_results) - 1, -1, -1):
            record = search_results[i]
            for term in query_word_list:
                if term not in record['name'].lower():
                    del search_results[i]
                    break

        # for record in search_results:
        #print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
        # print(record)
        #record['count_of_issues'] = record['count_of_isssues']
        #print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))

        # cache these search results
        cvc.add_search_results(original_series_name, search_results)

        return search_results

    def fetchVolumeData(self, series_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        cached_volume_result = cvc.get_volume_info(series_id)

        if cached_volume_result is not None:
            return cached_volume_result

        volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + \
            str(series_id) + "/?api_key=" + self.api_key + \
            "&field_list=name,id,start_year,publisher,count_of_issues&format=json"

        cv_response = self.getCVContent(volume_url)

        volume_results = cv_response['results']

        cvc.add_volume_info(volume_results)

        return volume_results

    def fetchIssuesByVolume(self, series_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        cached_volume_issues_result = cvc.get_volume_issues_info(series_id)

        if cached_volume_issues_result is not None:
            return cached_volume_issues_result

        #---------------------------------
        issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + \
            str(series_id) + \
            "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
        cv_response = self.getCVContent(issues_url)

        #------------------------------------

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']
        #print("total_result_count", total_result_count)

        #print("Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
        volume_issues_result = cv_response['results']
        page = 1
        offset = 0

        # see if we need to keep asking for more pages...
        while (current_result_count < total_result_count):
            #print("getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
            page += 1
            offset += cv_response['number_of_page_results']

            # print issues_url+ "&offset="+str(offset)
            cv_response = self.getCVContent(issues_url + "&offset=" +
                                            str(offset))

            volume_issues_result.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

        self.repairUrls(volume_issues_result)

        cvc.add_volume_issues_info(series_id, volume_issues_result)

        return volume_issues_result

    def fetchIssuesByVolumeIssueNumAndYear(self, volume_id_list, issue_number,
                                           year):
        volume_filter = "volume:"
        for vid in volume_id_list:
            volume_filter += str(vid) + "|"

        year_filter = ""
        if year is not None and str(year).isdigit():
            year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(
                year,
                int(year) + 1)

        issue_number = urllib.parse.quote_plus(
            str(issue_number).encode("utf-8"))

        filter = "&filter=" + volume_filter + \
            year_filter + ",issue_number:" + issue_number

        issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + \
            "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"

        cv_response = self.getCVContent(issues_url)

        #------------------------------------

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']
        #print("total_result_count", total_result_count)

        #print("Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
        filtered_issues_result = cv_response['results']
        page = 1
        offset = 0

        # see if we need to keep asking for more pages...
        while (current_result_count < total_result_count):
            #print("getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
            page += 1
            offset += cv_response['number_of_page_results']

            # print issues_url+ "&offset="+str(offset)
            cv_response = self.getCVContent(issues_url + "&offset=" +
                                            str(offset))

            filtered_issues_result.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

        self.repairUrls(filtered_issues_result)

        return filtered_issues_result

    def fetchIssueData(self, series_id, issue_number, settings):

        volume_results = self.fetchVolumeData(series_id)
        issues_list_results = self.fetchIssuesByVolume(series_id)

        found = False
        for record in issues_list_results:
            if IssueString(issue_number).asString() is None:
                issue_number = 1
            if IssueString(
                    record['issue_number']).asString().lower() == IssueString(
                        issue_number).asString().lower():
                found = True
                break

        if (found):
            issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
                str(record['id']) + "/?api_key=" + \
                self.api_key + "&format=json"

            cv_response = self.getCVContent(issue_url)
            issue_results = cv_response['results']

        else:
            return None

        # Now, map the Comic Vine data to generic metadata
        return self.mapCVDataToMetadata(volume_results, issue_results,
                                        settings)

    def fetchIssueDataByIssueID(self, issue_id, settings):

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
        cv_response = self.getCVContent(issue_url)

        issue_results = cv_response['results']

        volume_results = self.fetchVolumeData(issue_results['volume']['id'])

        # Now, map the Comic Vine data to generic metadata
        md = self.mapCVDataToMetadata(volume_results, issue_results, settings)
        md.isEmpty = False
        return md

    def mapCVDataToMetadata(self, volume_results, issue_results, settings):

        # Now, map the Comic Vine data to generic metadata
        metadata = GenericMetadata()

        metadata.series = issue_results['volume']['name']

        num_s = IssueString(issue_results['issue_number']).asString()
        metadata.issue = num_s
        metadata.title = issue_results['name']

        metadata.publisher = volume_results['publisher']['name']
        metadata.day, metadata.month, metadata.year = self.parseDateStr(
            issue_results['cover_date'])

        #metadata.issueCount = volume_results['count_of_issues']
        metadata.comments = self.cleanup_html(issue_results['description'],
                                              settings.remove_html_tables)
        if settings.use_series_start_as_volume:
            metadata.volume = volume_results['start_year']

        if settings.notes_format == 'Issue ID':
            #add the space for consistency
            settings.notes_format = 'Issue ID '
        metadata.notes = "Tagged with the {0} fork of ComicTagger {1} using info from Comic Vine on {2}.  [{3}{4}]".format(
            ctversion.fork, ctversion.version,
            datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            settings.notes_format, issue_results['id'])

        metadata.webLink = issue_results['site_detail_url']

        person_credits = issue_results['person_credits']
        for person in person_credits:
            if 'role' in person:
                roles = person['role'].split(',')
                for role in roles:
                    # can we determine 'primary' from CV??
                    metadata.addCredit(person['name'],
                                       role.title().strip(), False)

        character_credits = issue_results['character_credits']
        character_list = list()
        for character in character_credits:
            character_list.append(character['name'])
        metadata.characters = utils.listToString(character_list)

        team_credits = issue_results['team_credits']
        team_list = list()
        for team in team_credits:
            team_list.append(team['name'])
        metadata.teams = utils.listToString(team_list)

        location_credits = issue_results['location_credits']
        location_list = list()
        for location in location_credits:
            location_list.append(location['name'])
        metadata.locations = utils.listToString(location_list)

        story_arc_credits = issue_results['story_arc_credits']
        arc_list = []
        for arc in story_arc_credits:
            arc_list.append(arc['name'])
        if len(arc_list) > 0:
            metadata.storyArc = utils.listToString(arc_list)

        return metadata

    def cleanup_html(self, string, remove_html_tables):
        """
        converter = html2text.HTML2Text()
        #converter.emphasis_mark = '*'
        #converter.ignore_links = True
        converter.body_width = 0

        print(html2text.html2text(string))
        return string
        #return converter.handle(string)
        """

        if string is None:
            return ""
        # find any tables
        soup = BeautifulSoup(string, "html.parser")
        tables = soup.findAll('table')

        # remove all newlines first
        string = string.replace("\n", "")

        # put in our own
        string = string.replace("<br>", "\n")
        string = string.replace("</p>", "\n\n")
        string = string.replace("<h4>", "*")
        string = string.replace("</h4>", "*\n")

        # remove the tables
        p = re.compile(r'<table[^<]*?>.*?<\/table>')
        if remove_html_tables:
            string = p.sub('', string)
            string = string.replace("*List of covers and their creators:*", "")
        else:
            string = p.sub('{}', string)

        # now strip all other tags
        p = re.compile(r'<[^<]*?>')
        newstring = p.sub('', string)

        newstring = newstring.replace('&nbsp;', ' ')
        newstring = newstring.replace('&amp;', '&')

        newstring = newstring.strip()

        if not remove_html_tables:
            # now rebuild the tables into text from BSoup
            try:
                table_strings = []
                for table in tables:
                    rows = []
                    hdrs = []
                    col_widths = []
                    for hdr in table.findAll('th'):
                        item = hdr.string.strip()
                        hdrs.append(item)
                        col_widths.append(len(item))
                    rows.append(hdrs)

                    for row in table.findAll('tr'):
                        cols = []
                        col = row.findAll('td')
                        i = 0
                        for c in col:
                            item = c.string.strip()
                            cols.append(item)
                            if len(item) > col_widths[i]:
                                col_widths[i] = len(item)
                            i += 1
                        if len(cols) != 0:
                            rows.append(cols)
                    # now we have the data, make it into text
                    fmtstr = ""
                    for w in col_widths:
                        fmtstr += " {{:{}}}|".format(w + 1)
                    width = sum(col_widths) + len(col_widths) * 2
                    print("width=", width)
                    table_text = ""
                    counter = 0
                    for row in rows:
                        table_text += fmtstr.format(*row) + "\n"
                        if counter == 0 and len(hdrs) != 0:
                            table_text += "-" * width + "\n"
                        counter += 1

                    table_strings.append(table_text)

                newstring = newstring.format(*table_strings)
            except:
                # we caught an error rebuilding the table.
                # just bail and remove the formatting
                print("table parse error")
                newstring.replace("{}", "")

        return newstring

    def fetchIssueDate(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        day, month, year = self.parseDateStr(details['cover_date'])
        return month, year

    def fetchIssueCoverURLs(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        return details['image_url'], details['thumb_image_url']

    def fetchIssuePageURL(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        return details['site_detail_url']

    def fetchIssueSelectDetails(self, issue_id):

        #cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails(issue_id)
        cached_details = self.fetchCachedIssueSelectDetails(issue_id)
        if cached_details['image_url'] is not None:
            return cached_details

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + \
            "&format=json&field_list=image,cover_date,site_detail_url"

        details = dict()
        details['image_url'] = None
        details['thumb_image_url'] = None
        details['cover_date'] = None
        details['site_detail_url'] = None

        cv_response = self.getCVContent(issue_url)

        details['image_url'] = cv_response['results']['image']['super_url']
        details['thumb_image_url'] = cv_response['results']['image'][
            'thumb_url']
        details['cover_date'] = cv_response['results']['cover_date']
        details['site_detail_url'] = cv_response['results']['site_detail_url']

        if details['image_url'] is not None:
            self.cacheIssueSelectDetails(issue_id, details['image_url'],
                                         details['thumb_image_url'],
                                         details['cover_date'],
                                         details['site_detail_url'])
        # print(details['site_detail_url'])
        return details

    def fetchCachedIssueSelectDetails(self, issue_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        return cvc.get_issue_select_details(issue_id)

    def cacheIssueSelectDetails(self, issue_id, image_url, thumb_url,
                                cover_date, page_url):
        cvc = ComicVineCacher()
        cvc.add_issue_select_details(issue_id, image_url, thumb_url,
                                     cover_date, page_url)

    def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
        url_list = self.fetchCachedAlternateCoverURLs(issue_id)
        if url_list is not None:
            return url_list

        # scrape the CV issue page URL to get the alternate cover URLs
        resp = urllib.request.urlopen(issue_page_url, context=self.ssl)
        content = resp.read()
        alt_cover_url_list = self.parseOutAltCoverUrls(content)

        # cache this alt cover URL list
        self.cacheAlternateCoverURLs(issue_id, alt_cover_url_list)

        return alt_cover_url_list

    def parseOutAltCoverUrls(self, page_html):
        soup = BeautifulSoup(page_html, "html.parser")

        alt_cover_url_list = []

        # Using knowledge of the layout of the Comic Vine issue page here:
        # look for the divs that are in the classes 'imgboxart' and
        # 'issue-cover'
        div_list = soup.find_all('div')
        covers_found = 0
        for d in div_list:
            if 'class' in d.attrs:
                c = d['class']
                if ('imgboxart' in c and 'issue-cover' in c
                        and d.img['src'].startswith("http")):

                    covers_found += 1
                    if covers_found != 1:
                        alt_cover_url_list.append(d.img['src'])

        return alt_cover_url_list

    def fetchCachedAlternateCoverURLs(self, issue_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        url_list = cvc.get_alt_covers(issue_id)
        if url_list is not None:
            return url_list
        else:
            return None

    def cacheAlternateCoverURLs(self, issue_id, url_list):
        cvc = ComicVineCacher()
        cvc.add_alt_covers(issue_id, url_list)

    #-------------------------------------------------------------------------
    urlFetchComplete = pyqtSignal(str, str, int)

    def asyncFetchIssueCoverURLs(self, issue_id):

        self.issue_id = issue_id
        details = self.fetchCachedIssueSelectDetails(issue_id)
        if details['image_url'] is not None:
            self.urlFetchComplete.emit(details['image_url'],
                                       details['thumb_image_url'],
                                       self.issue_id)
            return

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + \
            "&format=json&field_list=image,cover_date,site_detail_url"
        self.nam = QNetworkAccessManager()
        self.nam.finished.connect(self.asyncFetchIssueCoverURLComplete)
        self.nam.get(QNetworkRequest(QUrl(issue_url)))

    def asyncFetchIssueCoverURLComplete(self, reply):

        # read in the response
        data = reply.readAll()

        try:
            cv_response = json.loads(bytes(data))
        except Exception as e:
            print("Comic Vine query failed to get JSON data", file=sys.stderr)
            print(str(data), file=sys.stderr)
            return

        if cv_response['status_code'] != 1:
            print("Comic Vine query failed with error:  [{0}]. ".format(
                cv_response['error']),
                  file=sys.stderr)
            return

        image_url = cv_response['results']['image']['super_url']
        thumb_url = cv_response['results']['image']['thumb_url']
        cover_date = cv_response['results']['cover_date']
        page_url = cv_response['results']['site_detail_url']

        self.cacheIssueSelectDetails(self.issue_id, image_url, thumb_url,
                                     cover_date, page_url)

        self.urlFetchComplete.emit(image_url, thumb_url, self.issue_id)

    altUrlListFetchComplete = pyqtSignal(list, int)

    def asyncFetchAlternateCoverURLs(self, issue_id, issue_page_url):
        # This async version requires the issue page url to be provided!
        self.issue_id = issue_id
        url_list = self.fetchCachedAlternateCoverURLs(issue_id)
        if url_list is not None:
            self.altUrlListFetchComplete.emit(url_list, int(self.issue_id))
            return

        self.nam = QNetworkAccessManager()
        self.nam.finished.connect(self.asyncFetchAlternateCoverURLsComplete)
        self.nam.get(QNetworkRequest(QUrl(str(issue_page_url))))

    def asyncFetchAlternateCoverURLsComplete(self, reply):
        # read in the response
        html = str(reply.readAll())
        alt_cover_url_list = self.parseOutAltCoverUrls(html)

        # cache this alt cover URL list
        self.cacheAlternateCoverURLs(self.issue_id, alt_cover_url_list)

        self.altUrlListFetchComplete.emit(alt_cover_url_list,
                                          int(self.issue_id))

    def repairUrls(self, issue_list):
        # make sure there are URLs for the image fields
        for issue in issue_list:
            if issue['image'] is None:
                issue['image'] = dict()
                issue['image']['super_url'] = ComicVineTalker.logo_url
                issue['image']['thumb_url'] = ComicVineTalker.logo_url
class Node(QObject):
    def __init__(self, node_id: str, parent=None):
        QObject.__init__(self, parent)
        self._temperature = 293
        self._node_id = node_id
        self._server_url = "localhost"
        self._access_card = ""
        self.updateServerUrl(self._server_url)

        self._all_chart_data = {}

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkFinished)
        self._data = None
        self._enabled = True
        self._incoming_connections = []
        self._outgoing_connections = []
        self._onFinishedCallbacks = {}  # type: Dict[QNetworkReply, Callable[[QNetworkReply], None]]
        self._description = ""
        self._static_properties = {}
        self._performance = 1
        self._target_performance = 1
        self._min_performance = 0.5
        self._max_performance = 1
        self._max_safe_temperature = 500
        self._heat_convection = 1.0
        self._heat_emissivity = 1.0
        self._modifiers = []
        self._active = True

        self._update_timer = QTimer()
        self._update_timer.setInterval(30000)
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self.partialUpdate)


        # Timer that is used when the server could not be reached.
        self._failed_update_timer = QTimer()
        self._failed_update_timer.setInterval(30000)
        self._failed_update_timer.setSingleShot(True)
        self._failed_update_timer.timeout.connect(self.fullUpdate)

        self._additional_properties = []
        self._converted_additional_properties = {}
        self.server_reachable = False
        self._optimal_temperature = 200
        self._is_temperature_dependant = False
        self._resources_required = []
        self._optional_resources_required = []
        self._resources_received = []
        self._resources_produced = []
        self._resources_provided = []
        self._health = 100

        self._max_amount_stored = 0
        self._amount_stored = 0
        self._effectiveness_factor = 0

        self._random_delay_timer = QTimer()
        self._random_delay_timer.setInterval(random.randint(0, 29000))
        self._random_delay_timer.setSingleShot(True)
        self._random_delay_timer.timeout.connect(self.fullUpdate)
        self._random_delay_timer.start()

    temperatureChanged = Signal()
    historyPropertiesChanged = Signal()
    historyDataChanged = Signal()
    enabledChanged = Signal()
    incomingConnectionsChanged = Signal()
    outgoingConnectionsChanged = Signal()
    performanceChanged = Signal()
    staticPropertiesChanged = Signal()
    modifiersChanged = Signal()
    additionalPropertiesChanged = Signal()
    minPerformanceChanged = Signal()
    maxPerformanceChanged = Signal()
    maxSafeTemperatureChanged = Signal()
    heatConvectionChanged = Signal()
    heatEmissivityChanged = Signal()
    serverReachableChanged = Signal(bool)
    isTemperatureDependantChanged = Signal()
    optimalTemperatureChanged = Signal()
    targetPerformanceChanged = Signal()
    resourcesRequiredChanged = Signal()
    optionalResourcesRequiredChanged = Signal()
    resourcesReceivedChanged = Signal()
    resourcesProducedChanged = Signal()
    resourcesProvidedChanged = Signal()
    healthChanged = Signal()
    maxAmountStoredChanged = Signal()
    amountStoredChanged = Signal()
    effectivenessFactorChanged = Signal()
    activeChanged = Signal()

    def setAccessCard(self, access_card):
        self._access_card = access_card
        self._updateUrlsWithAuth(self._server_url, access_card)

    def _updateUrlsWithAuth(self, server_url, access_card):
        self._performance_url = f"{self._server_url}/node/{self._node_id}/performance/?accessCardID={self._access_card}"

    def updateServerUrl(self, server_url):
        if server_url == "":
            return

        self._server_url = f"http://{server_url}:5000"

        self._source_url = f"{self._server_url}/node/{self._node_id}/"
        self._incoming_connections_url = f"{self._server_url}/node/{self._node_id}/connections/incoming/"
        self._all_chart_data_url = f"{self._server_url}/node/{self._node_id}/all_property_chart_data/?showLast=50"
        self._outgoing_connections_url = f"{self._server_url}/node/{self._node_id}/connections/outgoing/"
        self._static_properties_url = f"{self._server_url}/node/{self._node_id}/static_properties/"
        self._modifiers_url = f"{self._server_url}/node/{self._node_id}/modifiers/"
        self._updateUrlsWithAuth(self._server_url, self._access_card)

    def get(self, url: str, callback: Callable[[QNetworkReply], None]) -> None:
        reply = self._network_manager.get(QNetworkRequest(QUrl(url)))
        self._onFinishedCallbacks[reply] = callback

    def fullUpdate(self) -> None:
        """
        Request all data of this node from the server
        :return:
        """
        self.get(self._incoming_connections_url, self._onIncomingConnectionsFinished)
        self.get(self._outgoing_connections_url, self._onOutgoingConnectionsFinished)
        self.get(self._static_properties_url, self._onStaticPropertiesFinished)

        self.partialUpdate()
        self._update_timer.start()

    @Slot()
    def partialUpdate(self) -> None:
        """
        Request all the data that is dynamic
        :return:
        """
        self.get(self._source_url, self._onSourceUrlFinished)
        self.get(self._all_chart_data_url, self._onChartDataFinished)
        self.get(self._modifiers_url, self._onModifiersChanged)

    def _setServerReachable(self, server_reachable: bool):
        if self.server_reachable != server_reachable:
            self.server_reachable = server_reachable
            self.serverReachableChanged.emit(self.server_reachable)

    def _readData(self, reply: QNetworkReply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 404:
            print("Node was not found!")
            return
        # For some magical reason, it segfaults if I convert the readAll() data directly to bytes.
        # So, yes, the extra .data() is needed.
        data = bytes(reply.readAll().data())
        if not data or status_code == 503:
            self._failed_update_timer.start()
            self._update_timer.stop()
            self._setServerReachable(False)
            return None
        self._setServerReachable(True)
        try:
            return json.loads(data)
        except json.decoder.JSONDecodeError:
            return None

    def updateAdditionalProperties(self, data):
        if self._additional_properties != data:
            self._additional_properties = data
            self._converted_additional_properties = {}
            # Clear the list and convert them in a way that we can use them in a repeater.
            for additional_property in data:
                self._converted_additional_properties[additional_property["key"]] = {
                                                              "value": additional_property["value"],
                                                              "max_value": additional_property["max_value"]}

            self.additionalPropertiesChanged.emit()

    def _onModifiersChanged(self, reply: QNetworkReply):
        result = self._readData(reply)
        if result is None:
            result = []
        if self._modifiers != result:
            self._modifiers = result
            self.modifiersChanged.emit()

    def _onPerformanceChanged(self, reply: QNetworkReply):
        print("CALLBAAACk")
        result = self._readData(reply)
        if not result:
            return
        if self._performance != result:
            self._performance = result
            self.performanceChanged.emit()

    @Slot(float)
    def setPerformance(self, performance):
        data = "{\"performance\": %s}" % performance
        self._target_performance = performance
        self.targetPerformanceChanged.emit()
        reply = self._network_manager.put(QNetworkRequest(QUrl(self._performance_url)), data.encode())
        self._onFinishedCallbacks[reply] = self._onPerformanceChanged

    @Slot(str)
    def addModifier(self, modifier: str):
        data = "{\"modifier_name\": \"%s\"}" % modifier
        request = QNetworkRequest(QUrl(self._modifiers_url))
        request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")

        reply = self._network_manager.post(request, data.encode())

        self._onFinishedCallbacks[reply] = self._onModifiersChanged

    @Property(float, notify=performanceChanged)
    def performance(self):
        return self._performance

    @Property(float, notify=targetPerformanceChanged)
    def targetPerformance(self):
        return self._target_performance

    @Property("QVariantList", notify=resourcesRequiredChanged)
    def resourcesRequired(self):
        return self._resources_required

    @Property("QVariantList", notify=resourcesProducedChanged)
    def resourcesProduced(self):
        return self._resources_produced

    @Property("QVariantList", notify=resourcesProvidedChanged)
    def resourcesProvided(self):
        return self._resources_provided

    @Property("QVariantList", notify=resourcesReceivedChanged)
    def resourcesReceived(self):
        return self._resources_received

    @Property("QVariantList", notify=optionalResourcesRequiredChanged)
    def optionalResourcesRequired(self):
        return self._optional_resources_required

    @Property("QVariantList", notify=modifiersChanged)
    def modifiers(self):
        return self._modifiers

    @Property(float, notify=minPerformanceChanged)
    def min_performance(self):
        return self._min_performance

    @Property(float, notify=maxPerformanceChanged)
    def max_performance(self):
        return self._max_performance

    @Property(float, notify=healthChanged)
    def health(self):
        return self._health

    def _onStaticPropertiesFinished(self, reply: QNetworkReply) -> None:
        result = self._readData(reply)
        if not result:
            return
        if self._static_properties != result:
            self._static_properties = result
            self.staticPropertiesChanged.emit()

    def _onIncomingConnectionsFinished(self, reply: QNetworkReply):
        result = self._readData(reply)
        if not result:
            return
        self._incoming_connections = result
        self.incomingConnectionsChanged.emit()

    def _onOutgoingConnectionsFinished(self, reply: QNetworkReply):
        result = self._readData(reply)
        if not result:
            return
        self._outgoing_connections = result
        self.outgoingConnectionsChanged.emit()

    @Property("QVariantList", notify=incomingConnectionsChanged)
    def incomingConnections(self):
        return self._incoming_connections

    @Property(str, notify=staticPropertiesChanged)
    def description(self):
        return self._static_properties.get("description", "")

    @Property(str, notify=staticPropertiesChanged)
    def node_type(self):
        return self._static_properties.get("node_type", "")

    @Property(str, notify=staticPropertiesChanged)
    def custom_description(self):
        return self._static_properties.get("custom_description", "")

    @Property("QStringList", notify=staticPropertiesChanged)
    def supported_modifiers(self):
        return self._static_properties.get("supported_modifiers", "")

    @Property(bool, notify=staticPropertiesChanged)
    def hasSettablePerformance(self):
        return self._static_properties.get("has_settable_performance", False)

    @Property(str, notify=staticPropertiesChanged)
    def label(self):
        return self._static_properties.get("label", self._node_id)

    @Property(float, notify=staticPropertiesChanged)
    def surface_area(self):
        return self._static_properties.get("surface_area", 0)

    @Property(float, notify=isTemperatureDependantChanged)
    def isTemperatureDependant(self):
        return self._is_temperature_dependant

    @Property(float, notify=activeChanged)
    def active(self):
        return self._active

    @Property(float, notify=optimalTemperatureChanged)
    def optimalTemperature(self):
        return self._optimal_temperature

    @Property(float, notify=maxSafeTemperatureChanged)
    def max_safe_temperature(self):
        return self._max_safe_temperature

    @Property(float, notify=heatConvectionChanged)
    def heat_convection(self):
        return self._heat_convection

    @Property(float, notify=heatEmissivityChanged)
    def heat_emissivity(self):
        return self._heat_emissivity

    @Property("QVariantList", notify=outgoingConnectionsChanged)
    def outgoingConnections(self):
        return self._outgoing_connections

    def _onSourceUrlFinished(self, reply: QNetworkReply):
        data = self._readData(reply)
        if not data:
            return
        self._updateProperty("temperature", data["temperature"] - 273.15 )
        self._updateProperty("enabled", bool(data["enabled"]))
        self._updateProperty("active", bool(data["active"]))
        self._updateProperty("performance", data["performance"])
        self._updateProperty("min_performance", data["min_performance"])
        self._updateProperty("max_performance", data["max_performance"])
        self._updateProperty("max_safe_temperature", data["max_safe_temperature"] - 273.15)
        self._updateProperty("heat_convection", data["heat_convection"])
        self._updateProperty("heat_emissivity", data["heat_emissivity"])
        self._updateProperty("is_temperature_dependant", data["is_temperature_dependant"])
        self._updateProperty("optimal_temperature", data["optimal_temperature"] - 273.15)
        self._updateProperty("target_performance", data["target_performance"])
        self._updateProperty("health", data["health"])
        self._updateProperty("effectiveness_factor", data["effectiveness_factor"])

        # We need to update the resources a bit different to prevent recreation of QML items.
        # As such we use tiny QObjects with their own getters and setters.
        # If an object is already in the list with the right type, don't recreate it (just update it's value)
        self.updateResourceList("optional_resources_required", data["optional_resources_required"])
        self.updateResourceList("resources_received", data["resources_received"])
        self.updateResourceList("resources_required", data["resources_required"])
        self.updateResourceList("resources_produced", data["resources_produced"])
        self.updateResourceList("resources_provided", data["resources_provided"])

        self.updateAdditionalProperties(data["additional_properties"])

    def updateResourceList(self, property_name, data):
        list_to_check = getattr(self, "_" + property_name)
        list_updated = False

        for item in data:
            item_found = False
            for resource in list_to_check:
                if item["resource_type"] == resource.type:
                    item_found = True
                    resource.value = item["value"]
                    break

            if not item_found:
                list_updated = True
                list_to_check.append(NodeResource(item["resource_type"], item["value"]))

        if list_updated:
            signal_name = "".join(x.capitalize() for x in property_name.split("_"))
            signal_name = signal_name[0].lower() + signal_name[1:] + "Changed"
            getattr(self, signal_name).emit()

    def _updateProperty(self, property_name, property_value):
        if getattr(self, "_" + property_name) != property_value:
            setattr(self, "_" + property_name, property_value)
            signal_name = "".join(x.capitalize() for x in property_name.split("_"))
            signal_name = signal_name[0].lower() + signal_name[1:] + "Changed"
            getattr(self, signal_name).emit()

    def _onPutUpdateFinished(self, reply: QNetworkReply):
        pass

    def _onChartDataFinished(self, reply: QNetworkReply):
        data = self._readData(reply)
        if not data:
            return

        # Offset is given in the reply, but it's not a list of data. Remove it here.
        if "offset" in data:
            del data["offset"]

        all_keys = set(data.keys())

        keys_changed = False
        data_changed = False
        if set(self._all_chart_data.keys()) != all_keys:
            keys_changed = True
        if self._all_chart_data != data:
            data_changed = True
            self._all_chart_data = data

        if data_changed:
            self.historyDataChanged.emit()
        if keys_changed:
            self.historyPropertiesChanged.emit()

    def _onNetworkFinished(self, reply: QNetworkReply):
        if reply in self._onFinishedCallbacks:
            self._onFinishedCallbacks[reply](reply)
            del self._onFinishedCallbacks[reply]
        else:
            print("GOT A RESPONSE WITH NO CALLBACK!", reply.readAll())

    @Property(str, constant=True)
    def id(self):
        return self._node_id

    @Property(bool, notify = enabledChanged)
    def enabled(self):
        return self._enabled

    @Property(float, notify=amountStoredChanged)
    def amount_stored(self):
        return self._amount_stored

    @Property(float, notify=effectivenessFactorChanged)
    def effectiveness_factor(self):
        return self._effectiveness_factor

    @Property(float, notify=temperatureChanged)
    def temperature(self):
        return self._temperature

    @Property("QVariantList", notify=historyPropertiesChanged)
    def allHistoryProperties(self):
        return list(self._all_chart_data.keys())

    @Property("QVariantMap", notify=historyDataChanged)
    def historyData(self):
        return self._all_chart_data

    @Property("QVariantMap", notify=additionalPropertiesChanged)
    def additionalProperties(self):
        return self._converted_additional_properties

    @Slot()
    def toggleEnabled(self):
        url = self._source_url + "enabled/"
        reply = self._network_manager.put(QNetworkRequest(url), QByteArray())
        self._onFinishedCallbacks[reply] = self._onPutUpdateFinished
        # Already trigger an update, so the interface feels snappy
        self._enabled = not self._enabled
        self.enabledChanged.emit()
예제 #51
0
class HTTPClient(QObject):
    """A HTTP client based on QNetworkAccessManager.

    Intended for APIs, automatically decodes data.

    Attributes:
        _nam: The QNetworkAccessManager used.
        _timers: A {QNetworkReply: QTimer} dict.

    Signals:
        success: Emitted when the operation succeeded.
                 arg: The received data.
        error: Emitted when the request failed.
               arg: The error message, as string.
    """

    success = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._nam = QNetworkAccessManager(self)
        self._timers = {}

    def post(self, url, data=None):
        """Create a new POST request.

        Args:
            url: The URL to post to, as QUrl.
            data: A dict of data to send.
        """
        if data is None:
            data = {}
        encoded_data = urllib.parse.urlencode(data).encode('utf-8')
        request = QNetworkRequest(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          'application/x-www-form-urlencoded;charset=utf-8')
        reply = self._nam.post(request, encoded_data)
        self._handle_reply(reply)

    def get(self, url):
        """Create a new GET request.

        Emits success/error when done.

        Args:
            url: The URL to access, as QUrl.
        """
        request = QNetworkRequest(url)
        reply = self._nam.get(request)
        self._handle_reply(reply)

    def _handle_reply(self, reply):
        """Handle a new QNetworkReply."""
        if reply.isFinished():
            self.on_reply_finished(reply)
        else:
            timer = QTimer(self)
            timer.setInterval(10000)
            timer.timeout.connect(reply.abort)
            timer.start()
            self._timers[reply] = timer
            reply.finished.connect(
                functools.partial(self.on_reply_finished, reply))

    def on_reply_finished(self, reply):
        """Read the data and finish when the reply finished.

        Args:
            reply: The QNetworkReply which finished.
        """
        timer = self._timers.pop(reply)
        if timer is not None:
            timer.stop()
            timer.deleteLater()
        if reply.error() != QNetworkReply.NoError:
            self.error.emit(reply.errorString())
            return
        try:
            data = bytes(reply.readAll()).decode('utf-8')
        except UnicodeDecodeError:
            self.error.emit("Invalid UTF-8 data received in reply!")
            return
        self.success.emit(data)
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference("um3networkprinting/manual_instances", "") #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [printer_name for printer_name in self._printers]
            self._printers = {}
            self.printerListChanged.emit()
        # After network switching, one must make a new instance of Zeroconf
        # On windows, the instance creation is very fast (unnoticable). Other platforms?
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._onServiceChanged])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        name = address
        instance_name = "manual:%s" % address
        properties = { b"name": name.encode("utf-8"), b"manual": b"true", b"incomplete": b"true" }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        self.checkManualPrinter(address)

    def removeManualPrinter(self, key, address = None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def checkManualPrinter(self, address):
        # Check if a printer exists at this address
        # If a printer responds, it will replace the preliminary printer created above
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    ##  Handler for all requests that have finished.
    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "system" in reply_url:  # Name returned from printer.
                if status_code == 200:
                    system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    address = reply.url().host()
                    name = ("%s (%s)" % (system_info["name"], address))

                    instance_name = "manual:%s" % address
                    properties = { b"name": name.encode("utf-8"), b"firmware_version": system_info["firmware"].encode("utf-8"), b"manual": b"true" }
                    if instance_name in self._printers:
                        # Only replace the printer if it is still in the list of (manual) printers
                        self.removePrinter(instance_name)
                        self.addPrinter(instance_name, address, properties)

    ##  Stop looking for devices on network.
    def stop(self):
        if self._zero_conf is not None:
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("um_network_key"):
                self._printers[key].connect()
                self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    self._printers[key].close()

    ##  Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
    def addPrinter(self, name, address, properties):
        printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
            if printer.getKey() not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
                printer.disconnect()
        self.printerListChanged.emit()

    ##  Handler for when the connection state of one of the detected printers changes
    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        if self._printers[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    ##  Handler for zeroConf detection
    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties = {})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None).decode("utf-8")
                if type_of_device == "printer":
                    address = '.'.join(map(lambda n: str(n), info.address))
                    self.addPrinterSignal.emit(str(name), address, info.properties)
                else:
                    Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." %type_of_device )
            else:
                Logger.log("w", "Could not get information about %s" % name)

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))
예제 #53
0
class GridWidget(QWidget):
    Page = 0
    loadStarted = pyqtSignal(bool)

    def __init__(self, *args, **kwargs):
        super(GridWidget, self).__init__(*args, **kwargs)
        self._layout = QGridLayout(self, spacing=20)
        self._layout.setContentsMargins(20, 20, 20, 20)
        # 异步网络下载管理器
        self._manager = QNetworkAccessManager(self)
        self._manager.finished.connect(self.onFinished)

    def load(self):
        if self.Page == -1:
            return
        self.loadStarted.emit(True)
        # 延迟一秒后调用目的在于显示进度条
        QTimer.singleShot(1000, self._load)

    def _load(self):
        print("load url:", Url.format(self.Page * 30))
        url = QUrl(Url.format(self.Page * 30))
        self._manager.get(QNetworkRequest(url))

    def onFinished(self, reply):
        # 请求完成后会调用该函数
        req = reply.request()  # 获取请求
        iwidget = req.attribute(QNetworkRequest.User + 1, None)
        path = req.attribute(QNetworkRequest.User + 2, None)
        html = reply.readAll().data()
        reply.deleteLater()
        del reply
        if iwidget and path and html:
            # 这里是图片下载完毕
            open(path, "wb").write(html)
            iwidget.setCover(path)
            return
        # 解析网页
        self._parseHtml(html)
        self.loadStarted.emit(False)

    def splist(self, src, length):
        # 等分列表
        return (src[i:i + length] for i in range(len(src)) if i % length == 0)

    def _parseHtml(self, html):
        #         encoding = chardet.detect(html) or {}
        #         html = html.decode(encoding.get("encoding","utf-8"))
        html = HTML(html)
        # 查找所有的li list_item
        lis = html.xpath("//li[@class='list_item']")
        if not lis:
            self.Page = -1  # 后面没有页面了
            return
        lack_count = self._layout.count() % 30  # 获取布局中上次还缺几个5行*6列的标准
        row_count = int(self._layout.count() / 6)  # 行数
        print("lack_count:", lack_count)
        self.Page += 1  # 自增+1
        if lack_count != 0:  # 上一次没有满足一行6个,需要补齐
            lack_li = lis[:lack_count]
            lis = lis[lack_count:]
            self._makeItem(lack_li, row_count)  # 补齐
            if lack_li and lis:
                row_count += 1
                self._makeItem(lis, row_count)  # 完成剩下的
        else:
            self._makeItem(lis, row_count)

    def _makeItem(self, li_s, row_count):
        li_s = self.splist(li_s, 6)
        for row, lis in enumerate(li_s):
            for col, li in enumerate(lis):
                a = li.find("a")
                video_url = a.get("href")  # 视频播放地址
                img = a.find("img")
                cover_url = "http:" + img.get("r-lazyload")  # 封面图片
                figure_title = img.get("alt")  # 电影名
                figure_info = a.find("div/span")
                figure_info = "" if figure_info is None else figure_info.text  # 影片信息
                figure_score = "".join(li.xpath(".//em/text()"))  # 评分
                # 主演
                figure_desc = "<span style=\"font-size: 12px;\">主演:</span>" + \
                              "".join([Actor.format(**dict(fd.items()))
                                       for fd in li.xpath(".//div[@class='figure_desc']/a")])
                # 播放数
                figure_count = (
                    li.xpath(".//div[@class='figure_count']/span/text()")
                    or [""])[0]
                path = "cache/{0}.jpg".format(
                    os.path.splitext(os.path.basename(video_url))[0])
                cover_path = "Data/pic_v.png"
                if os.path.isfile(path):
                    cover_path = path
                iwidget = ItemWidget(cover_path, figure_info, figure_title,
                                     figure_score, figure_desc, figure_count,
                                     video_url, cover_url, path, self)
                self._layout.addWidget(iwidget, row_count + row, col)
예제 #54
0
class TopSellingProducts(QWidget):
    def __init__(self, parent=None):
        super(TopSellingProducts, self).__init__(parent)
        
        self.ui = Ui_TopSellingProducts()
        self.ui.setupUi(self)
        self.ui.tableWidget.setColumnCount(5);
        self.ui.tableWidget.setColumnWidth(0, 120);
        self.ui.tableWidget.setColumnWidth(1, 480);
        
        self.manager = QNetworkAccessManager(self)
        self.manager.finished.connect(self.on_finished)
        self.manager.sslErrors.connect(self.on_sslErrors)
        
        self.replyMap = dict()
        
    @pyqtSlot(bool)    
    def on_wipePushButton_clicked(self, checked):
        while self.ui.tableWidget.rowCount() > 0:
            self.ui.tableWidget.removeRow(0)
        
    @pyqtSlot(bool)    
    def on_searchPushButton_clicked(self, checked):
        merchandising = Merchandising(warnings = False)
        response = merchandising.execute('getTopSellingProducts')
        reply = response.reply
        productRecommendations = reply.productRecommendations.product
        row = self.ui.tableWidget.rowCount()
        for product in productRecommendations:
            self.ui.tableWidget.insertRow(row)
            if product.has_key('imageURL'):
                imageUrl = product.imageURL
                reply = self.manager.get(QNetworkRequest(QUrl(imageUrl)))
                self.replyMap[reply] = row
            viewProductURL = QLabel()
            viewProductURL.setOpenExternalLinks(True)
            #viewProductURL.setTextInteractionFlags(Qt.TextBrowserInteraction)
            title = '<a href="%s">%s</a>' % (product.productURL, product.title)
            viewProductURL.setText(title)
            self.ui.tableWidget.setCellWidget(row, 1, viewProductURL)
            self.ui.tableWidget.setItem(row, 2, QTableWidgetItem(product.catalogName))
            if product.has_key('priceRangeMin') and product.has_key('priceRangeMax'):
                self.ui.tableWidget.setItem(row, 3, QTableWidgetItem('%s - %s' % (product.priceRangeMin.value, product.priceRangeMax.value)))
            self.ui.tableWidget.setItem(row, 4, QTableWidgetItem(product.reviewCount))
            row += 1
                
    @pyqtSlot(QNetworkReply)
    def on_finished(self, reply):
        if reply in self.replyMap:
            row = self.replyMap.get(reply)
            del self.replyMap[reply]
            pixmap = QPixmap()
            pixmap.loadFromData(reply.readAll())
            image = QLabel()
            image.setPixmap(pixmap)
            self.ui.tableWidget.setCellWidget(row, 0, image)
            self.ui.tableWidget.setRowHeight(row, 120)
    
    @pyqtSlot(QNetworkReply, list)
    def on_sslErrors(self, reply, errors):
        if reply in self.replyMap:
            del self.replyMap[reply]
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(
            self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [
                printer_name for printer_name in self._printers
            ]
            self._printers = {}
            self.printerListChanged.emit()
        # After network switching, one must make a new instance of Zeroconf
        # On windows, the instance creation is very fast (unnoticable). Other platforms?
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf,
                                       u'_ultimaker._tcp.local.',
                                       [self._onServiceChanged])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true"
        }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        self.checkManualPrinter(address)

    def removeManualPrinter(self, key, address=None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

    def checkManualPrinter(self, address):
        # Check if a printer exists at this address
        # If a printer responds, it will replace the preliminary printer created above
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    ##  Handler for all requests that have finished.
    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "system" in reply_url:  # Name returned from printer.
                if status_code == 200:
                    system_info = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))
                    address = reply.url().host()

                    instance_name = "manual:%s" % address
                    machine = "unknown"
                    if "variant" in system_info:
                        variant = system_info["variant"]
                        if variant == "Ultimaker 3":
                            machine = "9066"
                        elif variant == "Ultimaker 3 Extended":
                            machine = "9511"

                    properties = {
                        b"name":
                        system_info["name"].encode("utf-8"),
                        b"address":
                        address.encode("utf-8"),
                        b"firmware_version":
                        system_info["firmware"].encode("utf-8"),
                        b"manual":
                        b"true",
                        b"machine":
                        machine.encode("utf-8")
                    }
                    if instance_name in self._printers:
                        # Only replace the printer if it is still in the list of (manual) printers
                        self.removePrinter(instance_name)
                        self.addPrinter(instance_name, address, properties)

    ##  Stop looking for devices on network.
    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("um_network_key"):
                if not self._printers[key].isConnected():
                    Logger.log("d", "Connecting [%s]..." % key)
                    self._printers[key].connect()
                    self._printers[key].connectionStateChanged.connect(
                        self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    Logger.log("d", "Closing connection [%s]..." % key)
                    self._printers[key].close()
                    self._printers[key].connectionStateChanged.disconnect(
                        self._onPrinterConnectionStateChanged)

    ##  Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
    def addPrinter(self, name, address, properties):
        printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(
            name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack and printer.getKey(
        ) == global_container_stack.getMetaDataEntry("um_network_key"):
            if printer.getKey(
            ) not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                Logger.log("d",
                           "addPrinter, connecting [%s]..." % printer.getKey())
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(
                    self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.disconnect()
                printer.connectionStateChanged.disconnect(
                    self._onPrinterConnectionStateChanged)
                Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
        self.printerListChanged.emit()

    ##  Handler for when the connection state of one of the detected printers changes
    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        if self._printers[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    ##  Handler for zeroConf detection
    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addPrinterSignal.emit(str(name), address,
                                                   info.properties)
                    else:
                        Logger.log(
                            "w",
                            "The type of the found device is '%s', not 'printer'! Ignoring.."
                            % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))
예제 #56
0
class NetworkMJPGImage(QQuickPaintedItem):

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self._stream_buffer = QByteArray()
        self._stream_buffer_start_index = -1
        self._network_manager = None  # type: QNetworkAccessManager
        self._image_request = None  # type: QNetworkRequest
        self._image_reply = None  # type: QNetworkReply
        self._image = QImage()
        self._image_rect = QRect()

        self._source_url = QUrl()
        self._started = False

        self._mirror = False

        self.setAntialiasing(True)

    ##  Ensure that close gets called when object is destroyed
    def __del__(self) -> None:
        self.stop()


    def paint(self, painter: "QPainter") -> None:
        if self._mirror:
            painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
            return

        painter.drawImage(self.contentsBoundingRect(), self._image)


    def setSourceURL(self, source_url: "QUrl") -> None:
        self._source_url = source_url
        self.sourceURLChanged.emit()
        if self._started:
            self.start()

    def getSourceURL(self) -> "QUrl":
        return self._source_url

    sourceURLChanged = pyqtSignal()
    source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)

    def setMirror(self, mirror: bool) -> None:
        if mirror == self._mirror:
            return
        self._mirror = mirror
        self.mirrorChanged.emit()
        self.update()

    def getMirror(self) -> bool:
        return self._mirror

    mirrorChanged = pyqtSignal()
    mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)

    imageSizeChanged = pyqtSignal()

    @pyqtProperty(int, notify = imageSizeChanged)
    def imageWidth(self) -> int:
        return self._image.width()

    @pyqtProperty(int, notify = imageSizeChanged)
    def imageHeight(self) -> int:
        return self._image.height()


    @pyqtSlot()
    def start(self) -> None:
        self.stop()  # Ensure that previous requests (if any) are stopped.

        if not self._source_url:
            Logger.log("w", "Unable to start camera stream without target!")
            return
        self._started = True

        self._image_request = QNetworkRequest(self._source_url)
        if self._network_manager is None:
            self._network_manager = QNetworkAccessManager()

        self._image_reply = self._network_manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)

    @pyqtSlot()
    def stop(self) -> None:
        self._stream_buffer = QByteArray()
        self._stream_buffer_start_index = -1

        if self._image_reply:
            try:
                try:
                    self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
                except Exception:
                    pass

                if not self._image_reply.isFinished():
                    self._image_reply.close()
            except Exception as e:  # RuntimeError
                pass  # It can happen that the wrapped c++ object is already deleted.

            self._image_reply = None
            self._image_request = None

        self._network_manager = None

        self._started = False


    def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
        # An MJPG stream is (for our purpose) a stream of concatenated JPG images.
        # JPG images start with the marker 0xFFD8, and end with 0xFFD9
        if self._image_reply is None:
            return
        self._stream_buffer += self._image_reply.readAll()

        if len(self._stream_buffer) > 2000000:  # No single camera frame should be 2 Mb or larger
            Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
            self.stop()  # resets stream buffer and start index
            self.start()
            return

        if self._stream_buffer_start_index == -1:
            self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
        stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
        # If this happens to be more than a single frame, then so be it; the JPG decoder will
        # ignore the extra data. We do it like this in order not to get a buildup of frames

        if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
            jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
            self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
            self._stream_buffer_start_index = -1
            self._image.loadFromData(jpg_data)

            if self._image.rect() != self._image_rect:
                self.imageSizeChanged.emit()

            self.update()
예제 #57
0
class HTTPClient(QObject):

    """An HTTP client based on QNetworkAccessManager.

    Intended for APIs, automatically decodes data.

    Attributes:
        _nam: The QNetworkAccessManager used.
        _timers: A {QNetworkReply: QTimer} dict.

    Signals:
        success: Emitted when the operation succeeded.
                 arg: The received data.
        error: Emitted when the request failed.
               arg: The error message, as string.
    """

    success = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._nam = QNetworkAccessManager(self)
        self._timers = {}

    def post(self, url, data=None):
        """Create a new POST request.

        Args:
            url: The URL to post to, as QUrl.
            data: A dict of data to send.
        """
        if data is None:
            data = {}
        encoded_data = urllib.parse.urlencode(data).encode('utf-8')
        request = QNetworkRequest(url)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          'application/x-www-form-urlencoded;charset=utf-8')
        reply = self._nam.post(request, encoded_data)
        self._handle_reply(reply)

    def get(self, url):
        """Create a new GET request.

        Emits success/error when done.

        Args:
            url: The URL to access, as QUrl.
        """
        request = QNetworkRequest(url)
        reply = self._nam.get(request)
        self._handle_reply(reply)

    def _handle_reply(self, reply):
        """Handle a new QNetworkReply."""
        if reply.isFinished():
            self.on_reply_finished(reply)
        else:
            timer = QTimer(self)
            timer.setInterval(10000)
            timer.timeout.connect(reply.abort)
            timer.start()
            self._timers[reply] = timer
            reply.finished.connect(functools.partial(
                self.on_reply_finished, reply))

    def on_reply_finished(self, reply):
        """Read the data and finish when the reply finished.

        Args:
            reply: The QNetworkReply which finished.
        """
        timer = self._timers.pop(reply)
        if timer is not None:
            timer.stop()
            timer.deleteLater()
        if reply.error() != QNetworkReply.NoError:
            self.error.emit(reply.errorString())
            return
        try:
            data = bytes(reply.readAll()).decode('utf-8')
        except UnicodeDecodeError:
            self.error.emit("Invalid UTF-8 data received in reply!")
            return
        self.success.emit(data)
예제 #58
0
class RepetierServerOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, port, properties):
        super().__init__(key)

        self._address = address
        self._port = port
        self._path = properties["path"] if "path" in properties else "/"
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None
        self._auto_print = True

        ##  Todo: Hardcoded value now; we should probably read this from the machine definition and Repetier-Server.
        self._num_extruders_set = False
        self._num_extruders = 1
        self._slug = "fanera1"
        self._api_version = "1"
        self._api_prefix = "printer/api/" + self._slug
        self._api_header = "X-Api-Key"
        self._api_key = None

        self._base_url = "http://%s:%d%s" % (self._address, self._port,
                                             self._path)
        self._api_url = self._base_url + self._api_prefix
        self._model_url = self._base_url + "printer/model/" + self._slug + "?a=upload"
        self._camera_url = "http://%s:8080/?action=snapshot" % self._address

        self.setPriority(
            2
        )  # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button", "Print with Repetier-Server"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip",
                               "Print with Repetier-Server"))
        self.setIconName("print")
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connected to Repetier-Server on {0}").format(
                                   self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._command_request = None
        self._command_reply = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(
            2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_timer = QTimer()
        self._camera_timer.setInterval(
            500)  # Todo: Add preference for camera update interval
        self._camera_timer.setSingleShot(False)
        self._camera_timer.timeout.connect(self._update_camera)

        self._camera_image_id = 0

        self._camera_image = QImage()

        self._connection_state_before_timeout = None

        self._last_response_time = None
        self._last_request_time = None
        self._response_timeout_time = 5
        self._recreate_network_manager_time = 30  # If we have no connection, re-create network manager every 30 sec.
        self._recreate_network_manager_count = 1

    def getProperties(self):
        return self._properties

    @pyqtSlot(str, result=str)
    def getProperty(self, key):
        key = key.encode("utf-8")
        if key in self._properties:
            return self._properties.get(key, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result=str)
    def getKey(self):
        return self._key

    ##  Set the API key of this Repetier-Server instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self):
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self):
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self):
        return self._base_url

    def _update_camera(self):
        ## Request new image
        url = QUrl(self._camera_url)
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)

    def _update(self):
        if self._last_response_time:
            time_since_last_response = time() - self._last_response_time
        else:
            time_since_last_response = 0
        if self._last_request_time:
            time_since_last_request = time() - self._last_request_time
        else:
            time_since_last_request = float(
                "inf")  # An irrelevantly large number of seconds

        # Connection is in timeout, check if we need to re-start the connection.
        # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
        # Re-creating the QNetworkManager seems to fix this issue.
        if self._last_response_time and self._connection_state_before_timeout:
            if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
                self._recreate_network_manager_count += 1
                # It can happen that we had a very long timeout (multiple times the recreate time).
                # In that case we should jump through the point that the next update won't be right away.
                while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
                    self._recreate_network_manager_count += 1
                Logger.log(
                    "d",
                    "Timeout lasted over 30 seconds (%.1fs), re-checking connection.",
                    time_since_last_response)
                self._createNetworkManager()
                return

        # Check if we have an connection in the first place.
        if not self._manager.networkAccessible():
            if not self._connection_state_before_timeout:
                Logger.log(
                    "d",
                    "The network connection seems to be disabled. Going into timeout mode"
                )
                self._connection_state_before_timeout = self._connection_state
                self.setConnectionState(ConnectionState.error)
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with the network was lost."))
                self._connection_message.show()
                # Check if we were uploading something. Abort if this is the case.
                # Some operating systems handle this themselves, others give weird issues.
                try:
                    if self._post_reply:
                        Logger.log(
                            "d",
                            "Stopping post upload because the connection was lost."
                        )
                        try:
                            self._post_reply.uploadProgress.disconnect(
                                self._onUploadProgress)
                        except TypeError:
                            pass  # The disconnection can fail on mac in some cases. Ignore that.

                        self._post_reply.abort()
                        self._progress_message.hide()
                except RuntimeError:
                    self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
            return
        else:
            if not self._connection_state_before_timeout:
                self._recreate_network_manager_count = 1

        # Check that we aren't in a timeout state
        if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
            if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
                # Go into timeout state.
                Logger.log(
                    "d",
                    "We did not receive a response for %s seconds, so it seems Repetier-Server is no longer accesible.",
                    time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with Repetier-Server was lost. Check your network-connections."
                    ))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        urlString = self._api_url + "?a=stateList"
        #Logger.log("d","XXX URL: " + urlString)
        url = QUrl(urlString)
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl(self._api_url + "?a=listPrinter")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(),
                                       self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def _createNetworkManager(self):
        if self._manager:
            self._manager.finished.disconnect(self._onRequestFinished)

        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()
        self._camera_timer.stop()
        self._camera_image = QImage()
        self.newImage.emit()

    def requestWrite(self, node, file_name=None, filter_by_machine=False):
        self.writeStarted.emit(self)
        self._gcode = getattr(
            Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        #self.close()  # Ensure that previous connection (if any) is killed.
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        self._createNetworkManager()

        self.setConnectionState(ConnectionState.connecting)
        self._update(
        )  # Manually trigger the first update, as we don't want to wait a few secs before it starts.

        Logger.log("d", "Connection with instance %s with ip %s started",
                   self._key, self._address)
        self._update_timer.start()

        if parseBool(
                global_container_stack.getMetaDataEntry(
                    "octoprint_show_camera", False)):
            self._update_camera()
            self._camera_timer.start()
        else:
            self._camera_timer.stop()
            self._camera_image = QImage()
            self.newImage.emit()

        self._last_response_time = None
        self.setAcceptsCommands(False)
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connecting to Repetier-Server on {0}").format(
                                   self._key))

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with ip %s stopped",
                   self._key, self._address)
        self.close()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify=newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):

        # if job_state == "abort":
        #     command = "cancel"
        # elif job_state == "print":
        #     if self.jobState == "paused":
        #         command = "pause"
        #     else:
        #         command = "start"
        # elif job_state == "pause":
        #     command = "pause"

        urlString = ""
        if job_state == "abort":
            command = "cancel"
            urlString = self._api_url + '?a=stopJob'
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
                urlString = self._api_url + '?a=send&data={"cmd":"@pause"}'
            else:
                command = "start"
                urlString = self._api_url + '?a=continueJob'
        elif job_state == "pause":
            command = "pause"
            urlString = self._api_url + '?a=send&data={"cmd":"@pause"}'

        Logger.log("d", "XXX:Command:" + command)
        Logger.log("d", "XXX:Command:" + urlString)

        if urlString:
            url = QUrl(urlString)
            self._printer_request = QNetworkRequest(url)
            self._printer_request.setRawHeader(self._api_header.encode(),
                                               self._api_key.encode())
            self._printer_reply = self._manager.get(self._printer_request)
        # if command:
        #     self._sendCommand(command)

    def startPrint(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        self._auto_print = parseBool(
            global_container_stack.getMetaDataEntry("repetier_auto_print",
                                                    True))
        if self._auto_print:
            Application.getInstance().showPrintMonitor.emit(True)

        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Repetier-Server is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending data to Repetier-Server"), 0,
                False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            last_process_events = time()
            for line in self._gcode:
                single_string_file_data += line
                if time() > last_process_events + 0.05:
                    # Ensure that the GUI keeps updated at least 20 times per second.
                    QCoreApplication.processEvents()
                    last_process_events = time()

            file_name = "%s.gcode" % Application.getInstance(
            ).getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            # self._post_part = QHttpPart()
            # self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
            # self._post_part.setBody(b"true")
            # self._post_multi_part.append(self._post_part)

            # if self._auto_print:
            #     self._post_part = QHttpPart()
            #     self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
            #     self._post_part.setBody(b"true")
            #     self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                      "form-data; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            # destination = "local"
            # if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)):
            #     destination = "sdcard"
            # TODO ??

            url = QUrl(self._model_url + "&name=" + file_name)

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(),
                                            self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request,
                                                  self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Unable to send data to Repetier-Server."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log(
                "e",
                "An exception occurred in network connection: %s" % str(e))

    def _sendCommand(self, command):
        url = QUrl(self._api_url + "job")
        self._command_request = QNetworkRequest(url)
        self._command_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._command_request.setHeader(QNetworkRequest.ContentTypeHeader,
                                        "application/json")

        data = "{\"command\": \"%s\"}" % command
        self._command_reply = self._manager.post(self._command_request,
                                                 data.encode())
        Logger.log("d", "Sent command to Repetier-Server instance: %s", data)

    def _setTargetBedTemperature(self, temperature):
        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    def _setTargetHotendTemperature(self, index, temperature):
        Logger.log("d", "Setting hotend %s temperature to %s", index,
                   temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    def _setHeadPosition(self, x, y, z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Y%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
        self._sendCommand("G90")

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error(
        ) == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log(
                    "d",
                    "We got a response from the instance after %s of silence",
                    time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "stateList" in reply.url().toString(
            ):  # Status update from /printer.
                if http_status_code == 200:
                    if not self.acceptsCommands:
                        self.setAcceptsCommands(True)
                        self.setConnectionText(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Connected to Repetier-Server on {0}").format(
                                    self._key))

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    if not self._num_extruders_set:
                        self._num_extruders = 0
                        ## TODO
                        # while "extruder" % self._num_extruders in json_data[self._slug]:
                        #     self._num_extruders = self._num_extruders + 1

                        # Reinitialise from PrinterOutputDevice to match the new _num_extruders
                        # self._hotend_temperatures = [0] * self._num_extruders
                        # self._target_hotend_temperatures = [0] * self._num_extruders
                        # self._num_extruders_set = True
                        # TODO

                    # Check for hotend temperatures
                    # for index in range(0, self._num_extruders):
                    #     temperature = json_data[self._slug]["extruder"]["tempRead"]
                    #     self._setHotendTemperature(index, temperature)
                    temperature = json_data[
                        self._slug]["extruder"][0]["tempRead"]
                    self._setHotendTemperature(0, temperature)

                    bed_temperature = json_data[
                        self._slug]["heatedBed"]["tempRead"]
                    #bed_temperature_set = json_data[self._slug]["heatedBed"]["tempSet"]
                    self._setBedTemperature(bed_temperature)

                elif http_status_code == 401:
                    self.setAcceptsCommands(False)
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "Repetier-Server on {0} does not allow access to print"
                        ).format(self._key))
                else:
                    pass  # TODO: Handle errors

            elif "listPrinter" in reply.url().toString(
            ):  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    for printer in json_data:
                        if printer["slug"] == self._slug:

                            job_name = printer["job"]
                            self.setJobName(job_name)

                            job_state = "offline"
                            # if printer["state"]["flags"]["error"]:
                            #     job_state = "error"
                            if printer["paused"] == "true":
                                job_state = "paused"
                            elif job_name != "none":
                                job_state = "printing"
                                self.setProgress(printer["done"])
                            elif job_name == "none":
                                job_state = "ready"
                            self._updateJobState(job_state)

                    # progress = json_data["progress"]["completion"]
                    # if progress:
                    #     self.setProgress(progress)

                    # if json_data["progress"]["printTime"]:
                    #     self.setTimeElapsed(json_data["progress"]["printTime"])
                    #     if json_data["progress"]["printTimeLeft"]:
                    #         self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                    #     elif json_data["job"]["estimatedPrintTime"]:
                    #         self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
                    #     elif progress > 0:
                    #         self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                    #     else:
                    #         self.setTimeTotal(0)
                    # else:
                    #     self.setTimeElapsed(0)
                    #     self.setTimeTotal(0)
                    # self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

            elif "snapshot" in reply.url().toString():  # Update from camera:
                if http_status_code == 200:
                    self._camera_image.loadFromData(reply.readAll())
                    self.newImage.emit()
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "model" in reply.url().toString(
            ):  # Result from /files command:
                if http_status_code == 201:
                    Logger.log(
                        "d",
                        "Resource created on Repetier-Server instance: %s",
                        reply.header(
                            QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                modelList = json_data["data"]

                lastModel = modelList[len(modelList) - 1]
                # Logger.log("d", "XXX1:len"+str(len(modelList)))
                # Logger.log("d", "XXX1:lastModel"+str(lastModel))
                modelId = lastModel["id"]

                # "http://%s:%d%s" % (self._address, self._port, self._path)
                urlString = self._api_url + '?a=copyModel&data={"id": %s}' % (
                    modelId)
                Logger.log("d", "XXX1: modelId: " + str(modelId))
                url = QUrl(urlString)
                self._printer_request = QNetworkRequest(url)
                self._printer_request.setRawHeader(self._api_header.encode(),
                                                   self._api_key.encode())
                self._printer_reply = self._manager.get(self._printer_request)
                Logger.log("d", "XXX1: modelId: " + str(urlString))

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()
                global_container_stack = Application.getInstance(
                ).getGlobalContainerStack()
                if not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(
                            reply.header(QNetworkRequest.LocationHeader).
                            toString()).fileName()
                        message = Message(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Saved to Repetier-Server as {0}").format(
                                    file_name))
                    else:
                        message = Message(
                            i18n_catalog.i18nc("@info:status",
                                               "Saved to Repetier-Server"))
                    message.addAction(
                        "open_browser",
                        i18n_catalog.i18nc("@action:button",
                                           "Open Repetier-Server..."), "globe",
                        i18n_catalog.i18nc(
                            "@info:tooltip",
                            "Open the Repetier-Server web interface"))
                    message.actionTriggered.connect(
                        self._onMessageActionTriggered)
                    message.show()

            elif "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Repetier-Server command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log(
                "d",
                "RepetierServerOutputDevice got an unhandled operation %s",
                reply.operation())

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                if progress > self._progress_message.getProgress():
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(
                    i18n_catalog.i18nc("@info:status",
                                       "Storing data on Repetier-Server"), 0,
                    False, -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._base_url))
예제 #59
0
class Downloader(QProgressDialog):

    """Downloader Dialog with complete informations and progress bar."""

    def __init__(self, parent=None):
        """Init class."""
        super(Downloader, self).__init__(parent)
        self.setWindowTitle(__doc__)
        if not os.path.isfile(__file__) or not __source__:
            return
        if not os.access(__file__, os.W_OK):
            error_msg = ("Destination file permission denied (not Writable)! "
                         "Try again to Update but as root or administrator.")
            log.critical(error_msg)
            QMessageBox.warning(self, __doc__.title(), error_msg)
            return
        self._time, self._date = time.time(), datetime.now().isoformat()[:-7]
        self._url, self._dst = __source__, __file__
        log.debug("Downloading from {} to {}.".format(self._url, self._dst))
        if not self._url.lower().startswith("https:"):
            log.warning("Unsecure Download over plain text without SSL.")
        self.template = """<h3>Downloading</h3><hr><table>
        <tr><td><b>From:</b></td>      <td>{}</td>
        <tr><td><b>To:  </b></td>      <td>{}</td> <tr>
        <tr><td><b>Started:</b></td>   <td>{}</td>
        <tr><td><b>Actual:</b></td>    <td>{}</td> <tr>
        <tr><td><b>Elapsed:</b></td>   <td>{}</td>
        <tr><td><b>Remaining:</b></td> <td>{}</td> <tr>
        <tr><td><b>Received:</b></td>  <td>{} MegaBytes</td>
        <tr><td><b>Total:</b></td>     <td>{} MegaBytes</td> <tr>
        <tr><td><b>Speed:</b></td>     <td>{}</td>
        <tr><td><b>Percent:</b></td>     <td>{}%</td></table><hr>"""
        self.manager = QNetworkAccessManager(self)
        self.manager.finished.connect(self.save_downloaded_data)
        self.manager.sslErrors.connect(self.download_failed)
        self.progreso = self.manager.get(QNetworkRequest(QUrl(self._url)))
        self.progreso.downloadProgress.connect(self.update_download_progress)
        self.show()
        self.exec_()

    def save_downloaded_data(self, data):
        """Save all downloaded data to the disk and quit."""
        log.debug("Download done. Update Done.")
        with open(os.path.join(self._dst), "wb") as output_file:
            output_file.write(data.readAll())
        data.close()
        QMessageBox.information(self, __doc__.title(),
                                "<b>You got the latest version of this App!")
        del self.manager, data
        return self.close()

    def download_failed(self, download_error):
        """Handle a download error, probable SSL errors."""
        log.error(download_error)
        QMessageBox.warning(self, __doc__.title(), str(download_error))

    def seconds_time_to_human_string(self, time_on_seconds=0):
        """Calculate time, with precision from seconds to days."""
        minutes, seconds = divmod(int(time_on_seconds), 60)
        hours, minutes = divmod(minutes, 60)
        days, hours = divmod(hours, 24)
        human_time_string = ""
        if days:
            human_time_string += "%02d Days " % days
        if hours:
            human_time_string += "%02d Hours " % hours
        if minutes:
            human_time_string += "%02d Minutes " % minutes
        human_time_string += "%02d Seconds" % seconds
        return human_time_string

    def update_download_progress(self, bytesReceived, bytesTotal):
        """Calculate statistics and update the UI with them."""
        downloaded_MB = round(((bytesReceived / 1024) / 1024), 2)
        total_data_MB = round(((bytesTotal / 1024) / 1024), 2)
        downloaded_KB, total_data_KB = bytesReceived / 1024, bytesTotal / 1024
        # Calculate download speed values, with precision from Kb/s to Gb/s
        elapsed = time.clock()
        if elapsed > 0:
            speed = round((downloaded_KB / elapsed), 2)
            if speed > 1024000:  # Gigabyte speeds
                download_speed = "{} GigaByte/Second".format(speed // 1024000)
            if speed > 1024:  # MegaByte speeds
                download_speed = "{} MegaBytes/Second".format(speed // 1024)
            else:  # KiloByte speeds
                download_speed = "{} KiloBytes/Second".format(int(speed))
        if speed > 0:
            missing = abs((total_data_KB - downloaded_KB) // speed)
        percentage = int(100.0 * bytesReceived // bytesTotal)
        self.setLabelText(self.template.format(
            self._url.lower()[:99], self._dst.lower()[:99],
            self._date, datetime.now().isoformat()[:-7],
            self.seconds_time_to_human_string(time.time() - self._time),
            self.seconds_time_to_human_string(missing),
            downloaded_MB, total_data_MB, download_speed, percentage))
        self.setValue(percentage)
예제 #60
0
class CloudApiClient:

    # The cloud URL to use for this remote cluster.
    ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
    CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
    CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)

    ## 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:
        super().__init__()
        self._manager = QNetworkAccessManager()
        self._account = account
        self._on_error = on_error
        self._upload = None  # type: Optional[ToolPathUploader]
        # In order to avoid garbage collection we keep the callbacks in this list.
        self._anti_gc_callbacks = []  # type: List[Callable[[], None]]

    ## Gets the account used for the API.
    @property
    def account(self) -> Account:
        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]) -> None:
        url = "{}/clusters".format(self.CLUSTER_API_ROOT)
        reply = self._manager.get(self._createEmptyRequest(url))
        self._addCallback(reply, on_finished, CloudClusterResponse)

    ## 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)
        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:
        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]):
        self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
        self._upload.start()

    # Requests a cluster to print the given print job.
    #  \param cluster_id: The ID of the cluster.
    #  \param job_id: The ID of the print job.
    #  \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)
        reply = self._manager.post(self._createEmptyRequest(url), b"")
        self._addCallback(reply, on_finished, CloudPrintResponse)

    ##  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 = QNetworkRequest(QUrl(path))
        if content_type:
            request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
        access_token = self._account.accessToken
        if access_token:
            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]]:
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        try:
            response = bytes(reply.readAll()).decode()
            return status_code, json.loads(response)
        except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
            error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code),
                               id=str(time()), http_status="500")
            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:
        if "data" in response:
            data = response["data"]
            if isinstance(data, list):
                results = [model_class(**c) for c in data]  # type: List[CloudApiClientModel]
                on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
                on_finished_list(results)
            else:
                result = model_class(**data)  # type: CloudApiClientModel
                on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished)
                on_finished_item(result)
        elif "errors" in response:
            self._on_error([CloudError(**error) for error in response["errors"]])
        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],
                     ) -> None:
        def parse() -> None:
            status_code, response = self._parseReply(reply)
            self._anti_gc_callbacks.remove(parse)
            return self._parseModels(response, on_finished, model)

        self._anti_gc_callbacks.append(parse)
        reply.finished.connect(parse)