merge: quick/metabase-expansion-v2 — expansion Metabase: snippets, notifications, filters, export, ProseMirror
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
from .client import MetabaseClient
|
from .client import MetabaseClient
|
||||||
from .users import metabase_list_users, metabase_get_user, metabase_create_user, metabase_update_user, metabase_deactivate_user
|
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, metabase_copy_card, metabase_move_card
|
from .cards import metabase_list_cards, metabase_get_card, metabase_create_card, metabase_update_card, metabase_delete_card, metabase_execute_card, metabase_execute_query, metabase_copy_card, metabase_move_card, metabase_export_card, metabase_create_model
|
||||||
from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard, metabase_copy_dashboard, metabase_move_dashboard
|
from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard, metabase_copy_dashboard, metabase_move_dashboard
|
||||||
from .databases import metabase_list_databases, metabase_add_database, metabase_get_database
|
from .databases import metabase_list_databases, metabase_add_database, metabase_get_database
|
||||||
from .documents import metabase_list_documents, metabase_get_document, metabase_create_document, metabase_update_document, metabase_archive_document, metabase_delete_document, metabase_list_document_comments, metabase_create_document_comment, metabase_resolve_document_comment, metabase_move_document, metabase_copy_document
|
from .documents import metabase_list_documents, metabase_get_document, metabase_create_document, metabase_update_document, metabase_archive_document, metabase_delete_document, metabase_list_document_comments, metabase_create_document_comment, metabase_resolve_document_comment, metabase_move_document, metabase_copy_document, prosemirror_card_embed
|
||||||
|
from .snippets import metabase_list_snippets, metabase_get_snippet, metabase_create_snippet, metabase_update_snippet, metabase_archive_snippet
|
||||||
|
from .notifications import metabase_list_notifications, metabase_create_card_alert, metabase_create_dashboard_subscription, metabase_update_notification, metabase_delete_notification
|
||||||
|
from .dashboard_filters import metabase_add_dashboard_filter
|
||||||
from .collections import metabase_move_collection
|
from .collections import metabase_move_collection
|
||||||
from .permissions import metabase_list_groups, metabase_get_group, metabase_create_group, metabase_update_group, metabase_delete_group, metabase_list_memberships, metabase_add_membership, metabase_delete_membership, metabase_get_permission_graph, metabase_update_permission_graph, metabase_get_collection_graph, metabase_update_collection_graph
|
from .permissions import metabase_list_groups, metabase_get_group, metabase_create_group, metabase_update_group, metabase_delete_group, metabase_list_memberships, metabase_add_membership, metabase_delete_membership, metabase_get_permission_graph, metabase_update_permission_graph, metabase_get_collection_graph, metabase_update_collection_graph
|
||||||
from .setup import metabase_setup
|
from .setup import metabase_setup
|
||||||
@@ -15,13 +18,16 @@ __all__ = [
|
|||||||
"MetabaseClient",
|
"MetabaseClient",
|
||||||
"metabase_list_users", "metabase_get_user", "metabase_create_user", "metabase_update_user", "metabase_deactivate_user",
|
"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_cards", "metabase_get_card", "metabase_create_card", "metabase_update_card", "metabase_delete_card", "metabase_execute_card", "metabase_execute_query",
|
||||||
"metabase_copy_card", "metabase_move_card",
|
"metabase_copy_card", "metabase_move_card", "metabase_export_card", "metabase_create_model",
|
||||||
"metabase_list_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
|
"metabase_list_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
|
||||||
"metabase_copy_dashboard", "metabase_move_dashboard",
|
"metabase_copy_dashboard", "metabase_move_dashboard",
|
||||||
"metabase_list_databases", "metabase_add_database", "metabase_get_database",
|
"metabase_list_databases", "metabase_add_database", "metabase_get_database",
|
||||||
"metabase_list_documents", "metabase_get_document", "metabase_create_document", "metabase_update_document", "metabase_archive_document", "metabase_delete_document",
|
"metabase_list_documents", "metabase_get_document", "metabase_create_document", "metabase_update_document", "metabase_archive_document", "metabase_delete_document",
|
||||||
"metabase_list_document_comments", "metabase_create_document_comment", "metabase_resolve_document_comment",
|
"metabase_list_document_comments", "metabase_create_document_comment", "metabase_resolve_document_comment",
|
||||||
"metabase_move_document", "metabase_copy_document",
|
"metabase_move_document", "metabase_copy_document", "prosemirror_card_embed",
|
||||||
|
"metabase_list_snippets", "metabase_get_snippet", "metabase_create_snippet", "metabase_update_snippet", "metabase_archive_snippet",
|
||||||
|
"metabase_list_notifications", "metabase_create_card_alert", "metabase_create_dashboard_subscription", "metabase_update_notification", "metabase_delete_notification",
|
||||||
|
"metabase_add_dashboard_filter",
|
||||||
"metabase_move_collection",
|
"metabase_move_collection",
|
||||||
"metabase_list_groups", "metabase_get_group", "metabase_create_group", "metabase_update_group", "metabase_delete_group",
|
"metabase_list_groups", "metabase_get_group", "metabase_create_group", "metabase_update_group", "metabase_delete_group",
|
||||||
"metabase_list_memberships", "metabase_add_membership", "metabase_delete_membership",
|
"metabase_list_memberships", "metabase_add_membership", "metabase_delete_membership",
|
||||||
|
|||||||
@@ -354,3 +354,83 @@ def metabase_create_card_raw(client: MetabaseClient, payload: dict) -> dict:
|
|||||||
>>> print(card["id"]) # ID asignado por Metabase
|
>>> print(card["id"]) # ID asignado por Metabase
|
||||||
"""
|
"""
|
||||||
return client.request("POST", "/api/card", json=payload)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Helpers para añadir filtros a dashboards de Metabase."""
|
||||||
|
|
||||||
|
import uuid as _uuid
|
||||||
|
|
||||||
|
from .client import MetabaseClient
|
||||||
|
from .metabase_update_dashboard_safe import metabase_update_dashboard_safe
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_add_dashboard_filter(
|
||||||
|
client: MetabaseClient,
|
||||||
|
dashboard_id: int,
|
||||||
|
name: str,
|
||||||
|
slug: str,
|
||||||
|
filter_type: str,
|
||||||
|
card_mappings: list[dict],
|
||||||
|
*,
|
||||||
|
default: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Añade un filtro a un dashboard y lo conecta a las cards indicadas.
|
||||||
|
|
||||||
|
Crea un parametro a nivel de dashboard y actualiza los parameter_mappings
|
||||||
|
de cada dashcard indicada. Usa metabase_update_dashboard_safe internamente.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
dashboard_id: ID del dashboard.
|
||||||
|
name: Nombre visible del filtro (ej: "Fecha desde").
|
||||||
|
slug: Slug URL-friendly (ej: "fecha_desde").
|
||||||
|
filter_type: Tipo del filtro. Comunes: "string/=", "number/=",
|
||||||
|
"date/single", "date/range", "date/relative", "category".
|
||||||
|
card_mappings: Lista de dicts con la conexión filtro→card. Formato:
|
||||||
|
[{"card_id": 42, "template_tag": "fecha_desde"}, ...]
|
||||||
|
Cada dict indica qué card y qué template-tag SQL debe recibir el valor.
|
||||||
|
default: Valor por defecto del filtro (opcional).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con resultado de metabase_update_dashboard_safe.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = metabase_add_dashboard_filter(
|
||||||
|
... client, 887, "Fecha desde", "fecha_desde", "string/=",
|
||||||
|
... [{"card_id": 7711, "template_tag": "fecha_desde"},
|
||||||
|
... {"card_id": 7719, "template_tag": "fecha_desde"}],
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
from .dashboards import metabase_get_dashboard
|
||||||
|
|
||||||
|
# 1. Leer dashboard actual
|
||||||
|
dash = metabase_get_dashboard(client, dashboard_id)
|
||||||
|
current_params = dash.get("parameters", [])
|
||||||
|
current_dashcards = dash.get("dashcards", [])
|
||||||
|
|
||||||
|
# 2. Crear el parameter
|
||||||
|
param_id = _uuid.uuid4().hex[:8]
|
||||||
|
new_param = {
|
||||||
|
"id": param_id,
|
||||||
|
"type": filter_type,
|
||||||
|
"name": name,
|
||||||
|
"slug": slug,
|
||||||
|
}
|
||||||
|
if default is not None:
|
||||||
|
new_param["default"] = default
|
||||||
|
|
||||||
|
updated_params = current_params + [new_param]
|
||||||
|
|
||||||
|
# 3. Build card_id → template_tag mapping
|
||||||
|
tag_by_card = {m["card_id"]: m["template_tag"] for m in card_mappings}
|
||||||
|
|
||||||
|
# 4. Update dashcards with parameter_mappings
|
||||||
|
updated_dashcards = []
|
||||||
|
for dc in current_dashcards:
|
||||||
|
dc_card_id = dc.get("card_id")
|
||||||
|
if dc_card_id in tag_by_card:
|
||||||
|
mappings = list(dc.get("parameter_mappings", []))
|
||||||
|
mappings.append({
|
||||||
|
"parameter_id": param_id,
|
||||||
|
"card_id": dc_card_id,
|
||||||
|
"target": ["variable", ["template-tag", tag_by_card[dc_card_id]]],
|
||||||
|
})
|
||||||
|
dc_copy = {**dc, "parameter_mappings": mappings}
|
||||||
|
updated_dashcards.append(dc_copy)
|
||||||
|
else:
|
||||||
|
updated_dashcards.append(dc)
|
||||||
|
|
||||||
|
# 5. Update via safe function
|
||||||
|
return metabase_update_dashboard_safe(
|
||||||
|
client, dashboard_id,
|
||||||
|
dashcards_update=updated_dashcards,
|
||||||
|
extra_fields={"parameters": updated_params},
|
||||||
|
)
|
||||||
@@ -4,22 +4,91 @@ Un document es un texto editable en Metabase (tipo Notion) serializado como
|
|||||||
ProseMirror JSON (content_type: application/json+vnd.prose-mirror). Permite
|
ProseMirror JSON (content_type: application/json+vnd.prose-mirror). Permite
|
||||||
embeber cards, smart links y flex containers.
|
embeber cards, smart links y flex containers.
|
||||||
|
|
||||||
Nodos ProseMirror soportados (observados en Metabase v0.59):
|
NODOS QUE RENDERIZAN (whitelist TipTap v0.59):
|
||||||
- doc, paragraph, heading (attrs.level 1-6), text
|
doc, paragraph, text, heading (level 1-6),
|
||||||
- bulletList, orderedList, listItem
|
bulletList, orderedList, listItem,
|
||||||
- blockquote, codeBlock (attrs.language), horizontalRule, hardBreak
|
blockquote, codeBlock (attrs.language), horizontalRule, hardBreak,
|
||||||
- cardEmbed (attrs.id — card_id), smartLink (attrs.entityId),
|
cardEmbed (attrs.id), flexContainer, smartLink (attrs.entityId),
|
||||||
flexContainer (attrs.columnWidths), resizeNode, mention
|
resizeNode, mention.
|
||||||
- image, iframe, table/tableRow/tableCell, callout, taskList/taskItem, details
|
|
||||||
|
|
||||||
Marks:
|
NODOS QUE LA API ACEPTA PERO EL FRONTEND IGNORA (doc queda vacio):
|
||||||
- bold, italic, strike, code, link (attrs.href), underline,
|
callout, taskList, taskItem, details, table, tableRow, tableCell,
|
||||||
highlight, subscript, textStyle
|
image, iframe.
|
||||||
|
|
||||||
|
MARKS QUE RENDERIZAN: bold, italic, strike, code, link (attrs.href).
|
||||||
|
MARKS IGNORADOS: underline, highlight, subscript, textStyle.
|
||||||
|
|
||||||
|
IMPORTANTE — cardEmbed:
|
||||||
|
Un cardEmbed desnudo renderiza pero queda muy pequeño (alto ~50px).
|
||||||
|
Para que se vea correctamente, envolver en un resizeNode:
|
||||||
|
|
||||||
|
{"type": "resizeNode",
|
||||||
|
"attrs": {"height": 400, "minHeight": 280},
|
||||||
|
"content": [
|
||||||
|
{"type": "cardEmbed", "attrs": {"id": <card_id>}}
|
||||||
|
]}
|
||||||
|
|
||||||
|
Usar el helper `prosemirror_card_embed(card_id)` para generar esto
|
||||||
|
automaticamente.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
from .client import MetabaseClient
|
from .client import MetabaseClient
|
||||||
|
|
||||||
|
|
||||||
|
def prosemirror_card_embed(card_id: int, height: int = 400) -> dict:
|
||||||
|
"""Genera un nodo cardEmbed envuelto en resizeNode listo para ProseMirror.
|
||||||
|
|
||||||
|
Un cardEmbed desnudo renderiza pero queda muy pequeño (~50px). Metabase
|
||||||
|
espera que vaya dentro de un resizeNode con height/minHeight para que
|
||||||
|
se vea con tamaño adecuado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
card_id: ID de la card/pregunta de Metabase a embeber.
|
||||||
|
height: Altura en pixeles del embed (default 400). minHeight se
|
||||||
|
fija en 280 (lo que usa la UI de Metabase).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict ProseMirror: resizeNode > cardEmbed, insertable directamente
|
||||||
|
en el array content de un document.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> node = prosemirror_card_embed(7711, height=500)
|
||||||
|
>>> doc = {"type": "doc", "content": [
|
||||||
|
... {"type": "heading", "attrs": {"level": 1},
|
||||||
|
... "content": [{"type": "text", "text": "Mi reporte"}]},
|
||||||
|
... node,
|
||||||
|
... ]}
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"type": "resizeNode",
|
||||||
|
"attrs": {"height": height, "minHeight": 280},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "cardEmbed",
|
||||||
|
"attrs": {"id": card_id, "name": None, "_id": str(uuid.uuid4())},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_before_send(name: str, document: dict | str) -> None:
|
||||||
|
"""Valida el payload del document antes de enviarlo. Raises ValueError."""
|
||||||
|
if not document or document == "":
|
||||||
|
return
|
||||||
|
if not isinstance(document, dict):
|
||||||
|
return
|
||||||
|
from .validation import metabase_validate_document_payload
|
||||||
|
|
||||||
|
issues = metabase_validate_document_payload({"name": name, "document": document})
|
||||||
|
if issues:
|
||||||
|
raise ValueError(
|
||||||
|
f"Document no renderizará correctamente en Metabase "
|
||||||
|
f"({len(issues)} issues):\n" + "\n".join(f" - {i}" for i in issues)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def metabase_list_documents(client: MetabaseClient) -> list[dict]:
|
def metabase_list_documents(client: MetabaseClient) -> list[dict]:
|
||||||
"""Lista documents de Metabase.
|
"""Lista documents de Metabase.
|
||||||
|
|
||||||
@@ -69,11 +138,21 @@ def metabase_create_document(
|
|||||||
name: str,
|
name: str,
|
||||||
document: dict,
|
document: dict,
|
||||||
collection_id: int = 0,
|
collection_id: int = 0,
|
||||||
|
*,
|
||||||
|
validate: bool = True,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Crea un document nuevo.
|
"""Crea un document nuevo.
|
||||||
|
|
||||||
Endpoint: POST /api/document.
|
Endpoint: POST /api/document.
|
||||||
|
|
||||||
|
Valida el arbol ProseMirror ANTES de enviar (por defecto). Si el
|
||||||
|
documento contiene nodos que la API acepta pero el frontend ignora
|
||||||
|
(callout, taskList, image, etc.), lanza ValueError con los issues.
|
||||||
|
Pasar validate=False para desactivar (uso bajo tu riesgo).
|
||||||
|
|
||||||
|
Para embeber cards, usar prosemirror_card_embed(card_id) que genera
|
||||||
|
el nodo resizeNode > cardEmbed con la altura correcta.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Cliente autenticado.
|
client: Cliente autenticado.
|
||||||
name: Titulo del document (1-254 chars, no blank).
|
name: Titulo del document (1-254 chars, no blank).
|
||||||
@@ -81,19 +160,28 @@ def metabase_create_document(
|
|||||||
{"type": "doc", "content": [{"type": "paragraph", "content": [...]}]}
|
{"type": "doc", "content": [{"type": "paragraph", "content": [...]}]}
|
||||||
O cadena vacia "" si se quiere arrancar en blanco.
|
O cadena vacia "" si se quiere arrancar en blanco.
|
||||||
collection_id: ID de coleccion destino. 0 = root.
|
collection_id: ID de coleccion destino. 0 = root.
|
||||||
|
validate: Si True (default), valida el ProseMirror antes de enviar.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Document creado con su id asignado.
|
Document creado con su id asignado.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Si validate=True y el arbol ProseMirror contiene
|
||||||
|
nodos o marks que el frontend de Metabase no renderiza.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> doc = metabase_create_document(client, "Notas", {
|
>>> from metabase.documents import prosemirror_card_embed
|
||||||
|
>>> doc = metabase_create_document(client, "Reporte", {
|
||||||
... "type": "doc",
|
... "type": "doc",
|
||||||
... "content": [{
|
... "content": [
|
||||||
... "type": "paragraph",
|
... {"type": "heading", "attrs": {"level": 1},
|
||||||
... "content": [{"type": "text", "text": "Hola mundo"}]
|
... "content": [{"type": "text", "text": "KPIs"}]},
|
||||||
... }]
|
... prosemirror_card_embed(42, height=450),
|
||||||
|
... ]
|
||||||
... })
|
... })
|
||||||
"""
|
"""
|
||||||
|
if validate:
|
||||||
|
_validate_before_send(name, document)
|
||||||
body: dict = {"name": name, "document": document}
|
body: dict = {"name": name, "document": document}
|
||||||
if collection_id > 0:
|
if collection_id > 0:
|
||||||
body["collection_id"] = collection_id
|
body["collection_id"] = collection_id
|
||||||
@@ -103,26 +191,39 @@ def metabase_create_document(
|
|||||||
def metabase_update_document(
|
def metabase_update_document(
|
||||||
client: MetabaseClient,
|
client: MetabaseClient,
|
||||||
document_id: int,
|
document_id: int,
|
||||||
|
*,
|
||||||
|
validate: bool = True,
|
||||||
**fields,
|
**fields,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Actualiza un document. Solo se envian los campos pasados.
|
"""Actualiza un document. Solo se envian los campos pasados.
|
||||||
|
|
||||||
Endpoint: PUT /api/document/:id.
|
Endpoint: PUT /api/document/:id.
|
||||||
|
|
||||||
|
Si se pasa el campo 'document', valida el arbol ProseMirror antes de
|
||||||
|
enviar (por defecto). Pasar validate=False para desactivar.
|
||||||
|
|
||||||
Campos tipicos: name, document, collection_id, archived.
|
Campos tipicos: name, document, collection_id, archived.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Cliente autenticado.
|
client: Cliente autenticado.
|
||||||
document_id: ID del document a actualizar.
|
document_id: ID del document a actualizar.
|
||||||
|
validate: Si True (default), valida el ProseMirror antes de enviar.
|
||||||
**fields: Campos a modificar.
|
**fields: Campos a modificar.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Document actualizado.
|
Document actualizado.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Si validate=True y el campo 'document' contiene
|
||||||
|
nodos o marks no soportados por el frontend de Metabase.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> metabase_update_document(client, 1, name="Nuevo titulo")
|
>>> metabase_update_document(client, 1, name="Nuevo titulo")
|
||||||
>>> metabase_update_document(client, 1, document={"type":"doc","content":[...]})
|
>>> metabase_update_document(client, 1, document={"type":"doc","content":[...]})
|
||||||
"""
|
"""
|
||||||
|
if validate and "document" in fields:
|
||||||
|
name = fields.get("name", f"document_{document_id}")
|
||||||
|
_validate_before_send(name, fields["document"])
|
||||||
return client.request("PUT", f"/api/document/{document_id}", json=fields)
|
return client.request("PUT", f"/api/document/{document_id}", json=fields)
|
||||||
|
|
||||||
|
|
||||||
@@ -282,38 +383,26 @@ def metabase_copy_document(
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
collection_id: int | None = None,
|
collection_id: int | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Copia un document (Metabase no tiene endpoint nativo).
|
"""Copia un document usando el endpoint nativo de Metabase.
|
||||||
|
|
||||||
Obtiene el document original con metabase_get_document, luego crea uno
|
Endpoint: POST /api/document/:id/copy.
|
||||||
nuevo con metabase_create_document clonando el contenido ProseMirror.
|
|
||||||
|
|
||||||
Si name=None usa "{original_name} (copia)".
|
|
||||||
Si collection_id=None copia a la misma coleccion del original.
|
|
||||||
|
|
||||||
Realiza 2 requests HTTP: GET /api/document/:id + POST /api/document.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Cliente autenticado.
|
client: Cliente autenticado.
|
||||||
document_id: ID del document a copiar.
|
document_id: ID del document a copiar.
|
||||||
name: Nombre del nuevo document. None = "{original} (copia)".
|
name: Nombre del nuevo document. None = Metabase asigna nombre automatico.
|
||||||
collection_id: Coleccion destino. None = misma coleccion del original.
|
collection_id: Coleccion destino. None = misma coleccion del original.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Document nuevo recien creado con su id asignado.
|
Document nuevo recien creado con su id asignado.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> copy = metabase_copy_document(client, 42)
|
|
||||||
>>> print(copy["name"]) # "Mi documento (copia)"
|
|
||||||
>>> print(copy["id"]) # nuevo ID
|
|
||||||
|
|
||||||
>>> # Clonar a otra coleccion con nombre personalizado:
|
|
||||||
>>> copy = metabase_copy_document(client, 42, name="Backup Q1", collection_id=5)
|
>>> copy = metabase_copy_document(client, 42, name="Backup Q1", collection_id=5)
|
||||||
|
>>> print(copy["id"])
|
||||||
"""
|
"""
|
||||||
original = metabase_get_document(client, document_id)
|
body: dict = {}
|
||||||
new_name = name if name is not None else f"{original['name']} (copia)"
|
if name is not None:
|
||||||
dest_collection = collection_id if collection_id is not None else original.get("collection_id", 0)
|
body["name"] = name
|
||||||
doc_content = original.get("document", "")
|
if collection_id is not None:
|
||||||
body: dict = {"name": new_name, "document": doc_content}
|
body["collection_id"] = collection_id
|
||||||
if dest_collection:
|
return client.request("POST", f"/api/document/{document_id}/copy", json=body)
|
||||||
body["collection_id"] = dest_collection
|
|
||||||
return client.request("POST", "/api/document", json=body)
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: metabase_add_dashboard_filter
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_add_dashboard_filter(client: MetabaseClient, dashboard_id: int, name: str, slug: str, filter_type: str, card_mappings: list[dict], *, default: str | None = None) -> dict"
|
||||||
|
description: "Añade un filtro a un dashboard de Metabase y lo conecta a las cards indicadas. Crea el parameter a nivel de dashboard y actualiza los parameter_mappings de cada dashcard."
|
||||||
|
tags: [metabase, dashboard, filter, parameter, create, api, python]
|
||||||
|
uses_functions:
|
||||||
|
- metabase_update_dashboard_safe_py_infra
|
||||||
|
- metabase_get_dashboard_py_infra
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [uuid]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: dashboard_id
|
||||||
|
desc: "ID del dashboard al que se añade el filtro"
|
||||||
|
- name: name
|
||||||
|
desc: "nombre visible del filtro en la UI de Metabase (ej: 'Fecha desde')"
|
||||||
|
- name: slug
|
||||||
|
desc: "slug URL-friendly del filtro, unico dentro del dashboard (ej: 'fecha_desde')"
|
||||||
|
- name: filter_type
|
||||||
|
desc: "tipo del filtro: 'string/=', 'number/=', 'date/single', 'date/range', 'date/relative', 'category'"
|
||||||
|
- name: card_mappings
|
||||||
|
desc: "lista de dicts [{card_id: int, template_tag: str}] conectando el filtro a template-tags SQL de las cards"
|
||||||
|
- name: default
|
||||||
|
desc: "valor por defecto del filtro; None = sin default"
|
||||||
|
output: "dict con resultado de metabase_update_dashboard_safe: {added, updated, removed, response}"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboard_filters.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = metabase_add_dashboard_filter(
|
||||||
|
client,
|
||||||
|
dashboard_id=887,
|
||||||
|
name="Fecha desde",
|
||||||
|
slug="fecha_desde",
|
||||||
|
filter_type="string/=",
|
||||||
|
card_mappings=[
|
||||||
|
{"card_id": 7711, "template_tag": "fecha_desde"},
|
||||||
|
{"card_id": 7719, "template_tag": "fecha_desde"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Con valor por defecto
|
||||||
|
result = metabase_add_dashboard_filter(
|
||||||
|
client,
|
||||||
|
dashboard_id=887,
|
||||||
|
name="Estado",
|
||||||
|
slug="estado",
|
||||||
|
filter_type="category",
|
||||||
|
card_mappings=[{"card_id": 7711, "template_tag": "estado"}],
|
||||||
|
default="activo",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Realiza 2 requests internamente via metabase_update_dashboard_safe (GET + PUT).
|
||||||
|
|
||||||
|
El `param_id` se genera como UUID hex de 8 caracteres — formato que usa la UI de Metabase.
|
||||||
|
|
||||||
|
`card_mappings` solo conecta cards que ya existen como dashcards en el dashboard. Cards no presentes en el dashboard se ignoran silenciosamente.
|
||||||
|
|
||||||
|
Para filtros de tipo date, los valores en `default` deben seguir el formato de Metabase: "2024-01-01" para `date/single`, "2024-01-01~2024-12-31" para `date/range`.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: metabase_archive_snippet
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_archive_snippet(client: MetabaseClient, snippet_id: int) -> dict"
|
||||||
|
description: "Archiva un SQL snippet en Metabase. Wrapper sobre metabase_update_snippet con archived=True."
|
||||||
|
tags: [metabase, snippet, archive, api, python]
|
||||||
|
uses_functions: [metabase_update_snippet_py_infra]
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: snippet_id
|
||||||
|
desc: "ID numerico del snippet a archivar"
|
||||||
|
output: "dict: snippet con archived=True y updated_at actualizado"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/snippets.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Archivar snippet obsoleto
|
||||||
|
result = metabase_archive_snippet(client, 42)
|
||||||
|
print(result["archived"]) # True
|
||||||
|
|
||||||
|
# Listar snippets activos despues de archivar
|
||||||
|
active = metabase_list_snippets(client, archived=False)
|
||||||
|
assert all(not s["archived"] for s in active)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Los snippets archivados no aparecen en el autocomplete de queries nativas en el editor de Metabase.
|
||||||
|
Las cards que ya referencian el snippet siguen funcionando correctamente despues de archivar.
|
||||||
|
Para desarchivar, usar metabase_update_snippet(client, snippet_id, archived=False).
|
||||||
@@ -6,9 +6,9 @@ domain: infra
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
purity: impure
|
purity: impure
|
||||||
signature: "def metabase_copy_document(client: MetabaseClient, document_id: int, name: str | None = None, collection_id: int | None = None) -> dict"
|
signature: "def metabase_copy_document(client: MetabaseClient, document_id: int, name: str | None = None, collection_id: int | None = None) -> dict"
|
||||||
description: "Copia un document clonando su contenido ProseMirror. Metabase no tiene endpoint nativo; realiza GET + POST internamente."
|
description: "Copia un document usando el endpoint nativo POST /api/document/:id/copy. Un solo request HTTP."
|
||||||
tags: [metabase, document, copy, clone, prosemirror, api, python]
|
tags: [metabase, document, copy, clone, api, python]
|
||||||
uses_functions: [metabase_get_document_py_infra, metabase_create_document_py_infra]
|
uses_functions: []
|
||||||
uses_types: []
|
uses_types: []
|
||||||
returns: []
|
returns: []
|
||||||
returns_optional: false
|
returns_optional: false
|
||||||
@@ -20,7 +20,7 @@ params:
|
|||||||
- name: document_id
|
- name: document_id
|
||||||
desc: "ID del document original a copiar"
|
desc: "ID del document original a copiar"
|
||||||
- name: name
|
- name: name
|
||||||
desc: "nombre del nuevo document; None usa '{original} (copia)'"
|
desc: "nombre del nuevo document; None = Metabase asigna nombre automatico"
|
||||||
- name: collection_id
|
- name: collection_id
|
||||||
desc: "coleccion destino; None copia a la misma coleccion del original"
|
desc: "coleccion destino; None copia a la misma coleccion del original"
|
||||||
output: "dict: document nuevo recien creado con id asignado y metadata completa"
|
output: "dict: document nuevo recien creado con id asignado y metadata completa"
|
||||||
@@ -33,20 +33,18 @@ file_path: "python/functions/metabase/documents.py"
|
|||||||
## Ejemplo
|
## Ejemplo
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Copia simple con nombre automatico a la misma coleccion
|
# Copia con nombre personalizado a otra coleccion
|
||||||
copy = metabase_copy_document(client, 42)
|
|
||||||
print(copy["name"]) # "Mi documento (copia)"
|
|
||||||
print(copy["id"]) # nuevo ID
|
|
||||||
|
|
||||||
# Clonar a otra coleccion con nombre personalizado
|
|
||||||
copy = metabase_copy_document(client, 42, name="Backup Q1", collection_id=5)
|
copy = metabase_copy_document(client, 42, name="Backup Q1", collection_id=5)
|
||||||
|
print(copy["id"]) # ID del nuevo document
|
||||||
|
|
||||||
|
# Copia a la misma coleccion con nombre automatico de Metabase
|
||||||
|
copy = metabase_copy_document(client, 42)
|
||||||
|
print(copy["name"]) # nombre asignado por Metabase
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notas
|
## Notas
|
||||||
|
|
||||||
Realiza 2 requests HTTP: `GET /api/document/:id` para obtener el original y
|
Usa el endpoint nativo `POST /api/document/:id/copy` — un solo request HTTP.
|
||||||
`POST /api/document` para crear la copia con el mismo arbol ProseMirror.
|
|
||||||
|
|
||||||
Metabase no tiene endpoint `POST /api/document/:id/copy` — esta funcion implementa
|
Metabase asigna el nombre automaticamente si no se especifica `name`. El contenido
|
||||||
la copia en cliente. Los `cardEmbed` del documento original apuntaran a los mismos
|
ProseMirror (incluyendo cardEmbeds) se copia tal cual; las cards embebidas no se duplican.
|
||||||
cards embebidos; no se duplican los cards embebidos.
|
|
||||||
|
|||||||
@@ -43,7 +43,58 @@ card = metabase_create_card(client, "Revenue", {
|
|||||||
}, display="scalar")
|
}, display="scalar")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ejemplo con template-tags (filtros)
|
||||||
|
|
||||||
|
```python
|
||||||
|
card = metabase_create_card(client, "Revenue filtrable", {
|
||||||
|
"database": 6, "type": "native",
|
||||||
|
"native": {
|
||||||
|
"query": "SELECT * FROM orders WHERE 1=1 [[AND date >= {{fecha_desde}}]]",
|
||||||
|
"template-tags": {
|
||||||
|
"fecha_desde": {
|
||||||
|
"name": "fecha_desde",
|
||||||
|
"display-name": "Fecha desde",
|
||||||
|
"id": "fecha_desde_tag",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, display="table")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formato dataset_query — IMPORTANTE
|
||||||
|
|
||||||
|
Hay DOS formatos y hay que saber cuál usar:
|
||||||
|
|
||||||
|
**Para CREAR cards (POST /api/card) — formato legacy:**
|
||||||
|
```python
|
||||||
|
{"database": id, "type": "native", "native": {"query": "SELECT ..."}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lo que DEVUELVE Metabase al LEER cards (GET /api/card/:id) — formato MBQL5:**
|
||||||
|
```python
|
||||||
|
{"lib/type": "mbql/query", "database": id,
|
||||||
|
"stages": [{"lib/type": "mbql.stage/native", "native": "SELECT ..."}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Son representaciones distintas de lo mismo. Al leer una card existente y querer extraer el SQL:
|
||||||
|
```python
|
||||||
|
card = metabase_get_card(client, 42)
|
||||||
|
ds = card["dataset_query"]
|
||||||
|
|
||||||
|
# Formato MBQL5 (Metabase reciente)
|
||||||
|
stages = ds.get("stages", [])
|
||||||
|
if stages:
|
||||||
|
sql = stages[0].get("native", "")
|
||||||
|
|
||||||
|
# Formato legacy
|
||||||
|
else:
|
||||||
|
sql = ds.get("native", {}).get("query", "")
|
||||||
|
```
|
||||||
|
|
||||||
|
Al crear/actualizar cards, usar SIEMPRE el formato legacy — Metabase lo acepta y lo convierte internamente.
|
||||||
|
|
||||||
## Notas
|
## Notas
|
||||||
|
|
||||||
dataset_query SQL nativo: `{"database": id, "type": "native", "native": {"query": "..."}}`
|
- `display` válidos: scalar, table, line, bar, pie, area, row, funnel, combo, pivot, map, scatter, waterfall, gauge, progress, smartscalar, sankey.
|
||||||
dataset_query MBQL: `{"database": id, "type": "query", "query": {"source-table": id, ...}}`
|
- Para queries que se repiten como base de otras cards, considerar crear un "model" (type="model") y referenciar desde otras cards con `source-table: "card__<id>"`.
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_card_alert
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_card_alert(client: MetabaseClient, card_id: int, cron_schedule: str, recipients: list[dict], send_condition: str = 'has_result', send_once: bool = False) -> dict"
|
||||||
|
description: "Crea una alerta sobre los resultados de una card en Metabase. Envia email segun cron cuando la card cumple la condicion (has_result, goal_above, goal_below). Endpoint: POST /api/notification."
|
||||||
|
tags: [metabase, notification, alert, card, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: card_id
|
||||||
|
desc: "ID de la card que dispara la alerta"
|
||||||
|
- name: cron_schedule
|
||||||
|
desc: "expresion cron de 5 campos (ej: '0 9 * * 1' = lunes 9am, '0 8 * * 1-5' = lun-vie 8am)"
|
||||||
|
- name: recipients
|
||||||
|
desc: "lista de destinatarios: usuario Metabase {'type': 'notification-recipient/user', 'user_id': N} o email externo {'type': 'notification-recipient/raw-value', 'details': {'email': 'x@y.com'}}"
|
||||||
|
- name: send_condition
|
||||||
|
desc: "condicion que dispara el envio: 'has_result' (tiene filas), 'goal_above' (supera goal), 'goal_below' (cae bajo goal)"
|
||||||
|
- name: send_once
|
||||||
|
desc: "si True, se envia una sola vez y se desactiva automaticamente"
|
||||||
|
output: "dict: notificacion creada con id, active, payload_type, payload, subscriptions, handlers, created_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/notifications.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Alerta: enviar email los lunes 9am si la card tiene resultados
|
||||||
|
alert = metabase_create_card_alert(
|
||||||
|
client,
|
||||||
|
card_id=7711,
|
||||||
|
cron_schedule="0 9 * * 1",
|
||||||
|
recipients=[
|
||||||
|
{"type": "notification-recipient/user", "user_id": 1},
|
||||||
|
{"type": "notification-recipient/raw-value", "details": {"email": "team@aurgi.com"}},
|
||||||
|
],
|
||||||
|
send_condition="has_result",
|
||||||
|
)
|
||||||
|
print(alert["id"], alert["active"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Requiere Metabase 0.57+. Reemplaza el antiguo /api/pulse.
|
||||||
|
El campo `subscriptions` acepta solo el tipo `notification-subscription/cron`.
|
||||||
|
Para alertas de goal configurar previamente el goal en la visualizacion de la card.
|
||||||
|
`send_once=True` es util para alertas puntuales que no deben repetirse.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_dashboard_subscription
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_dashboard_subscription(client: MetabaseClient, dashboard_id: int, cron_schedule: str, recipients: list[dict]) -> dict"
|
||||||
|
description: "Crea una suscripcion periodica a un dashboard de Metabase. Envia el dashboard completo por email segun el cron configurado. Endpoint: POST /api/notification."
|
||||||
|
tags: [metabase, notification, subscription, dashboard, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: dashboard_id
|
||||||
|
desc: "ID del dashboard a enviar periodicamente"
|
||||||
|
- name: cron_schedule
|
||||||
|
desc: "expresion cron de 5 campos (ej: '0 8 * * 1-5' = lun-vie 8am, '0 9 * * 1' = lunes 9am)"
|
||||||
|
- name: recipients
|
||||||
|
desc: "lista de destinatarios: usuario Metabase {'type': 'notification-recipient/user', 'user_id': N} o email externo {'type': 'notification-recipient/raw-value', 'details': {'email': 'x@y.com'}}"
|
||||||
|
output: "dict: suscripcion creada con id, active, payload_type, payload, subscriptions, handlers, created_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/notifications.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Suscripcion: enviar dashboard de ventas cada lunes a las 8am
|
||||||
|
sub = metabase_create_dashboard_subscription(
|
||||||
|
client,
|
||||||
|
dashboard_id=42,
|
||||||
|
cron_schedule="0 8 * * 1",
|
||||||
|
recipients=[
|
||||||
|
{"type": "notification-recipient/user", "user_id": 5},
|
||||||
|
{"type": "notification-recipient/raw-value", "details": {"email": "gerencia@aurgi.com"}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
print(sub["id"], sub["payload"]["dashboard_id"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Requiere Metabase 0.57+. Reemplaza el antiguo /api/pulse.
|
||||||
|
El dashboard se envia completo con todas sus cards renderizadas.
|
||||||
|
Para suscripciones diarias laborales usar cron "0 8 * * 1-5".
|
||||||
@@ -3,17 +3,17 @@ name: metabase_create_document
|
|||||||
kind: function
|
kind: function
|
||||||
lang: py
|
lang: py
|
||||||
domain: infra
|
domain: infra
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
purity: impure
|
purity: impure
|
||||||
signature: "def metabase_create_document(client: MetabaseClient, name: str, document: dict, collection_id: int = 0) -> dict"
|
signature: "def metabase_create_document(client: MetabaseClient, name: str, document: dict, collection_id: int = 0, *, validate: bool = True) -> dict"
|
||||||
description: "Crea un document nuevo con contenido ProseMirror. Endpoint: POST /api/document. Soporta cardEmbed, smartLink, flexContainer, callout, taskList y demas nodos custom de Metabase."
|
description: "Crea un document con contenido ProseMirror. Valida el arbol contra la whitelist de nodos ANTES de enviar (evita documentos que la API acepta pero el frontend renderiza vacíos). Usar prosemirror_card_embed() para embeber cards."
|
||||||
tags: [metabase, document, create, api, prosemirror, python]
|
tags: [metabase, document, create, api, prosemirror, python]
|
||||||
uses_functions: []
|
uses_functions: [metabase_validate_document_payload_py_infra]
|
||||||
uses_types: []
|
uses_types: []
|
||||||
returns: []
|
returns: []
|
||||||
returns_optional: false
|
returns_optional: false
|
||||||
error_type: "error_go_core"
|
error_type: "error_go_core"
|
||||||
imports: [httpx]
|
imports: [httpx, uuid]
|
||||||
params:
|
params:
|
||||||
- name: client
|
- name: client
|
||||||
desc: "instancia autenticada de MetabaseClient"
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
@@ -23,6 +23,8 @@ params:
|
|||||||
desc: "arbol ProseMirror JSON: {type: 'doc', content: [...]}, o '' para arrancar vacio"
|
desc: "arbol ProseMirror JSON: {type: 'doc', content: [...]}, o '' para arrancar vacio"
|
||||||
- name: collection_id
|
- name: collection_id
|
||||||
desc: "ID de coleccion destino (0 = root)"
|
desc: "ID de coleccion destino (0 = root)"
|
||||||
|
- name: validate
|
||||||
|
desc: "si True (default), valida el ProseMirror antes de enviar. Lanza ValueError si hay nodos no soportados"
|
||||||
output: "dict: document recien creado con id, entity_id y metadata"
|
output: "dict: document recien creado con id, entity_id y metadata"
|
||||||
tested: false
|
tested: false
|
||||||
tests: []
|
tests: []
|
||||||
@@ -33,17 +35,49 @@ file_path: "python/functions/metabase/documents.py"
|
|||||||
## Ejemplo
|
## Ejemplo
|
||||||
|
|
||||||
```python
|
```python
|
||||||
doc = metabase_create_document(client, "Notas", {
|
from metabase.documents import metabase_create_document, prosemirror_card_embed
|
||||||
|
|
||||||
|
doc = metabase_create_document(client, "Reporte Q1", {
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "paragraph", "content": [{"type": "text", "text": "Hola"}]}
|
{"type": "heading", "attrs": {"level": 1},
|
||||||
|
"content": [{"type": "text", "text": "KPIs"}]},
|
||||||
|
{"type": "paragraph",
|
||||||
|
"content": [{"type": "text", "text": "Revenue por canal:"}]},
|
||||||
|
prosemirror_card_embed(42, height=450),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
print(doc["id"])
|
```
|
||||||
|
|
||||||
|
## Nodos ProseMirror — whitelist
|
||||||
|
|
||||||
|
**Renderizan correctamente** (TipTap v0.59):
|
||||||
|
`doc, paragraph, text, heading, bulletList, orderedList, listItem, blockquote, codeBlock, horizontalRule, hardBreak, cardEmbed, flexContainer, smartLink, resizeNode, mention`
|
||||||
|
|
||||||
|
**La API acepta pero el frontend IGNORA** (resultado: documento vacío):
|
||||||
|
`callout, taskList, taskItem, details, table, tableRow, tableCell, image, iframe`
|
||||||
|
|
||||||
|
**Marks que renderizan:** `bold, italic, strike, code, link`
|
||||||
|
**Marks ignorados:** `underline, highlight, subscript, textStyle`
|
||||||
|
|
||||||
|
## cardEmbed — SIEMPRE envolver en resizeNode
|
||||||
|
|
||||||
|
Un `cardEmbed` desnudo renderiza pero queda con ~50px de alto. Metabase espera que vaya dentro de un `resizeNode`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# MAL — card diminuta
|
||||||
|
{"type": "cardEmbed", "attrs": {"id": 42}}
|
||||||
|
|
||||||
|
# BIEN — usar el helper
|
||||||
|
from metabase.documents import prosemirror_card_embed
|
||||||
|
prosemirror_card_embed(42, height=450)
|
||||||
|
# Genera: {"type": "resizeNode", "attrs": {"height": 450, "minHeight": 280},
|
||||||
|
# "content": [{"type": "cardEmbed", "attrs": {"id": 42, ...}}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notas
|
## Notas
|
||||||
|
|
||||||
Nodos custom de Metabase observados (v0.59): `cardEmbed` (attrs.id=card_id), `smartLink` (attrs.entityId), `flexContainer` (attrs.columnWidths), `resizeNode`, `mention`. Marks estandar + `underline`, `highlight`, `subscript`, `textStyle`.
|
- La validación (`validate=True`) llama internamente a `metabase_validate_document_payload`. Si detecta nodos no soportados, lanza `ValueError` ANTES de hacer el POST — evita documentos que se ven vacíos.
|
||||||
|
- Pasar `validate=False` solo si se está experimentando con nodos nuevos.
|
||||||
Cuando embebes un card via `cardEmbed`, Metabase crea una copia interna del card con `document_id` apuntando al document — no referencia el card original.
|
- Para destacar texto, usar `blockquote` (NO `callout`).
|
||||||
|
- Cuando embebes un card via `cardEmbed`, Metabase crea una referencia al card — el card debe existir.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_model
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_model(client: MetabaseClient, name: str, sql: str, database_id: int, collection_id: int = 0, description: str = '') -> dict"
|
||||||
|
description: "Crea un modelo de Metabase (card con type='model') que otras cards MBQL pueden usar como fuente via source-table: 'card__<id>'."
|
||||||
|
tags: [metabase, model, card, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: name
|
||||||
|
desc: "nombre del modelo"
|
||||||
|
- name: sql
|
||||||
|
desc: "query SQL que define el modelo"
|
||||||
|
- name: database_id
|
||||||
|
desc: "ID de la database en Metabase donde vive la query"
|
||||||
|
- name: collection_id
|
||||||
|
desc: "ID de coleccion destino; 0 = root"
|
||||||
|
- name: description
|
||||||
|
desc: "descripcion opcional del modelo"
|
||||||
|
output: "dict con el modelo creado: id, name, type='model', dataset_query y metadata completa"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Crear modelo base
|
||||||
|
model = metabase_create_model(
|
||||||
|
client,
|
||||||
|
name="supply_orders_base",
|
||||||
|
sql="SELECT * FROM supply_orders WHERE status != 'cancelled'",
|
||||||
|
database_id=6,
|
||||||
|
collection_id=42,
|
||||||
|
description="Ordenes de supply excluyendo canceladas",
|
||||||
|
)
|
||||||
|
print(model["id"]) # ej: 7820
|
||||||
|
|
||||||
|
# Usar el modelo como fuente en una card MBQL
|
||||||
|
card = metabase_create_card(client, "Revenue por proveedor", {
|
||||||
|
"database": 6,
|
||||||
|
"type": "query",
|
||||||
|
"query": {
|
||||||
|
"source-table": f"card__{model['id']}",
|
||||||
|
"aggregation": [["sum", ["field", "total", None]]],
|
||||||
|
"breakout": [["field", "supplier_id", None]],
|
||||||
|
},
|
||||||
|
}, display="bar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Un modelo es una card con `type="model"`. Metabase lo trata como una capa de abstraccion — las cards MBQL que lo referencian via `source-table: "card__<id>"` se benefician del schema inferido del modelo (tipos de columna, foreign keys, etc.).
|
||||||
|
|
||||||
|
A diferencia de `metabase_create_card`, esta funcion fuerza `type="model"` y siempre usa query SQL nativa. Para modelos MBQL o con configuracion avanzada (result_metadata, column types), usar `metabase_create_card_raw` con `type="model"` en el payload.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_snippet
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_snippet(client: MetabaseClient, name: str, content: str, description: str = \"\", collection_id: int = 0) -> dict"
|
||||||
|
description: "Crea un nuevo SQL snippet reutilizable en Metabase. El snippet se referencia en queries nativas con {{snippet: nombre}}."
|
||||||
|
tags: [metabase, snippet, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: name
|
||||||
|
desc: "nombre del snippet; se usa en queries con la sintaxis {{snippet: nombre}}"
|
||||||
|
- name: content
|
||||||
|
desc: "SQL del snippet: puede ser una CTE completa, subquery o cualquier fragmento SQL reutilizable"
|
||||||
|
- name: description
|
||||||
|
desc: "descripcion opcional del proposito del snippet"
|
||||||
|
- name: collection_id
|
||||||
|
desc: "ID de la coleccion donde guardar el snippet; 0 para la raiz (Our analytics)"
|
||||||
|
output: "dict: snippet creado con id asignado por Metabase, name, content, description, collection_id, created_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/snippets.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Crear snippet con CTE reutilizable
|
||||||
|
snippet = metabase_create_snippet(
|
||||||
|
client,
|
||||||
|
"supply_orders_cte",
|
||||||
|
"""
|
||||||
|
WITH supply_full AS (
|
||||||
|
SELECT so.id, so.service_request_id,
|
||||||
|
sr.channel_id, ch.name AS channel_name
|
||||||
|
FROM supply_orders so
|
||||||
|
LEFT JOIN service_requests sr ON so.service_request_id = sr.id
|
||||||
|
LEFT JOIN channels ch ON sr.channel_id = ch.id
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
description="CTE base de supply_orders con 6 JOINs para cruce con NAV",
|
||||||
|
)
|
||||||
|
print(snippet["id"], snippet["name"])
|
||||||
|
|
||||||
|
# Usar el snippet en una card
|
||||||
|
from metabase.cards import metabase_create_card
|
||||||
|
card = metabase_create_card(client, "Revenue", {
|
||||||
|
"database": 6,
|
||||||
|
"type": "native",
|
||||||
|
"native": {
|
||||||
|
"query": "{{snippet: supply_orders_cte}} SELECT channel_name, COUNT(*) FROM supply_full GROUP BY 1"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
El campo `name` debe ser unico en Metabase — el servidor retorna 400 si ya existe un snippet con ese nombre.
|
||||||
|
Si `collection_id` es 0 o no se provee, el snippet se guarda en la raiz.
|
||||||
|
Solo se envian al body los campos no vacios/cero para evitar conflictos con defaults del servidor.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: metabase_delete_notification
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_delete_notification(client: MetabaseClient, notification_id: int) -> None"
|
||||||
|
description: "Elimina una notificacion de Metabase (alerta de card o suscripcion de dashboard). Operacion irreversible. Endpoint: DELETE /api/notification/:id."
|
||||||
|
tags: [metabase, notification, delete, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: notification_id
|
||||||
|
desc: "ID de la notificacion a eliminar"
|
||||||
|
output: "None"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/notifications.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Eliminar una alerta por su ID
|
||||||
|
metabase_delete_notification(client, 99)
|
||||||
|
|
||||||
|
# Patron: listar y eliminar todas las alertas de una card
|
||||||
|
alerts = metabase_list_notifications(client, card_id=7711)
|
||||||
|
for a in alerts:
|
||||||
|
metabase_delete_notification(client, a["id"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
La operacion es irreversible. Para desactivar temporalmente usar metabase_update_notification con active=False.
|
||||||
|
Elimina tanto alertas de cards (notification/card) como suscripciones de dashboards (notification/dashboard).
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: metabase_export_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_export_card(client: MetabaseClient, card_id: int, format: str = 'csv') -> bytes"
|
||||||
|
description: "Exporta los resultados de una card de Metabase en CSV, XLSX o JSON. Endpoint: POST /api/card/:id/query/:format."
|
||||||
|
tags: [metabase, card, export, csv, xlsx, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: card_id
|
||||||
|
desc: "ID de la card cuyos resultados se exportan"
|
||||||
|
- name: format
|
||||||
|
desc: "formato de exportacion: 'csv', 'xlsx' o 'json'. Default: 'csv'"
|
||||||
|
output: "bytes con el contenido del archivo exportado listo para escribir a disco"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Exportar a CSV
|
||||||
|
data = metabase_export_card(client, 42, format="csv")
|
||||||
|
with open("export.csv", "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# Exportar a Excel
|
||||||
|
data = metabase_export_card(client, 42, format="xlsx")
|
||||||
|
with open("export.xlsx", "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# Exportar a JSON
|
||||||
|
import json
|
||||||
|
data = metabase_export_card(client, 42, format="json")
|
||||||
|
rows = json.loads(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Usa `client._http` directamente para acceder al objeto httpx.Client y obtener `.content` en bytes sin que el wrapper de `client.request` intente parsear la respuesta como JSON.
|
||||||
|
|
||||||
|
Para cards con queries parametrizadas, este endpoint no acepta parametros — ejecuta la query con los valores por defecto. Para pasar parametros, usar `metabase_execute_card` que devuelve JSON estructurado.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_snippet
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_get_snippet(client: MetabaseClient, snippet_id: int) -> dict"
|
||||||
|
description: "Obtiene un SQL snippet de Metabase por su ID. Endpoint: GET /api/native-query-snippet/:id."
|
||||||
|
tags: [metabase, snippet, get, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: snippet_id
|
||||||
|
desc: "ID numerico del snippet a obtener"
|
||||||
|
output: "dict: snippet completo con id, name, content, description, collection_id, creator_id, archived, created_at, updated_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/snippets.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
snippet = metabase_get_snippet(client, 42)
|
||||||
|
print(snippet["name"])
|
||||||
|
print(snippet["content"])
|
||||||
|
print(snippet["description"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Retorna 404 si el snippet no existe.
|
||||||
|
Util para verificar el contenido actual antes de hacer un update.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_notifications
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_list_notifications(client: MetabaseClient, card_id: int = 0, dashboard_id: int = 0) -> list[dict]"
|
||||||
|
description: "Lista notificaciones de Metabase (alertas y suscripciones). Filtra opcionalmente por card_id o dashboard_id. Endpoint: GET /api/notification."
|
||||||
|
tags: [metabase, notification, list, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: card_id
|
||||||
|
desc: "si > 0, filtra alertas de tipo notification/card asociadas a esta card"
|
||||||
|
- name: dashboard_id
|
||||||
|
desc: "si > 0, filtra suscripciones de tipo notification/dashboard asociadas a este dashboard"
|
||||||
|
output: "list[dict]: lista de notificaciones con id, active, payload_type, payload, subscriptions, handlers, created_at, updated_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/notifications.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Todas las notificaciones
|
||||||
|
notifs = metabase_list_notifications(client)
|
||||||
|
|
||||||
|
# Solo alertas de una card especifica
|
||||||
|
alerts = metabase_list_notifications(client, card_id=7711)
|
||||||
|
for a in alerts:
|
||||||
|
print(a["id"], a["payload_type"], a["active"])
|
||||||
|
|
||||||
|
# Solo suscripciones de un dashboard
|
||||||
|
subs = metabase_list_notifications(client, dashboard_id=42)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Requiere Metabase 0.57+. En versiones anteriores usar /api/pulse (deprecado).
|
||||||
|
Sin filtros retorna todas las notificaciones accesibles por el usuario autenticado.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_snippets
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_list_snippets(client: MetabaseClient, archived: bool = False) -> list[dict]"
|
||||||
|
description: "Lista SQL snippets reutilizables de Metabase. Un snippet se referencia en queries con {{snippet: nombre}}."
|
||||||
|
tags: [metabase, snippet, list, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: archived
|
||||||
|
desc: "si True, incluye snippets archivados (default False)"
|
||||||
|
output: "list[dict]: snippets con id, name, content, description, collection_id, creator_id, archived, created_at, updated_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/snippets.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
snippets = metabase_list_snippets(client)
|
||||||
|
for s in snippets:
|
||||||
|
print(s["name"], len(s["content"]))
|
||||||
|
|
||||||
|
# Incluir archivados
|
||||||
|
all_snippets = metabase_list_snippets(client, archived=True)
|
||||||
|
active = [s for s in all_snippets if not s["archived"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Si `archived=True` agrega el query param `?archived=true`.
|
||||||
|
Retorna lista directa de todos los snippets accesibles.
|
||||||
|
Los snippets archivados no aparecen en el autocomplete de queries de Metabase.
|
||||||
@@ -3,12 +3,12 @@ name: metabase_update_document
|
|||||||
kind: function
|
kind: function
|
||||||
lang: py
|
lang: py
|
||||||
domain: infra
|
domain: infra
|
||||||
version: "1.0.0"
|
version: "1.1.0"
|
||||||
purity: impure
|
purity: impure
|
||||||
signature: "def metabase_update_document(client: MetabaseClient, document_id: int, **fields) -> dict"
|
signature: "def metabase_update_document(client: MetabaseClient, document_id: int, *, validate: bool = True, **fields) -> dict"
|
||||||
description: "Actualiza un document. Solo envia los campos pasados. Endpoint: PUT /api/document/:id."
|
description: "Actualiza un document. Solo envia los campos pasados. Si se pasa 'document', valida el ProseMirror antes de enviar (evita documentos vacíos por nodos no soportados)."
|
||||||
tags: [metabase, document, update, api, python]
|
tags: [metabase, document, update, api, python]
|
||||||
uses_functions: []
|
uses_functions: [metabase_validate_document_payload_py_infra]
|
||||||
uses_types: []
|
uses_types: []
|
||||||
returns: []
|
returns: []
|
||||||
returns_optional: false
|
returns_optional: false
|
||||||
@@ -19,6 +19,8 @@ params:
|
|||||||
desc: "instancia autenticada de MetabaseClient"
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
- name: document_id
|
- name: document_id
|
||||||
desc: "ID del document a actualizar"
|
desc: "ID del document a actualizar"
|
||||||
|
- name: validate
|
||||||
|
desc: "si True (default), valida el ProseMirror antes de enviar cuando se pasa 'document'"
|
||||||
- name: fields
|
- name: fields
|
||||||
desc: "kwargs con campos a modificar: name, document (arbol ProseMirror), collection_id, archived"
|
desc: "kwargs con campos a modificar: name, document (arbol ProseMirror), collection_id, archived"
|
||||||
output: "dict: document actualizado"
|
output: "dict: document actualizado"
|
||||||
@@ -31,15 +33,27 @@ file_path: "python/functions/metabase/documents.py"
|
|||||||
## Ejemplo
|
## Ejemplo
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from metabase.documents import prosemirror_card_embed
|
||||||
|
|
||||||
# Renombrar
|
# Renombrar
|
||||||
metabase_update_document(client, 1, name="Nuevo titulo")
|
metabase_update_document(client, 1, name="Nuevo titulo")
|
||||||
|
|
||||||
# Reemplazar contenido completo
|
# Reemplazar contenido con card embebida
|
||||||
metabase_update_document(client, 1, document={
|
metabase_update_document(client, 1, document={
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
"content": [{"type": "paragraph", "content": [{"type": "text", "text": "Nuevo"}]}]
|
"content": [
|
||||||
|
{"type": "heading", "attrs": {"level": 1},
|
||||||
|
"content": [{"type": "text", "text": "Resumen"}]},
|
||||||
|
prosemirror_card_embed(42, height=450),
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
# Mover a coleccion
|
# Mover a coleccion
|
||||||
metabase_update_document(client, 1, collection_id=5)
|
metabase_update_document(client, 1, collection_id=5)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
- La validación es automática cuando se pasa `document=...`. Si contiene nodos que el frontend no renderiza (callout, taskList, etc.), lanza `ValueError` antes de enviar.
|
||||||
|
- Usar `blockquote` en vez de `callout` para destacar texto.
|
||||||
|
- Usar `prosemirror_card_embed(card_id)` en vez de `cardEmbed` desnudo.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_notification
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_update_notification(client: MetabaseClient, notification_id: int, **fields) -> dict"
|
||||||
|
description: "Actualiza una notificacion existente de Metabase (alerta o suscripcion). Permite modificar active, handlers, subscriptions o payload. Endpoint: PUT /api/notification/:id."
|
||||||
|
tags: [metabase, notification, update, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: notification_id
|
||||||
|
desc: "ID de la notificacion a modificar"
|
||||||
|
- name: "**fields"
|
||||||
|
desc: "campos a actualizar: active (bool), handlers (list[dict]), subscriptions (list[dict]), payload (dict)"
|
||||||
|
output: "dict: notificacion actualizada con id, active, payload_type, payload, subscriptions, handlers, updated_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/notifications.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Desactivar una alerta
|
||||||
|
updated = metabase_update_notification(client, 99, active=False)
|
||||||
|
print(updated["active"]) # False
|
||||||
|
|
||||||
|
# Cambiar destinatarios
|
||||||
|
updated = metabase_update_notification(
|
||||||
|
client,
|
||||||
|
99,
|
||||||
|
handlers=[{
|
||||||
|
"channel_type": "channel/email",
|
||||||
|
"recipients": [
|
||||||
|
{"type": "notification-recipient/user", "user_id": 2},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cambiar cron a diario a las 7am
|
||||||
|
updated = metabase_update_notification(
|
||||||
|
client,
|
||||||
|
99,
|
||||||
|
subscriptions=[{
|
||||||
|
"type": "notification-subscription/cron",
|
||||||
|
"cron_schedule": "0 7 * * *",
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Solo se envian los campos que se pasan como kwargs.
|
||||||
|
Para reactivar una alerta previamente desactivada usar active=True.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_snippet
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_update_snippet(client: MetabaseClient, snippet_id: int, **fields) -> dict"
|
||||||
|
description: "Actualiza campos de un SQL snippet en Metabase. Acepta name, content, description, collection_id, archived via **fields."
|
||||||
|
tags: [metabase, snippet, update, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
params:
|
||||||
|
- name: client
|
||||||
|
desc: "instancia autenticada de MetabaseClient"
|
||||||
|
- name: snippet_id
|
||||||
|
desc: "ID numerico del snippet a actualizar"
|
||||||
|
- name: "**fields"
|
||||||
|
desc: "campos a modificar: name (str), content (str), description (str), collection_id (int), archived (bool)"
|
||||||
|
output: "dict: snippet actualizado con todos los campos incluyendo updated_at"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/snippets.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Actualizar solo el contenido SQL
|
||||||
|
updated = metabase_update_snippet(
|
||||||
|
client, 42,
|
||||||
|
content="WITH supply_full AS (SELECT * FROM supply_orders)",
|
||||||
|
description="Version simplificada sin JOINs",
|
||||||
|
)
|
||||||
|
print(updated["updated_at"])
|
||||||
|
|
||||||
|
# Renombrar
|
||||||
|
metabase_update_snippet(client, 42, name="supply_orders_simple_cte")
|
||||||
|
|
||||||
|
# Mover a otra coleccion
|
||||||
|
metabase_update_snippet(client, 42, collection_id=7)
|
||||||
|
|
||||||
|
# Archivar (o usar metabase_archive_snippet)
|
||||||
|
metabase_update_snippet(client, 42, archived=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Solo los campos reconocidos (name, content, description, collection_id, archived) se incluyen en el body.
|
||||||
|
Campos desconocidos en **fields se ignoran silenciosamente para evitar errores 400.
|
||||||
|
Para archivar en una sola llamada, preferir metabase_archive_snippet.
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"""CRUD de notificaciones de Metabase (alertas y suscripciones).
|
||||||
|
|
||||||
|
Dos tipos:
|
||||||
|
- Card alerts (notification/card): alertas cuando una card tiene resultados,
|
||||||
|
supera un goal, etc. Cron configurable.
|
||||||
|
- Dashboard subscriptions (notification/dashboard): envio periodico de un
|
||||||
|
dashboard completo por email/slack.
|
||||||
|
|
||||||
|
API: /api/notification (Metabase 0.57+, reemplaza el antiguo /api/pulse).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import MetabaseClient
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_list_notifications(
|
||||||
|
client: MetabaseClient,
|
||||||
|
card_id: int = 0,
|
||||||
|
dashboard_id: int = 0,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Lista notificaciones activas de Metabase.
|
||||||
|
|
||||||
|
Endpoint: GET /api/notification.
|
||||||
|
Permite filtrar por card o por dashboard. Si no se pasan filtros,
|
||||||
|
retorna todas las notificaciones accesibles por el usuario autenticado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
card_id: Si > 0, filtra notificaciones de tipo notification/card
|
||||||
|
asociadas a esta card.
|
||||||
|
dashboard_id: Si > 0, filtra notificaciones de tipo
|
||||||
|
notification/dashboard asociadas a este dashboard.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista de dicts con: id, active, payload_type, payload,
|
||||||
|
subscriptions, handlers, created_at, updated_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Todas las notificaciones
|
||||||
|
>>> notifs = metabase_list_notifications(client)
|
||||||
|
>>> # Solo alertas de una card especifica
|
||||||
|
>>> alerts = metabase_list_notifications(client, card_id=7711)
|
||||||
|
>>> for a in alerts:
|
||||||
|
... print(a["id"], a["payload_type"], a["active"])
|
||||||
|
"""
|
||||||
|
params: dict = {}
|
||||||
|
if card_id > 0:
|
||||||
|
params["card_id"] = card_id
|
||||||
|
if dashboard_id > 0:
|
||||||
|
params["dashboard_id"] = dashboard_id
|
||||||
|
return client.request("GET", "/api/notification", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_create_card_alert(
|
||||||
|
client: MetabaseClient,
|
||||||
|
card_id: int,
|
||||||
|
cron_schedule: str,
|
||||||
|
recipients: list[dict],
|
||||||
|
send_condition: str = "has_result",
|
||||||
|
send_once: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""Crea una alerta sobre los resultados de una card.
|
||||||
|
|
||||||
|
Endpoint: POST /api/notification.
|
||||||
|
Envia un email cuando la card cumple la condicion especificada
|
||||||
|
(tiene resultados, supera un goal, etc.) segun el cron configurado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
card_id: ID de la card que dispara la alerta.
|
||||||
|
cron_schedule: Expresion cron de 5 campos (ej: "0 9 * * 1" = lunes 9am).
|
||||||
|
recipients: Lista de destinatarios. Cada dict puede ser:
|
||||||
|
- Usuario Metabase: {"type": "notification-recipient/user", "user_id": 1}
|
||||||
|
- Email externo: {"type": "notification-recipient/raw-value",
|
||||||
|
"details": {"email": "x@y.com"}}
|
||||||
|
send_condition: Condicion que dispara el envio:
|
||||||
|
"has_result" (tiene filas), "goal_above" (supera goal),
|
||||||
|
"goal_below" (cae bajo goal). Default: "has_result".
|
||||||
|
send_once: Si True, se envia una sola vez y se desactiva.
|
||||||
|
Default: False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con la notificacion creada: id, active, payload_type,
|
||||||
|
payload, subscriptions, handlers, created_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> alert = metabase_create_card_alert(
|
||||||
|
... client,
|
||||||
|
... card_id=7711,
|
||||||
|
... cron_schedule="0 9 * * 1",
|
||||||
|
... recipients=[
|
||||||
|
... {"type": "notification-recipient/user", "user_id": 1},
|
||||||
|
... {"type": "notification-recipient/raw-value",
|
||||||
|
... "details": {"email": "team@aurgi.com"}},
|
||||||
|
... ],
|
||||||
|
... send_condition="has_result",
|
||||||
|
... )
|
||||||
|
>>> print(alert["id"], alert["active"])
|
||||||
|
"""
|
||||||
|
body = {
|
||||||
|
"payload_type": "notification/card",
|
||||||
|
"payload": {
|
||||||
|
"card_id": card_id,
|
||||||
|
"send_condition": send_condition,
|
||||||
|
"send_once": send_once,
|
||||||
|
},
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"type": "notification-subscription/cron",
|
||||||
|
"cron_schedule": cron_schedule,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"channel_type": "channel/email",
|
||||||
|
"recipients": recipients,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active": True,
|
||||||
|
}
|
||||||
|
return client.request("POST", "/api/notification", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_create_dashboard_subscription(
|
||||||
|
client: MetabaseClient,
|
||||||
|
dashboard_id: int,
|
||||||
|
cron_schedule: str,
|
||||||
|
recipients: list[dict],
|
||||||
|
) -> dict:
|
||||||
|
"""Crea una suscripcion periodica a un dashboard.
|
||||||
|
|
||||||
|
Endpoint: POST /api/notification.
|
||||||
|
Envia el dashboard completo por email segun el cron configurado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
dashboard_id: ID del dashboard a enviar.
|
||||||
|
cron_schedule: Expresion cron de 5 campos (ej: "0 8 * * 1-5" = lun-vie 8am).
|
||||||
|
recipients: Lista de destinatarios. Cada dict puede ser:
|
||||||
|
- Usuario Metabase: {"type": "notification-recipient/user", "user_id": 1}
|
||||||
|
- Email externo: {"type": "notification-recipient/raw-value",
|
||||||
|
"details": {"email": "x@y.com"}}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con la suscripcion creada: id, active, payload_type,
|
||||||
|
payload, subscriptions, handlers, created_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> sub = metabase_create_dashboard_subscription(
|
||||||
|
... client,
|
||||||
|
... dashboard_id=42,
|
||||||
|
... cron_schedule="0 8 * * 1",
|
||||||
|
... recipients=[
|
||||||
|
... {"type": "notification-recipient/user", "user_id": 5},
|
||||||
|
... ],
|
||||||
|
... )
|
||||||
|
>>> print(sub["id"], sub["payload"]["dashboard_id"])
|
||||||
|
"""
|
||||||
|
body = {
|
||||||
|
"payload_type": "notification/dashboard",
|
||||||
|
"payload": {
|
||||||
|
"dashboard_id": dashboard_id,
|
||||||
|
},
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"type": "notification-subscription/cron",
|
||||||
|
"cron_schedule": cron_schedule,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"channel_type": "channel/email",
|
||||||
|
"recipients": recipients,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"active": True,
|
||||||
|
}
|
||||||
|
return client.request("POST", "/api/notification", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_update_notification(
|
||||||
|
client: MetabaseClient,
|
||||||
|
notification_id: int,
|
||||||
|
**fields,
|
||||||
|
) -> dict:
|
||||||
|
"""Actualiza una notificacion existente (alerta o suscripcion).
|
||||||
|
|
||||||
|
Endpoint: PUT /api/notification/:id.
|
||||||
|
Permite modificar campos como active, handlers, subscriptions,
|
||||||
|
payload, etc. Solo se envian los campos proporcionados.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
notification_id: ID de la notificacion a modificar.
|
||||||
|
**fields: Campos a actualizar. Ejemplos:
|
||||||
|
active=False para desactivar.
|
||||||
|
handlers=[...] para cambiar destinatarios.
|
||||||
|
subscriptions=[...] para cambiar el cron.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con la notificacion actualizada: id, active, payload_type,
|
||||||
|
payload, subscriptions, handlers, updated_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Desactivar una alerta
|
||||||
|
>>> updated = metabase_update_notification(client, 99, active=False)
|
||||||
|
>>> # Cambiar destinatarios
|
||||||
|
>>> updated = metabase_update_notification(
|
||||||
|
... client,
|
||||||
|
... 99,
|
||||||
|
... handlers=[{
|
||||||
|
... "channel_type": "channel/email",
|
||||||
|
... "recipients": [
|
||||||
|
... {"type": "notification-recipient/user", "user_id": 2},
|
||||||
|
... ],
|
||||||
|
... }],
|
||||||
|
... )
|
||||||
|
>>> print(updated["active"])
|
||||||
|
"""
|
||||||
|
return client.request("PUT", f"/api/notification/{notification_id}", json=fields)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_delete_notification(
|
||||||
|
client: MetabaseClient,
|
||||||
|
notification_id: int,
|
||||||
|
) -> None:
|
||||||
|
"""Elimina una notificacion (alerta o suscripcion) de Metabase.
|
||||||
|
|
||||||
|
Endpoint: DELETE /api/notification/:id.
|
||||||
|
La operacion es irreversible. Afecta tanto a alertas de cards
|
||||||
|
como a suscripciones de dashboards.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
notification_id: ID de la notificacion a eliminar.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> metabase_delete_notification(client, 99)
|
||||||
|
>>> # La notificacion ya no existe
|
||||||
|
"""
|
||||||
|
client.request("DELETE", f"/api/notification/{notification_id}")
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: prosemirror_card_embed
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def prosemirror_card_embed(card_id: int, height: int = 400) -> dict"
|
||||||
|
description: "Genera un nodo ProseMirror cardEmbed envuelto en resizeNode con altura adecuada. Un cardEmbed desnudo renderiza con ~50px — este helper produce el formato que Metabase espera para que la card se vea bien."
|
||||||
|
tags: [metabase, document, prosemirror, cardEmbed, resizeNode, pure, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [uuid]
|
||||||
|
params:
|
||||||
|
- name: card_id
|
||||||
|
desc: "ID de la card/pregunta de Metabase a embeber"
|
||||||
|
- name: height
|
||||||
|
desc: "altura en pixeles del embed (default 400). minHeight se fija en 280"
|
||||||
|
output: "dict ProseMirror: nodo resizeNode > cardEmbed, insertable en el array content de un document"
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/documents.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
from metabase.documents import metabase_create_document, prosemirror_card_embed
|
||||||
|
|
||||||
|
doc = metabase_create_document(client, "Reporte", {
|
||||||
|
"type": "doc",
|
||||||
|
"content": [
|
||||||
|
{"type": "heading", "attrs": {"level": 2},
|
||||||
|
"content": [{"type": "text", "text": "Revenue"}]},
|
||||||
|
prosemirror_card_embed(7711, height=500),
|
||||||
|
{"type": "paragraph",
|
||||||
|
"content": [{"type": "text", "text": "Datos actualizados."}]},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Por qué existe este helper
|
||||||
|
|
||||||
|
```python
|
||||||
|
# MAL — cardEmbed desnudo: renderiza con ~50px de alto, ilegible
|
||||||
|
{"type": "cardEmbed", "attrs": {"id": 42}}
|
||||||
|
|
||||||
|
# BIEN — prosemirror_card_embed: envuelto en resizeNode
|
||||||
|
prosemirror_card_embed(42, height=450)
|
||||||
|
# → {"type": "resizeNode", "attrs": {"height": 450, "minHeight": 280},
|
||||||
|
# "content": [{"type": "cardEmbed", "attrs": {"id": 42, "_id": "uuid...", "name": null}}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
El formato con `resizeNode` es el que usa el editor de Metabase cuando un usuario inserta una card manualmente — sin él, la card se renderiza diminuta.
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""CRUD de SQL snippets de Metabase.
|
||||||
|
|
||||||
|
Un snippet es un fragmento SQL reutilizable que se referencia en queries
|
||||||
|
nativas con la sintaxis {{snippet: nombre}}. Ideal para CTEs comunes
|
||||||
|
que se repiten en multiples cards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import MetabaseClient
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_list_snippets(client: MetabaseClient, archived: bool = False) -> list[dict]:
|
||||||
|
"""Lista SQL snippets de Metabase.
|
||||||
|
|
||||||
|
Endpoint: GET /api/native-query-snippet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
archived: Si True, incluye snippets archivados.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista de dicts con: id, name, content, description, collection_id,
|
||||||
|
creator_id, archived, created_at, updated_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> snippets = metabase_list_snippets(client)
|
||||||
|
>>> for s in snippets:
|
||||||
|
... print(s["name"], len(s["content"]))
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if archived:
|
||||||
|
params["archived"] = "true"
|
||||||
|
return client.request("GET", "/api/native-query-snippet", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_get_snippet(client: MetabaseClient, snippet_id: int) -> dict:
|
||||||
|
"""Obtiene un SQL snippet de Metabase por su ID.
|
||||||
|
|
||||||
|
Endpoint: GET /api/native-query-snippet/:id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
snippet_id: ID numerico del snippet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con campos completos del snippet: id, name, content, description,
|
||||||
|
collection_id, creator_id, archived, created_at, updated_at.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 404 si el snippet no existe.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> snippet = metabase_get_snippet(client, 42)
|
||||||
|
>>> print(snippet["name"], snippet["content"])
|
||||||
|
"""
|
||||||
|
return client.request("GET", f"/api/native-query-snippet/{snippet_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_create_snippet(
|
||||||
|
client: MetabaseClient,
|
||||||
|
name: str,
|
||||||
|
content: str,
|
||||||
|
description: str = "",
|
||||||
|
collection_id: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""Crea un nuevo SQL snippet en Metabase.
|
||||||
|
|
||||||
|
Endpoint: POST /api/native-query-snippet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
name: Nombre del snippet. Se usa en queries con {{snippet: nombre}}.
|
||||||
|
content: SQL del snippet. Puede ser una CTE completa, una subquery,
|
||||||
|
o cualquier fragmento SQL reutilizable.
|
||||||
|
description: Descripcion opcional del snippet.
|
||||||
|
collection_id: ID de la coleccion donde guardar el snippet.
|
||||||
|
Si es 0, se guarda en la raiz (Our analytics).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el snippet creado, incluyendo el campo "id" asignado
|
||||||
|
por Metabase.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 400 si el nombre ya existe o el SQL es invalido.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> snippet = metabase_create_snippet(
|
||||||
|
... client,
|
||||||
|
... "supply_orders_cte",
|
||||||
|
... '''WITH supply_full AS (
|
||||||
|
... SELECT so.id, so.service_request_id
|
||||||
|
... FROM supply_orders so
|
||||||
|
... LEFT JOIN service_requests sr ON so.service_request_id = sr.id
|
||||||
|
... )''',
|
||||||
|
... description="CTE base de supply_orders con JOINs para cruce con NAV",
|
||||||
|
... )
|
||||||
|
>>> print(snippet["id"], snippet["name"])
|
||||||
|
>>> # Usar en una card:
|
||||||
|
>>> # "{{snippet: supply_orders_cte}} SELECT ... FROM supply_full"
|
||||||
|
"""
|
||||||
|
body: dict = {"name": name, "content": content}
|
||||||
|
if description:
|
||||||
|
body["description"] = description
|
||||||
|
if collection_id:
|
||||||
|
body["collection_id"] = collection_id
|
||||||
|
return client.request("POST", "/api/native-query-snippet", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_update_snippet(
|
||||||
|
client: MetabaseClient,
|
||||||
|
snippet_id: int,
|
||||||
|
**fields,
|
||||||
|
) -> dict:
|
||||||
|
"""Actualiza campos de un SQL snippet en Metabase.
|
||||||
|
|
||||||
|
Endpoint: PUT /api/native-query-snippet/:id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
snippet_id: ID numerico del snippet a actualizar.
|
||||||
|
**fields: Campos a modificar. Campos validos:
|
||||||
|
- name (str): Nuevo nombre del snippet.
|
||||||
|
- content (str): Nuevo SQL del snippet.
|
||||||
|
- description (str): Nueva descripcion.
|
||||||
|
- collection_id (int): Nueva coleccion.
|
||||||
|
- archived (bool): True para archivar, False para desarchivar.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el snippet actualizado.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 404 si el snippet no existe.
|
||||||
|
httpx.HTTPStatusError: 400 si los campos son invalidos.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> updated = metabase_update_snippet(
|
||||||
|
... client, 42,
|
||||||
|
... content="WITH supply_full AS (SELECT * FROM supply_orders)",
|
||||||
|
... description="Version simplificada",
|
||||||
|
... )
|
||||||
|
>>> print(updated["updated_at"])
|
||||||
|
"""
|
||||||
|
valid_fields = {"name", "content", "description", "collection_id", "archived"}
|
||||||
|
body = {k: v for k, v in fields.items() if k in valid_fields}
|
||||||
|
return client.request("PUT", f"/api/native-query-snippet/{snippet_id}", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_archive_snippet(client: MetabaseClient, snippet_id: int) -> dict:
|
||||||
|
"""Archiva un SQL snippet en Metabase.
|
||||||
|
|
||||||
|
Wrapper sobre metabase_update_snippet con archived=True.
|
||||||
|
Los snippets archivados no aparecen en el autocomplete de queries
|
||||||
|
pero sus referencias existentes siguen funcionando.
|
||||||
|
|
||||||
|
Endpoint: PUT /api/native-query-snippet/:id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
snippet_id: ID numerico del snippet a archivar.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el snippet archivado.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 404 si el snippet no existe.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> result = metabase_archive_snippet(client, 42)
|
||||||
|
>>> print(result["archived"]) # True
|
||||||
|
"""
|
||||||
|
return metabase_update_snippet(client, snippet_id, archived=True)
|
||||||
Reference in New Issue
Block a user