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"""
7Middleware related to transactions and database connections.
8
9At this time it is very basic; but will eventually sprout all that
10two-phase commit goodness that I don't need.
11
12.. note::
13
14   This is experimental, and will change in the future.
15"""
16from paste.httpexceptions import HTTPException
17from wsgilib import catch_errors
18
19class 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
32class 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
53class 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
77def 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
112if '__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
121