From 04fa1402e4b46f63c34c91bc8cab9eda72e6aaf6 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Tue, 31 Mar 2026 12:55:03 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20CLI=20admin=20commands=20=E2=80=94=20re?= =?UTF-8?q?gister-table,=20discover-and-register,=20list-tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit da admin register-table: register single table da admin discover-and-register: auto-discover from Keboola API + bulk register da admin list-tables: show all registered tables Used to register all 142 Keboola tables on production. --- cli/commands/admin.py | 111 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/cli/commands/admin.py b/cli/commands/admin.py index c467634..80d205d 100644 --- a/cli/commands/admin.py +++ b/cli/commands/admin.py @@ -50,3 +50,114 @@ def remove_user(user_id: str = typer.Argument(..., help="User ID to remove")): else: typer.echo(f"Failed: {resp.text}", err=True) raise typer.Exit(1) + + +@admin_app.command("register-table") +def register_table( + name: str = typer.Argument(..., help="Table display name"), + source_type: str = typer.Option("keboola", help="Source type"), + bucket: str = typer.Option("", help="Source bucket/dataset"), + source_table: str = typer.Option("", help="Source table name"), + query_mode: str = typer.Option("local", help="Query mode: local or remote"), + description: str = typer.Option("", help="Table description"), +): + """Register a single table.""" + resp = api_post("/api/admin/register-table", json={ + "name": name, + "source_type": source_type, + "bucket": bucket, + "source_table": source_table or name, + "query_mode": query_mode, + "description": description, + }) + if resp.status_code == 201: + typer.echo(f"Registered: {name}") + elif resp.status_code == 409: + typer.echo(f"Already exists: {name}") + else: + typer.echo(f"Failed: {resp.json().get('detail', resp.text)}", err=True) + raise typer.Exit(1) + + +@admin_app.command("discover-and-register") +def discover_and_register( + source_type: str = typer.Option("keboola", help="Source type"), + token: str = typer.Option(None, help="Keboola Storage API token"), + url: str = typer.Option(None, help="Keboola stack URL"), + dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be registered"), + as_json: bool = typer.Option(False, "--json", help="Output as JSON"), +): + """Discover all tables from source and register them.""" + import httpx + import os + + kbc_token = token or os.environ.get("KEBOOLA_STORAGE_TOKEN", "") + kbc_url = url or os.environ.get("KEBOOLA_STACK_URL", "") + + if not kbc_token or not kbc_url: + typer.echo("Need KEBOOLA_STORAGE_TOKEN and KEBOOLA_STACK_URL (env or --token/--url)", err=True) + raise typer.Exit(1) + + typer.echo(f"Discovering tables from {kbc_url}...") + resp = httpx.get(f"{kbc_url.rstrip('/')}/v2/storage/tables", + headers={"X-StorageApi-Token": kbc_token}, timeout=30) + resp.raise_for_status() + tables = resp.json() + typer.echo(f"Found {len(tables)} tables") + + if as_json and dry_run: + typer.echo(json.dumps([{"id": t["id"], "name": t["name"], + "bucket": t.get("bucket", {}).get("id", ""), + "rows": t.get("rowsCount", 0)} for t in tables], indent=2)) + return + + registered = 0 + skipped = 0 + errors = 0 + + for t in tables: + table_id = t["id"] + name = t["name"] + bucket_id = t.get("bucket", {}).get("id", "") + + if dry_run: + typer.echo(f" [DRY RUN] {name:30s} bucket={bucket_id:20s} rows={t.get('rowsCount', 0):>10,}") + continue + + resp = api_post("/api/admin/register-table", json={ + "name": name, + "source_type": source_type, + "bucket": bucket_id, + "source_table": name, + "query_mode": "local", + "description": f"Auto-discovered from {source_type}", + }) + + if resp.status_code == 201: + registered += 1 + typer.echo(f" ✓ {name}") + elif resp.status_code == 409: + skipped += 1 + else: + errors += 1 + typer.echo(f" ✗ {name}: {resp.json().get('detail', resp.text)}") + + if not dry_run: + typer.echo(f"\nDone: {registered} registered, {skipped} already existed, {errors} errors") + + +@admin_app.command("list-tables") +def list_tables(as_json: bool = typer.Option(False, "--json")): + """List registered tables.""" + resp = api_get("/api/admin/registry") + if resp.status_code != 200: + typer.echo(f"Failed: {resp.text}", err=True) + raise typer.Exit(1) + + data = resp.json() + if as_json: + typer.echo(json.dumps(data, indent=2)) + else: + typer.echo(f"Registered tables: {data['count']}") + for t in data["tables"]: + typer.echo(f" {t['name']:30s} src={t.get('source_type','?'):10s} mode={t.get('query_mode','?'):6s} bucket={t.get('bucket',''):20s}")