feat: metabase_setup Python, fix list_databases, volumen Docker en init_metabase

Nueva función metabase_setup para setup inicial via API. Fix list_databases
que no extraía data del response wrapper. Pipeline init_metabase soporta
--mb-volumes para montar SQLite como volumen con fix de permisos automático.
Añadido .env a gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 23:23:20 +01:00
parent 2bae07d1f5
commit 72c572e1ea
12 changed files with 821 additions and 8 deletions
+3
View File
@@ -24,6 +24,9 @@ registry.db-wal
*.swo
*~
# Secrets
.env
# OS
.DS_Store
Thumbs.db
+411
View File
@@ -0,0 +1,411 @@
"""
apps/metabase_registry/main.py
==============================
Setup completo de fn-registry en Metabase: datasource, cards y dashboard.
USO
---
Via variables de entorno:
METABASE_URL=http://localhost:3000 \
METABASE_ADMIN_EMAIL=admin@example.com \
METABASE_ADMIN_PASSWORD=secret \
REGISTRY_DB_PATH=/registry.db \
python main.py
Via argumentos CLI:
python main.py \
--url http://localhost:3000 \
--admin-email admin@example.com \
--admin-password secret \
--registry-db-path /registry.db
Para crear un usuario nuevo (opcional):
python main.py ... \
--new-user-email dev@example.com \
--new-user-first-name Dev \
--new-user-last-name User \
--new-user-password devpass
NOTA SOBRE registry.db EN DOCKER
----------------------------------
Metabase corre en Docker y necesita acceder a registry.db. El path que
se configura en Metabase (--registry-db-path) debe ser la ruta DENTRO
del contenedor. Usa setup_volume.sh para copiar el archivo al contenedor
antes de ejecutar este script.
./setup_volume.sh /home/lucas/fn_registry/registry.db
Tras copiar, el archivo queda en /registry.db dentro del contenedor,
que es el valor por defecto de --registry-db-path.
DEPENDENCIAS
------------
Instalar con: pip install -r requirements.txt
O con uv: uv pip install -r requirements.txt
Las funciones metabase_add_database y metabase_list_databases deben
existir en python/functions/metabase/databases.py (creadas por el
fn-constructor).
"""
import argparse
import os
import sys
import json
# Agregar el directorio de funciones Python al path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
from metabase import (
MetabaseClient,
metabase_create_user,
metabase_create_card,
metabase_create_dashboard,
metabase_update_dashboard,
)
from metabase.client import metabase_auth
from metabase.databases import metabase_add_database, metabase_list_databases
# ---------------------------------------------------------------------------
# Configuracion de cards de ejemplo
# ---------------------------------------------------------------------------
CARDS_CONFIG = [
{
"name": "Funciones por dominio",
"description": "Cuenta de funciones agrupadas por dominio del registry",
"sql": "SELECT domain, count(*) AS total FROM functions GROUP BY domain ORDER BY total DESC",
"display": "bar",
},
{
"name": "Funciones puras vs impuras",
"description": "Distribucion de pureza funcional en el registry",
"sql": "SELECT purity, count(*) AS total FROM functions GROUP BY purity ORDER BY total DESC",
"display": "pie",
},
{
"name": "Funciones por kind",
"description": "Distribucion por tipo: function, pipeline, component",
"sql": "SELECT kind, count(*) AS total FROM functions GROUP BY kind ORDER BY total DESC",
"display": "bar",
},
{
"name": "Buscar funciones",
"description": "Lista completa de funciones con id, dominio, pureza y descripcion",
"sql": (
"SELECT id, domain, kind, purity, signature, description "
"FROM functions ORDER BY domain, name"
),
"display": "table",
},
{
"name": "Tipos por dominio",
"description": "Cuenta de tipos registrados por dominio",
"sql": "SELECT domain, count(*) AS total FROM types GROUP BY domain ORDER BY total DESC",
"display": "bar",
},
{
"name": "Proposals pendientes",
"description": "Proposals del registry que estan pendientes de revision",
"sql": (
"SELECT id, kind, title, created_by, created_at "
"FROM proposals WHERE status = 'pending' ORDER BY created_at DESC"
),
"display": "table",
},
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def log(msg: str) -> None:
print(f"[metabase_registry] {msg}", flush=True)
def log_ok(msg: str) -> None:
print(f"[metabase_registry] OK {msg}", flush=True)
def log_err(msg: str) -> None:
print(f"[metabase_registry] ERR {msg}", file=sys.stderr, flush=True)
def find_existing_database(client: MetabaseClient, name: str) -> dict | None:
"""Busca una database por nombre en Metabase. Retorna None si no existe."""
try:
databases = metabase_list_databases(client)
for db in databases:
if db.get("name") == name:
return db
except Exception as e:
log(f"No se pudo listar databases: {e}")
return None
def build_dataset_query(database_id: int, sql: str) -> dict:
"""Construye el dataset_query para una card SQL nativa."""
return {
"type": "native",
"database": database_id,
"native": {
"query": sql,
"template-tags": {},
},
}
def create_cards(client: MetabaseClient, database_id: int) -> list[dict]:
"""Crea todas las cards de ejemplo. Retorna lista de cards creadas."""
created = []
for cfg in CARDS_CONFIG:
log(f"Creando card: {cfg['name']!r} ...")
try:
card = metabase_create_card(
client=client,
name=cfg["name"],
dataset_query=build_dataset_query(database_id, cfg["sql"]),
display=cfg["display"],
description=cfg["description"],
)
log_ok(f"Card creada: id={card['id']} nombre={card['name']!r}")
created.append(card)
except Exception as e:
log_err(f"No se pudo crear card {cfg['name']!r}: {e}")
return created
def create_overview_dashboard(client: MetabaseClient, cards: list[dict]) -> dict | None:
"""Crea el dashboard 'fn-registry Overview' con todas las cards."""
log("Creando dashboard 'fn-registry Overview' ...")
try:
dashboard = metabase_create_dashboard(
client=client,
name="fn-registry Overview",
description="Vista general del function registry: funciones, tipos, proposals y metricas.",
)
log_ok(f"Dashboard creado: id={dashboard['id']}")
except Exception as e:
log_err(f"No se pudo crear dashboard: {e}")
return None
if not cards:
log("Sin cards para agregar al dashboard.")
return dashboard
# Posicionar cards en una grilla de 3 columnas, 24 unidades de ancho total.
# Cada card ocupa 8 columnas x 6 filas.
cols = 3
card_w = 8
card_h = 6
dashcards = []
for idx, card in enumerate(cards):
col = (idx % cols) * card_w
row = (idx // cols) * card_h
dashcards.append({
"id": -(idx + 1), # ID negativo = nueva dashcard
"card_id": card["id"],
"size_x": card_w,
"size_y": card_h,
"col": col,
"row": row,
"parameter_mappings": [],
"visualization_settings": {},
})
try:
metabase_update_dashboard(
client=client,
dashboard_id=dashboard["id"],
dashcards=dashcards,
)
log_ok(f"Dashboard actualizado con {len(dashcards)} cards.")
except Exception as e:
log_err(f"No se pudo poblar el dashboard con cards: {e}")
return dashboard
# ---------------------------------------------------------------------------
# Flujo principal
# ---------------------------------------------------------------------------
def run(args: argparse.Namespace) -> int:
"""Ejecuta el setup completo. Retorna 0 en exito, 1 en fallo critico."""
# 1. Autenticar como admin
log(f"Autenticando en {args.url} como {args.admin_email} ...")
try:
client = metabase_auth(args.url, args.admin_email, args.admin_password)
log_ok("Autenticacion exitosa.")
except Exception as e:
log_err(f"Fallo de autenticacion: {e}")
return 1
with client:
# 2. Crear usuario nuevo (opcional)
if args.new_user_email:
if not args.new_user_first_name or not args.new_user_last_name:
log_err("Para crear usuario se requieren --new-user-first-name y --new-user-last-name.")
return 1
log(f"Creando usuario {args.new_user_email} ...")
try:
user = metabase_create_user(
client=client,
first_name=args.new_user_first_name,
last_name=args.new_user_last_name,
email=args.new_user_email,
password=args.new_user_password or "",
)
log_ok(f"Usuario creado: id={user['id']} email={user['email']}")
except Exception as e:
log_err(f"No se pudo crear usuario: {e}")
# No es critico, continuar
# 3. Agregar registry.db como datasource SQLite
db_name = "fn-registry"
log(f"Verificando si ya existe la database {db_name!r} en Metabase ...")
existing_db = find_existing_database(client, db_name)
if existing_db:
db_id = existing_db["id"]
log_ok(f"Database ya existe: id={db_id} nombre={db_name!r}. Se reutiliza.")
else:
log(f"Registrando registry.db ({args.registry_db_path}) como datasource SQLite ...")
try:
new_db = metabase_add_database(
client=client,
name=db_name,
engine="sqlite",
details={"db": args.registry_db_path},
)
db_id = new_db["id"]
log_ok(f"Database registrada: id={db_id} path={args.registry_db_path}")
except Exception as e:
log_err(f"No se pudo registrar la database: {e}")
log_err(
"Verifica que registry.db este accesible desde el contenedor Docker. "
"Usa setup_volume.sh para copiar el archivo."
)
return 1
# 4. Crear cards de ejemplo
log(f"Creando {len(CARDS_CONFIG)} cards con database_id={db_id} ...")
cards = create_cards(client, db_id)
log(f"Cards creadas: {len(cards)}/{len(CARDS_CONFIG)}")
# 5. Crear dashboard
dashboard = create_overview_dashboard(client, cards)
if dashboard:
log_ok(
f"Dashboard disponible en: {args.url}/dashboard/{dashboard['id']}"
)
else:
log_err("El dashboard no pudo crearse.")
summary = {
"url": args.url,
"database_id": db_id,
"cards_created": len(cards),
"dashboard_id": dashboard["id"] if dashboard else None,
"dashboard_url": f"{args.url}/dashboard/{dashboard['id']}" if dashboard else None,
}
print("\n--- Resumen ---")
print(json.dumps(summary, indent=2))
return 0
# ---------------------------------------------------------------------------
# CLI / env vars
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="metabase_registry",
description="Setup de fn-registry como datasource en Metabase.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
# Conexion Metabase
p.add_argument(
"--url",
default=os.environ.get("METABASE_URL", "http://localhost:3000"),
help="URL base de Metabase (env: METABASE_URL). Default: http://localhost:3000",
)
p.add_argument(
"--admin-email",
default=os.environ.get("METABASE_ADMIN_EMAIL", "admin@example.com"),
dest="admin_email",
help="Email del admin (env: METABASE_ADMIN_EMAIL).",
)
p.add_argument(
"--admin-password",
default=os.environ.get("METABASE_ADMIN_PASSWORD", ""),
dest="admin_password",
help="Password del admin (env: METABASE_ADMIN_PASSWORD).",
)
# Registry DB path (ruta dentro del contenedor Docker)
p.add_argument(
"--registry-db-path",
default=os.environ.get("REGISTRY_DB_PATH", "/registry.db"),
dest="registry_db_path",
help=(
"Ruta al registry.db DENTRO del contenedor Docker "
"(env: REGISTRY_DB_PATH). Default: /registry.db"
),
)
# Nuevo usuario (todos opcionales)
p.add_argument(
"--new-user-email",
default=os.environ.get("NEW_USER_EMAIL", ""),
dest="new_user_email",
help="Email del nuevo usuario a crear (env: NEW_USER_EMAIL). Opcional.",
)
p.add_argument(
"--new-user-first-name",
default=os.environ.get("NEW_USER_FIRST_NAME", ""),
dest="new_user_first_name",
help="Nombre del nuevo usuario (env: NEW_USER_FIRST_NAME).",
)
p.add_argument(
"--new-user-last-name",
default=os.environ.get("NEW_USER_LAST_NAME", ""),
dest="new_user_last_name",
help="Apellido del nuevo usuario (env: NEW_USER_LAST_NAME).",
)
p.add_argument(
"--new-user-password",
default=os.environ.get("NEW_USER_PASSWORD", ""),
dest="new_user_password",
help="Password del nuevo usuario (env: NEW_USER_PASSWORD). Opcional.",
)
return p
def main() -> None:
parser = build_parser()
args = parser.parse_args()
if not args.admin_password:
parser.error(
"Se requiere la password del admin. "
"Usa --admin-password o la variable de entorno METABASE_ADMIN_PASSWORD."
)
sys.exit(run(args))
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
# Dependencias de apps/metabase_registry
#
# Instalar con pip:
# pip install -r requirements.txt
#
# O con uv (recomendado, desde la raiz del repo):
# cd /home/lucas/fn_registry/python && uv pip install -r ../apps/metabase_registry/requirements.txt
#
# O directamente desde el directorio python (que ya tiene httpx):
# cd /home/lucas/fn_registry/python && uv run python ../apps/metabase_registry/main.py
# HTTP client — mismo que usa el paquete python/functions/metabase
httpx>=0.27.0
+9 -4
View File
@@ -30,13 +30,17 @@ file_path: "functions/pipelines/init_metabase/main.go"
## Ejemplo
```bash
# Básico
go run functions/pipelines/init_metabase/main.go \
--project analytics \
--metabase-port 3000 \
--pg-port 5432 \
--pg-user metabase \
--pg-password metabase \
--pg-database metabase
--pg-password metabase
# Con volume para registry.db (detecta cambios en vivo)
go run functions/pipelines/init_metabase/main.go \
--project fn_registry \
--mb-volumes "/home/lucas/fn_registry/registry.db:/data/registry.db"
```
Salida JSON:
@@ -60,7 +64,8 @@ El pipeline orquesta 5 pasos secuenciales:
2. **Pull** — descarga `postgres:16` y `metabase/metabase:latest`
3. **Postgres** — inicia con volume persistente (named volume por defecto o bind mount con `--pg-volume`)
4. **Health check** — retry exponencial (hasta ~34 min) con `pg_isready` dentro del contenedor
5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable
5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable. Con `--mb-volumes` monta volumes adicionales (ej: registry.db para SQLite)
6. **Permisos** — ajusta ownership de directorios montados para el usuario `metabase` (UID 2000) dentro del contenedor
Reutiliza conceptualmente `docker_create_network`, `docker_pull_image`, `docker_run_container`, `docker_inspect_container` y `retry_with_backoff`, reimplementadas inline por ser un ejecutable independiente.
+33 -4
View File
@@ -28,7 +28,8 @@ func main() {
pgUser := flag.String("pg-user", "metabase", "Usuario Postgres")
pgPass := flag.String("pg-password", "metabase", "Password Postgres")
pgDB := flag.String("pg-database", "metabase", "Base de datos Postgres")
pgVolume := flag.String("pg-volume", "", "Path host para persistencia (default: docker named volume)")
pgVolume := flag.String("pg-volume", "", "Path host para persistencia Postgres (default: docker named volume)")
mbVolumes := flag.String("mb-volumes", "", "Volumes adicionales para Metabase, separados por coma (ej: /host/path:/container/path,/otro:/dest)")
flag.Parse()
if *project == "" {
@@ -37,7 +38,12 @@ func main() {
os.Exit(1)
}
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume)
var extraVolumes []string
if *mbVolumes != "" {
extraVolumes = strings.Split(*mbVolumes, ",")
}
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume, extraVolumes)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@@ -48,7 +54,7 @@ func main() {
enc.Encode(result)
}
func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string) (*MetabaseResult, error) {
func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string, mbExtraVolumes []string) (*MetabaseResult, error) {
networkName := project + "-net"
pgName := project + "-postgres"
mbName := project + "-metabase"
@@ -118,13 +124,36 @@ func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string
"-e", "MB_DB_USER=" + pgUser,
"-e", "MB_DB_PASS=" + pgPass,
"-e", "MB_DB_HOST=" + pgName,
"metabase/metabase:latest",
}
for _, v := range mbExtraVolumes {
mbArgs = append(mbArgs, "-v", strings.TrimSpace(v))
}
mbArgs = append(mbArgs, "metabase/metabase:latest")
mbID, err := dockerCmd(mbArgs...)
if err != nil {
return nil, fmt.Errorf("starting metabase: %w", err)
}
// 6. Fix permisos para volumes SQLite — Metabase corre como UID 2000
for _, v := range mbExtraVolumes {
parts := strings.SplitN(strings.TrimSpace(v), ":", 2)
if len(parts) < 2 {
continue
}
destPath := parts[1]
// Si es un archivo, fix el directorio padre; si es directorio, fix directo
dir := destPath
if strings.Contains(destPath, ".") {
// Probablemente un archivo, usar dirname
idx := strings.LastIndex(destPath, "/")
if idx > 0 {
dir = destPath[:idx]
}
}
fmt.Fprintf(os.Stderr, " Fijando permisos de %s para usuario metabase...\n", dir)
dockerCmd("exec", "-u", "root", mbName, "chown", "metabase:metabase", dir)
}
mbURL := fmt.Sprintf("http://localhost:%s", mbPort)
fmt.Fprintf(os.Stderr, "\nStack listo. Metabase disponible en %s\n", mbURL)
+4
View File
@@ -2,10 +2,14 @@ from .client import MetabaseClient
from .users import metabase_list_users, metabase_get_user, metabase_create_user, metabase_update_user, metabase_deactivate_user
from .cards import metabase_list_cards, metabase_get_card, metabase_create_card, metabase_update_card, metabase_delete_card, metabase_execute_card, metabase_execute_query
from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard
from .databases import metabase_list_databases, metabase_add_database, metabase_get_database
from .setup import metabase_setup
__all__ = [
"MetabaseClient",
"metabase_list_users", "metabase_get_user", "metabase_create_user", "metabase_update_user", "metabase_deactivate_user",
"metabase_list_cards", "metabase_get_card", "metabase_create_card", "metabase_update_card", "metabase_delete_card", "metabase_execute_card", "metabase_execute_query",
"metabase_list_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
"metabase_list_databases", "metabase_add_database", "metabase_get_database",
"metabase_setup",
]
+105
View File
@@ -0,0 +1,105 @@
"""CRUD de databases de Metabase."""
from .client import MetabaseClient
def metabase_list_databases(
client: MetabaseClient,
include_tables: bool = False,
) -> list:
"""Lista las databases configuradas en Metabase.
Endpoint: GET /api/database.
Args:
client: Cliente autenticado.
include_tables: Si True, incluye las tablas de cada database en la respuesta.
Returns:
Lista de dicts, cada uno con campos de la database:
- id: ID numerico de la database
- name: Nombre dado en Metabase
- engine: Motor de BD (sqlite, postgres, mysql, etc.)
- details: Dict con parametros de conexion
- is_full_sync: Si sincroniza el schema automaticamente
- tables: Lista de tablas (solo si include_tables=True)
Example:
>>> dbs = metabase_list_databases(client)
>>> for db in dbs:
... print(db["id"], db["name"], db["engine"])
>>> dbs = metabase_list_databases(client, include_tables=True)
>>> for db in dbs:
... print(db["name"], [t["name"] for t in db.get("tables", [])])
"""
params = {}
if include_tables:
params["include"] = "tables"
result = client.request("GET", "/api/database", params=params)
if isinstance(result, dict) and "data" in result:
return result["data"]
return result
def metabase_add_database(
client: MetabaseClient,
name: str,
engine: str,
details: dict,
) -> dict:
"""Agrega una nueva database a Metabase.
Endpoint: POST /api/database. Requiere permisos de superusuario.
Args:
client: Cliente autenticado con permisos admin.
name: Nombre descriptivo para la database en Metabase.
engine: Motor de base de datos. Ejemplos: "sqlite", "postgres",
"mysql", "h2", "mongo", "bigquery".
details: Dict con parametros de conexion especificos del engine.
Para SQLite: {"db": "/ruta/al/archivo.db"}
Para Postgres: {"host": "...", "port": 5432, "dbname": "...",
"user": "...", "password": "..."}
Returns:
Dict con la database creada, incluyendo el campo "id" asignado
por Metabase y todos los campos de configuracion.
Raises:
httpx.HTTPStatusError: 400 si los datos de conexion son invalidos.
Example:
>>> db = metabase_add_database(client, "Mi SQLite", "sqlite", {"db": "/data/ops.db"})
>>> print(db["id"], db["name"])
"""
body = {
"name": name,
"engine": engine,
"details": details,
}
return client.request("POST", "/api/database", json=body)
def metabase_get_database(client: MetabaseClient, database_id: int) -> dict:
"""Obtiene los detalles de una database de Metabase por su ID.
Endpoint: GET /api/database/:id.
Args:
client: Cliente autenticado.
database_id: ID numerico de la database.
Returns:
Dict con campos de la database: id, name, engine, details,
is_full_sync, is_on_demand, auto_run_queries, created_at,
updated_at, features, etc.
Raises:
httpx.HTTPStatusError: 404 si la database no existe.
Example:
>>> db = metabase_get_database(client, 2)
>>> print(db["name"], db["engine"])
"""
return client.request("GET", f"/api/database/{database_id}")
@@ -0,0 +1,44 @@
---
name: metabase_add_database
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_add_database(client: MetabaseClient, name: str, engine: str, details: dict) -> dict"
description: "Agrega una nueva database a Metabase via POST /api/database. Soporta cualquier engine (sqlite, postgres, mysql, etc.)."
tags: [metabase, database, add, create, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
# SQLite
db = metabase_add_database(client, "Ops DB", "sqlite", {"db": "/data/operations.db"})
print(db["id"])
# Postgres
db = metabase_add_database(client, "Prod PG", "postgres", {
"host": "localhost",
"port": 5432,
"dbname": "myapp",
"user": "reader",
"password": "secret",
})
```
## Notas
Requiere permisos de superusuario. El campo `details` depende del engine:
para SQLite solo necesita `{"db": "/ruta/archivo.db"}`.
Retorna la database creada con su `id` asignado por Metabase.
@@ -0,0 +1,33 @@
---
name: metabase_get_database
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_get_database(client: MetabaseClient, database_id: int) -> dict"
description: "Obtiene los detalles de una database de Metabase por su ID. Endpoint: GET /api/database/:id."
tags: [metabase, database, get, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
db = metabase_get_database(client, 2)
print(db["name"], db["engine"])
```
## Notas
Error 404 si la database no existe. Retorna campos completos incluyendo
id, name, engine, details, is_full_sync, auto_run_queries, created_at, features.
@@ -0,0 +1,39 @@
---
name: metabase_list_databases
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_list_databases(client: MetabaseClient, include_tables: bool = False) -> list"
description: "Lista las databases configuradas en Metabase. Endpoint: GET /api/database. Soporta incluir tablas con include_tables=True."
tags: [metabase, database, list, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
dbs = metabase_list_databases(client)
for db in dbs:
print(db["id"], db["name"], db["engine"])
# Con tablas incluidas
dbs = metabase_list_databases(client, include_tables=True)
for db in dbs:
print(db["name"], [t["name"] for t in db.get("tables", [])])
```
## Notas
Si include_tables=True agrega el query param `?include=tables` al request.
Retorna lista directa (no paginada) de todas las databases accesibles.
@@ -0,0 +1,44 @@
---
name: metabase_setup
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "metabase_setup(base_url: str, admin_email: str, admin_password: str, admin_first_name: str, admin_last_name: str, site_name: str, site_locale: str) -> dict"
description: "Ejecuta el setup inicial de una instancia Metabase nueva via POST /api/setup. Obtiene el setup-token automaticamente y crea el usuario admin con preferencias del sitio."
tags: [metabase, setup, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/setup.py"
---
## Ejemplo
```python
from metabase.setup import metabase_setup
result = metabase_setup(
base_url="http://localhost:3000",
admin_email="admin@fnregistry.local",
admin_password="FnRegistry2024!",
admin_first_name="Lucas",
admin_last_name="Admin",
site_name="fn-registry",
site_locale="es",
)
print(result["id"]) # session token
```
## Notas
Solo funciona en instancias sin configurar (setup-token disponible). Si Metabase ya tiene un usuario, lanza RuntimeError.
El setup-token se obtiene automaticamente de GET /api/session/properties. Una vez usado, Metabase invalida el token.
+83
View File
@@ -0,0 +1,83 @@
"""Setup inicial de Metabase via API."""
import httpx
def metabase_setup(
base_url: str,
admin_email: str,
admin_password: str,
admin_first_name: str = "Admin",
admin_last_name: str = "User",
site_name: str = "Metabase",
site_locale: str = "en",
) -> dict:
"""Ejecuta el setup inicial de una instancia Metabase nueva.
Usa el setup-token de una instancia sin configurar para crear el
usuario admin y configurar preferencias del sitio. Solo funciona
una vez — si Metabase ya tiene un usuario, retorna error.
Endpoint: POST /api/setup (requiere setup-token valido).
Args:
base_url: URL base de la instancia (ej: "http://localhost:3000").
admin_email: Email del usuario admin a crear.
admin_password: Password del admin (min 8 chars con complejidad).
admin_first_name: Nombre del admin.
admin_last_name: Apellido del admin.
site_name: Nombre del sitio Metabase.
site_locale: Locale del sitio (ej: "es", "en").
Returns:
Dict con el session id del admin recien creado.
Raises:
httpx.HTTPStatusError: 400 si el setup-token es invalido o
Metabase ya fue configurado.
RuntimeError: Si no se puede obtener el setup-token.
Example:
>>> result = metabase_setup(
... "http://localhost:3000",
... "admin@example.com",
... "SecurePass123!",
... site_name="fn-registry",
... site_locale="es",
... )
>>> print(result["id"]) # session token
"""
url = base_url.rstrip("/")
# Obtener setup-token
resp = httpx.get(f"{url}/api/session/properties")
resp.raise_for_status()
props = resp.json()
setup_token = props.get("setup-token")
if not setup_token:
raise RuntimeError(
"No setup-token disponible. Metabase ya fue configurado "
"o has-user-setup es True."
)
# Ejecutar setup
resp = httpx.post(
f"{url}/api/setup",
json={
"token": setup_token,
"user": {
"first_name": admin_first_name,
"last_name": admin_last_name,
"email": admin_email,
"password": admin_password,
},
"prefs": {
"site_name": site_name,
"site_locale": site_locale,
"allow_tracking": False,
},
},
timeout=30.0,
)
resp.raise_for_status()
return resp.json()