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

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