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:
2026-03-28 20:32:28 +01:00
parent 9e6bea681f
commit be5a7b582e
33 changed files with 1325 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
3.12
View File
+11
View File
@@ -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",
]
+227
View File
@@ -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)
+87
View File
@@ -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)
+143
View File
@@ -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.
+153
View File
@@ -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}")
+9
View File
@@ -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",
]
+91
View File
@@ -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" },
]