diff --git a/artdag.py b/artdag.py index eb41a14..541dd72 100755 --- a/artdag.py +++ b/artdag.py @@ -433,5 +433,306 @@ def publish(run_id, output_name): click.echo(f"Activity: {result['activity']['activity_id']}") +# ============ Metadata Commands ============ + +@cli.command() +@click.argument("content_hash") +@click.option("--origin", type=click.Choice(["self", "external"]), help="Set origin type") +@click.option("--origin-url", help="Set external origin URL") +@click.option("--origin-note", help="Note about the origin") +@click.option("--description", "-d", help="Set description") +@click.option("--tags", "-t", help="Set tags (comma-separated)") +@click.option("--folder", "-f", help="Set folder path") +@click.option("--add-collection", help="Add to collection") +@click.option("--remove-collection", help="Remove from collection") +def meta(content_hash, origin, origin_url, origin_note, description, tags, folder, add_collection, remove_collection): + """View or update metadata for a cached item. + + With no options, displays current metadata. + With options, updates the specified fields. + """ + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + # If no update options, just display current metadata + has_updates = any([origin, origin_url, origin_note, description, tags, folder, add_collection, remove_collection]) + + if not has_updates: + # GET metadata + try: + resp = requests.get(f"{get_server()}/cache/{content_hash}/meta", headers=headers) + if resp.status_code == 404: + click.echo(f"Content not found: {content_hash}", err=True) + sys.exit(1) + if resp.status_code == 403: + click.echo("Access denied", err=True) + sys.exit(1) + resp.raise_for_status() + meta = resp.json() + except requests.RequestException as e: + click.echo(f"Failed to get metadata: {e}", err=True) + sys.exit(1) + + click.echo(f"Content Hash: {content_hash}") + click.echo(f"Uploader: {meta.get('uploader', 'unknown')}") + click.echo(f"Uploaded: {meta.get('uploaded_at', 'unknown')}") + if meta.get("origin"): + origin_info = meta["origin"] + click.echo(f"Origin: {origin_info.get('type', 'unknown')}") + if origin_info.get("url"): + click.echo(f" URL: {origin_info['url']}") + if origin_info.get("note"): + click.echo(f" Note: {origin_info['note']}") + else: + click.echo("Origin: not set") + click.echo(f"Description: {meta.get('description', 'none')}") + click.echo(f"Tags: {', '.join(meta.get('tags', [])) or 'none'}") + click.echo(f"Folder: {meta.get('folder', '/')}") + click.echo(f"Collections: {', '.join(meta.get('collections', [])) or 'none'}") + if meta.get("published"): + pub = meta["published"] + click.echo(f"Published: {pub.get('asset_name')} ({pub.get('published_at')})") + return + + # Build update payload + update = {} + + if origin or origin_url or origin_note: + # Get current origin first + try: + resp = requests.get(f"{get_server()}/cache/{content_hash}/meta", headers=headers) + resp.raise_for_status() + current = resp.json() + current_origin = current.get("origin", {}) + except: + current_origin = {} + + update["origin"] = { + "type": origin or current_origin.get("type", "self"), + "url": origin_url if origin_url is not None else current_origin.get("url"), + "note": origin_note if origin_note is not None else current_origin.get("note") + } + + if description is not None: + update["description"] = description + + if tags is not None: + update["tags"] = [t.strip() for t in tags.split(",") if t.strip()] + + if folder is not None: + update["folder"] = folder + + if add_collection or remove_collection: + # Get current collections + try: + resp = requests.get(f"{get_server()}/cache/{content_hash}/meta", headers=headers) + resp.raise_for_status() + current = resp.json() + collections = set(current.get("collections", [])) + except: + collections = set() + + if add_collection: + collections.add(add_collection) + if remove_collection and remove_collection in collections: + collections.remove(remove_collection) + update["collections"] = list(collections) + + # PATCH metadata + try: + resp = requests.patch( + f"{get_server()}/cache/{content_hash}/meta", + json=update, + headers=headers + ) + if resp.status_code == 404: + click.echo(f"Content not found: {content_hash}", err=True) + sys.exit(1) + if resp.status_code == 400: + click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True) + sys.exit(1) + resp.raise_for_status() + except requests.RequestException as e: + click.echo(f"Failed to update metadata: {e}", err=True) + sys.exit(1) + + click.echo("Metadata updated.") + + +# ============ Folder Commands ============ + +@cli.group() +def folder(): + """Manage folders for organizing cached items.""" + pass + + +@folder.command("list") +def folder_list(): + """List all folders.""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.get( + f"{get_server()}/user/folders", + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + resp.raise_for_status() + folders = resp.json()["folders"] + except requests.RequestException as e: + click.echo(f"Failed to list folders: {e}", err=True) + sys.exit(1) + + click.echo("Folders:") + for f in folders: + click.echo(f" {f}") + + +@folder.command("create") +@click.argument("path") +def folder_create(path): + """Create a new folder.""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.post( + f"{get_server()}/user/folders", + params={"folder_path": path}, + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + if resp.status_code == 400: + click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True) + sys.exit(1) + resp.raise_for_status() + except requests.RequestException as e: + click.echo(f"Failed to create folder: {e}", err=True) + sys.exit(1) + + click.echo(f"Created folder: {path}") + + +@folder.command("delete") +@click.argument("path") +def folder_delete(path): + """Delete a folder (must be empty).""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.delete( + f"{get_server()}/user/folders", + params={"folder_path": path}, + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + if resp.status_code == 400: + click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True) + sys.exit(1) + if resp.status_code == 404: + click.echo(f"Folder not found: {path}", err=True) + sys.exit(1) + resp.raise_for_status() + except requests.RequestException as e: + click.echo(f"Failed to delete folder: {e}", err=True) + sys.exit(1) + + click.echo(f"Deleted folder: {path}") + + +# ============ Collection Commands ============ + +@cli.group() +def collection(): + """Manage collections for organizing cached items.""" + pass + + +@collection.command("list") +def collection_list(): + """List all collections.""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.get( + f"{get_server()}/user/collections", + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + resp.raise_for_status() + collections = resp.json()["collections"] + except requests.RequestException as e: + click.echo(f"Failed to list collections: {e}", err=True) + sys.exit(1) + + click.echo("Collections:") + for c in collections: + click.echo(f" {c['name']} (created: {c['created_at'][:10]})") + + +@collection.command("create") +@click.argument("name") +def collection_create(name): + """Create a new collection.""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.post( + f"{get_server()}/user/collections", + params={"name": name}, + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + if resp.status_code == 400: + click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True) + sys.exit(1) + resp.raise_for_status() + except requests.RequestException as e: + click.echo(f"Failed to create collection: {e}", err=True) + sys.exit(1) + + click.echo(f"Created collection: {name}") + + +@collection.command("delete") +@click.argument("name") +def collection_delete(name): + """Delete a collection.""" + token_data = load_token() + if not token_data.get("access_token"): + click.echo("Not logged in. Please run: artdag login ", err=True) + sys.exit(1) + + try: + resp = requests.delete( + f"{get_server()}/user/collections", + params={"name": name}, + headers={"Authorization": f"Bearer {token_data['access_token']}"} + ) + if resp.status_code == 404: + click.echo(f"Collection not found: {name}", err=True) + sys.exit(1) + resp.raise_for_status() + except requests.RequestException as e: + click.echo(f"Failed to delete collection: {e}", err=True) + sys.exit(1) + + click.echo(f"Deleted collection: {name}") + + if __name__ == "__main__": cli()