Add options to explicitly set database uris during install
[cascardo/ipsilon.git] / ipsilon / util / data.py
old mode 100755 (executable)
new mode 100644 (file)
index ec32b43..72e7f96
@@ -1,5 +1,3 @@
-#!/usr/bin/python
-#
 # Copyright (C) 2013  Simo Sorce <simo@redhat.com>
 #
 # see file 'COPYING' for use and warranty information
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import os
-import sqlite3
 import cherrypy
+from ipsilon.util.log import Log
+from sqlalchemy import create_engine
+from sqlalchemy import MetaData, Table, Column, Text
+from sqlalchemy.sql import select
+import ConfigParser
+import os
+import uuid
 
 
-class Store(object):
+OPTIONS_COLUMNS = ['name', 'option', 'value']
+UNIQUE_DATA_COLUMNS = ['uuid', 'name', 'value']
 
-    def __init__(self, path=None):
-        if path is None:
-            self._path = os.getcwd()
-        else:
-            self._path = path
-        self._admin_dbname = self._get_admin_dbname()
-        self._user_dbname = self._get_userprefs_dbname()
-
-    def _get_admin_dbname(self):
-        path = None
-        if 'admin.config.db' in cherrypy.config:
-            path = cherrypy.config['admin.config.db']
-        if not path:
-            path = os.path.join(self._path, 'adminconfig.sqlite')
-        return path
-
-    def _get_userprefs_dbname(self):
-        path = None
-        if 'user.prefs.db' in cherrypy.config:
-            path = cherrypy.config['user.prefs.db']
-        if not path:
-            path = os.path.join(self._path, 'userprefs.sqlite')
-        return path
-
-    def _load_config(self, dbname):
-        con = None
-        rows = []
-        try:
-            con = sqlite3.connect(dbname)
-            cur = con.cursor()
-            cur.executescript("""
-                CREATE TABLE IF NOT EXISTS config(name TEXT, value TEXT)
-                """)
-            cur.execute("SELECT * FROM config")
-            rows = cur.fetchall()
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to load config: [%s]" % e)
-        finally:
-            if con:
-                con.close()
-
-        conf = {}
-        for row in rows:
-            if row[0] in conf:
-                # multivalued
-                if conf[row[0]] is list:
-                    conf[row[0]].append(row[1])
-                else:
-                    v = conf[row[0]]
-                    conf[row[0]] = [v, row[1]]
-            else:
-                conf[row[0]] = row[1]
 
-        return conf
+class SqlStore(Log):
 
-    def get_admin_config(self):
-        return self._load_config(self._admin_dbname)
+    def __init__(self, name):
+        engine_name = name
+        if '://' not in engine_name:
+            engine_name = 'sqlite:///' + engine_name
+        self._dbengine = create_engine(engine_name)
+        self.is_readonly = False
 
-    def _load_user_prefs(self, dbname, user):
-        con = None
-        rows = []
-        try:
-            con = sqlite3.connect(dbname)
-            cur = con.cursor()
-            cur.executescript("""
-                CREATE TABLE IF NOT EXISTS users(name TEXT,
-                                                 option TEXT,
-                                                 value TEXT)
-                """)
-            cur.execute("SELECT option, value FROM users "
-                        "where name = '%s'" % user)
-            rows = cur.fetchall()
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to load %s's prefs from "
-                               "%s: [%s]" % (user, dbname, e))
-        finally:
-            if con:
-                con.close()
-
-        conf = {}
-        for row in rows:
-            conf[row[0]] = row[1]
-
-        return conf
-
-    def get_user_preferences(self, user):
-        return self._load_user_prefs(self._user_dbname, user)
+    def engine(self):
+        return self._dbengine
 
-    def save_user_preferences(self, user, options):
-        SELECT = "SELECT option, value FROM users WHERE name=?"
-        UPDATE = "UPDATE users SET value=? WHERE name=? AND option=?"
-        INSERT = "INSERT INTO users VALUES(?,?,?)"
-        con = None
-        try:
-            con = sqlite3.connect(self._user_dbname)
-            cur = con.cursor()
-            cur.execute("""
-                CREATE TABLE IF NOT EXISTS users(name TEXT,
-                                                 option TEXT,
-                                                 value TEXT)
-                """)
-            curvals = dict()
-            for row in cur.execute(SELECT, (user,)):
-                curvals[row[0]] = row[1]
+    def connection(self):
+        return self._dbengine.connect()
 
