--- name: resolve_pg_dsn kind: function lang: py domain: infra version: "1.0.0" purity: impure signature: "def resolve_pg_dsn(project: str) -> dict" description: "Resuelve el DSN de PostgreSQL de un proyecto conocido del ecosistema (captacion_clientes, seo_analytics) sin lanzar. Centraliza el patron inline repetido por el agente: leer el DSN desde la variable de entorno del proyecto (CAPTACION_DSN, SEO_DSN), caer a la linea = del .env del proyecto, y como ultimo recurso construirlo desde el secreto de pass (password en runtime, user/host/port/db fijos por proyecto). Cada proyecto declara su politica de resolucion en un mapa interno explicito (_PROJECTS) con alias para el nombre largo. Orden de resolucion: (1) env var, (2) .env, (3) pass. Devuelve {status:'ok', project, dsn, source} con source='env'|'dotenv'|'pass', o {status:'error', error} si el proyecto es desconocido o no se pudo construir el DSN. NUNCA hardcodea el password: lo lee de pass via pass_get_secret en runtime." tags: [postgres, postgresql, dsn, credential, infra] uses_functions: [pass_get_secret_py_infra] uses_types: [] returns: [] returns_optional: false error_type: "error_py_core" imports: [os] tested: true tests: ["env var seteada gana y source es env", "proyecto desconocido devuelve error sin lanzar", "alias largo resuelve a la clave canonica", "fallback a .env cuando no hay env var"] test_file_path: "python/functions/infra/resolve_pg_dsn_test.py" file_path: "python/functions/infra/resolve_pg_dsn.py" params: - name: project desc: "Nombre del proyecto. Acepta la clave canonica ('captacion', 'seo') o el alias largo ('captacion_clientes', 'seo_analytics'). Un nombre no registrado devuelve {status:'error'} con la lista de proyectos conocidos." output: "dict. En exito: {status:'ok', project:str (clave canonica), dsn:str (cadena postgresql://...), source:str ('env'|'dotenv'|'pass')}. En error (sin lanzar): {status:'error', error:str} para proyecto desconocido o DSN no resoluble." --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from infra.resolve_pg_dsn import resolve_pg_dsn # Por nombre corto o largo, da igual. res = resolve_pg_dsn("captacion") print(res["status"]) # ok print(res["source"]) # 'dotenv' (lee CAPTACION_DSN del .env del proyecto) # res["dsn"] -> "postgresql://captacion:***@localhost:5433/trends" # La env var, si esta seteada, gana sobre el .env y sobre pass. os.environ["SEO_DSN"] = "postgresql://captacion:x@localhost:5433/seo" print(resolve_pg_dsn("seo_analytics")["source"]) # env ``` ## Cuando usarla Usala antes de cualquier `psql`/`psycopg2`/`pg_query` contra el Postgres de un proyecto del ecosistema, en vez de reescribir a mano la resolucion del DSN (grep al .env + fallback a pass). Es el unico sitio que sabe como se llama la env var de cada proyecto, donde vive su .env y de que entry de pass sale el password. Si vas a lanzar varias queries seguidas, resuelve el DSN una vez y reusalo; para el caso comun de "una query a un proyecto" usa el pipeline `query_project_pg_py_pipelines` que ya compone esta resolucion con `pg_query`. ## Gotchas - Impura: lee variables de entorno, el `.env` del proyecto en disco y ejecuta `pass show` como subproceso. El resultado depende del entorno de la maquina. - El `dsn` devuelto **contiene el password en claro**. NO lo logees ni lo imprimas en produccion (el `## Ejemplo` lo redacta a proposito). - La ruta del `.env` se resuelve relativa a `FN_REGISTRY_ROOT` si esa env var esta seteada; si no, relativa al cwd. Lanza desde la raiz del registry o exporta `FN_REGISTRY_ROOT` para que el paso (2) `.env` funcione. - Solo conoce los proyectos del mapa `_PROJECTS`. Anadir uno nuevo = una entrada de diccionario (env_var + dotenv_path + pass_path + pg fijos), no otro bloque de bash inline. - El fallback de `seo` apunta hoy al mismo entry de pass que `captacion` (mismo contenedor Postgres, distinta db `seo`). Si seo_analytics pasa a tener credenciales propias, actualiza `_PROJECTS['seo']`.