#!/usr/bin/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.

'''Modulo para gestionar datos en MySQL'''

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

__date__ = '2015-06-26'
__credits__ = ''
__text__ = 'Gestion de datos en BBDD'
__file__ = 'sqldb.py'

#--- CHANGES ------------------------------------------------------------------
# 2015-06-26 v0.01 PL: - First version
# 2015-09-10 v0.03 PL: - Adaptado a python 3.x
# 2017-10-02 v0.43 PL: - Migrar a python 3.6


import sys
# sin bytecode
sys.dont_write_bytecode = True
import os
import re
import time
import copy
import logging
import logging.handlers

try:
    from importlib import import_module  # Python 3.4+ 
except ImportError:
    print('''IMPORTLIB not installed''')

RETRY_NUMBER  = 50       # Cuantas veces intenta reconectar
ATTEMPS_SLEEP = 0.300    # Tiempo de espera

BLACKLIST = ["--",";--",";","/*","*/","@@"," @","1=1",
             "char","nchar","varchar","nvarchar",
             "alter","begin","cast","create","cursor","declare","delete","drop","end","exec","execute",
             "fetch","insert","kill","open",
             "select", "sys","sysobjects","syscolumns",
             "table","update","<script","</script"]

SQL_OPERATORS = [
    '=',
    '!=',
    'like',
    'not like',
    'ilike',
    'not ilike',
    'in',
    'not in',
    '<=',
    '>=',
    '<',
    '>',
    ]

# PY3
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)




        
def sanitizeSQL(_string_):
    '''Limpiar datos con los que se realiza algun tipo de acceso a la base de datos'''
    '''_obj_ = re.compile('|'.join([re.escape(n) for n in BLACKLIST]), flags=re.IGNORECASE)
    return _obj_.sub("", _string_) '''
    return _string_

def sqlescape(o = None):

    t = {"\\": "\\\\",
        "\0": "\\0",
        "\r": "\\r",
        "\x08": "\\b",
        "\x09": "\\t",
        "\x1a": "\\z",
        "\n": "\\n",
        "\r": "\\r",
        '"': '\\"',
        "'": "\\'",
        "%": "\\%"
        }
    if isinstance(o,(dict,)):
        for k in list(o.keys()):
            if isinstance(o[k],(dict,list,)):
                 o[k] = copy.deepcopy(sqlescape(o[k]))
            elif isinstance(o[k],(str,bytes,)):
                 o[k] = o[k].translate(o[k].maketrans(t))
    elif isinstance(o,(list,)):
        for i in range(len(o)):
            if isinstance(o[i],(dict,list,)):
                o[i] = copy.deepcopy(sqlescape(o[i]))
            elif isinstance(o[i],(str,bytes,)):
                o[i] = o[i].translate(o[i].maketrans(t))
    elif isinstance(o,(str,bytes,)):
        o = o.translate(o.maketrans(t))
    return o


