#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of Pymetrick.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

try:
    from pymetrick.version import __author__, __version__, __license__, __copyright__
except ImportError:
    raise ImportError(
        'Failed to find (autogenerated) version.py '
    )

__date__ = '2012-09-21'
__credits__ = ''
__text__ = 'Utilidades diversas'
__file__ = 'helpers.py'

#--- CHANGES ------------------------------------------------------------------
# 2012-09-21 v0.01 PL: - First version
# 2017-10-02 v0.43 PL: - Migrar a PYTHON 3.6
# 2019-11-10 v0.50 PL: - import lxml -> if error | update <import imp> deprecated for <import importlib> 
#                         + update_list_of_dict()
import sys
# sin bytecode
sys.dont_write_bytecode = True
import os
import importlib
import importlib.util
import string
import re
import csv
import unicodedata
import hashlib
import locale
import logging
import logging.handlers
import codecs
import urllib
import base64
from itertools import permutations       # combinations y permutations
from datetime import date, time, datetime, timedelta
from time import localtime,mktime
from decimal import Decimal
from random import sample, choice
try:
    from lxml import etree
except:
    pass

import configparser  # python 3

try:
    import simplejson as json
except:
    import json
    
from pymetrick.common import *

long = int

'''
DEBUG - debug message
INFO - info message
WARNING - warn message
ERROR - error message
CRITICAL - critical message
'''
LOG_LEVEL_VALUE = {'DEBUG':10,'INFO':20,'WARNING':30,'ERROR':40,'CRITICAL':50}
if str(os.environ.get('PYMETRICK_LOG_LEVEL',None)).upper() in LOG_LEVEL_VALUE:
    LOG_LEVEL = LOG_LEVEL_VALUE[str(os.environ.get('PYMETRICK_LOG_LEVEL')).upper()]
else:
    LOG_LEVEL = 30
LOG_FILENAME = '-'.join([os.path.abspath(__file__).split(os.sep)[len(os.path.abspath(__file__).split(os.sep))-1],])[:-3]
LOG = logging.getLogger(LOG_FILENAME)

if 'LD_LIBRARY_PATH' in list(os.environ.keys()):
    # CGI environment
    sys.stdout = sys.stderr
    logging.basicConfig(stream = sys.stderr, level=LOG_LEVEL, format='%(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s')
else:
    # not CGI environment
    logging.basicConfig(stream=sys.stderr)
    hdlr = logging.handlers.RotatingFileHandler(filename=LOG_FILENAME+'.log',mode='a', encoding='utf-8', maxBytes=1048576, backupCount=3)
    formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s')
    hdlr.setFormatter(formatter)
    LOG.addHandler(hdlr)
    LOG.setLevel(LOG_LEVEL)

# Gestion de rendimiento #######################################################################################


# Gestion de TIMESTAMP #########################################################################################

# Tratamiento de hora para producir timestamp y su conversion desde timestamp a hora normalizada
def datetime2timestamp(**kwargs):
    '''Realiza conversion de 2014-02-04 12:21:27 a 1391512887
       puede calcular una fecha si incorporamos uno o varios parametros
       @Parameters
       <datetime> is object datetime or None, default value datetime.utcnow()
       <days> for add days to datetime
       <seconds> for add seconds to datetime
       <microseconds> for add microseconds to datetime
       <milliseconds> for add milliseconds to datetime
       <minutes> for add minutes to datetime
       <hours> for add hours to datetime
       <weeks> for add weeks to datetime
       @Return
       <integer>
    '''
    try:
        __datetime__ = None
        __days__ = 0
        __seconds__ = 0
        __microseconds__ = 0
        __milliseconds__ = 0
        __minutes__ = 0
        __hours__ = 0
        __weeks__ = 0
        if len(kwargs)>0:
            if 'datetime' in kwargs:
                __datetime__ = kwargs['datetime']
            if 'days' in kwargs:
                __days__ = kwargs['days']
            if 'seconds' in kwargs:
                __seconds__ = kwargs['seconds']
            if 'microseconds' in kwargs:
                __microseconds__ = kwargs['microseconds']
            if 'milliseconds' in kwargs:
                __milliseconds__ = kwargs['milliseconds']
            if 'minutes' in kwargs:
                __minutes__ = kwargs['minutes']
            if 'hours' in kwargs:
                __hours__ = kwargs['hours']
            if 'weeks' in kwargs:
                __weeks__ = kwargs['weeks']
        if not isinstance(__datetime__,(datetime,)):
            # Si no se le indica una fecha como parametro, obtendra la fecha del sistema
            __datetime__ = datetime.utcnow()
            # datetime.now()
        return int(mktime( (__datetime__ + timedelta( days=__days__, seconds=__seconds__, microseconds=__microseconds__, milliseconds=__milliseconds__, minutes=__minutes__, hours=__hours__, weeks=__weeks__)).timetuple()))
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (str(e),tb.tb_lineno))

def timestamp2datetime(__timestamp__=None):
    '''Realiza conversion de 1391512887 a 2014-02-04 12:21:27'''
    try:
        if __timestamp__:
            LOG.debug('timestamp2datetime %s' % datetime.fromtimestamp(int(__timestamp__)))
            if isinstance(__timestamp__,(int,float)):
                #LOG.debug('timestamp2datetime %s' % datetime.fromtimestamp(int(__timestamp__)))
                return(datetime.fromtimestamp(int(__timestamp__)))
            
        return datetime(1, 1, 1, 0, 0)
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (str(e),tb.tb_lineno))

cadena = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]

