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