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 | |
---|