diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index be1aa3e8..ac9608ce 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -55,9 +55,12 @@ sqlite3 registry.db ".schema" ``` fn-registry/ - functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity) + functions/{domain}/ # .go + .md por funcion Y tipo Go (core, finance, datascience, cybersecurity) functions/pipelines/ # Composiciones, siempre impuras - types/{domain}/ # .go + .md por tipo + types/{domain}/ # Solo .md de tipos (los .go viven en functions/{domain}/) + python/functions/ # .py + .md por funcion Python + python/types/ # .py + .md por tipo Python + bash/functions/ # .sh + .md por funcion Bash (core, infra, io, shell) 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 @@ -91,6 +94,23 @@ fn list [-d domain] [-k kind] fn show fn add -k function # Template +# Ejecutar funciones y pipelines (fn run) +fn run [args...] # Ejecuta por ID o nombre +fn run init_metabase --project test # Go pipeline (go run .) +fn run setup_metabase_volume # Bash pipeline (bash ) +fn run metabase_setup_py_infra # Python (python/.venv/bin/python3 ) +fn run my_component_ts_core # TypeScript (frontend/node_modules/.bin/tsx ) +fn run filter_slice_go_core # Go function con tests (go test -v) +fn run docker_pull_image_go_infra # Go function sin tests (go vet) +# Despacho por lenguaje: +# go (con main.go en dir) → go run . +# go (con tests) → go test -v -count=1 -tags fts5 ./pkg/ +# go (sin tests) → go vet -tags fts5 ./pkg/ +# py → python/.venv/bin/python3 +# bash → bash +# ts → frontend/node_modules/.bin/tsx +# Si el nombre es ambiguo, muestra los IDs para desambiguar. + # Proposals fn proposal add --kind new_function --title "..." --created-by agent [--target-id ] fn proposal list [-k kind] [-s status] @@ -110,12 +130,30 @@ fn ops assertion result add|list `FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio. +### Uso de fn run por agentes + +`fn run` permite ejecutar directamente funciones y pipelines del registry desde la terminal. Usar para: +- Lanzar pipelines con sus argumentos: `./fn run init_metabase --project fn_registry` +- Correr tests de funciones Go: `./fn run filter_slice_go_core` +- Ejecutar scripts Python/Bash del registry sin montar paths manualmente +- Verificar que funciones Go compilan correctamente (go vet) + +Entornos usados automaticamente: +- Python: `python/.venv/bin/python3` (venv del proyecto) +- TypeScript: `frontend/node_modules/.bin/tsx` (node del proyecto) +- Go: `go run .` / `go test` / `go vet` con `CGO_ENABLED=1 -tags fts5` +- Bash: `bash` del sistema + --- ## Añadir funciones 1. Consulta la BD para verificar que no existe algo similar -2. Crea dos archivos: `functions/{domain}/{name}.go` + `functions/{domain}/{name}.md` +2. Crea dos archivos segun el lenguaje: + - Go: `functions/{domain}/{name}.go` + `.md` + - Python: `python/functions/{domain}/{name}.py` + `.md` + - Bash: `bash/functions/{domain}/{name}.sh` + `.md` + - TypeScript: `frontend/functions/{domain}/{name}.ts` + `.md` 3. Ejecuta `./fn index` y verifica con `./fn show {id}` Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`. @@ -132,7 +170,13 @@ Reglas de integridad (el indexer las valida): ## Añadir tipos -Dos archivos: `types/{domain}/{name}.go` + `types/{domain}/{name}.md`. Ver template en `docs/templates/`. +Dos archivos en directorios separados: +- **Codigo Go:** `functions/{domain}/{name}.go` (junto a las funciones, mismo paquete Go) +- **Metadata .md:** `types/{domain}/{name}.md` con `file_path` apuntando a `functions/{domain}/{name}.go` + +Los `.go` de tipos viven en `functions/{domain}/` para que Go los compile en el mismo paquete que las funciones que los usan. Los `.md` se mantienen en `types/{domain}/` para que el indexer los identifique como tipos. + +Ver template en `docs/templates/`. --- diff --git a/.gitignore b/.gitignore index 84ae434f..ba7c706f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ registry.db-wal *.swo *~ +# Secrets +.env + # OS .DS_Store Thumbs.db diff --git a/apps/metabase_registry/main.py b/apps/metabase_registry/main.py new file mode 100644 index 00000000..2e5bd827 --- /dev/null +++ b/apps/metabase_registry/main.py @@ -0,0 +1,411 @@ +""" +apps/metabase_registry/main.py +============================== + +Setup completo de fn-registry en Metabase: datasource, cards y dashboard. + +USO +--- +Via variables de entorno: + + METABASE_URL=http://localhost:3000 \ + METABASE_ADMIN_EMAIL=admin@example.com \ + METABASE_ADMIN_PASSWORD=secret \ + REGISTRY_DB_PATH=/registry.db \ + python main.py + +Via argumentos CLI: + + python main.py \ + --url http://localhost:3000 \ + --admin-email admin@example.com \ + --admin-password secret \ + --registry-db-path /registry.db + +Para crear un usuario nuevo (opcional): + + python main.py ... \ + --new-user-email dev@example.com \ + --new-user-first-name Dev \ + --new-user-last-name User \ + --new-user-password devpass + +NOTA SOBRE registry.db EN DOCKER +---------------------------------- +Metabase corre en Docker y necesita acceder a registry.db. El path que +se configura en Metabase (--registry-db-path) debe ser la ruta DENTRO +del contenedor. Usa setup_volume.sh para copiar el archivo al contenedor +antes de ejecutar este script. + + ./setup_volume.sh /home/lucas/fn_registry/registry.db + +Tras copiar, el archivo queda en /registry.db dentro del contenedor, +que es el valor por defecto de --registry-db-path. + +DEPENDENCIAS +------------ +Instalar con: pip install -r requirements.txt +O con uv: uv pip install -r requirements.txt + +Las funciones metabase_add_database y metabase_list_databases deben +existir en python/functions/metabase/databases.py (creadas por el +fn-constructor). +""" + +import argparse +import os +import sys +import json + +# Agregar el directorio de funciones Python al path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions")) + +from metabase import ( + MetabaseClient, + metabase_create_user, + metabase_create_card, + metabase_create_dashboard, + metabase_update_dashboard, +) +from metabase.client import metabase_auth +from metabase.databases import metabase_add_database, metabase_list_databases + + +# --------------------------------------------------------------------------- +# Configuracion de cards de ejemplo +# --------------------------------------------------------------------------- + +CARDS_CONFIG = [ + { + "name": "Funciones por dominio", + "description": "Cuenta de funciones agrupadas por dominio del registry", + "sql": "SELECT domain, count(*) AS total FROM functions GROUP BY domain ORDER BY total DESC", + "display": "bar", + }, + { + "name": "Funciones puras vs impuras", + "description": "Distribucion de pureza funcional en el registry", + "sql": "SELECT purity, count(*) AS total FROM functions GROUP BY purity ORDER BY total DESC", + "display": "pie", + }, + { + "name": "Funciones por kind", + "description": "Distribucion por tipo: function, pipeline, component", + "sql": "SELECT kind, count(*) AS total FROM functions GROUP BY kind ORDER BY total DESC", + "display": "bar", + }, + { + "name": "Buscar funciones", + "description": "Lista completa de funciones con id, dominio, pureza y descripcion", + "sql": ( + "SELECT id, domain, kind, purity, signature, description " + "FROM functions ORDER BY domain, name" + ), + "display": "table", + }, + { + "name": "Tipos por dominio", + "description": "Cuenta de tipos registrados por dominio", + "sql": "SELECT domain, count(*) AS total FROM types GROUP BY domain ORDER BY total DESC", + "display": "bar", + }, + { + "name": "Proposals pendientes", + "description": "Proposals del registry que estan pendientes de revision", + "sql": ( + "SELECT id, kind, title, created_by, created_at " + "FROM proposals WHERE status = 'pending' ORDER BY created_at DESC" + ), + "display": "table", + }, +] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def log(msg: str) -> None: + print(f"[metabase_registry] {msg}", flush=True) + + +def log_ok(msg: str) -> None: + print(f"[metabase_registry] OK {msg}", flush=True) + + +def log_err(msg: str) -> None: + print(f"[metabase_registry] ERR {msg}", file=sys.stderr, flush=True) + + +def find_existing_database(client: MetabaseClient, name: str) -> dict | None: + """Busca una database por nombre en Metabase. Retorna None si no existe.""" + try: + databases = metabase_list_databases(client) + for db in databases: + if db.get("name") == name: + return db + except Exception as e: + log(f"No se pudo listar databases: {e}") + return None + + +def build_dataset_query(database_id: int, sql: str) -> dict: + """Construye el dataset_query para una card SQL nativa.""" + return { + "type": "native", + "database": database_id, + "native": { + "query": sql, + "template-tags": {}, + }, + } + + +def create_cards(client: MetabaseClient, database_id: int) -> list[dict]: + """Crea todas las cards de ejemplo. Retorna lista de cards creadas.""" + created = [] + for cfg in CARDS_CONFIG: + log(f"Creando card: {cfg['name']!r} ...") + try: + card = metabase_create_card( + client=client, + name=cfg["name"], + dataset_query=build_dataset_query(database_id, cfg["sql"]), + display=cfg["display"], + description=cfg["description"], + ) + log_ok(f"Card creada: id={card['id']} nombre={card['name']!r}") + created.append(card) + except Exception as e: + log_err(f"No se pudo crear card {cfg['name']!r}: {e}") + return created + + +def create_overview_dashboard(client: MetabaseClient, cards: list[dict]) -> dict | None: + """Crea el dashboard 'fn-registry Overview' con todas las cards.""" + log("Creando dashboard 'fn-registry Overview' ...") + try: + dashboard = metabase_create_dashboard( + client=client, + name="fn-registry Overview", + description="Vista general del function registry: funciones, tipos, proposals y metricas.", + ) + log_ok(f"Dashboard creado: id={dashboard['id']}") + except Exception as e: + log_err(f"No se pudo crear dashboard: {e}") + return None + + if not cards: + log("Sin cards para agregar al dashboard.") + return dashboard + + # Posicionar cards en una grilla de 3 columnas, 24 unidades de ancho total. + # Cada card ocupa 8 columnas x 6 filas. + cols = 3 + card_w = 8 + card_h = 6 + + dashcards = [] + for idx, card in enumerate(cards): + col = (idx % cols) * card_w + row = (idx // cols) * card_h + dashcards.append({ + "id": -(idx + 1), # ID negativo = nueva dashcard + "card_id": card["id"], + "size_x": card_w, + "size_y": card_h, + "col": col, + "row": row, + "parameter_mappings": [], + "visualization_settings": {}, + }) + + try: + metabase_update_dashboard( + client=client, + dashboard_id=dashboard["id"], + dashcards=dashcards, + ) + log_ok(f"Dashboard actualizado con {len(dashcards)} cards.") + except Exception as e: + log_err(f"No se pudo poblar el dashboard con cards: {e}") + + return dashboard + + +# --------------------------------------------------------------------------- +# Flujo principal +# --------------------------------------------------------------------------- + +def run(args: argparse.Namespace) -> int: + """Ejecuta el setup completo. Retorna 0 en exito, 1 en fallo critico.""" + + # 1. Autenticar como admin + log(f"Autenticando en {args.url} como {args.admin_email} ...") + try: + client = metabase_auth(args.url, args.admin_email, args.admin_password) + log_ok("Autenticacion exitosa.") + except Exception as e: + log_err(f"Fallo de autenticacion: {e}") + return 1 + + with client: + # 2. Crear usuario nuevo (opcional) + if args.new_user_email: + if not args.new_user_first_name or not args.new_user_last_name: + log_err("Para crear usuario se requieren --new-user-first-name y --new-user-last-name.") + return 1 + log(f"Creando usuario {args.new_user_email} ...") + try: + user = metabase_create_user( + client=client, + first_name=args.new_user_first_name, + last_name=args.new_user_last_name, + email=args.new_user_email, + password=args.new_user_password or "", + ) + log_ok(f"Usuario creado: id={user['id']} email={user['email']}") + except Exception as e: + log_err(f"No se pudo crear usuario: {e}") + # No es critico, continuar + + # 3. Agregar registry.db como datasource SQLite + db_name = "fn-registry" + log(f"Verificando si ya existe la database {db_name!r} en Metabase ...") + existing_db = find_existing_database(client, db_name) + + if existing_db: + db_id = existing_db["id"] + log_ok(f"Database ya existe: id={db_id} nombre={db_name!r}. Se reutiliza.") + else: + log(f"Registrando registry.db ({args.registry_db_path}) como datasource SQLite ...") + try: + new_db = metabase_add_database( + client=client, + name=db_name, + engine="sqlite", + details={"db": args.registry_db_path}, + ) + db_id = new_db["id"] + log_ok(f"Database registrada: id={db_id} path={args.registry_db_path}") + except Exception as e: + log_err(f"No se pudo registrar la database: {e}") + log_err( + "Verifica que registry.db este accesible desde el contenedor Docker. " + "Usa setup_volume.sh para copiar el archivo." + ) + return 1 + + # 4. Crear cards de ejemplo + log(f"Creando {len(CARDS_CONFIG)} cards con database_id={db_id} ...") + cards = create_cards(client, db_id) + log(f"Cards creadas: {len(cards)}/{len(CARDS_CONFIG)}") + + # 5. Crear dashboard + dashboard = create_overview_dashboard(client, cards) + if dashboard: + log_ok( + f"Dashboard disponible en: {args.url}/dashboard/{dashboard['id']}" + ) + else: + log_err("El dashboard no pudo crearse.") + + summary = { + "url": args.url, + "database_id": db_id, + "cards_created": len(cards), + "dashboard_id": dashboard["id"] if dashboard else None, + "dashboard_url": f"{args.url}/dashboard/{dashboard['id']}" if dashboard else None, + } + print("\n--- Resumen ---") + print(json.dumps(summary, indent=2)) + + return 0 + + +# --------------------------------------------------------------------------- +# CLI / env vars +# --------------------------------------------------------------------------- + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="metabase_registry", + description="Setup de fn-registry como datasource en Metabase.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + # Conexion Metabase + p.add_argument( + "--url", + default=os.environ.get("METABASE_URL", "http://localhost:3000"), + help="URL base de Metabase (env: METABASE_URL). Default: http://localhost:3000", + ) + p.add_argument( + "--admin-email", + default=os.environ.get("METABASE_ADMIN_EMAIL", "admin@example.com"), + dest="admin_email", + help="Email del admin (env: METABASE_ADMIN_EMAIL).", + ) + p.add_argument( + "--admin-password", + default=os.environ.get("METABASE_ADMIN_PASSWORD", ""), + dest="admin_password", + help="Password del admin (env: METABASE_ADMIN_PASSWORD).", + ) + + # Registry DB path (ruta dentro del contenedor Docker) + p.add_argument( + "--registry-db-path", + default=os.environ.get("REGISTRY_DB_PATH", "/registry.db"), + dest="registry_db_path", + help=( + "Ruta al registry.db DENTRO del contenedor Docker " + "(env: REGISTRY_DB_PATH). Default: /registry.db" + ), + ) + + # Nuevo usuario (todos opcionales) + p.add_argument( + "--new-user-email", + default=os.environ.get("NEW_USER_EMAIL", ""), + dest="new_user_email", + help="Email del nuevo usuario a crear (env: NEW_USER_EMAIL). Opcional.", + ) + p.add_argument( + "--new-user-first-name", + default=os.environ.get("NEW_USER_FIRST_NAME", ""), + dest="new_user_first_name", + help="Nombre del nuevo usuario (env: NEW_USER_FIRST_NAME).", + ) + p.add_argument( + "--new-user-last-name", + default=os.environ.get("NEW_USER_LAST_NAME", ""), + dest="new_user_last_name", + help="Apellido del nuevo usuario (env: NEW_USER_LAST_NAME).", + ) + p.add_argument( + "--new-user-password", + default=os.environ.get("NEW_USER_PASSWORD", ""), + dest="new_user_password", + help="Password del nuevo usuario (env: NEW_USER_PASSWORD). Opcional.", + ) + + return p + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + if not args.admin_password: + parser.error( + "Se requiere la password del admin. " + "Usa --admin-password o la variable de entorno METABASE_ADMIN_PASSWORD." + ) + + sys.exit(run(args)) + + +if __name__ == "__main__": + main() diff --git a/apps/metabase_registry/requirements.txt b/apps/metabase_registry/requirements.txt new file mode 100644 index 00000000..79865d30 --- /dev/null +++ b/apps/metabase_registry/requirements.txt @@ -0,0 +1,13 @@ +# Dependencias de apps/metabase_registry +# +# Instalar con pip: +# pip install -r requirements.txt +# +# O con uv (recomendado, desde la raiz del repo): +# cd /home/lucas/fn_registry/python && uv pip install -r ../apps/metabase_registry/requirements.txt +# +# O directamente desde el directorio python (que ya tiene httpx): +# cd /home/lucas/fn_registry/python && uv run python ../apps/metabase_registry/main.py + +# HTTP client — mismo que usa el paquete python/functions/metabase +httpx>=0.27.0 diff --git a/bash/functions/infra/assert_docker_container_running.md b/bash/functions/infra/assert_docker_container_running.md new file mode 100644 index 00000000..734792b9 --- /dev/null +++ b/bash/functions/infra/assert_docker_container_running.md @@ -0,0 +1,38 @@ +--- +name: assert_docker_container_running +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "assert_docker_container_running(container_name: string) -> void" +description: "Verifica que un contenedor Docker está corriendo. Sale con exit code 1 si no está activo, con mensaje a stderr." +tags: [assert, docker, container, running, validation, infra, bash] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/assert_docker_container_running.sh" +--- + +## Ejemplo + +```bash +source functions/infra/assert_docker_container_running.sh + +assert_docker_container_running metabase +echo "Contenedor activo, continuando..." +``` + +## Notas + +Usa `docker ps --format '{{.Names}}'` con grep anclado (`^name$`) para evitar matches parciales (ej: "metabase" no matchea "metabase-test"). + +Output limpio: void en éxito. El mensaje de error en stderr no incluye lista de contenedores activos — eso es responsabilidad del pipeline/caller. + +Requiere que `docker` esté en PATH. Combinar con `assert_command_exists` antes de llamar. diff --git a/bash/functions/infra/assert_docker_container_running.sh b/bash/functions/infra/assert_docker_container_running.sh new file mode 100644 index 00000000..fc0a4a7c --- /dev/null +++ b/bash/functions/infra/assert_docker_container_running.sh @@ -0,0 +1,19 @@ +# assert_docker_container_running +# -------------------------------- +# Verifica que un contenedor Docker está corriendo. +# No produce output a stdout en caso de éxito. +# Sale con exit code 1 si el contenedor no está corriendo, +# con mensaje descriptivo a stderr. +# +# USO (sourced): +# source assert_docker_container_running.sh +# assert_docker_container_running metabase + +assert_docker_container_running() { + local container_name="$1" + + if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then + echo "assert_docker_container_running: el contenedor '$container_name' no está corriendo" >&2 + return 1 + fi +} diff --git a/bash/functions/infra/docker_cp_file.md b/bash/functions/infra/docker_cp_file.md new file mode 100644 index 00000000..892dc922 --- /dev/null +++ b/bash/functions/infra/docker_cp_file.md @@ -0,0 +1,41 @@ +--- +name: docker_cp_file +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "docker_cp_file(local_path: string, container_name: string, dest_path: string) -> string" +description: "Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. Imprime JSON con local_size y remote_size a stdout. Sale con exit code 1 si docker cp falla o los tamaños difieren." +tags: [docker, cp, copy, file, container, transfer, infra, bash] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/docker_cp_file.sh" +--- + +## Ejemplo + +```bash +source functions/infra/docker_cp_file.sh + +result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db) +echo "$result" +# {"local_size":524288,"remote_size":524288} + +local_size=$(echo "$result" | grep -o '"local_size":[0-9]*' | cut -d: -f2) +``` + +## Notas + +La verificación de tamaño usa `docker exec stat -c%s` sobre el contenedor destino. Si `stat` no está disponible en el contenedor, `remote_size` será -1 y la función fallará. + +Output a stdout: JSON minificado con campos `local_size` y `remote_size` (enteros, bytes). + +Usa `printf` en lugar de `echo` para garantizar que no haya newline extra en el JSON. diff --git a/bash/functions/infra/docker_cp_file.sh b/bash/functions/infra/docker_cp_file.sh new file mode 100644 index 00000000..adf96d1f --- /dev/null +++ b/bash/functions/infra/docker_cp_file.sh @@ -0,0 +1,33 @@ +# docker_cp_file +# -------------- +# Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. +# Imprime JSON con local_size y remote_size a stdout si la copia es exitosa. +# Sale con exit code 1 si docker cp falla o si los tamaños no coinciden. +# +# USO (sourced): +# source docker_cp_file.sh +# result=$(docker_cp_file /ruta/local.db metabase /dest/path.db) + +docker_cp_file() { + local local_path="$1" + local container_name="$2" + local dest_path="$3" + + if ! docker cp "$local_path" "${container_name}:${dest_path}" 2>/dev/null; then + echo "docker_cp_file: fallo al copiar '$local_path' a '${container_name}:${dest_path}'" >&2 + return 1 + fi + + local local_size + local_size=$(stat -c%s "$local_path") + + local remote_size + remote_size=$(docker exec "$container_name" stat -c%s "$dest_path" 2>/dev/null || echo "-1") + + if [ "$local_size" != "$remote_size" ]; then + echo "docker_cp_file: tamaños no coinciden (local=${local_size}, remoto=${remote_size})" >&2 + return 1 + fi + + printf '{"local_size":%s,"remote_size":%s}' "$local_size" "$remote_size" +} diff --git a/bash/functions/pipelines/setup_metabase_volume.md b/bash/functions/pipelines/setup_metabase_volume.md new file mode 100644 index 00000000..89d49ac8 --- /dev/null +++ b/bash/functions/pipelines/setup_metabase_volume.md @@ -0,0 +1,65 @@ +--- +name: setup_metabase_volume +kind: pipeline +lang: bash +domain: pipelines +version: "1.0.0" +purity: impure +signature: "setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void" +description: "Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables." +tags: [metabase, docker, setup, launcher, pipeline, bash, infra] +uses_functions: + - assert_file_exists_bash_shell + - assert_command_exists_bash_shell + - assert_docker_container_running_bash_infra + - docker_cp_file_bash_infra +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/pipelines/setup_metabase_volume.sh" +--- + +## Ejemplo + +```bash +# Con defaults +./functions/pipelines/setup_metabase_volume.sh + +# Con argumentos explícitos +./functions/pipelines/setup_metabase_volume.sh \ + /home/lucas/fn_registry/registry.db \ + metabase \ + /registry.db +``` + +## Flujo + +1. `assert_file_exists` — verifica que `registry.db` existe localmente y obtiene su tamaño +2. `assert_command_exists` — verifica que `docker` está disponible en PATH +3. `assert_docker_container_running` — verifica que el contenedor destino está activo; si falla, muestra lista de contenedores activos +4. `docker_cp_file` — ejecuta `docker cp` y verifica que los tamaños local y remoto coinciden + +## Notas + +El pipeline usa `set -euo pipefail` — cualquier fallo en una función individual detiene la ejecución. + +Las funciones individuales se sourcean desde sus rutas en el registry, relativas a `REGISTRY_ROOT` detectado automáticamente desde la ubicación del script. + +Defaults: +- `REGISTRY_DB_PATH`: `/home/lucas/fn_registry/registry.db` +- `CONTAINER_NAME`: `metabase` +- `DEST_PATH`: `/registry.db` + +Nota de persistencia: `docker cp` copia al contenedor en ejecución. Si el contenedor se reinicia, el archivo se pierde. Para persistencia real, montar el directorio como volumen en docker-compose: + +```yaml +volumes: + - /home/lucas/fn_registry:/fn_registry:ro +``` + +Y usar `--registry-db-path /fn_registry/registry.db`. diff --git a/bash/functions/pipelines/setup_metabase_volume.sh b/bash/functions/pipelines/setup_metabase_volume.sh new file mode 100755 index 00000000..8468458b --- /dev/null +++ b/bash/functions/pipelines/setup_metabase_volume.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# setup_metabase_volume +# --------------------- +# Copia registry.db al contenedor Docker de Metabase. +# Compone: assert_file_exists + assert_command_exists + +# assert_docker_container_running + docker_cp_file +# +# USO: +# ./setup_metabase_volume.sh [REGISTRY_DB_PATH] [CONTAINER_NAME] [DEST_PATH] +# +# ARGUMENTOS (opcionales, con defaults): +# REGISTRY_DB_PATH Ruta local al registry.db +# Default: /home/lucas/fn_registry/registry.db +# CONTAINER_NAME Nombre del contenedor Docker de Metabase +# Default: metabase +# DEST_PATH Ruta destino dentro del contenedor +# Default: /registry.db + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh" +source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh" +source "$REGISTRY_ROOT/bash/functions/infra/assert_docker_container_running.sh" +source "$REGISTRY_ROOT/bash/functions/infra/docker_cp_file.sh" + +REGISTRY_DB_PATH="${1:-/home/lucas/fn_registry/registry.db}" +CONTAINER_NAME="${2:-metabase}" +DEST_PATH="${3:-/registry.db}" + +echo "[setup_metabase_volume] Configuracion:" +echo " registry.db local : $REGISTRY_DB_PATH" +echo " contenedor Docker : $CONTAINER_NAME" +echo " ruta en contenedor : $DEST_PATH" +echo "" + +# 1. Verificar archivo local +local_size=$(assert_file_exists "$REGISTRY_DB_PATH") +echo "[setup_metabase_volume] Archivo local encontrado: ${local_size} bytes" + +# 2. Verificar que docker esta disponible +assert_command_exists docker +echo "[setup_metabase_volume] docker disponible en PATH." + +# 3. Verificar que el contenedor esta corriendo +if ! assert_docker_container_running "$CONTAINER_NAME"; then + echo "" >&2 + echo "Contenedores activos:" >&2 + docker ps --format " {{.Names}}\t{{.Status}}\t{{.Image}}" >&2 + echo "" >&2 + echo "Si el contenedor se llama diferente, pasa el nombre como segundo argumento:" >&2 + echo " ./setup_metabase_volume.sh $REGISTRY_DB_PATH " >&2 + exit 1 +fi +echo "[setup_metabase_volume] Contenedor '$CONTAINER_NAME' encontrado y activo." + +# 4. Copiar archivo y verificar tamaños +echo "[setup_metabase_volume] Copiando $REGISTRY_DB_PATH -> ${CONTAINER_NAME}:${DEST_PATH} ..." +result=$(docker_cp_file "$REGISTRY_DB_PATH" "$CONTAINER_NAME" "$DEST_PATH") + +remote_size=$(echo "$result" | grep -o '"remote_size":[0-9]*' | cut -d: -f2) +echo "[setup_metabase_volume] OK Copia completada y verificada." +echo "[setup_metabase_volume] Tamanio: ${local_size} bytes (local) = ${remote_size} bytes (remoto)" + +echo "" +echo "---------------------------------------------------------------------" +echo "registry.db disponible en el contenedor como: $DEST_PATH" +echo "" +echo "Ahora ejecuta main.py con:" +echo "" +echo " METABASE_ADMIN_PASSWORD= \\" +echo " REGISTRY_DB_PATH=${DEST_PATH} \\" +echo " python apps/metabase_registry/main.py" +echo "" +echo "O bien:" +echo "" +echo " python apps/metabase_registry/main.py \\" +echo " --admin-password \\" +echo " --registry-db-path ${DEST_PATH}" +echo "---------------------------------------------------------------------" diff --git a/bash/functions/shell/assert_command_exists.md b/bash/functions/shell/assert_command_exists.md new file mode 100644 index 00000000..46e1f958 --- /dev/null +++ b/bash/functions/shell/assert_command_exists.md @@ -0,0 +1,36 @@ +--- +name: assert_command_exists +kind: function +lang: bash +domain: shell +version: "1.0.0" +purity: impure +signature: "assert_command_exists(command_name: string) -> void" +description: "Verifica que un comando está disponible en el PATH. Sale con exit code 1 si no se encuentra, con mensaje a stderr." +tags: [assert, command, exists, validation, shell, bash, path] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/shell/assert_command_exists.sh" +--- + +## Ejemplo + +```bash +source functions/shell/assert_command_exists.sh + +assert_command_exists docker +assert_command_exists jq +``` + +## Notas + +Usa `command -v` (POSIX) con redirección `&>/dev/null` para suprimir output. No produce nada a stdout en caso de éxito. + +Output limpio: void en éxito, mensaje a stderr en fallo. diff --git a/bash/functions/shell/assert_command_exists.sh b/bash/functions/shell/assert_command_exists.sh new file mode 100644 index 00000000..fddc16c8 --- /dev/null +++ b/bash/functions/shell/assert_command_exists.sh @@ -0,0 +1,18 @@ +# assert_command_exists +# --------------------- +# Verifica que un comando está disponible en el PATH. +# No produce output a stdout. +# Sale con exit code 1 si el comando no se encuentra. +# +# USO (sourced): +# source assert_command_exists.sh +# assert_command_exists docker + +assert_command_exists() { + local command_name="$1" + + if ! command -v "$command_name" &>/dev/null; then + echo "assert_command_exists: comando no encontrado en PATH: $command_name" >&2 + return 1 + fi +} diff --git a/bash/functions/shell/assert_file_exists.md b/bash/functions/shell/assert_file_exists.md new file mode 100644 index 00000000..5c9a35a4 --- /dev/null +++ b/bash/functions/shell/assert_file_exists.md @@ -0,0 +1,38 @@ +--- +name: assert_file_exists +kind: function +lang: bash +domain: shell +version: "1.0.0" +purity: impure +signature: "assert_file_exists(file_path: string) -> string" +description: "Verifica que un archivo existe en el filesystem. Imprime su tamaño en bytes a stdout. Sale con exit code 1 si el archivo no existe." +tags: [assert, file, exists, validation, shell, bash] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/shell/assert_file_exists.sh" +--- + +## Ejemplo + +```bash +source functions/shell/assert_file_exists.sh + +size=$(assert_file_exists /home/lucas/fn_registry/registry.db) +echo "Tamaño: $size bytes" +``` + +## Notas + +La función se sourcea, no se ejecuta directamente. Usa `stat -c%s` para obtener el tamaño en bytes (compatible con GNU coreutils / Linux). + +Output limpio: solo el número de bytes a stdout. Los errores van a stderr. + +No usa `set -e` internamente — el caller controla el flujo con el exit code de retorno. diff --git a/bash/functions/shell/assert_file_exists.sh b/bash/functions/shell/assert_file_exists.sh new file mode 100644 index 00000000..05299b2d --- /dev/null +++ b/bash/functions/shell/assert_file_exists.sh @@ -0,0 +1,20 @@ +# assert_file_exists +# ------------------ +# Verifica que un archivo existe en el filesystem. +# Imprime su tamaño en bytes a stdout. +# Sale con exit code 1 si el archivo no existe. +# +# USO (sourced): +# source assert_file_exists.sh +# size=$(assert_file_exists /ruta/al/archivo) + +assert_file_exists() { + local file_path="$1" + + if [ ! -f "$file_path" ]; then + echo "assert_file_exists: archivo no encontrado: $file_path" >&2 + return 1 + fi + + stat -c%s "$file_path" +} diff --git a/cmd/fn/main.go b/cmd/fn/main.go index cfa6a068..40507c40 100644 --- a/cmd/fn/main.go +++ b/cmd/fn/main.go @@ -33,6 +33,8 @@ func main() { cmdOps(os.Args[2:]) case "proposal": cmdProposal(os.Args[2:]) + case "run": + cmdRun(os.Args[2:]) case "help", "-h", "--help": printUsage() default: @@ -51,6 +53,7 @@ Usage: fn list [-k kind] [-d domain] [-l lang] fn show Muestra entrada completa fn add [-k kind] Abre $EDITOR con template + fn run [args...] Ejecuta funcion/pipeline (go/py/bash) fn ops Gestiona operations.db (fn ops help) fn proposal Gestiona proposals`) } diff --git a/cmd/fn/run.go b/cmd/fn/run.go new file mode 100644 index 00000000..6e6c4b4b --- /dev/null +++ b/cmd/fn/run.go @@ -0,0 +1,208 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "fn-registry/registry" +) + +func cmdRun(args []string) { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "usage: fn run [args...]") + os.Exit(1) + } + + idOrName := args[0] + passArgs := args[1:] + + registryRoot := root() + dbPath := filepath.Join(registryRoot, dbName) + + db, err := registry.Open(dbPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error: cannot open registry: %v\n", err) + os.Exit(1) + } + defer db.Close() + + fn, err := resolveFunction(db, idOrName) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if fn.FilePath == "" { + fmt.Fprintf(os.Stderr, "error: %s has no file_path in registry\n", fn.ID) + os.Exit(1) + } + + absPath := filepath.Join(registryRoot, fn.FilePath) + if _, err := os.Stat(absPath); os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "error: file not found: %s\n", absPath) + os.Exit(1) + } + + cmd, err := buildCommand(fn, registryRoot, absPath, passArgs) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + fmt.Fprintf(os.Stderr, "[fn run] %s (%s/%s) %s\n", fn.ID, fn.Lang, fn.Kind, strings.Join(passArgs, " ")) + + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func resolveFunction(db *registry.DB, idOrName string) (*registry.Function, error) { + fn, err := db.GetFunction(idOrName) + if err == nil { + return fn, nil + } + + fns, err := db.GetFunctionsByName(idOrName) + if err != nil { + return nil, fmt.Errorf("lookup failed: %w", err) + } + if len(fns) == 0 { + return nil, fmt.Errorf("function %q not found (tried as ID and name)", idOrName) + } + if len(fns) == 1 { + return &fns[0], nil + } + + var b strings.Builder + fmt.Fprintf(&b, "ambiguous name %q — found %d matches:\n", idOrName, len(fns)) + for _, f := range fns { + fmt.Fprintf(&b, " %s (%s/%s)\n", f.ID, f.Lang, f.Kind) + } + fmt.Fprintf(&b, "use the full ID to disambiguate") + return nil, fmt.Errorf("%s", b.String()) +} + +func buildCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) { + switch fn.Lang { + case "go": + return buildGoCommand(fn, registryRoot, absPath, args) + case "py": + return buildPyCommand(registryRoot, absPath, args) + case "bash": + return buildBashCommand(absPath, args) + case "ts": + return buildTsCommand(registryRoot, absPath, args) + default: + return nil, fmt.Errorf("unsupported lang %q for execution", fn.Lang) + } +} + +func buildGoCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) { + dir := filepath.Dir(absPath) + env := append(os.Environ(), "CGO_ENABLED=1") + + // If directory has main.go → go run . (pipelines and standalone executables) + mainGo := filepath.Join(dir, "main.go") + if _, err := os.Stat(mainGo); err == nil { + cmdArgs := append([]string{"run", "."}, args...) + cmd := exec.Command("go", cmdArgs...) + cmd.Dir = dir + cmd.Env = env + return cmd, nil + } + + // Library code: if it has tests → go test + if fn.Tested && fn.TestFilePath != "" { + testAbs := filepath.Join(registryRoot, fn.TestFilePath) + if _, err := os.Stat(testAbs); err == nil { + relPkg, _ := filepath.Rel(registryRoot, dir) + pkgPath := "./" + filepath.ToSlash(relPkg) + cmdArgs := append([]string{"test", "-v", "-count=1", "-tags", "fts5", pkgPath}, args...) + cmd := exec.Command("go", cmdArgs...) + cmd.Dir = registryRoot + cmd.Env = env + return cmd, nil + } + } + + // No tests: go vet (compilation check) + relPkg, _ := filepath.Rel(registryRoot, dir) + pkgPath := "./" + filepath.ToSlash(relPkg) + cmdArgs := []string{"vet", "-tags", "fts5", pkgPath} + cmd := exec.Command("go", cmdArgs...) + cmd.Dir = registryRoot + cmd.Env = env + fmt.Fprintf(os.Stderr, "[fn run] %s is library code without tests — running go vet\n", fn.ID) + return cmd, nil +} + +func buildPyCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) { + venvPython := filepath.Join(registryRoot, "python", ".venv", "bin", "python3") + pythonBin := "python3" + if _, err := os.Stat(venvPython); err == nil { + pythonBin = venvPython + } + + dir := filepath.Dir(absPath) + + // If the file is inside a package (has __init__.py), use python -m + // so relative imports work. PYTHONPATH points to python/functions/ or + // the equivalent parent that contains the domain packages. + initPy := filepath.Join(dir, "__init__.py") + if _, err := os.Stat(initPy); err == nil { + // The pythonPath is the well-known python/functions/ directory + // which contains domain packages (metabase/, etc.) + pythonPath := filepath.Join(registryRoot, "python", "functions") + if _, err := os.Stat(pythonPath); os.IsNotExist(err) { + // Fallback: walk up from dir to find the parent of the top package + pythonPath = filepath.Dir(dir) + } + + // Build module path: metabase/databases.py → metabase.databases + relToRoot, _ := filepath.Rel(pythonPath, absPath) + modPath := strings.TrimSuffix(relToRoot, ".py") + modPath = strings.ReplaceAll(filepath.ToSlash(modPath), "/", ".") + + cmdArgs := append([]string{"-m", modPath}, args...) + cmd := exec.Command(pythonBin, cmdArgs...) + cmd.Dir = pythonPath + cmd.Env = append(os.Environ(), "PYTHONPATH="+pythonPath) + return cmd, nil + } + + // Standalone script (no __init__.py) + cmdArgs := append([]string{absPath}, args...) + cmd := exec.Command(pythonBin, cmdArgs...) + cmd.Dir = dir + return cmd, nil +} + +func buildBashCommand(absPath string, args []string) (*exec.Cmd, error) { + cmdArgs := append([]string{absPath}, args...) + cmd := exec.Command("bash", cmdArgs...) + cmd.Dir = filepath.Dir(absPath) + return cmd, nil +} + +func buildTsCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) { + tsxBin := filepath.Join(registryRoot, "frontend", "node_modules", ".bin", "tsx") + if _, err := os.Stat(tsxBin); os.IsNotExist(err) { + return nil, fmt.Errorf("tsx not found — run: cd frontend && pnpm add -D tsx") + } + + cmdArgs := append([]string{absPath}, args...) + cmd := exec.Command(tsxBin, cmdArgs...) + cmd.Dir = filepath.Dir(absPath) + return cmd, nil +} diff --git a/frontend/package.json b/frontend/package.json index 0de59957..8c89cd00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "tailwindcss": "^4.2.2", + "tsx": "^4.21.0", "typescript": "^6.0.2", "vite": "^8.0.3" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index bdb9c5eb..133ea1d6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)) + version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -50,16 +50,19 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)) + version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^6.0.2 version: 6.0.2 vite: specifier: ^8.0.3 - version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) packages: @@ -236,6 +239,162 @@ packages: '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -872,6 +1031,11 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1011,6 +1175,9 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1539,6 +1706,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1729,6 +1899,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -2131,6 +2306,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -2383,12 +2636,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))': + '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) '@ts-morph/common@0.27.0': dependencies: @@ -2413,10 +2666,10 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))': + '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) accepts@2.0.0: dependencies: @@ -2643,6 +2896,35 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -2820,6 +3102,10 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3248,6 +3534,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -3501,6 +3789,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + tw-animate-css@1.4.0: {} type-fest@5.5.0: @@ -3539,7 +3834,7 @@ snapshots: vary@1.1.2: {} - vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1): + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3547,8 +3842,10 @@ snapshots: rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) tinyglobby: 0.2.15 optionalDependencies: + esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 + tsx: 4.21.0 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' diff --git a/types/core/error.go b/functions/core/error.go similarity index 100% rename from types/core/error.go rename to functions/core/error.go diff --git a/types/core/option.go b/functions/core/option.go similarity index 100% rename from types/core/option.go rename to functions/core/option.go diff --git a/types/core/pair.go b/functions/core/pair.go similarity index 100% rename from types/core/pair.go rename to functions/core/pair.go diff --git a/types/core/result.go b/functions/core/result.go similarity index 100% rename from types/core/result.go rename to functions/core/result.go diff --git a/types/core/result_test.go b/functions/core/result_test.go similarity index 100% rename from types/core/result_test.go rename to functions/core/result_test.go diff --git a/types/cybersecurity/cidr_block.go b/functions/cybersecurity/cidr_block.go similarity index 100% rename from types/cybersecurity/cidr_block.go rename to functions/cybersecurity/cidr_block.go diff --git a/types/cybersecurity/port_result.go b/functions/cybersecurity/port_result.go similarity index 100% rename from types/cybersecurity/port_result.go rename to functions/cybersecurity/port_result.go diff --git a/types/cybersecurity/threat_result.go b/functions/cybersecurity/threat_result.go similarity index 100% rename from types/cybersecurity/threat_result.go rename to functions/cybersecurity/threat_result.go diff --git a/types/datascience/outlier_result.go b/functions/datascience/outlier_result.go similarity index 100% rename from types/datascience/outlier_result.go rename to functions/datascience/outlier_result.go diff --git a/types/finance/bollinger_result.go b/functions/finance/bollinger_result.go similarity index 100% rename from types/finance/bollinger_result.go rename to functions/finance/bollinger_result.go diff --git a/types/finance/drawdown_result.go b/functions/finance/drawdown_result.go similarity index 100% rename from types/finance/drawdown_result.go rename to functions/finance/drawdown_result.go diff --git a/types/finance/ohlcv.go b/functions/finance/ohlcv.go similarity index 100% rename from types/finance/ohlcv.go rename to functions/finance/ohlcv.go diff --git a/types/finance/tick.go b/functions/finance/tick.go similarity index 100% rename from types/finance/tick.go rename to functions/finance/tick.go diff --git a/types/infra/container_info.go b/functions/infra/container_info.go similarity index 100% rename from types/infra/container_info.go rename to functions/infra/container_info.go diff --git a/types/infra/image_info.go b/functions/infra/image_info.go similarity index 100% rename from types/infra/image_info.go rename to functions/infra/image_info.go diff --git a/types/infra/metabase_client.go b/functions/infra/metabase_client.go similarity index 100% rename from types/infra/metabase_client.go rename to functions/infra/metabase_client.go diff --git a/functions/infra/types.go b/functions/infra/types.go deleted file mode 100644 index 9d515777..00000000 --- a/functions/infra/types.go +++ /dev/null @@ -1,28 +0,0 @@ -package infra - -// ContainerInfo representa la información básica de un contenedor Docker. -type ContainerInfo struct { - ID string - Name string - Image string - Status string - State string - Ports string - Created string - Labels map[string]string -} - -// ImageInfo representa la información básica de una imagen Docker local. -type ImageInfo struct { - ID string - Repository string - Tag string - 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 -} diff --git a/functions/pipelines/init_metabase.md b/functions/pipelines/init_metabase.md index a7cbc232..54f2a14f 100644 --- a/functions/pipelines/init_metabase.md +++ b/functions/pipelines/init_metabase.md @@ -30,13 +30,17 @@ file_path: "functions/pipelines/init_metabase/main.go" ## Ejemplo ```bash +# Básico go run functions/pipelines/init_metabase/main.go \ --project analytics \ --metabase-port 3000 \ - --pg-port 5432 \ --pg-user metabase \ - --pg-password metabase \ - --pg-database metabase + --pg-password metabase + +# Con volume para registry.db (detecta cambios en vivo) +go run functions/pipelines/init_metabase/main.go \ + --project fn_registry \ + --mb-volumes "/home/lucas/fn_registry/registry.db:/data/registry.db" ``` Salida JSON: @@ -60,7 +64,8 @@ El pipeline orquesta 5 pasos secuenciales: 2. **Pull** — descarga `postgres:16` y `metabase/metabase:latest` 3. **Postgres** — inicia con volume persistente (named volume por defecto o bind mount con `--pg-volume`) 4. **Health check** — retry exponencial (hasta ~34 min) con `pg_isready` dentro del contenedor -5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable +5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable. Con `--mb-volumes` monta volumes adicionales (ej: registry.db para SQLite) +6. **Permisos** — ajusta ownership de directorios montados para el usuario `metabase` (UID 2000) dentro del contenedor Reutiliza conceptualmente `docker_create_network`, `docker_pull_image`, `docker_run_container`, `docker_inspect_container` y `retry_with_backoff`, reimplementadas inline por ser un ejecutable independiente. diff --git a/functions/pipelines/init_metabase/main.go b/functions/pipelines/init_metabase/main.go index e061cc26..ba40b535 100644 --- a/functions/pipelines/init_metabase/main.go +++ b/functions/pipelines/init_metabase/main.go @@ -28,7 +28,8 @@ func main() { pgUser := flag.String("pg-user", "metabase", "Usuario Postgres") pgPass := flag.String("pg-password", "metabase", "Password Postgres") pgDB := flag.String("pg-database", "metabase", "Base de datos Postgres") - pgVolume := flag.String("pg-volume", "", "Path host para persistencia (default: docker named volume)") + pgVolume := flag.String("pg-volume", "", "Path host para persistencia Postgres (default: docker named volume)") + mbVolumes := flag.String("mb-volumes", "", "Volumes adicionales para Metabase, separados por coma (ej: /host/path:/container/path,/otro:/dest)") flag.Parse() if *project == "" { @@ -37,7 +38,12 @@ func main() { os.Exit(1) } - result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume) + var extraVolumes []string + if *mbVolumes != "" { + extraVolumes = strings.Split(*mbVolumes, ",") + } + + result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume, extraVolumes) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) @@ -48,7 +54,7 @@ func main() { enc.Encode(result) } -func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string) (*MetabaseResult, error) { +func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string, mbExtraVolumes []string) (*MetabaseResult, error) { networkName := project + "-net" pgName := project + "-postgres" mbName := project + "-metabase" @@ -118,13 +124,36 @@ func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string "-e", "MB_DB_USER=" + pgUser, "-e", "MB_DB_PASS=" + pgPass, "-e", "MB_DB_HOST=" + pgName, - "metabase/metabase:latest", } + for _, v := range mbExtraVolumes { + mbArgs = append(mbArgs, "-v", strings.TrimSpace(v)) + } + mbArgs = append(mbArgs, "metabase/metabase:latest") mbID, err := dockerCmd(mbArgs...) if err != nil { return nil, fmt.Errorf("starting metabase: %w", err) } + // 6. Fix permisos para volumes SQLite — Metabase corre como UID 2000 + for _, v := range mbExtraVolumes { + parts := strings.SplitN(strings.TrimSpace(v), ":", 2) + if len(parts) < 2 { + continue + } + destPath := parts[1] + // Si es un archivo, fix el directorio padre; si es directorio, fix directo + dir := destPath + if strings.Contains(destPath, ".") { + // Probablemente un archivo, usar dirname + idx := strings.LastIndex(destPath, "/") + if idx > 0 { + dir = destPath[:idx] + } + } + fmt.Fprintf(os.Stderr, " Fijando permisos de %s para usuario metabase...\n", dir) + dockerCmd("exec", "-u", "root", mbName, "chown", "metabase:metabase", dir) + } + mbURL := fmt.Sprintf("http://localhost:%s", mbPort) fmt.Fprintf(os.Stderr, "\nStack listo. Metabase disponible en %s\n", mbURL) diff --git a/types/shell/cmd_result.go b/functions/shell/cmd_result.go similarity index 100% rename from types/shell/cmd_result.go rename to functions/shell/cmd_result.go diff --git a/functions/shell/run_cmd.go b/functions/shell/run_cmd.go index a2b31c25..ebcc0d31 100644 --- a/functions/shell/run_cmd.go +++ b/functions/shell/run_cmd.go @@ -1,9 +1,11 @@ package shell +import "fmt" + // Implementation: github.com/lucasdataproyects/devfactory/shell // Run ejecuta un comando del sistema con timeout de 30 segundos y devuelve el resultado. -func Run(name string, args ...string) Result[CmdResult] { +func Run(name string, args ...string) (CmdResult, error) { // stub — implementation in devfactory/shell - return Result[CmdResult]{} + return CmdResult{}, fmt.Errorf("not implemented") } diff --git a/functions/shell/run_cmd_timeout.go b/functions/shell/run_cmd_timeout.go index 557037f1..3124940e 100644 --- a/functions/shell/run_cmd_timeout.go +++ b/functions/shell/run_cmd_timeout.go @@ -1,11 +1,14 @@ package shell -import "time" +import ( + "fmt" + "time" +) // Implementation: github.com/lucasdataproyects/devfactory/shell // RunWithTimeout ejecuta un comando del sistema con timeout configurable. -func RunWithTimeout(name string, timeout time.Duration, args ...string) Result[CmdResult] { +func RunWithTimeout(name string, timeout time.Duration, args ...string) (CmdResult, error) { // stub — implementation in devfactory/shell - return Result[CmdResult]{} + return CmdResult{}, fmt.Errorf("not implemented") } diff --git a/functions/shell/run_pipe.go b/functions/shell/run_pipe.go index 947b3be0..764a92a0 100644 --- a/functions/shell/run_pipe.go +++ b/functions/shell/run_pipe.go @@ -1,9 +1,11 @@ package shell +import "fmt" + // Implementation: github.com/lucasdataproyects/devfactory/shell // RunPipe encadena multiples comandos con pipe y devuelve el resultado final. -func RunPipe(commands ...string) Result[CmdResult] { +func RunPipe(commands ...string) (CmdResult, error) { // stub — implementation in devfactory/shell - return Result[CmdResult]{} + return CmdResult{}, fmt.Errorf("not implemented") } diff --git a/functions/shell/run_shell.go b/functions/shell/run_shell.go index 5899ce53..eeaf9659 100644 --- a/functions/shell/run_shell.go +++ b/functions/shell/run_shell.go @@ -1,9 +1,11 @@ package shell +import "fmt" + // Implementation: github.com/lucasdataproyects/devfactory/shell // RunShell ejecuta un comando shell interpretado por /bin/sh. -func RunShell(command string) Result[CmdResult] { +func RunShell(command string) (CmdResult, error) { // stub — implementation in devfactory/shell - return Result[CmdResult]{} + return CmdResult{}, fmt.Errorf("not implemented") } diff --git a/functions/shell/run_shell_timeout.go b/functions/shell/run_shell_timeout.go index 90c0cca4..7d4d9a70 100644 --- a/functions/shell/run_shell_timeout.go +++ b/functions/shell/run_shell_timeout.go @@ -1,11 +1,14 @@ package shell -import "time" +import ( + "fmt" + "time" +) // Implementation: github.com/lucasdataproyects/devfactory/shell // RunShellTimeout ejecuta un comando shell con timeout configurable. -func RunShellTimeout(command string, timeout time.Duration) Result[CmdResult] { +func RunShellTimeout(command string, timeout time.Duration) (CmdResult, error) { // stub — implementation in devfactory/shell - return Result[CmdResult]{} + return CmdResult{}, fmt.Errorf("not implemented") } diff --git a/functions/shell/which.go b/functions/shell/which.go index 3dcda7bc..2aa5ad7f 100644 --- a/functions/shell/which.go +++ b/functions/shell/which.go @@ -2,8 +2,8 @@ package shell // Implementation: github.com/lucasdataproyects/devfactory/shell -// Which busca la ruta de un ejecutable en el PATH del sistema. Devuelve None si no existe. -func Which(name string) Option[string] { +// Which busca la ruta de un ejecutable en el PATH del sistema. Devuelve ("", false) si no existe. +func Which(name string) (string, bool) { // stub — implementation in devfactory/shell - return Option[string]{} + return "", false } diff --git a/types/tui/base_model.go b/functions/tui/base_model.go similarity index 100% rename from types/tui/base_model.go rename to functions/tui/base_model.go diff --git a/types/tui/confirm_model.go b/functions/tui/confirm_model.go similarity index 100% rename from types/tui/confirm_model.go rename to functions/tui/confirm_model.go diff --git a/functions/tui/confirm_prompt.go b/functions/tui/confirm_prompt.go index 0c6ba041..d0a2cd87 100644 --- a/functions/tui/confirm_prompt.go +++ b/functions/tui/confirm_prompt.go @@ -1,9 +1,9 @@ package tui -// Implementation: github.com/lucasdataproyects/devfactory/tui +import "fmt" -// Confirm muestra un dialogo de confirmacion Si/No en terminal y devuelve la eleccion del usuario. -func Confirm(prompt string) Result[bool] { +// Confirm muestra un prompt de confirmacion si/no en la terminal. +func Confirm(prompt string) (bool, error) { // stub — implementation in devfactory/tui - return Result[bool]{Value: false} + return false, fmt.Errorf("not implemented") } diff --git a/types/tui/filtered_list_model.go b/functions/tui/filtered_list_model.go similarity index 100% rename from types/tui/filtered_list_model.go rename to functions/tui/filtered_list_model.go diff --git a/types/tui/list_item.go b/functions/tui/list_item.go similarity index 100% rename from types/tui/list_item.go rename to functions/tui/list_item.go diff --git a/types/tui/list_model.go b/functions/tui/list_model.go similarity index 100% rename from types/tui/list_model.go rename to functions/tui/list_model.go diff --git a/types/tui/multi_progress_model.go b/functions/tui/multi_progress_model.go similarity index 100% rename from types/tui/multi_progress_model.go rename to functions/tui/multi_progress_model.go diff --git a/types/tui/progress_model.go b/functions/tui/progress_model.go similarity index 100% rename from types/tui/progress_model.go rename to functions/tui/progress_model.go diff --git a/functions/tui/run_fullscreen.go b/functions/tui/run_fullscreen.go index 386634a7..0b3ed5ad 100644 --- a/functions/tui/run_fullscreen.go +++ b/functions/tui/run_fullscreen.go @@ -1,12 +1,10 @@ package tui -// Implementation: github.com/lucasdataproyects/devfactory/tui +import tea "github.com/charmbracelet/bubbletea" -import "github.com/charmbracelet/bubbletea" - -// RunFullscreen ejecuta un modelo Bubble Tea en modo fullscreen. -func RunFullscreen[T tea.Model](model T) Result[T] { +// RunFullscreen ejecuta un modelo Bubbletea en modo fullscreen. +func RunFullscreen[T tea.Model](model T) (T, error) { // stub — implementation in devfactory/tui var zero T - return Result[T]{Value: zero} + return zero, nil } diff --git a/functions/tui/run_model.go b/functions/tui/run_model.go index fefe151e..b6361f36 100644 --- a/functions/tui/run_model.go +++ b/functions/tui/run_model.go @@ -1,12 +1,10 @@ package tui -// Implementation: github.com/lucasdataproyects/devfactory/tui +import tea "github.com/charmbracelet/bubbletea" -import "github.com/charmbracelet/bubbletea" - -// RunModel ejecuta un modelo Bubble Tea y devuelve el modelo final o error. -func RunModel[T tea.Model](model T) Result[T] { +// RunModel ejecuta un modelo Bubbletea y devuelve el modelo final. +func RunModel[T tea.Model](model T) (T, error) { // stub — implementation in devfactory/tui var zero T - return Result[T]{Value: zero} + return zero, nil } diff --git a/functions/tui/run_with_mouse.go b/functions/tui/run_with_mouse.go index 238e2134..611d1906 100644 --- a/functions/tui/run_with_mouse.go +++ b/functions/tui/run_with_mouse.go @@ -1,12 +1,10 @@ package tui -// Implementation: github.com/lucasdataproyects/devfactory/tui +import tea "github.com/charmbracelet/bubbletea" -import "github.com/charmbracelet/bubbletea" - -// RunWithMouseSupport ejecuta un modelo Bubble Tea con soporte de raton habilitado. -func RunWithMouseSupport[T tea.Model](model T) Result[T] { +// RunWithMouseSupport ejecuta un modelo Bubbletea con soporte de mouse. +func RunWithMouseSupport[T tea.Model](model T) (T, error) { // stub — implementation in devfactory/tui var zero T - return Result[T]{Value: zero} + return zero, nil } diff --git a/types/tui/spinner_model.go b/functions/tui/spinner_model.go similarity index 100% rename from types/tui/spinner_model.go rename to functions/tui/spinner_model.go diff --git a/types/tui/spinner_with_timeout_model.go b/functions/tui/spinner_with_timeout_model.go similarity index 100% rename from types/tui/spinner_with_timeout_model.go rename to functions/tui/spinner_with_timeout_model.go diff --git a/types/tui/styles.go b/functions/tui/styles.go similarity index 100% rename from types/tui/styles.go rename to functions/tui/styles.go diff --git a/types/tui/theme.go b/functions/tui/theme.go similarity index 100% rename from types/tui/theme.go rename to functions/tui/theme.go diff --git a/go.mod b/go.mod index e15c4702..8907d0dc 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,35 @@ module fn-registry -go 1.22.2 +go 1.24.2 require ( github.com/mattn/go-sqlite3 v1.14.37 gopkg.in/yaml.v3 v3.0.1 ) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.9.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index 710b3523..39c6fd10 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,55 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= +github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= +github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/python/functions/metabase/__init__.py b/python/functions/metabase/__init__.py index bccd53c3..a7744901 100644 --- a/python/functions/metabase/__init__.py +++ b/python/functions/metabase/__init__.py @@ -2,10 +2,14 @@ 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 +from .databases import metabase_list_databases, metabase_add_database, metabase_get_database +from .setup import metabase_setup __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", + "metabase_list_databases", "metabase_add_database", "metabase_get_database", + "metabase_setup", ] diff --git a/python/functions/metabase/databases.py b/python/functions/metabase/databases.py new file mode 100644 index 00000000..2bb1cb0f --- /dev/null +++ b/python/functions/metabase/databases.py @@ -0,0 +1,105 @@ +"""CRUD de databases de Metabase.""" + +from .client import MetabaseClient + + +def metabase_list_databases( + client: MetabaseClient, + include_tables: bool = False, +) -> list: + """Lista las databases configuradas en Metabase. + + Endpoint: GET /api/database. + + Args: + client: Cliente autenticado. + include_tables: Si True, incluye las tablas de cada database en la respuesta. + + Returns: + Lista de dicts, cada uno con campos de la database: + - id: ID numerico de la database + - name: Nombre dado en Metabase + - engine: Motor de BD (sqlite, postgres, mysql, etc.) + - details: Dict con parametros de conexion + - is_full_sync: Si sincroniza el schema automaticamente + - tables: Lista de tablas (solo si include_tables=True) + + Example: + >>> dbs = metabase_list_databases(client) + >>> for db in dbs: + ... print(db["id"], db["name"], db["engine"]) + + >>> dbs = metabase_list_databases(client, include_tables=True) + >>> for db in dbs: + ... print(db["name"], [t["name"] for t in db.get("tables", [])]) + """ + params = {} + if include_tables: + params["include"] = "tables" + result = client.request("GET", "/api/database", params=params) + if isinstance(result, dict) and "data" in result: + return result["data"] + return result + + +def metabase_add_database( + client: MetabaseClient, + name: str, + engine: str, + details: dict, +) -> dict: + """Agrega una nueva database a Metabase. + + Endpoint: POST /api/database. Requiere permisos de superusuario. + + Args: + client: Cliente autenticado con permisos admin. + name: Nombre descriptivo para la database en Metabase. + engine: Motor de base de datos. Ejemplos: "sqlite", "postgres", + "mysql", "h2", "mongo", "bigquery". + details: Dict con parametros de conexion especificos del engine. + Para SQLite: {"db": "/ruta/al/archivo.db"} + Para Postgres: {"host": "...", "port": 5432, "dbname": "...", + "user": "...", "password": "..."} + + Returns: + Dict con la database creada, incluyendo el campo "id" asignado + por Metabase y todos los campos de configuracion. + + Raises: + httpx.HTTPStatusError: 400 si los datos de conexion son invalidos. + + Example: + >>> db = metabase_add_database(client, "Mi SQLite", "sqlite", {"db": "/data/ops.db"}) + >>> print(db["id"], db["name"]) + """ + body = { + "name": name, + "engine": engine, + "details": details, + } + return client.request("POST", "/api/database", json=body) + + +def metabase_get_database(client: MetabaseClient, database_id: int) -> dict: + """Obtiene los detalles de una database de Metabase por su ID. + + Endpoint: GET /api/database/:id. + + Args: + client: Cliente autenticado. + database_id: ID numerico de la database. + + Returns: + Dict con campos de la database: id, name, engine, details, + is_full_sync, is_on_demand, auto_run_queries, created_at, + updated_at, features, etc. + + Raises: + httpx.HTTPStatusError: 404 si la database no existe. + + Example: + >>> db = metabase_get_database(client, 2) + >>> print(db["name"], db["engine"]) + """ + return client.request("GET", f"/api/database/{database_id}") diff --git a/python/functions/metabase/metabase_add_database.md b/python/functions/metabase/metabase_add_database.md new file mode 100644 index 00000000..4c5e77be --- /dev/null +++ b/python/functions/metabase/metabase_add_database.md @@ -0,0 +1,44 @@ +--- +name: metabase_add_database +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def metabase_add_database(client: MetabaseClient, name: str, engine: str, details: dict) -> dict" +description: "Agrega una nueva database a Metabase via POST /api/database. Soporta cualquier engine (sqlite, postgres, mysql, etc.)." +tags: [metabase, database, add, create, api, python] +uses_functions: [] +uses_types: [MetabaseClient_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [httpx] +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/databases.py" +--- + +## Ejemplo + +```python +# SQLite +db = metabase_add_database(client, "Ops DB", "sqlite", {"db": "/data/operations.db"}) +print(db["id"]) + +# Postgres +db = metabase_add_database(client, "Prod PG", "postgres", { + "host": "localhost", + "port": 5432, + "dbname": "myapp", + "user": "reader", + "password": "secret", +}) +``` + +## Notas + +Requiere permisos de superusuario. El campo `details` depende del engine: +para SQLite solo necesita `{"db": "/ruta/archivo.db"}`. +Retorna la database creada con su `id` asignado por Metabase. diff --git a/python/functions/metabase/metabase_get_database.md b/python/functions/metabase/metabase_get_database.md new file mode 100644 index 00000000..0048a0aa --- /dev/null +++ b/python/functions/metabase/metabase_get_database.md @@ -0,0 +1,33 @@ +--- +name: metabase_get_database +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def metabase_get_database(client: MetabaseClient, database_id: int) -> dict" +description: "Obtiene los detalles de una database de Metabase por su ID. Endpoint: GET /api/database/:id." +tags: [metabase, database, get, api, python] +uses_functions: [] +uses_types: [MetabaseClient_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [httpx] +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/databases.py" +--- + +## Ejemplo + +```python +db = metabase_get_database(client, 2) +print(db["name"], db["engine"]) +``` + +## Notas + +Error 404 si la database no existe. Retorna campos completos incluyendo +id, name, engine, details, is_full_sync, auto_run_queries, created_at, features. diff --git a/python/functions/metabase/metabase_list_databases.md b/python/functions/metabase/metabase_list_databases.md new file mode 100644 index 00000000..fccd3ce4 --- /dev/null +++ b/python/functions/metabase/metabase_list_databases.md @@ -0,0 +1,39 @@ +--- +name: metabase_list_databases +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "def metabase_list_databases(client: MetabaseClient, include_tables: bool = False) -> list" +description: "Lista las databases configuradas en Metabase. Endpoint: GET /api/database. Soporta incluir tablas con include_tables=True." +tags: [metabase, database, list, api, python] +uses_functions: [] +uses_types: [MetabaseClient_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [httpx] +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/databases.py" +--- + +## Ejemplo + +```python +dbs = metabase_list_databases(client) +for db in dbs: + print(db["id"], db["name"], db["engine"]) + +# Con tablas incluidas +dbs = metabase_list_databases(client, include_tables=True) +for db in dbs: + print(db["name"], [t["name"] for t in db.get("tables", [])]) +``` + +## Notas + +Si include_tables=True agrega el query param `?include=tables` al request. +Retorna lista directa (no paginada) de todas las databases accesibles. diff --git a/python/functions/metabase/metabase_setup.md b/python/functions/metabase/metabase_setup.md new file mode 100644 index 00000000..33521160 --- /dev/null +++ b/python/functions/metabase/metabase_setup.md @@ -0,0 +1,44 @@ +--- +name: metabase_setup +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "metabase_setup(base_url: str, admin_email: str, admin_password: str, admin_first_name: str, admin_last_name: str, site_name: str, site_locale: str) -> dict" +description: "Ejecuta el setup inicial de una instancia Metabase nueva via POST /api/setup. Obtiene el setup-token automaticamente y crea el usuario admin con preferencias del sitio." +tags: [metabase, setup, api, infra] +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/setup.py" +--- + +## Ejemplo + +```python +from metabase.setup import metabase_setup + +result = metabase_setup( + base_url="http://localhost:3000", + admin_email="admin@fnregistry.local", + admin_password="FnRegistry2024!", + admin_first_name="Lucas", + admin_last_name="Admin", + site_name="fn-registry", + site_locale="es", +) +print(result["id"]) # session token +``` + +## Notas + +Solo funciona en instancias sin configurar (setup-token disponible). Si Metabase ya tiene un usuario, lanza RuntimeError. + +El setup-token se obtiene automaticamente de GET /api/session/properties. Una vez usado, Metabase invalida el token. diff --git a/python/functions/metabase/setup.py b/python/functions/metabase/setup.py new file mode 100644 index 00000000..c8eaa4b5 --- /dev/null +++ b/python/functions/metabase/setup.py @@ -0,0 +1,83 @@ +"""Setup inicial de Metabase via API.""" + +import httpx + + +def metabase_setup( + base_url: str, + admin_email: str, + admin_password: str, + admin_first_name: str = "Admin", + admin_last_name: str = "User", + site_name: str = "Metabase", + site_locale: str = "en", +) -> dict: + """Ejecuta el setup inicial de una instancia Metabase nueva. + + Usa el setup-token de una instancia sin configurar para crear el + usuario admin y configurar preferencias del sitio. Solo funciona + una vez — si Metabase ya tiene un usuario, retorna error. + + Endpoint: POST /api/setup (requiere setup-token valido). + + Args: + base_url: URL base de la instancia (ej: "http://localhost:3000"). + admin_email: Email del usuario admin a crear. + admin_password: Password del admin (min 8 chars con complejidad). + admin_first_name: Nombre del admin. + admin_last_name: Apellido del admin. + site_name: Nombre del sitio Metabase. + site_locale: Locale del sitio (ej: "es", "en"). + + Returns: + Dict con el session id del admin recien creado. + + Raises: + httpx.HTTPStatusError: 400 si el setup-token es invalido o + Metabase ya fue configurado. + RuntimeError: Si no se puede obtener el setup-token. + + Example: + >>> result = metabase_setup( + ... "http://localhost:3000", + ... "admin@example.com", + ... "SecurePass123!", + ... site_name="fn-registry", + ... site_locale="es", + ... ) + >>> print(result["id"]) # session token + """ + url = base_url.rstrip("/") + + # Obtener setup-token + resp = httpx.get(f"{url}/api/session/properties") + resp.raise_for_status() + props = resp.json() + setup_token = props.get("setup-token") + if not setup_token: + raise RuntimeError( + "No setup-token disponible. Metabase ya fue configurado " + "o has-user-setup es True." + ) + + # Ejecutar setup + resp = httpx.post( + f"{url}/api/setup", + json={ + "token": setup_token, + "user": { + "first_name": admin_first_name, + "last_name": admin_last_name, + "email": admin_email, + "password": admin_password, + }, + "prefs": { + "site_name": site_name, + "site_locale": site_locale, + "allow_tracking": False, + }, + }, + timeout=30.0, + ) + resp.raise_for_status() + return resp.json() diff --git a/registry/store.go b/registry/store.go index dd063fc2..8d24a8ee 100644 --- a/registry/store.go +++ b/registry/store.go @@ -239,6 +239,16 @@ func (db *DB) GetType(id string) (*Type, error) { return &ts[0], nil } +// GetFunctionsByName returns all functions matching a given name (across langs/domains). +func (db *DB) GetFunctionsByName(name string) ([]Function, error) { + rows, err := db.conn.Query("SELECT * FROM functions WHERE name = ? ORDER BY lang, domain", name) + if err != nil { + return nil, err + } + defer rows.Close() + return scanFunctions(rows) +} + // DeleteFunction removes a function by ID. func (db *DB) DeleteFunction(id string) error { _, err := db.conn.Exec("DELETE FROM functions WHERE id = ?", id) diff --git a/types/core/error.md b/types/core/error.md index 52de7883..f90737f1 100644 --- a/types/core/error.md +++ b/types/core/error.md @@ -11,5 +11,5 @@ definition: | description: "Tipo de error base del registry. Referenciado como error_type por funciones impuras." tags: [error, base, impure] uses_types: [] -file_path: "types/core/error.go" +file_path: "functions/core/error.go" --- diff --git a/types/core/option.md b/types/core/option.md index 98395988..9e1adcd3 100644 --- a/types/core/option.md +++ b/types/core/option.md @@ -11,7 +11,7 @@ definition: | description: "Tipo suma generico que representa un valor opcional: Some(T) o None. Alternativa a punteros nil para modelar ausencia de valor de forma explicita." tags: [option, sum, nullable, functional, generic] uses_types: [] -file_path: "types/core/option.go" +file_path: "functions/core/option.go" --- ## Notas diff --git a/types/core/pair.md b/types/core/pair.md index d94d2742..d907c0a8 100644 --- a/types/core/pair.md +++ b/types/core/pair.md @@ -12,5 +12,5 @@ definition: | description: "Tipo producto generico que agrupa dos valores de tipos potencialmente distintos. Util para ZipSlices y operaciones que devuelven dos resultados." tags: [pair, tuple, product, generic] uses_types: [] -file_path: "types/core/pair.go" +file_path: "functions/core/pair.go" --- diff --git a/types/core/result.md b/types/core/result.md index b3b455db..dd11f60a 100644 --- a/types/core/result.md +++ b/types/core/result.md @@ -12,7 +12,7 @@ definition: | description: "Tipo suma generico que representa exito (Ok) o fallo (Err). Permite componer operaciones que pueden fallar sin recurrir a multiples returns (T, error)." tags: [result, sum, error-handling, functional, generic] uses_types: [] -file_path: "types/core/result.go" +file_path: "functions/core/result.go" --- ## Notas diff --git a/types/cybersecurity/cidr_block.md b/types/cybersecurity/cidr_block.md index 2758028c..a8c0a95d 100644 --- a/types/cybersecurity/cidr_block.md +++ b/types/cybersecurity/cidr_block.md @@ -13,5 +13,5 @@ definition: | description: "Rango de red CIDR parseado con network, broadcast y numero de hosts." tags: [cybersecurity, network, cidr, ip] uses_types: [] -file_path: "types/cybersecurity/cidr_block.go" +file_path: "functions/cybersecurity/cidr_block.go" --- diff --git a/types/cybersecurity/port_result.md b/types/cybersecurity/port_result.md index 2b5de870..1a99941e 100644 --- a/types/cybersecurity/port_result.md +++ b/types/cybersecurity/port_result.md @@ -12,5 +12,5 @@ definition: | description: "Tipo suma para resultados de escaneo TCP: Open (con banner), Closed o Filtered." tags: [cybersecurity, port, scan, network] uses_types: [] -file_path: "types/cybersecurity/port_result.go" +file_path: "functions/cybersecurity/port_result.go" --- diff --git a/types/cybersecurity/threat_result.md b/types/cybersecurity/threat_result.md index 31dc0545..ed051b26 100644 --- a/types/cybersecurity/threat_result.md +++ b/types/cybersecurity/threat_result.md @@ -12,5 +12,5 @@ definition: | description: "Tipo suma para resultados de deteccion de amenazas: Clean, Suspicious o Malicious." tags: [cybersecurity, threat, detection, sqli] uses_types: [] -file_path: "types/cybersecurity/threat_result.go" +file_path: "functions/cybersecurity/threat_result.go" --- diff --git a/types/datascience/outlier_result.md b/types/datascience/outlier_result.md index e414449e..f9877ff2 100644 --- a/types/datascience/outlier_result.md +++ b/types/datascience/outlier_result.md @@ -11,5 +11,5 @@ definition: | description: "Tipo suma que clasifica un dato como Normal o Outlier con su z-score. Usado por DetectOutliers." tags: [datascience, outlier, anomaly, statistics] uses_types: [] -file_path: "types/datascience/outlier_result.go" +file_path: "functions/datascience/outlier_result.go" --- diff --git a/types/finance/bollinger_result.md b/types/finance/bollinger_result.md index 0ed05b3b..e182c692 100644 --- a/types/finance/bollinger_result.md +++ b/types/finance/bollinger_result.md @@ -13,5 +13,5 @@ definition: | description: "Resultado de Bollinger Bands con bandas superior, media e inferior." tags: [finance, bollinger, indicator, bands] uses_types: [] -file_path: "types/finance/bollinger_result.go" +file_path: "functions/finance/bollinger_result.go" --- diff --git a/types/finance/drawdown_result.md b/types/finance/drawdown_result.md index 45a2e5b4..a5d5a929 100644 --- a/types/finance/drawdown_result.md +++ b/types/finance/drawdown_result.md @@ -13,5 +13,5 @@ definition: | description: "Resultado de maximo drawdown con el valor de caida y los indices de inicio y fin." tags: [finance, drawdown, risk, metric] uses_types: [] -file_path: "types/finance/drawdown_result.go" +file_path: "functions/finance/drawdown_result.go" --- diff --git a/types/finance/ohlcv.md b/types/finance/ohlcv.md index 6c43b693..8a85d626 100644 --- a/types/finance/ohlcv.md +++ b/types/finance/ohlcv.md @@ -15,5 +15,5 @@ definition: | description: "Vela de mercado con precios de apertura, maximo, minimo, cierre y volumen." tags: [finance, market, candle, ohlcv] uses_types: [] -file_path: "types/finance/ohlcv.go" +file_path: "functions/finance/ohlcv.go" --- diff --git a/types/finance/tick.md b/types/finance/tick.md index f2413ab7..2ea7fb9f 100644 --- a/types/finance/tick.md +++ b/types/finance/tick.md @@ -14,5 +14,5 @@ definition: | description: "Evento de trade individual en un mercado. Contiene simbolo, precio, volumen y timestamp." tags: [finance, market, tick, trade] uses_types: [] -file_path: "types/finance/tick.go" +file_path: "functions/finance/tick.go" --- diff --git a/types/infra/container_info.md b/types/infra/container_info.md index 630f26a5..3430427d 100644 --- a/types/infra/container_info.md +++ b/types/infra/container_info.md @@ -18,7 +18,7 @@ definition: | description: "Información básica de un contenedor Docker: ID, nombre, imagen, estado, puertos, labels." tags: [docker, container, infra] uses_types: [] -file_path: "types/infra/container_info.go" +file_path: "functions/infra/container_info.go" --- ## Ejemplo diff --git a/types/infra/image_info.md b/types/infra/image_info.md index 7ba2a5d0..c7c05994 100644 --- a/types/infra/image_info.md +++ b/types/infra/image_info.md @@ -15,7 +15,7 @@ definition: | description: "Información básica de una imagen Docker local: ID, repositorio, tag, tamaño, fecha." tags: [docker, image, infra] uses_types: [] -file_path: "types/infra/image_info.go" +file_path: "functions/infra/image_info.go" --- ## Ejemplo diff --git a/types/infra/metabase_client.md b/types/infra/metabase_client.md index be7b1765..c6a8dd5f 100644 --- a/types/infra/metabase_client.md +++ b/types/infra/metabase_client.md @@ -12,7 +12,7 @@ definition: | 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" +file_path: "functions/infra/metabase_client.go" --- ## Notas diff --git a/types/shell/cmd_result.md b/types/shell/cmd_result.md index 2eb1cc8d..36234b37 100644 --- a/types/shell/cmd_result.md +++ b/types/shell/cmd_result.md @@ -13,7 +13,7 @@ definition: | description: "Resultado de la ejecucion de un comando del sistema con stdout, stderr y codigo de salida." tags: [shell, command, process, result] uses_types: [] -file_path: "types/shell/cmd_result.go" +file_path: "functions/shell/cmd_result.go" --- ## Notas diff --git a/types/tui/base_model.md b/types/tui/base_model.md index d6d099d7..642bbe31 100644 --- a/types/tui/base_model.md +++ b/types/tui/base_model.md @@ -9,5 +9,5 @@ definition: | description: "Modelo base que provee dimensiones de terminal, estilos y manejo de errores comunes a todas las vistas TUI." tags: [tui, base, model, component] uses_types: [styles_go_tui] -file_path: "types/tui/base_model.go" +file_path: "functions/tui/base_model.go" --- diff --git a/types/tui/confirm_model.md b/types/tui/confirm_model.md index 7678561a..2be7819c 100644 --- a/types/tui/confirm_model.md +++ b/types/tui/confirm_model.md @@ -9,5 +9,5 @@ definition: | description: "Dialogo de confirmacion Si/No interactivo. Embeds BaseModel. Implementa tea.Model." tags: [tui, confirm, dialog, component, interactive] uses_types: [base_model_go_tui] -file_path: "types/tui/confirm_model.go" +file_path: "functions/tui/confirm_model.go" --- diff --git a/types/tui/filtered_list_model.md b/types/tui/filtered_list_model.md index 4736eccf..15eee982 100644 --- a/types/tui/filtered_list_model.md +++ b/types/tui/filtered_list_model.md @@ -9,5 +9,5 @@ definition: | description: "Lista con filtrado por texto en tiempo real. Embeds ListModel y añade busqueda interactiva." tags: [tui, list, filter, component, interactive] uses_types: [list_model_go_tui, list_item_go_tui] -file_path: "types/tui/filtered_list_model.go" +file_path: "functions/tui/filtered_list_model.go" --- diff --git a/types/tui/list_item.md b/types/tui/list_item.md index abba26ff..fb33e6bf 100644 --- a/types/tui/list_item.md +++ b/types/tui/list_item.md @@ -9,5 +9,5 @@ definition: | description: "Item individual de una lista TUI con titulo, descripcion y valor arbitrario." tags: [tui, list, component] uses_types: [] -file_path: "types/tui/list_item.go" +file_path: "functions/tui/list_item.go" --- diff --git a/types/tui/list_model.md b/types/tui/list_model.md index ee5bee14..7498eb4e 100644 --- a/types/tui/list_model.md +++ b/types/tui/list_model.md @@ -9,5 +9,5 @@ definition: | description: "Componente lista seleccionable con cursor, scroll y seleccion simple o multiple. Implementa tea.Model." tags: [tui, list, component, interactive] uses_types: [list_item_go_tui, styles_go_tui] -file_path: "types/tui/list_model.go" +file_path: "functions/tui/list_model.go" --- diff --git a/types/tui/multi_progress_model.md b/types/tui/multi_progress_model.md index 63eb507b..af935664 100644 --- a/types/tui/multi_progress_model.md +++ b/types/tui/multi_progress_model.md @@ -9,5 +9,5 @@ definition: | description: "Gestor de multiples barras de progreso simultaneas. Implementa tea.Model." tags: [tui, progress, multi, component] uses_types: [progress_model_go_tui, styles_go_tui] -file_path: "types/tui/multi_progress_model.go" +file_path: "functions/tui/multi_progress_model.go" --- diff --git a/types/tui/progress_model.md b/types/tui/progress_model.md index ab81521d..c8a5ddae 100644 --- a/types/tui/progress_model.md +++ b/types/tui/progress_model.md @@ -9,5 +9,5 @@ definition: | description: "Barra de progreso con porcentaje, ETA y tiempo transcurrido. Implementa tea.Model." tags: [tui, progress, component, interactive] uses_types: [styles_go_tui] -file_path: "types/tui/progress_model.go" +file_path: "functions/tui/progress_model.go" --- diff --git a/types/tui/spinner_model.md b/types/tui/spinner_model.md index 90e961ed..80de7f05 100644 --- a/types/tui/spinner_model.md +++ b/types/tui/spinner_model.md @@ -9,5 +9,5 @@ definition: | description: "Indicador de carga animado con mensaje personalizable. Implementa tea.Model." tags: [tui, spinner, loading, component] uses_types: [] -file_path: "types/tui/spinner_model.go" +file_path: "functions/tui/spinner_model.go" --- diff --git a/types/tui/spinner_with_timeout_model.md b/types/tui/spinner_with_timeout_model.md index cc796d10..fd4a12dd 100644 --- a/types/tui/spinner_with_timeout_model.md +++ b/types/tui/spinner_with_timeout_model.md @@ -9,5 +9,5 @@ definition: | description: "Spinner que se auto-detiene tras un timeout configurable. Embeds SpinnerModel." tags: [tui, spinner, timeout, component] uses_types: [spinner_model_go_tui] -file_path: "types/tui/spinner_with_timeout_model.go" +file_path: "functions/tui/spinner_with_timeout_model.go" --- diff --git a/types/tui/styles.md b/types/tui/styles.md index 496aa8e2..6c65a093 100644 --- a/types/tui/styles.md +++ b/types/tui/styles.md @@ -9,5 +9,5 @@ definition: | description: "Coleccion completa de estilos lipgloss pre-configurados para tipografia, estados, componentes y layout." tags: [tui, styles, lipgloss, theme] uses_types: [theme_go_tui] -file_path: "types/tui/styles.go" +file_path: "functions/tui/styles.go" --- diff --git a/types/tui/theme.md b/types/tui/theme.md index def9e801..1f6e807d 100644 --- a/types/tui/theme.md +++ b/types/tui/theme.md @@ -9,5 +9,5 @@ definition: | description: "Paleta de colores para terminal con 9 colores semanticos. Base del sistema de estilos." tags: [tui, theme, styles, colors] uses_types: [] -file_path: "types/tui/theme.go" +file_path: "functions/tui/theme.go" ---