feat: funciones Python para API Metabase
Añade módulo Python con funciones para la API de Metabase en dominio infra. Incluye cliente HTTP, auth, y CRUD de cards, dashboards y users. Proyecto gestionado con uv (pyproject.toml).
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
from .client import MetabaseClient
|
||||||
|
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
|
||||||
|
from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MetabaseClient",
|
||||||
|
"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_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,227 @@
|
|||||||
|
"""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)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Cliente base para la API REST de Metabase."""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MetabaseClient:
|
||||||
|
"""Cliente HTTP para una instancia Metabase.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
base_url: URL base sin trailing slash (ej: "http://localhost:3000").
|
||||||
|
token: Session token o API key.
|
||||||
|
_http: Cliente httpx reutilizable con headers de auth.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, token: str) -> None:
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self._http = httpx.Client(
|
||||||
|
base_url=self.base_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Metabase-Session": token,
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def request(self, method: str, path: str, **kwargs) -> dict | list | None:
|
||||||
|
"""Ejecuta una peticion HTTP contra la API de Metabase.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method (GET, POST, PUT, DELETE).
|
||||||
|
path: Ruta relativa (ej: "/api/user").
|
||||||
|
**kwargs: Argumentos extra para httpx (json, params, etc.).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Respuesta deserializada como dict/list, o None si el body esta vacio.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: Si el status code no es 2xx.
|
||||||
|
"""
|
||||||
|
resp = self._http.request(method, path, **kwargs)
|
||||||
|
resp.raise_for_status()
|
||||||
|
if not resp.content:
|
||||||
|
return None
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Cierra el cliente HTTP."""
|
||||||
|
self._http.close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient:
|
||||||
|
"""Autentica contra Metabase con email y password.
|
||||||
|
|
||||||
|
Crea una sesion via POST /api/session y retorna un MetabaseClient
|
||||||
|
con el session token listo para usar. El token expira en 14 dias
|
||||||
|
por defecto (configurable con MAX_SESSION_AGE en Metabase).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: URL base de la instancia (ej: "http://localhost:3000").
|
||||||
|
email: Email del usuario Metabase.
|
||||||
|
password: Password del usuario.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MetabaseClient autenticado con session token.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: Si las credenciales son invalidas (401)
|
||||||
|
o hay rate limiting.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> client = metabase_auth("http://localhost:3000", "admin@example.com", "pass")
|
||||||
|
>>> # client listo para usar con todas las funciones CRUD
|
||||||
|
"""
|
||||||
|
resp = httpx.post(
|
||||||
|
f"{base_url.rstrip('/')}/api/session",
|
||||||
|
json={"username": email, "password": password},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
token = resp.json()["id"]
|
||||||
|
return MetabaseClient(base_url, token)
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""CRUD de dashboards de Metabase."""
|
||||||
|
|
||||||
|
from .client import MetabaseClient
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_list_dashboards(
|
||||||
|
client: MetabaseClient,
|
||||||
|
filter: str = "",
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Lista dashboards de Metabase con filtro opcional.
|
||||||
|
|
||||||
|
Endpoint: GET /api/dashboard. Retorna dashboards resumidos (sin dashcards).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
filter: "all", "mine" o "archived". Vacio = todas.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista de dicts con: id, name, description, collection_id,
|
||||||
|
creator_id, archived, created_at.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> dashboards = metabase_list_dashboards(client, filter="mine")
|
||||||
|
>>> for d in dashboards:
|
||||||
|
... print(d["id"], d["name"])
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if filter:
|
||||||
|
params["f"] = filter
|
||||||
|
return client.request("GET", "/api/dashboard", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict:
|
||||||
|
"""Obtiene un dashboard completo incluyendo sus cards.
|
||||||
|
|
||||||
|
Endpoint: GET /api/dashboard/:id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
dashboard_id: ID del dashboard.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con: id, name, description, dashcards (lista de cards posicionadas),
|
||||||
|
parameters (filtros), tabs, collection_id, archived.
|
||||||
|
|
||||||
|
Cada dashcard tiene: id, card_id, card (objeto completo), size_x, size_y,
|
||||||
|
col, row, dashboard_tab_id, parameter_mappings, visualization_settings.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> dash = metabase_get_dashboard(client, 1)
|
||||||
|
>>> for dc in dash["dashcards"]:
|
||||||
|
... print(f"Card {dc['card_id']} at ({dc['col']}, {dc['row']})")
|
||||||
|
"""
|
||||||
|
return client.request("GET", f"/api/dashboard/{dashboard_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_create_dashboard(
|
||||||
|
client: MetabaseClient,
|
||||||
|
name: str,
|
||||||
|
description: str = "",
|
||||||
|
collection_id: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""Crea un nuevo dashboard vacio en Metabase.
|
||||||
|
|
||||||
|
Endpoint: POST /api/dashboard.
|
||||||
|
Para agregar cards usar metabase_update_dashboard con dashcards.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
name: Nombre del dashboard.
|
||||||
|
description: Descripcion opcional.
|
||||||
|
collection_id: Coleccion destino. 0 = root.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el dashboard creado.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> dash = metabase_create_dashboard(client, "Sales Overview", "KPIs de ventas")
|
||||||
|
>>> # Agregar cards:
|
||||||
|
>>> metabase_update_dashboard(client, dash["id"], dashcards=[
|
||||||
|
... {"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
|
||||||
|
... ])
|
||||||
|
"""
|
||||||
|
body: dict = {"name": name}
|
||||||
|
if description:
|
||||||
|
body["description"] = description
|
||||||
|
if collection_id > 0:
|
||||||
|
body["collection_id"] = collection_id
|
||||||
|
return client.request("POST", "/api/dashboard", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict:
|
||||||
|
"""Actualiza un dashboard incluyendo metadata, cards y tabs.
|
||||||
|
|
||||||
|
Endpoint: PUT /api/dashboard/:id.
|
||||||
|
|
||||||
|
El campo dashcards representa el ESTADO COMPLETO DESEADO del dashboard:
|
||||||
|
- Agregar card: incluirla con ID negativo (-1, -2, etc.)
|
||||||
|
- Actualizar card existente: incluirla con su ID positivo
|
||||||
|
- Eliminar card: omitirla del array
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
dashboard_id: ID del dashboard.
|
||||||
|
**fields: Campos a actualizar. Validos:
|
||||||
|
name (str), description (str), archived (bool),
|
||||||
|
dashcards (list[dict]), tabs (list[dict]),
|
||||||
|
parameters (list[dict]), collection_id (int).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el dashboard actualizado.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Cambiar nombre
|
||||||
|
>>> metabase_update_dashboard(client, 1, name="Updated Name")
|
||||||
|
>>>
|
||||||
|
>>> # Agregar card (primero obtener existentes)
|
||||||
|
>>> dash = metabase_get_dashboard(client, 1)
|
||||||
|
>>> cards = list(dash["dashcards"])
|
||||||
|
>>> cards.append({"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0})
|
||||||
|
>>> metabase_update_dashboard(client, 1, dashcards=cards)
|
||||||
|
>>>
|
||||||
|
>>> # Archivar (soft-delete)
|
||||||
|
>>> metabase_update_dashboard(client, 1, archived=True)
|
||||||
|
"""
|
||||||
|
return client.request("PUT", f"/api/dashboard/{dashboard_id}", json=fields)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None:
|
||||||
|
"""Elimina permanentemente un dashboard.
|
||||||
|
|
||||||
|
Endpoint: DELETE /api/dashboard/:id. IRREVERSIBLE.
|
||||||
|
Para soft-delete preferir: metabase_update_dashboard(client, id, archived=True)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
dashboard_id: ID del dashboard a eliminar.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> metabase_delete_dashboard(client, 1)
|
||||||
|
>>> # Preferir: metabase_update_dashboard(client, 1, archived=True)
|
||||||
|
"""
|
||||||
|
client.request("DELETE", f"/api/dashboard/{dashboard_id}")
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: metabase_auth
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient"
|
||||||
|
description: "Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias. Endpoint: POST /api/session."
|
||||||
|
tags: [metabase, auth, session, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/client.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functions.metabase import metabase_auth
|
||||||
|
|
||||||
|
client = metabase_auth("http://localhost:3000", "admin@example.com", "pass")
|
||||||
|
# client listo para usar con todas las funciones CRUD
|
||||||
|
|
||||||
|
# Alternativa con API key:
|
||||||
|
from functions.metabase import MetabaseClient
|
||||||
|
client = MetabaseClient("http://localhost:3000", "mb_api_key_xxxxx")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Dos formas de obtener un client:
|
||||||
|
- `metabase_auth()`: login con email/password, obtiene session token via POST /api/session
|
||||||
|
- `MetabaseClient(base_url, api_key)`: constructor directo con API key (recomendado para automatizacion)
|
||||||
|
|
||||||
|
El client es un context manager: `with metabase_auth(...) as client:`
|
||||||
|
|
||||||
|
Errores comunes:
|
||||||
|
- 401: credenciales invalidas
|
||||||
|
- Rate limiting en intentos fallidos de login
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_card(client: MetabaseClient, name: str, dataset_query: dict, display: str = 'table', collection_id: int = 0, description: str = '') -> dict"
|
||||||
|
description: "Crea una card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card."
|
||||||
|
tags: [metabase, card, question, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
card = metabase_create_card(client, "Revenue", {
|
||||||
|
"database": 1, "type": "native",
|
||||||
|
"native": {"query": "SELECT SUM(total) FROM orders"},
|
||||||
|
}, display="scalar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
dataset_query SQL nativo: `{"database": id, "type": "native", "native": {"query": "..."}}`
|
||||||
|
dataset_query MBQL: `{"database": id, "type": "query", "query": {"source-table": id, ...}}`
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_dashboard(client: MetabaseClient, name: str, description: str = '', collection_id: int = 0) -> dict"
|
||||||
|
description: "Crea dashboard vacio en Metabase. Para agregar cards usar metabase_update_dashboard con dashcards. Endpoint: POST /api/dashboard."
|
||||||
|
tags: [metabase, dashboard, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
dash = metabase_create_dashboard(client, "Sales Overview", "KPIs")
|
||||||
|
# Agregar cards con metabase_update_dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Se crea vacio. Agregar cards con metabase_update_dashboard(dashcards=[...]).
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_user
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_create_user(client: MetabaseClient, first_name: str, last_name: str, email: str, password: str = '', group_ids: list[int] | None = None) -> dict"
|
||||||
|
description: "Crea un nuevo usuario en Metabase. Sin password envia invitacion por email. Requiere superusuario. Endpoint: POST /api/user."
|
||||||
|
tags: [metabase, user, create, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/users.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
user = metabase_create_user(client, "John", "Doe", "john@example.com", "pass123")
|
||||||
|
user = metabase_create_user(client, "Jane", "Smith", "jane@example.com", group_ids=[1, 3])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Email debe ser unico. Error 400 si ya existe.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: metabase_deactivate_user
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_deactivate_user(client: MetabaseClient, user_id: int) -> None"
|
||||||
|
description: "Desactiva (soft-delete) un usuario en Metabase. Reactivar con PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id."
|
||||||
|
tags: [metabase, user, delete, deactivate, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/users.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
metabase_deactivate_user(client, 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Soft-delete. El usuario se puede reactivar.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_delete_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_delete_card(client: MetabaseClient, card_id: int) -> None"
|
||||||
|
description: "Elimina permanentemente una card/pregunta. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/card/:id."
|
||||||
|
tags: [metabase, card, question, delete, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
metabase_delete_card(client, 42)
|
||||||
|
# Preferir: metabase_update_card(client, 42, archived=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
IRREVERSIBLE. Preferir soft-delete con metabase_update_card(archived=True).
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_delete_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None"
|
||||||
|
description: "Elimina permanentemente un dashboard. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, delete, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
metabase_delete_dashboard(client, 1)
|
||||||
|
# Preferir: metabase_update_dashboard(client, 1, archived=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
IRREVERSIBLE. Preferir soft-delete con metabase_update_dashboard(archived=True).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: metabase_execute_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_execute_card(client: MetabaseClient, card_id: int, parameters: list[dict] | None = None) -> dict"
|
||||||
|
description: "Ejecuta la query de una card guardada y retorna resultados con columnas y filas. Soporta parametros. Endpoint: POST /api/card/:id/query."
|
||||||
|
tags: [metabase, card, question, execute, query, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = metabase_execute_card(client, 42)
|
||||||
|
for row in result["data"]["rows"]:
|
||||||
|
print(row)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Respuesta: status, row_count, running_time, data.columns, data.rows, data.cols.
|
||||||
|
Limite default: 2000 filas. Para ad-hoc sin card usar metabase_execute_query.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_execute_query
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_execute_query(client: MetabaseClient, database_id: int, sql: str, max_results: int = 0) -> dict"
|
||||||
|
description: "Ejecuta query SQL ad-hoc contra Metabase sin guardarla como card. Util para exploracion rapida. Endpoint: POST /api/dataset."
|
||||||
|
tags: [metabase, query, execute, sql, dataset, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = metabase_execute_query(client, 1, "SELECT * FROM users LIMIT 10")
|
||||||
|
print(f"{result['row_count']} filas en {result['running_time']}ms")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Misma respuesta que metabase_execute_card. Default 2000 filas, override con max_results.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_get_card(client: MetabaseClient, card_id: int) -> dict"
|
||||||
|
description: "Obtiene detalles completos de una card/pregunta de Metabase incluyendo query, visualizacion y metadata. Endpoint: GET /api/card/:id."
|
||||||
|
tags: [metabase, card, question, get, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
card = metabase_get_card(client, 42)
|
||||||
|
print(card["name"], card["display"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Error 404 si no existe.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict"
|
||||||
|
description: "Obtiene dashboard completo con dashcards (cards posicionadas), tabs y parametros. Endpoint: GET /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, get, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
dash = metabase_get_dashboard(client, 1)
|
||||||
|
for dc in dash["dashcards"]:
|
||||||
|
print(f"Card {dc['card_id']} at ({dc['col']}, {dc['row']})")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Cada dashcard tiene: id, card_id, card, size_x, size_y, col, row, dashboard_tab_id, parameter_mappings.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_user
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_get_user(client: MetabaseClient, user_id: int) -> dict"
|
||||||
|
description: "Obtiene los detalles de un usuario de Metabase por su ID. Endpoint: GET /api/user/:id."
|
||||||
|
tags: [metabase, user, get, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/users.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
user = metabase_get_user(client, 1)
|
||||||
|
print(user["email"], user["is_superuser"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Error 404 si el usuario no existe.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_cards
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_list_cards(client: MetabaseClient, filter: str = '', model_id: int = 0) -> list[dict]"
|
||||||
|
description: "Lista preguntas/cards de Metabase. Filtros: all, mine, fav, archived, recent, popular, database, table. Endpoint: GET /api/card."
|
||||||
|
tags: [metabase, card, question, list, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
cards = metabase_list_cards(client, filter="mine")
|
||||||
|
cards = metabase_list_cards(client, filter="database", model_id=1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
No tiene paginacion offset/limit. Retorna todas las cards que coinciden.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_dashboards
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_list_dashboards(client: MetabaseClient, filter: str = '') -> list[dict]"
|
||||||
|
description: "Lista dashboards de Metabase. Filtros: all, mine, archived. Retorna resumen sin dashcards. Endpoint: GET /api/dashboard."
|
||||||
|
tags: [metabase, dashboard, list, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
dashboards = metabase_list_dashboards(client, filter="mine")
|
||||||
|
for d in dashboards:
|
||||||
|
print(d["id"], d["name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Para ver cards de un dashboard usar metabase_get_dashboard.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_users
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_list_users(client: MetabaseClient, status: str = '', query: str = '', limit: int = 0, offset: int = 0) -> dict"
|
||||||
|
description: "Lista usuarios de Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user."
|
||||||
|
tags: [metabase, user, list, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/users.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
users = metabase_list_users(client, status="active", query="john@")
|
||||||
|
for u in users["data"]:
|
||||||
|
print(u["email"], u["first_name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Retorna dict paginado con data, total, limit, offset.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_card
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_update_card(client: MetabaseClient, card_id: int, **fields) -> dict"
|
||||||
|
description: "Actualiza campos de una card/pregunta via kwargs. Campos: name, description, display, dataset_query, collection_id, archived. Endpoint: PUT /api/card/:id."
|
||||||
|
tags: [metabase, card, question, update, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/cards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
metabase_update_card(client, 42, name="New Name", archived=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Soft-delete con `archived=True`. Para delete permanente usar metabase_delete_card.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict"
|
||||||
|
description: "Actualiza dashboard incluyendo metadata, cards y tabs via kwargs. dashcards es el estado completo deseado: nuevas con ID negativo, existentes con positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, update, cards, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/dashboards.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Agregar card
|
||||||
|
dash = metabase_get_dashboard(client, 1)
|
||||||
|
cards = list(dash["dashcards"])
|
||||||
|
cards.append({"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0})
|
||||||
|
metabase_update_dashboard(client, 1, dashcards=cards)
|
||||||
|
|
||||||
|
# Archivar
|
||||||
|
metabase_update_dashboard(client, 1, archived=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
dashcards = estado completo. ID negativo = nueva, positivo = existente, omitida = eliminada.
|
||||||
|
Campos: name, description, archived, dashcards, tabs, parameters, collection_id.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_user
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def metabase_update_user(client: MetabaseClient, user_id: int, **fields) -> dict"
|
||||||
|
description: "Actualiza campos de un usuario en Metabase via keyword arguments. Campos: first_name, last_name, email, is_superuser, group_ids, locale. Endpoint: PUT /api/user/:id."
|
||||||
|
tags: [metabase, user, update, api, python]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [httpx]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/metabase/users.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
metabase_update_user(client, 5, first_name="Jane", is_superuser=True)
|
||||||
|
metabase_update_user(client, 5, group_ids=[1, 3, 5])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Solo se modifican los campos pasados como kwargs.
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
"""CRUD de usuarios de Metabase."""
|
||||||
|
|
||||||
|
from .client import MetabaseClient
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_list_users(
|
||||||
|
client: MetabaseClient,
|
||||||
|
status: str = "",
|
||||||
|
query: str = "",
|
||||||
|
limit: int = 0,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> dict:
|
||||||
|
"""Lista usuarios de Metabase con filtros opcionales.
|
||||||
|
|
||||||
|
Endpoint: GET /api/user. Requiere permisos de superusuario.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
status: "active" (default), "deactivated" o "all".
|
||||||
|
query: Filtro por nombre o email.
|
||||||
|
limit: Tamanio de pagina (0 = default Metabase).
|
||||||
|
offset: Offset para paginacion.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con estructura paginada:
|
||||||
|
- data: lista de usuarios (id, email, first_name, last_name, is_superuser, etc.)
|
||||||
|
- total: numero total de usuarios que coinciden
|
||||||
|
- limit: tamanio de pagina usado
|
||||||
|
- offset: offset usado
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> users = metabase_list_users(client, status="active", query="john@")
|
||||||
|
>>> for u in users["data"]:
|
||||||
|
... print(u["email"], u["first_name"])
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if status:
|
||||||
|
params["status"] = status
|
||||||
|
if query:
|
||||||
|
params["query"] = query
|
||||||
|
if limit > 0:
|
||||||
|
params["limit"] = limit
|
||||||
|
if offset > 0:
|
||||||
|
params["offset"] = offset
|
||||||
|
return client.request("GET", "/api/user", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_get_user(client: MetabaseClient, user_id: int) -> dict:
|
||||||
|
"""Obtiene un usuario de Metabase por su ID.
|
||||||
|
|
||||||
|
Endpoint: GET /api/user/:id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado.
|
||||||
|
user_id: ID numerico del usuario.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con datos del usuario: id, email, first_name, last_name,
|
||||||
|
is_superuser, is_active, common_name, date_joined, last_login,
|
||||||
|
group_ids, locale.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 404 si el usuario no existe.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> user = metabase_get_user(client, 1)
|
||||||
|
>>> print(user["email"], user["is_superuser"])
|
||||||
|
"""
|
||||||
|
return client.request("GET", f"/api/user/{user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_create_user(
|
||||||
|
client: MetabaseClient,
|
||||||
|
first_name: str,
|
||||||
|
last_name: str,
|
||||||
|
email: str,
|
||||||
|
password: str = "",
|
||||||
|
group_ids: list[int] | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Crea un nuevo usuario en Metabase.
|
||||||
|
|
||||||
|
Endpoint: POST /api/user. Requiere permisos de superusuario.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado con permisos admin.
|
||||||
|
first_name: Nombre del usuario.
|
||||||
|
last_name: Apellido del usuario.
|
||||||
|
email: Email unico del usuario.
|
||||||
|
password: Password. Vacio = Metabase envia invitacion por email.
|
||||||
|
group_ids: IDs de grupos a asignar. None = solo grupo default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el usuario creado (mismos campos que metabase_get_user).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
httpx.HTTPStatusError: 400 si el email ya existe.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> user = metabase_create_user(client, "John", "Doe", "john@example.com", "pass123")
|
||||||
|
>>> print(user["id"])
|
||||||
|
"""
|
||||||
|
body: dict = {
|
||||||
|
"first_name": first_name,
|
||||||
|
"last_name": last_name,
|
||||||
|
"email": email,
|
||||||
|
}
|
||||||
|
if password:
|
||||||
|
body["password"] = password
|
||||||
|
if group_ids:
|
||||||
|
body["group_ids"] = group_ids
|
||||||
|
return client.request("POST", "/api/user", json=body)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_update_user(client: MetabaseClient, user_id: int, **fields) -> dict:
|
||||||
|
"""Actualiza campos de un usuario en Metabase.
|
||||||
|
|
||||||
|
Endpoint: PUT /api/user/:id. Requiere permisos de superusuario.
|
||||||
|
Solo se modifican los campos pasados como keyword arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado con permisos admin.
|
||||||
|
user_id: ID del usuario a actualizar.
|
||||||
|
**fields: Campos a actualizar. Validos:
|
||||||
|
first_name (str), last_name (str), email (str),
|
||||||
|
is_superuser (bool), group_ids (list[int]),
|
||||||
|
locale (str), login_attributes (dict).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict con el usuario actualizado.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> user = metabase_update_user(client, 5, first_name="Jane", is_superuser=True)
|
||||||
|
>>> user = metabase_update_user(client, 5, group_ids=[1, 3, 5])
|
||||||
|
"""
|
||||||
|
return client.request("PUT", f"/api/user/{user_id}", json=fields)
|
||||||
|
|
||||||
|
|
||||||
|
def metabase_deactivate_user(client: MetabaseClient, user_id: int) -> None:
|
||||||
|
"""Desactiva (soft-delete) un usuario en Metabase.
|
||||||
|
|
||||||
|
Endpoint: DELETE /api/user/:id. Requiere permisos de superusuario.
|
||||||
|
El usuario no se elimina permanentemente, solo se marca como inactivo.
|
||||||
|
Para reactivar: PUT /api/user/:id/reactivate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Cliente autenticado con permisos admin.
|
||||||
|
user_id: ID del usuario a desactivar.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> metabase_deactivate_user(client, 5)
|
||||||
|
>>> # Para ver desactivados: metabase_list_users(client, status="deactivated")
|
||||||
|
"""
|
||||||
|
client.request("DELETE", f"/api/user/{user_id}")
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "fn-registry-python"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Funciones Python del fn-registry: Metabase API, ML, utilidades"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"httpx",
|
||||||
|
]
|
||||||
Generated
+91
@@ -0,0 +1,91 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.2.25"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fn-registry-python"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "httpx" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user