Add transactions support
[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 os
21 import sqlite3
22 import cherrypy
23 from ipsilon.util.log import Log
24 import uuid
25
26
27 OPTIONS_COLUMNS = ['name', 'option', 'value']
28 UNIQUE_DATA_COLUMNS = ['uuid', 'name', 'value']
29
30
31 class Store(Log):
32
33     def __init__(self, name):
34         self._dbname = name
35
36     def _build_where(self, kvfilter, kvout):
37         where = ""
38         sep = "WHERE"
39         for k in kvfilter:
40             mk = "where_%s" % k
41             kvout[mk] = kvfilter[k]
42             where += "%s %s=:%s" % (sep, k, mk)
43             sep = " AND"
44         return where
45
46     def _build_select(self, table, kvfilter=None, kvout=None, columns=None):
47         SELECT = "SELECT %(cols)s FROM %(table)s %(where)s"
48         cols = "*"
49         if columns:
50             cols = ",".join(columns)
51         where = ""
52         if kvfilter is not None:
53             where = self._build_where(kvfilter, kvout)
54         return SELECT % {'table': table, 'cols': cols, 'where': where}
55
56     def _select(self, cursor, table, kvfilter=None, columns=None):
57         kv = dict()
58         select = self._build_select(table, kvfilter, kv, columns)
59         cursor.execute(select, kv)
60         return cursor.fetchall()
61
62     def _create(self, cursor, table, columns):
63         CREATE = "CREATE TABLE IF NOT EXISTS %(table)s(%(cols)s)"
64         cols = ",".join(columns)
65         create = CREATE % {'table': table, 'cols': cols}
66         cursor.execute(create)
67
68     def _update(self, cursor, table, values, kvfilter):
69         UPDATE = "UPDATE %(table)s SET %(setval)s %(where)s"
70         kv = dict()
71
72         setval = ""
73         sep = ""
74         for k in values:
75             mk = "setval_%s" % k
76             kv[mk] = values[k]
77             setval += "%s%s=:%s" % (sep, k, mk)
78             sep = " , "
79
80         where = self._build_where(kvfilter, kv)
81
82         update = UPDATE % {'table': table, 'setval': setval, 'where': where}
83         cursor.execute(update, kv)
84
85     def _insert(self, cursor, table, values):
86         INSERT = "INSERT INTO %(table)s VALUES(%(values)s)"
87         vals = ""
88         sep = ""
89         for _ in values:
90             vals += "%s?" % sep
91             sep = ","
92         insert = INSERT % {'table': table, 'values': vals}
93         cursor.execute(insert, values)
94
95     def _delete(self, cursor, table, kvfilter):
96         DELETE = "DELETE FROM %(table)s %(where)s"
97         kv = dict()
98         where = self._build_where(kvfilter, kv)
99         delete = DELETE % {'table': table, 'where': where}
100         cursor.execute(delete, kv)
101
102     def _row_to_dict_tree(self, data, row):
103         name = row[0]
104         if len(row) > 2:
105             if name not in data:
106                 data[name] = dict()
107             d2 = data[name]
108             self._row_to_dict_tree(d2, row[1:])
109         else:
110             value = row[1]
111             if name in data:
112                 if data[name] is list:
113                     data[name].append(value)
114                 else:
115                     v = data[name]
116                     data[name] = [v, value]
117             else:
118                 data[name] = value
119
120     def _rows_to_dict_tree(self, rows):
121         data = dict()
122         for r in rows:
123             self._row_to_dict_tree(data, r)
124         return data
125
126     def _load_data(self, table, columns, kvfilter=None):
127         con = None
128         rows = []
129         try:
130             con = sqlite3.connect(self._dbname)
131             cur = con.cursor()
132             self._create(cur, table, columns)
133             rows = self._select(cur, table, kvfilter)
134             con.commit()
135         except sqlite3.Error, e:
136             if con:
137                 con.rollback()
138             self.error("Failed to load data for table %s: [%s]" % (table, e))
139         finally:
140             if con:
141                 con.close()
142
143         return self._rows_to_dict_tree(rows)
144
145     def load_config(self):
146         table = 'config'
147         columns = ['name', 'value']
148         return self._load_data(table, columns)
149
150     def load_options(self, table, name=None):
151         kvfilter = dict()
152         if name:
153             kvfilter['name'] = name
154         options = self._load_data(table, OPTIONS_COLUMNS, kvfilter)
155         if name and name in options:
156             return options[name]
157         return options
158
159     def save_options(self, table, name, options):
160         curvals = dict()
161         con = None
162         try:
163             con = sqlite3.connect(self._dbname)
164             cur = con.cursor()
165             self._create(cur, table, OPTIONS_COLUMNS)
166             rows = self._select(cur, table, {'name': name},
167                                 ['option', 'value'])
168             for row in rows:
169                 curvals[row[0]] = row[1]
170
171             for opt in options:
172                 if opt in curvals:
173                     self._update(cur, table,
174                                  {'value': options[opt]},
175                                  {'name': name, 'option': opt})
176                 else:
177                     self._insert(cur, table, (name, opt, options[opt]))
178
179             con.commit()
180         except sqlite3.Error, e:
181             if con:
182                 con.rollback()
183             self.error("Failed to store config: [%s]" % e)
184             raise
185         finally:
186             if con:
187                 con.close()
188
189     def delete_options(self, table, name, options=None):
190         kvfilter = {'name': name}
191         try:
192             con = sqlite3.connect(self._dbname)
193             cur = con.cursor()
194             self._create(cur, table, OPTIONS_COLUMNS)
195             if options is None:
196                 self._delete(cur, table, kvfilter)
197             else:
198                 for opt in options:
199                     kvfilter['option'] = opt
200                     self._delete(cur, table, kvfilter)
201             con.commit()
202         except sqlite3.Error, e:
203             if con:
204                 con.rollback()
205             self.error("Failed to delete from %s: [%s]" % (table, e))
206             raise
207         finally:
208             if con:
209                 con.close()
210
211     def new_unique_data(self, table, data):
212         con = None
213         try:
214             con = sqlite3.connect(self._dbname)
215             cur = con.cursor()
216             self._create(cur, table, UNIQUE_DATA_COLUMNS)
217             newid = str(uuid.uuid4())
218             for name in data:
219                 self._insert(cur, table, (newid, name, data[name]))
220             con.commit()
221         except sqlite3.Error, e:
222             if con:
223                 con.rollback()
224             cherrypy.log.error("Failed to store %s data: [%s]" % (table, e))
225             raise
226         finally:
227             if con:
228                 con.close()
229         return newid
230
231     def get_unique_data(self, table, uuidval=None, name=None, value=None):
232         kvfilter = dict()
233         if uuidval:
234             kvfilter['uuid'] = uuidval
235         if name:
236             kvfilter['name'] = name
237         if value:
238             kvfilter['value'] = value
239         return self._load_data(table, UNIQUE_DATA_COLUMNS, kvfilter)
240
241     def save_unique_data(self, table, data):
242         curvals = dict()
243         con = None
244         try:
245             con = sqlite3.connect(self._dbname)
246             cur = con.cursor()
247             self._create(cur, table, UNIQUE_DATA_COLUMNS)
248             for uid in data:
249                 curvals = dict()
250                 rows = self._select(cur, table, {'uuid': uid},
251                                     ['name', 'value'])
252                 for r in rows:
253                     curvals[r[0]] = r[1]
254
255                 datum = data[uid]
256                 for name in datum:
257                     if name in curvals:
258                         self._update(cur, table,
259                                      {'value': datum[name]},
260                                      {'uuid': uid, 'name': name})
261                     else:
262                         self._insert(cur, table, (uid, name, datum[name]))
263
264             con.commit()
265         except sqlite3.Error, e:
266             if con:
267                 con.rollback()
268             self.error("Failed to store data in %s: [%s]" % (table, e))
269             raise
270         finally:
271             if con:
272                 con.close()
273
274     def del_unique_data(self, table, uuidval):
275         kvfilter = {'uuid': uuidval}
276         con = None
277         try:
278             con = sqlite3.connect(self._dbname)
279             cur = con.cursor()
280             self._delete(cur, table, kvfilter)
281         except sqlite3.Error, e:
282             self.error("Failed to delete data from %s: [%s]" % (table, e))
283         finally:
284             if con:
285                 con.close()
286
287
288 class AdminStore(Store):
289
290     def __init__(self, path=None):
291         if path is None:
292             self._path = os.getcwd()
293         else:
294             self._path = path
295         self._name = None
296         if 'admin.config.db' in cherrypy.config:
297             self._name = cherrypy.config['admin.config.db']
298         if not self._name:
299             self._name = os.path.join(self._path, 'adminconfig.sqlite')
300         super(AdminStore, self).__init__(self._name)
301
302     def get_data(self, plugin, idval=None, name=None, value=None):
303         return self.get_unique_data(plugin+"_data", idval, name, value)
304
305     def save_data(self, plugin, data):
306         return self.save_unique_data(plugin+"_data", data)
307
308     def new_datum(self, plugin, datum):
309         table = plugin+"_data"
310         return self.new_unique_data(table, datum)
311
312     def del_datum(self, plugin, idval):
313         table = plugin+"_data"
314         return self.del_unique_data(table, idval)
315
316     def wipe_data(self, plugin):
317         table = plugin+"_data"
318         # Try to backup old data first, just in case
319         try:
320             con = sqlite3.connect(self._dbname)
321             cur = con.cursor()
322             cur.execute("DROP TABLE IF EXISTS " + table)
323             self._create(cur, table, UNIQUE_DATA_COLUMNS)
324             con.commit()
325         except sqlite3.Error, e:
326             if con:
327                 con.rollback()
328             cherrypy.log.error("Failed to wipe %s data: [%s]" % (plugin, e))
329             raise
330         finally:
331             if con:
332                 con.close()
333
334
335 class UserStore(Store):
336
337     def __init__(self, path=None):
338         if path is None:
339             self._path = os.getcwd()
340         else:
341             self._path = path
342         self._name = None
343         if 'user.prefs.db' in cherrypy.config:
344             self._name = cherrypy.config['user.prefs.db']
345         if not self._name:
346             self._name = os.path.join(self._path, 'userprefs.sqlite')
347         super(UserStore, self).__init__(self._name)
348
349     def save_user_preferences(self, user, options):
350         return self.save_options('users', user, options)
351
352
353 class TranStore(Store):
354
355     def __init__(self, path=None):
356         if path is None:
357             self._path = os.getcwd()
358         else:
359             self._path = path
360         self._name = None
361         if 'transactions.db' in cherrypy.config:
362             self._name = cherrypy.config['transactions.db']
363         if not self._name:
364             self._name = os.path.join(self._path, 'transactions.sqlite')
365         super(TranStore, self).__init__(self._name)