Skip to content

Commit 6472745

Browse files
SharangC96Copilot
andauthored
Make 'artifact list' default to user-in-context with flag interface (#1048)
* Make 'artifact list' default to user-in-context with flag interface Refactor 'clarifai artifact list' so it no longer requires a positional URI path. Adds --user_id/--app_id/--artifact_id flags matching the older flag convention (aligned in the Apr 30 UX Weekly). - --user_id defaults to the current CLI context user. - With no --app_id, list iterates the user's apps and aggregates artifacts (the backend ListArtifacts endpoint is app-scoped). - --versions now uses --app_id/--artifact_id flags instead of a URI. - Legacy positional URI path still works (with a deprecation warning) for back-compat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Allow --artifact_id without --versions for single-row lookup Mirrors kubectl-style behavior: 'clarifai af list --app_id a --artifact_id x' fetches a single artifact via Artifact.get and displays it as a one-row table. --artifact_id still requires --app_id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fixes * Review comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9b45b14 commit 6472745

6 files changed

Lines changed: 574 additions & 62 deletions

File tree

clarifai/cli/artifact.py

Lines changed: 200 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -263,15 +263,77 @@ def artifact():
263263

264264

265265
@artifact.command(['list', 'ls'])
266-
@click.argument('path')
267-
@click.option('--versions', is_flag=True, help='List artifact versions instead of artifacts')
266+
@click.argument('path', required=False)
267+
@click.option('--user_id', required=False, help='User ID. Defaults to the current context user.')
268+
@click.option(
269+
'--app_id',
270+
required=False,
271+
help=(
272+
"App ID. If omitted, lists artifacts across all of the user's apps. "
273+
"Required with --artifact_id."
274+
),
275+
)
276+
@click.option(
277+
'--artifact_id',
278+
required=False,
279+
help=(
280+
'Artifact ID. With --versions, lists versions for this artifact. '
281+
'Without --versions, lists the artifact itself. Requires --app_id.'
282+
),
283+
)
284+
@click.option(
285+
'--versions',
286+
is_flag=True,
287+
help='List versions for the specified artifact instead of artifacts.',
288+
)
289+
@click.option(
290+
'--page_no',
291+
type=click.IntRange(min=1),
292+
default=None,
293+
help=(
294+
'Page number to list (must be >= 1). '
295+
'Omit both --page_no and --per_page to list all results. '
296+
'Supported with --app_id or --versions.'
297+
),
298+
)
299+
@click.option(
300+
'--per_page',
301+
type=click.IntRange(min=1),
302+
default=None,
303+
help=('Number of items per page (must be >= 1). Supported with --app_id or --versions.'),
304+
)
268305
@click.pass_context
269-
def list(ctx, path, versions):
306+
def list(ctx, path, user_id, app_id, artifact_id, versions, page_no, per_page):
270307
"""List artifacts or artifact versions.
271308
309+
\b
272310
Examples:
273-
clarifai af list users/u/apps/a
274-
clarifai af list users/u/apps/a/artifacts/my-artifact --versions
311+
# List all artifacts for the current user across all of their apps
312+
clarifai af list
313+
314+
\b
315+
# Scope to a single app
316+
clarifai af list --app_id my-app
317+
318+
\b
319+
# Paginate within a single app
320+
clarifai af list --app_id my-app --page_no 2 --per_page 50
321+
322+
\b
323+
# List versions of a specific artifact
324+
clarifai af list --versions --app_id my-app --artifact_id my-artifact
325+
326+
\b
327+
# Look up a single artifact by id
328+
clarifai af list --app_id my-app --artifact_id my-artifact
329+
330+
\b
331+
# Another user
332+
clarifai af list --user_id other-user --app_id their-app
333+
334+
\b
335+
# Legacy URI path (deprecated)
336+
clarifai af list users/u/apps/a
275337
"""
276338

277339
from clarifai_grpc.grpc.api import resources_pb2
@@ -282,25 +344,48 @@ def list(ctx, path, versions):
282344
try:
283345
validate_context(ctx)
284346

285-
# Parse path and extract components
286-
if versions:
347+
# Back-compat: parse legacy positional URI path and back-fill any unset flags.
348+
if path:
349+
logger.warning(
350+
"Passing a URI path positionally is deprecated; use --user_id/--app_id/--artifact_id flags instead."
351+
)
287352
parsed = parse_artifact_path(path)
288-
if not parsed['artifact_id']:
353+
user_id = user_id or parsed.get('user_id')
354+
app_id = app_id or parsed.get('app_id')
355+
artifact_id = artifact_id or parsed.get('artifact_id')
356+
357+
# Default user_id from the current CLI context.
358+
if not user_id:
359+
user_id = getattr(ctx.obj.current, 'user_id', None)
360+
if not user_id:
361+
click.echo(
362+
"No user_id provided and none found in current context. "
363+
"Pass --user_id or set a context with a user_id.",
364+
err=True,
365+
)
366+
raise click.Abort()
367+
368+
if versions:
369+
if not app_id or not artifact_id:
289370
click.echo(
290-
"When using --versions, path must include artifact ID: users/<user-id>/apps/<app-id>/artifacts/<artifact-id>",
371+
"--versions requires --app_id and --artifact_id.",
291372
err=True,
292373
)
293374
raise click.Abort()
294375

295-
# List artifact versions with full data
296376
artifact_version = ArtifactVersion(
297-
artifact_id=parsed['artifact_id'],
298-
user_id=parsed['user_id'],
299-
app_id=parsed['app_id'],
377+
artifact_id=artifact_id,
378+
user_id=user_id,
379+
app_id=app_id,
300380
pat=ctx.obj.current.pat,
301381
base=ctx.obj.current.api_base,
302382
)
303-
versions_list = builtin_list(artifact_version.list())
383+
list_kwargs = {}
384+
if page_no is not None:
385+
list_kwargs['page'] = page_no
386+
if per_page is not None:
387+
list_kwargs['per_page'] = per_page
388+
versions_list = builtin_list(artifact_version.list(**list_kwargs))
304389

305390
if not versions_list:
306391
click.echo("No artifact versions found")
@@ -321,28 +406,51 @@ def list(ctx, path, versions):
321406
'CREATED_AT': lambda v: str(v.created_at.ToDatetime()) if v.created_at else '',
322407
},
323408
)
324-
else:
325-
# For listing artifacts, we expect app-level path: users/<user-id>/apps/<app-id>
326-
if '/artifacts/' in path:
327-
click.echo("To list artifacts, use: users/<user-id>/apps/<app-id>", err=True)
409+
return
410+
411+
# Listing artifacts (not versions).
412+
list_kwargs = {}
413+
if page_no is not None:
414+
list_kwargs['page'] = page_no
415+
if per_page is not None:
416+
list_kwargs['per_page'] = per_page
417+
418+
if artifact_id:
419+
# Single-artifact lookup mode (kubectl-style: pass the ID, get one row).
420+
if not app_id:
421+
click.echo(
422+
"--artifact_id requires --app_id.",
423+
err=True,
424+
)
328425
raise click.Abort()
329426

