"""Utilizario de funciones usadas en los procesos de robotizacion con Python."""

import os
import re
import shutil
import logging
from logging import NullHandler
import subprocess
import configparser
import sqlite3
from sqlite3 import Error
from datetime import datetime
from string import ascii_lowercase
import itertools

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from functools import wraps
import time
from os import PathLike
from typing import Union, Iterable
from pathlib import Path

log = logging.getLogger(__name__)
log.addHandler(NullHandler())

StrOrPath = Union[str, PathLike[str]]
StrOrPathList = Union[str, PathLike[str], Iterable[str], Iterable[PathLike[str]]]


def send_mail_by_exe(credentials={}, mail={}):
    """Funcion que permite ejecutar envio de correo a traves de un ejecutable inhouse.

    Args:
        credentials (dict, optional): Diccionario con credenciales del correo. Defaults to {}.
        mail (dict, optional): Diccionario con datos del correo a enviar. Defaults to {}.

    Raises:
        Exception: Generado por no detectar los campos necesarios del envio
        Exception: Generado al ejecutar el ejecutable mail.exe
    """
    path = os.path.dirname(__file__)
    program = os.path.join(path, 'mail.exe')

    try:
        arguments = credentials['username'] + '|' + credentials['password'] + '|'
        arguments += credentials['server'] + '|' + credentials['port'] + '|'
        arguments += mail['sender'] + '|' + mail['reciepients'] + '|' + mail['cc'] + '|'
        arguments += mail['body'] + '|' + mail['subject'] + '|' + mail['attachment']
    except IndexError as e:
        log.error("Uno de los campos email no fue encontrado")
        raise Exception(e)

    try:
        subprocess.call([program, arguments])
    except Exception as e:
        log.error("Ocurrio un error al ejecutar:" + program)
        raise Exception(e)


def create_folders(lista_carpetas=[]):
    """Funcion que crea carpetas de trabajo.

    Args:
        lista_carpetas (list, optional): Lista de carpetas a crear. Defaults to [].
    """
    for carpeta in lista_carpetas:
        if not os.path.exists(carpeta):
            os.makedirs(carpeta)


def create_folder_env(dir_path):
    """Funcion que crea las carpetas de trabajo estándar de trabajo.

    Args:
        dir_path (str): Directorio raiz donde se desea crear la estructura
    """
    dir_in = os.path.join(dir_path, "input")
    dir_out = os.path.join(dir_path, "output")
    dir_log = os.path.join(dir_path, "log")

    if not os.path.exists(os.path.join(dir_in, "backup")):
        os.makedirs(os.path.join(dir_in, "backup"))

    if not os.path.exists(os.path.join(dir_out, "backup")):
        os.makedirs(os.path.join(dir_out, "backup"))

    if not os.path.exists(dir_log):
        os.makedirs(dir_log)

    return


def config_logging(file_log=None, level=logging.DEBUG, console=True):
    """Funcion que configura la gestion de Logs.

    Args:
        file_log (str, optional): Nombre del archivo log. Defaults to None.
        level (logging.level, optional): Nivel de log. Defaults to logging.DEBUG.
        console (bool, optional): Define si se acepta impresion en consola. Defaults to True.

    Returns:
        logger: logger de logging
    """
    formatter = logging.Formatter('%(asctime)-5s %(filename)s %(lineno)d %(levelname)-8s %(message)s')

    log = logging.getLogger(__name__)

    # logging file
    if file_log and file_log != "":
        fh = logging.FileHandler(file_log)
        fh.setFormatter(formatter)
        log.addHandler(fh)

    # logging console
    if console:
        ch = logging.StreamHandler()
        ch.setFormatter(formatter)
        log.addHandler(ch)

    log.setLevel(level=level)
    log.info("Level log: " + logging.getLevelName(level))

    for i in log.handlers:
        log.info("Handler:" + type(i).__name__)

    return log


def check_if_string_in_file(filename, string_to_search):
    """Check if any line in the file contains given string."""
    # Open the file in read only mode
    with open(filename, 'r', errors='ignore', encoding='utf-8') as read_obj:
        # Read all lines in the file one by one
        for line in read_obj:
            # For each line, check if line contains the string
            if string_to_search.lower() in line.lower():
                return True
    return False