-            for name in options:
-                if name in curvals:
-                    cur.execute(UPDATE, (options[name], user, name))
-                else:
-                    cur.execute(INSERT, (user, name, options[name]))
 
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to store config: [%s]" % e)
-            raise
-        finally:
-            if con:
-                con.close()
+def SqlAutotable(f):
+    def at(self, *args, **kwargs):
+        self.create()
+        return f(self, *args, **kwargs)
+    return at
 
-    def get_plugins_config(self, facility):
-        con = None
-        rows = []
-        try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("CREATE TABLE IF NOT EXISTS " +
-                        facility + " (name TEXT,option TEXT,value TEXT)")
-            cur.execute("SELECT * FROM " + facility)
-            rows = cur.fetchall()
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to load %s config: [%s]" % (facility,
-                                                                   e))
-        finally:
-            if con:
-                con.close()
-
-        lpo = []
-        plco = dict()
-        for row in rows:
-            if row[0] == 'global':
-                if row[1] == 'order':
-                    lpo = row[2].split(',')
-                continue
-            if row[0] not in plco:
-                # one dict per provider
-                plco[row[0]] = dict()
-
-            conf = plco[row[0]]
-            if row[1] in conf:
-                if conf[row[1]] is list:
-                    conf[row[1]].append(row[2])
+
+class SqlQuery(Log):
+
+    def __init__(self, db_obj, table, columns, trans=True):
+        self._db = db_obj
+        self._con = self._db.connection()
+        self._trans = self._con.begin() if trans else None
+        self._table = self._get_table(table, columns)
+
+    def _get_table(self, name, columns):
+        table = Table(name, MetaData(self._db.engine()))
+        for c in columns:
+            table.append_column(Column(c, Text()))
+        return table
+
+    def _where(self, kvfilter):
+        where = None
+        if kvfilter is not None:
+            for k in kvfilter:
+                w = self._table.columns[k] == kvfilter[k]
+                if where is None:
+                    where = w
                 else:
-                    v = conf[row[1]]
-                    conf[row[1]] = [v, row[2]]
-            else:
-                conf[row[1]] = row[2]
+                    where = where & w
+        return where
+
+    def _columns(self, columns=None):
+        cols = None
+        if columns is not None:
+            cols = []
+            for c in columns:
+                cols.append(self._table.columns[c])
+        else:
+            cols = self._table.columns
+        return cols
 
-        return (lpo, plco)
+    def rollback(self):
+        self._trans.rollback()
 
-    def get_plugin_config(self, facility, plugin):
-        con = None
-        rows = []
+    def commit(self):
+        self._trans.commit()
+
+    def create(self):
+        self._table.create(checkfirst=True)
+
+    def drop(self):
+        self._table.drop(checkfirst=True)
+
+    @SqlAutotable
+    def select(self, kvfilter=None, columns=None):
+        return self._con.execute(select(self._columns(columns),
+                                        self._where(kvfilter)))
+
+    @SqlAutotable
+    def insert(self, values):
+        self._con.execute(self._table.insert(values))
+
+    @SqlAutotable
+    def update(self, values, kvfilter):
+        self._con.execute(self._table.update(self._where(kvfilter), values))
+
+    @SqlAutotable
+    def delete(self, kvfilter):
+        self._con.execute(self._table.delete(self._where(kvfilter)))
+
+
+class FileStore(Log):
+
+    def __init__(self, name):
+        self._filename = name
+        self.is_readonly = True
+        self._timestamp = None
+        self._config = None
+
+    def get_config(self):
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("CREATE TABLE IF NOT EXISTS " +
-                        facility + " (name TEXT,option TEXT,value TEXT)")
-            cur.execute("SELECT option, value FROM " +
-                        facility + " WHERE name=?", (plugin,))
-            rows = cur.fetchall()
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            fpe = (facility, plugin, e)
-            cherrypy.log.error("Failed to get %s/%s config: [%s]" % fpe)
+            stat = os.stat(self._filename)
+        except OSError, e:
+            self.error("Unable to check config file %s: [%s]" % (
+                self._filename, e))
+            self._config = None
             raise
