feat: Art DAG CLI client
- artdag.py CLI for L1 server interaction - run: start rendering jobs - runs: list all runs - status: check run status - cache/view: manage cached content (pipe to mpv with -o -) - assets: list known assets - import: add files to cache 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Art DAG Client
|
||||
|
||||
CLI for interacting with the Art DAG L1 rendering server.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Set server URL (default: http://localhost:8100)
|
||||
export ARTDAG_SERVER=http://localhost:8100
|
||||
|
||||
# Or pass with every command
|
||||
./artdag.py --server http://localhost:8100 <command>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Server Info
|
||||
```bash
|
||||
./artdag.py info
|
||||
```
|
||||
|
||||
### List Known Assets
|
||||
```bash
|
||||
./artdag.py assets
|
||||
```
|
||||
|
||||
### Start a Rendering Run
|
||||
```bash
|
||||
# Using asset name
|
||||
./artdag.py run dog cat
|
||||
|
||||
# Using content hash
|
||||
./artdag.py run dog 33268b6e167deaf018cc538de12dbe562612b33e89a749391cef855b320a269b
|
||||
|
||||
# Wait for completion
|
||||
./artdag.py run dog cat --wait
|
||||
|
||||
# Custom output name
|
||||
./artdag.py run dog cat --name my-dog-video
|
||||
```
|
||||
|
||||
### List Runs
|
||||
```bash
|
||||
./artdag.py runs
|
||||
./artdag.py runs --limit 20
|
||||
```
|
||||
|
||||
### Check Run Status
|
||||
```bash
|
||||
./artdag.py status <run-id>
|
||||
```
|
||||
|
||||
### List Cached Content
|
||||
```bash
|
||||
./artdag.py cache
|
||||
```
|
||||
|
||||
### View/Download Cached Content
|
||||
```bash
|
||||
# Show info
|
||||
./artdag.py view <content-hash>
|
||||
|
||||
# Download to file
|
||||
./artdag.py view <content-hash> -o output.mkv
|
||||
|
||||
# Pipe to mpv (use -o - for stdout)
|
||||
./artdag.py view <content-hash> -o - | mpv -
|
||||
```
|
||||
|
||||
### Import Local File to Cache
|
||||
```bash
|
||||
./artdag.py import /path/to/file.jpg
|
||||
```
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# Check server
|
||||
./artdag.py info
|
||||
|
||||
# See available assets
|
||||
./artdag.py assets
|
||||
|
||||
# Run dog effect on cat, wait for result
|
||||
./artdag.py run dog cat --wait
|
||||
|
||||
# List completed runs
|
||||
./artdag.py runs
|
||||
|
||||
# Download the output
|
||||
./artdag.py view <output-hash> -o result.mkv
|
||||
```
|
||||
233
artdag.py
Executable file
233
artdag.py
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Art DAG Client
|
||||
|
||||
CLI for interacting with the Art DAG L1 server.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
DEFAULT_SERVER = os.environ.get("ARTDAG_SERVER", "http://localhost:8100")
|
||||
|
||||
|
||||
def get_server():
|
||||
"""Get server URL from env or default."""
|
||||
return DEFAULT_SERVER
|
||||
|
||||
|
||||
def api_get(path: str):
|
||||
"""GET request to server."""
|
||||
resp = requests.get(f"{get_server()}{path}")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def api_post(path: str, data: dict = None, params: dict = None):
|
||||
"""POST request to server."""
|
||||
resp = requests.post(f"{get_server()}{path}", json=data, params=params)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("--server", "-s", envvar="ARTDAG_SERVER", default=DEFAULT_SERVER,
|
||||
help="L1 server URL")
|
||||
@click.pass_context
|
||||
def cli(ctx, server):
|
||||
"""Art DAG Client - interact with L1 rendering server."""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["server"] = server
|
||||
global DEFAULT_SERVER
|
||||
DEFAULT_SERVER = server
|
||||
|
||||
|
||||
@cli.command()
|
||||
def info():
|
||||
"""Show server info."""
|
||||
data = api_get("/")
|
||||
click.echo(f"Server: {get_server()}")
|
||||
click.echo(f"Name: {data['name']}")
|
||||
click.echo(f"Version: {data['version']}")
|
||||
click.echo(f"Cache: {data['cache_dir']}")
|
||||
click.echo(f"Runs: {data['runs_count']}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("recipe")
|
||||
@click.argument("input_hash")
|
||||
@click.option("--name", "-n", help="Output name")
|
||||
@click.option("--wait", "-w", is_flag=True, help="Wait for completion")
|
||||
def run(recipe, input_hash, name, wait):
|
||||
"""Start a rendering run.
|
||||
|
||||
RECIPE: Effect/recipe to apply (e.g., dog, identity)
|
||||
INPUT_HASH: Content hash of input asset
|
||||
"""
|
||||
# Resolve named assets
|
||||
assets = api_get("/assets")
|
||||
if input_hash in assets:
|
||||
input_hash = assets[input_hash]
|
||||
click.echo(f"Resolved input to: {input_hash[:16]}...")
|
||||
|
||||
data = {
|
||||
"recipe": recipe,
|
||||
"inputs": [input_hash],
|
||||
}
|
||||
if name:
|
||||
data["output_name"] = name
|
||||
|
||||
result = api_post("/runs", data)
|
||||
run_id = result["run_id"]
|
||||
|
||||
click.echo(f"Run started: {run_id}")
|
||||
click.echo(f"Status: {result['status']}")
|
||||
|
||||
if wait:
|
||||
click.echo("Waiting for completion...")
|
||||
while True:
|
||||
status = api_get(f"/runs/{run_id}")
|
||||
if status["status"] in ("completed", "failed"):
|
||||
break
|
||||
time.sleep(1)
|
||||
click.echo(".", nl=False)
|
||||
click.echo()
|
||||
|
||||
if status["status"] == "completed":
|
||||
click.echo(f"Completed!")
|
||||
click.echo(f"Output: {status['output_hash']}")
|
||||
else:
|
||||
click.echo(f"Failed: {status.get('error', 'Unknown error')}")
|
||||
|
||||
|
||||
@cli.command("runs")
|
||||
@click.option("--limit", "-l", default=10, help="Max runs to show")
|
||||
def list_runs(limit):
|
||||
"""List all runs."""
|
||||
runs = api_get("/runs")
|
||||
|
||||
if not runs:
|
||||
click.echo("No runs found.")
|
||||
return
|
||||
|
||||
click.echo(f"{'ID':<36} {'Status':<10} {'Recipe':<10} {'Output Hash':<20}")
|
||||
click.echo("-" * 80)
|
||||
|
||||
for run in runs[:limit]:
|
||||
output = run.get("output_hash", "")[:16] + "..." if run.get("output_hash") else "-"
|
||||
click.echo(f"{run['run_id']} {run['status']:<10} {run['recipe']:<10} {output}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("run_id")
|
||||
def status(run_id):
|
||||
"""Get status of a run."""
|
||||
try:
|
||||
run = api_get(f"/runs/{run_id}")
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
click.echo(f"Run not found: {run_id}")
|
||||
return
|
||||
raise
|
||||
|
||||
click.echo(f"Run ID: {run['run_id']}")
|
||||
click.echo(f"Status: {run['status']}")
|
||||
click.echo(f"Recipe: {run['recipe']}")
|
||||
click.echo(f"Inputs: {', '.join(run['inputs'])}")
|
||||
click.echo(f"Output Name: {run['output_name']}")
|
||||
click.echo(f"Created: {run['created_at']}")
|
||||
|
||||
if run.get("completed_at"):
|
||||
click.echo(f"Completed: {run['completed_at']}")
|
||||
|
||||
if run.get("output_hash"):
|
||||
click.echo(f"Output Hash: {run['output_hash']}")
|
||||
|
||||
if run.get("error"):
|
||||
click.echo(f"Error: {run['error']}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--limit", "-l", default=20, help="Max items to show")
|
||||
def cache(limit):
|
||||
"""List cached content."""
|
||||
items = api_get("/cache")
|
||||
|
||||
if not items:
|
||||
click.echo("Cache is empty.")
|
||||
return
|
||||
|
||||
click.echo(f"Cached content ({len(items)} items):")
|
||||
for item in items[:limit]:
|
||||
click.echo(f" {item}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("content_hash")
|
||||
@click.option("--output", "-o", type=click.Path(), help="Save to file (use - for stdout)")
|
||||
def view(content_hash, output):
|
||||
"""View or download cached content.
|
||||
|
||||
Use -o - to pipe to stdout, e.g.: artdag view <hash> -o - | mpv -
|
||||
"""
|
||||
url = f"{get_server()}/cache/{content_hash}"
|
||||
|
||||
try:
|
||||
if output == "-":
|
||||
# Stream to stdout for piping
|
||||
resp = requests.get(url, stream=True)
|
||||
resp.raise_for_status()
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
sys.stdout.buffer.write(chunk)
|
||||
elif output:
|
||||
# Download to file
|
||||
resp = requests.get(url, stream=True)
|
||||
resp.raise_for_status()
|
||||
with open(output, "wb") as f:
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
click.echo(f"Saved to: {output}", err=True)
|
||||
else:
|
||||
# Get info via GET with stream to check size
|
||||
resp = requests.get(url, stream=True)
|
||||
resp.raise_for_status()
|
||||
size = resp.headers.get("content-length", "unknown")
|
||||
content_type = resp.headers.get("content-type", "unknown")
|
||||
click.echo(f"Hash: {content_hash}")
|
||||
click.echo(f"Size: {size} bytes")
|
||||
click.echo(f"Type: {content_type}")
|
||||
click.echo(f"URL: {url}")
|
||||
resp.close()
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
click.echo(f"Not found: {content_hash}", err=True)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
@cli.command("import")
|
||||
@click.argument("filepath", type=click.Path(exists=True))
|
||||
def import_file(filepath):
|
||||
"""Import a local file to cache."""
|
||||
path = str(Path(filepath).resolve())
|
||||
result = api_post("/cache/import", params={"path": path})
|
||||
click.echo(f"Imported: {result['content_hash']}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
def assets():
|
||||
"""List known assets."""
|
||||
data = api_get("/assets")
|
||||
click.echo("Known assets:")
|
||||
for name, hash in data.items():
|
||||
click.echo(f" {name}: {hash[:16]}...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
click>=8.0.0
|
||||
requests>=2.31.0
|
||||
Reference in New Issue
Block a user