"""CLI entry point: python -m alteryx_runner run workflow.yxmd [options]""" from __future__ import annotations import sys from pathlib import Path import click import polars as pl @click.group() def main(): """Alteryx workflow runner — execute .yxmd files without Alteryx.""" @main.command() @click.argument("workflow", type=click.Path(exists=True, path_type=Path)) @click.option("--output-dir", default=None, type=click.Path(path_type=Path), help="Write output files to this directory.") @click.option("--param", multiple=True, metavar="KEY=VALUE", help="Set workflow constant (repeatable).") @click.option("--verbose", is_flag=True, default=False, help="Print Browse results and execution log.") @click.option("--dry-run", is_flag=True, default=False, help="Parse and validate only; do not execute.") @click.option("--format", "fmt", type=click.Choice(["json", "csv", "parquet"]), default="csv", help="Default output format for Browse nodes.") def run( workflow: Path, output_dir: Path | None, param: tuple[str, ...], verbose: bool, dry_run: bool, fmt: str, ) -> None: """Execute WORKFLOW (.yxmd file).""" # Import here so CLI loads fast even if deps are missing from engine.parser import parse_workflow from engine.executor import execute from engine.context import RunContext params: dict[str, str] = {} for p in param: if "=" in p: k, v = p.split("=", 1) params[k.strip()] = v.strip() else: click.echo(f"Warning: --param {p!r} ignored (no '=' found)", err=True) click.echo(f"Parsing {workflow} …") try: graph = parse_workflow(str(workflow)) except Exception as e: click.echo(f"Parse error: {e}", err=True) sys.exit(1) click.echo( f" {len(graph.nodes)} nodes, {len(graph.connections)} connections" ) if dry_run: click.echo("Dry run complete — no execution.") return ctx = RunContext( workflow_dir=str(workflow.parent), verbose=verbose, output_dir=str(output_dir) if output_dir else None, params=params, ) click.echo("Executing …") try: outputs = execute(graph, ctx) except Exception as e: click.echo(f"Execution error: {e}", err=True) if verbose: import traceback traceback.print_exc() sys.exit(1) n_frames = sum(1 for df in outputs.values() if isinstance(df, pl.DataFrame) and len(df) > 0) click.echo(f"Done. {n_frames} non-empty output frames produced.") @main.command("list-tools") def list_tools() -> None: """List all registered tool Plugin strings.""" from tools import _REGISTRY for plugin, cls in sorted(_REGISTRY.items()): click.echo(f" {plugin:<70} → {cls.__name__}") if __name__ == "__main__": main()