From ff5da0af90ea50dca8930ba5c0d0d9f663e5fc81 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Mon, 4 May 2026 18:39:05 +0200 Subject: [PATCH] feat(cli): agnes admin metrics {import,export,validate} --- cli/commands/admin.py | 2 + cli/commands/admin_metrics.py | 89 +++++++++++++++++++++++++++++++++ tests/test_cli_admin_metrics.py | 14 ++++++ 3 files changed, 105 insertions(+) create mode 100644 cli/commands/admin_metrics.py create mode 100644 tests/test_cli_admin_metrics.py diff --git a/cli/commands/admin.py b/cli/commands/admin.py index 5ffdaed..66ee0fb 100644 --- a/cli/commands/admin.py +++ b/cli/commands/admin.py @@ -5,9 +5,11 @@ import json import typer from cli.client import api_get, api_post, api_delete, api_patch +from cli.commands.admin_metrics import admin_metrics_app from cli.commands.memory_admin import memory_admin_app admin_app = typer.Typer(help="Admin operations (requires admin role)") +admin_app.add_typer(admin_metrics_app, name="metrics") admin_app.add_typer(memory_admin_app, name="memory") diff --git a/cli/commands/admin_metrics.py b/cli/commands/admin_metrics.py new file mode 100644 index 0000000..62ff641 --- /dev/null +++ b/cli/commands/admin_metrics.py @@ -0,0 +1,89 @@ +"""`agnes admin metrics {import,export,validate}` — lifted from cli/commands/metrics.py. + +Write paths to metric definitions live under `admin` because they mutate the +server-side metric registry (DuckDB direct, no API). Read paths (list/show) +live in `agnes catalog --metrics`. +""" + +from pathlib import Path + +import typer + +admin_metrics_app = typer.Typer(help="Admin: metric definition management") + + +@admin_metrics_app.command("import") +def import_metrics( + path: str = typer.Argument(..., help="Path to a YAML file or directory of YAML files"), +): + """Import metric definitions from YAML into DuckDB (direct, no API).""" + from src.db import get_system_db + from src.repositories.metrics import MetricRepository + + import_path = Path(path) + if not import_path.exists(): + typer.echo(f"Path not found: {path}", err=True) + raise typer.Exit(1) + + conn = get_system_db() + try: + repo = MetricRepository(conn) + count = repo.import_from_yaml(import_path) + typer.echo(f"Imported {count} metric(s) from {path}") + finally: + conn.close() + + +@admin_metrics_app.command("export") +def export_metrics( + output_dir: str = typer.Option("./export/", "--dir", help="Output directory for YAML files"), +): + """Export metric definitions from DuckDB to YAML files (direct, no API).""" + from src.db import get_system_db + from src.repositories.metrics import MetricRepository + + conn = get_system_db() + try: + repo = MetricRepository(conn) + count = repo.export_to_yaml(output_dir) + typer.echo(f"Exported {count} metric(s) to {output_dir}") + finally: + conn.close() + + +@admin_metrics_app.command("validate") +def validate_metrics(): + """Check each metric's table reference against registered tables (direct, no API).""" + from src.db import get_system_db + from src.repositories.metrics import MetricRepository + from src.repositories.table_registry import TableRegistryRepository + + conn = get_system_db() + try: + metric_repo = MetricRepository(conn) + registry_repo = TableRegistryRepository(conn) + + metrics = metric_repo.list() + registered_tables = {t["name"] for t in registry_repo.list_all()} + + ok_count = 0 + warn_count = 0 + + for m in metrics: + name = m.get("name", m.get("id", "?")) + table = m.get("table_name") + if not table: + typer.echo(f" OK {name:30s} (no table reference)") + ok_count += 1 + elif table in registered_tables: + typer.echo(f" OK {name:30s} table={table}") + ok_count += 1 + else: + typer.echo(f" WARN {name:30s} table={table} (not registered)") + warn_count += 1 + + typer.echo(f"\nTotal: {len(metrics)} metric(s) — {ok_count} OK, {warn_count} WARN") + if warn_count > 0: + raise typer.Exit(1) + finally: + conn.close() diff --git a/tests/test_cli_admin_metrics.py b/tests/test_cli_admin_metrics.py new file mode 100644 index 0000000..94ada2a --- /dev/null +++ b/tests/test_cli_admin_metrics.py @@ -0,0 +1,14 @@ +"""Tests for `agnes admin metrics {import,export,validate}` (lifted from `da metrics`).""" + +from typer.testing import CliRunner + +from cli.commands.admin import admin_app + + +def test_admin_metrics_subcommands_present(): + runner = CliRunner() + result = runner.invoke(admin_app, ["metrics", "--help"]) + assert result.exit_code == 0 + assert "import" in result.output + assert "export" in result.output + assert "validate" in result.output