def encodeDateTime2String(isValue):
    '''codificar un formato date-time yyyymmddsssss a string en base 36 segun list 'cadena'
       @parameters
           isValue con formato string p.ejemplo: 2016031736936 -> PQ5H8RXK
       @return
           devuelve un string en base 36 p.ejemplo PQ5H8RXK
    '''
    try:
        stringResult = ""
        nTotal = 0
        if type(isValue).__name__ == 'datetime':
            _decimal_ = int(datetime.strftime(isValue,"%Y%m%d"))*100000 + int(datetime.strftime(isValue,"%H"))*3600 + int(datetime.strftime(isValue,"%M"))*60 + int(datetime.strftime(isValue,"%S"))
        else:
            _decimal_ = int(isValue)
        while _decimal_>0:
            if _decimal_>36:
                stringResult = cadena[_decimal_%36] + stringResult
                _decimal_ = int(_decimal_/36)
            else:
                stringResult = cadena[_decimal_] + stringResult
                _decimal_ = 0
        return stringResult
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def decodeString2DateTime(isString,dtime=None):
    '''decodificar un formato string en base 36 a date-time yyyymmddsssss segun list 'cadena'
       @parameters
           isString con formato string p.ejemplo: PQ5H8RXK -> 2016031736936
           dtime si True responde con un string en formato datetime
       @return
           dtime=None responde con un string en formato yyyymmddsssss
           dtime=True responde con un objeto datetime yyyy-mm-dd hh:mm:ss.sssss
    '''
    try:
        nResult = 0
        for n in range(len(isString)):
            nBase = 36
            h = 0
            for m in range(len(isString)-2-n):
                h = h + 1
                nBase = nBase*36
            if n+1<len(isString):
                nResult = nResult+((long(cadena.index(isString[n])))*nBase)
            else:
                nResult = nResult+((long(cadena.index(isString[n]))))
        stringResult = str(nResult)
        if dtime:
            y = stringResult[0:4]
            _y = int(y)
            m = stringResult[4:6]
            _m = int(m)
            d = stringResult[6:8]
            _d = int(d)
            tt = stringResult[-5:]
            ss = int(tt)
            hh = int(ss/3600)
            ss = ss - (hh*3600)
            mm = int(ss/60)
            ss = ss - (mm*60)
            return datetime(_y,_m,_d,hh,mm,ss)
        else:
            return stringResult
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def decodeString2DateTime2(dt):
    '''decodificar un formato string en base 36 a date-time yyyymmddsssss segun list 'cadena'
       @parameters
           dt con formato string p.ejemplo: PQ5H8RXK -> 2016031736936
       @return
           obj datetime yyyy-mm-dd hh:mm:ss
    '''
    d = decodeString2DateTime(dt)
    t = int(d[-5:])
    h = int(t/3600)
    p = 'AM'
    m = int((t-(h*3600))/60)
    s = int(t-(h*3600)-(m*60))
    if t>=(12*3600):
        p = 'PM'
    if t>=(13*3600):
        h -= 12
    return datetime.strptime('{0}{1:02d}:{2:02d}:{3:02d}{4}'.format(d[:8],h,m,s,p), '%Y%m%d%I:%M:%S%p')

# Gestion de REGION ###########################################################################################

# Formato de fecha local para un pais
def localeES():
    # asignacion locale española
    d = datetime.datetime(2000,12,30)
    locale.setlocale(locale.LC_TIME, "sp")
    # 'Spanish_Spain.1252'
    fecha.strftime("%x")
    #'30/12/2000'

    # locale ingles
    locale.setlocale(locale.LC_TIME, "en")
    #'English_United States.1252'
    fecha.strftime("%x")
    # '12/30/2000'

    print (time.localtime())
    # time.struct_time(tm_year=2014, tm_mon=2, tm_mday=4, tm_hour=20, tm_min=1,
    # tm_sec=23, tm_wday=1, tm_yday=35, tm_isdst=0)

# Gestion de FICHEROS CONFIGURACION ###########################################################################

def read_config(file, section, title):
    '''Leer datos de ficheros de configuracion
    <section> se identifica en fichero como [...]
    y contenido en la <section> se encuentra <title...  :>
    '''
    config = ConfigParser.ConfigParser()
    config.read(file)
    Value = config.get(section,title)
    return Value

def save_config(file, section, title,value):
    '''Guardar o modificar datos en fichero de configuracion
    <section> se identifica en fichero como [...]
    y contenido en la <section> se encuentra <title    ...  :>
    '''
    config = ConfigParser.ConfigParser()
    config.add_section(section)
    config.set(section,title,value)
    # write to screen
    config.write(sys.stdout)
    f = open(file, 'w')
    config.write(f)
    f.close()
    return ''

# Gestion de FICHEROS SITEMAP.XML ###########################################################################

def sitemap(file=None, url_text=None, values=list()):
    '''Create SITEMAP.XML
       @Parameters
       file : path to write sitemap.xml without name of file.
       url_tex : static url text url to write on <loc> tag.  example url_text='https://pymetrick.org/product_item?sku='
       values : list of dinamic url text to write on <loc> tag
       @Return
       True if write file OK
       False if error
    '''
    if ("lxml" in sys.modules):
        generated_on = str(datetime.now())
        l_timestamp = generated_on.split()
        l_timestamp[1] = l_timestamp[1].split('.')[0]
        t_timestamp = 'T'.join(l_timestamp)+'+00:00'

        root = etree.XML('''\
        <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
              http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
        </urlset>
        ''')
        tree = etree.ElementTree(root)

        if url_text is None:
            url_text = ''

        if file is None:
            file = 'sitemap.xml'
        else:
            file += '/sitemap.xml'

        for n in range(len(values)):
            #root.append( etree.Element("urlset"))
            url = etree.SubElement( root, "url" )
            loc = etree.SubElement(url, "loc")
            lastmod = etree.SubElement(url, "lastmod")
            changefreq = etree.SubElement(url, "changefreq")
            priority = etree.SubElement(url, "priority")
            loc.text = "{0}{1}".format(url_text,values[n])
            lastmod.text = t_timestamp
            changefreq.text =  'weekly'
            priority.text = '0.50'

        all = b'''<?xml version="1.0" encoding="UTF-8"?>'''+etree.tostring(root)
        all = bytes2str(all.replace(b'\n',b''))

        try:
            if file is not None:
                with open(file,'w',encoding = 'utf-8') as f:
                    f.write(all)
                return True
            else:
                return False
        except Exception as e:
            tb = sys.exc_info()[2]
            LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
            return False
    else:
        LOG.error('lxml not in sys.modules !!!')
        return False