class Configuration(object):
    """Clase que permite cargar dinámicamente los archivos de configuración del proceso."""

    def __init__(self, file_config="Config.ini", section_names=["DEFAULT"]):
        """Inicializa los parametros de entrada.

        Args:
            file_config (str, optional): Ruta absoluta del archivo de configuración. Defaults to "Config.ini".
            section_names (list, optional): Lista de secciones a cargar. Defaults to ["DEFAULT"].

        Raises:
            ValueError: Generado al no encontrar el archivo de configuracion
        """
        parser = configparser.ConfigParser(interpolation=EnvInterpolation())
        parser.optionxform = str
        found = parser.read(file_config)

        if not found:
            raise ValueError('Archivo de configuracion no encontrado.')

        self.parser = parser

        # Cargando configuracion
        for name in section_names:
            self.__dict__.update(parser.items(name))


class EnvInterpolation(configparser.BasicInterpolation):
    """Interpolation which expands environment variables in values."""

    def before_get(self, parser, section, option, value, defaults):
        """Expand environment variables in the value."""
        value = super().before_get(parser, section, option, value, defaults)
        return os.path.expandvars(value)


def create_connection_sqlite(db_file=""):
    """Funcion que crea una coneccion a la base de datos SQLite.

    Args:
        db_file (str, optional): Ruta absoluta del archivo sqlite. Defaults to "".

    Returns:
        Connection: Coneccion sqlite
    """
    conn = None
    try:
        conn = sqlite3.connect(db_file)
    except Error as e:
        print(e)

    return conn


def get_now_format(format="%Y%m%d%H%M%S%f"):
    """Funcion que devuelve la fecha actual bajo un formato."""
    return str(datetime.now().strftime(format))


def replace_string_in_file(filename_in: StrOrPath,
                           filename_out: StrOrPath,
                           dict_words):
    """Funcion que eemplaza cadenas de un archivo.

    Args:
        filename_in (StrOrPath): Archivo entrada.
        filename_out (StrOrPath): Archivo salida.
        dict_words (dict): Diccionario de palabras y valores a reemplazar.
    """
    write_obj = open(filename_out, 'w')

    line = ""

    with open(filename_in, 'r') as read_obj:
        for line in read_obj:

            for k, v in dict_words.items():
                line = line.replace(k, v)

            # log.debug(line)
            write_obj.write(line)

    write_obj.close()


def sqlcmd(file_sql: StrOrPath,
           credentials,
           file_sql_log: StrOrPath,
           dict_variable={},
           validate_error=True,
           coding="65001"):
    """Funcion que ejecuta el utilitario SQLCMD de SQLServer en linea de comandos.

    Args:
        file_sql (StrOrPath): Archivo SQL
        credentials (dict): Diccionario con credenciales SQL
        file_sql_log (StrOrPath): Archivo prefijo para generar archivo log.
        dict_variable (dict, optional): Diccionario de variables a reemplazar. 
                                        Defaults to {}.
        validate_error (bool, optional): Flag para generar exception si se 
                                         encuentra la palabra error. 
                                         Defaults to True.
        coding (str, optional): Coding de respuesta. Defaults to "65001".

    Raises:
        FileNotFoundError: Se necesita ingresar archivo sql
        Exception: Se necesita ingresar archivo log
        Exception: Se encontro un error en el archivo log de la BD
        Exception: Archivo output sqlcmd no generado
    """
    sqlcmd_bin = '/opt/mssql-tools/bin/sqlcmd'

    if os.name == 'nt':
        sqlcmd_bin = 'sqlcmd'

    file_sql = Path(file_sql)
    file_sql_log = Path(file_sql_log)

    if not file_sql.exists():
        raise FileNotFoundError(file_sql)

    file_sql_tmp = Path(str(file_sql) + ".tmp")

    if len(dict_variable) > 0:
        if file_sql_tmp.exists():
            file_sql_tmp.unlink()
        replace_string_in_file(file_sql, file_sql_tmp, dict_variable)
    else:
        shutil.copy(file_sql, file_sql_tmp)

    cmd = (sqlcmd_bin + ' -e -y 0' +
                        ' -i ' + str(file_sql_tmp) +
                        ' -o ' + str(file_sql_log) +
                        ' -S ' + credentials['hostname'] +
                        ' -d ' + credentials['database'] +
                        ' -U ' + credentials['username'] +
                        ' -P ' + credentials['password'] +
                        ' -f ' + coding)

    log.debug("Ejecutando sql")
    log.debug(cmd)

    proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)

    while proc.poll() is None:
        line = proc.stdout.readline()
        # log.debug(line.rstrip())
        if isinstance(line, bytes):
            str_line = line.decode("utf-8")
        else:
            str_line = line

        if str_line != "":
            log.debug(str_line)

    file_sql_tmp.unlink()

    if file_sql_log.exists():
        log.debug(open(file_sql_log,
                       "r",
                       encoding='utf-8',
                       errors='ignore').read())

        if validate_error:
            if check_if_string_in_file(file_sql_log, "error"):
                raise Exception("Se encontro un error en el archivo log.")
    else:
        raise Exception("Archivo output sqlcmd no generado.")


