From e75a7dbf3ad98d56020250e28849e512a066f4c5 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sat, 25 May 2024 12:14:52 -0500 Subject: [PATCH 1/6] bringing in wwshell --- circup/backends.py | 80 ++++++++++++- circup/command_utils.py | 26 ++++ circup/wwshell/__init__.py | 4 + circup/wwshell/commands.py | 235 +++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 5 files changed, 342 insertions(+), 7 deletions(-) create mode 100644 circup/wwshell/__init__.py create mode 100644 circup/wwshell/commands.py diff --git a/circup/backends.py b/circup/backends.py index dde8f8c..64567a2 100644 --- a/circup/backends.py +++ b/circup/backends.py @@ -97,6 +97,12 @@ class Backend: """ raise NotImplementedError + def upload_file(self, target_file, location_to_paste): + """Paste a copy of the specified file at the location given + To be overridden by subclass + """ + raise NotImplementedError + # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks,too-many-statements def install_module( self, device_path, device_modules, name, pyext, mod_names, upgrade=False @@ -281,7 +287,9 @@ class WebBackend(Backend): ): super().__init__(logger) if password is None: - raise ValueError("--host needs --password") + raise ValueError( + "Must pass --password or set CIRCUP_WEBWORKFLOW_PASSWORD environment variable" + ) # pylint: disable=no-member # verify hostname/address @@ -305,6 +313,7 @@ class WebBackend(Backend): self.library_path = self.device_location + "/" + self.LIB_DIR_PATH self.timeout = timeout self.version_override = version_override + self.FS_URL = urljoin(self.device_location, self.FS_PATH) def install_file_http(self, source, location=None): """ @@ -316,6 +325,7 @@ class WebBackend(Backend): file_name = source.split(os.path.sep) file_name = file_name[-2] if file_name[-1] == "" else file_name[-1] + print(f"inside install_file_http location: '{location}'") if location is None: target = self.device_location + "/" + self.LIB_DIR_PATH + file_name else: @@ -324,7 +334,10 @@ class WebBackend(Backend): auth = HTTPBasicAuth("", self.password) with open(source, "rb") as fp: + print(f"upload file PUT URL: {target}") r = self.session.put(target, fp.read(), auth=auth, timeout=self.timeout) + print(f"install_file_http response status: {r.status_code}") + print(r.content) if r.status_code == 409: _writeable_error() r.raise_for_status() @@ -561,6 +574,52 @@ class WebBackend(Backend): else: self.install_file_http(target_file) + def upload_file(self, target_file, location_to_paste): + """ + copy a file from the host PC to the microcontroller + :param target_file: file on the host PC to copy + :param location_to_paste: Location on the microcontroller to paste it. + :return: + """ + print(f"inside upload_file location_to_paste: '{location_to_paste}'") + if os.path.isdir(target_file): + create_directory_url = urljoin( + self.device_location, + "/".join(("fs", location_to_paste, target_file, "")), + ) + self._create_library_directory(self.device_location, create_directory_url) + self.install_dir_http(target_file, location_to_paste) + else: + self.install_file_http(target_file, location_to_paste) + + def download_file(self, target_file, location_to_paste): + """ + Download a file from the MCU device to the local host PC + :param target_file: The file on the MCU to download + :param location_to_paste: The location on the host PC to put the downloaded copy. + :return: + """ + auth = HTTPBasicAuth("", self.password) + with self.session.get( + self.FS_URL + target_file, timeout=self.timeout, auth=auth + ) as r: + if r.status_code == 404: + click.secho(f"{target_file} was not found on the device", "red") + + file_name = target_file.split("/")[-1] + if location_to_paste is None: + with open(file_name, "wb") as f: + f.write(r.content) + + click.echo(f"Downloaded File: {file_name}") + else: + with open(os.path.join(location_to_paste, file_name), "wb") as f: + f.write(r.content) + + click.echo( + f"Downloaded File: {os.path.join(location_to_paste, file_name)}" + ) + def install_module_mpy(self, bundle, metadata): """ :param bundle library bundle. @@ -636,6 +695,7 @@ class WebBackend(Backend): return True if the file exists, otherwise False. """ auth = HTTPBasicAuth("", self.password) + print(f"URL: {self.get_file_path(filepath)}") resp = requests.get( self.get_file_path(filepath), auth=auth, timeout=self.timeout ) @@ -664,11 +724,8 @@ class WebBackend(Backend): """ retuns the full path on the device to a given file name. """ - return urljoin( - urljoin(self.device_location, "fs/", allow_fragments=False), - filename, - allow_fragments=False, - ) + return "/".join((self.device_location, "fs", filename)) + def is_device_present(self): """ @@ -739,6 +796,17 @@ class WebBackend(Backend): return r.json()["free"] * r.json()["block_size"] # bytes sys.exit(1) + def list_dir(self, dirpath): + auth = HTTPBasicAuth("", self.password) + with self.session.get( + urljoin(self.device_location, f"fs/{dirpath if dirpath else ''}"), + auth=auth, + headers={"Accept": "application/json"}, + timeout=self.timeout, + ) as r: + print(r.content) + return r.json()["files"] + class DiskBackend(Backend): """ diff --git a/circup/command_utils.py b/circup/command_utils.py index e366736..5baa164 100644 --- a/circup/command_utils.py +++ b/circup/command_utils.py @@ -625,3 +625,29 @@ def get_device_path(host, password, path): else: device_path = find_device() return device_path + + +def sorted_by_directory_then_alpha(list_of_files): + """ + Sort the list of files into alphabetical seperated + with directories grouped together before files. + """ + dirs = {} + files = {} + + for cur_file in list_of_files: + if cur_file["directory"]: + dirs[cur_file["name"]] = cur_file + else: + files[cur_file["name"]] = cur_file + + sorted_dir_names = sorted(dirs.keys()) + sorted_file_names = sorted(files.keys()) + + sorted_full_list = [] + for cur_name in sorted_dir_names: + sorted_full_list.append(dirs[cur_name]) + for cur_name in sorted_file_names: + sorted_full_list.append(files[cur_name]) + + return sorted_full_list diff --git a/circup/wwshell/__init__.py b/circup/wwshell/__init__.py new file mode 100644 index 0000000..c7804e5 --- /dev/null +++ b/circup/wwshell/__init__.py @@ -0,0 +1,4 @@ +from .commands import main + +if __name__ == "__main__": + main() diff --git a/circup/wwshell/commands.py b/circup/wwshell/commands.py new file mode 100644 index 0000000..df4f3b5 --- /dev/null +++ b/circup/wwshell/commands.py @@ -0,0 +1,235 @@ +# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +# ----------- CLI command definitions ----------- # + +The following functions have IO side effects (for instance they emit to +stdout). Ergo, these are not checked with unit tests. Most of the +functionality they provide is provided by the functions from util_functions.py, +and the respective Backends which *are* tested. Most of the logic of the following +functions is to prepare things for presentation to / interaction with the user. +""" +import os +import time +import sys +import re +import logging +import update_checker +from semver import VersionInfo +import click +import requests + + +from circup.backends import WebBackend +from circup.logging import logger, log_formatter, LOGFILE +from circup.shared import BOARDLESS_COMMANDS, get_latest_release_from_url + +from circup.command_utils import ( + get_device_path, + get_circup_version, + completion_for_install, + completion_for_example, + sorted_by_directory_then_alpha, +) + + +@click.group() +@click.option( + "--verbose", is_flag=True, help="Comprehensive logging is sent to stdout." +) +@click.option( + "--path", + type=click.Path(exists=True, file_okay=False), + help="Path to CircuitPython directory. Overrides automatic path detection.", +) +@click.option( + "--host", + help="Hostname or IP address of a device. Overrides automatic path detection.", + default="circuitpython.local", +) +@click.option( + "--password", + help="Password to use for authentication when --host is used." + " You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD" + " instead of passing this argument. If both exist the CLI arg takes precedent.", +) +@click.option( + "--timeout", + default=30, + help="Specify the timeout in seconds for any network operations.", +) +@click.version_option( + prog_name="CircFile", + message="%(prog)s, A CircuitPython web workflow file managemenr. Version %(version)s", +) +@click.pass_context +def main( # pylint: disable=too-many-locals + ctx, + verbose, + path, + host, + password, + timeout, +): # pragma: no cover + """ + A tool to manage files CircuitPython device over web workflow. + """ + # pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals + ctx.ensure_object(dict) + ctx.obj["TIMEOUT"] = timeout + + if password is None: + password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD") + + device_path = get_device_path(host, password, path) + + using_webworkflow = "host" in ctx.params.keys() and ctx.params["host"] is not None + print(f"host: {ctx.params['host']}") + print(f"using webworkflow: {using_webworkflow}") + if using_webworkflow: + if host == "circuitpython.local": + click.echo("Checking versions.json on circuitpython.local to find hostname") + versions_resp = requests.get( + "http://circuitpython.local/cp/version.json", timeout=timeout + ) + host = f'{versions_resp.json()["hostname"]}.local' + click.echo(f"Using hostname: {host}") + device_path = device_path.replace("circuitpython.local", host) + try: + ctx.obj["backend"] = WebBackend( + host=host, password=password, logger=logger, timeout=timeout + ) + except ValueError as e: + click.secho(e, fg="red") + time.sleep(0.3) + sys.exit(1) + except RuntimeError as e: + click.secho(e, fg="red") + sys.exit(1) + + if verbose: + # Configure additional logging to stdout. + ctx.obj["verbose"] = True + verbose_handler = logging.StreamHandler(sys.stdout) + verbose_handler.setLevel(logging.INFO) + verbose_handler.setFormatter(log_formatter) + logger.addHandler(verbose_handler) + click.echo("Logging to {}\n".format(LOGFILE)) + else: + ctx.obj["verbose"] = False + + logger.info("### Started Circfile ###") + + # If a newer version of circfile is available, print a message. + logger.info("Checking for a newer version of circfile") + version = get_circup_version() + if version: + update_checker.update_check("circfile", version) + + # stop early if the command is boardless + if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv: + return + + ctx.obj["DEVICE_PATH"] = device_path + latest_version = get_latest_release_from_url( + "https://github.com/adafruit/circuitpython/releases/latest", logger + ) + + if device_path is None or not ctx.obj["backend"].is_device_present(): + click.secho("Could not find a connected CircuitPython device.", fg="red") + sys.exit(1) + else: + click.echo("Found device at {}.".format(device_path)) + + +@main.command("ls") +@click.argument("file", required=True, nargs=1, default="/") +@click.pass_context +def ls_cli(ctx, file): # pragma: no cover + """ + Lists the contents of a directory. Defaults to root directory + if not supplied. + """ + logger.info("ls") + if not file.endswith("/"): + file += "/" + click.echo(f"running: ls {file}") + + files = ctx.obj["backend"].list_dir(file) + click.echo(f"Size\tName") + for cur_file in sorted_by_directory_then_alpha(files): + click.echo( + f"{cur_file['file_size']}\t{cur_file['name']}{'/' if cur_file['directory'] else ''}" + ) + + +@main.command("put") +@click.argument("file", required=True, nargs=1) +@click.argument("location", required=False, nargs=1, default="") +@click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.") +@click.pass_context +def put_cli(ctx, file, location, overwrite): + """ + Upload a copy of a file or directory from the local computer + to the device + """ + click.echo(f"Attempting PUT: {file} at {location} overwrite? {overwrite}") + if not ctx.obj["backend"].file_exists(f"{location}{file}"): + ctx.obj["backend"].upload_file(file, location) + click.echo(f"Successfully PUT {location}{file}") + else: + if overwrite: + click.secho( + f"{location}{file} already exists. Overwriting it.", fg="yellow" + ) + ctx.obj["backend"].upload_file(file, location) + click.echo(f"Successfully PUT {location}{file}") + else: + click.secho( + f"{location}{file} already exists. Pass --overwrite if you wish to replace it.", + fg="red", + ) + + +# pylint: enable=too-many-arguments,too-many-locals + + +@main.command("get") +@click.argument("file", required=True, nargs=1) +@click.argument("location", required=False, nargs=1) +@click.pass_context +def get_cli(ctx, file, location): # pragma: no cover + """ + Download a copy of a file or directory from the device to the local computer. + """ + # click.echo(f"file: {file}") + # click.echo(f"location: {location}") + click.echo(f"running: get {file} {location}") + ctx.obj["backend"].download_file(file, location) + pass + + +@main.command("rm") +@click.argument("file", nargs=-1) +@click.pass_context +def rm_cli(ctx, module): # pragma: no cover + """ + Delete a file on the device. + """ + # device_path = ctx.obj["DEVICE_PATH"] + # print(f"Uninstalling {module} from {device_path}") + # for name in module: + # device_modules = ctx.obj["backend"].get_device_versions() + # name = name.lower() + # mod_names = {} + # for module_item, metadata in device_modules.items(): + # mod_names[module_item.replace(".py", "").lower()] = metadata + # if name in mod_names: + # metadata = mod_names[name] + # module_path = metadata["path"] + # ctx.obj["backend"].uninstall(device_path, module_path) + # click.echo("Uninstalled '{}'.".format(name)) + # else: + # click.echo("Module '{}' not found on device.".format(name)) + # continue diff --git a/setup.py b/setup.py index 3760cb4..529f6b4 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,9 @@ setup( "Topic :: Software Development :: Embedded Systems", "Topic :: System :: Software Distribution", ], - entry_points={"console_scripts": ["circup=circup:main"]}, + entry_points={ + "console_scripts": ["circup=circup:main", "wwshell=circup.wwshell:main"] + }, # What does your project relate to? keywords="adafruit, blinka, circuitpython, micropython, libraries", # You can just specify the packages manually here if your project is From a01586b342527a066c1a930717638882fd0b9f50 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 28 May 2024 18:26:35 -0500 Subject: [PATCH 2/6] implement rm --- circup/backends.py | 1 - circup/wwshell/commands.py | 24 ++++++------------------ 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/circup/backends.py b/circup/backends.py index 64567a2..7ab3275 100644 --- a/circup/backends.py +++ b/circup/backends.py @@ -726,7 +726,6 @@ class WebBackend(Backend): """ return "/".join((self.device_location, "fs", filename)) - def is_device_present(self): """ returns True if the device is currently connected and running supported version diff --git a/circup/wwshell/commands.py b/circup/wwshell/commands.py index df4f3b5..1cdcaf7 100644 --- a/circup/wwshell/commands.py +++ b/circup/wwshell/commands.py @@ -211,25 +211,13 @@ def get_cli(ctx, file, location): # pragma: no cover @main.command("rm") -@click.argument("file", nargs=-1) +@click.argument("file", nargs=1) @click.pass_context -def rm_cli(ctx, module): # pragma: no cover +def rm_cli(ctx, file): # pragma: no cover """ Delete a file on the device. """ - # device_path = ctx.obj["DEVICE_PATH"] - # print(f"Uninstalling {module} from {device_path}") - # for name in module: - # device_modules = ctx.obj["backend"].get_device_versions() - # name = name.lower() - # mod_names = {} - # for module_item, metadata in device_modules.items(): - # mod_names[module_item.replace(".py", "").lower()] = metadata - # if name in mod_names: - # metadata = mod_names[name] - # module_path = metadata["path"] - # ctx.obj["backend"].uninstall(device_path, module_path) - # click.echo("Uninstalled '{}'.".format(name)) - # else: - # click.echo("Module '{}' not found on device.".format(name)) - # continue + click.echo(f"running: rm {file}") + ctx.obj["backend"].uninstall( + ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(file) + ) From 6c824c538e0c06e21e5d2def882d048b22d81e72 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 28 May 2024 20:32:16 -0500 Subject: [PATCH 3/6] pylint fixes --- circup/backends.py | 6 ++++++ circup/wwshell/__init__.py | 10 ++++++++++ circup/wwshell/commands.py | 15 +++------------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/circup/backends.py b/circup/backends.py index 7ab3275..c52b76c 100644 --- a/circup/backends.py +++ b/circup/backends.py @@ -796,6 +796,9 @@ class WebBackend(Backend): sys.exit(1) def list_dir(self, dirpath): + """ + Returns the list of files located in the given dirpath. + """ auth = HTTPBasicAuth("", self.password) with self.session.get( urljoin(self.device_location, f"fs/{dirpath if dirpath else ''}"), @@ -901,6 +904,9 @@ class DiskBackend(Backend): os.path.join(self.device_location, location_to_paste, target_filename), ) + def upload_file(self, target_file, location_to_paste): + self.copy_file(target_file, location_to_paste) + def install_module_mpy(self, bundle, metadata): """ :param bundle library bundle. diff --git a/circup/wwshell/__init__.py b/circup/wwshell/__init__.py index c7804e5..9bee51c 100644 --- a/circup/wwshell/__init__.py +++ b/circup/wwshell/__init__.py @@ -1,4 +1,14 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +wwshell is a CLI utility for managing files on CircuitPython devices via wireless workflows. +It currently supports Web Workflow. +""" from .commands import main + +# Allows execution via `python -m circup ...` +# pylint: disable=no-value-for-parameter if __name__ == "__main__": main() diff --git a/circup/wwshell/commands.py b/circup/wwshell/commands.py index 1cdcaf7..dd9a4d0 100644 --- a/circup/wwshell/commands.py +++ b/circup/wwshell/commands.py @@ -13,23 +13,19 @@ functions is to prepare things for presentation to / interaction with the user. import os import time import sys -import re import logging import update_checker -from semver import VersionInfo import click import requests from circup.backends import WebBackend from circup.logging import logger, log_formatter, LOGFILE -from circup.shared import BOARDLESS_COMMANDS, get_latest_release_from_url +from circup.shared import BOARDLESS_COMMANDS from circup.command_utils import ( get_device_path, get_circup_version, - completion_for_install, - completion_for_example, sorted_by_directory_then_alpha, ) @@ -132,9 +128,6 @@ def main( # pylint: disable=too-many-locals return ctx.obj["DEVICE_PATH"] = device_path - latest_version = get_latest_release_from_url( - "https://github.com/adafruit/circuitpython/releases/latest", logger - ) if device_path is None or not ctx.obj["backend"].is_device_present(): click.secho("Could not find a connected CircuitPython device.", fg="red") @@ -157,7 +150,7 @@ def ls_cli(ctx, file): # pragma: no cover click.echo(f"running: ls {file}") files = ctx.obj["backend"].list_dir(file) - click.echo(f"Size\tName") + click.echo("Size\tName") for cur_file in sorted_by_directory_then_alpha(files): click.echo( f"{cur_file['file_size']}\t{cur_file['name']}{'/' if cur_file['directory'] else ''}" @@ -203,11 +196,9 @@ def get_cli(ctx, file, location): # pragma: no cover """ Download a copy of a file or directory from the device to the local computer. """ - # click.echo(f"file: {file}") - # click.echo(f"location: {location}") + click.echo(f"running: get {file} {location}") ctx.obj["backend"].download_file(file, location) - pass @main.command("rm") From c24d48b4ea7c3c3e5d4b3ce9aada23893cd76440 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 28 May 2024 20:51:54 -0500 Subject: [PATCH 4/6] wwshell readme --- circup/wwshell/README.rst | 105 ++++++++++++++++++++++++++++++ circup/wwshell/README.rst.license | 3 + docs/index.rst | 2 + 3 files changed, 110 insertions(+) create mode 100644 circup/wwshell/README.rst create mode 100644 circup/wwshell/README.rst.license diff --git a/circup/wwshell/README.rst b/circup/wwshell/README.rst new file mode 100644 index 0000000..1773b0e --- /dev/null +++ b/circup/wwshell/README.rst @@ -0,0 +1,105 @@ + +wwshell +======= + +.. image:: https://readthedocs.org/projects/circup/badge/?version=latest + :target: https://circuitpython.readthedocs.io/projects/circup/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://adafru.it/discord + :alt: Discord + + +.. image:: https://github.com/adafruit/circup/workflows/Build%20CI/badge.svg + :target: https://github.com/adafruit/circup/actions + :alt: Build Status + + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code Style: Black + + +A tool to manage files on a CircuitPython device via wireless workflows. +Currently supports Web Workflow. + +.. contents:: + +Installation +------------ + +wwshell is bundled along with Circup. When you install Circup you'll get wwshell automatically. + +Circup requires Python 3.5 or higher. + +In a `virtualenv `_, +``pip install circup`` should do the trick. This is the simplest way to make it +work. + +If you have no idea what a virtualenv is, try the following command, +``pip3 install --user circup``. + +.. note:: + + If you use the ``pip3`` command to install CircUp you must make sure that + your path contains the directory into which the script will be installed. + To discover this path, + + * On Unix-like systems, type ``python3 -m site --user-base`` and append + ``bin`` to the resulting path. + * On Windows, type the same command, but append ``Scripts`` to the + resulting path. + +What does wwshell do? +--------------------- + +It lets you view, delete, upload, and download files from your Circuitpython device +via wireless workflows. Similar to ampy, but operates over wireless workflow rather +than USB serial. + +Usage +----- + +To use web workflow you need to enable it by putting WIFI credentials and a web workflow +password into your settings.toml file. `See here `_, + +To get help, just type the command:: + + $ wwshell + Usage: wwshell [OPTIONS] COMMAND [ARGS]... + + A tool to manage files CircuitPython device over web workflow. + + Options: + --verbose Comprehensive logging is sent to stdout. + --path DIRECTORY Path to CircuitPython directory. Overrides automatic path + detection. + --host TEXT Hostname or IP address of a device. Overrides automatic + path detection. + --password TEXT Password to use for authentication when --host is used. + You can optionally set an environment variable + CIRCUP_WEBWORKFLOW_PASSWORD instead of passing this + argument. If both exist the CLI arg takes precedent. + --timeout INTEGER Specify the timeout in seconds for any network + operations. + --version Show the version and exit. + --help Show this message and exit. + + Commands: + get Download a copy of a file or directory from the device to the... + ls Lists the contents of a directory. + put Upload a copy of a file or directory from the local computer to... + rm Delete a file on the device. + + +.. note:: + + If you find a bug, or you want to suggest an enhancement or new feature + feel free to create an issue or submit a pull request here: + + https://github.com/adafruit/circup + + +Discussion of this tool happens on the Adafruit CircuitPython +`Discord channel `_. diff --git a/circup/wwshell/README.rst.license b/circup/wwshell/README.rst.license new file mode 100644 index 0000000..c582de4 --- /dev/null +++ b/circup/wwshell/README.rst.license @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT diff --git a/docs/index.rst b/docs/index.rst index d06ef8f..6355972 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,8 @@ .. include:: ../README.rst +.. include:: ../circup/wwshell/README.rst + .. include:: ../CONTRIBUTING.rst API From 11868c327e79ab27945dff1f3dac65215d62a8a5 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 30 May 2024 17:26:40 -0500 Subject: [PATCH 5/6] wwshell mkdir implementation --- circup/backends.py | 21 ++++++++++----------- circup/wwshell/commands.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/circup/backends.py b/circup/backends.py index c52b76c..5937422 100644 --- a/circup/backends.py +++ b/circup/backends.py @@ -73,7 +73,7 @@ class Backend: """ return self.get_modules(os.path.join(self.device_location, self.LIB_DIR_PATH)) - def _create_library_directory(self, device_path, library_path): + def create_directory(self, device_path, directory): """ To be overridden by subclass """ @@ -195,7 +195,7 @@ class Backend: return # Create the library directory first. - self._create_library_directory(device_path, library_path) + self.create_directory(device_path, library_path) if local_path is None: if pyext: # Use Python source for module. @@ -555,10 +555,9 @@ class WebBackend(Backend): metadata["path"] = sfm_url result[sfm[:idx]] = metadata - def _create_library_directory(self, device_path, library_path): - url = urlparse(device_path) - auth = HTTPBasicAuth("", url.password) - with self.session.put(library_path, auth=auth, timeout=self.timeout) as r: + def create_directory(self, device_path, directory): + auth = HTTPBasicAuth("", self.password) + with self.session.put(directory, auth=auth, timeout=self.timeout) as r: if r.status_code == 409: _writeable_error() r.raise_for_status() @@ -569,7 +568,7 @@ class WebBackend(Backend): self.device_location, "/".join(("fs", location_to_paste, target_file, "")), ) - self._create_library_directory(self.device_location, create_directory_url) + self.create_directory(self.device_location, create_directory_url) self.install_dir_http(target_file) else: self.install_file_http(target_file) @@ -587,7 +586,7 @@ class WebBackend(Backend): self.device_location, "/".join(("fs", location_to_paste, target_file, "")), ) - self._create_library_directory(self.device_location, create_directory_url) + self.create_directory(self.device_location, create_directory_url) self.install_dir_http(target_file, location_to_paste) else: self.install_file_http(target_file, location_to_paste) @@ -887,9 +886,9 @@ class DiskBackend(Backend): """ return _get_modules_file(device_lib_path, self.logger) - def _create_library_directory(self, device_path, library_path): - if not os.path.exists(library_path): # pragma: no cover - os.makedirs(library_path) + def create_directory(self, device_path, directory): + if not os.path.exists(directory): # pragma: no cover + os.makedirs(directory) def copy_file(self, target_file, location_to_paste): target_filename = target_file.split(os.path.sep)[-1] diff --git a/circup/wwshell/commands.py b/circup/wwshell/commands.py index dd9a4d0..6f6856b 100644 --- a/circup/wwshell/commands.py +++ b/circup/wwshell/commands.py @@ -212,3 +212,16 @@ def rm_cli(ctx, file): # pragma: no cover ctx.obj["backend"].uninstall( ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(file) ) + + +@main.command("mkdir") +@click.argument("directory", nargs=1) +@click.pass_context +def mkdir_cli(ctx, directory): # pragma: no cover + """ + Create + """ + click.echo(f"running: mkdir {directory}") + ctx.obj["backend"].create_directory( + ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(directory) + ) From 431ccac9b38dd43ff97c027145c8ffb523adf319 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Mon, 17 Jun 2024 11:51:23 -0500 Subject: [PATCH 6/6] merge main --- pyproject.toml | 1 + setup.py | 0 2 files changed, 1 insertion(+) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index c271e40..866364f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} [project.scripts] circup = "circup:main" +wwshell = "circup.wwshell:main" [project.urls] homepage = "https://github.com/adafruit/circup" diff --git a/setup.py b/setup.py deleted file mode 100644 index e69de29..0000000