diff --git a/riocli/package/delete.py b/riocli/package/delete.py index 7a05b97c..c0900173 100644 --- a/riocli/package/delete.py +++ b/riocli/package/delete.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,30 +11,109 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import functools +from concurrent.futures import ThreadPoolExecutor +from queue import Queue + import click -from click_spinner import spinner +from rapyuta_io.clients.package import Package +from yaspin.api import Yaspin from riocli.config import new_client -from riocli.package.util import name_to_guid +from riocli.constants import Symbols, Colors +from riocli.package.util import fetch_packages, print_packages_for_confirmation +from riocli.utils import tabulate_data +from riocli.utils.spinner import with_spinner @click.command('delete') @click.option('--force', '-f', is_flag=True, default=False, help='Skip confirmation') +@click.option('--all', 'delete_all', is_flag=True, default=False, help='Deletes all packages in the project') @click.option('--version', 'package_version', type=str, help='Semantic version of the Package, only used when name is used instead of GUID') -@click.argument('package-name', type=str) -@name_to_guid -def delete_package(force: bool, package_name: str, package_guid: str) -> None: +@click.option('--workers', '-w', + help="Number of parallel workers while running deleting packages. Defaults to 10", + type=int, default=10) +@click.argument('package-name-or-regex', type=str, default="") +@with_spinner(text="Deleting package(s)...") +def delete_package( + package_name_or_regex: str, + package_version: str, + force: bool = False, + delete_all: bool = False, + workers: int = 10, + spinner: Yaspin = None, +) -> None: """ Delete the package from the Platform """ + client = new_client() + + if not (package_name_or_regex or delete_all): + spinner.text = "Nothing to delete" + spinner.green.ok(Symbols.SUCCESS) + return + + try: + packages = fetch_packages(client, package_name_or_regex, delete_all, package_version) + except Exception as e: + spinner.text = click.style( + 'Failed to find package(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + if not packages: + spinner.text = "Nothing to delete" + spinner.green.ok(Symbols.SUCCESS) + return + + with spinner.hidden(): + print_packages_for_confirmation(packages) + + spinner.write('') + if not force: - click.confirm('Deleting {} ({}) package'.format(package_name, package_guid), abort=True) + with spinner.hidden(): + click.confirm('Do you want to delete the above package(s)?', default=True, abort=True) + try: - client = new_client() - with spinner(): - client.delete_package(package_guid) - click.secho('Package deleted successfully!', fg='green') + result = Queue() + func = functools.partial(_apply_delete, result) + with ThreadPoolExecutor(max_workers=workers) as executor: + executor.map(func, packages) + + result = sorted(list(result.queue), key=lambda x: x[0]) + data, statuses = [], [] + for name, status in result: + fg = Colors.GREEN if status else Colors.RED + icon = Symbols.SUCCESS if status else Symbols.ERROR + statuses.append(status) + data.append([ + click.style(name, fg), + click.style(icon, fg) + ]) + + with spinner.hidden(): + tabulate_data(data, headers=['Name', 'Status']) + + icon = Symbols.SUCCESS if all(statuses) else Symbols.WARNING + fg = Colors.GREEN if all(statuses) else Colors.YELLOW + text = "successfully" if all(statuses) else "partially" + + spinner.text = click.style( + 'Package(s) deleted {}.'.format(text), fg) + spinner.ok(click.style(icon, fg)) except Exception as e: - click.secho(str(e), fg='red') - raise SystemExit(1) + spinner.text = click.style( + 'Failed to delete package(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + +def _apply_delete(result: Queue, package: Package) -> None: + name_version = "{}@{}".format(package.packageName, package.packageVersion) + try: + package.delete() + result.put((name_version, True)) + except Exception: + result.put((name_version, False)) diff --git a/riocli/package/util.py b/riocli/package/util.py index 2fa68959..8c1415cd 100644 --- a/riocli/package/util.py +++ b/riocli/package/util.py @@ -1,4 +1,4 @@ -# Copyright 2021 Rapyuta Robotics +# Copyright 2023 Rapyuta Robotics # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import functools +import re import typing +from typing import List import click from rapyuta_io import Client +from rapyuta_io.clients.package import Package from riocli.config import new_client +from riocli.utils import tabulate_data from riocli.utils.selector import show_selection @@ -71,3 +75,30 @@ def find_package_guid(client: Client, name: str, version: str = None) -> str: choice = show_selection(options, header='Following packages were found with the same name') return choice + + +def fetch_packages( + client: Client, + package_name_or_regex: str, + include_all: bool, + version: str = None +) -> List[Package]: + packages = client.get_all_packages(version=version) + + result = [] + for pkg in packages: + # We cannot delete public packages. Skip them instead. + if 'io-public' in pkg.packageId: + continue + + if include_all or re.search(package_name_or_regex, pkg.packageName): + result.append(pkg) + + return result + + +def print_packages_for_confirmation(packages: List[Package]) -> None: + headers = ['Name', 'Version'] + data = [[p.packageName, p.packageVersion] for p in packages] + + tabulate_data(data, headers)