# Gestion de TEXTOS ###########################################################################################

def bytes2str(text=None,encoding=None):
    '''Convierte un tipo 'bytes' en 'str'-unicode,
       @Arguments:
           text  bytes|list|tuple|dict|int|float with bytes
           keys of dict not changed
       @Returns a str|list|tuple|dict|int|float all with bytes-->str
    '''
    try:
        if text is not None:
            obj_text = ''
            if encoding is None:
                encoding = sys.getdefaultencoding()
            if isinstance(text,(bytes,bytearray)):
                obj_text = text
                obj_text=obj_text.decode(encoding,errors="replace")
            elif isinstance(text,(list,tuple)):
                obj_text = list(text)
                for n in range(len(obj_text)):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n]=bytes2str(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n]=bytes2str(obj_text[n])
                    elif isinstance(obj_text[n],(bytes,bytearray)):
                        obj_text[n]=obj_text[n].decode(encoding,errors="replace")
                return obj_text.copy()
            elif isinstance(text,(dict,)):
                obj_text = text.copy()
                for n in list(obj_text.keys()):
                    if isinstance(obj_text[n],(list,tuple)):
                        # if value is a list or tuple
                        obj_text[n]=bytes2str(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        # if value is a dict
                        obj_text[n]=bytes2str(obj_text[n])
                    if isinstance(n,(bytes,bytearray)):
                        # if key is bytes
                        m = n.decode(encoding,errors="replace")
                        obj_text[m] = obj_text[n]
                        # if value is bytes
                        if isinstance(obj_text[m],(bytes,bytearray)):
                            obj_text[m]=obj_text[m].decode(encoding,errors="replace")
                        del obj_text[n]
                        n = m
                    if isinstance(obj_text[n],(bytes,bytearray)):
                        # if value is bytes
                        obj_text[n]=obj_text[n].decode(encoding,errors="replace")
                return obj_text.copy()
            elif isinstance(text,(str,)):
                obj_text = text
            elif isinstance(text,(int,float)):
                obj_text = text
            return obj_text
        else:
            return None
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def str2bytes(text=None,encoding=None):
    '''Convierte un type 'str'-unicode en 'bytes',
       Arguments:
           text  str|list|tuple|dict|int|float
           keys of dict not changed
       Returns a bytes|list|tuple|dict|int|float all with str-->bytes
    '''
    try:
        if text is not None:
            obj_text = ''
            if encoding is None:
                encoding = sys.getdefaultencoding()
            if isinstance(text,(str,)):
                obj_text = text
                obj_text = obj_text.encode(encoding,errors="replace")
            elif isinstance(text,(list,tuple)):
                obj_text = list(text)
                for n in range(len(obj_text)):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n] = str2bytes(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n] = str2bytes(obj_text[n])
                    elif isinstance(obj_text[n],(str,)):
                        obj_text[n] = obj_text[n].encode(encoding,errors="replace")
                return obj_text.copy()
            elif isinstance(text,(dict,)):
                obj_text = text.copy()
                for n in list(obj_text.keys()):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n] = str2bytes(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n] = str2bytes(obj_text[n])
                    elif isinstance(obj_text[n],(str,)):
                        obj_text[n]=obj_text[n].encode(encoding,errors="replace")
                return obj_text.copy()
            elif isinstance(text,(bytes,bytearray)):
                obj_text = text
            elif isinstance(text,(int,float)):
                obj_text = text
            return obj_text
        else:
            return None
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def as_text(value):
    '''Only return empty space if value is NULL'''
    if value is None:
        return ""
    return str(value)

def isNumber(sNum):
    '''Comprueba si todos los elementos de un string son numero y operadores
       @parameters
          sNum = string
       @return
          int|float   <-- if it's rigth
          None        <-- if it's wrong
    '''
    try:
        _type = type(sNum).__name__
        if isinstance(sNum,(int,float)):
            sNum = str(sNum)
        n = ['0','1','2','3','4','5','6','7','8','9','+','-','.',',']
        s = ''
        sNum = sNum.strip()
        if len(sNum)>0:
            for m in sNum:
                if m in n:
                    s += m
            if len(s) == len(sNum) and s.count('+')<2 and s.count('-')<2 and s.count(',')<2 and s.count('.')<2:
                if (s.count('+')==1 and s[0]=='+' and s.count('-')==0) or (s.count('-')==1 and s[0]=='-' and s.count('+')==0) or (s.count('-')==0 and s.count('+')==0):
                    if (s.count(',')==1 and s.count('.')==0) or (s.count('.')==1 and s.count(',')==0):
                        #return float(s.replace(',','.').replace('+',''))
                        return True
                    if  (s.count(',')==0 and s.count('.')==0):
                        #return int(s.replace('+',''))
                        return True
                return False
        else:
            return False
    except Exception as e:
        print(e)
        return False
    
def append_str(*args):
    '''Devuelve un solo string incorporando todos lo valores
       Arguments:
           *args list of str|bytes
           Si es bytes --> str
           Si es str   --> str
       Returns a str
    '''
    try:
        text = ''
        if len(args)>0:
            for n in args:
                if isinstance(n,(str,)):
                    text += n
                elif isinstance(n,(bytes,bytesarray)):
                    text += n.decode()
                elif isinstance(n,(list,tuple,dict)):
                    n = bytes2str(n)
                    text += json.dumps(n, default=to_json)
                elif isinstance(n,(int,float)):
                    text += str(n)
        return text
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))