-        finally:
-            if con:
-                con.close()
-
-        res = dict()
-        for (option, value) in rows:
-            if option in res:
-                if res[option] is list:
-                    res[option].append(value)
-                else:
-                    v = res[option]
-                    res[option] = [v, value]
+        timestamp = stat.st_mtime
+        if self._config is None or timestamp > self._timestamp:
+            self._config = ConfigParser.RawConfigParser()
+            self._config.optionxform = str
+            self._config.read(self._filename)
+        return self._config
+
+
+class FileQuery(Log):
+
+    def __init__(self, fstore, table, columns, trans=True):
+        self._fstore = fstore
+        self._config = fstore.get_config()
+        self._section = table
+        if len(columns) > 3 or columns[-1] != 'value':
+            raise ValueError('Unsupported configuration format')
+        self._columns = columns
+
+    def rollback(self):
+        return
+
+    def commit(self):
+        return
+
+    def create(self):
+        raise NotImplementedError
+
+    def drop(self):
+        raise NotImplementedError
+
+    def select(self, kvfilter=None, columns=None):
+        if self._section not in self._config.sections():
+            return []
+
+        opts = self._config.options(self._section)
+
+        prefix = None
+        prefix_ = ''
+        if self._columns[0] in kvfilter:
+            prefix = kvfilter[self._columns[0]]
+            prefix_ = prefix + ' '
+
+        name = None
+        if len(self._columns) == 3 and self._columns[1] in kvfilter:
+            name = kvfilter[self._columns[1]]
+
+        value = None
+        if self._columns[-1] in kvfilter:
+            value = kvfilter[self._columns[-1]]
+
+        res = []
+        for o in opts:
+            if len(self._columns) == 3:
+                # 3 cols
+                if prefix and not o.startswith(prefix_):
+                    continue
+
+                col1, col2 = o.split(' ', 1)
+                if name and col2 != name:
+                    continue
+
+                col3 = self._config.get(self._section, o)
+                if value and col3 != value:
+                    continue
+
+                r = [col1, col2, col3]
             else:
-                res[option] = value
+                # 2 cols
+                if prefix and o != prefix:
+                    continue
+                r = [o, self._config.get(self._section, o)]
+
+            if columns:
+                s = []
+                for c in columns:
+                    s.append(r[self._columns.index(c)])
+                res.append(s)
+            else:
+                res.append(r)
 
+        self.debug('SELECT(%s, %s, %s) -> %s' % (self._section,
+                                                 repr(kvfilter),
+                                                 repr(columns),
+                                                 repr(res)))
         return res
 
-    def save_plugin_config(self, facility, plugin, options):
-        SELECT = "SELECT option, value FROM %s WHERE name=?" % facility
-        UPDATE = "UPDATE %s SET value=? WHERE name=? AND option=?" % facility
-        INSERT = "INSERT INTO %s VALUES(?,?,?)" % facility
-        con = None
+    def insert(self, values):
+        raise NotImplementedError
+
+    def update(self, values, kvfilter):
+        raise NotImplementedError
+
+    def delete(self, kvfilter):
+        raise NotImplementedError
+
+
+class Store(Log):
+    def __init__(self, config_name=None, database_url=None):
+        if config_name is None and database_url is None:
+            raise ValueError('config_name or database_url must be provided')
+        if config_name:
+            if config_name not in cherrypy.config:
+                raise NameError('Unknown database %s' % config_name)
+            name = cherrypy.config[config_name]
+        else:
+            name = database_url
+        if name.startswith('configfile://'):
+            _, filename = name.split('://')
+            self._db = FileStore(filename)
+            self._query = FileQuery
+        else:
+            self._db = SqlStore(name)
+            self._query = SqlQuery
+
+    @property
+    def is_readonly(self):
+        return self._db.is_readonly
+
+    def _row_to_dict_tree(self, data, row):
+        name = row[0]
+        if len(row) > 2:
+            if name not in data:
+                data[name] = dict()
+            d2 = data[name]
+            self._row_to_dict_tree(d2, row[1:])
+        else:
+            value = row[1]
+            if name in data:
+                if data[name] is list:
+                    data[name].append(value)
+                else:
+                    v = data[name]
+                    data[name] = [v, value]
+            else:
+                data[name] = value
+
+    def _rows_to_dict_tree(self, rows):
+        data = dict()
+        for r in rows:
+            self._row_to_dict_tree(data, r)
+        return data
+
+    def _load_data(self, table, columns, kvfilter=None):
+        rows = []
+        try:
+            q = self._query(self._db, table, columns, trans=False)
+            rows = q.select(kvfilter)
+        except Exception, e:  # pylint: disable=broad-except
+            self.error("Failed to load data for table %s: [%s]" % (table, e))
+        return self._rows_to_dict_tree(rows)
+
+    def load_config(self):
+        table = 'config'
+        columns = ['name', 'value']
+        return self._load_data(table, columns)
+
+    def load_options(self, table, name=None):
+        kvfilter = dict()
+        if name:
+            kvfilter['name'] = name
+        options = self._load_data(table, OPTIONS_COLUMNS, kvfilter)
+        if name and name in options:
+            return options[name]
+        return options
+
+    def save_options(self, table, name, options):
+        curvals = dict()
+        q = None
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("CREATE TABLE IF NOT EXISTS " +
-                        facility + " (name TEXT,option TEXT,value TEXT)")
-            curvals = dict()
-            for row in cur.execute(SELECT, (plugin,)):
+            q = self._query(self._db, table, OPTIONS_COLUMNS)
+            rows = q.select({'name': name}, ['option', 'value'])
+            for row in rows:
                 curvals[row[0]] = row[1]
 
