Allow to pass drectly a URL to the Store class
[cascardo/ipsilon.git] / ipsilon / util / data.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import cherrypy
21 from ipsilon.util.log import Log
22 from sqlalchemy import create_engine
23 from sqlalchemy import MetaData, Table, Column, Text
24 from sqlalchemy.sql import select
25 import ConfigParser
26 import os
27 import uuid
28
29
30 OPTIONS_COLUMNS = ['name', 'option', 'value']
31 UNIQUE_DATA_COLUMNS = ['uuid', 'name', 'value']
32
33
34 class SqlStore(Log):
35
36     def __init__(self, name):
37         engine_name = name
38         if '://' not in engine_name:
39             engine_name = 'sqlite:///' + engine_name
40         self._dbengine = create_engine(engine_name)
41         self.is_readonly = False
42
43     def engine(self):
44         return self._dbengine
45
46     def connection(self):
47         return self._dbengine.connect()
48
49
50 def SqlAutotable(f):
51     def at(self, *args, **kwargs):
52         self.create()
53         return f(self, *args, **kwargs)
54     return at
55
56
57 class SqlQuery(Log):
58
59     def __init__(self, db_obj, table, columns, trans=True):
60         self._db = db_obj
61         self._con = self._db.connection()
62         self._trans = self._con.begin() if trans else None
63         self._table = self._get_table(table, columns)
64
65     def _get_table(self, name, columns):
66         table = Table(name, MetaData(self._db.engine()))
67         for c in columns:
68             table.append_column(Column(c, Text()))
69         return table
70
71     def _where(self, kvfilter):
72         where = None
73         if kvfilter is not None:
74             for k in kvfilter:
75                 w = self._table.columns[k] == kvfilter[k]
76                 if where is None:
77                     where = w
78                 else:
79                     where = where & w
80         return where
81
82     def _columns(self, columns=None):
83         cols = None
84         if columns is not None:
85             cols = []
86             for c in columns:
87                 cols.append(self._table.columns[c])
88         else:
89             cols = self._table.columns
90         return cols
91
92     def rollback(self):
93         self._trans.rollback()
94
95     def commit(self):
96         self._trans.commit()
97
98     def create(self):
99         self._table.create(checkfirst=True)
100
101     def drop(self):
102         self._table.drop(checkfirst=True)
103
104     @SqlAutotable
105     def select(self, kvfilter=None, columns=None):
106         return self._con.execute(select(self._columns(columns),
107                                         self._where(kvfilter)))
108
109     @SqlAutotable
110     def insert(self, values):
111         self._con.execute(self._table.insert(values))
112
113     @SqlAutotable
114     def update(self, values, kvfilter):
115         self._con.execute(self._table.update(self._where(kvfilter), values))
116
117     @SqlAutotable
118     def delete(self, kvfilter):
119         self._con.execute(self._table.delete(self._where(kvfilter)))
120
121
122 class FileStore(Log):
123
124     def __init__(self, name):
125         self._filename = name
126         self.is_readonly = True
127         self._timestamp = None
128         self._config = None
129
130     def get_config(self):
131         try:
132             stat = os.stat(self._filename)
133         except OSError, e:
134             self.error("Unable to check config file %s: [%s]" % (
135                 self._filename, e))
136             self._config = None
137             raise
138         timestamp = stat.st_mtime
139         if self._config is None or timestamp > self._timestamp:
140             self._config = ConfigParser.RawConfigParser()
141             self._config.read(self._filename)
142         return self._config
143
144
145 class FileQuery(Log):
146
147     def __init__(self, fstore, table, columns, trans=True):
148         self._fstore = fstore
149         self._config = fstore.get_config()
150         self._section = table
151         if len(columns) > 3 or columns[-1] != 'value':
152             raise ValueError('Unsupported configuration format')
153         self._columns = columns
154
155     def rollback(self):
156         return
157
158     def commit(self):
159         return
160
161     def create(self):
162         raise NotImplementedError
163
164     def drop(self):
165         raise NotImplementedError
166
167     def select(self, kvfilter=None, columns=None):
168         if self._section not in self._config.sections():
169             return []
170
171         opts = self._config.options(self._section)
172
173         prefix = None
174         prefix_ = ''
175         if self._columns[0] in kvfilter:
176             prefix = kvfilter[self._columns[0]]
177             prefix_ = prefix + ' '
178
179         name = None
180         if len(self._columns) == 3 and self._columns[1] in kvfilter:
181             name = kvfilter[self._columns[1]]
182
183         value = None
184         if self._columns[-1] in kvfilter:
185             value = kvfilter[self._columns[-1]]
186
187         res = []
188         for o in opts:
189             if len(self._columns) == 3:
190                 # 3 cols
191                 if prefix and not o.startswith(prefix_):
192                     continue
193
194                 col1, col2 = o.split(' ', 1)
195                 if name and col2 != name:
196                     continue
197
198                 col3 = self._config.get(self._section, o)
199                 if value and col3 != value:
200                     continue
201
202                 r = [col1, col2, col3]
203             else:
204                 # 2 cols
205                 if prefix and o != prefix:
206                     continue
207                 r = [o, self._config.get(self._section, o)]
208
209             if columns:
210                 s = []
211                 for c in columns:
212                     s.append(r[self._columns.index(c)])
213                 res.append(s)
214             else:
215                 res.append(r)
216
217         self.debug('SELECT(%s, %s, %s) -> %s' % (self._section,
218                                                  repr(kvfilter),
219                                                  repr(columns),
220                                                  repr(res)))
221         return res
222
223     def insert(self, values):
224         raise NotImplementedError
225
226     def update(self, values, kvfilter):
227         raise NotImplementedError
228
229     def delete(self, kvfilter):
230         raise NotImplementedError
231
232
233 class Store(Log):
234     def __init__(self, config_name=None, database_url=None):
235         if config_name is None and database_url is None:
236             raise ValueError('config_name or database_url must be provided')
237         if config_name:
238             if config_name not in cherrypy.config:
239                 raise NameError('Unknown database %s' % config_name)
240             name = cherrypy.config[config_name]
241         else:
242             name = database_url
243         if name.startswith('configfile://'):
244             _, filename = name.split('://')
245             self._db = FileStore(filename)
246             self._query = FileQuery
247         else:
248             self._db = SqlStore(name)
249             self._query = SqlQuery
250
251     @property
252     def is_readonly(self):
253         return self._db.is_readonly
254
255     def _row_to_dict_tree(self, data, row):
256         name = row[0]
257         if len(row) > 2:
258             if name not in data:
259                 data[name] = dict()
260             d2 = data[name]
261             self._row_to_dict_tree(d2, row[1:])
262         else:
263             value = row[1]
264             if name in data:
265                 if data[name] is list:
266                     data[name].append(value)
267                 else:
268                     v = data[name]
269                     data[name] = [v, value]
270             else:
271                 data[name] = value
272
273     def _rows_to_dict_tree(self, rows):
274         data = dict()
275         for r in rows:
276             self._row_to_dict_tree(data, r)
277         return data
278
279     def _load_data(self, table, columns, kvfilter=None):
280         rows = []
281         try:
282             q = self._query(self._db, table, columns, trans=False)
283             rows = q.select(kvfilter)
284         except Exception, e:  # pylint: disable=broad-except
285             self.error("Failed to load data for table %s: [%s]" % (table, e))
286         return self._rows_to_dict_tree(rows)
287
288     def load_config(self):
289         table = 'config'
290         columns = ['name', 'value']
291         return self._load_data(table, columns)
292
293     def load_options(self, table, name=None):
294         kvfilter = dict()
295         if name:
296             kvfilter['name'] = name
297         options = self._load_data(table, OPTIONS_COLUMNS, kvfilter)
298         if name and name in options:
299             return options[name]
300         return options
301
302     def save_options(self, table, name, options):
303         curvals = dict()
304         q = None
305         try:
306             q = self._query(self._db, table, OPTIONS_COLUMNS)
307             rows = q.select({'name': name}, ['option', 'value'])
308             for row in rows:
309                 curvals[row[0]] = row[1]
310
311             for opt in options:
312                 if opt in curvals:
313                     q.update({'value': options[opt]},
314                              {'name': name, 'option': opt})
315                 else:
316                     q.insert((name, opt, options[opt]))
317
318             q.commit()
319         except Exception, e:  # pylint: disable=broad-except
320             if q:
321                 q.rollback()
322             self.error("Failed to save options: [%s]" % e)
323             raise
324
325     def delete_options(self, table, name, options=None):
326         kvfilter = {'name': name}
327         q = None
328         try:
329             q = self._query(self._db, table, OPTIONS_COLUMNS)
330             if options is None:
331                 q.delete(kvfilter)
332             else:
333                 for opt in options:
334                     kvfilter['option'] = opt
335                     q.delete(kvfilter)
336             q.commit()
337         except Exception, e:  # pylint: disable=broad-except
338             if q:
339                 q.rollback()
340             self.error("Failed to delete from %s: [%s]" % (table, e))
341             raise
342
343     def new_unique_data(self, table, data):
344         newid = str(uuid.uuid4())
345         q = None
346         try:
347             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
348             for name in data:
349                 q.insert((newid, name, data[name]))
350             q.commit()
351         except Exception, e:  # pylint: disable=broad-except
352             if q:
353                 q.rollback()
354             self.error("Failed to store %s data: [%s]" % (table, e))
355             raise
356         return newid
357
358     def get_unique_data(self, table, uuidval=None, name=None, value=None):
359         kvfilter = dict()
360         if uuidval:
361             kvfilter['uuid'] = uuidval
362         if name:
363             kvfilter['name'] = name
364         if value:
365             kvfilter['value'] = value
366         return self._load_data(table, UNIQUE_DATA_COLUMNS, kvfilter)
367
368     def save_unique_data(self, table, data):
369         q = None
370         try:
371             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
372             for uid in data:
373                 curvals = dict()
374                 rows = q.select({'uuid': uid}, ['name', 'value'])
375                 for r in rows:
376                     curvals[r[0]] = r[1]
377
378                 datum = data[uid]
379                 for name in datum:
380                     if name in curvals:
381                         q.update({'value': datum[name]},
382                                  {'uuid': uid, 'name': name})
383                     else:
384                         q.insert((uid, name, datum[name]))
385
386             q.commit()
387         except Exception, e:  # pylint: disable=broad-except
388             if q:
389                 q.rollback()
390             self.error("Failed to store data in %s: [%s]" % (table, e))
391             raise
392
393     def del_unique_data(self, table, uuidval):
394         kvfilter = {'uuid': uuidval}
395         try:
396             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS, trans=False)
397             q.delete(kvfilter)
398         except Exception, e:  # pylint: disable=broad-except
399             self.error("Failed to delete data from %s: [%s]" % (table, e))
400
401     def _reset_data(self, table):
402         try:
403             q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
404             q.drop()
405             q.create()
406             q.commit()
407         except Exception, e:  # pylint: disable=broad-except
408             if q:
409                 q.rollback()
410             self.error("Failed to erase all data from %s: [%s]" % (table, e))
411
412
413 class AdminStore(Store):
414
415     def __init__(self):
416         super(AdminStore, self).__init__('admin.config.db')
417
418     def get_data(self, plugin, idval=None, name=None, value=None):
419         return self.get_unique_data(plugin+"_data", idval, name, value)
420
421     def save_data(self, plugin, data):
422         return self.save_unique_data(plugin+"_data", data)
423
424     def new_datum(self, plugin, datum):
425         table = plugin+"_data"
426         return self.new_unique_data(table, datum)
427
428     def del_datum(self, plugin, idval):
429         table = plugin+"_data"
430         return self.del_unique_data(table, idval)
431
432     def wipe_data(self, plugin):
433         table = plugin+"_data"
434         self._reset_data(table)
435
436
437 class UserStore(Store):
438
439     def __init__(self, path=None):
440         super(UserStore, self).__init__('user.prefs.db')
441
442     def save_user_preferences(self, user, options):
443         self.save_options('users', user, options)
444
445     def load_user_preferences(self, user):
446         return self.load_options('users', user)
447
448     def save_plugin_data(self, plugin, user, options):
449         self.save_options(plugin+"_data", user, options)
450
451     def load_plugin_data(self, plugin, user):
452         return self.load_options(plugin+"_data", user)
453
454
455 class TranStore(Store):
456
457     def __init__(self, path=None):
458         super(TranStore, self).__init__('transactions.db')