330-
# Parse app-level path
331-
parsed = parse_artifact_path(path)
332-
333-
# Validate it's an app-level path (no artifact_id)
334-
if parsed['artifact_id'] is not None:
335-
click.echo("To list artifacts, use: users/<user-id>/apps/<app-id>", err=True)
336-
raise click.Abort()
427+
artifact_client = Artifact(
428+
user_id=user_id,
429+
app_id=app_id,
430+
pat=ctx.obj.current.pat,
431+
base=ctx.obj.current.api_base,
432+
)
433+
single = artifact_client.get(artifact_id=artifact_id)
434+
display_co_resources(
435+
[single],
436+
custom_columns={
437+
'ARTIFACT': lambda a: a.id,
438+
'LATEST_VERSION': lambda a: _resolve_latest_version_id(a),
439+
'VISIBILITY': lambda a: _resolve_visibility(resources_pb2, a),
440+
'CREATED_AT': lambda a: str(a.created_at.ToDatetime()) if a.created_at else '',
441+
},
442+
)
443+
return
337444

338-
# List artifacts
339-
artifact = Artifact(
340-
user_id=parsed['user_id'],
341-
app_id=parsed['app_id'],
445+
if app_id:
446+
# Single-app mode.
447+
artifact_client = Artifact(
448+
user_id=user_id,
449+
app_id=app_id,
342450
pat=ctx.obj.current.pat,
343451
base=ctx.obj.current.api_base,
344452
)
345-
artifacts_list = builtin_list(artifact.list())
453+
artifacts_list = builtin_list(artifact_client.list(**list_kwargs))
346454

347455
if not artifacts_list:
348456
click.echo("No artifacts found")
@@ -357,7 +465,69 @@ def list(ctx, path, versions):
357465
'CREATED_AT': lambda a: str(a.created_at.ToDatetime()) if a.created_at else '',
358466
},
359467
)
468+
return
469+
470+
if page_no is not None or per_page is not None:
471+
click.echo(
472+
"--page_no and --per_page require --app_id (or --versions). "
473+
"Global pagination across all apps is not supported.",
474+
err=True,
475+
)
476+
raise click.Abort()
477+
478+
# User-default mode: iterate apps and aggregate artifacts.
479+
from clarifai.client.user import User
480+
481+
user_client = User(
482+
user_id=user_id,
483+
pat=ctx.obj.current.pat,
484+
base_url=ctx.obj.current.api_base,
485+
)
486+
apps = builtin_list(user_client.list_apps())
487+
if not apps:
488+
click.echo("No apps found for user")
489+
return
490+
491+
logger.info(
492+
f"Listing all artifacts across {len(apps)} app(s) for user {user_id}. "
493+
"Use --app_id to scope to a single app."
494+
)
495+
496+
aggregated = []
497+
for app in apps:
498+
app_id_value = getattr(app, 'id', None) or getattr(app, 'app_id', None)
499+
if not app_id_value:
500+
continue
501+
try:
502+
art_client = Artifact(
503+
user_id=user_id,
504+
app_id=app_id_value,
505+
pat=ctx.obj.current.pat,
506+
base=ctx.obj.current.api_base,
507+
)
508+
for a in art_client.list(**list_kwargs):
509+
aggregated.append((app_id_value, a))
510+
except Exception as inner:
511+
logger.warning(f"Failed to list artifacts for app {app_id_value}: {inner}")
512+
513+
if not aggregated:
514+
click.echo("No artifacts found")
515+
return
516+
517+
app_id_by_artifact = {id(a): app_id_value for app_id_value, a in aggregated}
518+
display_co_resources(
519+
[a for _, a in aggregated],
520+
custom_columns={
521+
'APP_ID': lambda a: app_id_by_artifact.get(id(a), ''),
522+
'ARTIFACT': lambda a: a.id,
523+
'LATEST_VERSION': lambda a: _resolve_latest_version_id(a),
524+
'VISIBILITY': lambda a: _resolve_visibility(resources_pb2, a),
525+
'CREATED_AT': lambda a: str(a.created_at.ToDatetime()) if a.created_at else '',
526+
},
527+
)
360528

