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>
This commit is contained in:
2026-04-13 13:14:05 +02:00
parent 386a5471e0
commit 310b409ae0
32 changed files with 3116 additions and 0 deletions
@@ -0,0 +1,2 @@
METABASE_EMAIL=admin@auto-metabase.local
METABASE_PASSWORD=AutoMetabase123!
@@ -0,0 +1,28 @@
_meta:
kind: card
id: 48
slug: clientes_nuevos_por_mes
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.096661Z'
_refs:
database: sample_database
collection: null
payload:
description: Altas mensuales en PEOPLE (CREATED_AT)
archived: false
enable_embedding: false
query_type: native
name: Clientes nuevos por mes
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT FORMATDATETIME(CREATED_AT, 'yyyy-MM') AS mes,\n COUNT(*) AS nuevos\n FROM PEOPLE\n GROUP BY mes\n\
\ ORDER BY mes"
parameter_mappings: []
display: line
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,31 @@
_meta:
kind: card
id: 49
slug: clientes_por_edad
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.154517Z'
_refs:
database: sample_database
collection: null
payload:
description: Distribucion calculada desde BIRTH_DATE a fecha actual
archived: false
enable_embedding: false
query_type: native
name: Clientes por rango de edad
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT\n CASE\n WHEN DATEDIFF('year', BIRTH_DATE, CURRENT_DATE) < 25 THEN '1) <25'\n WHEN DATEDIFF('year',\
\ BIRTH_DATE, CURRENT_DATE) < 35 THEN '2) 25-34'\n WHEN DATEDIFF('year', BIRTH_DATE, CURRENT_DATE) < 45 THEN '3)\
\ 35-44'\n WHEN DATEDIFF('year', BIRTH_DATE, CURRENT_DATE) < 55 THEN '4) 45-54'\n WHEN DATEDIFF('year', BIRTH_DATE,\
\ CURRENT_DATE) < 65 THEN '5) 55-64'\n ELSE '6) 65+'\n END AS rango_edad,\n COUNT(*) AS clientes\nFROM PEOPLE\n\
WHERE BIRTH_DATE IS NOT NULL\nGROUP BY rango_edad\nORDER BY rango_edad"
parameter_mappings: []
display: bar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 46
slug: clientes_por_estado
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.215737Z'
_refs:
database: sample_database
collection: null
payload:
description: Top 10 estados (US) por numero de clientes
archived: false
enable_embedding: false
query_type: native
name: clientes_por_estado
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT STATE, COUNT(*) AS clientes\n FROM PEOPLE\n GROUP BY STATE\n ORDER BY clientes DESC\n LIMIT 10"
parameter_mappings: []
display: bar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,28 @@
_meta:
kind: card
id: 50
slug: clientes_por_source
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.276556Z'
_refs:
database: sample_database
collection: null
payload:
description: Distribucion por SOURCE (Google, Twitter, Facebook, Organic, Affiliate)
archived: false
enable_embedding: false
query_type: native
name: Clientes por canal
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT COALESCE(SOURCE, 'unknown') AS canal,\n COUNT(*) AS clientes\n FROM PEOPLE\n GROUP BY canal\n\
\ ORDER BY clientes DESC"
parameter_mappings: []
display: pie
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 51
slug: clientes_recientes
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.325878Z'
_refs:
database: sample_database
collection: null
payload:
description: Tabla de las 20 altas mas recientes en PEOPLE
archived: false
enable_embedding: false
query_type: native
name: Ultimos 20 clientes registrados
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT NAME, EMAIL, CITY, STATE, SOURCE, CREATED_AT\n FROM PEOPLE\n ORDER BY CREATED_AT DESC\n LIMIT 20"
parameter_mappings: []
display: table
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,28 @@
_meta:
kind: card
id: 52
slug: clientes_top_ciudades
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.37711Z'
_refs:
database: sample_database
collection: null
payload:
description: Ciudades con mas clientes
archived: false
enable_embedding: false
query_type: native
name: Top 15 ciudades
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT CITY, STATE, COUNT(*) AS clientes\n FROM PEOPLE\n GROUP BY CITY, STATE\n ORDER BY clientes DESC\n LIMIT\
\ 15"
parameter_mappings: []
display: bar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 42
slug: clientes_total
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.441467Z'
_refs:
database: sample_database
collection: null
payload:
description: Numero total de clientes en PEOPLE
archived: false
enable_embedding: false
query_type: native
name: clientes_total
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: SELECT COUNT(*) AS total FROM PEOPLE
parameter_mappings: []
display: scalar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,28 @@
_meta:
kind: card
id: 45
slug: compras_por_mes
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.516411Z'
_refs:
database: sample_database
collection: null
payload:
description: Numero de compras agrupadas por mes
archived: false
enable_embedding: false
query_type: native
name: compras_por_mes
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT FORMATDATETIME(CREATED_AT, 'yyyy-MM') AS mes,\n COUNT(*) AS compras\n FROM ORDERS\n GROUP BY\
\ mes\n ORDER BY mes"
parameter_mappings: []
display: line
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 43
slug: compras_total
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.586558Z'
_refs:
database: sample_database
collection: null
payload:
description: Numero total de compras en ORDERS
archived: false
enable_embedding: false
query_type: native
name: compras_total
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: SELECT COUNT(*) AS total FROM ORDERS
parameter_mappings: []
display: scalar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 44
slug: ingresos_totales
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.663393Z'
_refs:
database: sample_database
collection: null
payload:
description: Suma de TOTAL de todas las compras
archived: false
enable_embedding: false
query_type: native
name: ingresos_totales
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: SELECT ROUND(SUM(TOTAL), 2) AS ingresos FROM ORDERS
parameter_mappings: []
display: scalar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,27 @@
_meta:
kind: card
id: 40
slug: test_count_users
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.727051Z'
_refs:
database: metabase_internal_pg
collection: null
payload:
description: otro cambio externo
archived: false
enable_embedding: false
query_type: native
name: test_count_users
type: question
dataset_query:
lib/type: mbql/query
database: metabase_internal_pg
stages:
- lib/type: mbql.stage/native
native: SELECT COUNT(*) AS users FROM core_user
parameter_mappings: []
display: scalar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,26 @@
_meta:
kind: card
id: 41
slug: test_users_by_locale
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.796377Z'
_refs:
database: metabase_internal_pg
collection: null
payload:
archived: false
enable_embedding: false
query_type: native
name: test_users_by_locale
type: question
dataset_query:
lib/type: mbql/query
database: metabase_internal_pg
stages:
- lib/type: mbql.stage/native
native: SELECT COALESCE(locale, 'unknown') AS locale, COUNT(*) AS n FROM core_user GROUP BY locale ORDER BY n DESC
parameter_mappings: []
display: bar
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,29 @@
_meta:
kind: card
id: 47
slug: top_clientes
synced_at: '2026-04-13T10:49:59Z'
remote_updated_at: '2026-04-13T10:49:59.882916Z'
_refs:
database: sample_database
collection: null
payload:
description: Top 10 clientes por gasto total
archived: false
enable_embedding: false
query_type: native
name: top_clientes
type: question
dataset_query:
lib/type: mbql/query
database: sample_database
stages:
- lib/type: mbql.stage/native
native: "SELECT P.NAME AS cliente,\n P.EMAIL AS email,\n COUNT(O.ID) AS num_compras,\n ROUND(SUM(O.TOTAL),\
\ 2) AS total_gastado\n FROM PEOPLE P\n JOIN ORDERS O ON O.USER_ID = P.ID\n GROUP BY P.NAME, P.EMAIL\n ORDER BY\
\ total_gastado DESC\n LIMIT 10"
parameter_mappings: []
display: table
collection_preview: true
visualization_settings: {}
parameters: []
@@ -0,0 +1,15 @@
name: test_local
description: "Metabase de prueba en Docker local (container auto_metabase_test-metabase)"
base_url: http://localhost:3000
auth:
email_env: METABASE_EMAIL
password_env: METABASE_PASSWORD
sync:
# IDs de databases a ignorar (1 = Sample Database interno)
ignore_databases: [1]
# IDs de colecciones a ignorar
ignore_collections: []
# Si true, archiva en Metabase en vez de eliminar al hacer push
prefer_archive: true
@@ -0,0 +1,31 @@
_meta:
kind: dashboard
id: 2
slug: auto_metabase_test_dashboard
synced_at: '2026-04-13T10:50:00Z'
remote_updated_at: '2026-04-13T09:43:33.289419Z'
dashcards_count: 2
tabs_count: 0
parameters_count: 0
_refs:
collection: null
payload:
description: Dashboard de prueba para auto_metabase
archived: false
dashcards:
- size_x: 6
col: 0
size_y: 4
row: 0
card: test_count_users
- size_x: 6
col: 6
size_y: 4
row: 0
card: test_users_by_locale
tabs: []
enable_embedding: false
name: auto_metabase test dashboard
width: fixed
parameters: []
auto_apply_filters: true
@@ -0,0 +1,51 @@
_meta:
kind: dashboard
id: 4
slug: compras_y_clientes
synced_at: '2026-04-13T10:50:00Z'
remote_updated_at: '2026-04-13T10:40:30.171442Z'
dashcards_count: 6
tabs_count: 0
parameters_count: 0
_refs:
collection: null
payload:
description: Vista general de compras (ORDERS) y clientes (PEOPLE) del Sample Database
archived: false
dashcards:
- size_x: 12
col: 12
size_y: 6
row: 3
card: clientes_por_estado
- size_x: 12
col: 0
size_y: 6
row: 3
card: compras_por_mes
- size_x: 8
col: 0
size_y: 3
row: 0
card: clientes_total
- size_x: 8
col: 8
size_y: 3
row: 0
card: compras_total
- size_x: 8
col: 16
size_y: 3
row: 0
card: ingresos_totales
- size_x: 24
col: 0
size_y: 7
row: 9
card: top_clientes
tabs: []
enable_embedding: false
name: Compras y Clientes
width: fixed
parameters: []
auto_apply_filters: true
@@ -0,0 +1,36 @@
_meta:
kind: dashboard
id: 5
slug: kpis_minimal
synced_at: '2026-04-13T10:50:00Z'
remote_updated_at: '2026-04-13T10:47:00.301085Z'
dashcards_count: 3
tabs_count: 0
parameters_count: 0
_refs:
collection: null
payload:
description: Dashboard test del auto-inject — YAML sin id/viz/param_mappings en dashcards
archived: false
dashcards:
- size_x: 8
col: 0
size_y: 3
row: 0
card: clientes_total
- size_x: 8
col: 8
size_y: 3
row: 0
card: compras_total
- size_x: 8
col: 16
size_y: 3
row: 0
card: ingresos_totales
tabs: []
enable_embedding: false
name: KPIs Minimal
width: fixed
parameters: []
auto_apply_filters: true
@@ -0,0 +1,56 @@
_meta:
kind: dashboard
id: 6
slug: panel_clientes
synced_at: '2026-04-13T10:50:00Z'
remote_updated_at: '2026-04-13T10:50:00.213836Z'
dashcards_count: 7
tabs_count: 0
parameters_count: 0
_refs:
collection: null
payload:
description: Vista 360 de los clientes (PEOPLE) del Sample Database — total, altas, canal, geografia, edad y registros recientes.
archived: false
dashcards:
- size_x: 24
col: 0
size_y: 3
row: 0
card: clientes_total
- size_x: 24
col: 0
size_y: 6
row: 3
card: clientes_nuevos_por_mes
- size_x: 12
col: 0
size_y: 6
row: 9
card: clientes_por_source
- size_x: 12
col: 12
size_y: 6
row: 9
card: clientes_por_edad
- size_x: 12
col: 0
size_y: 6
row: 15
card: clientes_por_estado
- size_x: 12
col: 12
size_y: 6
row: 15
card: clientes_top_ciudades
- size_x: 24
col: 0
size_y: 8
row: 21
card: clientes_recientes
tabs: []
enable_embedding: false
name: Panel de Clientes
width: fixed
parameters: []
auto_apply_filters: true
@@ -0,0 +1,17 @@
_meta:
kind: database
id: 2
slug: metabase_internal_pg
_refs: {}
payload:
timezone: GMT
auto_run_queries: true
name: metabase_internal_pg
details:
host: auto_metabase_test-postgres
port: 5432
dbname: metabase
user: metabase
password: ${METABASE_DB_PASSWORD_METABASE_INTERNAL_PG}
ssl: false
engine: postgres
@@ -0,0 +1,29 @@
{
"cards": {
"clientes_nuevos_por_mes": 48,
"clientes_por_edad": 49,
"clientes_por_estado": 46,
"clientes_por_source": 50,
"clientes_recientes": 51,
"clientes_top_ciudades": 52,
"clientes_total": 42,
"compras_por_mes": 45,
"compras_total": 43,
"ingresos_totales": 44,
"test_count_users": 40,
"test_users_by_locale": 41,
"top_clientes": 47
},
"collections": {},
"dashboards": {
"auto_metabase_test_dashboard": 2,
"compras_y_clientes": 4,
"kpis_minimal": 5,
"panel_clientes": 6
},
"databases": {
"metabase_internal_pg": 2,
"sample_database": 1
},
"documents": {}
}