agnes-the-ai-analyst/cli/commands/explore.py
ZdenekSrotyr 1563b05f2e refactor(cli): hard-cutover env vars + config dir to AGNES_*
Task 0.5 of clean-analyst-bootstrap. Greenfield rewrite — no fallback,
no aliases. Existing dev environments lose their cached PAT and must
re-authenticate.

Env var renames (hard cutover):
- DA_CONFIG_DIR    -> AGNES_CONFIG_DIR
- DA_SERVER        -> AGNES_SERVER
- DA_SERVER_URL    -> AGNES_SERVER_URL  (test-only stale ref, not in spec)
- DA_NO_UPDATE_CHECK -> AGNES_NO_UPDATE_CHECK
- DA_LOCAL_DIR     -> AGNES_LOCAL_DIR
- DA_TOKEN         -> AGNES_TOKEN
- DA_STREAM_RETRIES -> AGNES_STREAM_RETRIES

Config dir rename: ~/.config/da/ -> ~/.config/agnes/ (across code,
comments, docstrings, error messages, install templates, dev scripts).

Stale `da X` references in CLI source (and adjacent app/, tests/):
swept docstrings, comments, help text, and error messages where the
verb survives the rewrite (init, pull, push, catalog, status, diagnose,
auth, admin, skills, query, schema, describe, explore, disk-info,
snapshot, login, logout, whoami, server, setup) and replaced `da X`
with `agnes X`. Intentionally kept `da sync`, `da fetch`, `da analyst`,
`da metrics` — those verbs are removed in later tasks; the legacy
strings will be detected by `_LEGACY_STRINGS` (added in Task 2).

Test fixes:
- TestCLIVersion now asserts output starts with `agnes ` (was `da `).

Test results: 2675 passed, 25 skipped (full pytest run, excluding 9
pre-existing test_db.py / test_user_management.py / test_e2e_extract.py
/ test_cli_binary_rename.py failures unrelated to this rename).
2026-05-04 16:35:44 +02:00

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: da sync", 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))