529+
except click.Abort:
530+
raise
361531
except UserError as e:
362532
click.echo(str(e), err=True)
363533
raise click.Abort()

clarifai/client/artifact.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Generator
1+
from typing import Generator, Optional
22

33
from clarifai_grpc.grpc.api import resources_pb2, service_pb2
44
from clarifai_grpc.grpc.api.status import status_code_pb2
@@ -188,17 +188,19 @@ def list(
188188
self,
189189
user_id: str = "",
190190
app_id: str = "",
191-
page: int = 1,
192-
per_page: int = DEFAULT_ARTIFACTS_PAGE_SIZE,
191+
page: Optional[int] = None,
192+
per_page: Optional[int] = None,
193193
**kwargs,
194194
) -> Generator[resources_pb2.Artifact, None, None]:
195195
"""List artifacts in an app.
196196
197197
Args:
198198
user_id: The user ID. Defaults to the user ID from initialization.
199199
app_id: The app ID. Defaults to the app ID from initialization.
200-
page: The page number for pagination. Defaults to 1.
201-
per_page: The number of results per page. Defaults to 20.
200+
page: The page number for pagination. If None and per_page is also None,
201+
all pages are listed.
202+
per_page: The number of results per page. If page is specified and per_page
203+
is omitted, the default page size is used.
202204
**kwargs: Additional keyword arguments to be passed to the BaseClient.
203205
204206
Yields:
@@ -214,17 +216,35 @@ def list(
214216
raise UserError("user_id is required")
215217
if not app_id:
216218
raise UserError("app_id is required")
219+
if page is not None and page < 1:
220+
raise UserError("page must be >= 1")
221+
if per_page is not None and per_page < 1:
222+
raise UserError("per_page must be >= 1")
217223

218-
request = service_pb2.ListArtifactsRequest(
219-
user_app_id=self.auth_helper.get_user_app_id_proto(user_id=user_id, app_id=app_id),
220-
page=page,
221-
per_page=per_page,
222-
)
224+
request_page = 1 if page is None else page
225+
request_per_page = per_page or DEFAULT_ARTIFACTS_PAGE_SIZE
223226

224-
response = self._grpc_request(self.STUB.ListArtifacts, request)
227+
while True:
228+
request = service_pb2.ListArtifactsRequest(
229+
user_app_id=self.auth_helper.get_user_app_id_proto(user_id=user_id, app_id=app_id),
230+
page=request_page,
231+
per_page=request_per_page,
232+
)
225233

226-
if response.status.code != status_code_pb2.SUCCESS:
227-
raise Exception(f"Failed to list artifacts: {response.status.description}")
234+
response = self._grpc_request(self.STUB.ListArtifacts, request)
235+
236+
if response.status.code != status_code_pb2.SUCCESS:
237+
raise Exception(f"Failed to list artifacts: {response.status.description}")
238+
239+
if not response.artifacts:
240+
break
241+
242+
for artifact_pb in response.artifacts:
243+
yield artifact_pb
244+
245+
if page is not None or per_page is not None:
246+
break
247+
if len(response.artifacts) < request_per_page:
248+
break
228249

229-
for artifact_pb in response.artifacts:
230-
yield artifact_pb
250+
request_page += 1

0 commit comments

Comments
 (0)