feat: pipelines Metabase — add ops db, create ops dashboard, fix permissions
Tres pipelines Python para gestionar operations.db en Metabase: - metabase_add_ops_db: registra la operations.db de una app como database SQLite - metabase_create_ops_dashboard: genera dashboard operativo con 14 cards (KPIs, distribución, executions, assertions) para cualquier app - metabase_fix_permissions: arregla SQLITE_READONLY_DIRECTORY haciendo chmod 777/666 sin chown (que se propaga al host via bind mount) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Pipeline: registra la operations.db de una app en Metabase.
|
||||
|
||||
Compone: metabase_auth + metabase_list_databases + metabase_add_database.
|
||||
|
||||
Requiere que el contenedor Metabase tenga montado el directorio de la app
|
||||
como volumen RW para que SQLite pueda crear journal files.
|
||||
|
||||
Uso:
|
||||
python metabase_add_ops_db.py <app_name>
|
||||
python metabase_add_ops_db.py docker_tui
|
||||
python metabase_add_ops_db.py --list
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from metabase.client import metabase_auth
|
||||
from metabase import metabase_list_databases, metabase_add_database
|
||||
|
||||
METABASE_URL = os.environ.get("METABASE_URL", "http://localhost:3000")
|
||||
EMAIL = os.environ.get("METABASE_ADMIN_EMAIL", "admin@fnregistry.local")
|
||||
PASSWORD = os.environ.get("METABASE_ADMIN_PASSWORD", "FnRegistry2024!")
|
||||
|
||||
CONTAINER_DATA_PREFIX = "/data/ops-"
|
||||
|
||||
|
||||
def find_apps(registry_root: str) -> list[str]:
|
||||
"""Lista apps que tienen operations.db."""
|
||||
apps_dir = os.path.join(registry_root, "apps")
|
||||
if not os.path.isdir(apps_dir):
|
||||
return []
|
||||
return sorted(
|
||||
d
|
||||
for d in os.listdir(apps_dir)
|
||||
if os.path.isfile(os.path.join(apps_dir, d, "operations.db"))
|
||||
)
|
||||
|
||||
|
||||
def db_name_for_app(app_name: str) -> str:
|
||||
return f"ops-{app_name.replace('_', '-')}"
|
||||
|
||||
|
||||
def container_path_for_app(app_name: str) -> str:
|
||||
return f"{CONTAINER_DATA_PREFIX}{app_name.replace('_', '-')}/operations.db"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Registra operations.db de una app en Metabase")
|
||||
parser.add_argument("app_name", nargs="?", help="Nombre de la app (directorio en apps/)")
|
||||
parser.add_argument("--list", action="store_true", help="Lista apps disponibles y su estado en Metabase")
|
||||
args = parser.parse_args()
|
||||
|
||||
registry_root = os.environ.get(
|
||||
"FN_REGISTRY_ROOT",
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")),
|
||||
)
|
||||
|
||||
if args.list:
|
||||
client = metabase_auth(METABASE_URL, EMAIL, PASSWORD)
|
||||
dbs = metabase_list_databases(client)
|
||||
db_names = {d["name"] for d in dbs}
|
||||
apps = find_apps(registry_root)
|
||||
print(f"Apps con operations.db ({len(apps)}):\n")
|
||||
for app in apps:
|
||||
name = db_name_for_app(app)
|
||||
status = "registrada" if name in db_names else "NO registrada"
|
||||
print(f" {app:30s} -> {name:30s} [{status}]")
|
||||
client.close()
|
||||
return
|
||||
|
||||
if not args.app_name:
|
||||
parser.error("Se requiere app_name o --list")
|
||||
|
||||
app_name = args.app_name
|
||||
ops_path = os.path.join(registry_root, "apps", app_name, "operations.db")
|
||||
if not os.path.isfile(ops_path):
|
||||
print(f"ERROR: No existe {ops_path}")
|
||||
print(f"Apps disponibles: {', '.join(find_apps(registry_root))}")
|
||||
sys.exit(1)
|
||||
|
||||
name = db_name_for_app(app_name)
|
||||
container_path = container_path_for_app(app_name)
|
||||
|
||||
print(f"Registrando {app_name} en Metabase...")
|
||||
print(f" Nombre: {name}")
|
||||
print(f" Path: {container_path}")
|
||||
|
||||
client = metabase_auth(METABASE_URL, EMAIL, PASSWORD)
|
||||
|
||||
# Verificar si ya existe
|
||||
dbs = metabase_list_databases(client)
|
||||
for db in dbs:
|
||||
if db["name"] == name:
|
||||
print(f" Ya existe con id={db['id']}, nada que hacer.")
|
||||
client.close()
|
||||
return
|
||||
|
||||
# Registrar
|
||||
result = metabase_add_database(client, name, "sqlite", {"db": container_path})
|
||||
db_id = result["id"]
|
||||
print(f" Registrada con id={db_id}")
|
||||
|
||||
# Recordar: el contenedor necesita el volumen montado
|
||||
print(f"\nIMPORTANTE: El contenedor Metabase debe tener montado:")
|
||||
print(f" -v {os.path.join(registry_root, 'apps', app_name)}:{CONTAINER_DATA_PREFIX}{app_name.replace('_', '-')}")
|
||||
print(f"\nSi el mount no existe, hay que recrear el contenedor.")
|
||||
print(f"\nDatabase disponible en: {METABASE_URL}/question#new?database={db_id}")
|
||||
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user