def run_system_command(command, password=None): logger = logging.getLogger( "octoprint.plugins.ws281x_led_status.commandline") caller = CommandlineCaller() try: if password: returncode, stdout, stderr = caller.call(command, input=password) else: returncode, stdout, stderr = caller.call(command) except Exception as e: logger.error("Error running command `{}`".format("".join(command))) logger.exception(e) return None, "exception" if returncode != 0: logger.error("Command for `{}` failed with return code {}".format( "".join(command), returncode)) logger.error("STDOUT: {}".format(stdout)) logger.error("STDOUT: {}".format(stderr)) error = "command" else: # Convert output to joined string instead of list stdout = "\n".join(stdout) stderr = "\n".join(stderr) if stderr and "Sorry" in stderr or "no password" in stdout: error = "password" else: error = None return stdout, error
def _try_generate_thumbnail(cls, ffmpeg, movie_path): logger = logging.getLogger(__name__) try: thumb_path = create_thumbnail_path(movie_path) commandline = settings().get( ["webcam", "ffmpegThumbnailCommandline"]) thumb_command_str = cls._create_ffmpeg_command_string( commandline=commandline, ffmpeg=ffmpeg, input=movie_path, output=thumb_path, fps=None, videocodec=None, threads=None, bitrate=None, ) c = CommandlineCaller() returncode, stdout_text, stderr_text = c.call(thumb_command_str, delimiter=b"\r", buffer_size=512) if returncode != 0: logger.warning("Failed to generate optional thumbnail %r: %s" % (returncode, stderr_text)) return True except Exception as ex: logger.warning( "Failed to generate thumbnail from {} to {} ({})".format( movie_path, thumb_path, ex)) return False
def _do_copy_log(self): # Get the name of the tarball we're going to copy the log # files to. tarball_filename = self.build_tarball_filename() self._log("tarball = <%s>" % tarball_filename) # Get the location where we're going to save the tarball. logpath = self.find_logpath() self._log("logpath = <%s>" % logpath) # Build the full path to the tarball tarball_path = os.path.join(logpath, tarball_filename) self._log("tarball_path = <%s>" % tarball_path) # Get the names of the log files logfiles = self.get_log_file_names() self._log("logfiles = <%s>" % logfiles) # Now build the command. command = "tar cvf %s %s" % (tarball_path, logfiles) self._log("command = <%s>" % command) # Construct a commandline caller to run the tar command. caller = CommandlineCaller() caller.on_log_call = self.display_call_stdout caller.on_log_stdout = self.display_call_stdout caller.on_log_stderr = self.display_stderr # Execute the command. self.display("") caller.call(command) # Finish off with a message stating that the tarball has been # created. done_message = "Logs copied to file \"%s\"" % tarball_path self._log(done_message) self.display("\n" + done_message) self.display("")
def restart_octoprint(self): command = self._octoprint_settings.get(["server", "commands", "serverRestartCommand"]) if not command: self._logger.warning("No command configured, can't restart") return caller = CommandlineCaller() try: code, stdout, stderr = caller.call(command, **{"shell": True}) # Use shell=True, as we have to trust user input except Exception as e: self._logger.error("Error calling command to restart server {}".format(command)) self._logger.exception(e) return if code != 0: self._logger.error("Non zero return code running '{}' to restart server: {}".format(command, code)) self._logger.exception("STDOUT: {}".format(stdout)) self._logger.exception("STDERR: {}".format(stderr))
def _render(self): """Rendering runnable.""" ffmpeg = settings().get(["webcam", "ffmpeg"]) commandline = settings().get(["webcam", "ffmpegCommandline"]) bitrate = settings().get(["webcam", "bitrate"]) if ffmpeg is None or bitrate is None: self._logger.warning( "Cannot create movie, path to ffmpeg or desired bitrate is unset" ) return if self._videocodec == "mpeg2video": extension = "mpg" else: extension = "mp4" input = os.path.join( self._capture_dir, self._capture_format.format( prefix=self._prefix, postfix=self._postfix if self._postfix is not None else "", ), ) output_name = self._output_format.format( prefix=self._prefix, postfix=self._postfix if self._postfix is not None else "", extension=extension, ) temporary = os.path.join(self._output_dir, ".{}".format(output_name)) output = os.path.join(self._output_dir, output_name) for i in range(4): if os.path.exists(input % i): break else: self._logger.warning("Cannot create a movie, no frames captured") self._notify_callback( "fail", output, returncode=0, stdout="", stderr="", reason="no_frames" ) return hflip = settings().getBoolean(["webcam", "flipH"]) vflip = settings().getBoolean(["webcam", "flipV"]) rotate = settings().getBoolean(["webcam", "rotate90"]) watermark = None if settings().getBoolean(["webcam", "watermark"]): watermark = os.path.join( os.path.dirname(__file__), "static", "img", "watermark.png" ) if sys.platform == "win32": # Because ffmpeg hiccups on windows' drive letters and backslashes we have to give the watermark # path a special treatment. Yeah, I couldn't believe it either... watermark = watermark.replace("\\", "/").replace(":", "\\\\:") # prepare ffmpeg command command_str = self._create_ffmpeg_command_string( commandline, ffmpeg, self._fps, bitrate, self._threads, input, temporary, self._videocodec, hflip=hflip, vflip=vflip, rotate=rotate, watermark=watermark, ) self._logger.debug("Executing command: {}".format(command_str)) with self.render_job_lock: try: self._notify_callback("start", output) self._logger.debug("Parsing ffmpeg output") c = CommandlineCaller() c.on_log_stderr = self._process_ffmpeg_output returncode, stdout_text, stderr_text = c.call( command_str, delimiter=b"\r", buffer_size=512 ) self._logger.debug("Done with parsing") if returncode == 0: shutil.move(temporary, output) self._notify_callback("success", output) else: self._logger.warning( "Could not render movie, got return code %r: %s" % (returncode, stderr_text) ) self._notify_callback( "fail", output, returncode=returncode, stdout=stdout_text, stderr=stderr_text, reason="returncode", ) except Exception: self._logger.exception("Could not render movie due to unknown error") self._notify_callback("fail", output, reason="unknown") finally: try: if os.path.exists(temporary): os.remove(temporary) except Exception: self._logger.warning( "Could not delete temporary timelapse {}".format(temporary) ) self._notify_callback("always", output)
class OctoPrintDevelCommands(click.MultiCommand): """ Custom `click.MultiCommand <http://click.pocoo.org/5/api/#click.MultiCommand>`_ implementation that provides commands relevant for (plugin) development based on availability of development dependencies. """ sep = ":" groups = ("plugin",) def __init__(self, *args, **kwargs): click.MultiCommand.__init__(self, *args, **kwargs) from octoprint.util.commandline import CommandlineCaller from functools import partial def log_util(f): def log(*lines): for line in lines: f(line) return log self.command_caller = CommandlineCaller() self.command_caller.on_log_call = log_util(lambda x: click.echo(">> {}".format(x))) self.command_caller.on_log_stdout = log_util(click.echo) self.command_caller.on_log_stderr = log_util(partial(click.echo, err=True)) def _get_prefix_methods(self, method_prefix): for name in [x for x in dir(self) if x.startswith(method_prefix)]: method = getattr(self, name) yield method def _get_commands_from_prefix_methods(self, method_prefix): for method in self._get_prefix_methods(method_prefix): result = method() if result is not None and isinstance(result, click.Command): yield result def _get_commands(self): result = dict() for group in self.groups: for command in self._get_commands_from_prefix_methods("{}_".format(group)): result[group + self.sep + command.name] = command return result def list_commands(self, ctx): result = [name for name in self._get_commands()] result.sort() return result def get_command(self, ctx, cmd_name): commands = self._get_commands() return commands.get(cmd_name, None) def plugin_new(self): try: import cookiecutter.main except ImportError: return None try: # we depend on Cookiecutter >= 1.4 from cookiecutter.prompt import StrictEnvironment except ImportError: return None import contextlib @contextlib.contextmanager def custom_cookiecutter_config(config): """ Allows overriding cookiecutter's user config with a custom dict with fallback to the original data. """ from octoprint.util import fallback_dict original_get_user_config = cookiecutter.main.get_user_config try: def f(*args, **kwargs): original_config = original_get_user_config(*args, **kwargs) return fallback_dict(config, original_config) cookiecutter.main.get_user_config = f yield finally: cookiecutter.main.get_user_config = original_get_user_config @contextlib.contextmanager def custom_cookiecutter_prompt(options): """ Custom cookiecutter prompter for the template config. If a setting is available in the provided options (read from the CLI) that will be used, otherwise the user will be prompted for a value via click. """ original_prompt_for_config = cookiecutter.main.prompt_for_config def custom_prompt_for_config(context, no_input=False): cookiecutter_dict = dict() env = StrictEnvironment() for key, raw in context['cookiecutter'].items(): if key in options: val = options[key] else: raw = raw if isinstance(raw, basestring) else str(raw) val = env.from_string(raw).render(cookiecutter=cookiecutter_dict) if not no_input: val = click.prompt(key, default=val) cookiecutter_dict[key] = val return cookiecutter_dict try: cookiecutter.main.prompt_for_config = custom_prompt_for_config yield finally: cookiecutter.main.prompt_for_config = original_prompt_for_config @click.command("new") @click.option("--name", "-n", help="The name of the plugin") @click.option("--package", "-p", help="The plugin package") @click.option("--author", "-a", help="The plugin author's name") @click.option("--email", "-e", help="The plugin author's mail address") @click.option("--license", "-l", help="The plugin's license") @click.option("--description", "-d", help="The plugin's description") @click.option("--homepage", help="The plugin's homepage URL") @click.option("--source", "-s", help="The URL to the plugin's source") @click.option("--installurl", "-i", help="The plugin's install URL") @click.argument("identifier", required=False) def command(name, package, author, email, description, license, homepage, source, installurl, identifier): """Creates a new plugin based on the OctoPrint Plugin cookiecutter template.""" from octoprint.util import tempdir # deleting a git checkout folder might run into access errors due # to write-protected sub folders, so we use a custom onerror handler # that tries to fix such permissions def onerror(func, path, exc_info): """Originally from http://stackoverflow.com/a/2656405/2028598""" import stat import os if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: raise with tempdir(onerror=onerror) as path: custom = dict(cookiecutters_dir=path) with custom_cookiecutter_config(custom): raw_options = dict( plugin_identifier=identifier, plugin_package=package, plugin_name=name, full_name=author, email=email, plugin_description=description, plugin_license=license, plugin_homepage=homepage, plugin_source=source, plugin_installurl=installurl ) options = dict((k, v) for k, v in raw_options.items() if v is not None) with custom_cookiecutter_prompt(options): cookiecutter.main.cookiecutter("gh:OctoPrint/cookiecutter-octoprint-plugin") return command def plugin_install(self): @click.command("install") @click.option("--path", help="Path of the local plugin development folder to install") def command(path): """ Installs the local plugin in development mode. Note: This can NOT be used to install plugins from remote locations such as the plugin repository! It is strictly for local development of plugins, to ensure the plugin is installed (editable) into the same python environment that OctoPrint is installed under. """ import os import sys if not path: path = os.getcwd() # check if this really looks like a plugin if not os.path.isfile(os.path.join(path, "setup.py")): click.echo("This doesn't look like an OctoPrint plugin folder") sys.exit(1) self.command_caller.call([sys.executable, "setup.py", "develop"], cwd=path) return command def plugin_uninstall(self): @click.command("uninstall") @click.argument("name") def command(name): """Uninstalls the plugin with the given name.""" import sys lower_name = name.lower() if not lower_name.startswith("octoprint_") and not lower_name.startswith("octoprint-"): click.echo("This doesn't look like an OctoPrint plugin name") sys.exit(1) call = [sys.executable, "-m", "pip", "uninstall", "--yes", name] self.command_caller.call(call) return command
class OctoPrintDevelCommands(click.MultiCommand): """ Custom `click.MultiCommand <http://click.pocoo.org/5/api/#click.MultiCommand>`_ implementation that provides commands relevant for (plugin) development based on availability of development dependencies. """ sep = ":" groups = ("plugin", "css") def __init__(self, *args, **kwargs): click.MultiCommand.__init__(self, *args, **kwargs) from functools import partial from octoprint.util.commandline import CommandlineCaller def log_util(f): def log(*lines): for line in lines: f(line) return log self.command_caller = CommandlineCaller() self.command_caller.on_log_call = log_util(lambda x: click.echo(f">> {x}")) self.command_caller.on_log_stdout = log_util(click.echo) self.command_caller.on_log_stderr = log_util(partial(click.echo, err=True)) def _get_prefix_methods(self, method_prefix): for name in [x for x in dir(self) if x.startswith(method_prefix)]: method = getattr(self, name) yield method def _get_commands_from_prefix_methods(self, method_prefix): for method in self._get_prefix_methods(method_prefix): result = method() if result is not None and isinstance(result, click.Command): yield result def _get_commands(self): result = {} for group in self.groups: for command in self._get_commands_from_prefix_methods(f"{group}_"): result[group + self.sep + command.name] = command return result def list_commands(self, ctx): result = [name for name in self._get_commands()] result.sort() return result def get_command(self, ctx, cmd_name): commands = self._get_commands() return commands.get(cmd_name, None) def plugin_new(self): try: import cookiecutter.main except ImportError: return None try: # we depend on Cookiecutter >= 1.4 from cookiecutter.prompt import StrictEnvironment except ImportError: return None import contextlib @contextlib.contextmanager def custom_cookiecutter_config(config): """ Allows overriding cookiecutter's user config with a custom dict with fallback to the original data. """ from octoprint.util import fallback_dict original_get_user_config = cookiecutter.main.get_user_config try: def f(*args, **kwargs): original_config = original_get_user_config(*args, **kwargs) return fallback_dict(config, original_config) cookiecutter.main.get_user_config = f yield finally: cookiecutter.main.get_user_config = original_get_user_config @contextlib.contextmanager def custom_cookiecutter_prompt(options): """ Custom cookiecutter prompter for the template config. If a setting is available in the provided options (read from the CLI) that will be used, otherwise the user will be prompted for a value via click. """ original_prompt_for_config = cookiecutter.main.prompt_for_config def custom_prompt_for_config(context, no_input=False): cookiecutter_dict = {} env = StrictEnvironment() for key, raw in context["cookiecutter"].items(): if key in options: val = options[key] else: if not isinstance(raw, str): raw = str(raw) val = env.from_string(raw).render(cookiecutter=cookiecutter_dict) if not no_input: val = click.prompt(key, default=val) cookiecutter_dict[key] = val return cookiecutter_dict try: cookiecutter.main.prompt_for_config = custom_prompt_for_config yield finally: cookiecutter.main.prompt_for_config = original_prompt_for_config @click.command("new") @click.option("--name", "-n", help="The name of the plugin") @click.option("--package", "-p", help="The plugin package") @click.option("--author", "-a", help="The plugin author's name") @click.option("--email", "-e", help="The plugin author's mail address") @click.option("--license", "-l", help="The plugin's license") @click.option("--description", "-d", help="The plugin's description") @click.option("--homepage", help="The plugin's homepage URL") @click.option("--source", "-s", help="The URL to the plugin's source") @click.option("--installurl", "-i", help="The plugin's install URL") @click.argument("identifier", required=False) def command( name, package, author, email, description, license, homepage, source, installurl, identifier, ): """Creates a new plugin based on the OctoPrint Plugin cookiecutter template.""" from octoprint.util import tempdir # deleting a git checkout folder might run into access errors due # to write-protected sub folders, so we use a custom onerror handler # that tries to fix such permissions def onerror(func, path, exc_info): """Originally from http://stackoverflow.com/a/2656405/2028598""" import os import stat if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: raise with tempdir(onerror=onerror) as path: custom = {"cookiecutters_dir": path} with custom_cookiecutter_config(custom): raw_options = { "plugin_identifier": identifier, "plugin_package": package, "plugin_name": name, "full_name": author, "email": email, "plugin_description": description, "plugin_license": license, "plugin_homepage": homepage, "plugin_source": source, "plugin_installurl": installurl, } options = {k: v for k, v in raw_options.items() if v is not None} with custom_cookiecutter_prompt(options): cookiecutter.main.cookiecutter( "gh:OctoPrint/cookiecutter-octoprint-plugin" ) return command def plugin_install(self): @click.command("install") @click.option( "--path", help="Path of the local plugin development folder to install" ) def command(path): """ Installs the local plugin in development mode. Note: This can NOT be used to install plugins from remote locations such as the plugin repository! It is strictly for local development of plugins, to ensure the plugin is installed (editable) into the same python environment that OctoPrint is installed under. """ import os if not path: path = os.getcwd() # check if this really looks like a plugin if not os.path.isfile(os.path.join(path, "setup.py")): click.echo("This doesn't look like an OctoPrint plugin folder") sys.exit(1) self.command_caller.call( [sys.executable, "-m", "pip", "install", "-e", "."], cwd=path ) return command def plugin_uninstall(self): @click.command("uninstall") @click.argument("name") def command(name): """Uninstalls the plugin with the given name.""" lower_name = name.lower() if not lower_name.startswith("octoprint_") and not lower_name.startswith( "octoprint-" ): click.echo("This doesn't look like an OctoPrint plugin name") sys.exit(1) call = [sys.executable, "-m", "pip", "uninstall", "--yes", name] self.command_caller.call(call) return command def css_build(self): @click.command("build") @click.option( "--file", "-f", "files", multiple=True, help="Specify files to build, for a list of options use --list", ) @click.option("--all", "all_files", is_flag=True, help="Build all less files") @click.option( "--list", "list_files", is_flag=True, help="List all available files and exit" ) def command(files, all_files, list_files): import os.path import shutil available_files = { "core": { "source": "static/less/octoprint.less", "output": "static/css/octoprint.css", }, "login": { "source": "static/less/login.less", "output": "static/css/login.css", }, "recovery": { "source": "static/less/recovery.less", "output": "static/css/recovery.css", }, "plugin_announcements": { "source": "plugins/announcements/static/less/announcements.less", "output": "plugins/announcements/static/css/announcements.css", }, "plugin_appkeys_core": { "source": "plugins/appkeys/static/less/appkeys.less", "output": "plugins/appkeys/static/css/appkeys.css", }, "plugin_appkeys_authdialog": { "source": "plugins/appkeys/static/less/authdialog.less", "output": "plugins/appkeys/static/css/authdialog.css", }, "plugin_backup": { "source": "plugins/backup/static/less/backup.less", "output": "plugins/backup/static/css/backup.css", }, "plugin_gcodeviewer": { "source": "plugins/gcodeviewer/static/less/gcodeviewer.less", "output": "plugins/gcodeviewer/static/css/gcodeviewer.css", }, "plugin_logging": { "source": "plugins/logging/static/less/logging.less", "output": "plugins/logging/static/css/logging.css", }, "plugin_pluginmanager": { "source": "plugins/pluginmanager/static/less/pluginmanager.less", "output": "plugins/pluginmanager/static/css/pluginmanager.css", }, "plugin_softwareupdate": { "source": "plugins/softwareupdate/static/less/softwareupdate.less", "output": "plugins/softwareupdate/static/css/softwareupdate.css", }, } if list_files: click.echo("Available files to build:") for name in available_files.keys(): click.echo(f"- {name}") sys.exit(0) if all_files: files = available_files.keys() if not files: click.echo( "No files specified. Use `--file <file>` to specify individual files, or `--all` to build all." ) sys.exit(1) # Check that lessc is installed less = shutil.which("lessc") if not less: click.echo( "lessc is not installed/not available, please install it first" ) click.echo( "Try `npm i -g less` to install it (NOT lessc in this command!)" ) sys.exit(1) # Find the folder of the `octoprint` package # Two folders up from this file octoprint = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) for file in files: if file not in available_files.keys(): click.echo(f"Unknown file {file}") sys.exit(1) source_path = os.path.join(octoprint, available_files[file]["source"]) output_path = os.path.join(octoprint, available_files[file]["output"]) # Check the target file exists if not os.path.exists(source_path): click.echo(f"Target file {source_path} does not exist") continue # Build command line, with necessary options # TODO -x is deprecated, find replacement? less_command = [less, "-x", source_path, output_path] self.command_caller.call(less_command) return command
class OctoPrintDevelCommands(click.MultiCommand): """ Custom `click.MultiCommand <http://click.pocoo.org/5/api/#click.MultiCommand>`_ implementation that provides commands relevant for (plugin) development based on availability of development dependencies. """ sep = ":" groups = ("plugin",) def __init__(self, *args, **kwargs): click.MultiCommand.__init__(self, *args, **kwargs) from octoprint.util.commandline import CommandlineCaller from functools import partial def log_util(f): def log(*lines): for line in lines: f(line) return log self.command_caller = CommandlineCaller() self.command_caller.on_log_call = log_util(lambda x: click.echo(">> {}".format(x))) self.command_caller.on_log_stdout = log_util(click.echo) self.command_caller.on_log_stderr = log_util(partial(click.echo, err=True)) def _get_prefix_methods(self, method_prefix): for name in [x for x in dir(self) if x.startswith(method_prefix)]: method = getattr(self, name) yield method def _get_commands_from_prefix_methods(self, method_prefix): for method in self._get_prefix_methods(method_prefix): result = method() if result is not None and isinstance(result, click.Command): yield result def _get_commands(self): result = dict() for group in self.groups: for command in self._get_commands_from_prefix_methods("{}_".format(group)): result[group + self.sep + command.name] = command return result def list_commands(self, ctx): result = [name for name in self._get_commands()] result.sort() return result def get_command(self, ctx, cmd_name): commands = self._get_commands() return commands.get(cmd_name, None) def plugin_new(self): try: import cookiecutter.main except ImportError: return None try: # we depend on Cookiecutter >= 1.4 from cookiecutter.prompt import StrictEnvironment except ImportError: return None import contextlib @contextlib.contextmanager def custom_cookiecutter_config(config): """ Allows overriding cookiecutter's user config with a custom dict with fallback to the original data. """ from octoprint.util import fallback_dict original_get_user_config = cookiecutter.main.get_user_config try: def f(*args, **kwargs): original_config = original_get_user_config(*args, **kwargs) return fallback_dict(config, original_config) cookiecutter.main.get_user_config = f yield finally: cookiecutter.main.get_user_config = original_get_user_config @contextlib.contextmanager def custom_cookiecutter_prompt(options): """ Custom cookiecutter prompter for the template config. If a setting is available in the provided options (read from the CLI) that will be used, otherwise the user will be prompted for a value via click. """ original_prompt_for_config = cookiecutter.main.prompt_for_config def custom_prompt_for_config(context, no_input=False): cookiecutter_dict = dict() env = StrictEnvironment() for key, raw in context['cookiecutter'].items(): if key in options: val = options[key] else: if not isinstance(raw, basestring): raw = str(raw) val = env.from_string(raw).render(cookiecutter=cookiecutter_dict) if not no_input: val = click.prompt(key, default=val) cookiecutter_dict[key] = val return cookiecutter_dict try: cookiecutter.main.prompt_for_config = custom_prompt_for_config yield finally: cookiecutter.main.prompt_for_config = original_prompt_for_config @click.command("new") @click.option("--name", "-n", help="The name of the plugin") @click.option("--package", "-p", help="The plugin package") @click.option("--author", "-a", help="The plugin author's name") @click.option("--email", "-e", help="The plugin author's mail address") @click.option("--license", "-l", help="The plugin's license") @click.option("--description", "-d", help="The plugin's description") @click.option("--homepage", help="The plugin's homepage URL") @click.option("--source", "-s", help="The URL to the plugin's source") @click.option("--installurl", "-i", help="The plugin's install URL") @click.argument("identifier", required=False) def command(name, package, author, email, description, license, homepage, source, installurl, identifier): """Creates a new plugin based on the OctoPrint Plugin cookiecutter template.""" from octoprint.util import tempdir # deleting a git checkout folder might run into access errors due # to write-protected sub folders, so we use a custom onerror handler # that tries to fix such permissions def onerror(func, path, exc_info): """Originally from http://stackoverflow.com/a/2656405/2028598""" import stat import os if not os.access(path, os.W_OK): os.chmod(path, stat.S_IWUSR) func(path) else: raise with tempdir(onerror=onerror) as path: custom = dict(cookiecutters_dir=path) with custom_cookiecutter_config(custom): raw_options = dict( plugin_identifier=identifier, plugin_package=package, plugin_name=name, full_name=author, email=email, plugin_description=description, plugin_license=license, plugin_homepage=homepage, plugin_source=source, plugin_installurl=installurl ) options = dict((k, v) for k, v in raw_options.items() if v is not None) with custom_cookiecutter_prompt(options): cookiecutter.main.cookiecutter("gh:OctoPrint/cookiecutter-octoprint-plugin") return command def plugin_install(self): @click.command("install") @click.option("--path", help="Path of the local plugin development folder to install") def command(path): """ Installs the local plugin in development mode. Note: This can NOT be used to install plugins from remote locations such as the plugin repository! It is strictly for local development of plugins, to ensure the plugin is installed (editable) into the same python environment that OctoPrint is installed under. """ import os import sys if not path: path = os.getcwd() # check if this really looks like a plugin if not os.path.isfile(os.path.join(path, "setup.py")): click.echo("This doesn't look like an OctoPrint plugin folder") sys.exit(1) self.command_caller.call([sys.executable, "-m", "pip", "install", "-e", "."], cwd=path) return command def plugin_uninstall(self): @click.command("uninstall") @click.argument("name") def command(name): """Uninstalls the plugin with the given name.""" import sys lower_name = name.lower() if not lower_name.startswith("octoprint_") and not lower_name.startswith("octoprint-"): click.echo("This doesn't look like an OctoPrint plugin name") sys.exit(1) call = [sys.executable, "-m", "pip", "uninstall", "--yes", name] self.command_caller.call(call) return command