merge: quick/fn-run-types-metabase — fn run multi-lenguaje, tipos Go unificados, metabase setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+48
-4
@@ -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 <id>
|
||||
fn add -k function # Template
|
||||
|
||||
# Ejecutar funciones y pipelines (fn run)
|
||||
fn run <id_or_name> [args...] # Ejecuta por ID o nombre
|
||||
fn run init_metabase --project test # Go pipeline (go run .)
|
||||
fn run setup_metabase_volume # Bash pipeline (bash <file>)
|
||||
fn run metabase_setup_py_infra # Python (python/.venv/bin/python3 <file>)
|
||||
fn run my_component_ts_core # TypeScript (frontend/node_modules/.bin/tsx <file>)
|
||||
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 <file>
|
||||
# bash → bash <file>
|
||||
# ts → frontend/node_modules/.bin/tsx <file>
|
||||
# Si el nombre es ambiguo, muestra los IDs para desambiguar.
|
||||
|
||||
# Proposals
|
||||
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <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/`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ registry.db-wal
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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`.
|
||||
+82
@@ -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 <nombre_contenedor>" >&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=<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 <password> \\"
|
||||
echo " --registry-db-path ${DEST_PATH}"
|
||||
echo "---------------------------------------------------------------------"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 <id> Muestra entrada completa
|
||||
fn add [-k kind] Abre $EDITOR con template
|
||||
fn run <id_or_name> [args...] Ejecuta funcion/pipeline (go/py/bash)
|
||||
fn ops <subcommand> Gestiona operations.db (fn ops help)
|
||||
fn proposal <add|list|show|update> Gestiona proposals`)
|
||||
}
|
||||
|
||||
+208
@@ -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 <id_or_name> [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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+305
-8
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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}")
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
@@ -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"
|
||||
---
|
||||
|
||||
+1
-1
@@ -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"
|
||||
---
|
||||
|
||||
+1
-1
@@ -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"
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user