1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2026-05-02 13:10:58 +02:00
lodel2_mirror/Database/sqlwrapper.py
2015-06-19 17:09:18 +02:00

515 lines
18 KiB
Python

# -*- coding: utf-8 -*-
import os
import re
import logging
import sqlalchemy as sqla
from sqlalchemy.ext.compiler import compiles
from django.conf import settings
from Database.sqlalter import *
#Logger config
#logger.getLogger().setLevel('WARNING')
logger = logging.getLogger('lodel2.Database.sqlwrapper')
logger.setLevel('CRITICAL')
logger.propagate = False
#To be able to use dango confs
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lodel.settings")
class SqlWrapper(object):
""" A wrapper class to sqlalchemy
Usefull to provide a standart API
__Note__ : This class is not thread safe (sqlAlchemy connections are not). Create a new instance of the class to use in different threads or use SqlWrapper::copy
"""
ENGINES = {'mysql': {
'driver': 'pymysql',
'encoding': 'utf8'
},
'postgresql': {
'driver': 'psycopg2',
'encoding': 'utf8',
},
'sqlite': {
'driver': 'pysqlite',
'encoding': 'utf8',
},
}
##Configuration dict alias for class access
config=settings.LODEL2SQLWRAPPER
##Wrapper instance list
wrapinstance = dict()
def __init__(self, name=None, alchemy_logs=None, read_db = "default", write_db = "default"):
""" Instanciate a new SqlWrapper
@param name str: The wrapper name
@param alchemy_logs bool: If true activate sqlalchemy logger
@param read_db str: The name of the db conf
@param write_db str: The name of the db conf
@todo Better use of name (should use self.cfg['wrapper'][name] to get engines configs
@todo Is it a really good idea to store instance in class scope ? Maybe not !!
"""
self.sqlalogging = False if alchemy_logs == None else bool(alchemy_logs)
if name == None:
self.name = read_db+':'+write_db
else:
self.name = name
self.r_dbconf = read_db
self.w_dbconf = write_db
self._checkConf() #raise if errors in configuration
if self.name in self.__class__.wrapinstance:
logger.warning("A SqlWrapper with the name "+self.name+" allready exist. Replacing the old one by the new one")
SqlWrapper.wrapinstance[self.name] = self
#Engine and wrapper initialisation
self.r_engine = self._getEngine(True, self.sqlalogging)
self.w_engine = self._getEngine(False, self.sqlalogging)
self.r_conn = None
self.w_conn = None
self.metadata = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
self.meta_crea = None
logger.debug("New wrapper instance : <"+self.name+" read:"+str(self.r_engine)+" write:"+str(self.w_engine))
pass
@classmethod
def getEngine(c, ename = 'default', logs = None):
return c(read_db = ename, write_db = ename, alchemy_logs = logs).r_engine
@property
def cfg(self):
""" Return the SqlWrapper.config dict """
return self.__class__.config;
@property
def _engines_cfg(self):
return self.__class__.ENGINES;
@property
def meta(self):
if self.metadata == None:
self.renewMetaData()
return self.metadata
def renewMetaData(self):
""" (Re)load the database schema """
self.metadata = sqla.MetaData(bind=self.r_engine)
self.metadata.reflect()
@property
def rconn(self):
""" Return the read connection
@warning Do not store the connection, call this method each time you need it
"""
return self._getConnection(True)
@property
def wconn(self):
""" Return the write connection
@warning Do not store the connection, call this method each time you need it
"""
return self._getConnection(False)
def _getConnection(self, read):
""" Return an opened connection
@param read bool: If true return the reading connection
@return A sqlAlchemy db connection
@private
"""
if read:
r = self.r_conn
else:
r = self.w_conn
if r == None:
#Connection not yet opened
self.connect(read)
r = self._getConnection(read) #TODO : Un truc plus safe/propre qu'un appel reccursif ?
return r
def connect(self, read = None):
""" Open a connection to a database
@param read bool|None: If None connect both, if True only connect the read side (False the write side)
@return None
"""
if read or read == None:
if self.r_conn != None:
logger.debug(' SqlWrapper("'+self.name+'") Unable to connect, already connected')
else:
self.r_conn = self.r_engine.connect()
if not read or read == None:
if self.w_conn != None:
logger.debug(' SqlWrapper("'+self.name+'") Unable to connect, already connected')
else:
self.w_conn = self.w_engine.connect()
def disconnect(self, read = None):
""" Close a connection to a database
@param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
@return None
"""
if read or read == None:
if self.r_conn == None:
logger.info('Unable to close read connection : connection not opened')
else:
self.r_conn.close()
self.r_conn = None
if not read or read == None:
if self.r_conn == None:
logger.info('Unable to close write connection : connection not opened')
else:
self.w_conn.close()
self.w_conn = None
def reconnect(self, read = None):
""" Close and reopen a connection to a database
@param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
@return None
"""
self.disconnect(read)
self.connect(read)
@classmethod
def reconnectAll(c, read = None):
""" Reconnect all the wrappers
@static
"""
for wname in c.wrapinstance:
c.wrapinstance[wname].reconnect(read)
def Table(self, tname):
""" Instanciate a new SqlAlchemy Table
@param tname str: The table name
@return A new instance of SqlAlchemy::Table
"""
if not isinstance(tname, str):
return TypeError('Excepting a <class str> but got a '+str(type(tname)))
#return sqla.Table(tname, self.meta, autoload_with=self.r_engine, autoload=True)
return sqla.Table(tname, self.meta)
def _getEngine(self, read=True, sqlalogging = None):
""" Return a sqlalchemy engine
@param read bool: If True return the read engine, else
return the write one
@return a sqlachemy engine instance
@todo Put the check on db config in SqlWrapper.checkConf()
"""
#Loading confs
cfg = self.cfg['db'][self.r_dbconf if read else self.w_dbconf]
edata = self._engines_cfg[cfg['ENGINE']] #engine infos
conn_str = ""
if cfg['ENGINE'] == 'sqlite':
#Sqlite connection string
conn_str = '%s+%s:///%s'%( cfg['ENGINE'],
edata['driver'],
cfg['NAME'])
else:
#Mysql and Postgres connection string
user = cfg['USER']
user += (':'+cfg['PASSWORD'] if 'PASSWORD' in cfg else '')
if 'HOST' not in cfg:
logger.info('Not HOST in configuration, using localhost')
host = 'localhost'
else:
host = cfg['HOST']
host += (':'+cfg['PORT'] if 'PORT' in cfg else '')
conn_str = '%s+%s://'%(cfg['ENGINE'], edata['driver'])
conn_str += '%s@%s/%s'%(user,host,cfg['NAME'])
ret = sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
logger.debug("Getting engine :"+str(ret))
return ret
@classmethod
def getWrapper(c, name):
""" Return a wrapper instance from a wrapper name
@param name str: The wrapper name
@return a SqlWrapper instance
@throw KeyError
"""
if name not in c.wrapinstance:
raise KeyError("No wrapper named '"+name+"' exists")
return c.wrapinstance[name]
def _checkConf(self):
""" Class method that check the configuration
Configuration looks like
- db (mandatory)
- ENGINE (mandatory)
- NAME (mandatory)
- USER
- PASSWORD
- engines (mandatory)
- driver (mandatory)
- encoding (mandatory)
- dbread (mandatory if no default db)
- dbwrite (mandatory if no default db)
"""
err = []
if 'db' not in self.cfg:
err.append('Missing "db" in configuration')
else:
for dbname in [self.r_dbconf, self.w_dbconf]:
if dbname not in self.cfg['db']:
err.append('Missing "'+dbname+'" db configuration')
else:
db = self.cfg['db'][dbname]
if db['ENGINE'] not in self._engines_cfg:
err.append('Unknown engine "'+db['ENGINE']+'"')
elif db['ENGINE'] != 'sqlite' and 'USER' not in db:
err.append('Missing "User" in configuration of database "'+dbname+'"')
if 'NAME' not in db:
err.append('Missing "NAME" in database "'+dbname+'"')
if len(err)>0:
err_str = "\n"
for e in err:
err_str += "\t\t"+e+"\n"
raise NameError('Configuration errors in LODEL2SQLWRAPPER:'+err_str)
def dropAll(self):
""" Drop ALL tables from the database """
if not settings.DEBUG:
logger.critical("Trying to drop all tables but we are not in DEBUG !!!")
raise RuntimeError("Trying to drop all tables but we are not in DEBUG !!!")
meta = sqla.MetaData(bind=self.w_engine)
meta.reflect()
meta.drop_all()
pass
def createAllFromConf(self, schema):
""" Create a bunch of tables from a schema
@param schema list: A list of table schema
@see SqlWrapper::createTable()
"""
self.meta_crea = sqla.MetaData()
logger.info("Running function createAllFromConf")
for i,table in enumerate(schema):
if not isinstance(table, dict):
raise TypeError("Excepted a list of dict but got a "+str(type(schema))+" in the list")
self.createTable(**table)
self.meta_crea.create_all(bind = self.w_engine)
logger.info("All tables created")
self.meta_crea = None
self.renewMetaData()
pass
def createTable(self, name, columns, **kw):
""" Create a table
@param name str: The table name
@param columns list: A list of columns description dict
@param extra dict: Extra arguments for table creation
@see SqlWrapper::createColumn()
"""
if self.meta_crea == None:
self.meta_crea = sqla.MetaData()
crea_now = True
else:
crea_now = False
if not isinstance(name, str):
raise TypeError("<class str> excepted for table name, but got "+type(name))
#if not 'mysql_engine' in kw and self.w_engine.dialect.name == 'mysql':
# kw['mysql_engine'] = 'InnoDB'
res = sqla.Table(name, self.meta_crea, **kw)
for i,col in enumerate(columns):
res.append_column(self.createColumn(**col))
if crea_now:
self.meta_crea.create_all(self.w_engine)
logger.debug("Table '"+name+"' created")
pass
def createColumn(self, **kwargs):
""" Create a Column
Accepte named parameters :
- name : The column name
- type : see SqlWrapper::_strToSqlAType()
- extra : a dict like { "primarykey":True, "nullable":False, "default":"test"...}
@param **kwargs
"""
if not 'name' in kwargs or ('type' not in kwargs and 'type_' not in kwargs):
pass#ERROR
#Converting parameters
if 'type_' not in kwargs and 'type' in kwargs:
kwargs['type_'] = self._strToSqlAType(kwargs['type'])
del kwargs['type']
if 'extra' in kwargs:
#put the extra keys in kwargs
for exname in kwargs['extra']:
kwargs[exname] = kwargs['extra'][exname]
del kwargs['extra']
if 'foreignkey' in kwargs:
#Instanciate a fk
fk = sqla.ForeignKey(kwargs['foreignkey'])
del kwargs['foreignkey']
else:
fk = None
if 'primarykey' in kwargs:
#renaming primary_key in primarykey in kwargs
kwargs['primary_key'] = kwargs['primarykey']
del kwargs['primarykey']
res = sqla.Column(**kwargs)
if fk != None:
res.append_foreign_key(fk)
#logger.debug("Column '"+kwargs['name']+"' created")
return res
def _strToSqlAType(self, strtype):
""" Convert a string to an sqlAlchemy column type """
if 'VARCHAR' in strtype:
return self._strToVarchar(strtype)
else:
try:
return getattr(sqla, strtype)
except AttributeError:
raise NameError("Unknown type '"+strtype+"'")
pass
def _strToVarchar(self, vstr):
""" Convert a string like 'VARCHAR(XX)' (with XX an integer) to a SqlAlchemy varchar type"""
check_length = re.search(re.compile('VARCHAR\(([\d]+)\)', re.IGNORECASE), vstr)
column_length = int(check_length.groups()[0]) if check_length else None
return sqla.VARCHAR(length=column_length)
def dropColumn(self, tname, colname):
""" Drop a column from a table
@param tname str|sqlalchemy.Table: The table name or a Table object
@param colname str|sqlalchemy.Column: The column name or a column object
@return None
"""
if tname not in self.meta.tables: #Useless ?
raise NameError("The table '"+tname+"' dont exist")
table = self.Table(tname)
col = sqla.Column(colname)
ddl = DropColumn(table, col)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '"+sql+"'")
ret = bool(self.w_engine.execute(sql))
self.renewMetaData()
return ret
def addColumn(self, tname, colname, coltype):
""" Add a column to a table
@param tname str: The table name
@param colname str: The column name
@param coltype str: The new column type
@return True if query success False if it fails
"""
if tname not in self.meta.tables: #Useless ?
raise NameError("The table '"+tname+"' dont exist")
table = self.Table(tname)
newcol = self.createColumn(name=colname, type_ = coltype)
ddl = AddColumn(table, newcol)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '"+sql+"'")
ret = bool(self.wconn.execute(sql))
self.renewMetaData()
return ret
## AddColumnObject
#
# Adds a column from a SQLAlchemy Column Object
#
# @param tname str: Name of the table in which to add the column
# @param column Column: Column object to add to the table
# @return True if query is successful, False if it fails
def addColumnObject(self, tname, column):
if tname not in self.meta.tables:
raise NameError("The table '%s' doesn't exist" % tname)
table = self.Table(tname)
ddl = AddColumn(table, column)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '%s'" % sql)
ret = bool(self.wconn.execute(sql))
self.renewMetaData()
return ret
def alterColumn(self, tname, colname, col_newtype):
""" Change the type of a column
@param tname str: The table name
@param colname str: The column name
@param col_newtype str: The column new type
@return True if query successs False if it fails
"""
if tname not in self.meta.tables: #Useless ?
raise NameError("The table '"+tname+"' dont exist")
col = self.createColumn(name=colname, type_=col_newtype)
table = self.Table(tname)
ddl = AlterColumn(table, newcol)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '"+sql+"'")
ret = bool(self.wconn.execute(sql))
self.renewMetaData()
return ret
def _debug__printSchema(self):
""" Debug function to print the db schema """
print(self.meta)
for tname in self.meta.tables:
self._debug__printTable(tname)
def _debug__printTable(self, tname):
t = self.meta.tables[tname]
tstr = 'Table : "'+tname+'" :\n'
for c in t.c:
tstr += '\t\t"'+c.name+'"('+str(c.type)+') \n'
print(tstr)