def bcp(table,
        file: StrOrPath,
        operation,
        credentials,
        file_sql_log: StrOrPath,
        validate_error=True,
        coding="-C 65001"):
    """Funcion que ejecuta el utilitario BCP de SQLServer en linea de comandos.

    Args:
        table (str): Tabla a trabajar
        file (str): Archivo de entrada o salida segun la operacion
        operation (str): operacion a ejecutar IN u OUT
        credentials (str): Credenciales de conexion a la Base de Datos SQLServer
        file_sql_log (str): Archivo prefijo para generar archivo log (file_sql_log + ".ahora.db")
        validate_error (bool, optional): Flag para generar exception si se encuentra la palabra error. Defaults to True.
        coding (str, optional): Coding de respuesta. Defaults to "65001".

    Raises:
        Exception: Se necesita ingresar archivo log
        Exception: Operacion BCP no permitida
        Exception: Se encontro un error en el archivo log de la BD
        Exception: Archivo output bcp no generado
    """
    bcpcmd_bin = '/opt/mssql-tools/bin/bcp'
    if os.name == 'nt':
        bcpcmd_bin = 'bcp'

    file = Path(file)
    file_sql_log = Path(file_sql_log)

    if operation not in ("IN", "OUT"):
        raise Exception("Operacion BCP no permitida")

    cmd = (bcpcmd_bin + ' ' + table + ' ' +
           operation + ' "' + str(file) + '"' +
           ' -e ' + str(file_sql_log) +
           ' -S ' + credentials['hostname'] +
           ' -d ' + credentials['database'] +
           ' -U ' + credentials['username'] +
           ' -P ' + credentials['password'] +
           ' -c ' + coding +
           ' -b1000 -m1000 -t"|"')

    log.debug("Ejecutando sql")
    log.debug(cmd)

    proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)

    while proc.poll() is None:
        line = proc.stdout.readline()
        # log.debug(line.rstrip())
        if isinstance(line, bytes):
            str_line = line.decode("utf-8")
        else:
            str_line = line

        if str_line != "":
            log.debug(str_line)

    if file_sql_log.exists():
        log.debug(open(file_sql_log, "r", encoding='utf-8').read())

        if validate_error:
            if check_if_string_in_file(file_sql_log, "error"):
                raise Exception("Se encontro un error en el archivo log")
    else:
        raise Exception("Archivo output bcp no generado.")


def get_data_from_sqllog(file_sql_log: StrOrPath,
                         file_result: StrOrPath,
                         prefix="DATA:"):
    """Funcion que filtra las lineas de texto que comiencen con el prefijo.

    Args:
        file_sql_log (str): Archivo log de entrada
        file_result (str): Archivo resultado
        prefix (str, optional): Prefijo a filtrar los datos. Defaults to "DATA:".

    Raises:
        FileNotFoundError: Archivo de entrada no encontrado
    """
    file_sql_log = Path(file_sql_log)
    file_result = Path(file_result)

    if not file_sql_log.exists():
        raise FileNotFoundError(file_sql_log)

    file_out = open(file_result, "w", encoding='utf-8')

    with open(file_sql_log, 'r', encoding='utf-8', errors='ignore') as res:
        f_data = False

        for line in res:
            if re.match("INI_DATA_SQLSERVER", line):
                log.debug("UBICADO:"+line)
                f_data = True
            if f_data and re.search(prefix, line):
                file_out.write(line.replace(prefix, ""))

    file_out.close()