def get_keywords(text=None, exclusion_list=list()):
    '''Devuelve una lista de palabras de un texto ordenado por el numero de apariciones
       permite una lista de palabras a excluir del analisis
    '''
    try:
        keywords = []
        list_of_words = re.split('\W+', text )

        if len(exclusion_list)>0:
            for exclusion_word in exclusion_list:
                try:
                    list_of_words.remove (exclusion_word)
                except ValueError:
                    pass
        words_count = []
        while list_of_words != []:
            word = list_of_words[0]
            words_count.append( (list_of_words.count( word ), word ) )
            salir = False
            while (not salir):
                try:
                    list_of_words.remove( word )
                except (ValueError, IndexError):
                    salir = True

        words_count.sort( key=lambda word_count : word_count[0], reverse = True )

        for word in words_count:
            keywords.append( word[1] )

        return keywords
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def do_wordcount(text,word):
    '''Cuantas veces una palabra aparece en un texto'''
    try:
        if text.__len__() and word.__len__():
            return len(re.findall(word, text))
        else:
            return 0
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def strings_differ(string1, string2):
    '''Esta funcion devuelve verdadero si la cadena indicada son diferentes y Falso si iguales'''
    try:
        if len(string1) != len(string2):
            return True

        invalid_bits = 0
        for a, b in zip(string1, string2):
            invalid_bits += a != b

        return invalid_bits != 0
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def diff2list(*args):
    '''Separa una lista de cadenas alfanumericas, devolviendo un list con [0] Palabras comunes en las cadenas, [1] Lista con palabras diferentes de cada cadena
    @args   ['Talla T12','Talla T23','Talla T35']
    @return ['Talla', ['T12', 'T23', 'T35']]
    @error  Si *args contiene numeros, [0] = ''
    '''
    try:
        if type(args).__name__ in ['list','tuple']:
            s = list()
            ss = list()
            if len(args)>1:
                for n in range(1,len(args)):
                    if type(args[n-1]).__name__ in ['str','unicode'] and type(args[n]).__name__ in ['str','unicode']:
                        s.append(' '.join(list(set(args[n-1].split())-set(args[n].split()))))
                    else:
                        break
                if type(args[0]).__name__ in ['str','unicode'] and type(args[n]).__name__ in ['str','unicode']:
                    s.append(' '.join(list(set(args[n].split())-set(args[0].split()))))
                if len(s)>0:
                    ss = ' '.join([x for x in args[0].split() if x not in s[0]])
                    return [ss,s]
                else:
                    return ['',args]
    except Exception as e:
        return ['',args]

def do_truncate(s, length=255, killwords=False, end='...'):
    '''Return a truncated copy of the string. The length is specified
       with the first parameter which defaults to ``255``. If the second
       parameter is ``true`` the filter will cut the text at length. Otherwise
       it will try to save the last word. If the text was in fact
       truncated it will append an ellipsis sign (``"..."``). If you want a
       different ellipsis sign than ``"..."`` you can specify it using the
       third parameter.

       {{ mytext|truncate(300, false, '&raquo;') }}
       truncate mytext to 300 chars, don't split up words, use a
       right pointing double arrow as ellipsis sign.
    '''
    try:
        if len(s) <= length:
            return s
        elif killwords:
            return s[:length] + end
        words = s.split(' ')
        result = []
        m = 0
        for word in words:
            m += len(word) + 1
            if m > length:
                break
            result.append(word)
        result.append(end)
        return b' '.join(result)
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def stem(words=None,language='ES'):
    '''Con origen en una lista de palabras, se transforman en singular y plural,
       obteniendo una list con diferentes combinaciones - only spanish (ES)
    '''
    try:
        if words is not None and language == 'ES':
            # convertir bytes a str
            if type(words).__name__ == 'list':
                words = bytes2str(words)
                words = list(map(lambda x:x.lower(),words))
                # buscar por nombre de producto
                # comprobar si palabra plural -> singular o singular->plural
                list_stemming = list()
                words_search = list()
                for n in words:
                    if n.endswith('es'):
                        if n[:-2].endswith('c'):
                            list_stemming.append(''.join([n[:-3],'z']))
                        else:
                            list_stemming.append(n[:-1])
                    elif n.endswith('s'):
                        list_stemming.append(n[:-1])
                    elif n.endswith('z'):
                        list_stemming.append(''.join([n[:-1],'ces']))
                    elif n.endswith(('\xc3\xad','\xc3\xba')):
                        list_stemming.append(''.join([n,'es']))
                    elif n.endswith(('a','e','i','o','u')):
                        list_stemming.append(''.join([n,'s']))
                    else:
                        list_stemming.append(''.join([n,'es']))
                if len(words)>1:
                    # para multiples palabras
                    # permutaciones de elementos, hasta un maximo de 4 palabras
                    for n in permutations(words[:4]):
                        w = list()
                        w.append(n[0])
                        for m in range(1,len(n)):
                            w.append('.+%s' % n[m])
                        words_search.append(''.join(w))   # se guardan permutaciones de la forma palabra1.+palabra2.+palabra3.+palabra4
                    # permutaciones de elementos, hasta un maximo de 4 list_stemming
                    for n in permutations(list_stemming[:4]):
                        w = list()
                        w.append(n[0])
                        for m in range(1,len(n)):
                            w.append('.+%s' % n[m])
                        words_search.append(''.join(w))   # se guardan permutaciones de la forma palabra1.+palabra2.+palabra3.+palabra4
                else:
                    #palabras sencillas
                    words_search.append(words[0])
                    if len(list_stemming)>0:
                        words_search.append(list_stemming[0])
                return words_search
            return []
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

# Tratamiento de variables y textos en base64 #################################################################

