diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6f1401 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*_trial_temp* +*.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_unidb.py b/test/test_unidb.py new file mode 100644 index 0000000..4198c6e --- /dev/null +++ b/test/test_unidb.py @@ -0,0 +1,246 @@ +from twisted.trial.unittest import TestCase +from twisted.python.filepath import FilePath as FP +from twisted.internet.defer import inlineCallbacks, Deferred, returnValue +from zope.interface.verify import verifyClass +import web + +import sqlite3 + +from smd.unidb import AsyncSqliteDB, AsyncDB, AsyncPostgresDB +from smd.unidb import SyncSqliteDB +from smd.unidb import IDeferredReturningDB, ISynchronousDB + +#------------------------------------------------------------------------------ +# postgres skip? +#------------------------------------------------------------------------------ +try: + skipPostgres = None + import psycopg2 +except Exception, e: + skipPostgres = str(e) + + +class IDeferredReturningDBMixin: + + timeout = 3 + + def getDB(self): + """ + I return an instance of an IDeferredReturningDB implementor + """ + raise NotImplementedError('You need to have this method return an instance ' + 'of an IDeferredReturningDB-implementing class -- the one you want to test.') + + @inlineCallbacks + def getReadyDB(self): + """ + I create and delete the necessary things to run all the rest of the tests + in this thing. + """ + db = self.getDB() + createsql = 'create table foobar (id integer primary key, value text)' + try: + b = yield db.dQuery(createsql) + except Exception, e: + _ = yield db.dQuery('drop table foobar') + _ = yield db.dQuery(createsql) + returnValue(db) + + @inlineCallbacks + def test_verifyClass(self): + """ + I verify that this class implements the IDeferredReturningDB. + """ + s = yield self.getReadyDB() + verifyClass(IDeferredReturningDB, s.__class__) + + @inlineCallbacks + def test_query_is_deferred(self): + """ + I test that .query returns a Deferred. + """ + s = yield self.getReadyDB() + a = s.dQuery('select 1 as foo;') + self.assertTrue(isinstance(a, Deferred), a) + a.addErrback(lambda x: None) + + @inlineCallbacks + def test_query_base_test(self): + """ + I test the very basic functionality of .query + """ + s = yield self.getReadyDB() + a = yield s.dQuery("select 'hey' as foo;") + self.assertEqual(len(a), 1) + self.assertEqual(a[0].foo, 'hey') + + @inlineCallbacks + def test_insert_is_deferred(self): + """ + I test that .insert returns a Deferred. + """ + s = yield self.getReadyDB() + a = s.dInsert('foobar', value='hey') + self.assertTrue(isinstance(a, Deferred), a) + a.addErrback(lambda x: None) + + @inlineCallbacks + def test_insert_base_test(self): + """ + I test that insert returns the last inserted row id + """ + s = yield self.getReadyDB() + a = yield s.dInsert('foobar', value='hey') + self.assertEqual(a, 1) + + @inlineCallbacks + def test_select_is_deferred(self): + """ + I test that .select returns a Deferred + """ + s = yield self.getReadyDB() + a = s.dSelect('foobar') + self.assertTrue(isinstance(a, Deferred), a) + a.addErrback(lambda x: None) + + @inlineCallbacks + def test_select_base_test(self): + """ + I test that .select returns a list of web.Storage objects. + """ + s = yield self.getReadyDB() + a = yield s.dInsert('foobar', value='foo') + b = yield s.dInsert('foobar', value='bar') + c = yield s.dSelect('foobar', order='id asc') + self.assertEqual(len(c), 2) + self.assertTrue(isinstance(c[0], web.Storage), c[0]) + self.assertEqual(c[0].value, 'foo') + self.assertEqual(c[1].value, 'bar') + + def test_select_no_table(self): + """ + I test that an error is returned for a select on a non-existant table + """ + s = self.getReadyDB() + def cb1(s): + q1 = s.dSelect('garbage') + def cb2(q2): + self.fail('Expected an exception') + def eb2(e): + return None + q1.addCallbacks(cb2, eb2) + s.addCallback(cb1).addBoth(lambda x: None) + return s + + @inlineCallbacks + def test_update_is_deferred(self): + """ + I test that .update returns a Deferred. + """ + s = yield self.getReadyDB() + a = s.dUpdate('foobar', where='1=1', value='something') + self.assertTrue(isinstance(a, Deferred), a) + a.addErrback(lambda x: None) + + @inlineCallbacks + def test_update_base_test(self): + """ + I test basic functionality of an update, namely that it returns the number of + rows updated. + """ + s = yield self.getReadyDB() + a = yield s.dInsert('foobar', value='foo') + b = yield s.dInsert('foobar', value='bar') + c = yield s.dUpdate('foobar', where='value=$value', vars={'value':'foo'}, value='bar') + self.assertEqual(c, 1) + c = yield s.dUpdate('foobar', where='value=$value', vars={'value':'bar'}, value='sam') + self.assertEqual(c, 2) + + @inlineCallbacks + def test_delete_is_deferred(self): + """ + I test that .delete returns a Deferred. + """ + s = yield self.getReadyDB() + a = s.dDelete('foobar', where='1=1') + self.assertTrue(isinstance(a, Deferred), a) + a.addErrback(lambda x: None) + + @inlineCallbacks + def test_delete_base_test(self): + """ + I test that delete returns the number of rows deleted. + """ + i = [] + def f(): + i.append(True) + s = yield self.getReadyDB() + f() + a = yield s.dInsert('foobar', value='foo') + f() + a = yield s.dInsert('foobar', value='bar') + f() + a = yield s.dInsert('foobar', value='bar') + f() + c = yield s.dDelete('foobar', where='value=$value', vars={'value':'foo'}) + f() + self.assertEqual(c, 1) + f() + c = yield s.dDelete('foobar', where='value=$value', vars={'value':'bar'}) + f() + self.assertEqual(c, 2) + + +#------------------------------------------------------------------------------ + +class TestAsyncPostgresDB(TestCase, IDeferredReturningDBMixin): + + skip = skipPostgres + timeout = 3 + + def test_basic(self): + self.fail('You have psycopg2?. Please write these tests!') + + def test_another(self): + self.fail('You have psycopg2?. Please write these tests!') + +#------------------------------------------------------------------------------ + +class TestAsyncSqliteDB(TestCase, IDeferredReturningDBMixin): + + timeout = 3 + + def getDB(self): + f = FP(self.mktemp()) + db = AsyncSqliteDB(f.path) + return db + +#------------------------------------------------------------------------------ + +class TestSyncSqliteDB(TestCase, IDeferredReturningDBMixin): + + timeout = 3 + + def getDB(self): + f = FP(self.mktemp()) + db = SyncSqliteDB(f.path) + return db + + def test_syncQuery(self): + db = self.getDB() + db.query('create table foobar (name text)') + db.insert('foobar', name='bar') + a = db.select('foobar') + self.assertEqual(len(a), 1) + self.assertEqual(a[0].name, 'bar') + + def test_verifySyncClass(self): + db = self.getDB() + verifyClass(ISynchronousDB, db.__class__) + + + + + + + diff --git a/unidb.py b/unidb.py new file mode 100644 index 0000000..bf3dcae --- /dev/null +++ b/unidb.py @@ -0,0 +1,225 @@ +from zope.interface import Interface, implements +from twisted.enterprise import adbapi +from twisted.internet import defer +from twisted.python import failure + +import sqlite3 +sqlite3.paramstyle = 'qmark' +from web.db import DB, SqliteDB, PostgresDB +import web + +#------------------------------------------------------------------------------ +# +#------------------------------------------------------------------------------ +class IDeferredReturningDB(Interface): + """I define the combination web.py/twisted database api. + """ + def dQuery(sql_query, vars=None, processed=False, _test=False): + """Return as Deferred the result of web.db.DB.query though iterBetter + is unwrapped into a plain list. + """ + + def dSelect(tables, vars=None, what='*', where=None, order=None, group=None, + limit=None, offset=None, _test=False): + """Return as Deferred the result of web.db.DB.select though iterBetter + is unwrapped into a plain list. + """ + + def dInsert(tablename, seqname=None, _test=False, **values): + """Return as Deferred the result of web.db.DB.insert + """ + + def dUpdate(tables, where, vars=None, _test=False, **values): + """Return as Deferred the result of web.db.DB.update + """ + + def dDelete(table, where, using=None, vars=None, _test=False): + """Return as Deferred the result of web.db.DB.delete + """ + +class ISynchronousDB(Interface): + """I define the web.py database api. + """ + def query(sql_query, vars=None, processed=False, _test=False): + """Return as Deferred the result of web.db.DB.query though iterBetter + is unwrapped into a plain list. + """ + + def select(tables, vars=None, what='*', where=None, order=None, group=None, + limit=None, offset=None, _test=False): + """Return as Deferred the result of web.db.DB.select though iterBetter + is unwrapped into a plain list. + """ + + def insert(tablename, seqname=None, _test=False, **values): + """Return as Deferred the result of web.db.DB.insert + """ + + def update(tables, where, vars=None, _test=False, **values): + """Return as Deferred the result of web.db.DB.update + """ + + def delete(table, where, using=None, vars=None, _test=False): + """Return as Deferred the result of web.db.DB.delete + """ + +#------------------------------------------------------------------------------ +# Utility functions +#------------------------------------------------------------------------------ + +def unIter(rows): + """ + Turn an iterator into a list. + """ + if hasattr(rows, '__iter__'): + return [x for x in rows] + return rows + +#------------------------------------------------------------------------------ +# Database API for twisted applications +#------------------------------------------------------------------------------ +class AsyncDB(adbapi.ConnectionPool): + """ + I attempt to be an asynchronous web.py database interface. + """ + implements(IDeferredReturningDB) + + fakedb = DB(None, {}) + paramstyle = 'pyformat' + + min = 3 + max = 5 + + def dict_factory(self, cursor, row): + d = web.Storage() + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + def connect(self): + conn = adbapi.ConnectionPool.connect(self) + conn.row_factory = self.dict_factory + return conn + + def errBack(self, res): + return res + + def dQuery(self, *args, **kwargs): + kwargs['_test'] = True + q = self.fakedb.query(*args, **kwargs) + return self.runQuery(q.query(self.paramstyle), q.values()).addErrback(self.errBack) + + def dSelect(self, *args, **kwargs): + kwargs['_test'] = True + q = self.fakedb.select(*args, **kwargs) + return self.runQuery(q.query(self.paramstyle), q.values()) + + def dInsert(self, *args, **kwargs): + kwargs['_test'] = True + q = self.fakedb.insert(*args, **kwargs) + return self.runInteraction(self._execReturnAttrib, q, 'lastrowid') + + def getLastId(self, res): + return res + + def dUpdate(self, *args, **kwargs): + kwargs['_test'] = True + query = self.fakedb.update(*args, **kwargs) + return self.runInteraction(self._execReturnAttrib, query, 'rowcount') + + def dDelete(self, *args, **kwargs): + kwargs['_test'] = True + query = self.fakedb.delete(*args, **kwargs) + return self.runInteraction(self._execReturnAttrib, query, 'rowcount') + + def _execReturnAttrib(self, txn, q, attrib): + txn.execute(q.query(self.paramstyle), q.values()) + return getattr(txn, attrib) + + +class AsyncPostgresDB(AsyncDB): + + def __init__(self, *args, **kwargs): + adbapi.ConnectionPool.__init__(self, *args, **kwargs) + self.fakedb = PostgresDB() + + +class AsyncSqliteDB(AsyncDB): + + paramstyle = 'qmark' + + def __init__(self, *args, **kwargs): + kwargs['check_same_thread'] = False + adbapi.ConnectionPool.__init__(self, 'sqlite3', *args, **kwargs) + self.fakedb = SqliteDB(db=':memory:') + + + +#------------------------------------------------------------------------------ +# Database api for synchronous applications +#------------------------------------------------------------------------------ + +def _Dwrap(func, *args, **kwargs): + d = defer.Deferred() + try: + res = func(*args, **kwargs) + if isinstance(res, defer.Deferred): + return res + d.callback(res) + except Exception, e: + d.errback(e) + return d + +class SyncDB(DB): + + implements(IDeferredReturningDB, ISynchronousDB) + + + + +class SyncSqliteDB(SqliteDB): + + implements(IDeferredReturningDB, ISynchronousDB) + + def __init__(self, dbname): + SqliteDB.__init__(self, db=dbname) + self.printing = False + + def dQuery(self, *args, **kwargs): + return _Dwrap(SqliteDB.query, self, *args, **kwargs).addCallback(unIter) + + def dSelect(self, *args, **kwargs): + return _Dwrap(SqliteDB.select, self, *args, **kwargs).addCallback(unIter) + + def dInsert(self, *args, **kwargs): + return _Dwrap(SqliteDB.insert, self, *args, **kwargs) + + def dUpdate(self, *args, **kwargs): + return _Dwrap(SqliteDB.update, self, *args, **kwargs) + + def dDelete(self, *args, **kwargs): + return _Dwrap(SqliteDB.delete, self, *args, **kwargs) + + def query(self, *args, **kwargs): + return unIter(SqliteDB.query(self, *args, **kwargs)) + + def select(self, *args, **kwargs): + return unIter(SqliteDB.select(self, *args, **kwargs)) + + + + + + + + + + + + + + + + + +