Files
fn_registry/apps/auto_metabase/app.md
T
egutierrez 310b409ae0 feat(auto_metabase): push-all + describe/sql + auto-inject de dashcards
- push_all(): pushea todos los YAMLs de un proyecto (cards primero,
  dashboards despues), solo CREATE/UPDATE, resiliente a fallos por item
- explore.py: comandos describe (schema de DB) y sql (query ad-hoc con
  limite, cap 5MB, bloqueo de escrituras destructivas)
- payload.py: auto-inyecta id:-N, visualization_settings:{} y
  parameter_mappings:[] en dashcards nuevas para evitar 500 en push
- test_local: 11 cards + 3 dashboards sobre Sample Database de Metabase
- registry.db regenerado con auto_metabase_py_analytics indexada

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:14:05 +02:00

14 KiB

name, lang, domain, description, tags, uses_functions, uses_types, framework, entry_point, dir_path
name lang domain description tags uses_functions uses_types framework entry_point dir_path
auto_metabase py analytics Sincronizacion bidireccional entre archivos YAML locales y una instancia Metabase. Cada dashboard, card, database, collection y document es un archivo editable; pull/push mantiene Metabase y disco en sintonia. Inspirado en rapid_dashboards.
metabase
sync
declarative
yaml
dashboards
gitops
metabase_auth_py_infra
metabase_list_databases_py_infra
metabase_get_database_py_infra
metabase_add_database_py_infra
metabase_list_dashboards_py_infra
metabase_get_dashboard_py_infra
metabase_create_dashboard_py_infra
metabase_update_dashboard_py_infra
metabase_delete_dashboard_py_infra
metabase_list_cards_py_infra
metabase_get_card_py_infra
metabase_create_card_py_infra
metabase_update_card_py_infra
metabase_delete_card_py_infra
metabase_execute_query_py_infra
httpx main.py apps/auto_metabase

Idea

Cada artefacto de Metabase (dashboard, card, database, collection, document) vive como un archivo YAML editable. Los archivos son la fuente de verdad.

Multi-proyecto: cada entorno Metabase (local, prod, staging, ...) es un proyecto aislado bajo projects/{name}/ con su propio config.yaml, .env, state/ y carpetas de YAMLs. El proyecto activo se elige con -p NAME o por defecto desde config.yaml top-level (default_project).

Comandos

Comando Categoria Que hace
projects proyectos Lista proyectos disponibles y marca el default
init-project NAME --base-url URL proyectos Crea proyecto nuevo (config + carpetas + index vacio)
login sesion Autentica contra Metabase y guarda token en state/session.json
status sesion Muestra estado del proyecto (sesion, items en index, archivos en disco)
describe <db> exploracion Lista tablas, columnas y tipos de un database (--samples para 3 filas demo)
sql <db> "QUERY" exploracion Ejecuta SQL ad-hoc, NO crea card. --limit 100 por defecto
remote <kind> exploracion Lista items en Metabase sin descargar nada
pull <kind> <ref> sync Trae UN item de Metabase a disco (per-item, nunca bulk)
validate <kind> <slug> sync Valida YAML local sin tocar Metabase. --check-sql ejecuta la query
push <kind> <slug> sync Aplica UN item a Metabase. Dry-run por defecto. --apply para enviar
push-all sync Pushea TODOS los YAMLs del proyecto. Solo CREATE/UPDATE, nunca DELETE
restore <kind> <slug> sync Restaura YAML local desde backup (no aplica a Metabase)
diff <kind> <slug> sync Alias temporal de validate --show-payload

Layout

apps/auto_metabase/
  config.yaml                       # default_project + projects_dir
  main.py                           # CLI entrypoint
  payload.py                        # builders: YAML -> payload Metabase (resuelve refs, auto-inyecta)
  sync_pull.py                      # pull per-item desde Metabase
  sync_push.py                      # push per-item + push_all (R1-R20)
  sync_validate.py                  # validacion estructural + SQL check
  sync_restore.py                   # restore desde backups locales
  explore.py                        # describe + sql ad-hoc
  scripts/seed_test_data.py         # seed inicial de db+cards+dashboard de prueba
  projects/
    {name}/                         # un directorio por entorno Metabase
      config.yaml                   # base_url + auth + sync rules
      .env                          # credenciales (gitignored)
      state/
        session.json                # token de sesion (gitignored)
        index.json                  # slug local <-> metabase_id por tipo
        push.log                    # JSONL append-only de cada push
        backups/{ts}/{kind}/...     # backup automatico antes de UPDATE
      databases/{slug}.yaml         # connections
      collections/{slug}.yaml       # collections (carpetas)
      cards/{slug}.yaml             # cards reusables
      dashboards/{slug}.yaml        # dashboards con dashcards
      documents/{slug}.yaml         # [pendiente] Metabase >= v0.51

Configuracion

