"""Parsea el texto de un secreto de `pass` para credenciales de Metabase. Distingue dos formatos sin tocar disco ni red (funcion pura): - API-key: una sola linea con la clave (las API keys de Metabase empiezan por ``mb_``, p.ej. el secreto ``metabase/aurgi-api-key``). - Sesion: multi-linea estilo ``captacion/metabase`` — la primera linea es la contrasena y una linea posterior lleva el email/usuario con un prefijo reconocible (``email:``, ``user:``, ``login:`` o ``username:``). El caller decide el ``mode`` y este parser solo extrae los campos del texto. """ # Prefijos (case-insensitive) que identifican la linea del email/usuario en un # secreto multi-linea de pass. Se prueban en este orden. _EMAIL_PREFIXES = ("email:", "login:", "username:", "user:") def parse_metabase_secret(secret_text: str, mode: str = "auto") -> dict: """Extrae credenciales de Metabase del texto crudo de un secreto de pass. No ejecuta `pass` ni hace I/O: recibe el texto ya leido y lo interpreta. Funcion pura y determinista, apta para tests unitarios. Args: secret_text: contenido completo del secreto (varias lineas separadas por ``\\n``). Por convencion de pass la primera linea es la contrasena/clave; las siguientes son metadata. mode: ``"api_key"``, ``"session"`` o ``"auto"`` (default). En ``auto`` se detecta el formato: si hay una linea de email/usuario reconocible se asume sesion; si no, se asume api_key (una sola linea de clave). Returns: Dict. Nunca lanza: - api_key -> ``{"status": "ok", "mode": "api_key", "api_key": str}`` - session -> ``{"status": "ok", "mode": "session", "email": str, "password": str}`` - error -> ``{"status": "error", "error": str}`` para texto vacio, modo invalido, o session sin email/password localizables. Example: >>> parse_metabase_secret("mb_abc123") {'status': 'ok', 'mode': 'api_key', 'api_key': 'mb_abc123'} >>> parse_metabase_secret("hunter2\\nemail: a@b.com\\nurl: http://x") {'status': 'ok', 'mode': 'session', 'email': 'a@b.com', 'password': 'hunter2'} """ if mode not in ("api_key", "session", "auto"): return {"status": "error", "error": f"invalid mode {mode!r}"} lines = secret_text.splitlines() if not lines or not lines[0].strip(): return {"status": "error", "error": "empty secret"} email = _find_email(lines) if mode == "auto": mode = "session" if email is not None else "api_key" if mode == "api_key": return { "status": "ok", "mode": "api_key", "api_key": lines[0].strip(), } # mode == "session" if email is None: return { "status": "error", "error": ( "session secret without email/user line " f"(expected one of {', '.join(_EMAIL_PREFIXES)})" ), } password = lines[0].strip() if not password: return {"status": "error", "error": "session secret without password"} return { "status": "ok", "mode": "session", "email": email, "password": password, } def _find_email(lines: list[str]) -> str | None: """Devuelve el email/usuario de la primera linea con prefijo reconocido.""" for raw in lines[1:]: low = raw.strip().lower() for prefix in _EMAIL_PREFIXES: if low.startswith(prefix): # Conserva el valor original (no el lowercased) tras el prefijo. value = raw.strip()[len(prefix):].strip() if value: return value return None