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.
This commit is contained in:
2026-04-13 23:31:42 +02:00
parent e42c59de16
commit 4300f1242d
53 changed files with 5102 additions and 5 deletions
+159
View File
@@ -141,3 +141,162 @@ def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None
>>> # 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