No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sqlwrapper.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. # -*- coding: utf-8 -*-
  2. import os
  3. import re
  4. import logging
  5. import sqlalchemy as sqla
  6. from sqlalchemy.ext.compiler import compiles
  7. from django.conf import settings
  8. from Database.sqlalter import *
  9. #Logger config
  10. #logger.getLogger().setLevel('WARNING')
  11. logger = logging.getLogger('lodel2.Database.sqlwrapper')
  12. logger.setLevel('WARNING')
  13. logger.propagate = False
  14. #To be able to use dango confs
  15. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lodel.settings")
  16. class SqlWrapper(object):
  17. """ A wrapper class to sqlalchemy
  18. Usefull to provide a standart API
  19. __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
  20. """
  21. ENGINES = {'mysql': {
  22. 'driver': 'pymysql',
  23. 'encoding': 'utf8'
  24. },
  25. 'postgresql': {
  26. 'driver': 'psycopg2',
  27. 'encoding': 'utf8',
  28. },
  29. 'sqlite': {
  30. 'driver': 'pysqlite',
  31. 'encoding': 'utf8',
  32. },
  33. }
  34. ##Configuration dict alias for class access
  35. config=settings.LODEL2SQLWRAPPER
  36. ##Wrapper instance list
  37. wrapinstance = dict()
  38. def __init__(self, name=None, alchemy_logs=None, read_db = "default", write_db = "default"):
  39. """ Instanciate a new SqlWrapper
  40. @param name str: The wrapper name
  41. @param alchemy_logs bool: If true activate sqlalchemy logger
  42. @param read_db str: The name of the db conf
  43. @param write_db str: The name of the db conf
  44. @todo Better use of name (should use self.cfg['wrapper'][name] to get engines configs
  45. @todo Is it a really good idea to store instance in class scope ? Maybe not !!
  46. """
  47. self.sqlalogging = False if alchemy_logs == None else bool(alchemy_logs)
  48. if name == None:
  49. self.name = read_db+'+'+write_db
  50. else:
  51. self.name = name
  52. self.r_dbconf = read_db
  53. self.w_dbconf = write_db
  54. self._checkConf() #raise if errors in configuration
  55. if self.name in self.__class__.wrapinstance:
  56. logger.warning("A SqlWrapper with the name "+self.name+" allready exist. Replacing the old one by the new one")
  57. SqlWrapper.wrapinstance[self.name] = self
  58. #Engine and wrapper initialisation
  59. self.r_engine = self._getEngine(True, self.sqlalogging)
  60. self.w_engine = self._getEngine(False, self.sqlalogging)
  61. self.r_conn = None
  62. self.w_conn = None
  63. self.metadata = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
  64. self.meta_crea = None
  65. logger.debug("New wrapper instance : <"+self.name+" read:"+str(self.r_engine)+" write:"+str(self.w_engine))
  66. pass
  67. @classmethod
  68. def getEngine(c, ename = 'default', logs = None):
  69. return c(read_db = ename, write_db = ename, alchemy_logs = logs).r_engine
  70. @property
  71. def cfg(self):
  72. """ Return the SqlWrapper.config dict """
  73. return self.__class__.config;
  74. @property
  75. def _engines_cfg(self):
  76. return self.__class__.ENGINES;
  77. @property
  78. def meta(self):
  79. if self.metadata == None:
  80. self.renewMetaData()
  81. return self.metadata
  82. def renewMetaData(self):
  83. """ (Re)load the database schema """
  84. self.metadata = sqla.MetaData(bind=self.r_engine)
  85. self.metadata.reflect()
  86. @property
  87. def rconn(self):
  88. """ Return the read connection
  89. @warning Do not store the connection, call this method each time you need it
  90. """
  91. return self._getConnection(True)
  92. @property
  93. def wconn(self):
  94. """ Return the write connection
  95. @warning Do not store the connection, call this method each time you need it
  96. """
  97. return self._getConnection(False)
  98. def _getConnection(self, read):
  99. """ Return an opened connection
  100. @param read bool: If true return the reading connection
  101. @return A sqlAlchemy db connection
  102. @private
  103. """
  104. if read:
  105. r = self.r_conn
  106. else:
  107. r = self.w_conn
  108. if r == None:
  109. #Connection not yet opened
  110. self.connect(read)
  111. r = self._getConnection(read) #TODO : Un truc plus safe/propre qu'un appel reccursif ?
  112. return r
  113. def connect(self, read = None):
  114. """ Open a connection to a database
  115. @param read bool|None: If None connect both, if True only connect the read side (False the write side)
  116. @return None
  117. """
  118. if read or read == None:
  119. if self.r_conn != None:
  120. logger.debug(' SqlWrapper("'+self.name+'") Unable to connect, already connected')
  121. else:
  122. self.r_conn = self.r_engine.connect()
  123. if not read or read == None:
  124. if self.w_conn != None:
  125. logger.debug(' SqlWrapper("'+self.name+'") Unable to connect, already connected')
  126. else:
  127. self.w_conn = self.w_engine.connect()
  128. def disconnect(self, read = None):
  129. """ Close a connection to a database
  130. @param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
  131. @return None
  132. """
  133. if read or read == None:
  134. if self.r_conn == None:
  135. logger.info('Unable to close read connection : connection not opened')
  136. else:
  137. self.r_conn.close()
  138. self.r_conn = None
  139. if not read or read == None:
  140. if self.r_conn == None:
  141. logger.info('Unable to close write connection : connection not opened')
  142. else:
  143. self.w_conn.close()
  144. self.w_conn = None
  145. def reconnect(self, read = None):
  146. """ Close and reopen a connection to a database
  147. @param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
  148. @return None
  149. """
  150. self.disconnect(read)
  151. self.connect(read)
  152. @classmethod
  153. def reconnectAll(c, read = None):
  154. """ Reconnect all the wrappers
  155. @static
  156. """
  157. for wname in c.wrapinstance:
  158. c.wrapinstance[wname].reconnect(read)
  159. def Table(self, tname):
  160. """ Instanciate a new SqlAlchemy Table
  161. @param tname str: The table name
  162. @return A new instance of SqlAlchemy::Table
  163. """
  164. if not isinstance(tname, str):
  165. return TypeError('Excepting a <class str> but got a '+str(type(tname)))
  166. #return sqla.Table(tname, self.meta, autoload_with=self.r_engine, autoload=True)
  167. return sqla.Table(tname, self.meta)
  168. def _getEngine(self, read=True, sqlalogging = None):
  169. """ Return a sqlalchemy engine
  170. @param read bool: If True return the read engine, else
  171. return the write one
  172. @return a sqlachemy engine instance
  173. @todo Put the check on db config in SqlWrapper.checkConf()
  174. """
  175. #Loading confs
  176. cfg = self.cfg['db'][self.r_dbconf if read else self.w_dbconf]
  177. edata = self._engines_cfg[cfg['ENGINE']] #engine infos
  178. conn_str = ""
  179. if cfg['ENGINE'] == 'sqlite':
  180. #Sqlite connection string
  181. conn_str = '%s+%s:///%s'%( cfg['ENGINE'],
  182. edata['driver'],
  183. cfg['NAME'])
  184. else:
  185. #Mysql and Postgres connection string
  186. user = cfg['USER']
  187. user += (':'+cfg['PASSWORD'] if 'PASSWORD' in cfg else '')
  188. if 'HOST' not in cfg:
  189. logger.info('Not HOST in configuration, using localhost')
  190. host = 'localhost'
  191. else:
  192. host = cfg['HOST']
  193. host += (':'+cfg['PORT'] if 'PORT' in cfg else '')
  194. conn_str = '%s+%s://'%(cfg['ENGINE'], edata['driver'])
  195. conn_str += '%s@%s/%s'%(user,host,cfg['NAME'])
  196. ret = sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
  197. logger.debug("Getting engine :"+str(ret))
  198. return ret
  199. @classmethod
  200. def getWrapper(c, name):
  201. """ Return a wrapper instance from a wrapper name
  202. @param name str: The wrapper name
  203. @return a SqlWrapper instance
  204. @throw KeyError
  205. """
  206. if name not in c.wrapinstance:
  207. raise KeyError("No wrapper named '"+name+"' exists")
  208. return c.wrapinstance[name]
  209. def _checkConf(self):
  210. """ Class method that check the configuration
  211. Configuration looks like
  212. - db (mandatory)
  213. - ENGINE (mandatory)
  214. - NAME (mandatory)
  215. - USER
  216. - PASSWORD
  217. - engines (mandatory)
  218. - driver (mandatory)
  219. - encoding (mandatory)
  220. - dbread (mandatory if no default db)
  221. - dbwrite (mandatory if no default db)
  222. """
  223. err = []
  224. if 'db' not in self.cfg:
  225. err.append('Missing "db" in configuration')
  226. else:
  227. for dbname in [self.r_dbconf, self.w_dbconf]:
  228. if dbname not in self.cfg['db']:
  229. err.append('Missing "'+dbname+'" db configuration')
  230. else:
  231. db = self.cfg['db'][dbname]
  232. if db['ENGINE'] not in self._engines_cfg:
  233. err.append('Unknown engine "'+db['ENGINE']+'"')
  234. elif db['ENGINE'] != 'sqlite' and 'USER' not in db:
  235. err.append('Missing "User" in configuration of database "'+dbname+'"')
  236. if 'NAME' not in db:
  237. err.append('Missing "NAME" in database "'+dbname+'"')
  238. if len(err)>0:
  239. err_str = "\n"
  240. for e in err:
  241. err_str += "\t\t"+e+"\n"
  242. raise NameError('Configuration errors in LODEL2SQLWRAPPER:'+err_str)
  243. def dropAll(self):
  244. """ Drop ALL tables from the database """
  245. if not settings.DEBUG:
  246. logger.critical("Trying to drop all tables but we are not in DEBUG !!!")
  247. raise RuntimeError("Trying to drop all tables but we are not in DEBUG !!!")
  248. meta = sqla.MetaData(bind=self.w_engine)
  249. meta.reflect()
  250. meta.drop_all()
  251. pass
  252. def createAllFromConf(self, schema):
  253. """ Create a bunch of tables from a schema
  254. @param schema list: A list of table schema
  255. @see SqlWrapper::createTable()
  256. """
  257. self.meta_crea = sqla.MetaData()
  258. logger.info("Running function createAllFromConf")
  259. for i,table in enumerate(schema):
  260. if not isinstance(table, dict):
  261. raise TypeError("Excepted a list of dict but got a "+str(type(schema))+" in the list")
  262. self.createTable(**table)
  263. self.meta_crea.create_all(bind = self.w_engine)
  264. logger.info("All tables created")
  265. self.meta_crea = None
  266. self.renewMetaData()
  267. pass
  268. def createTable(self, name, columns, **kw):
  269. """ Create a table
  270. @param name str: The table name
  271. @param columns list: A list of columns description dict
  272. @param extra dict: Extra arguments for table creation
  273. @see SqlWrapper::createColumn()
  274. """
  275. if self.meta_crea == None:
  276. self.meta_crea = sqla.MetaData()
  277. crea_now = True
  278. else:
  279. crea_now = False
  280. if not isinstance(name, str):
  281. raise TypeError("<class str> excepted for table name, but got "+type(name))
  282. #if not 'mysql_engine' in kw and self.w_engine.dialect.name == 'mysql':
  283. # kw['mysql_engine'] = 'InnoDB'
  284. res = sqla.Table(name, self.meta_crea, **kw)
  285. for i,col in enumerate(columns):
  286. res.append_column(self.createColumn(**col))
  287. if crea_now:
  288. self.meta_crea.create_all(self.w_engine)
  289. logger.debug("Table '"+name+"' created")
  290. pass
  291. def createColumn(self, **kwargs):
  292. """ Create a Column
  293. Accepte named parameters :
  294. - name : The column name
  295. - type : see SqlWrapper::_strToSqlAType()
  296. - extra : a dict like { "primarykey":True, "nullable":False, "default":"test"...}
  297. @param **kwargs
  298. """
  299. if not 'name' in kwargs or ('type' not in kwargs and 'type_' not in kwargs):
  300. pass#ERROR
  301. #Converting parameters
  302. if 'type_' not in kwargs and 'type' in kwargs:
  303. kwargs['type_'] = self._strToSqlAType(kwargs['type'])
  304. del kwargs['type']
  305. if 'extra' in kwargs:
  306. #put the extra keys in kwargs
  307. for exname in kwargs['extra']:
  308. kwargs[exname] = kwargs['extra'][exname]
  309. del kwargs['extra']
  310. if 'foreignkey' in kwargs:
  311. #Instanciate a fk
  312. fk = sqla.ForeignKey(kwargs['foreignkey'])
  313. del kwargs['foreignkey']
  314. else:
  315. fk = None
  316. if 'primarykey' in kwargs:
  317. #renaming primary_key in primarykey in kwargs
  318. kwargs['primary_key'] = kwargs['primarykey']
  319. del kwargs['primarykey']
  320. res = sqla.Column(**kwargs)
  321. if fk != None:
  322. res.append_foreign_key(fk)
  323. #logger.debug("Column '"+kwargs['name']+"' created")
  324. return res
  325. def _strToSqlAType(self, strtype):
  326. """ Convert a string to an sqlAlchemy column type """
  327. if 'VARCHAR' in strtype:
  328. return self._strToVarchar(strtype)
  329. else:
  330. try:
  331. return getattr(sqla, strtype)
  332. except AttributeError:
  333. raise NameError("Unknown type '"+strtype+"'")
  334. pass
  335. def _strToVarchar(self, vstr):
  336. """ Convert a string like 'VARCHAR(XX)' (with XX an integer) to a SqlAlchemy varchar type"""
  337. check_length = re.search(re.compile('VARCHAR\(([\d]+)\)', re.IGNORECASE), vstr)
  338. column_length = int(check_length.groups()[0]) if check_length else None
  339. return sqla.VARCHAR(length=column_length)
  340. def dropColumn(self, tname, colname):
  341. """ Drop a column from a table
  342. @param tname str|sqlalchemy.Table: The table name or a Table object
  343. @param colname str|sqlalchemy.Column: The column name or a column object
  344. @return None
  345. """
  346. if tname not in self.meta.tables: #Useless ?
  347. raise NameError("The table '"+tname+"' dont exist")
  348. table = self.Table(tname)
  349. col = sqla.Column(colname)
  350. ddl = DropColumn(table, col)
  351. sql = ddl.compile(dialect=self.w_engine.dialect)
  352. sql = str(sql)
  353. logger.debug("Executing SQL : '"+sql+"'")
  354. ret = bool(self.w_engine.execute(sql))
  355. self.renewMetaData()
  356. return ret
  357. def addColumn(self, tname, colname, coltype):
  358. """ Add a column to a table
  359. @param tname str: The table name
  360. @param colname str: The column name
  361. @param coltype str: The new column type
  362. @return True if query success False if it fails
  363. """
  364. if tname not in self.meta.tables: #Useless ?
  365. raise NameError("The table '"+tname+"' dont exist")
  366. table = self.Table(tname)
  367. newcol = self.createColumn(name=colname, type_ = coltype)
  368. ddl = AddColumn(table, newcol)
  369. sql = ddl.compile(dialect=self.w_engine.dialect)
  370. sql = str(sql)
  371. logger.debug("Executing SQL : '"+sql+"'")
  372. ret = bool(self.wconn.execute(sql))
  373. self.renewMetaData()
  374. return ret
  375. def alterColumn(self, tname, colname, col_newtype):
  376. """ Change the type of a column
  377. @param tname str: The table name
  378. @param colname str: The column name
  379. @param col_newtype str: The column new type
  380. @return True if query successs False if it fails
  381. """
  382. if tname not in self.meta.tables: #Useless ?
  383. raise NameError("The table '"+tname+"' dont exist")
  384. col = self.createColumn(name=colname, type_=col_newtype)
  385. table = self.Table(tname)
  386. ddl = AlterColumn(table, newcol)
  387. sql = ddl.compile(dialect=self.w_engine.dialect)
  388. sql = str(sql)
  389. logger.debug("Executing SQL : '"+sql+"'")
  390. ret = bool(self.wconn.execute(sql))
  391. self.renewMetaData()
  392. return ret
  393. def _debug__printSchema(self):
  394. """ Debug function to print the db schema """
  395. print(self.meta)
  396. for tname in self.meta.tables:
  397. self._debug__printTable(tname)
  398. def _debug__printTable(self, tname):
  399. t = self.meta.tables[tname]
  400. tstr = 'Table : "'+tname+'" :\n'
  401. for c in t.c:
  402. tstr += '\t\t"'+c.name+'"('+str(c.type)+') \n'
  403. print(tstr)