Add config CLI commands
- Add PyYAML dependency - Add upload-config: upload config YAML files - Add configs: list uploaded configs - Add config: show config details - Add run-config: run config with variable inputs - Add delete-config: delete configs (with pinning check) Example workflow: artdag upload-config recipe.yaml artdag configs artdag run-config <config_id> -i node_id:content_hash --wait Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
215
artdag.py
215
artdag.py
@@ -13,6 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
import yaml
|
||||||
|
|
||||||
DEFAULT_SERVER = os.environ.get("ARTDAG_SERVER", "http://localhost:8100")
|
DEFAULT_SERVER = os.environ.get("ARTDAG_SERVER", "http://localhost:8100")
|
||||||
DEFAULT_L2_SERVER = os.environ.get("ARTDAG_L2", "http://localhost:8200")
|
DEFAULT_L2_SERVER = os.environ.get("ARTDAG_L2", "http://localhost:8200")
|
||||||
@@ -875,5 +876,219 @@ def collection_delete(name):
|
|||||||
click.echo(f"Deleted collection: {name}")
|
click.echo(f"Deleted collection: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Config Commands ============
|
||||||
|
|
||||||
|
@cli.command("upload-config")
|
||||||
|
@click.argument("filepath", type=click.Path(exists=True))
|
||||||
|
def upload_config(filepath):
|
||||||
|
"""Upload a config YAML file. Requires login."""
|
||||||
|
token_data = load_token()
|
||||||
|
if not token_data.get("access_token"):
|
||||||
|
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Validate YAML locally first
|
||||||
|
with open(filepath) as f:
|
||||||
|
try:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
click.echo(f"Invalid YAML: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
if not config.get("name"):
|
||||||
|
click.echo("Config must have a 'name' field", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
try:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
files = {"file": (Path(filepath).name, f)}
|
||||||
|
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||||
|
resp = requests.post(f"{get_server()}/configs/upload", files=files, headers=headers)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
click.echo("Authentication failed. Please login again.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
resp.raise_for_status()
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
click.echo(f"Uploaded config: {result['name']} v{result.get('version', '1.0')}")
|
||||||
|
click.echo(f"Config ID: {result['config_id']}")
|
||||||
|
click.echo(f"Variable inputs: {result['variable_inputs']}")
|
||||||
|
click.echo(f"Fixed inputs: {result['fixed_inputs']}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
click.echo(f"Upload failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("configs")
|
||||||
|
@click.option("--limit", "-l", default=10, help="Max configs to show")
|
||||||
|
def list_configs(limit):
|
||||||
|
"""List uploaded configs."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{get_server()}/configs")
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
click.echo(f"Failed to list configs: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
configs = data.get("configs", [])
|
||||||
|
if not configs:
|
||||||
|
click.echo("No configs found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(f"{'Name':<20} {'Version':<8} {'Variables':<10} {'Config ID':<24}")
|
||||||
|
click.echo("-" * 70)
|
||||||
|
|
||||||
|
for config in configs[:limit]:
|
||||||
|
config_id = config["config_id"][:20] + "..."
|
||||||
|
var_count = len(config.get("variable_inputs", []))
|
||||||
|
click.echo(f"{config['name']:<20} {config['version']:<8} {var_count:<10} {config_id}")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("config")
|
||||||
|
@click.argument("config_id")
|
||||||
|
def show_config(config_id):
|
||||||
|
"""Show details of a config."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{get_server()}/configs/{config_id}")
|
||||||
|
if resp.status_code == 404:
|
||||||
|
click.echo(f"Config not found: {config_id}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
resp.raise_for_status()
|
||||||
|
config = resp.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
click.echo(f"Failed to get config: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
click.echo(f"Name: {config['name']}")
|
||||||
|
click.echo(f"Version: {config['version']}")
|
||||||
|
click.echo(f"Description: {config.get('description', 'N/A')}")
|
||||||
|
click.echo(f"Config ID: {config['config_id']}")
|
||||||
|
click.echo(f"Owner: {config.get('owner', 'N/A')}")
|
||||||
|
click.echo(f"Uploaded: {config['uploaded_at']}")
|
||||||
|
|
||||||
|
if config.get("variable_inputs"):
|
||||||
|
click.echo("\nVariable Inputs:")
|
||||||
|
for inp in config["variable_inputs"]:
|
||||||
|
req = "*" if inp.get("required", True) else ""
|
||||||
|
click.echo(f" - {inp['name']}{req}: {inp.get('description', 'No description')}")
|
||||||
|
|
||||||
|
if config.get("fixed_inputs"):
|
||||||
|
click.echo("\nFixed Inputs:")
|
||||||
|
for inp in config["fixed_inputs"]:
|
||||||
|
click.echo(f" - {inp['asset']}: {inp['content_hash'][:16]}...")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("run-config")
|
||||||
|
@click.argument("config_id")
|
||||||
|
@click.option("--input", "-i", "inputs", multiple=True, help="Input as node_id:content_hash")
|
||||||
|
@click.option("--wait", "-w", is_flag=True, help="Wait for completion")
|
||||||
|
def run_config(config_id, inputs, wait):
|
||||||
|
"""Run a config with variable inputs. Requires login.
|
||||||
|
|
||||||
|
CONFIG_ID: The config ID (content hash)
|
||||||
|
|
||||||
|
Example: artdag run-config abc123 -i source_image:def456
|
||||||
|
"""
|
||||||
|
token_data = load_token()
|
||||||
|
if not token_data.get("access_token"):
|
||||||
|
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Parse inputs
|
||||||
|
input_dict = {}
|
||||||
|
for inp in inputs:
|
||||||
|
if ":" not in inp:
|
||||||
|
click.echo(f"Invalid input format: {inp} (expected node_id:content_hash)", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
node_id, content_hash = inp.split(":", 1)
|
||||||
|
input_dict[node_id] = content_hash
|
||||||
|
|
||||||
|
# Run
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||||
|
resp = requests.post(
|
||||||
|
f"{get_server()}/configs/{config_id}/run",
|
||||||
|
json={"inputs": input_dict},
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
click.echo("Authentication failed. Please login again.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
if resp.status_code == 400:
|
||||||
|
error = resp.json().get("detail", "Bad request")
|
||||||
|
click.echo(f"Error: {error}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
if resp.status_code == 404:
|
||||||
|
click.echo(f"Config not found: {config_id}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
resp.raise_for_status()
|
||||||
|
result = resp.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
click.echo(f"Run failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
click.echo(f"Run started: {result['run_id']}")
|
||||||
|
click.echo(f"Recipe: {result['recipe']}")
|
||||||
|
click.echo(f"Status: {result['status']}")
|
||||||
|
|
||||||
|
if wait:
|
||||||
|
click.echo("Waiting for completion...")
|
||||||
|
run_id = result["run_id"]
|
||||||
|
while True:
|
||||||
|
time.sleep(2)
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{get_server()}/runs/{run_id}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
run = resp.json()
|
||||||
|
except requests.RequestException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if run["status"] == "completed":
|
||||||
|
click.echo(f"Completed! Output: {run.get('output_hash', 'N/A')}")
|
||||||
|
break
|
||||||
|
elif run["status"] == "failed":
|
||||||
|
click.echo(f"Failed: {run.get('error', 'Unknown error')}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("delete-config")
|
||||||
|
@click.argument("config_id")
|
||||||
|
@click.option("--force", "-f", is_flag=True, help="Skip confirmation")
|
||||||
|
def delete_config(config_id, force):
|
||||||
|
"""Delete a config. Requires login."""
|
||||||
|
token_data = load_token()
|
||||||
|
if not token_data.get("access_token"):
|
||||||
|
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
if not click.confirm(f"Delete config {config_id[:16]}...?"):
|
||||||
|
click.echo("Cancelled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||||
|
resp = requests.delete(f"{get_server()}/configs/{config_id}", headers=headers)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
click.echo("Authentication failed. Please login again.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
if resp.status_code == 400:
|
||||||
|
error = resp.json().get("detail", "Cannot delete")
|
||||||
|
click.echo(f"Error: {error}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
if resp.status_code == 404:
|
||||||
|
click.echo(f"Config not found: {config_id}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
click.echo(f"Delete failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
click.echo(f"Deleted config: {config_id[:16]}...")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
click>=8.0.0
|
click>=8.0.0
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
|
PyYAML>=6.0
|
||||||
|
|||||||
Reference in New Issue
Block a user