[3] | 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
---|
| 3 | # (c) 2005 Clark C. Evans |
---|
| 4 | # This module is part of the Python Paste Project and is released under |
---|
| 5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
| 6 | """ |
---|
| 7 | Middleware related to transactions and database connections. |
---|
| 8 | |
---|
| 9 | At this time it is very basic; but will eventually sprout all that |
---|
| 10 | two-phase commit goodness that I don't need. |
---|
| 11 | |
---|
| 12 | .. note:: |
---|
| 13 | |
---|
| 14 | This is experimental, and will change in the future. |
---|
| 15 | """ |
---|
| 16 | from paste.httpexceptions import HTTPException |
---|
| 17 | from wsgilib import catch_errors |
---|
| 18 | |
---|
| 19 | class TransactionManagerMiddleware(object): |
---|
| 20 | |
---|
| 21 | def __init__(self, application): |
---|
| 22 | self.application = application |
---|
| 23 | |
---|
| 24 | def __call__(self, environ, start_response): |
---|
| 25 | environ['paste.transaction_manager'] = manager = Manager() |
---|
| 26 | # This makes sure nothing else traps unexpected exceptions: |
---|
| 27 | environ['paste.throw_errors'] = True |
---|
| 28 | return catch_errors(self.application, environ, start_response, |
---|
| 29 | error_callback=manager.error, |
---|
| 30 | ok_callback=manager.finish) |
---|
| 31 | |
---|
| 32 | class Manager(object): |
---|
| 33 | |
---|
| 34 | def __init__(self): |
---|
| 35 | self.aborted = False |
---|
| 36 | self.transactions = [] |
---|
| 37 | |
---|
| 38 | def abort(self): |
---|
| 39 | self.aborted = True |
---|
| 40 | |
---|
| 41 | def error(self, exc_info): |
---|
| 42 | self.aborted = True |
---|
| 43 | self.finish() |
---|
| 44 | |
---|
| 45 | def finish(self): |
---|
| 46 | for trans in self.transactions: |
---|
| 47 | if self.aborted: |
---|
| 48 | trans.rollback() |
---|
| 49 | else: |
---|
| 50 | trans.commit() |
---|
| 51 | |
---|
| 52 | |
---|
| 53 | class ConnectionFactory(object): |
---|
| 54 | """ |
---|
| 55 | Provides a callable interface for connecting to ADBAPI databases in |
---|
| 56 | a WSGI style (using the environment). More advanced connection |
---|
| 57 | factories might use the REMOTE_USER and/or other environment |
---|
| 58 | variables to make the connection returned depend upon the request. |
---|
| 59 | """ |
---|
| 60 | def __init__(self, module, *args, **kwargs): |
---|
| 61 | #assert getattr(module,'threadsaftey',0) > 0 |
---|
| 62 | self.module = module |
---|
| 63 | self.args = args |
---|
| 64 | self.kwargs = kwargs |
---|
| 65 | |
---|
| 66 | # deal with database string quoting issues |
---|
| 67 | self.quote = lambda s: "'%s'" % s.replace("'","''") |
---|
| 68 | if hasattr(self.module,'PgQuoteString'): |
---|
| 69 | self.quote = self.module.PgQuoteString |
---|
| 70 | |
---|
| 71 | def __call__(self, environ=None): |
---|
| 72 | conn = self.module.connect(*self.args, **self.kwargs) |
---|
| 73 | conn.__dict__['module'] = self.module |
---|
| 74 | conn.__dict__['quote'] = self.quote |
---|
| 75 | return conn |
---|
| 76 | |
---|
| 77 | def BasicTransactionHandler(application, factory): |
---|
| 78 | """ |
---|
| 79 | Provides a simple mechanism for starting a transaction based on the |
---|
| 80 | factory; and for either committing or rolling back the transaction |
---|
| 81 | depending on the result. It checks for the response's current |
---|
| 82 | status code either through the latest call to start_response; or |
---|
| 83 | through a HTTPException's code. If it is a 100, 200, or 300; the |
---|
| 84 | transaction is committed; otherwise it is rolled back. |
---|
| 85 | """ |
---|
| 86 | def basic_transaction(environ, start_response): |
---|
| 87 | conn = factory(environ) |
---|
| 88 | environ['paste.connection'] = conn |
---|
| 89 | should_commit = [500] |
---|
| 90 | def finalizer(exc_info=None): |
---|
| 91 | if exc_info: |
---|
| 92 | if isinstance(exc_info[1], HTTPException): |
---|
| 93 | should_commit.append(exc_info[1].code) |
---|
| 94 | if should_commit.pop() < 400: |
---|
| 95 | conn.commit() |
---|
| 96 | else: |
---|
| 97 | try: |
---|
| 98 | conn.rollback() |
---|
| 99 | except: |
---|
| 100 | # TODO: check if rollback has already happened |
---|
| 101 | return |
---|
| 102 | conn.close() |
---|
| 103 | def basictrans_start_response(status, headers, exc_info = None): |
---|
| 104 | should_commit.append(int(status.split(" ")[0])) |
---|
| 105 | return start_response(status, headers, exc_info) |
---|
| 106 | return catch_errors(application, environ, basictrans_start_response, |
---|
| 107 | finalizer, finalizer) |
---|
| 108 | return basic_transaction |
---|
| 109 | |
---|
| 110 | __all__ = ['ConnectionFactory', 'BasicTransactionHandler'] |
---|
| 111 | |
---|
| 112 | if '__main__' == __name__ and False: |
---|
| 113 | from pyPgSQL import PgSQL |
---|
| 114 | factory = ConnectionFactory(PgSQL, database="testing") |
---|
| 115 | conn = factory() |
---|
| 116 | curr = conn.cursor() |
---|
| 117 | curr.execute("SELECT now(), %s" % conn.quote("B'n\\'gles")) |
---|
| 118 | (time, bing) = curr.fetchone() |
---|
| 119 | print bing, time |
---|
| 120 | |
---|