agnes-the-ai-analyst/cli/commands/explore.py

104 lines
3.5 KiB
Python

"""Explore commands — agnes explore {table}."""
import json
import os
from pathlib import Path
import typer
explore_app = typer.Typer(help="Explore data tables")
@explore_app.callback(invoke_without_command=True)
def explore(
table: str = typer.Argument(..., help="Table name to explore"),
remote: bool = typer.Option(False, "--remote", help="Fetch from server"),
as_json: bool = typer.Option(False, "--json", help="Output as JSON"),
):
"""Show profile and sample data for a table."""
if remote:
_explore_remote(table, as_json)
else:
_explore_local(table, as_json)
def _explore_local(table: str, as_json: bool):
import duckdb
local_dir = Path(os.environ.get("AGNES_LOCAL_DIR", "."))
db_path = local_dir / "user" / "duckdb" / "analytics.duckdb"
if not db_path.exists():
typer.echo("Local DuckDB not found. Run: agnes pull", err=True)
raise typer.Exit(1)
conn = duckdb.connect(str(db_path), read_only=True)
try:
# Check table exists
tables = [r[0] for r in conn.execute(
"SELECT table_name FROM information_schema.tables WHERE table_name = ?", [table]
).fetchall()]
if not tables:
# Also check views
tables = [r[0] for r in conn.execute(
"SELECT table_name FROM information_schema.tables WHERE table_name = ? AND table_type='VIEW'", [table]
).fetchall()]
if not tables:
typer.echo(f"Table '{table}' not found. Available:", err=True)
for r in conn.execute("SELECT table_name FROM information_schema.tables ORDER BY table_name").fetchall():
typer.echo(f" {r[0]}")
raise typer.Exit(1)
# Row count
count = conn.execute(f'SELECT count(*) FROM "{table}"').fetchone()[0]
# Column info
columns = conn.execute(f"DESCRIBE \"{table}\"").fetchall()
col_info = [{"name": c[0], "type": c[1], "nullable": c[2]} for c in columns]
# Sample rows
sample = conn.execute(f'SELECT * FROM "{table}" LIMIT 5').fetchall()
sample_cols = [desc[0] for desc in conn.description]
info = {
"table": table,
"row_count": count,
"columns": col_info,
"sample_rows": [dict(zip(sample_cols, row)) for row in sample],
}
if as_json:
typer.echo(json.dumps(info, indent=2, default=str))
else:
typer.echo(f"Table: {table}")
typer.echo(f"Rows: {count:,}")
typer.echo(f"Columns ({len(col_info)}):")
for c in col_info:
typer.echo(f" {c['name']:30s} {c['type']}")
typer.echo(f"\nSample ({min(5, count)} rows):")
from rich.console import Console
from rich.table import Table
console = Console()
t = Table()
for c in sample_cols:
t.add_column(c)
for row in sample:
t.add_row(*(str(v) if v is not None else "" for v in row))
console.print(t)
finally:
conn.close()
def _explore_remote(table: str, as_json: bool):
from cli.client import api_get
resp = api_get(f"/api/catalog/profile/{table}")
if resp.status_code != 200:
typer.echo(f"Profile not found: {resp.json().get('detail', resp.text)}", err=True)
raise typer.Exit(1)
if as_json:
typer.echo(json.dumps(resp.json(), indent=2))
else:
profile = resp.json()
typer.echo(f"Table: {table}")
typer.echo(json.dumps(profile, indent=2, default=str))