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))
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()
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)]
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)
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()
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()
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)
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())])
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()
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)
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_)
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()
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)
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)
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
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)
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()))
def get(self, url): return QNetworkAccessManager.get(self, self._getRequest(url))
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()
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()
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()
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
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"))
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)))
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 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
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)]
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)
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)
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()
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())])
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)
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()))
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)
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)
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))
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()
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()
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()
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()
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()
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)
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
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(' ', ' ') newstring = newstring.replace('&', '&') 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()
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))
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)
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))
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()
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)
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))
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)
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)