-            for name in options:
-                if name in curvals:
-                    cur.execute(UPDATE, (options[name], plugin, name))
+            for opt in options:
+                if opt in curvals:
+                    q.update({'value': options[opt]},
+                             {'name': name, 'option': opt})
                 else:
-                    cur.execute(INSERT, (plugin, name, options[name]))
+                    q.insert((name, opt, options[opt]))
 
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to store config: [%s]" % e)
+            q.commit()
+        except Exception, e:  # pylint: disable=broad-except
+            if q:
+                q.rollback()
+            self.error("Failed to save options: [%s]" % e)
             raise
-        finally:
-            if con:
-                con.close()
 
-    def wipe_plugin_config(self, facility, plugin):
-        # Try to backup old data first, just in case ?
+    def delete_options(self, table, name, options=None):
+        kvfilter = {'name': name}
+        q = None
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("CREATE TABLE IF NOT EXISTS " +
-                        facility + " (name TEXT,option TEXT,value TEXT)")
-            cur.execute("DELETE FROM " + facility + " WHERE name=?",
-                        (plugin,))
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to wipe %s config: [%s]" % (plugin, e))
+            q = self._query(self._db, table, OPTIONS_COLUMNS)
+            if options is None:
+                q.delete(kvfilter)
+            else:
+                for opt in options:
+                    kvfilter['option'] = opt
+                    q.delete(kvfilter)
+            q.commit()
+        except Exception, e:  # pylint: disable=broad-except
+            if q:
+                q.rollback()
+            self.error("Failed to delete from %s: [%s]" % (table, e))
             raise
-        finally:
-            if con:
-                con.close()
 
-    def get_data(self, plugin, idval=None, name=None, value=None):
-        con = None
-        rows = []
-        names = None
-        values = ()
-        if idval or name or value:
-            names = ""
-            if idval:
-                names += " id=?"
-                values = values + (idval,)
-            if name:
-                if len(names) != 0:
-                    names += " AND"
-                names += " name=?"
-                values = values + (name,)
-            if value:
-                if len(names) != 0:
-                    names += " AND"
-                names += " value=?"
-                values = values + (value,)
+    def new_unique_data(self, table, data):
+        newid = str(uuid.uuid4())
+        q = None
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("CREATE TABLE IF NOT EXISTS " +
-                        plugin + "_data (id INTEGER, name TEXT, value TEXT)")
-            if not names:
-                cur.execute("SELECT * FROM " + plugin + "_data")
-            else:
-                cur.execute("SELECT * FROM " + plugin + "_data WHERE" +
-                            names, values)
-            rows = cur.fetchall()
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to load %s data: [%s]" % (plugin, e))
-            cherrypy.log.error(repr([names, values]))
-        finally:
-            if con:
-                con.close()
-
-        data = dict()
-        for row in rows:
-            if row[0] not in data:
-                data[row[0]] = dict()
-
-            item = data[row[0]]
-            if row[1] in item:
-                if item[row[1]] is list:
-                    item[row[1]].append(row[2])
-                else:
-                    v = item[row[1]]
-                    item[row[1]] = [v, row[2]]
-            else:
-                item[row[1]] = row[2]
-
-        return data
-
-    def save_data(self, plugin, data):
-        SELECT = "SELECT name, value FROM %s_data WHERE id=?" % plugin
-        UPDATE = "UPDATE %s_data SET value=? WHERE id=? AND name=?" % plugin
-        INSERT = "INSERT INTO %s_data VALUES(?,?,?)" % plugin
-        con = None
+            q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
+            for name in data:
+                q.insert((newid, name, data[name]))
+            q.commit()
+        except Exception, e:  # pylint: disable=broad-except
+            if q:
+                q.rollback()
+            self.error("Failed to store %s data: [%s]" % (table, e))
+            raise
+        return newid
+
+    def get_unique_data(self, table, uuidval=None, name=None, value=None):
+        kvfilter = dict()
+        if uuidval:
+            kvfilter['uuid'] = uuidval
+        if name:
+            kvfilter['name'] = name
+        if value:
+            kvfilter['value'] = value
+        return self._load_data(table, UNIQUE_DATA_COLUMNS, kvfilter)
+
+    def save_unique_data(self, table, data):
+        q = None
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            for idval in data:
+            q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
+            for uid in data:
                 curvals = dict()