def obj_b64encode(text=None):
    '''Convierte los valores de un dict a base64,
       Arguments:
           text  str|list|tuple|dict|int|float
           keys of dict not changed
       Returns a bytes|list|tuple|dict|int|float
    '''
    try:
        if text is not None:
            obj_text = ''
            if isinstance(text,(bytes,)):
                obj_text = text
                obj_text = set_base64(obj_text)
            elif isinstance(text,(list,tuple)):
                obj_text = list(text)
                for n in range(len(obj_text)):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n] = obj_b64encode(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n] = obj_b64encode(obj_text[n])
                    elif isinstance(obj_text[n],(bytes,str)):
                        obj_text[n] = set_base64(obj_text[n])
                return obj_text.copy()
            elif isinstance(text,(dict,)):
                obj_text = text.copy()
                for n in list(obj_text.keys()):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n] = obj_b64encode(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n] = obj_b64encode(obj_text[n])
                    elif isinstance(obj_text[n],(bytes,str)):
                        obj_text[n]=set_base64(obj_text[n])
                return obj_text.copy()
            elif isinstance(text,(bytes,bytearray)):
                obj_text = set_base64(text)
            elif isinstance(text,(int,float)):
                obj_text = text
            return obj_text
        else:
            return None
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def obj_b64decode(text=None):
    '''Convierte base64 en byte,
       @Arguments:
           text  bytes|list|tuple|dict|int|float with bytes
           keys of dict not changed
       @Returns a str|list|tuple|dict|int|float
    '''
    try:
        if text is not None:
            obj_text = ''
            if isinstance(text,(bytes,bytearray)):
                obj_text = text
                obj_text=get_base64(obj_text)

            elif isinstance(text,(list,tuple)):
                obj_text = list(text)
                for n in range(len(obj_text)):
                    if isinstance(obj_text[n],(list,tuple)):
                        obj_text[n]=obj_b64decode(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        obj_text[n]=obj_b64decode(obj_text[n])
                    elif isinstance(obj_text[n],(bytes,bytearray,str)):
                        obj_text[n]=get_base64(obj_text[n])
                return obj_text.copy()
            elif isinstance(text,(dict,)):
                obj_text = text.copy()
                for n in list(obj_text.keys()):
                    if isinstance(obj_text[n],(list,tuple)):
                        # if value is a list or tuple
                        obj_text[n]=obj_b64decode(obj_text[n])
                    elif isinstance(obj_text[n],(dict,)):
                        # if value is a dict
                        obj_text[n]=obj_b64decode(obj_text[n])
                    elif isinstance(obj_text[n],(bytes,bytearray,str)):
                        obj_text[n]=get_base64(obj_text[n])
                return obj_text.copy()
            elif isinstance(text,(str,)):
                obj_text = text
            elif isinstance(text,(int,float)):
                obj_text = text
            return obj_text
        else:
            return None
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def get_base64(text=None, prefix=b'b64.'):
    '''Decode byte like base64 with start prefix 'b64.'
       Transform chars -_ to +/ and add = to the end of byte
    '''
    try:
        # check is bytes o bytearray
        if not isinstance(prefix,(bytes,bytearray)):
            prefix= str2bytes(prefix)
        if not isinstance(text,(bytes,bytearray)):
            text = str2bytes(text)

        if text.strip().startswith(prefix):
            try:
                text = text[len(prefix):]
                if len(text)%4>0:
                    text = text + b"==="[0:4-len(text)%4]
                text = base64.b64decode(text,altchars=b'-_', validate=False)
            except TypeError:
                pass
    except Exception as e:
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
    finally:
        return text

def set_base64(text=None, prefix=b'b64.'):
    '''Encode byte like base64 with start prefix 'b64.'
       Transform chars +/ to -_ and delete all chars = at the end of byte
    '''
    try:
        # check is bytes o bytearray
        if not isinstance(prefix,(bytes,bytearray)):
            prefix= str2bytes(prefix)
        if not isinstance(text,(bytes,bytearray)):
            text = str2bytes(text)
        try:
            text = b''+prefix+base64.b64encode(text,altchars=b'-_').strip(b'=')
        except TypeError:
            pass
    except Exception as e:
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
    finally:
        return text

# Gestion de FICHEROS #########################################################################################

def import_from(uri,absl=False):
    """Importar un modulo dinamicamente"""
    if not absl:
        uri= os.path.normpath(os.path.join(os.path.dirname(__file__),uri))

    path, fname = os.path.split(uri)

    mname, ext = os.path.splitext(fname)

    try:
        spec = importlib.util.find_spec(mname,path)
        if spec is None:
            LOG.error("Import error 0: " + " module not found")
            sys.exit(0)
        toolbox = spec.loader.load_module()
    except (ValueError, ImportError) as e:
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
        sys.exit(0)

def csv2dict(_path_,separator=';'):
    """Como parametro se le indica un fichero csv
       La primera fila debe incluir la clave de cada columna

       @return
       list de dict cotenido de los valores de cada fila
    """
    try:
        outlist = list()
        with open(_path_, mode='r') as infile:
            reader = csv.DictReader(infile, delimiter=separator)

            for rows in reader:
                result = {}
                for column, value in rows.items():
                    result.setdefault(column,value)
                outlist.append(result)
    except Exception as e:
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
        return None
    finally:
        return outlist

def purge(fn, pattern):
    """Delete files in a dir matching pattern
    """
    for f in os.listdir(fn):
        if re.search(pattern, f):
            os.remove(os.path.join(fn, f))

def checksum(filename, chunk_size=65336):
    """Checksum of file"""
    digest = hashlib.md5()
    with open(filename, 'rb') as f:
        [digest.update(chunk) for chunk in iter(lambda: f.read(chunk_size), b"")]
    return digest.hexdigest()

# Gestion de OBJETOS ##########################################################################################
def update_list_of_dict(id_key=None, list_a=dict(), list_b=dict()):
    """Update list of dictionaries <a> with other list of dictionaries <b> related by id key, with equal
       keys or incorporate new keys
    """
    try:
        if id_key is not None:
            # sort dict by id key, descendent
            list_a = sorted(list_a, key=lambda k: k[id_key],reverse=True)
            list_b = sorted(list_b, key=lambda k: k[id_key],reverse=True)
            # Group dictionaries with the same key in a single list
            l = [[list_a[i] if i < len(list_a) else {},list_b[i] if i < len(list_b) else {}] for i in range(len(list_b) if len(list_b)>len(list_a) else len(list_a)) if (i == len(list_a) or list_a[i][id_key]==list_b[i][id_key]) ] 
            # Update list <a> with list <b>
            list_c = [ {**l[i][0],**l[i][1]} for i in range(len(l))]
            return list_c
        else:
            return list()
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def sort_dict(d):
    '''Ordena claves de un dict
    @parameters
       dict
    @return
       str
    '''
    try:
        keys = list(d.keys())
        keys.sort()
        return "{" + ", ".join(["%r: %r" % (k, d[k]) for k in keys]) + "}"
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))

