mirror of
https://github.com/yweber/lodel2.git
synced 2026-05-02 13:10:58 +02:00
515 lines
18 KiB
Python
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)
|
|
|
|
|