root/livinglogic.python.nightshade/nightshade.py @ 16:44fc74571ee8

Revision 16:44fc74571ee8, 7.0 KB (checked in by Walter Doerwald <walter@…>, 14 years ago)

Fix handling of UTC timestamps (offset was substracted twice).

Line 
1# -*- coding: iso-8859-1 -*-
2
3import time, datetime, threading
4
5import cherrypy
6
7import cx_Oracle
8
9weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
10monthname = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
11
12
13class UTC(datetime.tzinfo):
14    def utcoffset(self, dt):
15        return datetime.timedelta(0)
16
17    def dst(self, dt):
18        return datetime.timedelta(0)
19
20    def tzname(self, dt):
21        return "UTC"
22
23utc = UTC()
24
25
26def httpdate(dt):
27    """
28    <par>Return a string suitable for a <z>Last-Modified</z> and <z>Expires</z> header.</par>
29   
30    <par><arg>dt</arg> is a <class>datetime.datetime</class> object.
31    If <lit><arg>dt</arg>.tzinfo</lit> is <lit>None</lit> <arg>dt</arg> is assumed
32    to be in the local timezone (using the current UTFC offset which might be
33    different from the one used by <arg>dt</arg>).</par>
34    """
35    if dt.tzinfo is None:
36        dt += datetime.timedelta(seconds=[time.timezone, time.altzone][time.daylight])
37    else:
38        dt -= dt.tzinfo.utcoffset(dt)
39    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)
40
41
42class cache(object):
43    """
44    <par>Decorator that adds caching to a CherryPy handler.</par>
45   
46    <par>Calling a <class>cache</class> object is callable will cache return
47    values of the decorated function for a certain amount of time. You can pass
48    the timespan either via <arg>timedelta</arg> (which must be a
49    <class>datetime.timedelta</class> object), or via <arg>timedeltaargs</arg>
50    (which will be used as keyword arguments for creating a
51    <class>datetime.timedelta</class> object).
52    """
53    def __init__(self, timedelta=None, **timedeltaargs):
54        if timedelta is None:
55            timedelta = datetime.timedelta(**timedeltaargs)
56        self.timedelta = timedelta
57        self.lock = threading.Lock()
58        self.cache = {}
59
60    def __call__(self, func):
61        def wrapper(*args, **kwargs):
62            # Don't cache POST requests etc.
63            if cherrypy.request.method != "GET":
64                return func(*args, **kwargs)
65            now = datetime.datetime.utcnow().replace(tzinfo=utc)
66            cachekey = (args, tuple(sorted(kwargs.iteritems())))
67            self.lock.acquire()
68            try:
69                fetch = False
70                try:
71                    (timestamp, content, headers) = self.cache[cachekey]
72                except KeyError:
73                    fetch = True
74                else:
75                    if timestamp+self.timedelta < now:
76                        fetch = True
77                    else:
78                        cherrypy.response.headers.update(headers)
79                if fetch:
80                    timestamp = datetime.datetime.utcnow()
81                    content = func(*args, **kwargs)
82                    # Don't cache error responses
83                    if cherrypy.response.status is None or 200 <= cherrypy.response.status <= 203:
84                        self.cache[cachekey] = (timestamp, content, cherrypy.response.headers.copy())
85            finally:
86                self.lock.release()
87            return content
88        wrapper.__name__ = func.__name__
89        wrapper.__doc__ = func.__doc__
90        wrapper.__dict__.update(func.__dict__)
91        return wrapper
92
93
94def conditional(func):
95    """
96    <par>Decorator that adds handling of  conditional <lit>GET</lit>s to a CherryPy handler.</par>
97   
98    <par>The decorated function will correctly handle the <lit>If-Modified-Since</lit>
99    and <lit>If-None-Match</lit> request headers. For this to work properly
100    the decorated function should set the <lit>Last-Modified</lit> and/or
101    the <lit>ETag</lit> header.
102    """
103    def wrapper(*args, **kwargs):
104        data = func(*args, **kwargs)
105
106        req_ifmodifiedsince = cherrypy.request.headerMap.get("If-Modified-Since", None)
107        req_ifnonematch = cherrypy.request.headerMap.get("If-None-Match", None)
108        res_lastmodified = cherrypy.response.headerMap.get("Last-Modified", None)
109        res_etag = cherrypy.response.headerMap.get("ETag", None)
110
111        modified = True
112        if req_ifmodifiedsince is not None and res_lastmodified is not None:
113            modified = req_ifmodifiedsince != res_lastmodified
114        if req_ifnonematch is not None and res_etag is not None and not modified:
115            modified = req_ifnonematch != res_etag
116        if not modified:
117            cherrypy.response.status = "304 Not Modified"
118            # The proper headers have already been set, but Content-Length seems to be broken
119            cherrypy.response.body = None
120            return ""
121        return data
122
123    wrapper.__name__ = func.__name__
124    wrapper.__doc__ = func.__doc__
125    wrapper.__dict__.update(func.__dict__)
126    return wrapper
127
128
129class Proc(object):
130    """
131    <par>Wrap an Oracle procedure in a CherryPy handler.</par>
132
133    <par><class>Proc</class> object wraps a procedure object from
134    <pyref module="ll.orasql"><module>ll.orasql</module></pyref> and make it
135    callable just like a CherryPy handler.
136    """
137    def __init__(self, proc, pool=None, connection=None):
138        """
139        Create a
140        """
141        if (pool is not None) == (connection is not None):
142            raise TypeError("either pool or connection must be specified")
143        self.proc = proc
144        # Calculate parameter mapping now, so we don't get concurrency problems later
145        if pool is not None:
146            proc._calcrealargs(pool.acquire().cursor())
147        else:
148            proc._calcrealargs(connection.cursor())
149        self.pool = pool
150        self.connection = connection
151
152    def __call__(self, *args, **kwargs):
153        """
154        Call the procedure with the arguments <arg>args</arg> and <arg>kwargs</arg>
155        mapping Python function arguments to Oracle procedure arguments. On return
156        from the procedure the <lit>out</lit> parameter is mapped to the CherryPy
157        response body, and the parameters <lit>expires</lit> (the number of days
158        from now), <lit>lastmodified</lit> (a date in UTV), <lit>mimetype</lit>
159        (a string), <lit>encoding</lit> (a string) and <lit>etag</lit> (a string)
160        are mapped to the appropriate CherryPy response headers. If <lit>etag</lit>
161        is not specified a value is calculated.
162        """
163       
164        now = datetime.datetime.utcnow().replace(tzinfo=utc)
165        if self.pool is not None:
166            while 1:
167                connection = self.pool.acquire()
168                cursor = connection.cursor()
169                try:
170                    result = self.proc(cursor, *args, **kwargs)
171                except cx_Oracle.DatabaseError, exc:
172                    if "ORA-03114" in str(exc):
173                        # Drop dead connection and retry
174                        self.pool.drop(connection)
175                    else:
176                        raise
177                else:
178                    break
179        else:
180            cursor = self.connection.cursor()
181            result = self.proc(cursor, *args, **kwargs)
182
183        # Set HTTP headers from parameters
184        expires = result.get("expires", None)
185        if expires is not None:
186            cherrypy.response.headers["Expire"] = httpdate(now + datetime.timedelta(days=expires))
187        lastmodified = result.get("lastmodified", None)
188        if lastmodified is not None:
189            cherrypy.response.headers["Last-Modified"] = httpdate(lastmodified)
190        mimetype = result.get("mimetype", None)
191        if mimetype is not None:
192            encoding = result.get("encoding", None)
193            if encoding is not None:
194                cherrypy.response.headers["Content-Type"] = "%s; charset=%s" % (mimetype, encoding)
195            else:
196                cherrypy.response.headers["Content-Type"] = mimetype
197        hasetag = False
198        etag = result.get("etag", None)
199        if etag is None:
200            cherrypy.response.headers["ETag"] = etag
201            hasetag = True
202
203        # Get status code
204        status = result.get("status", None)
205        if status is not None:
206            cherrypy.response.status = status
207
208        # Get response body
209        if "out" in result:
210            result = result.out
211            if hasattr(result, "read"):
212                result = result.read()
213            if not hasetag:
214                cherrypy.response.headers["ETag"] = '"%x"' % hash(result)
215
216        return result
Note: See TracBrowser for help on using the browser.