File manager - Edit - /home/ferretapmx/public_html/python-packages.tar
Back
mysqlsh/util.py 0000644 00000002401 15231164450 0007571 0 ustar 00 # Copyright (c) 2021, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA def to_dict(obj): """Recursively convert a built-in Shell object to native types""" pass mysqlsh/plugin_manager/repositories.py 0000644 00000033153 15231164450 0014343 0 ustar 00 # Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os.path import json import mysqlsh from mysqlsh.plugin_manager import plugin, plugin_function from . import general # Define default repositories DEFAULT_PLUGIN_REPOSITORIES = [{ "name": "Official MySQL Shell Plugin Repository", "description": "The official MySQL Shell Plugin Repository maintained by " "the MySQL Team at Oracle.", "url": "https://cdn.mysql.com/mysql_shell/plugins_manifest.zip", "manifestPath": "manifest/mysql-shell-plugins-manifest.json" }] def format_repository_listing(repositories): """Returns a formatted list of repositories. Args: repositories (list): A list of repositories. Returns: The formated list as string """ out = "" i = 1 for r in repositories: out += (f"{i:>4} {r.get('name', '??')}\n" f" {r.get('description', '-')}\n" f" {r.get('url', '??')}\n\n") i += 1 return out def get_user_repositories(raise_exceptions=False): """Fetches the registered user repositories Args: raise_exceptions (bool): Whether exceptions are raised Returns: The registered repositories as list """ user_repo_file_path = os.path.join(general.get_shell_user_dir(), "plugin-repositories.json") if os.path.isfile(user_repo_file_path): try: with open(user_repo_file_path, 'r') as user_repo_file: user_repos = json.load(user_repo_file).get("repositories") return user_repos except json.JSONDecodeError as e: print( f"ERROR: Could not parse JSON file '{user_repo_file_path}'.\n" f"{str(e.msg)}, line: {e.lineno}, col: {e.colno}") if raise_exceptions: raise return except OSError as e: print(f"ERROR: Error reading file '{user_repo_file_path}'.\n" f"{str(e)}") if raise_exceptions: raise else: return [] def set_user_repositories(user_repos, raise_exceptions=False): """Stores the registered user repositories Args: user_repos (list): The list of raise_exceptions (bool): Whether exceptions are raised Returns: True on success """ user_repo_file_content = { "fileType": "MySQL Shell Plugin Repositories", "version": "0.0.1", "repositories": user_repos } user_repo_file_path = os.path.join(general.get_shell_user_dir(), "plugin-repositories.json") try: with open(user_repo_file_path, 'w') as user_repo_file: json.dump(user_repo_file_content, user_repo_file, indent=4) return True except OSError as e: print(f"ERROR: Error writing to file '{user_repo_file_path}'.\n" f"{str(e)}") if raise_exceptions: raise return False @plugin_function('plugins.repositories.list') def get_plugin_repositories(**kwargs): """Lists all registered plugin repositories This function will list all registered MySQL Shell plugin repositories. To add a repository use the plugins.repositories.add() function. Args: **kwargs: Optional parameters Keyword Args: return_formatted (bool): If set to true, a list object is returned. raise_exceptions (bool): Whether exceptions are raised Returns: None or a list of dicts representing the plugins """ return_formatted = kwargs.get('return_formatted', True) raise_exceptions = kwargs.get('raise_exceptions', False) # Init the repo list with the default repositories repos = DEFAULT_PLUGIN_REPOSITORIES.copy() user_repos = get_user_repositories(raise_exceptions=raise_exceptions) if user_repos: repos.extend(user_repos) if not return_formatted: return repos else: # cSpell:ignore repositor print("Registered MySQL Shell Plugin Repositories.\n\n" f"{format_repository_listing(repositories=repos)}" f"Total of {len(repos)} repositor" f"{'y' if len(repos) == 1 else 'ies'}.\n") @plugin_function('plugins.repositories.add') def add_plugin_repository(url=None, **kwargs): """Adds a new plugin repository By calling this function a new plugin repository can be added. The url parameter allows for shortcuts. 'domain.com' expands to https://domain.com/mysql-shell-plugins-manifest.json 'domain.com/plugins' expands to https://domain.com/plugins/mysql-shell-plugins-manifest.json 'github/username' expands to https://raw.githubusercontent.com/username/mysql-shell-plugins/master/mysql-shell-plugins-manifest.json 'github/username/repo' expands to https://raw.githubusercontent.com/username/repo/master/mysql-shell-plugins-manifest.json Args: url (str): The url of a MySQL Shell plugins repository. **kwargs: Optional parameters Keyword Args: interactive (bool): Whether user input is accepted raise_exceptions (bool): Whether exceptions are raised Returns: None """ interactive = kwargs.get('interactive', True) raise_exceptions = kwargs.get('raise_exceptions', True) if not url and interactive: print("To add a new MySQL Shell plugin repository the URL of the " "repository manifest file\nneeds to be specified.\n\n" "Examples:\n" " domain.com\n" " domain.com/plugins\n" " github/username\n" " github/username/repository\n") url = mysqlsh.globals.shell.prompt( f"Please enter the URL of the plugin repository: ", { 'defaultValue': '' }).strip() if not url: print("No URL given. Cancelling Operation") return if url.lower().startswith("github/"): github_str = url.split("/") if len(github_str) == 1: print("A github user needs to be specified") return github_user = github_str[1] # Get the github repo if len(github_str) == 2: github_repo = "mysql-shell-plugins" else: github_repo = github_str[2] url = ( f"https://raw.githubusercontent.com/{github_user}/{github_repo}/" f"master/mysql-shell-plugins-manifest.json") else: # Add https:// if not specified if not url.startswith("https://") and not url.startswith("http://"): url = f"https://{url}" if not url.endswith(".json"): if url.endswith("/"): url = f"{url}mysql-shell-plugins-manifest.json" else: url = f"{url}/mysql-shell-plugins-manifest.json" try: manifest = general.download_file(url) if not manifest: return manifest_content = json.loads(manifest) repository = manifest_content.get("repository") name = repository.get('name') if not name: print("The given repository misses the 'name' property.") return description = repository.get('description') if not name: print("The given repository misses the 'description' property.") return plugins = manifest_content.get('plugins') if not plugins or len(plugins) == 0: print("The repository does not contain any plugins. " "Operation cancelled.") return # Warn the user about possible consequences print("\nWARNING:\n" "You are about to add an external MySQL Shell plugin repository." "\nExternal plugin repositories and their plugins complement\n" "the functionality of MySQL Shell and can contain system\n" "level software that could be potentially harmful to your\n" "system. Please review the description below and only proceed\n" "if you have obtained the external plugin repository URL from\n" "a trusted source.\n\n" "Oracle and its affiliates cannot be held responsible for\n" "any potential harm caused by using plugins from external " "sources.\n") # Ensure the user will explicitly confirm the full URL print(f"{'Repository :'} {name}\n" f"{'Description:'} {description}\n" f"URL: {url}\n\n" "The repository contains the following plugins:\n") for plugin in plugins: print(f" - {plugin.get('caption')}\n") prompt = mysqlsh.globals.shell.prompt( f"Are you sure you want to add the repository '{name}'" " [yes/NO]: ", { 'defaultValue': 'no' }).strip().lower() if prompt != 'yes': print("Operation cancelled.") return print("Fetching current user repositories...") user_repos = get_user_repositories(raise_exceptions=raise_exceptions) if user_repos is None: return # Check if the given repository is already in the list for r in user_repos: if r.get("url").lower() == url.lower(): print(f"The repository '{name}' has already been added.\n") return print(f"Adding repository '{name}'...") user_repos.append({ "name": name, "description": description, "url": url }) if set_user_repositories(user_repos): print(f"Repository '{name}' successfully added.\n") except json.JSONDecodeError as e: print(f"ERROR: Could not parse JSON file. {str(e.msg)}, " f"line: {e.lineno}, col: {e.colno}") if raise_exceptions: raise return def search_user_repo(url, user_repos): # Search by name nr = 0 found = False for r in user_repos: if r.get("url") == url: found = True break nr += 1 if found: return nr else: return None @plugin_function('plugins.repositories.remove') def remove_plugin_repository(**kwargs): """Removes a registered plugin repository This function will remove a shell plugin repository previously registered. Args: **kwargs: Optional parameters Keyword Args: url (str): The url of a MySQL Shell plugins repository. interactive (bool): Whether user input is accepted raise_exceptions (bool): Whether exceptions are raised Returns: None """ url = kwargs.get('url') interactive = kwargs.get('interactive', True) raise_exceptions = kwargs.get('raise_exceptions', False) user_repos = get_user_repositories(raise_exceptions=raise_exceptions) if not user_repos: print("No custom MySQL Shell plugin repositories registered.\n") return repo_index = None if url: repo_index = search_user_repo(url, user_repos) if repo_index is None: print(f"No user repository matches '{url}'") if repo_index is None and interactive: print("Removing a plugin repository.\n\n" f"{format_repository_listing(user_repos)}") # Let the user choose from the list while repo_index is None: prompt = mysqlsh.globals.shell.prompt( "Please enter the index or URL of a repository: ").strip() if prompt: try: try: # If the user provided an index, try to map that nr = int(prompt) if nr > 0 and nr <= len(user_repos): repo_index = nr - 1 else: raise IndexError except ValueError: repo_index = search_user_repo(prompt, user_repos) if repo_index is None: raise ValueError except (ValueError, IndexError): print(f"The repository '{prompt}' was not found. Please try " "again or leave empty to cancel the operation.\n") else: break if repo_index is None: print("No valid URL provided. Cancelling operation.") else: url = user_repos[repo_index].get("url", '') print(f"\nRemoving repository '{url}'...") del user_repos[repo_index] if set_user_repositories(user_repos): print(f"Repository successfully removed.\n") mysqlsh/plugin_manager/plugins.py 0000644 00000073020 15231164450 0013272 0 ustar 00 # Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import json import os import re import shutil import zipfile import mysqlsh from enum import Enum from . import general from . import repositories from mysqlsh.plugin_manager import validate_shell_version class Filter(Enum): NONE = "all plugins" ONLY_INSTALLED = "installed plugins" NOT_INSTALLED = "available plugins for installation" UPDATABLE = "updatable plugins" def parse_version(version): ret_val = tuple(int(x) if x.isnumeric() else x for x in version.split(".")) return ret_val def get_repositories_with_plugins(plugin_filter=Filter.NONE, printouts=True, raise_exceptions=False): """Downloads all plugin information from the available repositories Args: printouts (bool): Whether to print out information raise_exceptions (bool): Whether exceptions should be raised Returns: A list of repositories including their plugins """ if printouts: print(f"Fetching list of {plugin_filter.value}...\n") # Get the list of all repositories, cSpell:ignore repos repos = repositories.get_plugin_repositories(return_formatted=False) try: i = 1 for r in repos: # If the manifest is stored in a zip, extact that zip and get the # manifest file if r.get("url").endswith(".zip"): # Download the zip file zip_file_path = general.get_shell_temp_dir("manifest.zip") # If there is an earlier manifest.zip, remove it if os.path.isfile(zip_file_path): os.remove(zip_file_path) if not general.download_file(r.get("url"), zip_file_path): continue # Extract the zip file manifest_dir = general.get_shell_user_dir( "temp", "manifest_zip") if os.path.isdir(manifest_dir): shutil.rmtree(manifest_dir) try: with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: zip_ref.extractall(manifest_dir) except zipfile.BadZipFile as e: print(f"ERROR: Could not extract zip file {zip_file_path}." f"\n{str(e)}") if raise_exceptions: raise # Remove zip file os.remove(zip_file_path) zip_manifest_path = r.get("manifestPath") if not zip_manifest_path: zip_manifest_path = "mysql-shell-plugins-manifest.json" manifest_path = manifest_dir for path_item in zip_manifest_path.split('/'): manifest_path = os.path.join(manifest_path, path_item) if not os.path.isfile(manifest_path): shutil.rmtree(manifest_dir) print(f"ERROR: Cannot open file '{manifest_path}'.") return with open(manifest_path, "r") as manifest_file: manifest = manifest_file.read() shutil.rmtree(manifest_dir) else: manifest = general.download_file(r.get("url")) # Load the current plugin manifest of the repo if manifest: plugins = json.loads(manifest).get("plugins") # Add id, installed, installedVersion, # installedDevelopmentStage and updateAvailable fields to all # plugins for p in plugins: p["id"] = i i += 1 # Set installed, installedVersion and updateAvailable name = p.get("name") installed = name in dir(mysqlsh.globals) p["installed"] = installed if installed: latest_version = p.get("latestVersion") plugin = getattr(mysqlsh.globals, name) if plugin and "version" in dir(plugin): installed_version = plugin.version() p["installedVersion"] = installed_version if parse_version(latest_version) > \ parse_version(installed_version): p["updateAvailable"] = True version_data = get_plugin_version_data( p, installed_version) if version_data: installed_dev_stage = version_data.get( "developmentStage") if installed_dev_stage: p["installedDevelopmentStage"] = \ installed_dev_stage.upper() # Filter out installed plugins if needed if plugin_filter == Filter.ONLY_INSTALLED: plugins = [p for p in plugins if p.get("installed")] elif plugin_filter == Filter.NOT_INSTALLED: plugins = [p for p in plugins if not p.get("installed")] elif plugin_filter == Filter.UPDATABLE: plugins = [ p for p in plugins if p.get("updateAvailable", False) ] # Add list of plugins to the repo r["plugins"] = plugins return repos except json.JSONDecodeError as e: print(f"ERROR: Could not parse JSON file. {str(e.msg)}, " f"line: {e.lineno}, col: {e.colno}") if raise_exceptions: raise except Exception as e: print(f"ERROR: Could not get the list of repositories.\n{str(e)}") if raise_exceptions: raise def get_plugin_version_data(plugin, version): """Returns the plugin version data for a given version Args: plugin (dict): A dict representing a plugin version (str): The version to look up Returns: The version data as a dict or None if the version is not found """ versions = plugin.get("versions") if not versions: return for v in versions: if v.get("version") == version: return v def get_plugin_count(repositories): """Returns the total number of plugin Args: repositories (dict): A list of repositories with their plugins. Returns: The number of plugins """ i = 0 if repositories: for r in repositories: plugins = r.get("plugins") if not plugins: continue i += len(plugins) return i def get_installed_plugin_count(repositories): """Returns the total number of plugin Args: repositories (dict): A list of repositories with their plugins. Returns: The number of plugins """ i = 0 for r in repositories: plugins = r.get("plugins") if not plugins: continue for p in plugins: if p.get("installed", False): i += 1 return i def get_available_update_count(repositories): """Returns the number of available plugin updates Args: repositories (dict): A list of repositories with their plugins. print_info (bool): If set to True, information about the updates will be printed Returns: A tuple containing the number of available updates and info text """ updateable_count = 0 for r in repositories: plugins = r.get("plugins") if not plugins: continue for p in plugins: if p.get("updateAvailable", False): updateable_count += 1 if updateable_count == 1: out = (f"One update is available. " "Use plugins.update() to install the update.") elif updateable_count > 1: out = (f"{updateable_count} updates are available. " "Use plugins.update() to install the updates.") else: out = "No updates available." return updateable_count, out def format_plugin_listing(repositories, add_description=True): """Returns a formatted list of plugins. Args: repositories (list): A list of repositories with their plugins. Returns: The formated list as string """ i = 1 out = "" header = (f" # {'Name':20} {'Caption':34} {'Version':16} " f"{'Installed':16}") sep = f"---- {'-'*20} {'-'*34} {'-'*16} {'-'*16}" for r in repositories: plugins = r.get("plugins") if not plugins: continue repo_name = r.get('name', 'Repository: ?') if out == "": out += f"{header}\n{sep}\n" else: if not add_description: out += '\n' #out += f"{repo_name}\n{'='*94}\n" for p in plugins: id = p.get("id", i) name = p.get("name") name = name[:18] + '..' if len(name) > 20 else name # Caption caption = p.get("caption") caption = re.sub( r'[\n\r]', ' ', caption[:32] + '..' if len(caption) > 34 else caption) # Description desc = p.get("description") desc = re.sub(r'[\n\r]', ' ', desc[:73] + '..' if len(desc) > 75 else desc) # Latest Version latest_v = p.get("latestVersion") # Development Stage dev_stage = "" version_data = get_plugin_version_data(p, latest_v) if version_data: dev_stage = version_data.get("developmentStage") if dev_stage: dev_stage = dev_stage.upper() version_str = latest_v + " " + dev_stage # Installed Version installed = p.get("installed", False) installed_version = p.get("installedVersion", "?.?.?") installed_dev_stage = p.get("installedDevelopmentStage", "??") update_available = "^" if p.get("updateAvailable", False) else "" installed_str = installed_version + " " + installed_dev_stage + \ update_available if installed: out += f"*{id:>3} " else: out += f"{id:>4} " out += (f"{name:20} {caption:34} {version_str:16} " f"{installed_str if installed else 'No':16}\n") if add_description: out += f" {desc}\n\n" i += 1 return out def get_plugin(repositories, name=None, interactive=True): """Returns a plugin dict Args: repositories (list): A list of repositories with their plugins. name (str): The name of the plugin interactive (bool): Whether user input is accepted Returns: A dict representing the plugin """ selected_item = None plugin_count = get_plugin_count(repositories) # If there are no plugins, return None if plugin_count == 0: print("No plugins available.") return # If no name is given but there is only one plugin, return that one # if name is None and plugin_count == 1: # for r in repositories: # plugins = r.get("plugins") # if not plugins: # continue # if len(plugins) > 0: # selected_item = plugins[0] if name is None and interactive: out = format_plugin_listing(repositories, add_description=False) print(out) # Let the user choose from the list while selected_item is None: prompt = mysqlsh.globals.shell.prompt( "Please enter the index or name of a plugin: ").strip().lower( ) if not prompt: print("Operation cancelled.") return plugin_count = get_plugin_count(repositories) try: try: # If the user provided an index, try to map that nr = int(prompt) if nr > 0 and nr <= plugin_count: for r in repositories: plugins = r.get("plugins") if not plugins: continue for p in plugins: if nr == p.get("id"): selected_item = p break if selected_item: break else: raise IndexError except ValueError: # Search by name for r in repositories: plugins = r.get("plugins") if not plugins: continue for p in plugins: if prompt == p.get("name"): selected_item = p break if selected_item: break if selected_item is None: raise ValueError except (ValueError, IndexError): print(f"The plugin '{prompt}' was not found. Please try again " "or leave empty to cancel the operation.\n") return # Add empty line print("") else: for r in repositories: plugins = r.get("plugins") if not plugins: continue for p in plugins: if name == p.get("name"): selected_item = p break if selected_item: break if not selected_item: print(f"The plugin '{name}' was not found.") return selected_item def list_plugins(**kwargs): """Lists plugins all available MySQL Shell plugins. This function will list all all available plugins in the registered plugin repositories. To add a new plugin repository use the plugins.repositories.add() function. Args: **kwargs: Optional parameters Keyword Args: return_formatted (bool): If set to true, a list object is returned. interactive (bool): Whether user input is accepted Returns: None or a list of dicts representing the plugins """ return_formatted = kwargs.get('return_formatted', True) interactive = kwargs.get('interactive', True) # Get available repositories including the plugin lists repositories = get_repositories_with_plugins() if not repositories: return if not return_formatted: return repositories else: # Get number of available updates updates_available, update_info = get_available_update_count( repositories) out = format_plugin_listing(repositories) plugin_count = get_plugin_count(repositories=repositories) inst_count = get_installed_plugin_count(repositories=repositories) if inst_count > 0: out += (f"* {inst_count} plugin{'s' if inst_count > 1 else ''} " "installed, ") if plugin_count > 0: out += (f"{plugin_count} plugin" f"{'s' if plugin_count > 1 else ''}" " total.") # If there are available updates, add that information if updates_available > 0: out += f"\n^ {update_info}" if interactive: out += ( "\n\nUse plugins.details() to get more information about a " "specific plugin.\n") else: out += "\n" return out def install_plugin(name=None, **kwargs): """Installs a MySQL Shell plugin. This function download and install a plugin Args: name (str): The name of the plugin. **kwargs: Optional parameters Keyword Args: version (str): If specified, that specific version of the plugin will be installed force_install (bool): It set to true will first remove the plugin if it already exists return_object (bool): Whether to return the object interactive (bool): Whether user input is accepted printouts (bool): Whether information should be printed raise_exceptions (bool): Whether exceptions are raised Returns: None or plugin information """ version = kwargs.get("version") force_install = kwargs.get("force_install") return_object = kwargs.get("return_object", False) interactive = kwargs.get("interactive", True) printouts = kwargs.get("printouts", True) raise_exceptions = kwargs.get("raise_exceptions", False) try: # Get the list of repositories and their plugins repositories = get_repositories_with_plugins( plugin_filter=Filter.NONE if name or version or force_install else Filter.NOT_INSTALLED, printouts=printouts) if not repositories: return # Get the plugin by name or interactive user selection plugin = get_plugin(repositories, name, interactive) if plugin is None: return # Get plugin name and caption name = plugin.get('name') caption = plugin.get('caption') module_name = plugin.get('moduleName') installed = name in dir(mysqlsh.globals) if installed and not force_install: print(f"The plugin '{name}' is already installed. Use the " "force_install parameter to re-install it anyway.\n") return # Get version data version = version if version else plugin.get('latestVersion') version_data = get_plugin_version_data(plugin=plugin, version=version) if not version_data: print( f"ERROR: Version {version} not found for plugin {caption}.\n") return shell_version_min = version_data.get('shellVersionMin',None) shell_version_max = version_data.get('shellVersionMax',None) try: validate_shell_version(min=shell_version_min, max=shell_version_max) except Exception as e: print(str(e)) return if printouts: print(f"Installing {caption} ...") # Get the right download URL for the plugin urls = version_data.get('urls') if not urls: print(f"ERROR: No URLs available for plugin {caption}.\n") return # TODO: Check if the shell binary is a community or commercial build url = urls.get('community') zip_file_name = url.split('/')[-1] zip_file_path = general.get_shell_temp_dir(zip_file_name) # If the zip file already exists, delete it if os.path.isfile(zip_file_path): os.remove(zip_file_path) # Download the file if not general.download_file(url=url, file_path=zip_file_path): return # Set plugin directory using the module_name of the plugin plugin_dir = general.get_plugins_dir(module_name) # If the plugin folder already exists, delete it if force_install and os.path.isdir(plugin_dir): shutil.rmtree(plugin_dir) # Extract the zip file try: with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: zip_ref.extractall(plugin_dir) except zipfile.BadZipFile as e: print( f"ERROR: Could not extract zip file {zip_file_path}.\n{str(e)}" ) if raise_exceptions: raise # Remove zip file os.remove(zip_file_path) if printouts: print(f"{caption} has been installed successfully.\n\n" f"Please restart the shell to load the plugin. To get help " f"type '\\? {name}' after restart.\n") if return_object: return plugin except Exception as e: print(f"ERROR: Could not install the plugin.\n{str(e)}") if raise_exceptions: raise def uninstall_plugin(name=None, **kwargs): """Uninstalls a MySQL Shell plugin. This function uninstall a plugin Args: name (str): The name of the plugin. **kwargs: Optional parameters Keyword Args: interactive (bool): Whether user input is accepted printouts (bool): Whether information should be printed raise_exceptions (bool): Whether exceptions are raised Returns: None or plugin information """ interactive = kwargs.get("interactive", True) printouts = kwargs.get("printouts", True) raise_exceptions = kwargs.get("raise_exceptions", False) try: # Get the list of repositories and their plugins repositories = get_repositories_with_plugins( plugin_filter=Filter.ONLY_INSTALLED, printouts=printouts) if not repositories: return # Get the plugin by name or interactive user selection plugin = get_plugin(repositories, name, interactive) if plugin is None: print("Cancelling operation.") return # Get plugin name and caption name = plugin.get('name') caption = plugin.get('caption') module_name = plugin.get('moduleName') if interactive: prompt = mysqlsh.globals.shell.prompt( f"\nAre you sure you want to uninstall the plugin '{name}'" " [YES/no]: ", { 'defaultValue': 'yes' }).strip().lower() if prompt != 'yes': print("Operation cancelled.") return if printouts: print(f"Uninstalling {caption} ...") # Get plugins_dir plugins_dir = general.get_plugins_dir() # Set plugin directory using the name of the plugin plugin_dir = os.path.join(plugins_dir, module_name) # Delete plugin directory and its contents shutil.rmtree(plugin_dir) if printouts: print(f"{caption} has been uninstalled successfully.\n\n" f"Please restart the shell to unload the plugin.\n") except Exception as e: print(f"ERROR: Could not uninstall the plugin.\n{str(e)}") if raise_exceptions: raise def update_plugin(name=None, **kwargs): """Updates MySQL Shell plugins. This function updates on or all plugins Args: name (str): The name of the plugin. **kwargs: Optional parameters Keyword Args: interactive (bool): Whether user input is accepted raise_exceptions (bool): Whether exceptions are raised Returns: None or plugin information """ interactive = kwargs.get("interactive", True) raise_exceptions = kwargs.get("raise_exceptions", False) try: # Get available repositories including the plugin lists repositories = get_repositories_with_plugins( plugin_filter=Filter.UPDATABLE) if not repositories: return # Build list of plugins and ask the user to confirm when interactive is # set plugins = [] plugin_count = get_plugin_count(repositories=repositories) if plugin_count == 0: print("No updates available.\n") return elif plugin_count == 1: plugin = None if name: plugin = get_plugin(repositories=repositories, name=name, interactive=False) if not plugin: print(f"No update available for plugin '{name}'.") return elif plugin_count == 1: plugin = get_plugin(repositories=repositories) if interactive: caption = plugin.get("caption") prompt = mysqlsh.globals.shell.prompt( f"Are you sure you want to update the '{caption}' " "[YES/no]: ", { 'defaultValue': 'yes' }).strip().lower() if prompt != 'yes': print("Operation cancelled.") return plugins.append(plugin) else: for r in repositories: plugins = r.get("plugins") if not plugins: continue plugins.append(plugins) print("There are updates pending for the following plugins:") for plugin in plugins: print(f" - {plugin.get('caption')}") prompt = mysqlsh.globals.shell.prompt( f"\nAre you sure you want to update these plugins [YES/no]: ", { 'defaultValue': 'yes' }).strip().lower() if prompt != 'yes': print("Operation cancelled.") return # Perform the updates for plugin in plugins: name = plugin.get("name") caption = plugin.get("caption") try: print(f"Updating {caption} ...") install_plugin(name=name, force_install=True, interactive=False, printouts=False, raise_exceptions=True) print(f"\nThe update{'s have' if len(plugins) > 1 else ' has'}" " been completed successfully.\n\n" "Please restart the shell to reload the plugin.\n") except Exception as e: print(f"ERROR: Could not update plugin '{name}'.\n{str(e)}") if raise_exceptions: raise except Exception as e: print(f"ERROR: Could not successfully update all plugins.\n{str(e)}") if raise_exceptions: raise def plugin_details(name=None, **kwargs): """Gives detailed information about a MySQL Shell plugin. Args: name (str): The name of the plugin. **kwargs: Optional parameters Keyword Args: interactive (bool): Whether user input is accepted Returns: None or plugin information """ interactive = kwargs.get("interactive", True) # Get available repositories including the plugin lists repositories = get_repositories_with_plugins() # Get a plugin to print details about plugin = get_plugin(repositories=repositories, name=name, interactive=interactive) if not plugin: return caption = plugin.get('caption', '') description = plugin.get('description', '').replace("\n", f"\n{' ' * 13}") latest_v = plugin.get('latestVersion', '') v_data = get_plugin_version_data(plugin=plugin, version=latest_v) latest_v_dev_stage = v_data.get('developmentStage', '').upper() shell_ver_min = v_data.get('shellVersionMin','') shell_ver_max = v_data.get('shellVersionMax','') versions = plugin.get("versions", []) print(f"{caption}\n{'-' * len(caption)}\n" f"{'Plugin Name:':>20} {plugin.get('name', '')}\n" f"{'Latest Version:':>20} {latest_v}\n" f"{'Dev Stage:':>20} {latest_v_dev_stage}\n" f"{'Min. Shell Version:':>20} {shell_ver_min}\n" f"{'Max. Shell Version:':>20} {shell_ver_max}\n" f"{'Description:':>20} {description}\n" f"{'Available Versions:':>20} {len(versions)}") i = 0 for version in versions: v_str = (f"{version.get('version', '?.?.?')} " f"{version.get('developmentStage', '?').upper()}") v_str = f"{v_str:>18}" out = v_str change_list = version.get("changes") if change_list and len(change_list) > 0: changes = "" for change in change_list: if changes == "": changes += " - " else: changes += f"{' ' * len(v_str)} - " changes += change + "\n" out += changes print(out) i += 1 if interactive and i == 5 and len(versions) > 5: rem = len(versions) - i prompt = mysqlsh.globals.shell.prompt( f"Do you want to print the remaining list of " f"{rem} version{'s' if rem > 1 else ''}? [yes/NO]: ", { 'defaultValue': 'no' }).strip().lower() if prompt != 'yes': break mysqlsh/plugin_manager/registrar.py 0000644 00000074073 15231164450 0013624 0 ustar 00 # Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """Plugin Manager used for simplified plugin registration""" import inspect import re from functools import wraps, partial # Callbacks for additional handling on registered plugin # functions should be added here, they should be in the form # def callback(definition) # # Where definition is an instance of FunctionData _registration_callbacks = [] def add_registration_callback(callback): _registration_callbacks.append(callback) def validate_shell_version(min=None, max=None): """ Validates the plugin Shell version requirements for plugin. """ import mysqlsh raw_version = mysqlsh.globals.shell.version shell_version = tuple([int(v) for v in raw_version.split()[1].split('-')[0].split('.')]) # Ensures the plugin can be installed on the current version of the Shell min_version_ok = True if not min is None: min_version = tuple([int(v) for v in min.split('.')]) min_version_ok = shell_version >= min_version max_version_ok = True if not max is None: max_version = tuple([int(v) for v in max.split('.')]) max_version_ok = shell_version <= max_version error = "" if not min_version_ok or not max_version_ok: if min is not None and max is not None: error = f"This plugin requires Shell between versions {min} and {max}." elif min is not None: error = f"This plugin requires at least Shell version {min}." elif max is not None: error = f"This plugin does not work on Shell versions newer than {max}." if len(error) != 0: raise Exception(error) class PluginRegistrar: """Helper class to register a shell plugin. It should be used by calling: register_object which requires: - The object path as it should be seen in the shell. - The python function that will be added as members of the object. - The documentation for the object (brief and details). Examples for object name: - 'cloud': Would register the 'cloud' as a shell global object. - 'cloud.os': Would register the 'os' object as a child of the 'cloud' global object. The name can have any number of parents in the chain, the conditions is that such parent should be already registered. For this reason, the caller may have to do something like: register_object('myGlobal', [], {'brief':'...', 'details':[]}) register_object('myGlobal.myChild', [], {'brief':'...', 'details':[]}) Before calling: register_object('myGlobal.myChild.myGrandChild', [function1, function2], {'brief':'...', 'details':[]}) Any object that is already defined will NOT be re-defined. """ @staticmethod def sphinx_2shell_type(type): """Helper function to translate the sphinx types into the types required by the shell""" if type == "str": return "string" elif type == "int": return "integer" elif type == "dict": return "dictionary" elif type == "list": return "array" else: return type class ItemDoc: """ Simple container for brief and details of the different items""" def __init__(self): self.brief = "" self.details = None class OptionData: """Holds the documentation for a specific option. This object is completely created from parsed docs for an option. TODO: Does not support 'details' for option as sphinx doesn't seem to have a way to specify that, so the details MUST be placed at the function details section (which is OK considering that's what the shell will do when rendering the help data) """ def __init__(self, option_def): self.docs = PluginRegistrar.ItemDoc() self.name = option_def["name"] self.type = option_def.get("type") self.docs.brief = option_def["brief"] self.options = [] self.required = option_def.get("required", False) if "details" in option_def: self.docs.details = option_def["details"] if "options" in option_def: for option in option_def["options"]: self.options.append(PluginRegistrar.OptionData(option)) self.default = None def format_info(self): """Translates the parameter definition to shell required format""" info = { "name": self.name, "brief": self.docs.brief, } if self.type: info["type"] = PluginRegistrar.sphinx_2shell_type(self.type) if self.docs.details is not None: info["details"] = self.docs.details if self.required: info["required"] = True if self.default is not None: info["default"] = self.default if "type" in info.keys() and info["type"] == "dictionary": options = [] for o in self.options: options.append(o.format_info()) info["options"] = options return info class ParameterData: """Holds the documentation for a specific argument. This object is created part from document parsing and part from inspection of the function definition. The function definition provides parameter name, whether it is optional or not and the default value. The documentation provides type, brief and options (when applicable) TODO: Does not support 'details' for option as sphinx doesn't seem to have a way to specify that, so the details MUST be placed at the function details section (which is OK considering that's what the shell will do when rendering the help data) """ def __init__(self, parameter): self.definition = parameter self.docs = PluginRegistrar.ItemDoc() self.type = "" self.options = [] def set_info(self, info): """Fills parameter details that come from documentation""" if "type" in info: self.type = info["type"] self.docs.brief = info["brief"] # Note: this is here even it's not really supported. try: self.docs.details = info["details"] except KeyError: pass try: options = info["options"] for option in options: self.options.append(PluginRegistrar.OptionData(option)) except KeyError: pass def format_info(self): """Translates the parameter definition to shell required format""" info = { "name": self.definition.name, "brief": self.docs.brief, } if self.type: info["type"] = PluginRegistrar.sphinx_2shell_type(self.type) if self.docs.details is not None: info["details"] = self.docs.details if self.definition.default != inspect.Parameter.empty: info["default"] = self.definition.default info["required"] = False if "type" in info.keys() and info["type"] == "dictionary" and len(self.options): options = [] for o in self.options: options.append(o.format_info()) info["options"] = options if self.definition.kind == inspect.Parameter.VAR_KEYWORD: info["required"] = False return info class FunctionData: """Holds the documentation for the function. This object is created part from document parsing and part from inspection of the function definition. The function definition provides parameter information. The documentation provides brief and details. """ def __init__(self, function, fully_qualified_name=None, shell=True, cli=False, web=False): # Get the plugin name by stripping the function name name = None if not fully_qualified_name is None: name_path = fully_qualified_name.split(".") name = name_path[-1] self.function = function self.fully_qualified_name = fully_qualified_name self.name = function.__name__ if name is None else name self.docs = PluginRegistrar.ItemDoc() # These flags indicate where the function should be available self.shell = shell self.cli = cli self.web = web signature = inspect.signature(function) self.parameters = [ PluginRegistrar.ParameterData(p) for p in signature.parameters.values() ] docs = inspect.getdoc(function) if docs: self._parse_docs(docs) def format_info(self): """Translates the parameter definition to shell required format""" info = {"brief": self.docs.brief, "details": self.docs.details} if len(self.parameters): params = [] for param in self.parameters: params.append(param.format_info()) info["parameters"] = params if self.cli: info["cli"] = self.cli return info def _get_doc_section(self, name): """Ensures the indicated section exists and returns it""" try: return self._doc_sections[name] except KeyError: self._doc_sections[name] = [] self._doc_section_list.append(name) return self._doc_sections[name] def _parse_docs(self, docstring): """Main parser function. Retrieves the documentation and splits it in sections, then process the different sections such as: brief, parameters and details. There are different sections that are considered on the parsing: - 'Args:': Contains the parameter definitions. - 'Keyword Args:': Contains parameter definitions for keyword arguments. - 'Allowed options for <dictionary>:': Contains option definitions for a dictionary parameter or option. These options will be consumed during the parsing process. """ self._doc_index = 0 self._doc_lines = [line for line in docstring.split("\n")] self._doc_sections = {} self._doc_section_list = [] # Document splitting logic section_name = "global" section = self._get_doc_section(section_name) global_count = 0 for line in self._doc_lines: if line.endswith(":"): section_name = line section = self._get_doc_section(section_name) elif section_name.startswith("global") or len(line) == 0 or line[0] == " " or line.startswith("* "): section.append(line) else: global_count = global_count + 1 section_name = "global{}".format(global_count) section = self._get_doc_section(section_name) section.append(line) # Removes leading and trailing blank lines from all the sections for name, section in self._doc_sections.items(): name = name # name is not used while section and len(section[0]) == 0: section.pop(0) while section and len(section[-1]) == 0: section.pop(-1) if len(section) == 0: raise Exception(f"Invalid format: section without content: {name}") # Parses the function brief description self._parse_function_brief() # Triggers the parameter parsing and updates the parameter # definitions arg_docs = self._parse_args() if arg_docs: self._set_parameter_docs(arg_docs) # Constructs the function details self._parse_details() def _parse_function_brief(self): """Constructs the function brief from the beginning of the docs and until the first blank line is found""" brief_lines = [] lines = self._doc_sections["global"] for line in lines: if len(line): brief_lines.append(line) else: break count = len(brief_lines) if count: self.docs.brief = " ".join(brief_lines) # Removes any subsequent blank line, rest will be part of the # function details while ( count < len(self._doc_sections["global"]) and len(self._doc_sections["global"][count]) == 0 ): count = count + 1 self._doc_sections["global"] = self._doc_sections["global"][count:] def _parse_args(self): """Parses the function arguments. This function will ensure: - All the parameters are documented - No nonexisting parameters are documented """ args_section = self._doc_sections.pop("Args:", None) arg_docs = [] if len(self.parameters) and args_section is None: raise Exception("Missing arguments documentation") elif args_section is None and len(self.parameters): raise Exception("Unexpected argument documentation") elif args_section: while len(args_section): arg_docs.append(self._parse_parameter_doc(args_section)) return arg_docs def _parse_return_value(self): """Some day it will parse the return section TODO: The shell extension objects do not do any handling of the return value so it's documentation is embedded into the function details (in an ugly way). """ pass def _parse_details(self): """ Creates the details section of the function. When this function is called, the remaining sections will be glued together to create the function details. This means, it is possible to define totally unrelated sections of documentation and they will also be included on the shell docs. """ details = [] paragraph = [] for section_name in self._doc_section_list: section = self._doc_sections.pop(section_name, None) if section: # Sections other than globals need to include the header if not section_name.startswith("global"): details.append("<b>" + section_name + "</b>") # Backup the section index, in case it was not really a # section, i.e. it is the description of a list of items # the bold will be removed section_index = len(details) - 1 for line in section: stripped_line = line.strip() if len(stripped_line) == 0: details.append(" ".join(paragraph)) paragraph.clear() elif line.startswith("* "): # If this is the first thing added to the section, # then it was not a real section but the # description of a list of items. Remove the <b> if section_index == len(details) - 2: details[section_index] = section_name details.append(line.replace("* ", "@li ")) else: paragraph.append(line) if len(paragraph): details.append(" ".join(paragraph)) paragraph.clear() if details: self.docs.details = details def _set_parameter_docs(self, docs): """Fills all the parameter definitions with the information coming from document parsing""" for doc in docs: target_param = None for param in self.parameters: if doc["name"] == param.definition.name: target_param = param if target_param is None: raise Exception( "Parameter does not exist but is documented: {}." "".format(doc) ) else: target_param.set_info(doc) missing = [] for p in self.parameters: if len(p.docs.brief) == 0: missing.append(p.definition.name) if len(missing): raise Exception( "Missing documentation for the following parameters: " "{}".format(", ".join(missing)) ) def _parse_parameter_doc(self, section): """Parses the first parameter/option definition coming on the section. Once it is parsed it will be removed from the section, next call will process the next definition. """ # Parameters are documented as <name> [(<type>)]: <brief> # kwards is documented as **<name>: <brief> info = {} param_line = section.pop(0) param_doc = param_line.strip() brief = [] match = re.match( "^(\\*\\*)?([a-z|A-Z|_][a-z|A-Z|0-9|_]*)(\\s(\\(([a-z|,|\\s]+)\\)))?:\\s(.*)$", param_doc) if match: options_section = None info["name"] = match.group(2) # kwargs definition if match.group(1): info["type"] = "dictionary" options_section = self._doc_sections.pop( "Keyword Args:", None) # data type definition included elif match.group(5): param_options = match.group(5).split(",") info["type"] = PluginRegistrar.sphinx_2shell_type( param_options[0]) if len(param_options) > 1: if param_options[1] == "required": info["required"] = True if info["type"] == "dictionary": options_section = self._doc_sections.pop( "Allowed options for {}:".format(info["name"]), None, ) if options_section: info["options"] = [] while len(options_section): info["options"].append( self._parse_parameter_doc(options_section) ) brief.append(match.group(6)) ident_size = param_line.find(param_doc) while ( len(section) and len(section[0]) > ident_size and section[0][ident_size + 1] == " " ): brief.append(section.pop(0).strip()) info["brief"] = " ".join(brief) else: raise Exception("Invalid parameter documentation: {}" "".format(param_line)) return info def __init__(self): pass def _get_python_name(self, name): return "_".join([x.lower() for x in re.split("([A-Z][a-z0-9_]*)", name) if x]) def register_object(self, name, docs, members=None): try: plugin_obj = self.get_plugin_object(name, docs) if plugin_obj is None: raise ValueError(f"Plugin object {name} was not found.") if members is not None: for member in members: if inspect.isfunction(member): self.register_function(plugin_obj, member) except Exception as e: raise Exception( f"Could not register object '{name}'.\nERROR: {str(e)}" ) def get_plugin_object(self, name, docs): """Get the leaf object in the object hierarchy defined in name. If the leaf object does not exist, it will be created with the provided documentation. If any object in the middle of the chain does not exist, an error will be raised. """ import mysqlsh shell_obj = mysqlsh.globals.shell hierarchy = name.split(".") try: plugin_obj = None if len(hierarchy) > 1: # Get the name of the last plugin object in the # fully qualified name plugin_name = hierarchy[-1] # Loop over all plugins in the hierarchy and ensure they exist parent = mysqlsh.globals for h_name in hierarchy[0:-1]: py_name = h_name # Global objects have the same name in JS/PY, but child # objects are registered as properties in which case naming # convention is followed if parent != mysqlsh.globals: py_name = self._get_python_name(h_name) if py_name in dir(parent): parent = getattr(parent, py_name) else: raise Exception( f"Object {py_name} not found in hierarchy: {name}" ) # Return the plugin object if it already exists py_plugin_name = self._get_python_name(plugin_name) plugin_obj = getattr(parent, py_plugin_name, None) if not plugin_obj: if docs is None: raise ValueError( f"No docs specified for plugin object " f"{name}" ) # If it does not exist yet, create it plugin_obj = shell_obj.create_extension_object() shell_obj.add_extension_object_member( parent, plugin_name, plugin_obj, docs ) else: try: plugin_obj = getattr(mysqlsh.globals, name, None) except KeyError: plugin_obj = shell_obj.create_extension_object() shell_obj.register_global(name, plugin_obj, docs) return plugin_obj except Exception as e: raise Exception( f"Could not get plugin object '{name}'.\nERROR: {str(e)}" ) def register_function( self, plugin_obj, function, fully_qualified_name=None, shell=True, cli=False, web=False ): """Registers a new member into the provided shell extension object""" import mysqlsh shell_obj = mysqlsh.globals.shell if cli and not shell: raise Exception( "The CLI can only be enabled on registered functions.") definition = PluginRegistrar.FunctionData( function, shell=shell, fully_qualified_name=fully_qualified_name, cli=cli, web=web) try: if shell: shell_obj.add_extension_object_member( plugin_obj, definition.name, function, definition.format_info(), ) # On success registration, the function is reported to the callbacks for callback in _registration_callbacks: callback(definition) except Exception as e: raise Exception( f"Could not add function '{definition.name}' " f"using {definition.format_info()}.\nERROR: {str(e)}" ) def register_property(self, property): pass def plugin(cls=None, shell_version_min=None, shell_version_max=None, parent=None): """Decorator to register a class as a Shell extension object This decorator can be used to register a class structure as a Shell extension object. After registering the class it self it will also scan for inner classes and register them as nested extension objects. Args: cls (class): The class structure to register Returns: """ if cls is None: return partial(plugin, shell_version_min=shell_version_min, shell_version_max=shell_version_max, parent=parent) else: try: validate_shell_version(shell_version_min, shell_version_max) plugin_manager = PluginRegistrar() # Use the class name as the plugin name and the DocString as docs and # register the class as Shell plugin object_qualified_name = cls.__name__ if parent is None else parent + "." + cls.__name__ plugin_manager.register_object( object_qualified_name, PluginRegistrar.FunctionData(cls).format_info() ) def register_inner_classes(cls): """Register all inner classes as nested extension objects Args: cls (class): The class containing the subclasses to register """ # Get all inner classes inner_classes = [ inner_class for inner_class in cls.__dict__.values() if inspect.isclass(inner_class) ] # Register those as extension objects for inner_class in inner_classes: plugin_manager.register_object( inner_class.__qualname__, PluginRegistrar.FunctionData(inner_class).format_info(), ) # Recursively also register the inner classes of this class register_inner_classes(inner_class) # Create an instance of the class to also register the plugin functions # initalized in the class's constructor __init__ cls() # Register the inner classes as nested extension objects register_inner_classes(cls) except Exception as e: print( "Could not register plugin object '{0}'.\nERROR: {1}".format( cls.__name__, str(e) ) ) raise @wraps(cls) def wrapper(*args, **kwargs): result = cls(*args, **kwargs) return result return wrapper def plugin_function(fully_qualified_name, plugin_docs=None, shell=True, cli=False, web=False): """Decorator factory to register Shell plugins functions Args: fully_qualified_name (str): The fully qualified name of the function, e.g. cloud.create.mysqlDbSystem plugin_docs (dict): The documentation structure of the plugin. This is only required for the first function that will be registered cli (bool): Defines whether the function should be available in the CLI Returns: The decorator function """ def decorator(function): try: plugin_manager = PluginRegistrar() # Get the plugin name by stripping the function name name_path = fully_qualified_name.split(".") plugin_name = ".".join(name_path[0:-1]) function_name = name_path[-1] # Get the plugin object or create it if it is available yet plugin_obj = plugin_manager.get_plugin_object( plugin_name, plugin_docs ) # register the function plugin_manager.register_function( plugin_obj, function, fully_qualified_name=fully_qualified_name, shell=shell, cli=cli, web=web ) except Exception as e: print( f"Could not register function '{function_name}' as a member " f"of the {plugin_name} plugin object.\nERROR: {str(e)}" ) raise @wraps(function) def wrapper(*args, **kwargs): # TODO investigate if automated handling of certain args is # beneficial result = function(*args, **kwargs) # TODO investigate if transformation of results should be done # when functions are called from the shell prompt return result return wrapper return decorator mysqlsh/plugin_manager/__init__.py 0000644 00000002447 15231164450 0013355 0 ustar 00 # Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """The MySQL Shell Plugin Manager""" from .registrar import plugin, plugin_function, validate_shell_version from .general import VERSION mysqlsh/plugin_manager/general.py 0000644 00000014703 15231164450 0013231 0 ustar 00 # Copyright (c) 2020, 2024, Oracle and/or its affiliates. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2.0, # as published by the Free Software Foundation. # # This program is designed to work with certain software (including # but not limited to OpenSSL) that is licensed under separate terms, # as designated in a particular file or component or in included license # documentation. The authors of MySQL hereby grant you an additional # permission to link the program and your derivative works with the # separately licensed software that they have either included with # the program or referenced in the documentation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License, version 2.0, for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import certifi import platform import os import urllib.error import urllib.request import stat import ssl from pathlib import Path # Define plugin version VERSION = "0.0.1" URL_OPEN_TIMEOUT_SECONDS = 10 def get_shell_user_dir(*argv): """Returns the MySQL Shell's user directory The directory will be created if it does not exist yet Args: *argv: The list of directories to be added to the path Returns: The plugin directory path as string """ if "MYSQLSH_USER_CONFIG_HOME" in os.environ: shell_dir = os.environ["MYSQLSH_USER_CONFIG_HOME"] else: home_dir = Path.home() if not home_dir: raise Exception("No home directory set") os_name = platform.system() if os_name == "Windows": shell_dir = os.path.join( home_dir, "AppData", "Roaming", "MySQL", "mysqlsh" ) else: shell_dir = os.path.join(home_dir, ".mysqlsh") # Ensure the path exists try: Path(shell_dir).mkdir(parents=True) except FileExistsError: pass for arg in argv: shell_dir = os.path.join(shell_dir, arg) return shell_dir def get_shell_temp_dir(*argv): """Returns the MySQL Shell's temp directory The directory will be created if it does not exist yet Args: *argv: The list of directories to be added to the path Returns: The temp directory path as string """ temp_dir = get_shell_user_dir("temp") # Ensure the path exists try: Path(temp_dir).mkdir(parents=True) except FileExistsError: pass for arg in argv: temp_dir = os.path.join(temp_dir, arg) return temp_dir def get_plugins_dir(*argv): """Returns the MySQL Shell's plugin directory The directory will be created if it does not exist yet Args: *argv: The list of directories to be added to the path Returns: The plugin directory path as string """ from pathlib import Path import os.path plugins_dir = get_shell_user_dir("plugins") # Ensure the path exists try: Path(plugins_dir).mkdir(parents=True) except FileExistsError: pass for arg in argv: plugins_dir = os.path.join(plugins_dir, arg) return plugins_dir def download_file(url, file_path=None, raise_exceptions=False, ctx=None): """Downloads a file from a given URL Args: url (str): The URL to the text file to download file_path (str): The file to store the downloaded file raise_exceptions (bool): Whether exceptions are raised Returns: The contents of the file decoded to utf-8 if no file is given or True on success or None on failure """ try: os_name = platform.system() if ctx is None and os_name == "Darwin": # NOTE: On this call we require raising exceptions so if it fails in OSX # It still goes and attempt using the certifi-ca below. return download_file(url, file_path=file_path, raise_exceptions=True, ctx=ssl.create_default_context(cafile="/private/etc/ssl/cert.pem")) with urllib.request.urlopen(url, timeout=URL_OPEN_TIMEOUT_SECONDS, context=ctx) as response: data = response.read() if not file_path: return data.decode("utf-8") else: with open(file_path, "wb") as out_file: out_file.write(data) return True except urllib.error.HTTPError as e: if raise_exceptions: raise if e.code == 404: print(f"Could not download file from {url}\nERROR: {str(e)}") return else: raise except urllib.error.URLError as e: # Since the reason might be either text or another exception, we get the # string representation so we can validate the error below. reason = str(e.reason) if 'CERTIFICATE_VERIFY_FAILED' in reason and ctx is None: cafile = install_ssl_certificates() return download_file(url, file_path=file_path, raise_exceptions=raise_exceptions, ctx=ssl.create_default_context(cafile=cafile)) else: print(f"Could not download file from {url}\nERROR: {str(e)}") if raise_exceptions: raise return None def install_ssl_certificates(): STAT_0o775 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH ) # Shell CA File Path shell_cafile = os.path.join(get_shell_user_dir(), "certifi-ca.pem") print("Installing SSL certificate...") relpath_to_certifi_cafile = os.path.relpath(certifi.where()) print(" -- removing any existing file or link") try: os.remove(shell_cafile) except FileNotFoundError: pass print(" -- creating symlink to certifi certificate bundle") os.symlink(relpath_to_certifi_cafile, shell_cafile) print(" -- setting permissions") os.chmod(shell_cafile, STAT_0o775) print(" -- update complete") return shell_cafile mysqlsh/plugin_manager/__pycache__/registrar.cpython-39.pyc 0000644 00000051202 15231164450 0020100 0 ustar 00 a kb�g;x � @ s` d Z ddlZddlZddlmZmZ g Zdd� Zddd�ZG dd � d �Z dd d�Z ddd�ZdS )z6Plugin Manager used for simplified plugin registration� N)�wraps�partialc C s t �| � d S �N)�_registration_callbacks�append)�callback� r �D/usr/lib/mysqlsh/python-packages/mysqlsh/plugin_manager/registrar.py�add_registration_callback% s r c C s ddl }|jjj}tdd� |�� d �d�d �d�D ��}d}| durhtd d� | �d�D ��}||k}d}|dur�td d� |�d�D ��}||k}d} |r�|s�| dur�|dur�d| � d |� d�} n*| dur�d| � d�} n|dur�d|� d�} t| �dk�rt| ��dS )zE Validates the plugin Shell version requirements for plugin. r Nc S s g | ]}t |��qS r ��int��.0�vr r r � <listcomp>0 � z*validate_shell_version.<locals>.<listcomp>� �-�.Tc S s g | ]}t |��qS r r r r r r r 5 r c S s g | ]}t |��qS r r r r r r r : r � z,This plugin requires Shell between versions z and z,This plugin requires at least Shell version z7This plugin does not work on Shell versions newer than )�mysqlsh�globals�shell�version�tuple�split�len� Exception) �min�maxr Zraw_versionZ shell_versionZmin_version_okZmin_versionZmax_version_okZmax_version�errorr r r �validate_shell_version) s* *r! c @ s� e Zd ZdZedd� �ZG dd� d�ZG dd� d�ZG dd � d �ZG d d� d�Z dd � Z dd� Zddd�Zdd� Z ddd�Zdd� ZdS )�PluginRegistrara Helper class to register a shell plugin. It should be used by calling: register_object which requires: - The object path as it should be seen in the shell. - The python function that will be added as members of the object. - The documentation for the object (brief and details). Examples for object name: - 'cloud': Would register the 'cloud' as a shell global object. - 'cloud.os': Would register the 'os' object as a child of the 'cloud' global object. The name can have any number of parents in the chain, the conditions is that such parent should be already registered. For this reason, the caller may have to do something like: register_object('myGlobal', [], {'brief':'...', 'details':[]}) register_object('myGlobal.myChild', [], {'brief':'...', 'details':[]}) Before calling: register_object('myGlobal.myChild.myGrandChild', [function1, function2], {'brief':'...', 'details':[]}) Any object that is already defined will NOT be re-defined. c C s8 | dkrdS | dkrdS | dkr$dS | dkr0dS | S d S ) zZHelper function to translate the sphinx types into the types required by the shell�str�stringr Zinteger�dict� dictionary�listZarrayNr )�typer r r �sphinx_2shell_typed s z"PluginRegistrar.sphinx_2shell_typec @ s e Zd ZdZdd� ZdS )zPluginRegistrar.ItemDocz> Simple container for brief and details of the different itemsc C s d| _ d | _d S �Nr ��brief�details��selfr r r �__init__v s z PluginRegistrar.ItemDoc.__init__N)�__name__� __module__�__qualname__�__doc__r0 r r r r �ItemDocs s r5 c @ s e Zd ZdZdd� Zdd� ZdS )zPluginRegistrar.OptionDataa� Holds the documentation for a specific option. This object is completely created from parsed docs for an option. TODO: Does not support 'details' for option as sphinx doesn't seem to have a way to specify that, so the details MUST be placed at the function details section (which is OK considering that's what the shell will do when rendering the help data) c C s� t �� | _|d | _|�d�| _|d | j_g | _|�dd�| _d|v rT|d | j_ d|v r||d D ]}| j� t �|�� qdd | _d S )N�namer( r, �requiredFr- �options) r"