def remove_empty(d):
    '''Elimina los valores nulos de un list o dict'''
    if d:
        empty_check = True   # valor inicial de comprobacion
    else:
        empty_check = False
    while empty_check:
        empty_check = False
        if isinstance(d,(dict,)):
            for k in list(d.keys()):
                try:
                    if not d[k]:
                        del d[k]
                        # solicitar nueva comprobacion
                        empty_check = True
                    else:
                        if isinstance(d[k],(dict,list)):
                            d[k] = remove_empty(d[k])
                            if not d[k]:
                                # solicitar nueva comprobacion
                                empty_check = True
                except ValueError:
                    pass
        elif isinstance(d,(list,)):
            for k in d:
                try:
                    if not k:
                        d.remove(k)
                        # solicitar nueva comprobacion
                        empty_check = True
                    else:
                        if isinstance(k,(dict,list)):
                            k = remove_empty(k)
                            if not k:
                                # solicitar nueva comprobacion
                                empty_check = True
                except ValueError:
                    pass
    return d

def cartesian_product(*args, repeat=1):
    ''' Producto cartesiano de lists
        @Parameters
        ('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
        (['A','B','C','D'], ['x','y']) --> ('A','x'), ('A','y'), ('B','x'), ('B','y'), ('C','x'), ('C','y'), ('D','x'), ('D','y')
        (range(2), repeat=3) --> 000 001 010 011 100 101 110 111
        @Returns
        generator
    '''
    try:
        pools = [tuple(pool) for pool in args] * repeat
        result = [[]]
        for pool in pools:
            result = [x+[y] for x in result for y in pool]
        for prod in result:
            yield tuple(prod)
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))


def object_description(object):
    """ Produce a human-consumable text description of ``object``,
        usually involving a Python dotted name. For example:

       >>> object_description(None)
       u'None'
       >>> from xml.dom import minidom
       >>> object_description(minidom)
       u'module xml.dom.minidom'
       >>> object_description(minidom.Attr)
       u'class xml.dom.minidom.Attr'
       >>> object_description(minidom.Attr.appendChild)
       u'method appendChild of class xml.dom.minidom.Attr'
       >>>

       If this method cannot identify the type of the object, a generic
       description ala ``object <object.__name__>`` will be returned.

       If the object passed is already a string, it is simply returned.  If it
       is a boolean, an integer, a list, a tuple, a set, or ``None``, a
       (possibly shortened) string representation is returned.
    """
    if isinstance(object, string_types):
        return text_(object)
    if isinstance(object, integer_types):
        return text_(str(object))
    if isinstance(object, (bool, float, type(None))):
        return text_(str(object))
    if isinstance(object, set):
        return shortrepr(object, '}')
    if isinstance(object, tuple):
        return shortrepr(object, ')')
    if isinstance(object, list):
        return shortrepr(object, ']')
    if isinstance(object, dict):
        return shortrepr(object, '}')
    module = inspect.getmodule(object)
    if module is None:
        return text_('object %s' % str(object))
    modulename = module.__name__
    if inspect.ismodule(object):
        return text_('module %s' % modulename)
    if inspect.ismethod(object):
        oself = getattr(object, '__self__', None)
        if oself is None: # pragma: no cover
            oself = getattr(object, 'im_self', None)
        return text_('method %s of class %s.%s' %
                     (object.__name__, modulename,
                      oself.__class__.__name__))

    if inspect.isclass(object):
        dottedname = '%s.%s' % (modulename, object.__name__)
        return text_('class %s' % dottedname)
    if inspect.isfunction(object):
        dottedname = '%s.%s' % (modulename, object.__name__)
        return text_('function %s' % dottedname)
    return text_('object %s' % repr(object))

def to_json(obj):
    '''Transforma elementos con json.dumps
       Decimal en float
       byte en str
       @how  json.dumps(obj, default=to_json)
    '''
    if isinstance(obj, Decimal):
        obj = float(obj)
    if isinstance(obj, float):
        return float(format(obj, '0.4f'))
    if isinstance(obj, (bytes, bytearray)):
        obj = bytes2str(obj)
        return obj
    raise TypeError(repr(obj) + ' is not JSON serializable')

def list2dict(*args,**kwargs):
    '''A partir de dos list se combinan para generar un dict,
       @parameters :
           keys = list()
           values = list()
       @return
           dict
    '''
    newDict = dict()
    try:
        if len(args)>0:
            keys = args[0]
            values = args[1]
        elif len(kwargs)>0:
            keys = kwargs['keys']
            values = kwargs['values']
        if (type(keys).__name__ in ['list','tuple']) and (type(values).__name__ in ['list','tuple']):
            newDict = dict(zip(keys, values))
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error('<%s> in line %s !!!' % (repr(e),tb.tb_lineno))
    finally:
        return newDict