def iter_all_strings():
    """Funcion que devuelve lista de letras secuenciales.

    Yields:
        str: Secuencia
    """
    for size in itertools.count(1):
        for s in itertools.product(ascii_lowercase, repeat=size):
            yield "".join(s)


def enviar_correo_smtp(mail):
    """Funcion que envia correos via SMTP.

    Args:
        mail (dict): Diccionanrio con credenciales smtp y cuerpo de correo

    Returns:
        bool: Resultado de ejecucion
    """
    try:
        email_smtp_host = mail['smtp_host']
        email_smtp_port = mail['smtp_port']
        email_from = mail['from']
        email_to = mail['to'].split(';')
        email_cc = mail['cc'].split(';')
        email_subject = mail['subject']
        email_body = mail['body']
        email_attachment = mail['attachment']
    except Exception as e:
        log.info(e, exc_info=True)
        log.info("Uno de los parametros del correo no esta definido")
        return False

    try:
        msg = MIMEMultipart()
        msg['From'] = email_from
        msg['To'] = ','.join(email_to)
        msg['Subject'] = email_subject
        msg['cc'] = ','.join(email_cc)

        msg.attach(MIMEText(email_body, 'html', 'utf-8'))
        # msg.attach(MIMEText(email_body,'plain'))

        emails = email_to + email_cc

        filename = mail['attachment']

        if filename != "":
            for filename in email_attachment.split(";"):
                attachment = open(filename, 'rb')
                part = MIMEBase('application', 'octet-stream')
                part.set_payload((attachment).read())
                encoders.encode_base64(part)
                part.add_header('Content-Disposition', "attachment; filename= " + os.path.basename(filename))
                msg.attach(part)

        text = msg.as_string()
        server = smtplib.SMTP(host=email_smtp_host, port=email_smtp_port)

        server.sendmail(email_from, emails, text)
        server.quit()
    except Exception as e:
        log.info(e, exc_info=True)
        log.info("Ocurrio un error al enviar el correo")
        return False

    return True


