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).
104 lines
3.5 KiB
Python
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))
|