Top-level config.yaml (en la raiz del app) selecciona el proyecto por defecto:

default_project: test_local
projects_dir: projects

Por proyecto projects/{name}/config.yaml define la URL y reglas de sync:

name: test_local
description: "Metabase de prueba en Docker local"
base_url: http://localhost:3000
auth:
  email_env: METABASE_EMAIL
  password_env: METABASE_PASSWORD
sync:
  ignore_collections: []   # IDs de colecciones a no sincronizar
  ignore_databases: [1]    # 1 = "Sample Database" interno de Metabase
  prefer_archive: true     # archivar en vez de borrar al hacer push

Credenciales en projects/{name}/.env (gitignored):

METABASE_EMAIL=admin@example.com
METABASE_PASSWORD=changeme

Estado

state/index.json mantiene el mapeo slug local <-> id de Metabase para no duplicar al hacer push. Estructura:

{
  "databases":   { "registry": 2, "ops_demo": 3 },
  "collections": { "auto_metabase": 5 },
  "cards":       { "totals_by_domain": 12 },
  "dashboards":  { "fn_overview": 4 }
}

Pendiente

  • Soporte de Metabase Documents (a investigar — endpoint nuevo en versiones recientes).
  • Soporte de Pulses / Alerts / Subscriptions.
  • Soporte de Permissions (groups + permission_graph).
  • Diff con render colorizado en TUI.
  • Recovery automatico cuando un CREATE de dashboard falla a medias (POST ok + PUT fail deja un dashboard huerfano sin entrar al index).

Crear cards y dashboards desde cero (workflow probado)

1. Referenciar una database que no se sincroniza

Si quieres apuntar cards a la Sample Database de Metabase (db 1, ignore_databases: [1] por defecto), basta con anadir el slug al state/index.json — no hace falta YAML en databases/:

"databases": {
  "metabase_internal_pg": 2,
  "sample_database": 1
}

Las cards luego usan _refs.database: sample_database y dataset_query.database: sample_database, y el builder lo resuelve a 1 via index.

2. Formato del dataset_query (MBQL v2 con SQL nativo)

Metabase moderno usa MBQL stages, no la forma vieja {type: native, native: {query: ...}}. La forma correcta para cards SQL:

dataset_query:
  lib/type: mbql/query
  database: <slug_o_id>
  stages:
    - lib/type: mbql.stage/native
      native: |-
        SELECT ...

query_type: native se mantiene en el payload top-level (Metabase lo usa internamente).

3. SQL del Sample Database = H2

Tablas en MAYUSCULAS (PEOPLE, ORDERS, PRODUCTS...) y funciones H2:

  • FORMATDATETIME(col, 'yyyy-MM') para truncar fechas (no DATE_TRUNC).
  • ROUND(SUM(x), 2) igual que en Postgres.
  • Joins y agregaciones SQL-92 estandar.

4. Crear cards nuevas

YAML minimo de una card nueva:

_meta:
  kind: card
  id: null              # null = nueva, push hara CREATE
  slug: clientes_total  # debe coincidir con el filename
_refs:
  database: sample_database
  collection: null
payload:
  name: clientes_total
  description: ...
  type: question
  query_type: native
  display: scalar       # o table, bar, line, pie, area, ...
  archived: false
  enable_embedding: false
  collection_preview: true
  visualization_settings: {}
  parameters: []
  parameter_mappings: []
  dataset_query:
    lib/type: mbql/query
    database: sample_database
    stages:
      - lib/type: mbql.stage/native
        native: SELECT COUNT(*) AS total FROM PEOPLE

Validar antes de pushear:

python main.py validate card clientes_total --check-sql

--check-sql ejecuta la query contra Metabase (solo para cards native). Pillar errores de sintaxis aqui es mas barato que en push.

Push:

python main.py push card clientes_total --apply

Sin --apply es dry-run. Tras un CREATE exitoso:

  • Se asigna id real, se actualiza state/index.json.
  • Se hace pull automatico → el YAML se reescribe con _meta.id, synced_at, remote_updated_at.
  • Hay que pushear las cards UNA POR UNA (no hay batch).

5. Crear dashboards nuevos

Las cards referenciadas deben existir antes (ya en el index). Estructura minima del YAML:

_meta:
  kind: dashboard
  id: null
  slug: compras_y_clientes
_refs:
  collection: null
payload:
  name: Compras y Clientes
  description: ...
  archived: false
  enable_embedding: false
  width: fixed              # o "full"
  auto_apply_filters: true
  parameters: []
  tabs: []
  dashcards:
    - card: clientes_total  # slug, se resuelve a card_id
      row: 0
      col: 0
      size_x: 8             # grid de 24 columnas
      size_y: 3
    - card: compras_total
      row: 0
      col: 8
      size_x: 8
      size_y: 3

Push:

python main.py push dashboard compras_y_clientes --apply

