root/livinglogic.python.nightshade/src/ll/nightshade.py @ 96:60afb9ce1dee

Revision 96:60afb9ce1dee, 8.9 KB (checked in by Walter Doerwald <walter@…>, 10 years ago)

Call now does a commit.

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                    result = (None, self.callable(cursor, *args, **kwargs))
236                else:
237                    result = self.callable(cursor, *args, **kwargs)
238                cursor.connection.commit()
239                return result
240            except orasql.DatabaseError, exc:
241                if exc.args:
242                    code = getattr(exc[0], "code", 0)
243                    if 20200 <= code <= 20599:
244                        raise cherrypy.HTTPError(code-20000)
245                    else:
246                        raise
247
248        now = getnow()
249        (body, result) = call(*args, **kwargs)
250
251        # Set HTTP headers from parameters
252        expires = result.get("p_expires", None)
253        if expires is not None:
254            cherrypy.response.headers["Expires"] = httpdate(now + datetime.timedelta(days=expires))
255        lastmodified = result.get("p_lastmodified", None)
256        if lastmodified is not None:
257            cherrypy.response.headers["Last-Modified"] = httpdate(lastmodified)
258        encoding = None
259        if isinstance(result, unicode):
260            encoding = "utf-8"
261            result = result.encoding(encoding)
262        mimetype = result.get("p_mimetype", None)
263        if mimetype is not None:
264            if encoding is None:
265                encoding = result.get("p_encoding", None)
266            if encoding is not None:
267                cherrypy.response.headers["Content-Type"] = "%s; charset=%s" % (mimetype, encoding)
268            else:
269                cherrypy.response.headers["Content-Type"] = mimetype
270        hasetag = False
271        etag = result.get("p_etag", None)
272        if etag is not None:
273            cherrypy.response.headers["ETag"] = etag
274            hasetag = True
275        cachecontrol = result.get("p_cachecontrol", None)
276        if cachecontrol is not None:
277            cherrypy.response.headers["Cache-Control"] = cachecontrol
278
279        # Get status code
280        status = result.get("p_status", None)
281        if status is not None:
282            cherrypy.response.status = status
283
284        # Get response body
285        if "c_out" in result:
286            body = result.c_out
287            if hasattr(result, "read"):
288                result = result.read()
289            if not hasetag:
290                cherrypy.response.headers["ETag"] = '"%x"' % hash(body)
291
292        if hasattr(body, "read"):
293            body = body.read()
294        return body
Note: See TracBrowser for help on using the browser.