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 ebbc3bfdab
commit 2e5bdacdcf
12 changed files with 821 additions and 8 deletions
+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()