Auto-inyeccion (build_dashboard_payload) — para cada dashcard sin id, inyecta automaticamente:

  • id: negativo unico (-1, -2, ...) en orden de aparicion. Metabase lo necesita para distinguir dashcards nuevas (id < 0) de las existentes (id > 0). Si la dashcard ya trae id (positivo o negativo), se respeta tal cual.
  • visualization_settings: {} si falta.
  • parameter_mappings: [] si falta.

Esto resuelve el escollo historico: antes, omitir esos campos provocaba que el POST creara el dashboard pero el PUT siguiente devolviera 500 — quedando un dashboard huerfano en Metabase que no entraba al index local. Ahora los YAMLs de dashboards son tan simples como los de cards: solo card, row, col, size_x, size_y por dashcard.

6. push-all — proyecto entero en un comando

# Dry-run de todo el proyecto (cards primero, dashboards despues)
python main.py push-all

# Aplicar
python main.py push-all --apply

# Solo cards (saltar dashboards)
python main.py push-all --apply --kinds card

# Forzar saltando R17 (freshness) y R18 (count) en cada item
python main.py push-all --apply --force-overwrite

# Permitir warnings estructurales
python main.py push-all --apply --allow-warnings

Garantias:

  • Solo CREATE o UPDATE — nunca DELETE. Si un YAML local desaparece, el item correspondiente sigue intacto en Metabase. La unica via para borrar algo en Metabase es manualmente via UI o API.
  • Cards primero, dashboards despues. Asi cualquier card recien creada ya esta en el index cuando se construye el payload del dashboard que la referencia.
  • Resiliente a fallos por item. Si una card falla (validation error, SQL invalido, conflicto de freshness), push-all captura el SystemExit, lo registra en el resumen final y continua con el siguiente item.
  • Reusa toda la logica de push_one: backup obligatorio antes de UPDATE (R6), freshness check (R17), count check para dashboards (R18), log JSONL en state/push.log (R13).

Resumen final mostrado en stdout:

=== resumen push all ===
  OK:      11  ['card:clientes_total', 'card:compras_total', ...]
  FAILED:  0   []

Exit code: 1 si hubo fallos, 0 si todo OK.

7. Grid de Metabase

24 columnas, alturas en filas de ~30px. Layout que funciona bien para 6 cards:

Fila Cards Filas (size_y)
0 3 KPIs scalar (8 cols c/u) 3
3 2 graficos (12 cols c/u) 6
9 1 tabla ancha (24 cols) 7

8. Exploracion rapida: describe + sql

Dos comandos para no escribir cards a ciegas. Read-only, no tocan nada en disco ni en Metabase.

# Schema del database (tablas + columnas + tipos)
python main.py describe sample_database
python main.py describe sample_database --tables-only        # solo nombres + descripcion
python main.py describe sample_database --filter products    # una sola tabla
python main.py describe sample_database --samples            # +3 filas de ejemplo por tabla

# SQL ad-hoc — ideal para iterar antes de guardar como card
python main.py sql sample_database "SELECT CATEGORY, COUNT(*) FROM PRODUCTS GROUP BY CATEGORY"
python main.py sql sample_database "SELECT * FROM ORDERS" --limit 5

Tres barreras anti-explosion en sql:

  1. --limit N (default 100) → se envia como max-results constraint a Metabase. Metabase corta server-side, no transferimos filas de mas.
  2. Hard ceiling 10 000 filas — incluso --limit 999999 se cappea.
  3. 5 MB de payload — si las filas son anchas y exceden, se recorta antes de imprimir.

Queries que empiezan por INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER/CREATE se bloquean. --allow-write las deja pasar (Metabase normalmente las bloquea igualmente en /api/dataset).

Errores SQL devuelven el mensaje de Metabase limpio:

$ python main.py sql sample_database "SELECT NOPE FROM PEOPLE"
ERROR (400): Column "NOPE" not found; SQL statement: SELECT NOPE FROM PEOPLE [42122-214]

Workflow recomendado para card nueva:

python main.py describe sample_database --filter orders        # 1. ver columnas
python main.py sql sample_database "SELECT ... FROM ORDERS"    # 2. iterar SQL en stdout
# 3. cuando la query funciona, copiarla a cards/<slug>.yaml
python main.py push card <slug> --apply                        # 4. guardarla como card

9. Resumen del flujo

1. Editar YAMLs de cards en projects/{name}/cards/
2. Editar YAML del dashboard con dashcards (solo card slug + row/col/size — auto-inject hace el resto)
3. python main.py push-all --apply
4. Abrir http://localhost:3000/dashboard/<id>

Cualquier cambio futuro: editar el YAML, push-all --apply (o push <kind> <slug> --apply si quieres aplicar solo uno). Cualquier cambio hecho desde la UI de Metabase: pull <kind> <slug>. La logica de freshness (R17) avisa si hubo cambios remotos no traidos antes de pushear local.