Skip to content

Commit 89a6e0e

Browse files
CENG-452: Add --sort flag for package list command (#185)
1 parent f6d5506 commit 89a6e0e

4 files changed

Lines changed: 142 additions & 2 deletions

File tree

cloudsmith_cli/cli/commands/list_.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,34 @@ def entitlements_(*args, **kwargs): # pylint: disable=missing-docstring
127127
"--query",
128128
help=("A boolean-like search term for querying package attributes."),
129129
)
130+
@click.option(
131+
"--sort",
132+
type=click.Choice(
133+
[
134+
"date",
135+
"-date",
136+
"downloads",
137+
"-downloads",
138+
"format",
139+
"-format",
140+
"name",
141+
"-name",
142+
"scan",
143+
"-scan",
144+
"scan_date",
145+
"-scan_date",
146+
"size",
147+
"-size",
148+
"status",
149+
"-status",
150+
"version",
151+
"-version",
152+
]
153+
),
154+
help=("Sort packages by field. Prefix with '-' for descending order."),
155+
)
130156
@click.pass_context
131-
def packages(ctx, opts, owner_repo, page, page_size, query):
157+
def packages(ctx, opts, owner_repo, page, page_size, query, sort):
132158
"""
133159
List packages for a repository.
134160
@@ -165,6 +191,18 @@ def packages(ctx, opts, owner_repo, page, page_size, query):
165191
166192
--query 'name:^foo$ filename:.zip$ architecture:~x86'
167193
194+
You can sort the results using --sort with these fields:
195+
- date/-date: Sort by creation date
196+
- downloads/-downloads: Sort by download count
197+
- format/-format: Sort by package format
198+
- name/-name: Sort by package name
199+
- scan/-scan: Sort by scan status
200+
- scan_date/-scan_date: Sort by last scan date
201+
- size/-size: Sort by package size
202+
- status/-status: Sort by package status
203+
- version/-version: Sort by version
204+
205+
Prefix any field with '-' for descending order (e.g. -date for newest first).
168206
"""
169207
owner, repo = owner_repo
170208

@@ -177,7 +215,12 @@ def packages(ctx, opts, owner_repo, page, page_size, query):
177215
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
178216
with maybe_spinner(opts):
179217
packages_, page_info = list_packages(
180-
owner=owner, repo=repo, page=page, page_size=page_size, query=query
218+
owner=owner,
219+
repo=repo,
220+
page=page,
221+
page_size=page_size,
222+
query=query,
223+
sort=sort,
181224
)
182225

183226
click.secho("OK", fg="green", err=use_stderr)

cloudsmith_cli/cli/tests/commands/test_package_commands.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,88 @@ def test_push_and_delete_raw_package(
7878
)
7979
data = json.loads(result.output)["data"]
8080
assert len(data) == 0
81+
82+
83+
@pytest.mark.usefixtures("set_api_key_env_var", "set_api_host_env_var")
84+
def test_list_packages_with_sort(runner, organization, tmp_repository, tmp_path):
85+
"""Test listing packages with different sort options."""
86+
org_repo = f'{organization}/{tmp_repository["slug"]}'
87+
88+
# Create and push two packages with different names
89+
for name in ["aaa", "zzz"]:
90+
pkg_file = tmp_path / f"{name}.txt"
91+
with open(pkg_file, "wb") as f:
92+
f.write(b"test content")
93+
94+
runner.invoke(
95+
push,
96+
args=["raw", org_repo, str(pkg_file.resolve())],
97+
catch_exceptions=False,
98+
)
99+
100+
# Wait for package to sync
101+
result = runner.invoke(
102+
list_, args=["pkgs", org_repo, "-F", "json"], catch_exceptions=False
103+
)
104+
data = json.loads(result.output)["data"]
105+
pkg_slug = next(pkg["slug"] for pkg in data if pkg["filename"] == pkg_file.name)
106+
org_repo_package = f"{org_repo}/{pkg_slug}"
107+
108+
for _ in range(10):
109+
time.sleep(5)
110+
result = runner.invoke(
111+
status, args=[org_repo_package], catch_exceptions=False
112+
)
113+
if "Fully Synchronised" in result.output:
114+
break
115+
else:
116+
raise TimeoutError("Test timed out waiting for package sync")
117+
118+
# Test ascending sort by name
119+
result = runner.invoke(
120+
list_,
121+
args=["pkgs", org_repo, "--sort", "name", "-F", "json"],
122+
catch_exceptions=False,
123+
)
124+
data = json.loads(result.output)["data"]
125+
assert len(data) == 2
126+
assert data[0]["filename"] == "aaa.txt"
127+
assert data[1]["filename"] == "zzz.txt"
128+
129+
# Test descending sort by name
130+
result = runner.invoke(
131+
list_,
132+
args=["pkgs", org_repo, "--sort", "-name", "-F", "json"],
133+
catch_exceptions=False,
134+
)
135+
data = json.loads(result.output)["data"]
136+
assert len(data) == 2
137+
assert data[0]["filename"] == "zzz.txt"
138+
assert data[1]["filename"] == "aaa.txt"
139+
140+
# Test sort by date (newest first)
141+
result = runner.invoke(
142+
list_,
143+
args=["pkgs", org_repo, "--sort", "-date", "-F", "json"],
144+
catch_exceptions=False,
145+
)
146+
data = json.loads(result.output)["data"]
147+
assert len(data) == 2
148+
assert data[0]["filename"] == "zzz.txt" # Last uploaded
149+
assert data[1]["filename"] == "aaa.txt" # First uploaded
150+
151+
# Cleanup - delete both packages
152+
for pkg in data:
153+
org_repo_package = f"{org_repo}/{pkg['slug']}"
154+
runner.invoke(delete, args=["-y", org_repo_package], catch_exceptions=False)
155+
156+
# Wait for deletion
157+
for _ in range(10):
158+
time.sleep(5)
159+
result = runner.invoke(
160+
status, args=[org_repo_package], catch_exceptions=False
161+
)
162+
if "status: 404 - Not Found" in result.output:
163+
break
164+
else:
165+
raise TimeoutError("Test timed out waiting for package deletion")

cloudsmith_cli/core/api/packages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def list_packages(owner, repo, **kwargs):
222222
api_kwargs = {}
223223
api_kwargs.update(utils.get_page_kwargs(**kwargs))
224224
api_kwargs.update(utils.get_query_kwargs(**kwargs))
225+
api_kwargs.update(utils.get_sort_kwargs(**kwargs))
225226

226227
with catch_raise_api_exception():
227228
data, _, headers = client.packages_list_with_http_info(

cloudsmith_cli/core/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,14 @@ def get_query_kwargs(**kwargs):
8282
query_kwargs["query"] = query
8383

8484
return query_kwargs
85+
86+
87+
def get_sort_kwargs(**kwargs):
88+
"""Construct sort kwargs (if present)."""
89+
sort_kwargs = {}
90+
91+
sort = kwargs.get("sort")
92+
if sort:
93+
sort_kwargs["sort"] = sort
94+
95+
return sort_kwargs

0 commit comments

Comments
 (0)