-                for row in cur.execute(SELECT, (idval,)):
-                    curvals[row[0]] = row[1]
+                rows = q.select({'uuid': uid}, ['name', 'value'])
+                for r in rows:
+                    curvals[r[0]] = r[1]
 
-                datum = data[idval]
+                datum = data[uid]
                 for name in datum:
                     if name in curvals:
-                        cur.execute(UPDATE, (datum[name], idval, name))
+                        q.update({'value': datum[name]},
+                                 {'uuid': uid, 'name': name})
                     else:
-                        cur.execute(INSERT, (idval, name, datum[name]))
+                        q.insert((uid, name, datum[name]))
 
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to store %s data: [%s]" % (plugin, e))
+            q.commit()
+        except Exception, e:  # pylint: disable=broad-except
+            if q:
+                q.rollback()
+            self.error("Failed to store data in %s: [%s]" % (table, e))
             raise
-        finally:
-            if con:
-                con.close()
 
-    def wipe_data(self, plugin):
-        # Try to backup old data first, just in case
+    def del_unique_data(self, table, uuidval):
+        kvfilter = {'uuid': uuidval}
         try:
-            con = sqlite3.connect(self._admin_dbname)
-            cur = con.cursor()
-            cur.execute("DROP TABLE IF EXISTS " + plugin + "_data")
-            cur.execute("CREATE TABLE " + plugin + "_data"
-                        "(id INTEGER, name TEXT, value TEXT)")
-            con.commit()
-        except sqlite3.Error, e:
-            if con:
-                con.rollback()
-            cherrypy.log.error("Failed to wipe %s data: [%s]" % (plugin, e))
-            raise
-        finally:
-            if con:
-                con.close()
+            q = self._query(self._db, table, UNIQUE_DATA_COLUMNS, trans=False)
+            q.delete(kvfilter)
+        except Exception, e:  # pylint: disable=broad-except
+            self.error("Failed to delete data from %s: [%s]" % (table, e))
+
+    def _reset_data(self, table):
+        q = None
+        try:
+            q = self._query(self._db, table, UNIQUE_DATA_COLUMNS)
+            q.drop()
+            q.create()
+            q.commit()
+        except Exception, e:  # pylint: disable=broad-except
+            if q:
+                q.rollback()
+            self.error("Failed to erase all data from %s: [%s]" % (table, e))
+
+
+class AdminStore(Store):
+
+    def __init__(self):
+        super(AdminStore, self).__init__('admin.config.db')
+
+    def get_data(self, plugin, idval=None, name=None, value=None):
+        return self.get_unique_data(plugin+"_data", idval, name, value)
+
+    def save_data(self, plugin, data):
+        return self.save_unique_data(plugin+"_data", data)
+
+    def new_datum(self, plugin, datum):
+        table = plugin+"_data"
+        return self.new_unique_data(table, datum)
+
+    def del_datum(self, plugin, idval):
+        table = plugin+"_data"
+        return self.del_unique_data(table, idval)
+
+    def wipe_data(self, plugin):
+        table = plugin+"_data"
+        self._reset_data(table)
+
+
+class UserStore(Store):
+
+    def __init__(self, path=None):
+        super(UserStore, self).__init__('user.prefs.db')
+
+    def save_user_preferences(self, user, options):
+        self.save_options('users', user, options)
+
+    def load_user_preferences(self, user):
+        return self.load_options('users', user)
+
+    def save_plugin_data(self, plugin, user, options):
+        self.save_options(plugin+"_data", user, options)
+
+    def load_plugin_data(self, plugin, user):
+        return self.load_options(plugin+"_data", user)
+
+
+class TranStore(Store):
+
+    def __init__(self, path=None):
+        super(TranStore, self).__init__('transactions.db')