feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""Resuelve el DSN de PostgreSQL de un proyecto conocido del ecosistema.
|
||||
|
||||
Centraliza el patrón que el agente reescribía inline una y otra vez: leer el
|
||||
DSN de un proyecto desde su variable de entorno, caer al fichero ``.env`` del
|
||||
proyecto, y como último recurso construirlo desde el secreto guardado en
|
||||
``pass``. Cada proyecto declara su política de resolución en un mapa interno
|
||||
explícito (``_PROJECTS``), de modo que añadir un proyecto nuevo es una sola
|
||||
entrada de diccionario, no otra copia del bloque de bash.
|
||||
|
||||
Es una función impura (lee env, ficheros y ``pass``) que NUNCA lanza: devuelve
|
||||
un dict ``{status:'ok', ...}`` en éxito y ``{status:'error', error}`` en fallo,
|
||||
siguiendo el estilo del resto de funciones I/O del registry. El password sale
|
||||
de ``pass`` en runtime — jamás está hardcodeado en este módulo.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from infra.pass_get_secret import pass_get_secret
|
||||
|
||||
|
||||
# Mapa EXPLÍCITO de proyectos conocidos -> cómo resolver su DSN.
|
||||
#
|
||||
# Cada entrada declara:
|
||||
# env_var: variable de entorno que (si está seteada) gana sobre todo.
|
||||
# dotenv_path: ruta (relativa a la raíz del registry) del .env del proyecto.
|
||||
# La línea buscada dentro del .env es "<env_var>=<dsn>".
|
||||
# pass_path: ruta del secreto en `pass` desde la que construir el fallback.
|
||||
# pg: parámetros fijos para construir el DSN desde el secreto de pass.
|
||||
# user/host/port/db son estables por proyecto; el password es la
|
||||
# primera línea del secreto de pass y se lee en runtime.
|
||||
#
|
||||
# Los alias (claves múltiples que apuntan a la misma config) permiten llamar a
|
||||
# la función con el nombre corto ("captacion") o el largo ("captacion_clientes").
|
||||
_PROJECTS = {
|
||||
"captacion": {
|
||||
"env_var": "CAPTACION_DSN",
|
||||
"dotenv_path": "projects/captacion_clientes/.env",
|
||||
"pass_path": "captacion/postgres",
|
||||
"pg": {"user": "captacion", "host": "localhost", "port": "5433", "db": "trends"},
|
||||
},
|
||||
"seo": {
|
||||
"env_var": "SEO_DSN",
|
||||
# seo_analytics no fija un .env canónico hoy; se resuelve por env var
|
||||
# (la convención que ya usa ingest_gsc_search_analytics) o por pass.
|
||||
"dotenv_path": "projects/seo_analytics/.env",
|
||||
"pass_path": "captacion/postgres",
|
||||
"pg": {"user": "captacion", "host": "localhost", "port": "5433", "db": "seo"},
|
||||
},
|
||||
}
|
||||
|
||||
# Alias: nombre largo del proyecto -> clave canónica en _PROJECTS.
|
||||
_ALIASES = {
|
||||
"captacion_clientes": "captacion",
|
||||
"seo_analytics": "seo",
|
||||
}
|
||||
|
||||
|
||||
def _canonical(project: str) -> str:
|
||||
"""Normaliza el nombre del proyecto a su clave canónica en _PROJECTS."""
|
||||
key = (project or "").strip().lower()
|
||||
return _ALIASES.get(key, key)
|
||||
|
||||
|
||||
def _read_dotenv_line(dotenv_path: str, env_var: str) -> str:
|
||||
"""Devuelve el valor de la línea ``<env_var>=...`` del .env, o "" si no está.
|
||||
|
||||
Resuelve la ruta relativa a la raíz del registry usando FN_REGISTRY_ROOT si
|
||||
está disponible; en su defecto asume el cwd actual. Quita comillas dobles o
|
||||
simples envolventes del valor.
|
||||
"""
|
||||
root = os.environ.get("FN_REGISTRY_ROOT", "").strip()
|
||||
full = os.path.join(root, dotenv_path) if root else dotenv_path
|
||||
try:
|
||||
with open(full, "r", encoding="utf-8") as fh:
|
||||
prefix = env_var + "="
|
||||
for raw in fh:
|
||||
line = raw.strip()
|
||||
if line.startswith(prefix):
|
||||
value = line[len(prefix):].strip()
|
||||
if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
|
||||
value = value[1:-1]
|
||||
return value
|
||||
except OSError:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def resolve_pg_dsn(project: str) -> dict:
|
||||
"""Resuelve el DSN PostgreSQL de un proyecto conocido sin lanzar.
|
||||
|
||||
Orden de resolución (gana el primero que tenga valor):
|
||||
1. La variable de entorno del proyecto (``env``).
|
||||
2. La línea ``<ENV_VAR>=<dsn>`` del ``.env`` del proyecto (``dotenv``).
|
||||
3. Un DSN construido a partir del secreto de ``pass`` (``pass``): el
|
||||
password es la primera línea del secreto; user/host/port/db son fijos
|
||||
por proyecto. El password NO se hardcodea: se lee en runtime.
|
||||
|
||||
Args:
|
||||
project: nombre del proyecto. Acepta la clave canónica ("captacion",
|
||||
"seo") o el alias largo ("captacion_clientes", "seo_analytics").
|
||||
|
||||
Returns:
|
||||
dict. En éxito: ``{status:'ok', project, dsn, source}`` donde ``source``
|
||||
es ``'env'`` | ``'dotenv'`` | ``'pass'`` según de dónde salió el DSN.
|
||||
En error (sin lanzar): ``{status:'error', error}`` (proyecto desconocido
|
||||
o no se pudo construir el DSN por ningún medio).
|
||||
"""
|
||||
canonical = _canonical(project)
|
||||
cfg = _PROJECTS.get(canonical)
|
||||
if cfg is None:
|
||||
known = ", ".join(sorted(set(_PROJECTS) | set(_ALIASES)))
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"unknown project '{project}'. Known: {known}",
|
||||
}
|
||||
|
||||
env_var = cfg["env_var"]
|
||||
|
||||
# 1. Variable de entorno (gana sobre todo).
|
||||
env_dsn = os.environ.get(env_var, "").strip()
|
||||
if env_dsn:
|
||||
return {"status": "ok", "project": canonical, "dsn": env_dsn, "source": "env"}
|
||||
|
||||
# 2. Línea del .env del proyecto.
|
||||
dotenv_dsn = _read_dotenv_line(cfg["dotenv_path"], env_var)
|
||||
if dotenv_dsn:
|
||||
return {"status": "ok", "project": canonical, "dsn": dotenv_dsn, "source": "dotenv"}
|
||||
|
||||
# 3. Fallback: construir desde el secreto de pass (password en runtime).
|
||||
secret = pass_get_secret(cfg["pass_path"], line=1)
|
||||
if secret.get("status") != "ok":
|
||||
return {
|
||||
"status": "error",
|
||||
"error": (
|
||||
f"could not resolve DSN for '{canonical}': env var {env_var} unset, "
|
||||
f"no line in .env, and pass failed: {secret.get('error')}"
|
||||
),
|
||||
}
|
||||
password = secret["value"]
|
||||
pg = cfg["pg"]
|
||||
dsn = f"postgresql://{pg['user']}:{password}@{pg['host']}:{pg['port']}/{pg['db']}"
|
||||
return {"status": "ok", "project": canonical, "dsn": dsn, "source": "pass"}
|
||||
Reference in New Issue
Block a user