Files
fn_registry/python/functions/metabase/dashboards.py
T
egutierrez 9a28d08e38 feat(metabase): expansion de funciones Python — documents, collections, permissions, validation
Añade un conjunto amplio de funciones al paquete python/functions/metabase:
- Nuevos modulos: collections.py, documents.py, maintenance.py, permissions.py, validation.py (+ test).
- Ampliacion de cards.py, dashboards.py, client.py e __init__.py para exponer las nuevas operaciones.
- Funciones de documentos (create/get/update/delete/archive/copy/move + comentarios), grupos y memberships, permission/collection graphs, copy/move de cards y dashboards, validacion de MBQL/SQL y payloads, actualizacion segura de dashboards y fix_null_ratio.
- .md por funcion con frontmatter para que fn index los registre.
- Actualiza pyproject.toml y uv.lock con las dependencias resultantes.

Impacto: ampliamente mas cobertura de la API de Metabase desde el registry, reutilizable por apps y analisis. No toca Go ni frontend.
2026-04-13 23:31:42 +02:00

303 lines
11 KiB
Python

"""CRUD de dashboards de Metabase."""
from .client import MetabaseClient
def metabase_list_dashboards(
client: MetabaseClient,
filter: str = "",
) -> list[dict]:
"""Lista dashboards de Metabase con filtro opcional.
Endpoint: GET /api/dashboard. Retorna dashboards resumidos (sin dashcards).
Args:
client: Cliente autenticado.
filter: "all", "mine" o "archived". Vacio = todas.
Returns:
Lista de dicts con: id, name, description, collection_id,
creator_id, archived, created_at.
Example:
>>> dashboards = metabase_list_dashboards(client, filter="mine")
>>> for d in dashboards:
... print(d["id"], d["name"])
"""
params = {}
if filter:
params["f"] = filter
return client.request("GET", "/api/dashboard", params=params)
def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict:
"""Obtiene un dashboard completo incluyendo sus cards.
Endpoint: GET /api/dashboard/:id.
Args:
client: Cliente autenticado.
dashboard_id: ID del dashboard.
Returns:
Dict con: id, name, description, dashcards (lista de cards posicionadas),
parameters (filtros), tabs, collection_id, archived.
Cada dashcard tiene: id, card_id, card (objeto completo), size_x, size_y,
col, row, dashboard_tab_id, parameter_mappings, visualization_settings.
Example:
>>> dash = metabase_get_dashboard(client, 1)
>>> for dc in dash["dashcards"]:
... print(f"Card {dc['card_id']} at ({dc['col']}, {dc['row']})")
"""
return client.request("GET", f"/api/dashboard/{dashboard_id}")
def metabase_create_dashboard(
client: MetabaseClient,
name: str,
description: str = "",
collection_id: int = 0,
) -> dict:
"""Crea un nuevo dashboard vacio en Metabase.
Endpoint: POST /api/dashboard.
Para agregar cards usar metabase_update_dashboard con dashcards.
Args:
client: Cliente autenticado.
name: Nombre del dashboard.
description: Descripcion opcional.
collection_id: Coleccion destino. 0 = root.
Returns:
Dict con el dashboard creado.
Example:
>>> dash = metabase_create_dashboard(client, "Sales Overview", "KPIs de ventas")
>>> # Agregar cards:
>>> metabase_update_dashboard(client, dash["id"], dashcards=[
... {"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
... ])
"""
body: dict = {"name": name}
if description:
body["description"] = description
if collection_id > 0:
body["collection_id"] = collection_id
return client.request("POST", "/api/dashboard", json=body)
def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict:
"""Actualiza un dashboard incluyendo metadata, cards y tabs.
Endpoint: PUT /api/dashboard/:id.
El campo dashcards representa el ESTADO COMPLETO DESEADO del dashboard:
- Agregar card: incluirla con ID negativo (-1, -2, etc.)
- Actualizar card existente: incluirla con su ID positivo
- Eliminar card: omitirla del array
Args:
client: Cliente autenticado.
dashboard_id: ID del dashboard.
**fields: Campos a actualizar. Validos:
name (str), description (str), archived (bool),
dashcards (list[dict]), tabs (list[dict]),
parameters (list[dict]), collection_id (int).
Returns:
Dict con el dashboard actualizado.
Example:
>>> # Cambiar nombre
>>> metabase_update_dashboard(client, 1, name="Updated Name")
>>>
>>> # Agregar card (primero obtener existentes)
>>> dash = metabase_get_dashboard(client, 1)
>>> cards = list(dash["dashcards"])
>>> cards.append({"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0})
>>> metabase_update_dashboard(client, 1, dashcards=cards)
>>>
>>> # Archivar (soft-delete)
>>> metabase_update_dashboard(client, 1, archived=True)
"""
return client.request("PUT", f"/api/dashboard/{dashboard_id}", json=fields)
def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None:
"""Elimina permanentemente un dashboard.
Endpoint: DELETE /api/dashboard/:id. IRREVERSIBLE.
Para soft-delete preferir: metabase_update_dashboard(client, id, archived=True)
Args:
client: Cliente autenticado.
dashboard_id: ID del dashboard a eliminar.
Example:
>>> metabase_delete_dashboard(client, 1)
>>> # Preferir: metabase_update_dashboard(client, 1, archived=True)
"""
client.request("DELETE", f"/api/dashboard/{dashboard_id}")
def metabase_copy_dashboard(
client: MetabaseClient,
dashboard_id: int,
name: str | None = None,
collection_id: int | None = None,
description: str | None = None,
is_deep_copy: bool = False,
) -> dict:
"""Crea una copia de un dashboard existente en Metabase.
Endpoint: POST /api/dashboard/:id/copy. Usa el endpoint nativo de Metabase para
duplicar el dashboard junto con su layout de dashcards y parametros.
Con is_deep_copy=True tambien clona las cards referenciadas.
Args:
client: Cliente autenticado.
dashboard_id: ID del dashboard a copiar.
name: Nombre para la copia. None = Metabase asigna "Copy of <nombre>".
collection_id: Coleccion destino. None = misma coleccion que el original.
description: Descripcion de la copia. None = misma que el original.
is_deep_copy: Si True, clona tambien todas las cards referenciadas por el
dashboard (deep copy). Si False, la copia referencia las cards originales.
Returns:
Dict con el dashboard nuevo creado por Metabase. Incluye el campo `id`
asignado a la copia y el layout de dashcards copiado.
Example:
>>> copy = metabase_copy_dashboard(client, 1)
>>> print(copy["id"], copy["name"]) # "Copy of ..."
>>> # Deep copy a otra coleccion:
>>> copy = metabase_copy_dashboard(client, 1, name="Sales Q2", collection_id=7, is_deep_copy=True)
"""
body: dict = {"is_deep_copy": is_deep_copy}
if name is not None:
body["name"] = name
if collection_id is not None:
body["collection_id"] = collection_id
if description is not None:
body["description"] = description
return client.request("POST", f"/api/dashboard/{dashboard_id}/copy", json=body)
def metabase_move_dashboard(
client: MetabaseClient,
dashboard_id: int,
collection_id: int | None,
) -> dict:
"""Mueve un dashboard a otra coleccion.
Wrapper thin sobre PUT /api/dashboard/:id que solo actualiza collection_id.
Equivalente a metabase_update_dashboard(client, id, collection_id=...) pero
con intencion explicita y soporte para mover a root con None.
Endpoint: PUT /api/dashboard/:id.
Args:
client: Cliente autenticado.
dashboard_id: ID del dashboard a mover.
collection_id: ID de la coleccion destino. None mueve a "Our analytics" (root).
Returns:
Dict con el dashboard actualizado, incluyendo el nuevo collection_id.
Example:
>>> dash = metabase_move_dashboard(client, 1, collection_id=7)
>>> print(dash["collection_id"]) # 7
>>> # Mover a root:
>>> dash = metabase_move_dashboard(client, 1, collection_id=None)
"""
return client.request("PUT", f"/api/dashboard/{dashboard_id}", json={"collection_id": collection_id})
def metabase_create_dashboard_raw(client: MetabaseClient, payload: dict) -> dict:
"""Crea un dashboard en Metabase con payload completo ya construido por el caller.
Version raw de metabase_create_dashboard. El caller es responsable de
construir el payload completo — no se realiza validacion ni transformacion
local. Util para flujos "Metabase as code" donde el YAML define todos los
campos del dashboard tal como los espera la API.
COMPORTAMIENTO EN DOS PASOS (limitacion de la API de Metabase):
El endpoint POST /api/dashboard NO acepta `dashcards` en el body inicial;
solo crea el dashboard vacio. Para añadir cards es necesario un PUT posterior.
Esta funcion maneja esa limitacion automaticamente:
1. Si el payload contiene `dashcards`, se extraen antes del POST.
2. POST /api/dashboard crea el dashboard vacio (sin dashcards).
3. Si habia dashcards, PUT /api/dashboard/:id con {"dashcards": [...]}
las añade al dashboard recien creado.
4. Retorna la respuesta del PUT (con dashcards pobladas), o la del POST
si el payload original no contenia dashcards.
Endpoint inicial: POST /api/dashboard.
Endpoint secundario (condicional): PUT /api/dashboard/:id.
El payload puede incluir:
- name (str, requerido): nombre del dashboard.
- description (str): descripcion.
- collection_id (int): ID de coleccion destino.
- parameters (list[dict]): filtros/parametros del dashboard.
- dashcards (list[dict]): cards posicionadas. Cada dashcard necesita:
id (int negativo para nuevas, ej: -1, -2),
card_id (int), size_x (int), size_y (int), col (int), row (int).
Campos opcionales: visualization_settings, parameter_mappings.
- tabs (list[dict]): pestañas del dashboard.
- enable_embedding (bool): habilitar embedding publico.
- embedding_params (dict): configuracion de embedding.
Si Metabase devuelve 4xx/5xx en cualquier paso, httpx lanza HTTPStatusError.
Args:
client: Cliente autenticado con sesion activa.
payload: Dict con el payload completo del dashboard tal como lo espera
la API de Metabase. Puede incluir dashcards.
Returns:
Dict con el dashboard creado. Si habia dashcards, la respuesta es la
del PUT final e incluye el campo `dashcards` con las cards posicionadas.
Si no habia dashcards, es la respuesta del POST inicial.
Example:
>>> dash = metabase_create_dashboard_raw(client, {
... "name": "Sales Overview",
... "description": "KPIs de ventas mensuales",
... "collection_id": 5,
... "parameters": [],
... "dashcards": [
... {
... "id": -1,
... "card_id": 42,
... "size_x": 6,
... "size_y": 4,
... "col": 0,
... "row": 0,
... "visualization_settings": {},
... "parameter_mappings": [],
... },
... ],
... })
>>> print(dash["id"]) # ID asignado por Metabase
>>> print(len(dash["dashcards"])) # 1
"""
body = {k: v for k, v in payload.items() if k != "dashcards"}
dashcards = payload.get("dashcards")
created = client.request("POST", "/api/dashboard", json=body)
if dashcards:
dashboard_id = created["id"]
return client.request(
"PUT",
f"/api/dashboard/{dashboard_id}",
json={"dashcards": dashcards},
)
return created