4300f1242d
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.
303 lines
11 KiB
Python
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
|