diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5deb5cf2..6d51e6f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,3 +43,10 @@ jobs: asset_path: ./cloudsmith-${{ env.VERSION }}.pyz asset_name: cloudsmith-${{ env.VERSION }}.pyz asset_content_type: application/zip + - name: Rename asset to cloudsmith.pyz + run: mv ./cloudsmith-${{ env.VERSION }}.pyz cloudsmith.pyz + - name: Upload to Cloudsmith + run: ./cloudsmith.pyz push raw ${{ vars.CLOUDSMITH_NAMESPACE }}/${{ vars.CLOUDSMITH_REPO }} cloudsmith.pyz --name "cloudsmith-cli" --version ${{ env.VERSION }} + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + VERSION: ${{ env.VERSION }} diff --git a/.gitignore b/.gitignore index 1c36532f..dee5e5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ reports/ .env # Zipapp -*.pyz \ No newline at end of file +*.pyz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 859cf2c9..b704762d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,6 @@ For most purposes, you probably just want `pip install -r requirements.txt`. Our [direnv config](./.envrc) file codifies the development environment setup which we use internally. - ## Coding Conventions Please ensure code conforms to [PEP-8](https://www.python.org/dev/peps/pep-0008/) and [PEP-257](https://www.python.org/dev/peps/pep-0257/). diff --git a/cloudsmith_cli/cli/commands/dependencies.py b/cloudsmith_cli/cli/commands/dependencies.py index 9d8628b5..b11dcb71 100644 --- a/cloudsmith_cli/cli/commands/dependencies.py +++ b/cloudsmith_cli/cli/commands/dependencies.py @@ -42,7 +42,7 @@ def list_dependencies(ctx, opts, owner_repo_package): for the package format yet. - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the - REPO name where the package is stored, and the PACKAGE name (identifier) of the + REPO name where the package is stored, and the PACKAGE name (identifier/slug) of the package itself. All separated by a slash. Example: 'your-org/awesome-repo/better-pkg'. diff --git a/cloudsmith_cli/cli/commands/list_.py b/cloudsmith_cli/cli/commands/list_.py index 1fa5f5af..adeb9b69 100644 --- a/cloudsmith_cli/cli/commands/list_.py +++ b/cloudsmith_cli/cli/commands/list_.py @@ -9,7 +9,7 @@ from ...core.api.packages import get_package_format_names_with_distros, list_packages from .. import command, decorators, utils, validators from ..exceptions import handle_api_exceptions -from ..utils import maybe_spinner +from ..utils import maybe_spinner, paginate_results from . import dependencies, entitlements from .main import main from .repos import get as get_repos @@ -128,7 +128,7 @@ def entitlements_(*args, **kwargs): # pylint: disable=missing-docstring help=("A boolean-like search term for querying package attributes."), ) @click.pass_context -def packages(ctx, opts, owner_repo, page, page_size, query): +def packages(ctx, opts, owner_repo, page, page_size, query, show_all): """ List packages for a repository. @@ -176,8 +176,14 @@ def packages(ctx, opts, owner_repo, page, page_size, query): context_msg = "Failed to get list of packages!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): - packages_, page_info = list_packages( - owner=owner, repo=repo, page=page, page_size=page_size, query=query + packages_, page_info = paginate_results( + list_packages, + show_all, + page, + page_size, + owner=owner, + repo=repo, + query=query, ) click.secho("OK", fg="green", err=use_stderr) @@ -209,10 +215,17 @@ def packages(ctx, opts, owner_repo, page, page_size, query): click.echo() num_results = len(packages_) - list_suffix = "package%s visible" % ("s" if num_results != 1 else "") - utils.pretty_print_list_info( - num_results=num_results, page_info=page_info, suffix=list_suffix - ) + list_suffix = "package%s" % ("s" if num_results != 1 else "") + if show_all: + utils.pretty_print_list_info( + num_results=num_results, suffix=f"{list_suffix} retrieved", show_all=True + ) + else: + utils.pretty_print_list_info( + num_results=num_results, + page_info=page_info, + suffix=f"{list_suffix} visible", + ) @list_.command() @@ -229,7 +242,7 @@ def packages(ctx, opts, owner_repo, page, page_size, query): required=False, ) @click.pass_context -def repos(ctx, opts, owner_repo, page, page_size): +def repos(ctx, opts, owner_repo, page, page_size, show_all): """ List repositories for a namespace (owner). diff --git a/cloudsmith_cli/cli/commands/policy/license.py b/cloudsmith_cli/cli/commands/policy/license.py index 6096f938..dfc8b8d0 100644 --- a/cloudsmith_cli/cli/commands/policy/license.py +++ b/cloudsmith_cli/cli/commands/policy/license.py @@ -13,13 +13,12 @@ maybe_spinner, maybe_truncate_list, maybe_truncate_string, + paginate_results, ) from .command import policy def print_license_policies(policies): - """Print license policies as a table or output in another format.""" - headers = [ "Name", "Description", @@ -57,10 +56,6 @@ def print_license_policies(policies): utils.pretty_print_table(headers, rows) click.echo() - num_results = len(rows) - list_suffix = "license polic%s" % ("y" if num_results == 1 else "ies") - utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix) - @policy.group(cls=command.AliasGroup, name="license", aliases=[]) @decorators.common_cli_config_options @@ -86,7 +81,7 @@ def licence(*args, **kwargs): "owner", metavar="OWNER", callback=validators.validate_owner, required=True ) @click.pass_context -def ls(ctx, opts, owner, page, page_size): +def ls(ctx, opts, owner, page, page_size, show_all): """ List license policies. @@ -111,8 +106,8 @@ def ls(ctx, opts, owner, page, page_size): context_msg = "Failed to get license policies!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): - policies, page_info = api.list_license_policies( - owner=owner, page=page, page_size=page_size + policies, page_info = paginate_results( + api.list_license_policies, show_all, page, page_size, owner=owner ) click.secho("OK", fg="green", err=use_stderr) @@ -122,6 +117,17 @@ def ls(ctx, opts, owner, page, page_size): print_license_policies(policies) + click.echo() + + num_results = len(policies) + list_suffix = "license polic%s" % ("y" if num_results == 1 else "ies") + utils.pretty_print_list_info( + num_results=num_results, + page_info=None if show_all else page_info, + suffix=list_suffix, + show_all=show_all, + ) + @licence.command(aliases=["new"]) @decorators.common_cli_config_options diff --git a/cloudsmith_cli/cli/commands/policy/vulnerability.py b/cloudsmith_cli/cli/commands/policy/vulnerability.py index ff883837..98d070c0 100644 --- a/cloudsmith_cli/cli/commands/policy/vulnerability.py +++ b/cloudsmith_cli/cli/commands/policy/vulnerability.py @@ -6,7 +6,13 @@ from ....core.api import orgs as api from ... import command, decorators, utils, validators from ...exceptions import handle_api_exceptions -from ...utils import fmt_bool, fmt_datetime, maybe_spinner, maybe_truncate_string +from ...utils import ( + fmt_bool, + fmt_datetime, + maybe_spinner, + maybe_truncate_string, + paginate_results, +) from .command import policy @@ -45,10 +51,6 @@ def print_vulnerability_policies(policies): utils.pretty_print_table(headers, rows, title="Vulnerability Policies") click.echo() - num_results = len(rows) - list_suffix = "vulnerability polic%s" % ("y" if num_results == 1 else "ies") - utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix) - @policy.group(cls=command.AliasGroup, name="vulnerability", aliases=[]) @decorators.common_cli_config_options @@ -74,7 +76,7 @@ def vulnerability(*args, **kwargs): "owner", metavar="OWNER", callback=validators.validate_owner, required=True ) @click.pass_context -def ls(ctx, opts, owner, page, page_size): +def ls(ctx, opts, owner, page, page_size, show_all): """ List vulnerability policies. @@ -99,8 +101,8 @@ def ls(ctx, opts, owner, page, page_size): context_msg = "Failed to get package vulnerability policies!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): - policies, page_info = api.list_vulnerability_policies( - owner=owner, page=page, page_size=page_size + policies, page_info = paginate_results( + api.list_vulnerability_policies, show_all, page, page_size, owner=owner ) click.secho("OK", fg="green", err=use_stderr) @@ -110,6 +112,17 @@ def ls(ctx, opts, owner, page, page_size): print_vulnerability_policies(policies) + click.echo() + + num_results = len(policies) + list_suffix = "vulnerability polic%s" % ("y" if num_results == 1 else "ies") + utils.pretty_print_list_info( + num_results=num_results, + page_info=None if show_all else page_info, + suffix=list_suffix, + show_all=show_all, + ) + @vulnerability.command(aliases=["new"]) @decorators.common_cli_config_options diff --git a/cloudsmith_cli/cli/commands/repos.py b/cloudsmith_cli/cli/commands/repos.py index db11c148..68184cdd 100644 --- a/cloudsmith_cli/cli/commands/repos.py +++ b/cloudsmith_cli/cli/commands/repos.py @@ -8,11 +8,11 @@ from ...core.api import repos as api from .. import command, decorators, utils, validators from ..exceptions import handle_api_exceptions -from ..utils import maybe_spinner +from ..utils import maybe_spinner, paginate_results from .main import main -def print_repositories(opts, data, page_info=None, show_list_info=True): +def print_repositories(opts, data, page_info=None, show_list_info=True, show_all=False): """Print repositories as a table or output in another format.""" headers = [ "Name", @@ -46,13 +46,23 @@ def print_repositories(opts, data, page_info=None, show_list_info=True): click.echo() utils.pretty_print_table(headers, rows) + if not show_list_info: + return + click.echo() num_results = len(data) - list_suffix = "repositor%s visible" % ("ies" if num_results != 1 else "y") - utils.pretty_print_list_info( - num_results=num_results, page_info=page_info, suffix=list_suffix - ) + list_suffix = "repositor%s" % ("ies" if num_results != 1 else "y") + if show_all: + utils.pretty_print_list_info( + num_results=num_results, suffix=f"{list_suffix} retrieved", show_all=True + ) + else: + utils.pretty_print_list_info( + num_results=num_results, + page_info=page_info, + suffix=f"{list_suffix} visible", + ) @main.group(cls=command.AliasGroup, name="repositories", aliases=["repos"]) @@ -83,7 +93,7 @@ def repositories(ctx, opts): # pylink: disable=unused-argument required=False, ) @click.pass_context -def get(ctx, opts, owner_repo, page, page_size): +def get(ctx, opts, owner_repo, page, page_size, show_all): """ List repositories for a namespace (owner). @@ -118,8 +128,8 @@ def get(ctx, opts, owner_repo, page, page_size): context_msg = "Failed to get list of repositories!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): - repos_, page_info = api.list_repos( - owner=owner, repo=repo, page=page, page_size=page_size + repos_, page_info = paginate_results( + api.list_repos, show_all, page, page_size, owner=owner, repo=repo ) click.secho("OK", fg="green", err=use_stderr) @@ -128,7 +138,11 @@ def get(ctx, opts, owner_repo, page, page_size): return print_repositories( - opts=opts, data=repos_, show_list_info=False, page_info=page_info + opts=opts, + data=repos_, + show_list_info=True, + page_info=page_info, + show_all=show_all, ) @@ -192,7 +206,10 @@ def create(ctx, opts, owner, repo_config_file): click.secho("OK", fg="green", err=use_stderr) - print_repositories(opts=opts, data=[repository], show_list_info=False) + if utils.maybe_print_as_json(opts, [repository]): + return + + print_repositories(opts=opts, data=[repository], show_list_info=True) @repositories.command() @@ -252,7 +269,10 @@ def update(ctx, opts, owner_repo, repo_config_file): click.secho("OK", fg="green", err=use_stderr) - print_repositories(opts=opts, data=[repository], show_list_info=False) + if utils.maybe_print_as_json(opts, [repository]): + return + + print_repositories(opts=opts, data=[repository], show_list_info=True) @repositories.command(aliases=["rm"]) diff --git a/cloudsmith_cli/cli/commands/upstream.py b/cloudsmith_cli/cli/commands/upstream.py index 759c34f0..01ae755e 100644 --- a/cloudsmith_cli/cli/commands/upstream.py +++ b/cloudsmith_cli/cli/commands/upstream.py @@ -13,6 +13,7 @@ maybe_spinner, maybe_truncate_list, maybe_truncate_string, + paginate_results, ) from .main import main @@ -31,7 +32,7 @@ ] -def print_upstreams(upstreams, upstream_fmt): +def print_upstreams(upstreams, upstream_fmt, page_info=None, show_all=False): """Print upstreams as a table or output in another format.""" def build_row(u): @@ -109,9 +110,14 @@ def build_row(u): utils.pretty_print_table(headers, rows) click.echo() - num_results = len(rows) + num_results = len(upstreams) list_suffix = "upstream%s" % ("" if num_results == 1 else "s") - utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix) + utils.pretty_print_list_info( + num_results=num_results, + page_info=None if show_all else page_info, + suffix=list_suffix, + show_all=show_all, + ) @main.group(cls=command.AliasGroup, name="upstream", aliases=[]) @@ -158,7 +164,7 @@ def build_upstream_list_command(upstream_fmt): "owner_repo", metavar="OWNER/REPO", callback=validators.validate_owner_repo ) @click.pass_context - def func(ctx, opts, owner_repo, page, page_size): + def func(ctx, opts, owner_repo, page, page_size, show_all): owner, repo = owner_repo # Use stderr for messages if the output is something else (e.g. # JSON) @@ -169,12 +175,14 @@ def func(ctx, opts, owner_repo, page, page_size): context_msg = "Failed to get upstreams!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): - upstreams, page_info = api.list_upstreams( + upstreams, page_info = paginate_results( + api.list_upstreams, + show_all, + page, + page_size, owner=owner, repo=repo, upstream_format=upstream_fmt, - page=page, - page_size=page_size, ) click.secho("OK", fg="green", err=use_stderr) @@ -182,7 +190,7 @@ def func(ctx, opts, owner_repo, page, page_size): if utils.maybe_print_as_json(opts, upstreams, page_info): return - print_upstreams(upstreams, upstream_fmt) + print_upstreams(upstreams, upstream_fmt, page_info, show_all) func.__doc__ = f""" List {upstream_fmt} upstreams for a repository. diff --git a/cloudsmith_cli/cli/commands/whoami.py b/cloudsmith_cli/cli/commands/whoami.py index f1173770..4b87dc98 100644 --- a/cloudsmith_cli/cli/commands/whoami.py +++ b/cloudsmith_cli/cli/commands/whoami.py @@ -5,7 +5,7 @@ from ...core.api.user import get_user_brief from .. import decorators from ..exceptions import handle_api_exceptions -from ..utils import maybe_spinner +from ..utils import maybe_print_as_json, maybe_spinner from .main import main @@ -17,13 +17,32 @@ @click.pass_context def whoami(ctx, opts): """Retrieve your current authentication status.""" - click.echo("Retrieving your authentication status from the API ... ", nl=False) + # Use stderr for messages if the output is something else (e.g. JSON) + use_stderr = opts.output != "pretty" + + click.echo( + "Retrieving your authentication status from the API ... ", + nl=False, + err=use_stderr, + ) context_msg = "Failed to retrieve your authentication status!" with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg): with maybe_spinner(opts): is_auth, username, email, name = get_user_brief() - click.secho("OK", fg="green") + + click.secho("OK", fg="green", err=use_stderr) + + data = { + "authenticated": is_auth, + "username": username, + "email": email, + "name": name, + } + + if maybe_print_as_json(opts, data): + return + click.echo("You are authenticated as:") if not is_auth: click.secho("Nobody (i.e. anonymous user)", fg="yellow") diff --git a/cloudsmith_cli/cli/decorators.py b/cloudsmith_cli/cli/decorators.py index a8b41173..96c4c674 100644 --- a/cloudsmith_cli/cli/decorators.py +++ b/cloudsmith_cli/cli/decorators.py @@ -4,8 +4,10 @@ import click +from cloudsmith_cli.cli import validators + from ..core.api.init import initialise_api as _initialise_api -from . import config, utils, validators +from . import config, utils def report_retry(seconds, context=None): @@ -145,12 +147,11 @@ def common_cli_list_options(f): """Add common list options to commands.""" @click.option( - "-p", - "--page", - default=1, - type=int, - help="The page to view for lists, where 1 is the first page", - callback=validators.validate_page, + "--show-all", + default=False, + is_flag=True, + help="Show all results. Cannot be used with --page (-p) or --page-size (-l).", + callback=validators.validate_show_all, ) @click.option( "-l", @@ -160,6 +161,14 @@ def common_cli_list_options(f): help="The amount of items to view per page for lists.", callback=validators.validate_page_size, ) + @click.option( + "-p", + "--page", + default=1, + type=int, + help="The page to view for lists, where 1 is the first page", + callback=validators.validate_page, + ) @click.pass_context @functools.wraps(f) def wrapper(ctx, *args, **kwargs): diff --git a/cloudsmith_cli/cli/tests/commands/policy/test_licence.py b/cloudsmith_cli/cli/tests/commands/policy/test_licence.py index 75bc388c..2d66ae49 100644 --- a/cloudsmith_cli/cli/tests/commands/policy/test_licence.py +++ b/cloudsmith_cli/cli/tests/commands/policy/test_licence.py @@ -109,7 +109,11 @@ def test_license_policy_commands(runner, organization, tmp_path): catch_exceptions=False, ) assert ( - "Creating " + policy_name + " license policy for the cloudsmith namespace ...OK" + "Creating " + + policy_name + + " license policy for the " + + organization + + " namespace ...OK" in result.output ) slug_perm = assert_output_matches_policy_config( @@ -121,6 +125,23 @@ def test_license_policy_commands(runner, organization, tmp_path): assert "Getting license policies ... OK" in result.output assert_output_matches_policy_config(result.output, policy_config_file_path) + # List policies with --show-all flag + result = runner.invoke( + ls, args=[organization, "--show-all"], catch_exceptions=False + ) + assert "Getting license policies ... OK" in result.output + + assert "Results: " in result.output and " license policies" in result.output + + # Fail to use --show-all with --page or --page-size + result = runner.invoke( + ls, [organization, "--show-all", "--page", "1"], catch_exceptions=False + ) + assert ( + "The --show-all option cannot be used with --page (-p) or --page-size (-l) options." + in result.output + ) + # Change the values in the config file policy_config_file_path = create_license_policy_config_file( directory=tmp_path, @@ -139,7 +160,11 @@ def test_license_policy_commands(runner, organization, tmp_path): catch_exceptions=False, ) assert ( - "Updating " + slug_perm + " license policy in the cloudsmith namespace ...OK" + "Updating " + + slug_perm + + " license policy in the " + + organization + + " namespace ...OK" in result.output ) assert_output_matches_policy_config(result.output, policy_config_file_path) @@ -151,7 +176,9 @@ def test_license_policy_commands(runner, organization, tmp_path): assert ( "Are you absolutely certain you want to delete the " + slug_perm - + " license policy from the cloudsmith namespace? [y/N]: N" + + " license policy from the " + + organization + + " namespace? [y/N]: N" in result.output ) assert "OK, phew! Close call. :-)" in result.output @@ -163,10 +190,12 @@ def test_license_policy_commands(runner, organization, tmp_path): assert ( "Are you absolutely certain you want to delete the " + slug_perm - + " license policy from the cloudsmith namespace? [y/N]: Y" + + " license policy from the " + + organization + + " namespace? [y/N]: Y" in result.output ) assert ( - "Deleting " + slug_perm + " from the cloudsmith namespace ... OK" + "Deleting " + slug_perm + " from the " + organization + " namespace ... OK" in result.output ) diff --git a/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py b/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py index de03d046..bff03191 100644 --- a/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py +++ b/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py @@ -113,7 +113,9 @@ def test_vulnerability_policy_commands(runner, organization, tmp_path): assert ( "Creating " + policy_name - + " vulnerability policy for the cloudsmith namespace ...OK" + + " vulnerability policy for the " + + organization + + " namespace ...OK" in result.output ) slug_perm = assert_output_matches_policy_config( @@ -124,6 +126,21 @@ def test_vulnerability_policy_commands(runner, organization, tmp_path): result = runner.invoke(ls, args=[organization], catch_exceptions=False) assert "Getting vulnerability policies ... OK" in result.output assert_output_matches_policy_config(result.output, policy_config_file_path) + # List policies with --show-all flag + result = runner.invoke( + ls, args=[organization, "--show-all"], catch_exceptions=False + ) + assert "Getting vulnerability policies ... OK" in result.output + assert "Results: " in result.output and " vulnerability policies" in result.output + + # Fail to use --show-all with --page or --page-size + result = runner.invoke( + ls, [organization, "--show-all", "--page", "1"], catch_exceptions=False + ) + assert ( + "The --show-all option cannot be used with --page (-p) or --page-size (-l) options." + in result.output + ) # Change the values in the config file policy_config_file_path = create_vulnerability_policy_config_file( @@ -145,7 +162,9 @@ def test_vulnerability_policy_commands(runner, organization, tmp_path): assert ( "Updating " + slug_perm - + " vulnerability policy in the cloudsmith namespace ...OK" + + " vulnerability policy in the " + + organization + + " namespace ...OK" in result.output ) assert_output_matches_policy_config(result.output, policy_config_file_path) @@ -157,7 +176,9 @@ def test_vulnerability_policy_commands(runner, organization, tmp_path): assert ( "Are you absolutely certain you want to delete the " + slug_perm - + " vulnerability policy from the cloudsmith namespace? [y/N]: N" + + " vulnerability policy from the " + + organization + + " namespace? [y/N]: N" in result.output ) assert "OK, phew! Close call. :-)" in result.output @@ -169,10 +190,12 @@ def test_vulnerability_policy_commands(runner, organization, tmp_path): assert ( "Are you absolutely certain you want to delete the " + slug_perm - + " vulnerability policy from the cloudsmith namespace? [y/N]: Y" + + " vulnerability policy from the " + + organization + + " namespace? [y/N]: Y" in result.output ) assert ( - "Deleting " + slug_perm + " from the cloudsmith namespace ... OK" + "Deleting " + slug_perm + " from the " + organization + " namespace ... OK" in result.output ) diff --git a/cloudsmith_cli/cli/tests/commands/test_package_commands.py b/cloudsmith_cli/cli/tests/commands/test_package_commands.py index 83671b6b..10f9e34f 100644 --- a/cloudsmith_cli/cli/tests/commands/test_package_commands.py +++ b/cloudsmith_cli/cli/tests/commands/test_package_commands.py @@ -50,6 +50,13 @@ def test_push_and_delete_raw_package( small_file_data = data[0] assert small_file_data["filename"] == pkg_file.name + # List packages with --show-all flag + result = runner.invoke( + list_, args=["pkgs", org_repo, "--show-all"], catch_exceptions=False + ) + assert "Getting list of packages ... OK" in result.output + assert "Results: 1 package retrieved" in result.output + # Wait for the package to sync. org_repo_package = f"{org_repo}/{small_file_data['slug']}" for _ in range(10): diff --git a/cloudsmith_cli/cli/tests/commands/test_repos.py b/cloudsmith_cli/cli/tests/commands/test_repos.py index d879b197..7ccdaeca 100644 --- a/cloudsmith_cli/cli/tests/commands/test_repos.py +++ b/cloudsmith_cli/cli/tests/commands/test_repos.py @@ -119,6 +119,21 @@ def test_repos_commands(runner, organization, tmp_path): result.output, organization, repo_config_file_path ) + # List repositories with --show-all flag + result = runner.invoke(get, [organization, "--show-all"], catch_exceptions=False) + assert "Getting list of repositories ... OK" in result.output + # A static number cannot be used here because we are performing this test in cloudsmith org which is active. + assert "Results: " in result.output and " repositories retrieved" in result.output + + # Fail to use --show-all with --page or --page-size + result = runner.invoke( + get, [organization, "--show-all", "--page", "1"], catch_exceptions=False + ) + assert ( + "The --show-all option cannot be used with --page (-p) or --page-size (-l) options." + in result.output + ) + # Change the repository description in the repo config file. repository_description = random_str() repo_config_file_path = create_repo_config_file( @@ -134,6 +149,14 @@ def test_repos_commands(runner, organization, tmp_path): update, [owner_slash_repo, str(repo_config_file_path)], catch_exceptions=False ) assert result.exit_code == 0 + assert ( + "Updating " + + repository_slug + + " repository in the " + + organization + + " namespace ...OK" + in result.output + ) assert "Results: 1 repository visible" in result.output assert_output_is_equal_to_repo_config( result.output, organization, repo_config_file_path diff --git a/cloudsmith_cli/cli/tests/commands/test_upstream.py b/cloudsmith_cli/cli/tests/commands/test_upstream.py index 4559eef0..4f08f6cf 100644 --- a/cloudsmith_cli/cli/tests/commands/test_upstream.py +++ b/cloudsmith_cli/cli/tests/commands/test_upstream.py @@ -71,6 +71,26 @@ def test_upstream_commands( assert result_data["name"] == upstream_config["name"] assert result_data["upstream_url"] == upstream_config["upstream_url"] + # List upstreams with --show-all flag + result = runner.invoke( + upstream, + args=[upstream_format, "ls", org_repo, "--show-all"], + catch_exceptions=False, + ) + assert "Getting upstreams... OK" in result.output + assert "Results: 1 upstream" in result.output + + # Fail to use --show-all with --page or --page-size + result = runner.invoke( + upstream, + [upstream_format, "ls", org_repo, "--show-all", "--page", "1"], + catch_exceptions=False, + ) + assert ( + "The --show-all option cannot be used with --page (-p) or --page-size (-l) options." + in result.output + ) + slug_perm = result_data["slug_perm"] assert slug_perm org_repo_slug_perm = f"{org_repo}/{slug_perm}" diff --git a/cloudsmith_cli/cli/utils.py b/cloudsmith_cli/cli/utils.py index f7ab9690..1bef39c0 100644 --- a/cloudsmith_cli/cli/utils.py +++ b/cloudsmith_cli/cli/utils.py @@ -18,32 +18,38 @@ def make_user_agent(prefix=None): return f"cloudsmith-cli/{prefix} cli:{get_cli_version()} api:{get_api_version()}" -def pretty_print_list_info(num_results, page_info=None, suffix=None): - """Pretty print list info, with pagination, for user display.""" - num_results_fg = "green" if num_results else "red" - num_results_text = click.style(str(num_results), fg=num_results_fg) - - if page_info and page_info.is_valid: - page_range = page_info.calculate_range(num_results) - page_info_text = f"page: {click.style(str(page_info.page), bold=True)}/{click.style(str(page_info.page_total), bold=True)}, page size: {click.style(str(page_info.page_size), bold=True)}" - range_results_text = "%(from)s-%(to)s (%(num_results)s) of %(total)s" % { - "num_results": num_results_text, - "from": click.style(str(page_range[0]), fg=num_results_fg), - "to": click.style(str(page_range[1]), fg=num_results_fg), - "total": click.style(str(page_info.count), fg=num_results_fg), - } +def pretty_print_list_info(num_results, page_info=None, suffix="", show_all=False): + """Print information about list results.""" + if show_all: + click.echo( + "Results: %(num_results)d %(suffix)s" + % { + "num_results": num_results, + "suffix": suffix, + } + ) + elif page_info and page_info.page is not None and page_info.page_size is not None: + start = (page_info.page - 1) * page_info.page_size + 1 + end = min(start + num_results - 1, page_info.count or 0) + click.echo( + "Results: %(start)d-%(end)d (%(count)d) of %(total)d %(suffix)s " + "(page: %(page)d/%(pages)d, page size: %(page_size)d)" + % { + "start": start, + "end": end, + "count": num_results, + "total": page_info.count or 0, + "suffix": suffix, + "page": page_info.page, + "pages": page_info.page_total or 1, + "page_size": page_info.page_size, + } + ) else: - page_info_text = "" - range_results_text = num_results_text - - click.secho( - "Results: %(range_results)s %(suffix)s%(page_info)s" - % { - "range_results": range_results_text, - "page_info": " (%s)" % page_info_text if page_info_text else "", - "suffix": suffix or "item(s)", - } - ) + click.echo( + "Results: %(num_results)d %(suffix)s" + % {"num_results": num_results, "suffix": suffix} + ) def fmt_datetime(value): @@ -191,3 +197,38 @@ def maybe_spinner(opts): else: with spinner() as spin: yield spin + + +def paginate_results(api_function, show_all, page, page_size=1000, **kwargs): + """ + Paginate results from an API function. + + :param api_function: The API function to call for retrieving results + :param show_all: Boolean flag to show all results + :param page: The page number to start from + :param page_size: The number of items per page + :param kwargs: Additional keyword arguments to pass to the API function + :return: A tuple of (results, page_info) + """ + if show_all: + all_results = [] + current_page = 1 + max_page_size = 1000 + while True: + page_results, page_info = api_function( + page=current_page, page_size=max_page_size, **kwargs + ) + all_results.extend(page_results) + if not page_results: + break + if len(page_results) < max_page_size: + break + if ( + page_info.page_total is not None + and current_page >= page_info.page_total + ): + break + current_page += 1 + page_info.count = len(all_results) + return all_results, page_info + return api_function(page=page, page_size=page_size, **kwargs) diff --git a/cloudsmith_cli/cli/validators.py b/cloudsmith_cli/cli/validators.py index cc4016d9..1733b99c 100644 --- a/cloudsmith_cli/cli/validators.py +++ b/cloudsmith_cli/cli/validators.py @@ -155,6 +155,26 @@ def validate_page_size(ctx, param, value): return value +def validate_show_all(ctx, param, value): + """Ensure that --show-all is not used with --page (-p) or --page-size (-l).""" + if not value: + return value + + # Check both ctx.params and sys.argv for pagination flags + import sys + + has_pagination = any(param in ctx.params for param in ["page", "page_size"]) or any( + flag in sys.argv for flag in ["--page", "-p", "--page-size", "-l"] + ) + + if has_pagination: + raise click.UsageError( + "The --show-all option cannot be used with --page (-p) or --page-size (-l) options." + ) + + return value + + def validate_optional_timestamp(ctx, param, value): """Ensure that a valid value for a timestamp is used."""