def multikey2dict(data_dict,multikey_string):
    """Actualiza los datos de un dict o list - que puede estar compuesto a su vez por LIST y DICT hasta una profundidad de 4 -
       y que existe previamente, a partir de un dict con claves en formato <string-multikey>

       El formato <string-multikey> es un string compuesto de varias claves separadas entre si por puntos
       @example work
                      multikey2dict({ 'address':[ { 'number' : 0 }]}  , { address.0.number : 41} )

                      --> { 'address':[ { 'number' : 41 }]}

       <string-multikey> es un formato de clave mas apropiado para html form o aplanar dict
       Los string hacen referencia a claves dict y los numeros al orden en list.
    """
    a = dict()
    for n in list(multikey_string.keys()):
        g = n.split('.')
        v = multikey_string[n]
        if isinstance(data_dict,(dict,)):
            # es un dict con clave g[0]
            if len(g)>1:
                if isinstance(data_dict[g[0]],(list,)):
                    # el siguiente valor clave corresponde a un int = list order
                    if isinstance(data_dict[g[0]][int(g[1])],(list,)):
                        if len(g)>2:
                            data_dict[g[0]][int(g[1])][int(g[2])] = v
                        else:
                            data_dict[g[0]][int(g[1])] = v
                    elif isinstance(a[g[0]][int(g[1])],(dict,)):
                        if len(g)>2:
                            data_dict[g[0]][int(g[1])][g[2]] = v
                        else:
                            data_dict[g[0]][int(g[1])] = v
                else:
                    # el siguiente valor clave corresponde a un string = dict key
                    if isinstance(data_dict[g[0]][g[1]],(list,)):
                        if len(g)>2:
                            data_dict[g[0]][g[1]][int(g[2])] = v
                        else:
                            data_dict[g[0]][g[1]] = v
                    elif isinstance(a[g[0]][g[1]],(dict,)):
                        if len(g)>2:
                            data_dict[g[0]][g[1]][g[2]] = v
                        else:
                            data_dict[g[0]][g[1]] = v
            else:
                a[g[0]] = v
        else:

            # es una list con clave int(g[0])
            if len(g)>1:
                if isinstance(a[int(g[0])],(list,)):
                    # el siguiente valor clave corresponde a un int = list order
                    if isinstance(a[int(g[0])][int(g[1])],(list,)):
                        if len(g)>2:
                            a[int(g[0])][int(g[1])][int(g[2])] = v
                        else:
                            a[int(g[0])][int(g[1])]
                    elif type(a[int(g[0])][int(g[1])]).__name__ == 'dict':
                        if len(g)>2:
                            a[int(g[0])][int(g[1])][g[2]] = v
                        else:
                            a[int(g[0])][int(g[1])] = v
                else:
                    # el siguiente valor clave corresponde a un string = dict key
                    if isinstance(a[int(g[0])][g[1]],(list,)):
                        if len(g)>2:
                            a[int(g[0])][g[1]][int(g[2])] = v
                        else:
                            a[int(g[0])][g[1]] = v
                    elif isinstance(a[int(g[0])][g[1]],(dict,)):
                        if len(g)>2:
                            a[int(g[0])][g[1]][g[2]] = v
                        else:
                            a[int(g[0])][g[1]] = v

            else:
                a[int(g[0])] = v
    return a

def dict2multikey(data_dict):
    """Transforma un dict o list - que puede estar compuesto por LIST y DICT hasta una profundidad de 4 -
       a un dict con claves en formato <string-multikey>

       El formato <string-multikey> es un string compuesto de varias claves separadas entre si por puntos

       @example work
                      dict2multikey({ 'address':[ { 'number' : 41 }]} )

                      --> { address.0.number : 41 }

       que es un formato mas apropiado para realizar html form
       Los string hacen referencia a claves dict y los numeros al orden en list.
    """
    ll = dict()
    if type(data_dict).__name__ == 'dict':
        # es un dict
        for n in list(data_dict.keys()):
            if isinstance(data_dict[n],(dict,)):
                for m in list(data_dict[n].keys()):
                    if isinstance(data_dict[n][m],(dict,)):
                        for l in list(data_dict[n][m].keys()):
                            if not isinstance(data_dict[n][m][l],('dict','list')):
                                ll['.'.join([n,m,l])] = data_dict[n][m][l]

                    elif isinstance(data_dict[n][m],(list,)):
                        for l in range(len(data_dict[n][m])):
                            if not isinstance(data_dict[n][m][l],('dict','list')):
                                ll['.'.join([n,m,str(l)])] = data_dict[n][m][l]
                    else:
                        ll['.'.join([n,m])] = data_dict[n][m]

            elif isinstance(data_dict[n],(list,)):
                for m in range(len(data_dict[n])):
                    if isinstance(data_dict[n][m],(dict,)):
                        for l in list(data_dict[n][m].keys()):
                            if not isinstance(data_dict[n][m][l],('dict','list')):
                                ll['.'.join([n,str(m),l])] = data_dict[n][m][l]
                    elif isinstance(data_dict[n][m],(list,)):
                        for l in range(len(data_dict[n][m])):
                            if not isinstance(data_dict[n][m][l],('dict','list')):
                                ll['.'.join([n,str(m),str(l)])] = data_dict[n][m][l]
                    else:
                        ll['.'.join([n,repr(m)])] = data_dict[n][m]
            else:
                ll[n] = data_dict[n]
    else:
        # es un list
        for n in range(len(data_dict)):
            if type(data_dict[n]).__name__ == 'dict':
                for m in list(data_dict[n].keys()):
                    if type(data_dict[n][m]).__name__ == 'dict':
                        for l in list(data_dict[n][m].keys()):
                            if type(data_dict[n][m][l]).__name__ not in ['dict','list']:
                                ll['.'.join([str(n),m,l])] = data_dict[n][m][l]

                    elif type(data_dict[n][m]).__name__ == 'list':
                        for l in range(len(data_dict[n][m])):
                            if type(data_dict[n][m][l]).__name__ not in ['dict','list']:
                                ll['.'.join([str(n),m,str(l)])] = data_dict[n][m][l]
                    else:
                        ll['.'.join([str(n),m])] = data_dict[n][m]

            elif type(data_dict[n]).__name__ == 'list':
                for m in range(len(data_dict[n])):
                    if type(data_dict[n][m]).__name__ == 'dict':
                        for l in list(data_dict[n][m].keys()):
                            if type(data_dict[n][m][l]).__name__ not in ['dict','list']:
                                ll['.'.join([str(n),str(m),l])] = data_dict[n][m][l]

                    elif type(a[n][m]).__name__ == 'list':
                        for l in range(len(data_dict[n][m])):
                            if type(data_dict[n][m][l]).__name__ not in ['dict','list']:
                                ll['.'.join([str(n),str(m),str(l)])] = data_dict[n][m][l]
                    else:
                        ll['.'.join([str(n),m])] = data_dict[n][m]
            else:
                ll[str(n)] = data_dict[n]

    return ll

