diff --git a/www/transwarp/__init__.py b/www/transwarp/__init__.py new file mode 100644 index 0000000..a5682fb --- /dev/null +++ b/www/transwarp/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/www/transwarp/db.py b/www/transwarp/db.py new file mode 100644 index 0000000..465cb8b --- /dev/null +++ b/www/transwarp/db.py @@ -0,0 +1,510 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = 'Hao Li' + +''' +Database operation module. +''' + +import time, uuid, functool, threading, logging + +# Dict object: + +class Dict(dict): + ''' + Simple dict but support access as x.y style. + + >>> d1 = Dict() + >>> d1['x'] = 100 + >>> d1.x + 100 + >>> d1.y = 200 + >>> d1['y'] + 200 + >>> d2 = Dict(a=1, b=2, c='3') + >>> d2.c + '3' + >>> d2['empty'] + Traceback (most recent call last): + ... + KeyError: 'empty' + >>> d2.empty + Traceback (most recent call last): + ... + AttributeError: 'Dict' object has no attribute 'empty' + >>> d3 = Dict(('a', 'b', 'c'), (1, 2, 3)) + >>> d3.a + 1 + >>> d3.b + 2 + >>> d3.c + 3 + ''' + def __init__(self, names=(), values=(), **kw): + super(Dict, self).__init__(**kw) + for k, v in zip(names, values): + #>>> x = [1, 2, 3] + #>>> y = [4, 5, 6] + #>>> zipped = zip(x, y) + #>>> zipped + #[(1, 4), (2, 5), (3, 6)] + self[k] = v + + def __getattr__(self, key): #can get Dict.x + try: + return self[key] + except KeyError: + raise AttributeError(r"'Dict' object has no attribute '%s'" % key) + + def __setattr__(self, key, value):#can set Dict.x = a; + self[key] = value + +def next_id(t=None): + ''' + Return next id as 50-char string. + + Args: + t: unix timestamp, default to None and using time.time(). + ''' + if t is None: + t = time.time() + return '%015d%s000' % (int(t * 1000), uuid.uuid4().hex) + #%[flags][width][.precision]type + # flag 0: zero padded; 15: width; d: integer; uuid return 32 hex digits + +def _profiling(start, sql=''): + #info log + t = time.time() - start + if t > 0.1: + logging.warning('[PROFILING] [DB] %s: %s' % (t, sql)) + else: + logging.info('[PROFILING] [DB] %s: %s' % (t, sql)) + +class DBError(Exception): #define error type + pass + +class MultiColumnsError(DBError): + pass + +class _LasyConnection(object): #put connection and db operation here, don't care transaction + + def __init__(self): + self.connection = None # have a connection property + + def cursor(self): + if self.connection is None: + connection = engine.connect() + # now connect to db; engine is global; call engine.connect() to connect db + logging.info('open connection <%s>...' % hex(id(connection))) + #print log of connection id + self.connection = connection + return self.connection.cursor() # return a cursor + + def commit(self): + self.connection.commit() + + def rollback(self): + self.connection.rollback() #undo all changes in current transaction + + def cleanup(self): + if self.connection: + connection = self.connection + self.connection = None # set connection property back to default when quitting + logging.info('close connection <%s>...' % hex(id(connection))) + connection.close() + +class _DbCtx(threading.local): #manage db connection and also "transaction"!!! + ''' + Thread local object that holds connection info. + ''' + def __init__(self): + self.connection = None + self.transactions = 0 + + def is_init(self): + return not self.connection is None + + def init(self): + logging.info('open lazy connection...') + self.connection = _LasyConnection() #call lasyconnection, return a db connection instance + self.transactions = 0 + + def cleanup(self): + self.connection.cleanup() #self.connection is a _LasyConnection instance, which is a connection instance + self.connection = None + + def cursor(self): + ''' + Return cursor + ''' + return self.connection.cursor() + +# thread-local db context: +_db_ctx = _DbCtx() + +# global engine object: +engine = None + +class _Engine(object): # have a connect property = a function, call _Engine(connect1).connect() = connect1() +#设计此类,从而可以提前设置db引擎,在任意时间调用connect,即可进行db连接,无需重复调用mysql.connector + def __init__(self, connect): + self._connect = connect + + def connect(self): + return self._connect() + +def create_engine(user, password, database, host='127.0.0.1', port=3306, **kw): + import mysql.connector + global engine + if engine is not None: + raise DBError('Engine is already initialized.') + params = dict(user=user, password=password, database=database, host=host, port=port) + defaults = dict(use_unicode=True, charset='utf8', collation='utf8_general_ci', autocommit=False) + for k, v in defaults.iteritems(): + #将kw里的设置参数传给param,如果没有,则选择默认值 + params[k] = kw.pop(k, v) + #kw.pop: if k is in kw, return kw[k] and remove it, else return v + #by iteration, move setup keys and corrsponding values from kw or defaults into params + params.update(kw) #将kw里的剩余参数传递给params + params['buffered'] = True + engine = _Engine(lambda: mysql.connector.connect(**params)) + #设置engine instance,调用engine.connect()时进行db连接 + # test connection... + logging.info('Init mysql engine <%s> ok.' % hex(id(engine))) + +class _ConnectionCtx(object): + ''' + _ConnectionCtx object that can open and close connection context. _ConnectionCtx object can be nested and only the most + outer connection has effect. + + with connection(): + pass + with connection(): + pass + ''' + #定义了__enter__和__exit__方法的类可以用with语句调用,可以保证__exit__一定会被调用 + def __enter__(self): + global _db_ctx + self.should_cleanup = False + if not _db_ctx.is_init(): + _db_ctx.init() + self.should_cleanup = True + return self + + def __exit__(self, exctype, excvalue, traceback): + global _db_ctx + if self.should_cleanup: + _db_ctx.cleanup() + +def connection(): + ''' + Return _ConnectionCtx object that can be used by 'with' statement: + + with connection(): + pass + ''' + return _ConnectionCtx() + +def with_connection(func): + ''' + Decorator for reuse connection. + + @with_connection + def foo(*args, **kw): + f1() + f2() + f3() + ''' + @functools.wraps(func) + def _wrapper(*args, **kw): + with _ConnectionCtx(): + return func(*args, **kw) + return _wrapper + +class _TransactionCtx(object): + ''' + _TransactionCtx object that can handle transactions. + + with _TransactionCtx(): + pass + ''' + + def __enter__(self): + global _db_ctx + self.should_close_conn = False + if not _db_ctx.is_init(): + # needs open a connection first: + _db_ctx.init() + self.should_close_conn = True + _db_ctx.transactions = _db_ctx.transactions + 1 + logging.info('begin transaction...' if _db_ctx.transactions==1 else 'join current transaction...') + return self + + def __exit__(self, exctype, excvalue, traceback): #exctype: exception type + global _db_ctx + _db_ctx.transactions = _db_ctx.transactions - 1 + try: + if _db_ctx.transactions==0: + if exctype is None: #if no error + self.commit() + else: + self.rollback() + finally: + if self.should_close_conn: + _db_ctx.cleanup() + + def commit(self): + global _db_ctx + logging.info('commit transaction...') + try: + _db_ctx.connection.commit() + logging.info('commit ok.') + except: + logging.warning('commit failed. try rollback...') + _db_ctx.connection.rollback() + logging.warning('rollback ok.') + raise + + def rollback(self): + global _db_ctx + logging.warning('rollback transaction...') + _db_ctx.connection.rollback() + logging.info('rollback ok.') + +def transaction(): + ''' + Create a transaction object so can use with statement: + + with transaction(): + pass + + >>> def update_profile(id, name, rollback): + ... u = dict(id=id, name=name, email='%s@test.org' % name, passwd=name, last_modified=time.time()) + ... insert('user', **u) + ... r = update('update user set passwd=? where id=?', name.upper(), id) + ... if rollback: + ... raise StandardError('will cause rollback...') + >>> with transaction(): + ... update_profile(900301, 'Python', False) + >>> select_one('select * from user where id=?', 900301).name + u'Python' + >>> with transaction(): + ... update_profile(900302, 'Ruby', True) + Traceback (most recent call last): + ... + StandardError: will cause rollback... + >>> select('select * from user where id=?', 900302) + [] + ''' + return _TransactionCtx() + +def with_transaction(func): + ''' + A decorator that makes function around transaction. + + >>> @with_transaction + ... def update_profile(id, name, rollback): + ... u = dict(id=id, name=name, email='%s@test.org' % name, passwd=name, last_modified=time.time()) + ... insert('user', **u) + ... r = update('update user set passwd=? where id=?', name.upper(), id) + ... if rollback: + ... raise StandardError('will cause rollback...') + >>> update_profile(8080, 'Julia', False) + >>> select_one('select * from user where id=?', 8080).passwd + u'JULIA' + >>> update_profile(9090, 'Robert', True) + Traceback (most recent call last): + ... + StandardError: will cause rollback... + >>> select('select * from user where id=?', 9090) + [] + ''' + @functools.wraps(func) + def _wrapper(*args, **kw): + _start = time.time() + with _TransactionCtx(): + return func(*args, **kw) + _profiling(_start) # 我们可以在return之后继续执行语句 + return _wrapper + +def _select(sql, first, *args): + ' execute select SQL and return unique result or list results.' + global _db_ctx + cursor = None + sql = sql.replace('?', '%s') + logging.info('SQL: %s, ARGS: %s' % (sql, args)) + try: + cursor = _db_ctx.connection.cursor() + cursor.execute(sql, args) + if cursor.description: + names = [x[0] for x in cursor.description] + #cursor.description contains a list of discriptions for found rows + #each discription contains name, type_code, display_size, internal_size, precision, scale, null_ok + #so x[0] corresponds to the name of each discription + if first: + values = cursor.fetchone() + if not values: + return None + return Dict(names, values) + return [Dict(names, x) for x in cursor.fetchall()] + finally: + if cursor: + cursor.close() + +@with_connection +def select_one(sql, *args): + ''' + Execute select SQL and expected one result. + If no result found, return None. + If multiple results found, the first one returned. + + >>> u1 = dict(id=100, name='Alice', email='alice@test.org', passwd='ABC-12345', last_modified=time.time()) + >>> u2 = dict(id=101, name='Sarah', email='sarah@test.org', passwd='ABC-12345', last_modified=time.time()) + >>> insert('user', **u1) + 1 + >>> insert('user', **u2) + 1 + >>> u = select_one('select * from user where id=?', 100) + >>> u.name + u'Alice' + >>> select_one('select * from user where email=?', 'abc@email.com') + >>> u2 = select_one('select * from user where passwd=? order by email', 'ABC-12345') + >>> u2.name + u'Alice' + ''' + return _select(sql, True, *args) + +@with_connection +def select_int(sql, *args): + ''' + Execute select SQL and expected one int and only one int result. + + >>> n = update('delete from user') + >>> u1 = dict(id=96900, name='Ada', email='ada@test.org', passwd='A-12345', last_modified=time.time()) + >>> u2 = dict(id=96901, name='Adam', email='adam@test.org', passwd='A-12345', last_modified=time.time()) + >>> insert('user', **u1) + 1 + >>> insert('user', **u2) + 1 + >>> select_int('select count(*) from user') + 2 + >>> select_int('select count(*) from user where email=?', 'ada@test.org') + 1 + >>> select_int('select count(*) from user where email=?', 'notexist@test.org') + 0 + >>> select_int('select id from user where email=?', 'ada@test.org') + 96900 + >>> select_int('select id, name from user where email=?', 'ada@test.org') + Traceback (most recent call last): + ... + MultiColumnsError: Expect only one column. + ''' + d = _select(sql, True, *args) + if len(d)!=1: + raise MultiColumnsError('Expect only one column.') + return d.values()[0] + +@with_connection +def select(sql, *args): + ''' + Execute select SQL and return list or empty list if no result. + + >>> u1 = dict(id=200, name='Wall.E', email='wall.e@test.org', passwd='back-to-earth', last_modified=time.time()) + >>> u2 = dict(id=201, name='Eva', email='eva@test.org', passwd='back-to-earth', last_modified=time.time()) + >>> insert('user', **u1) + 1 + >>> insert('user', **u2) + 1 + >>> L = select('select * from user where id=?', 900900900) + >>> L + [] + >>> L = select('select * from user where id=?', 200) + >>> L[0].email + u'wall.e@test.org' + >>> L = select('select * from user where passwd=? order by id desc', 'back-to-earth') + >>> L[0].name + u'Eva' + >>> L[1].name + u'Wall.E' + ''' + return _select(sql, False, *args) + +@with_connection +def _update(sql, *args): + global _db_ctx + cursor = None + sql = sql.replace('?', '%s') + logging.info('SQL: %s, ARGS: %s' % (sql, args)) + try: + cursor = _db_ctx.connection.cursor() + cursor.execute(sql, args) + r = cursor.rowcount + if _db_ctx.transactions==0: + # no transaction enviroment: + logging.info('auto commit') + _db_ctx.connection.commit() + return r + finally: + if cursor: + cursor.close() + +def insert(table, **kw): + ''' + Execute insert SQL. + + >>> u1 = dict(id=2000, name='Bob', email='bob@test.org', passwd='bobobob', last_modified=time.time()) + >>> insert('user', **u1) + 1 + >>> u2 = select_one('select * from user where id=?', 2000) + >>> u2.name + u'Bob' + >>> insert('user', **u2) + Traceback (most recent call last): + ... + IntegrityError: 1062 (23000): Duplicate entry '2000' for key 'PRIMARY' + ''' + cols, args = zip(*kw.iteritems()) + #>>> zipped =[(1, 4), (2, 5), (3, 6)] + #>>> x2, y2 = zip(*zipped) -> unzip + sql = 'insert into `%s` (%s) values (%s)' % (table, ','.join(['`%s`' % col for col in cols]), ','.join(['?' for i in range(len(cols))])) + return _update(sql, *args) + +def update(sql, *args): + r''' + Execute update SQL. + + >>> u1 = dict(id=1000, name='Michael', email='michael@test.org', passwd='123456', last_modified=time.time()) + >>> insert('user', **u1) + 1 + >>> u2 = select_one('select * from user where id=?', 1000) + >>> u2.email + u'michael@test.org' + >>> u2.passwd + u'123456' + >>> update('update user set email=?, passwd=? where id=?', 'michael@example.org', '654321', 1000) + 1 + >>> u3 = select_one('select * from user where id=?', 1000) + >>> u3.email + u'michael@example.org' + >>> u3.passwd + u'654321' + >>> update('update user set passwd=? where id=?', '***', '123\' or id=\'456') + 0 + ''' + return _update(sql, *args) + +if __name__=='__main__': + logging.basicConfig(level=logging.DEBUG) + create_engine('www-data', 'www-data', 'test') + update('drop table if exists user') + update('create table user (id int primary key, name text, email text, passwd text, last_modified real)') + import doctest + doctest.testmod() + + #The doctest module searches for pieces of text that look like interactive Python sessions, + #and then executes those sessions to verify that they work exactly as shown. + #run: python **.py -v + + + diff --git a/www/transwarp/orm.py b/www/transwarp/orm.py new file mode 100644 index 0000000..e3bdca6 --- /dev/null +++ b/www/transwarp/orm.py @@ -0,0 +1,334 @@ + #!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = 'Hao Li' + +''' +Database operation module. This module is independent with web module. +''' + +import time, logging + +import db + +class Field(object): + + _count = 0 + + def __init__(self, **kw): + self.name = kw.get('name', None) + self._default = kw.get('default', None) + self.primary_key = kw.get('primary_key', False) + self.nullable = kw.get('nullable', False) #whether can be empty + self.updatable = kw.get('updatable', True) + self.insertable = kw.get('insertable', True) + self.ddl = kw.get('ddl', '') + self._order = Field._count + Field._count = Field._count + 1 + + @property + def default(self): + d = self._default + return d() if callable(d) else d + #通过property装饰,default变为属性,可以调用Filed().default + #如果self._default是方法,则返回_default(),否则返回_default + + def __str__(self): #函数打印 + s = ['<%s:%s,%s,default(%s),' % (self.__class__.__name__, self.name, self.ddl, self._default)] + self.nullable and s.append('N') #如果前者为true,则执行后者 + self.updatable and s.append('U') + self.insertable and s.append('I') + s.append('>') + return ''.join(s) + +class StringField(Field): #string类  + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = '' #default string + if not 'ddl' in kw: + kw['ddl'] = 'varchar(255)'#default string type and length + super(StringField, self).__init__(**kw) + +class IntegerField(Field): + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = 0 + if not 'ddl' in kw: + kw['ddl'] = 'bigint' + super(IntegerField, self).__init__(**kw) + +class FloatField(Field): + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = 0.0 + if not 'ddl' in kw: + kw['ddl'] = 'real' + super(FloatField, self).__init__(**kw) + +class BooleanField(Field): + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = False + if not 'ddl' in kw: + kw['ddl'] = 'bool' + super(BooleanField, self).__init__(**kw) + +class TextField(Field): + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = '' + if not 'ddl' in kw: + kw['ddl'] = 'text' + super(TextField, self).__init__(**kw) + +class BlobField(Field): + + def __init__(self, **kw): + if not 'default' in kw: + kw['default'] = '' + if not 'ddl' in kw: + kw['ddl'] = 'blob' + super(BlobField, self).__init__(**kw) + +class VersionField(Field): #version: only requires name for init + + def __init__(self, name=None): + super(VersionField, self).__init__(name=name, default=0, ddl='bigint') + +_triggers = frozenset(['pre_insert', 'pre_update', 'pre_delete']) #unchangable set + +def _gen_sql(table_name, mappings): #generate a sql command for creatign table with properties in mappings + pk = None + sql = ['-- generating SQL for %s:' % table_name, 'create table `%s` (' % table_name] + for f in sorted(mappings.values(), lambda x, y: cmp(x._order, y._order)): + if not hasattr(f, 'ddl'): + raise StandardError('no ddl in field "%s".' % n) + ddl = f.ddl + nullable = f.nullable + if f.primary_key: + pk = f.name + sql.append(nullable and ' `%s` %s,' % (f.name, ddl) or ' `%s` %s not null,' % (f.name, ddl)) + #if can be null, then return, e.g., 'f.name' bigint; otherwise, append 'f.name' bigint not null + sql.append(' primary key(`%s`)' % pk) + sql.append(');') + return '\n'.join(sql) + +class ModelMetaclass(type): + ''' + Metaclass for model objects. + ''' + def __new__(cls, name, bases, attrs): + # skip base Model class: + if name=='Model': + return type.__new__(cls, name, bases, attrs) + + # store all subclasses info: + if not hasattr(cls, 'subclasses'): + cls.subclasses = {} + if not name in cls.subclasses: + cls.subclasses[name] = name + else: + logging.warning('Redefine class: %s' % name) + + logging.info('Scan ORMapping %s...' % name) + mappings = dict() + primary_key = None + for k, v in attrs.iteritems(): + if isinstance(v, Field): + #v is a 表头 in table (for a column); v has name, primary_key, updateble, _default and default, nullable, insertable, ddl (type) + if not v.name: + v.name = k + logging.info('Found mapping: %s => %s' % (k, v)) + # check duplicate primary key: + if v.primary_key: + if primary_key: + raise TypeError('Cannot define more than 1 primary key in class: %s' % name) + if v.updatable: + logging.warning('NOTE: change primary key to non-updatable.') + v.updatable = False + if v.nullable: + logging.warning('NOTE: change primary key to non-nullable.') + v.nullable = False + primary_key = v + mappings[k] = v + #循环把属性把进mapping + # check exist of primary key: + if not primary_key: + raise TypeError('Primary key not defined in class: %s' % name) + for k in mappings.iterkeys(): + attrs.pop(k) # 已经放入mapping,所以删掉attrs里面对应值 + if not '__table__' in attrs: + attrs['__table__'] = name.lower() + attrs['__mappings__'] = mappings + attrs['__primary_key__'] = primary_key + attrs['__sql__'] = lambda self: _gen_sql(attrs['__table__'], mappings) + # 返回一个create table的sql语句(string) + for trigger in _triggers: + if not trigger in attrs: + attrs[trigger] = None + return type.__new__(cls, name, bases, attrs) + #now User class has __table__,__mappings__,__primary_key__,__sql__,pre_insert,pre_update,pre_delete + +#model class: User's father +class Model(dict): + ''' + Base class for ORM. + + >>> class User(Model): + ... id = IntegerField(primary_key=True) + ... name = StringField() + ... email = StringField(updatable=False) + ... passwd = StringField(default=lambda: '******') + ... last_modified = FloatField() + ... def pre_insert(self): + ... self.last_modified = time.time() + >>> u = User(id=10190, name='Michael', email='orm@db.org') + >>> r = u.insert() + >>> u.email + 'orm@db.org' + >>> u.passwd + '******' + >>> u.last_modified > (time.time() - 2) + True + >>> f = User.get(10190) + >>> f.name + u'Michael' + >>> f.email + u'orm@db.org' + >>> f.email = 'changed@db.org' + >>> r = f.update() # change email but email is non-updatable! + >>> len(User.find_all()) + 1 + >>> g = User.get(10190) + >>> g.email + u'orm@db.org' + >>> r = g.delete() + >>> len(db.select('select * from user where id=10190')) + 0 + >>> import json + >>> print User().__sql__() + -- generating SQL for user: + create table `user` ( + `id` bigint not null, + `name` varchar(255) not null, + `email` varchar(255) not null, + `passwd` varchar(255) not null, + `last_modified` real not null, + primary key(`id`) + ); + ''' + __metaclass__ = ModelMetaclass + + def __init__(self, **kw): + super(Model, self).__init__(**kw) + + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(r"'Dict' object has no attribute '%s'" % key) + + def __setattr__(self, key, value): + self[key] = value + + @classmethod + #classe method receives class as the first argument: use cls + #instance method receives instance as its first argument: use self + def get(cls, pk): + ''' + Get by primary key. + ''' + d = db.select_one('select * from %s where %s=?' % (cls.__table__, cls.__primary_key__.name), pk) + return cls(**d) if d else None + #here return a newly-created User instance with params = d + @classmethod + def find_first(cls, where, *args): + ''' + Find by where clause and return one result. If multiple results found, + only the first one returned. If no result found, return None. + ''' + d = db.select_one('select * from %s %s' % (cls.__table__, where), *args) + return cls(**d) if d else None + + @classmethod + def find_all(cls, *args): + ''' + Find all and return list. + ''' + L = db.select('select * from `%s`' % cls.__table__) + return [cls(**d) for d in L] #return a list of User instances + + @classmethod + def find_by(cls, where, *args): + ''' + Find by where clause and return list. + ''' + L = db.select('select * from `%s` %s' % (cls.__table__, where), *args) + return [cls(**d) for d in L] + + @classmethod + def count_all(cls): + ''' + Find by 'select count(pk) from table' and return integer. + ''' + return db.select_int('select count(`%s`) from `%s`' % (cls.__primary_key__.name, cls.__table__)) + + @classmethod + def count_by(cls, where, *args): + ''' + Find by 'select count(pk) from table where ... ' and return int. + ''' + return db.select_int('select count(`%s`) from `%s` %s' % (cls.__primary_key__.name, cls.__table__, where), *args) + + def update(self): + self.pre_update and self.pre_update() #pre_update is defined in User, otherwise it's None + L = [] + args = [] + for k, v in self.__mappings__.iteritems(): + if v.updatable: + if hasattr(self, k): + arg = getattr(self, k) + else: + arg = v.default + setattr(self, k, arg) + L.append('`%s`=?' % k) + args.append(arg) + pk = self.__primary_key__.name + args.append(getattr(self, pk)) + db.update('update `%s` set %s where %s=?' % (self.__table__, ','.join(L), pk), *args) + #args has params that need to be updated and id + return self + + def delete(self): + self.pre_delete and self.pre_delete() #defined in User + pk = self.__primary_key__.name + args = (getattr(self, pk), ) + db.update('delete from `%s` where `%s`=?' % (self.__table__, pk), *args) + return self + + def insert(self): + self.pre_insert and self.pre_insert() + params = {} + for k, v in self.__mappings__.iteritems(): + if v.insertable: + if not hasattr(self, k): + setattr(self, k, v.default) + params[v.name] = getattr(self, k) + db.insert('%s' % self.__table__, **params) + return self + +if __name__=='__main__': + logging.basicConfig(level=logging.DEBUG) + db.create_engine('mysql', 'password', 'test') + db.update('drop table if exists user') + db.update('create table user (id int primary key, name text, email text, passwd text, last_modified real)') + import doctest + doctest.testmod() + + +