import marimo __generated_with = "0.16.5" app = marimo.App(width="columns") @app.cell def _(): import marimo as mo return (mo,) @app.cell(hide_code=True) def _(mo): mo.md(r"""# 🐘 Postgres: DDBB con extensiones avanzadas (pgvector, PostGIS, TimescaleDB, etc.)""") return @app.cell def _(): import os import textwrap import yaml import subprocess from pathlib import Path # ==== Parámetros ==== PUERTO_POSTGRES = 55432 SERVICIO = "postgres_ext" PASSWORD = "mipassword" USER = "postgres" DBNAME = "basededatos" # ==== Extensiones que quieres habilitar ==== EXTENSIONES = [ # Builtins "hstore", "citext", "uuid-ossp", "pg_trgm", "fuzzystrmatch", "tablefunc", "unaccent", "ltree", # Ecosistema extendido "postgis", "pgvector", "timescaledb", ] RUTA_PROYECTO = Path(".").resolve() return ( DBNAME, EXTENSIONES, PASSWORD, PUERTO_POSTGRES, Path, RUTA_PROYECTO, SERVICIO, USER, subprocess, yaml, ) @app.function def pkgs_para_extensiones(exts, pg_major=15): """ Devuelve (pkgs_apt, builtins, needs_timescale_repo) builtins = extensiones que no requieren apt pkgs_apt = paquetes apt necesarios para extensiones adicionales needs_timescale_repo = True si hay que añadir el repo de TimescaleDB """ builtins = [] pkgs_apt = [] needs_timescale_repo = False for e in exts: e_low = e.lower() if e_low in { "hstore", "citext", "uuid-ossp", "pg_trgm", "fuzzystrmatch", "tablefunc", "unaccent", "ltree" }: builtins.append(e_low) elif e_low == "postgis": pkgs_apt += [ f"postgresql-{pg_major}-postgis-3", f"postgresql-{pg_major}-postgis-3-scripts", "gdal-bin", "proj-bin", ] elif e_low == "pgvector": pkgs_apt += [f"postgresql-{pg_major}-pgvector"] elif e_low == "timescaledb": needs_timescale_repo = True pkgs_apt += [f"timescaledb-2-postgresql-{pg_major}"] else: raise ValueError(f"Extensión no soportada: {e}") pkgs_apt = sorted(set(pkgs_apt)) return pkgs_apt, builtins, needs_timescale_repo @app.cell def _(DBNAME, Path, USER): from textwrap import dedent from textwrap import indent from textwrap import dedent def generar_dockerfile(ruta: Path, exts, pg_major=15): ruta.mkdir(parents=True, exist_ok=True) pkgs_apt, builtins, needs_timescale_repo = pkgs_para_extensiones(exts, pg_major) base_image = f"postgres:{pg_major}" apt_lines = [ "RUN apt-get update && \\", " apt-get install -y wget gnupg && \\", ] if needs_timescale_repo: apt_lines += [ " wget -qO- https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /usr/share/keyrings/timescaledb.gpg && \\", ' echo \"deb [signed-by=/usr/share/keyrings/timescaledb.gpg] https://packagecloud.io/timescale/timescaledb/debian bookworm main\" > /etc/apt/sources.list.d/timescaledb.list && \\', " apt-get update && \\", ] if pkgs_apt: apt_lines.append(" DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\") for pkg in pkgs_apt: apt_lines.append(f" {pkg} \\") apt_lines.append(" && rm -rf /var/lib/apt/lists/*") apt_block = "\n".join(apt_lines) # Si usa Timescale, añadimos pre-carga preload_block = "" if "timescaledb" in [e.lower() for e in exts]: preload_block = ( "\n# Preload TimescaleDB\n" "RUN echo \"shared_preload_libraries = 'timescaledb'\" >> /usr/share/postgresql/postgresql.conf.sample\n" ) dockerfile = f""" FROM {base_image} # Variables de entorno ENV POSTGRES_USER={USER} \\ POSTGRES_DB={DBNAME} {apt_block} {preload_block} # Copiamos scripts de inicialización COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ """ (ruta / "Dockerfile").write_text(dedent(dockerfile).strip() + "\n", encoding="utf-8") return (generar_dockerfile,) @app.cell def _(Path): def generar_init_sql(ruta: Path, exts): init_dir = ruta / "docker-entrypoint-initdb.d" init_dir.mkdir(parents=True, exist_ok=True) lines = ["-- Habilitar extensiones solicitadas"] for e in exts: e_low = e.lower() ext_name = { "pgvector": "vector", "postgis": "postgis", "hstore": "hstore", "citext": "citext", "uuid-ossp": "\"uuid-ossp\"", "pg_trgm": "pg_trgm", "fuzzystrmatch": "fuzzystrmatch", "tablefunc": "tablefunc", "unaccent": "unaccent", "ltree": "ltree", "timescaledb": "timescaledb", }.get(e_low, e_low) lines.append(f"CREATE EXTENSION IF NOT EXISTS {ext_name};") sql = "\n".join(lines) + "\n" (init_dir / "10-extensions.sql").write_text(sql, encoding="utf-8") return (generar_init_sql,) @app.cell def _(DBNAME, Path, USER, yaml): def crear_docker_compose(ruta: Path, servicio: str, puerto_host: int, password: str): compose = { "services": { servicio: { "build": {"context": "."}, "restart": "always", "ports": [f"{puerto_host}:5432"], "environment": { "POSTGRES_PASSWORD": password, "POSTGRES_USER": USER, "POSTGRES_DB": DBNAME, }, "healthcheck": { "test": ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"], "interval": "10s", "timeout": "5s", "retries": 5, }, "volumes": ["postgres_data:/var/lib/postgresql/data"], } }, "volumes": {"postgres_data": {}}, } (ruta / "docker-compose.yml").write_text(yaml.dump(compose, sort_keys=False), encoding="utf-8") return (crear_docker_compose,) @app.cell def _(Path, subprocess): def construir_y_levantar(ruta: Path): def _run(cmd): subprocess.run(cmd, cwd=ruta, check=True) try: _run(["docker", "compose", "build"]) _run(["docker", "compose", "up", "-d"]) except Exception: _run(["docker-compose", "build"]) _run(["docker-compose", "up", "-d"]) return (construir_y_levantar,) @app.cell def _( EXTENSIONES, PASSWORD, PUERTO_POSTGRES, RUTA_PROYECTO, SERVICIO, construir_y_levantar, crear_docker_compose, generar_dockerfile, generar_init_sql, ): if __name__ == "__main__": RUTA_PROYECTO.mkdir(parents=True, exist_ok=True) generar_dockerfile(RUTA_PROYECTO, EXTENSIONES) generar_init_sql(RUTA_PROYECTO, EXTENSIONES) crear_docker_compose(RUTA_PROYECTO, SERVICIO, PUERTO_POSTGRES, PASSWORD) construir_y_levantar(RUTA_PROYECTO) print(f"✅ Postgres inicializado en http://localhost:{PUERTO_POSTGRES}") print("📦 Extensiones instaladas:", ", ".join(EXTENSIONES)) return if __name__ == "__main__": app.run()