123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- # -*- 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 = book(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)
-
|