merge: quick/frontend-setup-metabase-docs — frontend setup, metabase Go/Py, documentación en registry
This commit is contained in:
+20
-6
@@ -10,13 +10,23 @@ Registry personal de codigo reutilizable con busqueda FTS. Diseñado para compos
|
||||
|
||||
---
|
||||
|
||||
## Explorar el registry (USAR SIEMPRE)
|
||||
## Explorar el registry (OBLIGATORIO)
|
||||
|
||||
Antes de escribir codigo, SIEMPRE consulta registry.db para evitar duplicados y descubrir funciones reutilizables.
|
||||
**SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
|
||||
|
||||
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Estos campos tambien estan indexados en FTS5, asi que puedes buscar dentro del codigo y la documentacion directamente. Para leer el codigo de una funcion: `SELECT code FROM functions WHERE id = '...'`. Para leer su documentacion: `SELECT documentation FROM functions WHERE id = '...'`.
|
||||
|
||||
**Busquedas FTS5 obligatorias:** Usa SIEMPRE la tabla FTS5 para buscar tanto por `name` como por `description`. Esto encuentra coincidencias parciales y similares que una busqueda exacta perderia. Usa operadores FTS5: `OR` para ampliar, `*` para prefijos, `NEAR` para proximidad.
|
||||
|
||||
```bash
|
||||
# FTS5
|
||||
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'slice') ORDER BY name;"
|
||||
# Busqueda FTS5 por nombre Y descripcion (USAR SIEMPRE ESTE PATRON)
|
||||
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slice OR description:slice') ORDER BY name;"
|
||||
|
||||
# FTS5 con prefijo (encuentra slice, slicing, sliced...)
|
||||
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slic* OR description:slic*') ORDER BY name;"
|
||||
|
||||
# FTS5 en tipos
|
||||
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:result OR description:result') ORDER BY name;"
|
||||
|
||||
# Por dominio
|
||||
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
|
||||
@@ -24,7 +34,7 @@ sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain =
|
||||
# Puras de un dominio
|
||||
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;"
|
||||
|
||||
# Tipos
|
||||
# Tipos por dominio
|
||||
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';"
|
||||
|
||||
# Dependencias
|
||||
@@ -37,6 +47,8 @@ sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status
|
||||
sqlite3 registry.db ".schema"
|
||||
```
|
||||
|
||||
**Regla:** Si necesitas saber si algo existe o hay algo similar, haz la consulta FTS5 sobre la BD. No asumas que no existe sin consultar primero.
|
||||
|
||||
---
|
||||
|
||||
## Estructura
|
||||
@@ -45,8 +57,10 @@ sqlite3 registry.db ".schema"
|
||||
fn-registry/
|
||||
functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity)
|
||||
functions/pipelines/ # Composiciones, siempre impuras
|
||||
functions/components/ # React (.tsx)
|
||||
types/{domain}/ # .go + .md por tipo
|
||||
frontend/ # pnpm + vite + react + tailwind + shadcn
|
||||
frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
|
||||
frontend/types/ # .ts + .md por tipo
|
||||
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
|
||||
fn_operations/ # Paquete Go: operations database (libreria)
|
||||
apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db
|
||||
|
||||
@@ -289,6 +289,15 @@ func printFunction(f *registry.Function) {
|
||||
if f.Example != "" {
|
||||
fmt.Printf("\nExample:\n%s\n", f.Example)
|
||||
}
|
||||
if f.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", f.Notes)
|
||||
}
|
||||
if f.Documentation != "" {
|
||||
fmt.Printf("\nDocumentation:\n%s\n", f.Documentation)
|
||||
}
|
||||
if f.Code != "" {
|
||||
fmt.Printf("\nCode:\n%s\n", f.Code)
|
||||
}
|
||||
if f.Kind == registry.KindComponent {
|
||||
fmt.Printf("Framework: %s\n", f.Framework)
|
||||
if f.HasState != nil {
|
||||
@@ -316,6 +325,18 @@ func printType(t *registry.Type) {
|
||||
if t.Definition != "" {
|
||||
fmt.Printf("\nDefinition:\n%s\n", t.Definition)
|
||||
}
|
||||
if t.Examples != "" {
|
||||
fmt.Printf("\nExamples:\n%s\n", t.Examples)
|
||||
}
|
||||
if t.Notes != "" {
|
||||
fmt.Printf("\nNotes:\n%s\n", t.Notes)
|
||||
}
|
||||
if t.Documentation != "" {
|
||||
fmt.Printf("\nDocumentation:\n%s\n", t.Documentation)
|
||||
}
|
||||
if t.Code != "" {
|
||||
fmt.Printf("\nCode:\n%s\n", t.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// --- add ---
|
||||
|
||||
Vendored
+1
-1
@@ -17,7 +17,7 @@ imports: [react]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/components/DataTable.tsx"
|
||||
file_path: "frontend/functions/ui/data_table.tsx"
|
||||
props:
|
||||
- name: data
|
||||
type: "T[]"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "base-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>fn-registry frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"add": "pnpm dlx shadcn@latest add"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.17.0",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shadcn": "^4.1.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
Generated
+3609
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,130 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@fontsource-variable/geist";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: 'Geist Variable', sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import "./globals.css";
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": ["src", "functions", "types"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MetabaseAuth autentica con email y password contra una instancia Metabase.
|
||||
// Retorna un MetabaseClient con el session token listo para usar.
|
||||
// baseURL es la URL base sin trailing slash (ej: "http://localhost:3000").
|
||||
func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error) {
|
||||
payload, _ := json.Marshal(map[string]string{
|
||||
"username": email,
|
||||
"password": password,
|
||||
})
|
||||
|
||||
resp, err := http.Post(baseURL+"/api/session", "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return MetabaseClient{}, fmt.Errorf("metabase auth: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return MetabaseClient{}, fmt.Errorf("read auth response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return MetabaseClient{}, fmt.Errorf("metabase auth: status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return MetabaseClient{}, fmt.Errorf("parse auth response: %w", err)
|
||||
}
|
||||
|
||||
return MetabaseClient{BaseURL: baseURL, Token: result.ID}, nil
|
||||
}
|
||||
|
||||
// MetabaseNewClient crea un MetabaseClient usando una API key en lugar de session token.
|
||||
// Las API keys se crean en Settings > Authentication > API Keys del admin de Metabase.
|
||||
func MetabaseNewClient(baseURL, apiKey string) MetabaseClient {
|
||||
return MetabaseClient{BaseURL: baseURL, Token: apiKey}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: metabase_auth
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)"
|
||||
description: "Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session."
|
||||
tags: [metabase, auth, session, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: [MetabaseClient_go_infra]
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [bytes, encoding/json, fmt, io, net/http]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_auth.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Autenticar con credenciales
|
||||
client, err := MetabaseAuth("http://localhost:3000", "admin@example.com", "password123")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// client.Token contiene el session token
|
||||
|
||||
// Alternativa: usar API key directamente
|
||||
client := MetabaseNewClient("http://localhost:3000", "mb_api_key_xxxxx")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Dos formas de obtener un MetabaseClient:
|
||||
- `MetabaseAuth`: login con email/password, obtiene session token via POST /api/session. Token expira en 14 dias por defecto.
|
||||
- `MetabaseNewClient`: usa una API key creada en el admin UI. No expira. Recomendado para automatizacion.
|
||||
|
||||
El token se envia como header `X-Metabase-Session` en todas las llamadas subsiguientes.
|
||||
|
||||
### Para un LLM que use estas funciones
|
||||
|
||||
1. Primero obtener un client con `MetabaseAuth()` o `MetabaseNewClient()`
|
||||
2. Pasar el client a todas las funciones CRUD (usuarios, cards, dashboards)
|
||||
3. Si recibes error 401, el token expiro — re-autenticar
|
||||
4. Rate limiting: Metabase limita intentos de login fallidos
|
||||
@@ -0,0 +1,35 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseCreateCard crea una nueva card/pregunta en Metabase.
|
||||
// name: nombre de la pregunta (obligatorio).
|
||||
// datasetQuery: query de la card (obligatorio). 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", etc.).
|
||||
// collectionID: ID de la coleccion/carpeta (0 = root).
|
||||
// description: descripcion opcional (vacio = sin descripcion).
|
||||
func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error) {
|
||||
body := map[string]any{
|
||||
"name": name,
|
||||
"dataset_query": datasetQuery,
|
||||
"display": display,
|
||||
"visualization_settings": map[string]any{},
|
||||
}
|
||||
if collectionID > 0 {
|
||||
body["collection_id"] = collectionID
|
||||
}
|
||||
if description != "" {
|
||||
body["description"] = description
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/card", body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase create card: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: metabase_create_card
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error)"
|
||||
description: "Crea una nueva card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card."
|
||||
tags: [metabase, card, question, create, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_create_card.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Crear pregunta con SQL nativo
|
||||
card, err := MetabaseCreateCard(client, "Revenue by Month", map[string]any{
|
||||
"database": 1,
|
||||
"type": "native",
|
||||
"native": map[string]any{
|
||||
"query": "SELECT date_trunc('month', created_at) as month, SUM(total) as revenue FROM orders GROUP BY 1 ORDER BY 1",
|
||||
},
|
||||
}, "line", 5, "Monthly revenue trend")
|
||||
|
||||
// Crear pregunta con MBQL (structured query)
|
||||
card, err := MetabaseCreateCard(client, "Order Count", map[string]any{
|
||||
"database": 1,
|
||||
"type": "query",
|
||||
"query": map[string]any{
|
||||
"source-table": 4,
|
||||
"aggregation": []any{[]any{"count"}},
|
||||
},
|
||||
}, "scalar", 0, "Total number of orders")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| name | string | si | Nombre de la pregunta |
|
||||
| datasetQuery | map[string]any | si | Query. Ver estructura abajo |
|
||||
| display | string | si | Tipo de visualizacion |
|
||||
| collectionID | int | no | ID de coleccion. 0 = root collection |
|
||||
| description | string | no | Descripcion. Vacio = sin descripcion |
|
||||
|
||||
### Estructura de datasetQuery
|
||||
|
||||
**SQL nativo:**
|
||||
```json
|
||||
{
|
||||
"database": <database_id>,
|
||||
"type": "native",
|
||||
"native": {"query": "SELECT ..."}
|
||||
}
|
||||
```
|
||||
|
||||
**MBQL (structured):**
|
||||
```json
|
||||
{
|
||||
"database": <database_id>,
|
||||
"type": "query",
|
||||
"query": {
|
||||
"source-table": <table_id>,
|
||||
"aggregation": [["count"]],
|
||||
"breakout": [["field", <field_id>, {"temporal-unit": "month"}]],
|
||||
"filter": ["=", ["field", <field_id>, null], "value"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Valores de display
|
||||
|
||||
table, bar, line, pie, scalar, area, row, combo, funnel, map, scatter, waterfall, progress, gauge
|
||||
@@ -0,0 +1,26 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseCreateDashboard crea un nuevo dashboard en Metabase.
|
||||
// name: nombre del dashboard (obligatorio).
|
||||
// description: descripcion opcional (vacio = sin descripcion).
|
||||
// collectionID: ID de la coleccion/carpeta (0 = root).
|
||||
func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error) {
|
||||
body := map[string]any{
|
||||
"name": name,
|
||||
}
|
||||
if description != "" {
|
||||
body["description"] = description
|
||||
}
|
||||
if collectionID > 0 {
|
||||
body["collection_id"] = collectionID
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dashboard", body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase create dashboard: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: metabase_create_dashboard
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error)"
|
||||
description: "Crea un nuevo dashboard vacio en Metabase. Para agregar cards usar MetabaseUpdateDashboard con el campo dashcards. Endpoint: POST /api/dashboard."
|
||||
tags: [metabase, dashboard, create, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_create_dashboard.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Crear dashboard vacio
|
||||
dashboard, err := MetabaseCreateDashboard(client, "Sales Overview", "KPIs de ventas", 5)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dashboardID := int(dashboard["id"].(float64))
|
||||
|
||||
// Luego agregar cards con MetabaseUpdateDashboard
|
||||
MetabaseUpdateDashboard(client, dashboardID, map[string]any{
|
||||
"dashcards": []map[string]any{
|
||||
{"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| name | string | si | Nombre del dashboard |
|
||||
| description | string | no | Descripcion. Vacio = sin descripcion |
|
||||
| collectionID | int | no | Coleccion destino. 0 = root |
|
||||
|
||||
El dashboard se crea vacio. Para agregar cards, usar MetabaseUpdateDashboard con el array dashcards.
|
||||
Retorna el objeto dashboard creado.
|
||||
@@ -0,0 +1,28 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseCreateUser crea un nuevo usuario en Metabase.
|
||||
// firstName, lastName y email son obligatorios.
|
||||
// password es opcional: si esta vacio, Metabase envia email de invitacion.
|
||||
// groupIDs es opcional: IDs de grupos a asignar (nil = solo grupo default).
|
||||
func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error) {
|
||||
body := map[string]any{
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
"email": email,
|
||||
}
|
||||
if password != "" {
|
||||
body["password"] = password
|
||||
}
|
||||
if len(groupIDs) > 0 {
|
||||
body["group_ids"] = groupIDs
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/user", body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase create user: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: metabase_create_user
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error)"
|
||||
description: "Crea un nuevo usuario en Metabase. Si no se provee password, Metabase envia email de invitacion. Requiere permisos de superusuario. Endpoint: POST /api/user."
|
||||
tags: [metabase, user, create, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_create_user.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Crear usuario con password
|
||||
user, err := MetabaseCreateUser(client, "John", "Doe", "john@example.com", "securePass123", nil)
|
||||
|
||||
// Crear usuario sin password (envia invitacion por email)
|
||||
user, err := MetabaseCreateUser(client, "Jane", "Smith", "jane@example.com", "", []int{1, 3})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado con permisos admin |
|
||||
| firstName | string | si | Nombre del usuario |
|
||||
| lastName | string | si | Apellido del usuario |
|
||||
| email | string | si | Email unico del usuario |
|
||||
| password | string | no | Password. Vacio = Metabase envia invitacion |
|
||||
| groupIDs | []int | no | IDs de grupos. nil = solo grupo default |
|
||||
|
||||
El email debe ser unico. Si ya existe, retorna error 400.
|
||||
Retorna el objeto usuario creado como map (mismos campos que MetabaseGetUser).
|
||||
@@ -0,0 +1,17 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseDeactivateUser desactiva (soft-delete) un usuario en Metabase.
|
||||
// El usuario no se elimina permanentemente, solo se marca como inactivo.
|
||||
// Requiere permisos de superusuario.
|
||||
func MetabaseDeactivateUser(client MetabaseClient, userID int) error {
|
||||
path := fmt.Sprintf("/api/user/%d", userID)
|
||||
|
||||
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase deactivate user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: metabase_deactivate_user
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseDeactivateUser(client MetabaseClient, userID int) error"
|
||||
description: "Desactiva (soft-delete) un usuario en Metabase. El usuario no se elimina permanentemente, solo se marca como inactivo. Para reactivar, usar PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id."
|
||||
tags: [metabase, user, delete, deactivate, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_deactivate_user.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
err := MetabaseDeactivateUser(client, 5)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Usuario 5 ahora esta inactivo
|
||||
// Para ver desactivados: MetabaseListUsers(client, "deactivated", "", 0, 0)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Es un soft-delete: el usuario se desactiva pero no se borra. Se puede reactivar con PUT /api/user/:id/reactivate.
|
||||
|
||||
Para listar usuarios desactivados, usar `MetabaseListUsers` con status "deactivated".
|
||||
|
||||
Requiere permisos de superusuario. Error 403 si no eres admin.
|
||||
@@ -0,0 +1,16 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseDeleteCard elimina permanentemente una card/pregunta de Metabase.
|
||||
// Para soft-delete, usar MetabaseUpdateCard con archived: true.
|
||||
func MetabaseDeleteCard(client MetabaseClient, cardID int) error {
|
||||
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||
|
||||
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase delete card %d: %w", cardID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: metabase_delete_card
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseDeleteCard(client MetabaseClient, cardID int) error"
|
||||
description: "Elimina permanentemente una card/pregunta de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateCard con archived:true. Endpoint: DELETE /api/card/:id."
|
||||
tags: [metabase, card, question, delete, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_delete_card.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Eliminar permanentemente
|
||||
err := MetabaseDeleteCard(client, 42)
|
||||
|
||||
// Preferir soft-delete cuando sea posible:
|
||||
// MetabaseUpdateCard(client, 42, map[string]any{"archived": true})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
**ATENCION**: Esta operacion es irreversible. La card se elimina permanentemente.
|
||||
|
||||
Para un borrado seguro, preferir archivar con `MetabaseUpdateCard(client, cardID, map[string]any{"archived": true})` que permite recuperar la card despues.
|
||||
@@ -0,0 +1,16 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseDeleteDashboard elimina permanentemente un dashboard de Metabase.
|
||||
// Para soft-delete, usar MetabaseUpdateDashboard con archived: true.
|
||||
func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error {
|
||||
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||
|
||||
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase delete dashboard %d: %w", dashboardID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: metabase_delete_dashboard
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error"
|
||||
description: "Elimina permanentemente un dashboard de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateDashboard con archived:true. Endpoint: DELETE /api/dashboard/:id."
|
||||
tags: [metabase, dashboard, delete, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_delete_dashboard.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Eliminar permanentemente
|
||||
err := MetabaseDeleteDashboard(client, 1)
|
||||
|
||||
// Preferir soft-delete:
|
||||
// MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
**ATENCION**: Esta operacion es irreversible. El dashboard y todas sus dashcards se eliminan permanentemente.
|
||||
|
||||
Para un borrado seguro, preferir archivar con `MetabaseUpdateDashboard(client, dashboardID, map[string]any{"archived": true})`.
|
||||
@@ -0,0 +1,22 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseExecuteCard ejecuta la query de una card/pregunta guardada.
|
||||
// parameters: parametros de la query (nil si no tiene parametros).
|
||||
// Retorna los resultados con columnas y filas.
|
||||
func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/card/%d/query", cardID)
|
||||
|
||||
var body map[string]any
|
||||
if len(parameters) > 0 {
|
||||
body = map[string]any{"parameters": parameters}
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("POST", client.BaseURL, client.Token, path, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase execute card %d: %w", cardID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: metabase_execute_card
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error)"
|
||||
description: "Ejecuta la query de una card/pregunta guardada en Metabase y retorna los resultados. Soporta parametros para queries parametrizadas. Endpoint: POST /api/card/:id/query."
|
||||
tags: [metabase, card, question, execute, query, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_execute_card.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Ejecutar sin parametros
|
||||
result, err := MetabaseExecuteCard(client, 42, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
data := result["data"].(map[string]any)
|
||||
rows := data["rows"].([]any)
|
||||
fmt.Printf("Filas: %d\n", len(rows))
|
||||
|
||||
// Ejecutar con parametros
|
||||
result, err := MetabaseExecuteCard(client, 42, []map[string]any{
|
||||
{
|
||||
"type": "category",
|
||||
"target": []any{"variable", []any{"template-tag", "status"}},
|
||||
"value": "active",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Estructura de la respuesta
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| status | string | "completed" o "failed" |
|
||||
| row_count | float64 | Numero de filas |
|
||||
| running_time | float64 | Tiempo de ejecucion en ms |
|
||||
| data.columns | []string | Nombres de columnas |
|
||||
| data.rows | [][]any | Filas de datos |
|
||||
| data.cols | []map | Metadata de columnas (name, base_type, display_name) |
|
||||
| data.native_form.query | string | SQL ejecutado |
|
||||
|
||||
### Parametros para queries parametrizadas
|
||||
|
||||
```go
|
||||
[]map[string]any{
|
||||
{
|
||||
"type": "category", // tipo del parametro
|
||||
"target": []any{"variable", []any{"template-tag", "tag"}}, // referencia al template-tag
|
||||
"value": "valor", // valor a inyectar
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Limite por defecto: 2000 filas. Para queries ad-hoc sin card, usar MetabaseExecuteQuery.
|
||||
@@ -0,0 +1,28 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseExecuteQuery ejecuta una query ad-hoc (sin guardar como card) en Metabase.
|
||||
// databaseID: ID de la base de datos en Metabase.
|
||||
// sql: query SQL a ejecutar.
|
||||
// maxResults: limite de filas (0 = default 2000 de Metabase).
|
||||
func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error) {
|
||||
body := map[string]any{
|
||||
"database": databaseID,
|
||||
"type": "native",
|
||||
"native": map[string]any{"query": sql},
|
||||
}
|
||||
if maxResults > 0 {
|
||||
body["constraints"] = map[string]any{
|
||||
"max-results": maxResults,
|
||||
"max-results-bare-rows": maxResults,
|
||||
}
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dataset", body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase execute query: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: metabase_execute_query
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error)"
|
||||
description: "Ejecuta una query SQL ad-hoc contra una database de Metabase sin guardarla como card. Util para consultas rapidas y exploracion. Endpoint: POST /api/dataset."
|
||||
tags: [metabase, query, execute, sql, dataset, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_execute_query.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Query simple
|
||||
result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM users LIMIT 10", 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
data := result["data"].(map[string]any)
|
||||
rows := data["rows"].([]any)
|
||||
|
||||
// Query con limite custom
|
||||
result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM orders", 5000)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| databaseID | int | si | ID de la database en Metabase (obtener con GET /api/database) |
|
||||
| sql | string | si | Query SQL a ejecutar |
|
||||
| maxResults | int | no | Limite de filas. 0 = default 2000 |
|
||||
|
||||
### Diferencia con MetabaseExecuteCard
|
||||
|
||||
- `MetabaseExecuteQuery`: query ad-hoc, no se guarda. Usa POST /api/dataset.
|
||||
- `MetabaseExecuteCard`: ejecuta una card ya guardada. Usa POST /api/card/:id/query.
|
||||
|
||||
Usar esta funcion para exploracion rapida. Si la query se va a reutilizar, crear una card con MetabaseCreateCard.
|
||||
|
||||
### Estructura de la respuesta
|
||||
|
||||
Misma estructura que MetabaseExecuteCard:
|
||||
- `data.columns`: nombres de columnas
|
||||
- `data.rows`: filas de datos
|
||||
- `row_count`: numero de filas
|
||||
- `running_time`: tiempo en ms
|
||||
- `status`: "completed" o "failed"
|
||||
@@ -0,0 +1,15 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseGetCard obtiene una card/pregunta de Metabase por su ID.
|
||||
func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||
|
||||
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase get card %d: %w", cardID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: metabase_get_card
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error)"
|
||||
description: "Obtiene los detalles completos de una card/pregunta de Metabase por su ID. Incluye la query, visualizacion y metadata. Endpoint: GET /api/card/:id."
|
||||
tags: [metabase, card, question, get, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_get_card.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
card, err := MetabaseGetCard(client, 42)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(card["name"], card["display"])
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Retorna el objeto card completo. Error 404 si no existe.
|
||||
|
||||
### Campos principales
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID de la card |
|
||||
| name | string | Nombre |
|
||||
| description | string | Descripcion |
|
||||
| display | string | Tipo visualizacion |
|
||||
| dataset_query | map | Query (native.query para SQL, query para MBQL) |
|
||||
| visualization_settings | map | Config de visualizacion |
|
||||
| collection_id | float64 | Coleccion contenedora |
|
||||
| database_id | float64 | Database asociada |
|
||||
| archived | bool | Archivada |
|
||||
| creator | map | Objeto del usuario creador |
|
||||
| created_at | string | Fecha creacion |
|
||||
| updated_at | string | Fecha actualizacion |
|
||||
@@ -0,0 +1,15 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseGetDashboard obtiene un dashboard completo de Metabase incluyendo sus cards.
|
||||
func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||
|
||||
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase get dashboard %d: %w", dashboardID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: metabase_get_dashboard
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error)"
|
||||
description: "Obtiene un dashboard completo de Metabase incluyendo todas sus dashcards (cards posicionadas en el dashboard), tabs y parametros. Endpoint: GET /api/dashboard/:id."
|
||||
tags: [metabase, dashboard, get, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_get_dashboard.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
dashboard, err := MetabaseGetDashboard(client, 1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(dashboard["name"])
|
||||
|
||||
// Acceder a las cards del dashboard
|
||||
dashcards := dashboard["dashcards"].([]any)
|
||||
for _, dc := range dashcards {
|
||||
card := dc.(map[string]any)
|
||||
fmt.Printf("Card ID: %v, Position: (%v, %v)\n",
|
||||
card["card_id"], card["col"], card["row"])
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Campos principales
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID del dashboard |
|
||||
| name | string | Nombre |
|
||||
| description | string | Descripcion |
|
||||
| dashcards | []map | Array de dashcards (cards posicionadas) |
|
||||
| parameters | []map | Filtros del dashboard |
|
||||
| tabs | []map | Tabs del dashboard |
|
||||
| collection_id | float64 | Coleccion contenedora |
|
||||
| archived | bool | Archivado |
|
||||
|
||||
### Estructura de cada dashcard
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID del dashcard (positivo) |
|
||||
| card_id | float64 | ID de la card/pregunta asociada |
|
||||
| card | map | Objeto card completo |
|
||||
| size_x | float64 | Ancho en grid (1-18) |
|
||||
| size_y | float64 | Alto en grid |
|
||||
| col | float64 | Columna en grid (0-based) |
|
||||
| row | float64 | Fila en grid (0-based) |
|
||||
| dashboard_tab_id | float64 | Tab al que pertenece (null = sin tabs) |
|
||||
| parameter_mappings | []map | Mapeo de filtros a la card |
|
||||
| visualization_settings | map | Settings de visualizacion |
|
||||
|
||||
Usar estos datos para construir el payload de MetabaseUpdateDashboard.
|
||||
@@ -0,0 +1,15 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseGetUser obtiene un usuario de Metabase por su ID.
|
||||
func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/user/%d", userID)
|
||||
|
||||
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase get user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: metabase_get_user
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error)"
|
||||
description: "Obtiene los detalles de un usuario de Metabase por su ID numerico. Endpoint: GET /api/user/:id."
|
||||
tags: [metabase, user, get, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_get_user.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
user, err := MetabaseGetUser(client, 1)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(user["email"], user["first_name"])
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Retorna el objeto usuario completo como map. Error 404 si el ID no existe.
|
||||
|
||||
### Campos del usuario retornado
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID numerico |
|
||||
| email | string | Email |
|
||||
| first_name | string | Nombre |
|
||||
| last_name | string | Apellido |
|
||||
| is_superuser | bool | Es admin |
|
||||
| is_active | bool | Esta activo |
|
||||
| common_name | string | Nombre completo |
|
||||
| date_joined | string | Fecha de creacion |
|
||||
| last_login | string | Ultimo login |
|
||||
| group_ids | []float64 | IDs de grupos |
|
||||
| locale | string | Locale del usuario |
|
||||
@@ -0,0 +1,107 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// metabaseRequest ejecuta una peticion HTTP contra la API de Metabase.
|
||||
// method: GET, POST, PUT, DELETE
|
||||
// baseURL: URL base sin trailing slash
|
||||
// token: session token o API key
|
||||
// path: ruta relativa (ej: "/api/user")
|
||||
// body: payload JSON (nil para requests sin body)
|
||||
// Retorna el body deserializado como map o nil si el body esta vacio.
|
||||
func metabaseRequest(method, baseURL, token, path string, body map[string]any) (map[string]any, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Metabase-Session", token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http %s %s: %w", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
if len(respBody) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// metabaseRequestList es como metabaseRequest pero para endpoints que retornan un array JSON.
|
||||
func metabaseRequestList(method, baseURL, token, path string, body map[string]any) ([]map[string]any, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Metabase-Session", token)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http %s %s: %w", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
if len(respBody) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result []map[string]any
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseListCards lista preguntas/cards de Metabase.
|
||||
// filter: "all", "mine", "fav", "archived", "recent", "popular", "database", "table" (vacio = todas).
|
||||
// modelID: ID de database o tabla cuando filter es "database" o "table" (0 = ignorar).
|
||||
func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error) {
|
||||
path := "/api/card"
|
||||
sep := "?"
|
||||
if filter != "" {
|
||||
path += sep + "f=" + filter
|
||||
sep = "&"
|
||||
}
|
||||
if modelID > 0 {
|
||||
path += fmt.Sprintf("%smodel_id=%d", sep, modelID)
|
||||
}
|
||||
|
||||
result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase list cards: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: metabase_list_cards
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error)"
|
||||
description: "Lista preguntas/cards de Metabase con filtro opcional. Retorna array de cards. Endpoint: GET /api/card."
|
||||
tags: [metabase, card, question, list, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_list_cards.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Listar todas las cards
|
||||
cards, err := MetabaseListCards(client, "all", 0)
|
||||
|
||||
// Solo mis preguntas
|
||||
cards, err := MetabaseListCards(client, "mine", 0)
|
||||
|
||||
// Cards de una database especifica
|
||||
cards, err := MetabaseListCards(client, "database", 1)
|
||||
|
||||
// Cards archivadas
|
||||
cards, err := MetabaseListCards(client, "archived", 0)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| filter | string | no | "all", "mine", "fav", "archived", "recent", "popular", "database", "table". Vacio = todas |
|
||||
| modelID | int | no | ID de database/tabla. Solo aplica con filter "database" o "table". 0 = ignorar |
|
||||
|
||||
No tiene paginacion con offset/limit. Retorna todas las cards que coinciden.
|
||||
|
||||
### Campos principales de cada card
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID numerico de la card |
|
||||
| name | string | Nombre de la pregunta |
|
||||
| description | string | Descripcion |
|
||||
| display | string | Tipo de visualizacion (table, bar, line, pie, etc.) |
|
||||
| collection_id | float64 | ID de la coleccion/carpeta |
|
||||
| database_id | float64 | ID de la database |
|
||||
| creator_id | float64 | ID del creador |
|
||||
| archived | bool | Esta archivada |
|
||||
| dataset_query | map | Query de la card (native o structured) |
|
||||
@@ -0,0 +1,19 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseListDashboards lista dashboards de Metabase.
|
||||
// filter: "all", "mine" o "archived" (vacio = todas).
|
||||
func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error) {
|
||||
path := "/api/dashboard"
|
||||
if filter != "" {
|
||||
path += "?f=" + filter
|
||||
}
|
||||
|
||||
result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase list dashboards: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: metabase_list_dashboards
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error)"
|
||||
description: "Lista dashboards de Metabase con filtro opcional. Retorna array de dashboards resumidos (sin dashcards). Endpoint: GET /api/dashboard."
|
||||
tags: [metabase, dashboard, list, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_list_dashboards.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Listar todos los dashboards
|
||||
dashboards, err := MetabaseListDashboards(client, "all")
|
||||
|
||||
// Solo mis dashboards
|
||||
dashboards, err := MetabaseListDashboards(client, "mine")
|
||||
|
||||
// Dashboards archivados
|
||||
dashboards, err := MetabaseListDashboards(client, "archived")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| filter | string | no | "all", "mine", "archived". Vacio = todas |
|
||||
|
||||
Retorna dashboards resumidos (sin cards). Para ver las cards de un dashboard, usar MetabaseGetDashboard.
|
||||
|
||||
### Campos principales de cada dashboard
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID del dashboard |
|
||||
| name | string | Nombre |
|
||||
| description | string | Descripcion |
|
||||
| collection_id | float64 | Coleccion contenedora |
|
||||
| creator_id | float64 | ID del creador |
|
||||
| archived | bool | Archivado |
|
||||
| created_at | string | Fecha creacion |
|
||||
@@ -0,0 +1,30 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseListUsers lista usuarios de Metabase con filtros opcionales.
|
||||
// status: "active", "deactivated" o "all" (vacio = "active").
|
||||
// query: filtro por nombre o email (vacio = sin filtro).
|
||||
// limit/offset: paginacion (0 = valores por defecto de Metabase).
|
||||
func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error) {
|
||||
path := "/api/user?"
|
||||
if status != "" {
|
||||
path += "status=" + status + "&"
|
||||
}
|
||||
if query != "" {
|
||||
path += "query=" + query + "&"
|
||||
}
|
||||
if limit > 0 {
|
||||
path += fmt.Sprintf("limit=%d&", limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
path += fmt.Sprintf("offset=%d&", offset)
|
||||
}
|
||||
|
||||
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase list users: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: metabase_list_users
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error)"
|
||||
description: "Lista usuarios de una instancia Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. Requiere permisos de superusuario."
|
||||
tags: [metabase, user, list, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_list_users.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
client, _ := MetabaseAuth("http://localhost:3000", "admin@example.com", "pass")
|
||||
|
||||
// Listar todos los usuarios activos
|
||||
users, err := MetabaseListUsers(client, "active", "", 0, 0)
|
||||
|
||||
// Buscar usuario por email
|
||||
users, err := MetabaseListUsers(client, "", "john@", 10, 0)
|
||||
|
||||
// Listar desactivados
|
||||
users, err := MetabaseListUsers(client, "deactivated", "", 25, 0)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Retorna un map con la estructura paginada de Metabase:
|
||||
- `data`: array de objetos usuario (id, email, first_name, last_name, is_superuser, etc.)
|
||||
- `total`: numero total de usuarios que coinciden
|
||||
- `limit`: tamanio de pagina usado
|
||||
- `offset`: offset usado
|
||||
|
||||
### Parametros para un LLM
|
||||
|
||||
| Parametro | Tipo | Requerido | Descripcion |
|
||||
|-----------|------|-----------|-------------|
|
||||
| client | MetabaseClient | si | Cliente autenticado |
|
||||
| status | string | no | "active" (default), "deactivated", "all" |
|
||||
| query | string | no | Filtro por nombre o email |
|
||||
| limit | int | no | Tamanio de pagina (0 = default Metabase) |
|
||||
| offset | int | no | Offset para paginacion (0 = inicio) |
|
||||
|
||||
### Campos del usuario retornado
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| id | float64 | ID numerico del usuario |
|
||||
| email | string | Email unico |
|
||||
| first_name | string | Nombre |
|
||||
| last_name | string | Apellido |
|
||||
| is_superuser | bool | Es admin |
|
||||
| is_active | bool | Esta activo |
|
||||
| common_name | string | Nombre completo |
|
||||
| last_login | string | Fecha ultimo login |
|
||||
@@ -0,0 +1,18 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseUpdateCard actualiza campos de una card/pregunta en Metabase.
|
||||
// fields es un map con los campos a actualizar.
|
||||
// Campos comunes: name, description, display, dataset_query, visualization_settings,
|
||||
// collection_id, archived, enable_embedding.
|
||||
func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||
|
||||
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase update card %d: %w", cardID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: metabase_update_card
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error)"
|
||||
description: "Actualiza campos de una card/pregunta en Metabase. Solo se modifican los campos incluidos en el map. Endpoint: PUT /api/card/:id."
|
||||
tags: [metabase, card, question, update, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_update_card.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Cambiar nombre y descripcion
|
||||
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||
"name": "Updated Revenue Chart",
|
||||
"description": "Now includes refunds",
|
||||
})
|
||||
|
||||
// Archivar una card (soft-delete)
|
||||
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||
"archived": true,
|
||||
})
|
||||
|
||||
// Mover a otra coleccion
|
||||
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||
"collection_id": 10,
|
||||
})
|
||||
|
||||
// Cambiar la query SQL
|
||||
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||
"dataset_query": map[string]any{
|
||||
"database": 1,
|
||||
"type": "native",
|
||||
"native": map[string]any{"query": "SELECT * FROM users LIMIT 100"},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Campos actualizables
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| name | string | Nombre de la pregunta |
|
||||
| description | string | Descripcion |
|
||||
| display | string | Tipo de visualizacion |
|
||||
| dataset_query | map | Query SQL o MBQL |
|
||||
| visualization_settings | map | Config de visualizacion |
|
||||
| collection_id | int | Mover a otra coleccion |
|
||||
| archived | bool | Archivar/desarchivar (soft-delete) |
|
||||
| enable_embedding | bool | Habilitar embedding publico |
|
||||
| embedding_params | map | Parametros de embedding |
|
||||
|
||||
Solo incluir los campos que se quieren cambiar.
|
||||
Para eliminar permanentemente usar MetabaseDeleteCard. Para soft-delete usar archived: true.
|
||||
@@ -0,0 +1,23 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseUpdateDashboard actualiza un dashboard en Metabase.
|
||||
// fields puede incluir metadata del dashboard Y/O la lista completa de dashcards y tabs.
|
||||
//
|
||||
// Para gestionar cards en el dashboard, incluir "dashcards" en fields:
|
||||
// - Agregar card: incluirla con ID negativo (ej: -1, -2)
|
||||
// - Actualizar card: incluirla con su ID positivo existente
|
||||
// - Eliminar card: omitirla del array (el array es el estado deseado completo)
|
||||
//
|
||||
// Campos comunes: name, description, archived, parameters, dashcards, tabs, collection_id.
|
||||
func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||
|
||||
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase update dashboard %d: %w", dashboardID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: metabase_update_dashboard
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error)"
|
||||
description: "Actualiza un dashboard en Metabase incluyendo metadata, cards y tabs. El campo dashcards representa el estado completo deseado: cards nuevas con ID negativo, existentes con ID positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id."
|
||||
tags: [metabase, dashboard, update, cards, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_update_dashboard.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Cambiar nombre
|
||||
MetabaseUpdateDashboard(client, 1, map[string]any{
|
||||
"name": "Updated Dashboard",
|
||||
})
|
||||
|
||||
// Agregar una card al dashboard
|
||||
// Primero obtener las dashcards existentes
|
||||
dash, _ := MetabaseGetDashboard(client, 1)
|
||||
existingCards := dash["dashcards"].([]any)
|
||||
|
||||
// Construir nuevo array con las existentes + la nueva
|
||||
dashcards := make([]map[string]any, 0)
|
||||
for _, dc := range existingCards {
|
||||
dashcards = append(dashcards, dc.(map[string]any))
|
||||
}
|
||||
// Agregar nueva card (ID negativo = nueva)
|
||||
dashcards = append(dashcards, map[string]any{
|
||||
"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0,
|
||||
})
|
||||
|
||||
MetabaseUpdateDashboard(client, 1, map[string]any{
|
||||
"dashcards": dashcards,
|
||||
})
|
||||
|
||||
// Archivar dashboard (soft-delete)
|
||||
MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Gestion de dashcards (IMPORTANTE)
|
||||
|
||||
El array `dashcards` representa el **estado completo deseado** del dashboard:
|
||||
|
||||
| Accion | Como hacerlo |
|
||||
|--------|-------------|
|
||||
| Agregar card | Incluir con **ID negativo** (-1, -2, etc.) |
|
||||
| Actualizar card | Incluir con su **ID positivo** existente |
|
||||
| Eliminar card | **Omitir** del array |
|
||||
| No cambiar cards | No incluir el campo dashcards |
|
||||
|
||||
**Flujo tipico para agregar una card:**
|
||||
1. `MetabaseGetDashboard` para obtener dashcards existentes
|
||||
2. Copiar las existentes al nuevo array
|
||||
3. Agregar la nueva con ID negativo
|
||||
4. Enviar el array completo
|
||||
|
||||
### Estructura de una dashcard
|
||||
|
||||
```go
|
||||
map[string]any{
|
||||
"id": -1, // negativo = nueva, positivo = existente
|
||||
"card_id": 42, // ID de la card/pregunta
|
||||
"size_x": 6, // ancho (1-18)
|
||||
"size_y": 4, // alto
|
||||
"col": 0, // columna (0-based)
|
||||
"row": 0, // fila (0-based)
|
||||
"dashboard_tab_id": nil, // tab (nil = sin tabs)
|
||||
"parameter_mappings": []map[string]any{}, // mapeo de filtros
|
||||
"visualization_settings": map[string]any{}, // settings custom
|
||||
}
|
||||
```
|
||||
|
||||
### Gestion de tabs
|
||||
|
||||
```go
|
||||
map[string]any{
|
||||
"tabs": []map[string]any{
|
||||
{"id": 1, "name": "Overview"}, // tab existente
|
||||
{"id": -1, "name": "Details"}, // tab nuevo (ID negativo)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Campos actualizables
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| name | string | Nombre del dashboard |
|
||||
| description | string | Descripcion |
|
||||
| archived | bool | Archivar/desarchivar |
|
||||
| dashcards | []map | Estado completo de cards |
|
||||
| tabs | []map | Tabs del dashboard |
|
||||
| parameters | []map | Filtros del dashboard |
|
||||
| collection_id | int | Mover a otra coleccion |
|
||||
@@ -0,0 +1,17 @@
|
||||
package infra
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MetabaseUpdateUser actualiza campos de un usuario en Metabase.
|
||||
// fields es un map con los campos a actualizar. Campos validos:
|
||||
// first_name, last_name, email, is_superuser, group_ids, locale, login_attributes.
|
||||
func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/user/%d", userID)
|
||||
|
||||
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metabase update user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: metabase_update_user
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error)"
|
||||
description: "Actualiza campos de un usuario en Metabase. Solo se modifican los campos incluidos en el map. Requiere permisos de superusuario. Endpoint: PUT /api/user/:id."
|
||||
tags: [metabase, user, update, api]
|
||||
uses_functions: []
|
||||
uses_types: [MetabaseClient_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/metabase_update_user.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Cambiar nombre
|
||||
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||
"first_name": "Jane",
|
||||
"last_name": "Smith",
|
||||
})
|
||||
|
||||
// Promover a admin
|
||||
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||
"is_superuser": true,
|
||||
})
|
||||
|
||||
// Cambiar grupos
|
||||
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||
"group_ids": []int{1, 3, 5},
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
### Campos actualizables
|
||||
|
||||
| Campo | Tipo | Descripcion |
|
||||
|-------|------|-------------|
|
||||
| first_name | string | Nombre |
|
||||
| last_name | string | Apellido |
|
||||
| email | string | Email (debe ser unico) |
|
||||
| is_superuser | bool | Permisos de admin |
|
||||
| group_ids | []int | IDs de grupos del usuario |
|
||||
| locale | string | Locale (ej: "es", "en") |
|
||||
| login_attributes | map | Atributos para sandboxing |
|
||||
|
||||
Solo incluir los campos que se quieren cambiar. Los demas se mantienen sin modificar.
|
||||
Retorna el objeto usuario actualizado.
|
||||
@@ -20,3 +20,9 @@ type ImageInfo struct {
|
||||
Size string
|
||||
Created string
|
||||
}
|
||||
|
||||
// MetabaseClient holds the connection details for a Metabase instance API.
|
||||
type MetabaseClient struct {
|
||||
BaseURL string // e.g. "http://localhost:3000"
|
||||
Token string // session token or API key
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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" },
|
||||
]
|
||||
+47
-19
@@ -19,6 +19,9 @@ type IndexResult struct {
|
||||
// and populates the database. It uses two passes:
|
||||
// 1. Parse all entries and collect known IDs
|
||||
// 2. Validate references against known IDs, then insert valid entries
|
||||
//
|
||||
// Scans functions/ and types/ at the root level, plus any language-specific
|
||||
// directories (e.g. python/functions/, python/types/).
|
||||
func Index(db *DB, root string) (*IndexResult, error) {
|
||||
if err := db.Purge(); err != nil {
|
||||
return nil, fmt.Errorf("purging database: %w", err)
|
||||
@@ -26,39 +29,50 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
||||
|
||||
result := &IndexResult{}
|
||||
|
||||
// Pass 1: parse everything
|
||||
// Pass 1: parse everything from all source directories
|
||||
var functions []*Function
|
||||
var types []*Type
|
||||
|
||||
functionsDir := filepath.Join(root, "functions")
|
||||
if _, err := os.Stat(functionsDir); err == nil {
|
||||
filepath.Walk(functionsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
f, err := ParseFunctionMD(path)
|
||||
// Directories to scan for functions and types.
|
||||
// Base dirs + language-specific dirs discovered automatically.
|
||||
funcDirs := []string{filepath.Join(root, "functions")}
|
||||
typeDirs := []string{filepath.Join(root, "types")}
|
||||
|
||||
// Discover language-specific directories (e.g. python/functions/, python/types/)
|
||||
entries, _ := os.ReadDir(root)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
langFuncs := filepath.Join(root, e.Name(), "functions")
|
||||
if fi, err := os.Stat(langFuncs); err == nil && fi.IsDir() {
|
||||
funcDirs = append(funcDirs, langFuncs)
|
||||
}
|
||||
langTypes := filepath.Join(root, e.Name(), "types")
|
||||
if fi, err := os.Stat(langTypes); err == nil && fi.IsDir() {
|
||||
typeDirs = append(typeDirs, langTypes)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range funcDirs {
|
||||
walkMD(dir, func(path string) {
|
||||
f, err := ParseFunctionMD(path, root)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
|
||||
return nil
|
||||
return
|
||||
}
|
||||
functions = append(functions, f)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
typesDir := filepath.Join(root, "types")
|
||||
if _, err := os.Stat(typesDir); err == nil {
|
||||
filepath.Walk(typesDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
t, err := ParseTypeMD(path)
|
||||
for _, dir := range typeDirs {
|
||||
walkMD(dir, func(path string) {
|
||||
t, err := ParseTypeMD(path, root)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("parse %s: %v", path, err))
|
||||
return nil
|
||||
return
|
||||
}
|
||||
types = append(types, t)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,3 +113,17 @@ func Index(db *DB, root string) (*IndexResult, error) {
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// walkMD walks a directory recursively and calls fn for each .md file found.
|
||||
func walkMD(dir string, fn func(path string)) {
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return
|
||||
}
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".md") {
|
||||
return nil
|
||||
}
|
||||
fn(path)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Add documentation fields to functions and types.
|
||||
-- examples: extracted code blocks from ## Ejemplo
|
||||
-- notes: extracted text from ## Notas
|
||||
-- documentation: remaining body text from .md
|
||||
-- code: source code from the referenced .go/.py/.tsx file
|
||||
|
||||
ALTER TABLE functions ADD COLUMN notes TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE functions ADD COLUMN documentation TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE functions ADD COLUMN code TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE types ADD COLUMN examples TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE types ADD COLUMN notes TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE types ADD COLUMN documentation TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE types ADD COLUMN code TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- Rebuild FTS for functions: add examples, notes, documentation, code
|
||||
DROP TRIGGER IF EXISTS functions_ai;
|
||||
DROP TRIGGER IF EXISTS functions_ad;
|
||||
DROP TRIGGER IF EXISTS functions_au;
|
||||
|
||||
INSERT INTO functions_fts(functions_fts) VALUES('rebuild');
|
||||
DROP TABLE IF EXISTS functions_fts;
|
||||
|
||||
CREATE VIRTUAL TABLE functions_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
signature,
|
||||
domain,
|
||||
example,
|
||||
notes,
|
||||
documentation,
|
||||
code,
|
||||
content='functions',
|
||||
content_rowid='rowid'
|
||||
);
|
||||
|
||||
-- Populate FTS from existing data
|
||||
INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
|
||||
SELECT rowid, id, name, description, tags, signature, domain, example, notes, documentation, code
|
||||
FROM functions;
|
||||
|
||||
CREATE TRIGGER functions_ai AFTER INSERT ON functions BEGIN
|
||||
INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER functions_ad AFTER DELETE ON functions BEGIN
|
||||
INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER functions_au AFTER UPDATE ON functions BEGIN
|
||||
INSERT INTO functions_fts(functions_fts, rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.signature, old.domain, old.example, old.notes, old.documentation, old.code);
|
||||
INSERT INTO functions_fts(rowid, id, name, description, tags, signature, domain, example, notes, documentation, code)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.signature, new.domain, new.example, new.notes, new.documentation, new.code);
|
||||
END;
|
||||
|
||||
-- Rebuild FTS for types: add examples, notes, documentation, code
|
||||
DROP TRIGGER IF EXISTS types_ai;
|
||||
DROP TRIGGER IF EXISTS types_ad;
|
||||
DROP TRIGGER IF EXISTS types_au;
|
||||
|
||||
INSERT INTO types_fts(types_fts) VALUES('rebuild');
|
||||
DROP TABLE IF EXISTS types_fts;
|
||||
|
||||
CREATE VIRTUAL TABLE types_fts USING fts5(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
domain,
|
||||
examples,
|
||||
notes,
|
||||
documentation,
|
||||
code,
|
||||
content='types',
|
||||
content_rowid='rowid'
|
||||
);
|
||||
|
||||
-- Populate FTS from existing data
|
||||
INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
|
||||
SELECT rowid, id, name, description, tags, domain, examples, notes, documentation, code
|
||||
FROM types;
|
||||
|
||||
CREATE TRIGGER types_ai AFTER INSERT ON types BEGIN
|
||||
INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.examples, new.notes, new.documentation, new.code);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER types_ad AFTER DELETE ON types BEGIN
|
||||
INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain, examples, notes, documentation, code)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.examples, old.notes, old.documentation, old.code);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER types_au AFTER UPDATE ON types BEGIN
|
||||
INSERT INTO types_fts(types_fts, rowid, id, name, description, tags, domain, examples, notes, documentation, code)
|
||||
VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain, old.examples, old.notes, old.documentation, old.code);
|
||||
INSERT INTO types_fts(rowid, id, name, description, tags, domain, examples, notes, documentation, code)
|
||||
VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain, new.examples, new.notes, new.documentation, new.code);
|
||||
END;
|
||||
+10
-3
@@ -47,6 +47,9 @@ type Function struct {
|
||||
ErrorType string `json:"error_type"`
|
||||
Imports []string `json:"imports"`
|
||||
Example string `json:"example"`
|
||||
Notes string `json:"notes"`
|
||||
Documentation string `json:"documentation"`
|
||||
Code string `json:"code"`
|
||||
Tested bool `json:"tested"`
|
||||
Tests []string `json:"tests"`
|
||||
TestFilePath string `json:"test_file_path"`
|
||||
@@ -82,9 +85,13 @@ type Type struct {
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
UsesTypes []string `json:"uses_types"`
|
||||
FilePath string `json:"file_path"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Examples string `json:"examples"`
|
||||
Notes string `json:"notes"`
|
||||
Documentation string `json:"documentation"`
|
||||
Code string `json:"code"`
|
||||
FilePath string `json:"file_path"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ProposalKind classifies a proposal.
|
||||
|
||||
+93
-37
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -73,7 +74,8 @@ func extractFrontmatter(data []byte) ([]byte, []byte, error) {
|
||||
}
|
||||
|
||||
// ParseFunctionMD parses a function .md file into a Function.
|
||||
func ParseFunctionMD(path string) (*Function, error) {
|
||||
// root is the registry root directory, used to resolve file_path for code reading.
|
||||
func ParseFunctionMD(path string, root string) (*Function, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", path, err)
|
||||
@@ -99,7 +101,7 @@ func ParseFunctionMD(path string) (*Function, error) {
|
||||
return nil, fmt.Errorf("%s: description is required", path)
|
||||
}
|
||||
|
||||
example := extractExample(body)
|
||||
sections := extractSections(body)
|
||||
|
||||
f := &Function{
|
||||
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
|
||||
@@ -118,6 +120,9 @@ func ParseFunctionMD(path string) (*Function, error) {
|
||||
ReturnsOptional: raw.ReturnsOptional,
|
||||
ErrorType: raw.ErrorType,
|
||||
Imports: raw.Imports,
|
||||
Example: sections.example,
|
||||
Notes: sections.notes,
|
||||
Documentation: sections.documentation,
|
||||
Tested: raw.Tested,
|
||||
Tests: raw.Tests,
|
||||
TestFilePath: raw.TestFilePath,
|
||||
@@ -129,21 +134,25 @@ func ParseFunctionMD(path string) (*Function, error) {
|
||||
Variant: raw.Variant,
|
||||
}
|
||||
|
||||
if example != "" && f.Example == "" {
|
||||
f.Example = example
|
||||
if root != "" && raw.FilePath != "" {
|
||||
codePath := filepath.Join(root, raw.FilePath)
|
||||
if codeData, err := os.ReadFile(codePath); err == nil {
|
||||
f.Code = string(codeData)
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ParseTypeMD parses a type .md file into a Type.
|
||||
func ParseTypeMD(path string) (*Type, error) {
|
||||
// root is the registry root directory, used to resolve file_path for code reading.
|
||||
func ParseTypeMD(path string, root string) (*Type, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", path, err)
|
||||
}
|
||||
|
||||
fm, _, err := extractFrontmatter(data)
|
||||
fm, body, err := extractFrontmatter(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing %s: %w", path, err)
|
||||
}
|
||||
@@ -160,49 +169,96 @@ func ParseTypeMD(path string) (*Type, error) {
|
||||
return nil, fmt.Errorf("%s: description is required", path)
|
||||
}
|
||||
|
||||
sections := extractSections(body)
|
||||
|
||||
t := &Type{
|
||||
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
|
||||
Name: raw.Name,
|
||||
Lang: raw.Lang,
|
||||
Domain: raw.Domain,
|
||||
Version: raw.Version,
|
||||
Algebraic: Algebraic(raw.Algebraic),
|
||||
Definition: strings.TrimSpace(raw.Definition),
|
||||
Description: raw.Description,
|
||||
Tags: raw.Tags,
|
||||
UsesTypes: raw.UsesTypes,
|
||||
FilePath: raw.FilePath,
|
||||
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
|
||||
Name: raw.Name,
|
||||
Lang: raw.Lang,
|
||||
Domain: raw.Domain,
|
||||
Version: raw.Version,
|
||||
Algebraic: Algebraic(raw.Algebraic),
|
||||
Definition: strings.TrimSpace(raw.Definition),
|
||||
Description: raw.Description,
|
||||
Tags: raw.Tags,
|
||||
UsesTypes: raw.UsesTypes,
|
||||
Examples: sections.example,
|
||||
Notes: sections.notes,
|
||||
Documentation: sections.documentation,
|
||||
FilePath: raw.FilePath,
|
||||
}
|
||||
|
||||
if root != "" && raw.FilePath != "" {
|
||||
codePath := filepath.Join(root, raw.FilePath)
|
||||
if codeData, err := os.ReadFile(codePath); err == nil {
|
||||
t.Code = string(codeData)
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// extractExample pulls the first code block after an "## Ejemplo" heading.
|
||||
func extractExample(body []byte) string {
|
||||
// bodySections holds the extracted sections from a .md body.
|
||||
type bodySections struct {
|
||||
example string // content under ## Ejemplo
|
||||
notes string // content under ## Notas
|
||||
documentation string // everything else
|
||||
}
|
||||
|
||||
// extractSections splits the markdown body into named sections.
|
||||
// Known sections (## Ejemplo, ## Notas) are extracted separately.
|
||||
// All other content (including unknown ## headings) goes into documentation.
|
||||
func extractSections(body []byte) bodySections {
|
||||
lines := strings.Split(string(body), "\n")
|
||||
inExample := false
|
||||
inCode := false
|
||||
var code []string
|
||||
var s bodySections
|
||||
|
||||
type section struct {
|
||||
name string
|
||||
lines []string
|
||||
}
|
||||
|
||||
var current *section
|
||||
var sections []section
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "## Ejemplo") {
|
||||
inExample = true
|
||||
continue
|
||||
}
|
||||
if inExample && !inCode && strings.HasPrefix(trimmed, "```") {
|
||||
inCode = true
|
||||
continue
|
||||
}
|
||||
if inCode {
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
return strings.Join(code, "\n")
|
||||
if strings.HasPrefix(trimmed, "## ") {
|
||||
if current != nil {
|
||||
sections = append(sections, *current)
|
||||
}
|
||||
code = append(code, line)
|
||||
current = §ion{name: trimmed}
|
||||
continue
|
||||
}
|
||||
if inExample && !inCode && strings.HasPrefix(trimmed, "##") {
|
||||
break
|
||||
if current != nil {
|
||||
current.lines = append(current.lines, line)
|
||||
} else {
|
||||
// Content before any ## heading goes to documentation
|
||||
sections = append(sections, section{name: "_preamble", lines: []string{line}})
|
||||
}
|
||||
}
|
||||
return ""
|
||||
if current != nil {
|
||||
sections = append(sections, *current)
|
||||
}
|
||||
|
||||
var docParts []string
|
||||
for _, sec := range sections {
|
||||
content := strings.TrimSpace(strings.Join(sec.lines, "\n"))
|
||||
if content == "" && sec.name == "_preamble" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(sec.name, "## Ejemplo"):
|
||||
s.example = content
|
||||
case strings.HasPrefix(sec.name, "## Notas"):
|
||||
s.notes = content
|
||||
case sec.name == "_preamble":
|
||||
docParts = append(docParts, content)
|
||||
default:
|
||||
// Unknown sections go to documentation with their heading
|
||||
docParts = append(docParts, sec.name+"\n\n"+content)
|
||||
}
|
||||
}
|
||||
s.documentation = strings.TrimSpace(strings.Join(docParts, "\n\n"))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ imports: [react]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/components/DataTable.tsx"
|
||||
file_path: "frontend/functions/ui/data_table.tsx"
|
||||
props:
|
||||
- name: data
|
||||
type: "T[]"
|
||||
@@ -105,7 +105,7 @@ func writeTempFile(t *testing.T, dir, name, content string) string {
|
||||
func TestParseFunctionMD(t *testing.T) {
|
||||
path := writeTempFile(t, t.TempDir(), "filter_slice.md", functionMD)
|
||||
|
||||
f, err := ParseFunctionMD(path)
|
||||
f, err := ParseFunctionMD(path, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func TestParseFunctionMD(t *testing.T) {
|
||||
func TestParseTypeMD(t *testing.T) {
|
||||
path := writeTempFile(t, t.TempDir(), "ohlcv.md", typeMD)
|
||||
|
||||
typ, err := ParseTypeMD(path)
|
||||
typ, err := ParseTypeMD(path, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestParseTypeMD(t *testing.T) {
|
||||
func TestParseComponentMD(t *testing.T) {
|
||||
path := writeTempFile(t, t.TempDir(), "DataTable.md", componentMD)
|
||||
|
||||
f, err := ParseFunctionMD(path)
|
||||
f, err := ParseFunctionMD(path, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func TestParseComponentMD(t *testing.T) {
|
||||
func TestParseMissingFrontmatter(t *testing.T) {
|
||||
path := writeTempFile(t, t.TempDir(), "bad.md", "# No frontmatter here\n")
|
||||
|
||||
_, err := ParseFunctionMD(path)
|
||||
_, err := ParseFunctionMD(path, "")
|
||||
if err == nil {
|
||||
t.Error("expected error for missing frontmatter")
|
||||
}
|
||||
|
||||
+11
-4
@@ -82,19 +82,22 @@ func (db *DB) InsertFunction(f *Function) error {
|
||||
description, tags, uses_functions, uses_types, returns,
|
||||
returns_optional, error_type, imports, example, tested,
|
||||
tests, test_file_path, file_path, created_at, updated_at,
|
||||
props, emits, has_state, framework, variant
|
||||
props, emits, has_state, framework, variant,
|
||||
notes, documentation, code
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?
|
||||
)`,
|
||||
f.ID, f.Name, string(f.Kind), f.Lang, f.Domain, f.Version, string(f.Purity), f.Signature,
|
||||
f.Description, marshalStrings(f.Tags), marshalStrings(f.UsesFunctions), marshalStrings(f.UsesTypes), marshalStrings(f.Returns),
|
||||
f.ReturnsOptional, f.ErrorType, marshalStrings(f.Imports), f.Example, f.Tested,
|
||||
marshalStrings(f.Tests), f.TestFilePath, f.FilePath, f.CreatedAt.Format(time.RFC3339), now,
|
||||
marshalProps(f.Props), marshalStrings(f.Emits), hasState, f.Framework, marshalStrings(f.Variant),
|
||||
f.Notes, f.Documentation, f.Code,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -115,11 +118,13 @@ func (db *DB) InsertType(t *Type) error {
|
||||
INSERT OR REPLACE INTO types (
|
||||
id, name, lang, domain, version, algebraic,
|
||||
definition, description, tags, uses_types,
|
||||
file_path, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
file_path, created_at, updated_at,
|
||||
examples, notes, documentation, code
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ID, t.Name, t.Lang, t.Domain, t.Version, string(t.Algebraic),
|
||||
t.Definition, t.Description, marshalStrings(t.Tags), marshalStrings(t.UsesTypes),
|
||||
t.FilePath, t.CreatedAt.Format(time.RFC3339), now,
|
||||
t.Examples, t.Notes, t.Documentation, t.Code,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -270,6 +275,7 @@ func scanFunctions(rows interface{ Next() bool; Scan(...any) error }) ([]Functio
|
||||
&f.ReturnsOptional, &f.ErrorType, &importsJSON, &f.Example, &f.Tested,
|
||||
&testsJSON, &f.TestFilePath, &f.FilePath, &createdAt, &updatedAt,
|
||||
&propsJSON, &emitsJSON, &hasState, &f.Framework, &variantJSON,
|
||||
&f.Notes, &f.Documentation, &f.Code,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning function: %w", err)
|
||||
@@ -308,6 +314,7 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error
|
||||
&t.ID, &t.Name, &t.Lang, &t.Domain, &t.Version, &t.Algebraic,
|
||||
&t.Definition, &t.Description, &tagsJSON, &usesTypJSON,
|
||||
&t.FilePath, &createdAt, &updatedAt,
|
||||
&t.Examples, &t.Notes, &t.Documentation, &t.Code,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning type: %w", err)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package infra
|
||||
|
||||
// MetabaseClient holds the connection details for a Metabase instance API.
|
||||
type MetabaseClient struct {
|
||||
BaseURL string // e.g. "http://localhost:3000"
|
||||
Token string // session token or API key
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: MetabaseClient
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
definition: |
|
||||
type MetabaseClient struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
}
|
||||
description: "Cliente para la API REST de Metabase. Contiene la URL base de la instancia y el token de autenticacion (session token o API key)."
|
||||
tags: [metabase, api, client, infra]
|
||||
uses_types: []
|
||||
file_path: "types/infra/metabase_client.go"
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
Tipo producto con dos campos obligatorios:
|
||||
- `BaseURL`: URL base de la instancia Metabase sin trailing slash (ej: `http://localhost:3000`)
|
||||
- `Token`: token de sesion obtenido con `MetabaseAuth()` o una API key creada en el admin UI de Metabase
|
||||
|
||||
El token se envia como header `X-Metabase-Session` en session tokens o `x-api-key` en API keys. Las funciones del registry usan `X-Metabase-Session` por defecto.
|
||||
Reference in New Issue
Block a user