class SQLdb(object):
    __instance__ = None

    start_time = time.time()
    query_time = 0

    _sql_driver = None
    _sql_driver_extras = None
    _sql_driver_errorcode = None
    connect = None       # connector
    cursor = None        # cursor


    value_type = {'CHAR':'text',
                 'VARCHAR':'text',
                 'TINYBLOB':'text',
                 'TINYTEXT':'text',
                 'BLOB':'text',
                 'TEXT':'text',
                 'MEDIUMBLOB':'text',
                 'MEDIUMTEXT':'text',
                 'LONGBLOB':'text',
                 'LONGTEXT':'text',
                 'TINYINT':'int',
                 'SMALLINT':'int',
                 'MEDIUMINT':'int',
                 'INT':'int',
                 'INTEGER':'int',
                 'BIGINT':'long',
                 'FLOAT':'float',
                 'DOUBLE':'float',
                 'REAL':'float',
                 'DECIMAL':'decimal',
                 'NUMERIC':'decimal',
                 'DATE':'date',
                 'DATETIME':'datetime',
                 'TIMESTAMP':'datetime',
                 'TIME':'float',
                 'YEAR':'int'}

    field_type = {0: 'DECIMAL',
                  1: 'TINY',
                  2: 'SHORT',
                  3: 'LONG',
                  4: 'FLOAT',
                  5: 'DOUBLE',
                  6: 'NULL',
                  7: 'TIMESTAMP',
                  8: 'LONGLONG',
                  9: 'INT24',
                 10: 'DATE',
                 11: 'TIME',
                 12: 'DATETIME',
                 13: 'YEAR',
                 14: 'NEWDATE',
                 15: 'VARCHAR',
                 16: 'BIT',
                246: 'NEWDECIMAL',
                247: 'INTERVAL',
                248: 'SET',
                249: 'TINY_BLOB',
                250: 'MEDIUM_BLOB',
                251: 'LONG_BLOB',
                252: 'BLOB',
                253: 'VAR_STRING',
                254: 'STRING',
                255: 'GEOMETRY' }


    def __new__(cls, *args, **kwargs):
        if SQLdb.__instance__ is None:
            SQLdb.__instance__ = object.__new__(cls)
        SQLdb.__instance__.args = args
        SQLdb.__instance__.kwargs = kwargs
        return SQLdb.__instance__


    def __init__(self,*args, **kwargs):
        '''Abrir acceso a bbdd'''
        self.connect = None
        self.cursor = None
        self.config = dict()
        
        self.driver = kwargs.get('driver','mariadb') 
        self.config['database'] = kwargs['database']
        self._key_delim = '"'
        self._str_delim = "'"

        if self.driver in ('mysql','mariadb'):
            try:
                try:
                    # mysql-connector-python
                    self._sql_driver = import_module('mysql.connector')
                    if ( 'mysql.connector' in self._sql_driver.__dict__['__name__']):
                        self._sql_driver_errorcode = import_module('.errorcode', package='mysql.connector')
                except Exception:
                    try:
                        # pymysql
                        self._sql_driver = import_module('pymysql')
                        if ( 'pymysql' in self._sql_driver.__dict__['__name__']):
                            self._sql_driver_errorcode = import_module('.err', package='pymysql')
                            #from pymysql.err import IntegrityError
                    except Exception:
                        LOG.error(f"""Driver {self.driver} not installed !!!""")
                        return
                

                self.config['host'] = kwargs.get('host','127.0.0.1')
                self.config['port'] = int(kwargs.get('port',3306))
                self.config['user'] = kwargs['username']
                self.config['password'] = kwargs['password']
                self._key_delim = '`'
                  
                   
                if 'pymysql' in self._sql_driver.__dict__['__name__']:
                    self.config['charset'] = kwargs.get('charset',"utf8mb4")
                    self.config['autocommit'] = kwargs.get('autocommit',True)
                    self.config['use_unicode'] = kwargs.get('use_unicode',True)
                
                if 'mysql.connector' in self._sql_driver.__dict__['__name__']:
                    self.config['charset'] = kwargs.get('charset',"utf8mb4")   # utf8 DEFAULT
                    self.config['autocommit'] = kwargs.get('autocommit',True)
                    self.config['use_unicode'] = kwargs.get('use_unicode',True)
                    self.config['time_zone'] = kwargs.get('time_zone',"+0:00")
                    self.config['sql_mode'] = kwargs.get('sql_mode',"TRADITIONAL")
                    self.config['collation'] = kwargs.get('collation',"utf8mb4_general_ci") # utf8mb4_general_ci in 2.x  ó utf8mb4_general_ai_ci DEFAULT
                    self.config['compress'] = kwargs.get('compress',False)
                    self.config['raise_on_warnings']= kwargs.get('raise_on_warnings',True)
                    self.config['get_warnings'] = kwargs.get('get_warnings',True)
                    self.config['buffered'] = kwargs.get('buffered',True)
                
            except Exception as e:
                LOG.error(f"""{self.driver} not installed !!!""")
                tb = sys.exc_info()[2]
                LOG.error("""{0} in line {1} !!!""".format(repr(e),tb.tb_lineno))

        elif self.driver == 'postgresql':
            try:
                self._sql_driver = import_module('psycopg2')
                self._sql_driver_extras = import_module('psycopg2.extras')
                self.config['host'] = kwargs.get('host','127.0.0.1')
                self.config['port'] = kwargs.get('port','5432')
                self.config['user'] = kwargs['username']
                self.config['password'] = kwargs['password']
            except Exception as e:
                LOG.error('PSYCOPG2 not installed !!!')
                tb = sys.exc_info()[2]
                LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))

        elif self.driver == 'sqlite3':
            try:
                self._sql_driver = import_module('sqlite3')
            except Exception as e:
                LOG.error('SQLITE3 not installed !!!')
                tb = sys.exc_info()[2]
                LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            '''sqlite3.connect(database[, timeout, detect_types, isolation_level, check_same_thread, factory, cached_statements, uri])'''
            pass
        else:
            LOG.error('Driver Error Unknown database driver !!!')
        # self.rows permite obtener las filas afectadas
        self.rows = 0
        self.lastId = 0

        try:
            self.reconnect()
        except Exception as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))

    def __del__(self):
        self.close()

    def close(self):
        '''Cierra la conexion a la base de datos'''
        try:
            if getattr(self,"connect",None):
                if getattr(self,"cursor",None):
                    self.cursor.close()
                    self.cursor = None
                #closing database connection.
                try:
                    if(self.connect.is_connected()):
                        self.connect.close()
                        self.connect = None
                except:
                    pass
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))

    def reconnect(self):
        '''Cierra la conexion con la base de datos y vuelve a abrirla'''
        try:
            self.close()
            if self.driver in ('mysql','mariadb'):
                n_retry = RETRY_NUMBER
                while n_retry > 0:
                    if ('mysql.connector' in self._sql_driver.__dict__['__name__'] and hasattr(self._sql_driver, 'reconnect')):
                        self._sql_driver.reconnect(attempts=RETRY_NUMBER,delay=ATTEMPS_SLEEP)
                    elif ('pymysql' in self._sql_driver.__dict__['__name__']):
                        self.connect = self._sql_driver.connect(**self.config)
                        n_retry -= 1
                        self.connect.ping(reconnect=True)
                    else:
                        self.connect = self._sql_driver.connect(**self.config)
                        n_retry -= 1
                    if ('mysql.connector' in self._sql_driver.__dict__['__name__'] and self.connect.is_connected()) or (('pymysql' in self._sql_driver.__dict__['__name__']) and self.connect.open):
                        self.cursor = self.connect.cursor()
                        break
                    else:
                        time.sleep(ATTEMPS_SLEEP)
                if ('mysql.connector' in self._sql_driver.__dict__['__name__'] and self.connect.is_connected()) or (('pymysql' in self._sql_driver.__dict__['__name__']) and self.connect.open):
                    is_conn = True
                else:
                    is_conn = False
                return is_conn
            elif self.driver == 'postgresql':
                self.connect = self._sql_driver.connect(**self.config)
                #conn = psycopg2.connect(database='test',user='postgres',password='pass', host='localhost')
                self.cursor = self.connect.cursor()
                return True
            elif self.driver == 'sqlite3':
                '''sqlite3.connect(database[, timeout, detect_types, isolation_level, check_same_thread, factory, cached_statements, uri])'''
                self.connect = self._sql_driver.connect(**self.config)
                #con = sqlite3.connect(":memory:")
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False

    def execute(self, sql=None , parameters=None):
        '''Ejecuta consultas teniendo en cuenta transacciones
           sql parametro con el codigo
           parameters son las variables que se pueden incorporar al codigo sql
        '''
        try:
            self.is_connect()
            if sql:
                if self.config['autocommit']:
                    if getattr(self.connect,"start_transaction",None) and sql[0:6].upper() in ('INSERT','UPDATE','DELETE'):
                        self.connect.start_transaction()
                else:
                    if getattr(self.connect,"begin",None) and sql[0:6].upper() in ('INSERT','UPDATE','DELETE'):
                        self.connect.begin()
                """    
                # identificar warnings
                warnings = self.cursor.fetchwarnings()
                if warnings:
                    ids = [ i for l,i,m in warnings]
                    del warnings
                    if 1266 in ids:
                        LOG.debug('''Table was created as MYISAM, no transaction support. Bailing out, no use to continue. Make sure InnoDB is available!''')
                        self.connect.close()
                        return None
                """  
                if parameters is not None:
                    LOG.debug('QUERY {0}'.format(sql % parameters,))
                    if isinstance(parameters[0],(list,tuple)):
                        self.cursor.executemany(sql, parameters)
                    else:
                        self.cursor.execute(sql, parameters)
                       
                else:
                    LOG.debug('QUERY {0}'.format(sql,))
                    sql_list = sql.split(';')
                    if len(sql_list)>2 or sql[0:4].upper() in ('CALL',):
                        request = []
                        results = self.cursor.execute(sql,multi=True)
                        for cur in results:
                            if cur.with_rows:
                                request.append(self.cursor.fetchall())
                        LOG.debug('QUERY MULTI=TRUE {0}'.format(request,))
                        return request
                    else:
                        LOG.debug('QUERY {0}'.format(sql))
                        self.cursor.execute(sql)

                if sql[0:6].upper() in ('INSERT',):
                    # Informa de num. de ultima fila autoincremental
                    if hasattr(self.connect,"commit"):
                        self.connect.commit()
                    self.lastId = self.cursor.lastrowid
                    return self.lastId

                elif sql[0:6].upper() in ('UPDATE','DELETE'):
                    # Informa de num. filas afectadas
                    if hasattr(self.connect,"commit"):
                        self.connect.commit()
                    self.rows = self.cursor.rowcount
                    # is rows = -1 not cursor.execute
                    return self.rows

                elif sql[0:6].upper() in ('SELECT',):
                    # Devuelve informacion de todas las filas seleccionadas
                    n = self.cursor.fetchall()
                    return n

        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            if not self.config['autocommit']:
                self.connect.rollback()
        finally:
            self.close()

    def set_auto_increment(self,table=None,id=1):
        '''Fija el autoincremento en la tabla correspondiente al parametro'''
        try:
            self.is_connect()
            if table is not None:
                table_list = self.show_tables()
                if isinstance(table_list,(list,tuple)):
                    if (table,) in table_list:
                        self.cursor.execute('''ALTER TABLE {0} AUTO_INCREMENT = {1}'''.format(table,id))
                        # identificar warnings
                        warnings = self.cursor.fetchwarnings()
                        if len(warnings)>0:
                            ids = [ i for l,i,m in warnings]
                            LOG.warning('''{0}'''.format(ids))
                            del warnings
                        return True
            return False
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False
        finally:
            self.close()

    def set_wait_timeout(self,seconds=29000):
        '''Fija wait_timeout y interactive_timeout a 60 por defecto'''
        ''' IMPORTANTE no funciona correctamente '''
        try:
            self.is_connect()
            if seconds is not None:
                self.cursor.execute("SET wait_timeout=={}".format(seconds))
                self.cursor.execute("SET GLOBAL wait_timeout=={}".format(seconds))
                self.cursor.execute("SET SESSION wait_timeout=={}".format(seconds))
                self.cursor.execute("set interactive_timeout={}".format(seconds))
            return True
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False

    def set_global(self,request=None):
        try:
            self.is_connect()
            if len(request)>0:
                self.cursor.execute(request)
            return True
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False

    def set_optimize(self,table=None):
        '''Optimizacion de tablas INNODB'''
        try:
            if table is not None:
                table_list = self.show_tables()
                if isinstance(table_list,(list,tuple)):
                    if (table,) in table_list:
                        self.is_connect()
                        self.cursor.execute('''OPTIMIZE TABLE {};'''.format(table))
                        # identificar warnings
                        warnings = self.cursor.fetchwarnings()
                        if len(warnings)>0:
                            ids = [ i for l,i,m in warnings]
                            LOG.debug('''{0}'''.format(ids))
                            del warnings
                        return True
            return False
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False
        finally:
            self.close()

    def show_version(self):
        '''Version de MySQL'''
        try:
            self.is_connect()
            self.cursor.execute('''SELECT VERSION();''')
            return self.cursor.fetchone()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return list()
        finally:
            self.close()

    def create_database(self,database=None):
        '''create database'''
        try:
            self.is_connect()
            if database is not None:
                self.cursor.execute('''CREATE DATABASE IF NOT EXISTS {};'''.format(database))
                return True
            return False
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
            return False
        finally:
            self.close()

    def create_table(self,table):
        '''Crear una tabla'''
        try:
            self.is_connect()
            pass
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))

    def show_database(self):
        '''Obtener database name'''
        
        try:
            self.is_connect()
            self.cursor.execute('''SELECT DATABASE();''')
            return self.cursor.fetchall()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()
           

    def show_tables(self):
        '''Ver lista de tablas'''
        try:
            self.is_connect()
            self.cursor.execute('''SHOW TABLES;''')
            return self.cursor.fetchall()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()

    def show_columns(self, table=None):
        '''Ver lista de columnas'''
        try:
            if table is not None:
                table_list = self.show_tables()
                if isinstance(table_list,(list,tuple)):
                    if (table,) in table_list:
                        self.is_connect()
                        self.cursor.execute(f'''SHOW COLUMNS FROM {table};''')
                        return self.cursor.fetchall()
            return list()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()

    def show_index(self, table=None):
        '''Ver lista de indices'''
        try:
            if table is not None:
                table_list = self.show_tables()
                if isinstance(table_list,(list,tuple)):
                    if (table,) in table_list:
                        self.is_connect()
                        self.cursor.execute('''SHOW INDEX FROM {};'''.format(table))
                        return self.cursor.fetchall()
            return list()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()


    def show_vars(self):
        '''Ver lista de variables'''
        try:
            self.is_connect()
            self.cursor.execute('''SHOW VARIABLES;''')
            return self.cursor.fetchall()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()

    def show_process(self):
        '''Ver lista de procesos del usuario'''
        try:
            self.is_connect()
            self.cursor.execute('''SHOW PROCESSLIST WHERE USER LIKE {};'''.format(self._db_args['user']))
            return self.cursor.fetchall()
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
        finally:
            self.close()

    def is_connect(self):
        ''' Comprueba conexion a bb.dd. '''
        try:
            retry = 0
            while retry < RETRY_NUMBER:
                try:
                    if self.reconnect():
                       # si la reconexion es correcta
                       retry = RETRY_NUMBER
                except self._sql_driver.Error as e:
                    tb = sys.exc_info()[2]
                    LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
                retry += 1
        except self._sql_driver.Error as e:
            tb = sys.exc_info()[2]
            LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))


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 ('''
        Gestion de BB.DD. en MySQL o MariaDB o POSTGRESQL :\n\n''')
    try:

        __db__ = SQLdb(host='10.0.0.1',database='dacartoys',username='root',password='headcracker_sql',driver='mariadb',port='4306')
        print(type(__db__))
        n = __db__.show_version()
        print(n)
        n = __db__.show_columns("entities")
        for row in n:
            print(row)
    except Exception as e:
        tb = sys.exc_info()[2]
        LOG.error("{0} in line {1} !!!".format(repr(e),tb.tb_lineno))
