root/livinglogic.python.nightshade/src/ll/nightshade.py @ 92:0df1b8fbf076

Revision 92:0df1b8fbf076, 8.9 KB (checked in by Walter Doerwald <walter@…>, 10 years ago)

Add methods commmit, rollback, close and cancel to Connection.

Line 
1# -*- coding: utf-8 -*-
2
3"""
4This module provides a class :class:`Call` that allows you to use Oracle PL/SQL
5procedures/functions as CherryPy__ response handlers. A :class:`Call` objects
6wraps a :class:`ll.orasql.Procedure` or :class:`ll.orasql.Function` object from
7the :mod:`ll.orasql` module.
8
9__ http://www.cherrypy.org/
10
11For example, you might have the following PL/SQL function::
12
13    create or replace function helloworld
14    (
15        who varchar2
16    )
17    return varchar2
18    as
19    begin
20        return '<html><head><h>Hello ' || who || '</h></head><body><h1>Hello, ' || who || '!</h1></body></html>';
21    end;
22
23Using this function as a CherryPy response handler can be done like this::
24
25    import cherrypy
26
27    from ll import orasql, nightshade
28
29
30    proc = nightshade.Call(orasql.Function("helloworld"), connectstring="user/pwd")
31
32    class HelloWorld(object):
33        @cherrypy.expose
34        def default(self, who="World"):
35            cherrypy.response.headers["Content-Type"] = "text/html"
36            return proc(who=who)
37
38    cherrypy.quickstart(HelloWorld())
39"""
40
41import time, datetime, threading
42
43import cherrypy
44
45from ll import orasql
46
47
48__docformat__ = "reStructuredText"
49
50weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
51monthname = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
52
53
54class UTC(datetime.tzinfo):
55    """
56    Timezone object for UTC
57    """
58    def utcoffset(self, dt):
59        return datetime.timedelta(0)
60
61    def dst(self, dt):
62        return datetime.timedelta(0)
63
64    def tzname(self, dt):
65        return "UTC"
66
67utc = UTC()
68
69
70def getnow():
71    """
72    Get the current date and time as a :class:`datetime.datetime` object in UTC
73    with timezone info.
74    """
75    return datetime.datetime.utcnow().replace(tzinfo=utc)
76
77
78def httpdate(dt):
79    """
80    Return a string suitable for a "Last-Modified" and "Expires" header.
81   
82    :var:`dt` is a :class:`datetime.datetime` object. If ``:var:`dt`.tzinfo`` is
83    :const:`None` :var:`dt` is assumed to be in the local timezone (using the
84    current UTC offset which might be different from the one used by :var:`dt`).
85    """
86    if dt.tzinfo is None:
87        dt += datetime.timedelta(seconds=[time.timezone, time.altzone][time.daylight])
88    else:
89        dt -= dt.tzinfo.utcoffset(dt)
90    return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekdayname[dt.weekday()], dt.day, monthname[dt.month], dt.year, dt.hour, dt.minute, dt.second)
91
92
93class Connect(object):
94    """
95    :class:`Connect` objects can be used as decorators that wraps a function
96    that needs a database connection.
97   
98    If calling the wrapped function results in a database exception that has
99    been caused by a lost connection to the database or similar problems,
100    the function is retried with a new database connection.
101    """
102    _badoracleexceptions = set((
103        28,    # your session has been killed
104        1012,  # not logged on
105        1014,  # Oracle shutdown in progress
106        1033,  # Oracle startup or shutdown in progress
107        1034,  # Oracle not available
108        1035,  # Oracle only available to users with RESTRICTED SESSION privilege
109        1089,  # immediate shutdown in progress - no operations are permitted
110        1090,  # Shutdown in progress - connection is not permitted
111        1092,  # ORACLE instance terminated. Disconnection forced
112        3106,  # fatal two-task communication protocol error
113        3113,  # end-of-file on communication channel
114        3114,  # not connected to ORACLE
115        3135,  # connection lost contact
116        12154, # TNS:could not resolve the connect identifier specified
117        12540, # TNS:internal limit restriction exceeded
118        12541, # TNS:no listener
119        12543, # TNS:destination host unreachable
120    ))
121
122    def __init__(self, connectstring=None, pool=None, retry=3, **kwargs):
123        """
124        Create a new parameterized :class:`Connect` decorator. Either
125        :var:`connectstring` or :var:`pool` (a database pool object) must be
126        specified. :var:`retry` specifies how often to retry calling the wrapped
127        function after a database exception. :var:`kwargs` will be passed on to
128        the :func:`connect` call.
129        """
130        if (connectstring is not None) == (pool is not None):
131            raise TypeError("either connectstring or pool must be specified")
132        self.pool = pool
133        self._connection = None
134        self.connectstring = connectstring
135        self.retry = retry
136        self.kwargs = kwargs
137
138    def _isbadoracleexception(self, exc):
139        if exc.args:
140            code = getattr(exc[0], "code", 0)
141            if code in self._badoracleexceptions:
142                return True
143        return False
144
145    def _getconnection(self):
146        if self.pool is not None:
147            return self.pool.acquire()
148        elif self._connection is None:
149            self._connection = orasql.connect(self.connectstring, threaded=True, **self.kwargs)
150        return self._connection
151
152    def _dropconnection(self, connection):
153        if self.pool is not None:
154            self.pool.drop(connection)
155        else:
156            self._connection = None
157
158    def cursor(self, **kwargs):
159        connection = self._getconnection()
160        return connection.cursor(**kwargs)
161
162    def commit(self):
163        self._getconnection().commit()
164
165    def rollback(self):
166        self._getconnection().rollback()
167
168    def close(self):
169        connection = self._getconnection()
170        connection.close()
171        self._dropconnection(connection)
172
173    def cancel(self):
174        self._getconnection().cancel()
175
176    def __call__(self, func):
177        def wrapper(*args, **kwargs):
178            for i in xrange(self.retry):
179                connection = self._getconnection()
180                try:
181                    # This only works if func is using the same connection
182                    return func(*args, **kwargs)
183                except orasql.DatabaseError, exc:
184                    if i<self.retry-1 and self._isbadoracleexception(exc):
185                        # Drop bad connection and retry
186                        self._dropconnection(connection)
187                    else:
188                        raise
189        wrapper.__name__ = func.__name__
190        wrapper.__doc__ = func.__doc__
191        wrapper.__dict__.update(func.__dict__)
192        return wrapper
193
194
195class Call(object):
196    """
197    Wrap an Oracle procedure or function in a CherryPy handler.
198
199    A :class:`Call` object wraps a procedure or function object from
200    :mod:`ll.orasql` and makes it callable just like a CherryPy handler.
201    """
202    def __init__(self, callable, connection):
203        """
204        Create a :class:`Call` object wrapping the function or procedure
205        :var:`callable`.
206        """
207        self.callable = callable
208        # Calculate parameter mapping now, so we don't get concurrency problems later
209        self.connection = connection
210        callable._calcargs(connection.cursor())
211
212    def __call__(self, *args, **kwargs):
213        """
214        Call the procedure/function with the arguments :var:`args` and
215        :var:`kwargs` mapping Python function arguments to
216        Oracle procedure/function arguments. On return from the procedure the
217        :var:`c_out` parameter is mapped to the CherryPy response body, and the
218        parameters :var:`p_expires` (the number of days from now),
219        :var:`p_lastmodified` (a date in UTC), :var:`p_mimetype`: (a string),
220        :var:`p_encoding` (a string), :var:`p_etag` (a string) and
221        :var:`p_cachecontrol` (a string) are mapped to the appropriate CherryPy
222        response headers. If :var:`p_etag` is not specified a value is calculated.
223   
224        If the procedure/function raised a PL/SQL exception with a code between
225        20200 and 20599, 20000 will be substracted from this value and the
226        resulting value will be used as the HTTP response code, i.e. 20404 will
227        give a "Not Found" response.
228        """
229
230        @self.connection
231        def call(*args, **kwargs):
232            cursor = self.connection.cursor()
233            try:
234                if isinstance(self.callable, orasql.Procedure):
235                    return (None, self.callable(cursor, *args, **kwargs))
236                else:
237                    return self.callable(cursor, *args, **kwargs)
238                return (body, result)
239            except orasql.DatabaseError, exc:
240                if exc.args:
241                    code = getattr(exc[0], "code", 0)
242                    if 20200 <= code <= 20599:
243                        raise cherrypy.HTTPError(code-20000)
244                    else:
245                        raise
246
247        now = getnow()
248        (body, result) = call(*args, **kwargs)
249
250        # Set HTTP headers from parameters
251        expires = result.get("p_expires", None)
252        if expires is not None:
253            cherrypy.response.headers["Expires"] = httpdate(now + datetime.timedelta(days=expires))
254        lastmodified = result.get("p_lastmodified", None)
255        if lastmodified is not None:
256            cherrypy.response.headers["Last-Modified"] = httpdate(lastmodified)
257        encoding = None
258        if isinstance(result, unicode):
259            encoding = "utf-8"
260            result = result.encoding(encoding)
261        mimetype = result.get("p_mimetype", None)
262        if mimetype is not None:
263            if encoding is None:
264                encoding = result.get("p_encoding", None)
265            if encoding is not None:
266                cherrypy.response.headers["Content-Type"] = "%s; charset=%s" % (mimetype, encoding)
267            else:
268                cherrypy.response.headers["Content-Type"] = mimetype
269        hasetag = False
270        etag = result.get("p_etag", None)
271        if etag is not None:
272            cherrypy.response.headers["ETag"] = etag
273            hasetag = True
274        cachecontrol = result.get("p_cachecontrol", None)
275        if cachecontrol is not None:
276            cherrypy.response.headers["Cache-Control"] = cachecontrol
277
278        # Get status code
279        status = result.get("p_status", None)
280        if status is not None:
281            cherrypy.response.status = status
282
283        # Get response body
284        if "c_out" in result:
285            body = result.c_out
286            if hasattr(result, "read"):
287                result = result.read()
288            if not hasetag:
289                cherrypy.response.headers["ETag"] = '"%x"' % hash(body)
290
291        if hasattr(body, "read"):
292            body = body.read()
293        return body
Note: See TracBrowser for help on using the browser.