Files
fn_registry/python/functions/metabase/cards.py
T
egutierrez 58539f45c9 feat(metabase): expansion cards y documents — export, model, ProseMirror validation, copy nativo
Cards: export_card (CSV/XLSX/JSON), create_model (type=model para fuentes MBQL).
Documents: prosemirror_card_embed helper (resizeNode envolviendo cardEmbed),
validacion automatica contra whitelist TipTap antes de enviar, copy_document
refactorizado al endpoint nativo POST /api/document/:id/copy.
Docs: dataset_query legacy vs MBQL5, template-tags, whitelist de nodos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:03:19 +02:00

437 lines
15 KiB
Python

"""CRUD de cards/preguntas de Metabase y ejecucion de queries."""
from .client import MetabaseClient
def metabase_list_cards(
client: MetabaseClient,
filter: str = "",
model_id: int = 0,
) -> list[dict]:
"""Lista preguntas/cards de Metabase con filtro opcional.
Endpoint: GET /api/card. No tiene paginacion offset/limit.
Args:
client: Cliente autenticado.
filter: "all", "mine", "fav", "archived", "recent", "popular",
"database", "table". Vacio = todas.
model_id: ID de database/tabla. Solo aplica con filter "database" o "table".
Returns:
Lista de dicts, cada uno con: id, name, description, display,
collection_id, database_id, creator_id, archived, dataset_query.
Example:
>>> cards = metabase_list_cards(client, filter="mine")
>>> for c in cards:
... print(c["id"], c["name"], c["display"])
"""
params = {}
if filter:
params["f"] = filter
if model_id > 0:
params["model_id"] = model_id
return client.request("GET", "/api/card", params=params)
def metabase_get_card(client: MetabaseClient, card_id: int) -> dict:
"""Obtiene los detalles completos de una card/pregunta.
Endpoint: GET /api/card/:id.
Args:
client: Cliente autenticado.
card_id: ID de la card.
Returns:
Dict con: id, name, description, display, dataset_query,
visualization_settings, collection_id, database_id, archived,
creator, created_at, updated_at.
Example:
>>> card = metabase_get_card(client, 42)
>>> print(card["name"], card["display"])
>>> print(card["dataset_query"]["native"]["query"]) # SQL
"""
return client.request("GET", f"/api/card/{card_id}")
def metabase_create_card(
client: MetabaseClient,
name: str,
dataset_query: dict,
display: str = "table",
collection_id: int = 0,
description: str = "",
) -> dict:
"""Crea una nueva card/pregunta en Metabase.
Endpoint: POST /api/card.
Args:
client: Cliente autenticado.
name: Nombre de la pregunta.
dataset_query: Query de la card. Estructura:
SQL nativo: {"database": 1, "type": "native", "native": {"query": "SELECT ..."}}
MBQL: {"database": 1, "type": "query", "query": {"source-table": 4, ...}}
display: Tipo de visualizacion: "table", "bar", "line", "pie", "scalar",
"area", "row", "combo", "funnel", "scatter", "waterfall", etc.
collection_id: ID de coleccion destino. 0 = root.
description: Descripcion opcional.
Returns:
Dict con la card creada.
Example:
>>> card = metabase_create_card(client, "Revenue by Month", {
... "database": 1,
... "type": "native",
... "native": {"query": "SELECT date_trunc('month', created_at), SUM(total) FROM orders GROUP BY 1"},
... }, display="line", description="Monthly revenue trend")
"""
body: dict = {
"name": name,
"dataset_query": dataset_query,
"display": display,
"visualization_settings": {},
}
if collection_id > 0:
body["collection_id"] = collection_id
if description:
body["description"] = description
return client.request("POST", "/api/card", json=body)
def metabase_update_card(client: MetabaseClient, card_id: int, **fields) -> dict:
"""Actualiza campos de una card/pregunta en Metabase.
Endpoint: PUT /api/card/:id. Solo se modifican los campos pasados.
Args:
client: Cliente autenticado.
card_id: ID de la card.
**fields: Campos a actualizar. Validos:
name (str), description (str), display (str),
dataset_query (dict), visualization_settings (dict),
collection_id (int), archived (bool),
enable_embedding (bool), embedding_params (dict).
Returns:
Dict con la card actualizada.
Example:
>>> metabase_update_card(client, 42, name="Updated Name", archived=True)
>>> metabase_update_card(client, 42, dataset_query={
... "database": 1, "type": "native",
... "native": {"query": "SELECT * FROM users LIMIT 100"},
... })
"""
return client.request("PUT", f"/api/card/{card_id}", json=fields)
def metabase_delete_card(client: MetabaseClient, card_id: int) -> None:
"""Elimina permanentemente una card/pregunta.
Endpoint: DELETE /api/card/:id. IRREVERSIBLE.
Para soft-delete preferir: metabase_update_card(client, card_id, archived=True)
Args:
client: Cliente autenticado.
card_id: ID de la card a eliminar.
Example:
>>> metabase_delete_card(client, 42)
>>> # Preferir soft-delete: metabase_update_card(client, 42, archived=True)
"""
client.request("DELETE", f"/api/card/{card_id}")
def metabase_execute_card(
client: MetabaseClient,
card_id: int,
parameters: list[dict] | None = None,
) -> dict:
"""Ejecuta la query de una card/pregunta guardada.
Endpoint: POST /api/card/:id/query.
Args:
client: Cliente autenticado.
card_id: ID de la card a ejecutar.
parameters: Parametros para queries parametrizadas. Cada parametro:
{"type": "category", "target": ["variable", ["template-tag", "tag"]], "value": "val"}
Returns:
Dict con resultados:
- status: "completed" o "failed"
- row_count: numero de filas
- running_time: tiempo en ms
- data.columns: nombres de columnas
- data.rows: filas de datos (lista de listas)
- data.cols: metadata de columnas
- data.native_form.query: SQL ejecutado
Example:
>>> result = metabase_execute_card(client, 42)
>>> for row in result["data"]["rows"]:
... print(row)
>>> # Con parametros:
>>> result = metabase_execute_card(client, 42, parameters=[
... {"type": "category", "target": ["variable", ["template-tag", "status"]], "value": "active"},
... ])
"""
body = {}
if parameters:
body["parameters"] = parameters
return client.request("POST", f"/api/card/{card_id}/query", json=body or None)
def metabase_execute_query(
client: MetabaseClient,
database_id: int,
sql: str,
max_results: int = 0,
) -> dict:
"""Ejecuta una query SQL ad-hoc sin guardarla como card.
Endpoint: POST /api/dataset. Util para exploracion rapida y consultas
que no necesitan persistirse.
Args:
client: Cliente autenticado.
database_id: ID de la database en Metabase.
sql: Query SQL a ejecutar.
max_results: Limite de filas. 0 = default 2000.
Returns:
Dict con misma estructura que metabase_execute_card:
data.columns, data.rows, row_count, running_time, status.
Example:
>>> result = metabase_execute_query(client, 1, "SELECT * FROM users LIMIT 10")
>>> print(f"{result['row_count']} filas en {result['running_time']}ms")
>>> for row in result["data"]["rows"]:
... print(row)
"""
body: dict = {
"database": database_id,
"type": "native",
"native": {"query": sql},
}
if max_results > 0:
body["constraints"] = {
"max-results": max_results,
"max-results-bare-rows": max_results,
}
return client.request("POST", "/api/dataset", json=body)
def metabase_copy_card(
client: MetabaseClient,
card_id: int,
name: str | None = None,
collection_id: int | None = None,
description: str | None = None,
) -> dict:
"""Crea una copia de una card/pregunta existente en Metabase.
Endpoint: POST /api/card/:id/copy. Usa el endpoint nativo de Metabase para
duplicar la card, copiando dataset_query, display y visualization_settings.
Los campos name, collection_id y description se pueden sobrescribir via body.
Args:
client: Cliente autenticado.
card_id: ID de la card 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.
Returns:
Dict con la card nueva creada por Metabase. Incluye el campo `id`
asignado a la copia y todos los campos heredados del original.
Example:
>>> copy = metabase_copy_card(client, 42)
>>> print(copy["id"], copy["name"]) # "Copy of ..."
>>> # Copiar a otra coleccion con nombre propio:
>>> copy = metabase_copy_card(client, 42, name="Revenue Q2", collection_id=7)
"""
body: dict = {}
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/card/{card_id}/copy", json=body or None)
def metabase_move_card(
client: MetabaseClient,
card_id: int,
collection_id: int | None,
) -> dict:
"""Mueve una card/pregunta a otra coleccion.
Wrapper thin sobre PUT /api/card/:id que solo actualiza collection_id.
Equivalente a metabase_update_card(client, card_id, collection_id=...) pero
con intencion explicita y soporte para mover a root con None.
Endpoint: PUT /api/card/:id.
Args:
client: Cliente autenticado.
card_id: ID de la card a mover.
collection_id: ID de la coleccion destino. None mueve a "Our analytics" (root).
Returns:
Dict con la card actualizada, incluyendo el nuevo collection_id.
Example:
>>> card = metabase_move_card(client, 42, collection_id=7)
>>> print(card["collection_id"]) # 7
>>> # Mover a root:
>>> card = metabase_move_card(client, 42, collection_id=None)
"""
return client.request("PUT", f"/api/card/{card_id}", json={"collection_id": collection_id})
def metabase_create_card_raw(client: MetabaseClient, payload: dict) -> dict:
"""Crea una card en Metabase con payload completo ya construido por el caller.
Version raw de metabase_create_card. El caller es responsable de construir
el payload completo antes de llamar a esta funcion — no se realiza ninguna
validacion ni transformacion local. Util para flujos "Metabase as code"
donde el YAML define todos los campos de la card tal como los espera la API.
Endpoint: POST /api/card.
El payload minimo necesita:
- name (str): nombre de la card.
- dataset_query (dict): query SQL nativa o MBQL.
- display (str): tipo de visualizacion (table, bar, scalar, etc.).
Campos opcionales que esta funcion preserva (a diferencia de metabase_create_card):
- visualization_settings (dict): configuracion detallada del grafico.
- parameters (list[dict]): parametros de la query con template tags.
- parameter_mappings (list[dict]): mapeo de parametros a dashboard filters.
- type (str): "question" (default), "model", "metric".
- collection_id (int): ID de coleccion destino.
- description (str): descripcion de la card.
- archived (bool): estado de archivo inicial.
- enable_embedding (bool): habilitar embedding publico.
- embedding_params (dict): configuracion de embedding.
Si Metabase devuelve 4xx/5xx, httpx lanza HTTPStatusError sin capturar.
Args:
client: Cliente autenticado con sesion activa.
payload: Dict con el payload completo de la card tal como lo espera
la API de Metabase. Se envia sin modificaciones.
Returns:
Dict con la card recien creada. Incluye el campo `id` asignado por
Metabase y todos los campos normalizados (display, dataset_query,
visualization_settings, created_at, etc.).
Example:
>>> card = metabase_create_card_raw(client, {
... "name": "Revenue by Month",
... "dataset_query": {
... "database": 1,
... "type": "native",
... "native": {"query": "SELECT date_trunc('month', created_at), SUM(total) FROM orders GROUP BY 1"},
... },
... "display": "line",
... "visualization_settings": {
... "graph.x_axis.title_text": "Month",
... "graph.y_axis.title_text": "Revenue",
... },
... "description": "Monthly revenue trend",
... "collection_id": 5,
... })
>>> print(card["id"]) # ID asignado por Metabase
"""
return client.request("POST", "/api/card", json=payload)
def metabase_export_card(
client: MetabaseClient,
card_id: int,
format: str = "csv",
) -> bytes:
"""Exporta los resultados de una card en CSV, XLSX o JSON.
Endpoint: POST /api/card/:id/query/:format.
Args:
client: Cliente autenticado.
card_id: ID de la card.
format: Formato de exportación: "csv", "xlsx" o "json".
Returns:
bytes con el contenido del archivo exportado.
Example:
>>> data = metabase_export_card(client, 42, format="csv")
>>> with open("export.csv", "wb") as f:
... f.write(data)
"""
resp = client._http.request("POST", f"/api/card/{card_id}/query/{format}")
resp.raise_for_status()
return resp.content
def metabase_create_model(
client: MetabaseClient,
name: str,
sql: str,
database_id: int,
collection_id: int = 0,
description: str = "",
) -> dict:
"""Crea un modelo (card tipo model) que otras cards pueden referenciar.
Un modelo es una card con type="model". Otras cards MBQL pueden usarlo
como fuente con source-table: "card__<model_id>".
Endpoint: POST /api/card con type="model".
Args:
client: Cliente autenticado.
name: Nombre del modelo.
sql: Query SQL del modelo.
database_id: ID de la database en Metabase.
collection_id: Coleccion destino. 0 = root.
description: Descripcion opcional.
Returns:
Dict con el modelo creado (id, name, type="model").
Example:
>>> model = metabase_create_model(client, "supply_orders_base",
... "SELECT * FROM supply_orders WHERE ...", database_id=6)
>>> # Usar en otra card MBQL:
>>> card = metabase_create_card(client, "Revenue", {
... "database": 6, "type": "query",
... "query": {"source-table": f"card__{model['id']}"}
... })
"""
body: dict = {
"name": name,
"type": "model",
"dataset_query": {
"database": database_id,
"type": "native",
"native": {"query": sql},
},
"display": "table",
"visualization_settings": {},
}
if collection_id > 0:
body["collection_id"] = collection_id
if description:
body["description"] = description
return client.request("POST", "/api/card", json=body)