class RpaNotificacionCorreo:
    """Clase que define la configuracion y metodos para el envio de Correo."""

    def __init__(self, host="10.4.40.99", port="25", secure_mode=None, use_executable=False, **kwargs):
        """Funcion que inicializa valores plantilla del correo a enviar.

        Args:
            host (str, optional): Ip del servidor de correos SMTP. Defaults to "10.4.40.99".
            port (str, optional): Puerto del servidor de correos SMTP. Defaults to "25".
            secure_mode (_type_, optional): Modo seguro para autenticarse. Solo puede ingresarse SSL o TLS.
            use_executable (bool, optional): Booleano que determina si se debe o no usar el .exe (contigencia).
            **kwargs : optional
                Parametros opcionales para la autenticacion
                user
                password
        Raises:
            ValueError: secure_mode: Solo pude ingresar el modo seguro SSL o TLS
        """
        self.host = host
        self.port = port

        # Definiendo modo seguro
        self.secure_mode = None
        if secure_mode:
            if not isinstance(secure_mode, str) or secure_mode not in ["SSL", "TLS"]:
                raise ValueError("secure_mode: Solo pude ingresar el modo seguro SSL o TLS")
            self.secure_mode = secure_mode

        # Definiendo autenticacion. Por defecto se usa relay smtp.
        self.use_auth = False
        if 'user' in kwargs and 'password' in kwargs:
            self.use_auth = True
            self.user = kwargs['user']
            self.password = kwargs['password']

        # Definiendo uso de contingenca .exe (Cuando no existe relay y los metodos SMTP
        # desarrollados hasta ahora no soportan el envio)
        self.use_executable = use_executable

        self.subject = ''
        self.body = ''
        self.email_from = ''
        self.email_to = []
        self.email_cc = []
        self.email_cco = []

    def __str__(self):
        """Funcion que devuelve los parametros configurados en los atributos.

        Returns:
            str: Servidor + Plantilla
        """
        con = ''
        if not self.use_auth:
            con += "Servidor : " + self.host + ":" + self.port + "\n"
        else:
            con += "Servidor: " + self.user + "/" + self.password + "@" + self.host + ":" + self.port + "\n"

        con += "Plantilla From   : " + self.email_from + "\n"
        con += "Plantilla To     : " + str(self.email_to) + "\n"
        con += "Plantilla Cc     : " + str(self.email_cc) + "\n"
        con += "Plantilla Cco    : " + str(self.email_cco) + "\n"
        con += "Plantilla Subject: " + self.subject + "\n"
        con += "Plantilla Body   : " + self.body

        return con

    def cargar_plantilla(self,
                         subject_template=None,
                         body_template=None,
                         email_from=None,
                         email_to=None,
                         email_cc=None,
                         email_cco=None):
        """Funcion que carga los valores plantilla del correo a enviar.

        Args:
            subject_template (_type_, optional): Asunto plantilla a usar en el correo. Defaults to None.
            body_template (_type_, optional): Cuerpo plantilla a usar en el correo. Defaults to None.
            email_from (_type_, optional): Emisor a colocar en el correo a enviar. Defaults to None.
            email_to (_type_, optional): Destinatarios a colocar en el correo a enviar. Si existe mas de uno,
                                         colocar como separador ';'. Defaults to None.
            email_cc (_type_, optional): Destinatarios Con Copia a colocar en el correo a enviar. Si existe mas de uno,
                                         colocar como separador ';'. Defaults to None.
            email_cco (_type_, optional): Destinatarios Con Copia Oculta a colocar en el correo a enviar.
                                          Si existe mas de uno, colocar como separador ';'. Defaults to None.

        Raises:
            ValueError: From: Debe ingresar un correo
            ValueError: From: Debe ingresar una cadena de correo(s) separados por ';'
            ValueError: Cc: Solo puede ingresar una cadena de correos separado por ';'
            ValueError: Cco: Solo puede ingresar una cadena de correos separado por ';'
            ValueError: Debe ingresar al menos un correo destinatario
        """
        if subject_template:
            self.subject = subject_template

        if body_template:
            self.body = body_template

        if type(email_from) not in [str]:
            raise ValueError("From: Debe ingresar un correo")

        if type(email_to) not in [str, type(None)]:
            raise ValueError("From: Debe ingresar una cadena de correo(s) separados por ';'")

        if type(email_cc) not in [str, type(None)]:
            raise ValueError("Cc: Solo puede ingresar una cadena de correos separado por ';'")

        if type(email_cco) not in [str, type(None)]:
            raise ValueError("Cco: Solo puede ingresar una cadena de correos separado por ';'")

        if email_to is None and email_cc is None and email_cco is None:
            raise ValueError("Debe ingresar al menos un correo destinatario")

        self.email_from = email_from
        self.email_to = email_to.split(";") if isinstance(email_to, str) else []
        self.email_cc = email_cc.split(";") if isinstance(email_cc, str) else []
        self.email_cco = email_cco.split(";") if isinstance(email_cco, str) else []

    def enviar_correo(self, subject_replace=None, body_replace=None, attachment_files=None):
        """Funcion que envia correos.

        Args:
            subject_replace (_type_, optional): Palabras clave a reemplazar en el asunto del correo.
                                                Cada llave del diccionario será buscada en la plantilla 
                                                cargada inicialmente y reemplazada por su valor. Defaults to None.
            body_replace (_type_, optional): Palabras clave a reemplazar en el cuerpo del correo.
                                             Cada llave del diccionario será buscada en la plantilla cargada
                                             inicialmente y reemplazada por su valor.. Defaults to None.
            attachment_files (_type_, optional): Cadena de archivos a adjuntar en el correo. Los archivos
                                                 deben tener la ruta absoluta y estar separados por ';' en caso
                                                 de existir mas de uno. Defaults to None.

        Raises:
            ValueError: Ocurrio un error al enviar el correo

        Returns:
            bool: Se devolvera True si es que no se genero alguna excepcion. De lo contrario el valor
                  devuelto sera False
        """
        try:
            # Armando subject
            subject = self.subject
            if subject_replace:
                if isinstance(subject_replace, dict) is False:
                    raise ValueError(
                        "Subject Replace: Debe ingresar un diccionario con las palabras clave a reemplazar")
                for i in subject_replace:
                    subject = str(subject).replace(i, subject_replace[i])

            # Armando body
            body = self.body
            if body_replace:
                if isinstance(body_replace, dict) is False:
                    raise ValueError("Body Replace: Debe ingresar un diccionario con las palabras clave a reemplazar")
                for i in body_replace:
                    body = body.replace(i, body_replace[i])

        except Exception as e:
            log.info(e, exc_info=True)
            log.info("Ocurrio un error al enviar el correo")
            return False

        if not self.use_executable:
            log.info("Envio de correo via SMTPLib")
            try:
                msg = MIMEMultipart()
                msg['From'] = self.email_from
                msg['To'] = ','.join(self.email_to)
                msg['cc'] = ','.join(self.email_cc)
                msg['cco'] = ','.join(self.email_cco)
                msg['Subject'] = subject

                # Integrando
                msg.attach(MIMEText(body, 'html', 'utf-8'))

                # Armando lista de correos
                emails = self.email_to + self.email_cc + self.email_cco

                # Ingresando adjunto
                if attachment_files:
                    for filename in attachment_files.split(";"):
                        attachment = open(filename, 'rb')
                        part = MIMEBase('application', 'octet-stream')
                        part.set_payload((attachment).read())
                        encoders.encode_base64(part)
                        part.add_header('Content-Disposition', "attachment; filename= " + os.path.basename(filename))
                        msg.attach(part)

                # Enviando
                text = msg.as_string()

                if not self.secure_mode:
                    # Usado en el relay smtp telefonica robotizacion
                    server = smtplib.SMTP(host=self.host, port=self.port)
                    if self.use_auth:
                        # Habilitado si es que en algun momento se necesita autenticar
                        server.login(self.user, self.password)
                else:
                    # Habilitado para conexiones seguras en el futuro
                    if self.secure_mode == "SSL":
                        server = smtplib.SMTP_SSL(host=self.host, port=self.port)
                    if self.secure_mode == "TLS":
                        server = smtplib.SMTP(host=self.host, port=self.port)
                        server.starttls()

                    if self.use_auth:
                        server.login(self.user, self.password)

                server.sendmail(self.email_from, emails, text)
                server.quit()
            except Exception as e:
                log.info(e, exc_info=True)
                log.info("Ocurrio un error al enviar el correo")
                return False
        else:
            log.info("Envio de correo via executable")
            try:
                path = os.path.dirname(__file__)
                program = os.path.join(path, 'bin', 'mail.exe')

                attachment = ''
                if attachment_files:
                    attachment = attachment_files

                arguments = self.user + '|' + self.password + '|'
                arguments += self.host + '|' + self.port + '|'
                arguments += self.email_from + '|' + ";".join(self.email_to) + '|' + ";".join(self.email_cc) + '|'
                arguments += body + '|' + subject + '|' + attachment
            except IndexError as e:
                log.info(e, exc_info=True)
                log.error("Uno de los campos email no fue encontrado")
                return False

            try:
                subprocess.call([program, arguments])
            except Exception as e:
                log.error(f"Ocurrio un error al ejecutar:{program}: {e}", exc_info=True)
                return False

        return True


def notifica_exception(exception=None, notifica=None, lista_exceptions_bl=[], subject_replace=None, body_replace=None):
    """Función que notifica todas las excepciones. Si alguna se encuentra en la lista blacklist, no se notifica."""
    if not subject_replace:
        subject_replace = "Error"
    if not body_replace:
        body_replace = "IDS000-Ocurrio un error generico en el proceso: "

    if str(type(exception).__name__).startswith("ID"):
        if type(exception).__name__ not in lista_exceptions_bl:
            notifica.enviar_correo({'[TEXTO]': subject_replace}, {'[TEXTO]': str(exception)})
    else:
        notifica.enviar_correo({'[TEXTO]': subject_replace}, {'[TEXTO]': body_replace + str(exception)})


def timed(func):
    """Decorador para loggear el tiempo de ejecucion de una funcion.

    Args:
        func (object): funcion que sera decorada

    Returns:
        wrapper: funcion decorada
    """
    func_name = func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs):

        log.info("############### Inicio: {} ###############".format(func_name))

        start = time.time()
        result = func(*args, **kwargs)
        duration = time.time() - start

        log.info("## Tiempo transcurrido: {}".format(round(duration, 2)))
        log.info("############### Fin: {} ###############".format(func_name))

        return result

    return wrapper