# Gestion de NUMEROS ###########################################################################################

def do_round(value, precision=0, method='common'):
    """Round the number to a given precision. The first
       parameter specifies the precision (default is ``0``), the
       second the rounding method:

       - ``'common'`` rounds either up or down
       - ``'ceil'`` always rounds up
       - ``'floor'`` always rounds down

       If you don't specify a method ``'common'`` is used.

          {{ 42.55|round }}
          -> 43.0
          {{ 42.55|round(1, 'floor') }}
          -> 42.5

       Note that even if rounded to 0 precision, a float is returned. If
       you need a real integer, pipe it through `int`:


       {{ 42.55|round|int }}
       -> 43
    """
    if not method in ('common', 'ceil', 'floor'):
        raise FilterArgumentError('method must be common, ceil or floor')
    if method == 'common':
        return round(value, precision)
    func = getattr(math, method)
    return func(value * (10 ** precision)) / (10 ** precision)

# Gestion de Codigos de Barras ####################################################################################

def check_EAN(EAN = None, withChecksum = False ):
    return check_GTIN(GTIN = EAN, withChecksum = withChecksum)

def check_GTIN(GTIN = None, withChecksum = False ):
    '''Comprueba si EAN es correcto o completa digito de control,
       Posibles codigos 8, 12, 13 y 14
       @parameters :
           EAN = Text
           withChecksum = Boolean  True devuelve un EAN con un digito de control calculado, False solo comprueba el EAN
       @return
           EAN completo si todo es correcto o calculado y Text vacío si no es correcto.
    '''
    try:
        if isinstance(GTIN,(int,)):
            GTIN = repr(GTIN)
            
        if GTIN is not None and len(GTIN)>0 and isinstance(GTIN,(str,bytes,bytearray)) and all(i.isdigit() for i in GTIN) :
            ean = GTIN.strip()
            if (len(ean)==8 or len(ean)==12 or len(ean)==13 or len(ean)==14) and not withChecksum:
                ean = ean[:-1]
            # cambiar orden del texto izq->dcha a dcha->izq
            ean_list = list()
            for n in ean:
                ean_list.append(n)
            i_sum = 0
            p_sum = 0
            pos = 1
            # extraer valores de cola LIFO y calcular valores pares e impares
            # el sumatorio de numeros impares se multiplica por 3
            # el sumatorio de numeros pares no sufre modificacion
            while (len(ean_list)>0):
                n = ean_list.pop()
                if pos%2==0:
                    p_sum += int(n)
                else:
                    i_sum += int(n)
                pos += 1
            # Calcular total de sumatorio_impar * 3 + sumatorio_par
            i_sum = i_sum * 3
            t_sum = i_sum+p_sum
            # modulo 10 del total
            ean_mod = t_sum%10
            # 10 - modulo 10 del total
            
            check_digit = (repr(10-ean_mod)).strip() if ean_mod>0 else '0';
            if not withChecksum:
                # solo comprobar el digito de control
                if GTIN[-1:] == check_digit:
                    return GTIN
                else:
                    LOG.debug('''GTIN %s not valid checksum''' % GTIN)
                    return ''
            else:
                # completar digito de control para GTIN-8,GTIN-12,GTIN-13,GTIN-14
                if (len(ean)==7 or len(ean)==11 or len(ean)==12 or len(ean)==13) and withChecksum:
                    GTIN = ''.join([GTIN,check_digit])
            return GTIN
        else:
            return ''
    except Exception as e:
        LOG.error('Error en check_GTIN %s en linea %s' % (e,format(sys.exc_info()[-1].tb_lineno)))

# Gestion de Validaciones ####################################################################################

def validate_email(email):
    return re.search("^[\W\w\.\+\-]+\@[\W\w\.]+\.[A-Za-z0-9]{2,}$", email) != None


if __name__ == '__main__' :
    print ('''copyright {0}'''.format( __copyright__))
    print ('''license {0}'''.format( __license__))
    print ('''version {0}'''.format( __version__))
    if len(sys.argv) < 2:
        sys.stderr.write("for help use -h o --help")
    elif sys.argv[1]=='-h' or sys.argv[1]=='--help':
        print (b'''Utilidades diversas\n
                   datetime2timestamp(fecha o None) genera un entero timestamp que representa la fecha y hora actual o como parametro \n
                   timestamp2datetime(entero) devuelve una fecha a partir de un entero timestamp %s\n
                   Leer datos de un fichero de configuracion read_config("inicio.cfg","USER","Title")\n
                   Guardar datos en un fichero de configuracion save_config("inicio.cfg","SEGURO","Password","NadaDeNada")\n
                   Devuelve lista de palabras de un texto ordenado por el numero de apariciones permite una lista de palabras a excluir del analisis get_keywords(texto,lista_de_palabras_a_excluir)\n
                   Cuentas las veces que una palabra aparece en un texto do_wordcount( text, word)\n
                   A partir de 2 listas se genera un diccionario list2dict(keys,values) keys=('primero','segundo') values=(1,2) %s\n
                   remove_empty({'a': [{}], 'b': ['beta','gamma',{}], 'g': {'1':[],'2':[]}}))
                   remove_empty([['',{}], {'b': [None,None]}, {'g': []}]))
                   remove_empty([{}]))
                   remove_empty({'n':[]}))
                    ''')



