|
@@ -11,108 +11,149 @@ logger.getLogger().setLevel('DEBUG')
|
11
|
11
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lodel.settings")
|
12
|
12
|
|
13
|
13
|
class SqlWrapper(object):
|
14
|
|
- """ A wrapper class to sqlalchemy
|
15
|
|
- Usefull to provide a standart API
|
16
|
|
- """
|
|
14
|
+ """ A wrapper class to sqlalchemy
|
17
|
15
|
|
18
|
|
- ##Read Engine
|
19
|
|
- rengine = None
|
20
|
|
- ##Write Engine
|
21
|
|
- wengine = None
|
22
|
|
-
|
23
|
|
- ##Read connection
|
24
|
|
- rconn = None
|
25
|
|
- ##Write connection
|
26
|
|
- wconn = None
|
|
16
|
+ Usefull to provide a standart API
|
27
|
17
|
|
|
18
|
+ __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
|
|
19
|
+ """
|
|
20
|
+ ENGINES = {'mysql': {
|
|
21
|
+ 'driver': 'pymysql',
|
|
22
|
+ 'encoding': 'utf8'
|
|
23
|
+ },
|
|
24
|
+ 'postgresql': {
|
|
25
|
+ 'driver': 'psycopg2',
|
|
26
|
+ 'encoding': 'utf8',
|
|
27
|
+ },
|
|
28
|
+ 'sqlite': {
|
|
29
|
+ 'driver': 'pysqlite',
|
|
30
|
+ 'encoding': 'utf8',
|
|
31
|
+ },
|
|
32
|
+ }
|
|
33
|
+
|
28
|
34
|
##Configuration dict alias for class access
|
29
|
35
|
config=settings.LODEL2SQLWRAPPER
|
30
|
|
- ##SqlAlchemy logging
|
31
|
|
- sqla_logging = False
|
|
36
|
+
|
|
37
|
+ ##Wrapper instance list
|
|
38
|
+ wrapinstance = dict()
|
32
|
39
|
|
33
|
|
- def __init__(self, alchemy_logs=None):
|
|
40
|
+ def __init__(self, name="default", alchemy_logs=None, read_db = "default", write_db = "default"):
|
34
|
41
|
""" Instanciate a new SqlWrapper
|
|
42
|
+ @param name str: The wrapper name
|
35
|
43
|
@param alchemy_logs bool: If true activate sqlalchemy logger
|
|
44
|
+ @param read_db str: The name of the db conf
|
|
45
|
+ @param write_db str: The name of the db conf
|
36
|
46
|
"""
|
37
|
47
|
|
38
|
|
- if (alchemy_logs != None and bool(alchemy_logs) != self.__class__.sqla_logging):
|
39
|
|
- #logging config changed for sqlalchemy
|
40
|
|
- self.__class__.restart()
|
|
48
|
+ self.sqlalogging = False if alchemy_logs == None else bool(alchemy_logs)
|
41
|
49
|
|
42
|
|
- if not self.__class__.started():
|
43
|
|
- self.__class__.start()
|
44
|
|
-
|
|
50
|
+ self.name = name
|
|
51
|
+
|
|
52
|
+ self.r_dbconf = read_db
|
|
53
|
+ self.w_dbconf = write_db
|
|
54
|
+
|
|
55
|
+ self.checkConf() #raise if errors in configuration
|
|
56
|
+
|
|
57
|
+ if name in self.__class__.wrapinstance:
|
|
58
|
+ logger.wraning("A SqlWrapper with the name "+name+" allready exist. Replacing the old one by the new one")
|
|
59
|
+ SqlWrapper.wrapinstance[name] = self
|
|
60
|
+
|
|
61
|
+ #Engine and wrapper initialisation
|
|
62
|
+ self.r_engine = self._getEngine(True, self.sqlalogging)
|
|
63
|
+ self.w_engine = self._getEngine(False, self.sqlalogging)
|
|
64
|
+ self.r_conn = None
|
|
65
|
+ self.w_conn = None
|
45
|
66
|
pass
|
46
|
67
|
|
47
|
|
- @classmethod
|
48
|
|
- def table(c, tname):
|
49
|
|
- """ Return a SqlAlchemy Table object
|
50
|
|
- @param o str: Table name
|
51
|
|
- @return a SqlAlchemy Table instance
|
|
68
|
+ @property
|
|
69
|
+ def cfg(self): return self.__class__.config;
|
|
70
|
+ @property
|
|
71
|
+ def engines_cfg(self): return self.__class__.ENGINES;
|
|
72
|
+
|
|
73
|
+ @property
|
|
74
|
+ def rconn(self):
|
|
75
|
+ """ Return the read connection
|
|
76
|
+ @warning Do not store the connection, call this method each time you need it
|
52
|
77
|
"""
|
53
|
|
- if not isinstance(tname, str):
|
54
|
|
- raise TypeError("Excepting a str but got a "+str(type(name)))
|
55
|
|
- return sqla.Table(o, sqla.MetaData())
|
|
78
|
+ return self.getConnection(True)
|
|
79
|
+ @property
|
|
80
|
+ def wconn(self):
|
|
81
|
+ """ Return the write connection
|
|
82
|
+ @warning Do not store the connection, call this method each time you need it
|
|
83
|
+ """
|
|
84
|
+ return self.getConnection(False)
|
56
|
85
|
|
57
|
|
- @classmethod
|
58
|
|
- def connect(c,read = None):
|
59
|
|
-
|
60
|
|
- if read == None:
|
61
|
|
- return c.connect(True) and c.coonect(False)
|
62
|
|
- elif read:
|
63
|
|
- c.rconn = c.rengine.connect()
|
64
|
|
- else:
|
65
|
|
- c.wconn = c.wengine.connect()
|
66
|
|
- return True #TODO attention c'est pas checké...
|
67
|
86
|
|
68
|
|
- @classmethod
|
69
|
|
- def conn(c, read=True):
|
|
87
|
+ def getConnection(self, read):
|
|
88
|
+ """ Return an opened connection
|
|
89
|
+ @param read bool: If true return the reading connection
|
|
90
|
+ @return A sqlAlchemy db connection
|
|
91
|
+ """
|
70
|
92
|
if read:
|
71
|
|
- res = c.rconn
|
|
93
|
+ r = self.r_conn
|
72
|
94
|
else:
|
73
|
|
- res = c.wconn
|
|
95
|
+ r = self.w_conn
|
74
|
96
|
|
75
|
|
- if res == None:
|
76
|
|
- if not c.connect(read):
|
77
|
|
- raise RuntimeError('Unable to connect to Db')
|
78
|
|
- return c.conn(read)
|
|
97
|
+ if r == None:
|
|
98
|
+ #Connection not yet opened
|
|
99
|
+ self.connect(read)
|
|
100
|
+ r = self.getConnection(read) #TODO : Un truc plus safe/propre qu'un appel reccursif ?
|
|
101
|
+ return r
|
79
|
102
|
|
80
|
|
- return c.rconn
|
81
|
103
|
|
82
|
|
- @classmethod
|
83
|
|
- def rc(c): return c.conn(True)
|
84
|
|
- @classmethod
|
85
|
|
- def wc(c): return c.conn(False)
|
86
|
|
-
|
87
|
|
- @classmethod
|
88
|
|
- def start(c, sqlalogging = None):
|
89
|
|
- """ Load engines
|
90
|
|
- Open connections to databases
|
91
|
|
- @param sqlalogging bool: overwrite class parameter about sqlalchemy logging
|
92
|
|
- @return False if already started
|
|
104
|
+ def connect(self, read = None):
|
|
105
|
+ """ Open a connection to a database
|
|
106
|
+ @param read bool|None: If None connect both, if True only connect the read side (False the write side)
|
|
107
|
+ @return None
|
93
|
108
|
"""
|
94
|
|
- c.checkConf()
|
95
|
|
- if c.started():
|
96
|
|
- logger.warning('Starting SqlWrapper but it is allready started')
|
97
|
|
- return False
|
|
109
|
+ if read or read == None:
|
|
110
|
+ if self.r_conn != None:
|
|
111
|
+ logger.debug(' SqlWrapper("'+self.name+'") Unable to connect, already connected')
|
|
112
|
+ else:
|
|
113
|
+ self.r_conn = self.r_engine.connect()
|
98
|
114
|
|
99
|
|
- c.rengine = c._getEngine(read=True, sqlalogging=None)
|
100
|
|
- c.wengine = c._getEngine(read=False, sqlalogging=None)
|
101
|
|
- return True
|
|
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()
|
102
|
120
|
|
103
|
|
- @classmethod
|
104
|
|
- def stop(c): c.rengine = c.wengine = None; pass
|
105
|
|
- @classmethod
|
106
|
|
- def restart(c): c.stop(); c.start(); pass
|
107
|
|
- @classmethod
|
108
|
|
- def started(c): return (c.rengine != None and c.rengine != None)
|
109
|
|
-
|
110
|
|
- @classmethod
|
111
|
|
- def _sqllog(c,sqlalogging = None):
|
112
|
|
- return bool(sqlalogging) if sqlalogging != None else c.sqla_logging
|
|
121
|
+ def disconnect(self, read = None):
|
|
122
|
+ """ Close a connection to a database
|
|
123
|
+ @param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
|
|
124
|
+ @return None
|
|
125
|
+ """
|
|
126
|
+ if read or read == None:
|
|
127
|
+ self.r_conn.close()
|
|
128
|
+ self.r_conn = None
|
|
129
|
+
|
|
130
|
+ if not read or read == None:
|
|
131
|
+ self.w_conn.close()
|
|
132
|
+ self.w_conn = None
|
|
133
|
+
|
|
134
|
+ def reconnect(self, read = None):
|
|
135
|
+ """ Close and reopen a connection to a database
|
|
136
|
+ @param read bool|None: If None disconnect both, if True only connect the read side (False the write side)
|
|
137
|
+ @return None
|
|
138
|
+ """
|
|
139
|
+ self.disconnect(read)
|
|
140
|
+ self.connect(read)
|
113
|
141
|
|
114
|
142
|
@classmethod
|
115
|
|
- def _getEngine(c, read=True, sqlalogging = None):
|
|
143
|
+ def reconnectAll(c, read = None):
|
|
144
|
+ for wname in c.wrapinstance:
|
|
145
|
+ c.wrapinstance[wname].reconnect(read)
|
|
146
|
+
|
|
147
|
+ def Table(self, tname):
|
|
148
|
+ """ Instanciate a new SqlAlchemy Table
|
|
149
|
+ @param tname str: The table name
|
|
150
|
+ @return A new instance of SqlAlchemy::Table
|
|
151
|
+ """
|
|
152
|
+ if not isinstance(tname, str):
|
|
153
|
+ return TypeError('Excepting a <class str> but got a '+str(type(tname)))
|
|
154
|
+ return sqla.Table(tname, sqla.MetaData(), autoload_with=self.r_engine, autoload=True)
|
|
155
|
+
|
|
156
|
+ def _getEngine(self, read=True, sqlalogging = None):
|
116
|
157
|
""" Return a sqlalchemy engine
|
117
|
158
|
@param read bool: If True return the read engine, else
|
118
|
159
|
return the write one
|
|
@@ -121,16 +162,10 @@ class SqlWrapper(object):
|
121
|
162
|
@todo Put the check on db config in SqlWrapper.checkConf()
|
122
|
163
|
"""
|
123
|
164
|
#Loading confs
|
124
|
|
- connconf = 'dbread' if read else 'dbwrite'
|
125
|
|
- dbconf = connconf if connconf in c.config['db'] else 'default'
|
|
165
|
+ cfg = self.cfg['db'][self.r_dbconf if read else self.w_dbconf]
|
126
|
166
|
|
127
|
|
- if dbconf not in c.config['db']: #This check should not be here
|
128
|
|
- raise NameError('Configuration error no db "'+dbconf+'" in configuration files')
|
129
|
|
-
|
130
|
|
- cfg = c.config['db'][dbconf] #Database config
|
131
|
|
-
|
132
|
|
- edata = c.config['engines'][cfg['ENGINE']] #engine infos
|
133
|
|
- conn_str = cfg['ENGINE']+'+'+edata['driver']+'://'
|
|
167
|
+ edata = self.engines_cfg[cfg['ENGINE']] #engine infos
|
|
168
|
+ conn_str = ""
|
134
|
169
|
|
135
|
170
|
if cfg['ENGINE'] == 'sqlite':
|
136
|
171
|
#Sqlite connection string
|
|
@@ -153,10 +188,21 @@ class SqlWrapper(object):
|
153
|
188
|
conn_str = '%s+%s://'%(cfg['ENGINE'], edata['driver'])
|
154
|
189
|
conn_str += '%s@%s/%s'%(user,host,cfg['NAME'])
|
155
|
190
|
|
156
|
|
- return sqla.create_engine(conn_str, encoding=edata['encoding'], echo=c._sqllog(sqlalogging))
|
|
191
|
+ return sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
|
157
|
192
|
|
158
|
193
|
@classmethod
|
159
|
|
- def checkConf(c):
|
|
194
|
+ def getWrapper(c, name):
|
|
195
|
+ """ Return a wrapper instance from a wrapper name
|
|
196
|
+ @param name str: The wrapper name
|
|
197
|
+ @return a SqlWrapper instance
|
|
198
|
+
|
|
199
|
+ @throw KeyError
|
|
200
|
+ """
|
|
201
|
+ if name not in c.wrapinstance:
|
|
202
|
+ raise KeyError("No wrapper named '"+name+"' exists")
|
|
203
|
+ return c.wrapinstance[name]
|
|
204
|
+
|
|
205
|
+ def checkConf(self):
|
160
|
206
|
""" Class method that check the configuration
|
161
|
207
|
|
162
|
208
|
Configuration looks like
|
|
@@ -172,44 +218,27 @@ class SqlWrapper(object):
|
172
|
218
|
- dbwrite (mandatory if no default db)
|
173
|
219
|
"""
|
174
|
220
|
err = []
|
175
|
|
- if 'db' not in c.config:
|
|
221
|
+ if 'db' not in self.cfg:
|
176
|
222
|
err.append('Missing "db" in configuration')
|
177
|
223
|
else:
|
178
|
|
- if 'default' not in c.config['db']:
|
179
|
|
- if 'dbread' not in c.config:
|
180
|
|
- err.append('Missing "dbread" in configuration and no "default" db declared')
|
181
|
|
- if 'dbwrite' not in c.config:
|
182
|
|
- err.append('Missing "dbwrite" in configuration and no "default" db declared')
|
183
|
|
- for dbname in c.config['db']:
|
184
|
|
- db = c.config['db'][dbname]
|
185
|
|
- if 'ENGINE' not in db:
|
186
|
|
- err.append('Missing "ENGINE" in database "'+db+'"')
|
|
224
|
+ for dbname in [self.r_dbconf, self.w_dbconf]:
|
|
225
|
+ if dbname not in self.cfg['db']:
|
|
226
|
+ err.append('Missing "'+dbname+'" db configuration')
|
187
|
227
|
else:
|
188
|
|
- if db['ENGINE'] != 'sqlite' and 'USER' not in db:
|
189
|
|
- err.append('Missing "USER" in database "'+db+'"')
|
190
|
|
- if 'NAME' not in db:
|
191
|
|
- err.append('Missing "NAME" in database "'+db+'"')
|
192
|
|
-
|
193
|
|
- if len(c.config['engines']) == 0:
|
194
|
|
- err.append('Missing "engines" in configuration')
|
195
|
|
- for ename in c.config['engines']:
|
196
|
|
- engine = c.config['engines'][ename]
|
197
|
|
- if 'driver' not in engine:
|
198
|
|
- err.append('Missing "driver" in database engine "'+ename+'"')
|
199
|
|
- if 'encoding' not in engine:
|
200
|
|
- err.append('Missing "encoding" in database engine "'+ename+'"')
|
201
|
|
-
|
|
228
|
+ db = self.cfg['db'][dbname]
|
|
229
|
+ if 'ENGINE' not in db:
|
|
230
|
+ err.append('Missing "ENGINE" in database "'+db+'"')
|
|
231
|
+ else:
|
|
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
|
+
|
202
|
239
|
if len(err)>0:
|
203
|
240
|
err_str = "\n"
|
204
|
241
|
for e in err:
|
205
|
242
|
err_str += "\t\t"+e+"\n"
|
206
|
243
|
raise NameError('Configuration errors in LODEL2SQLWRAPPER:'+err_str)
|
207
|
244
|
|
208
|
|
-
|
209
|
|
- @property
|
210
|
|
- def cfg(self):
|
211
|
|
- """ Get the dict of options for the wrapper
|
212
|
|
- Its an alias to the classes property SqlWrapper.config
|
213
|
|
- @return a dict containing the Db settings"""
|
214
|
|
- return self.__class__.config
|
215
|
|
-
|