Changeset 8:1444bd7e562a in livinglogic.python.nightshade

Show
Ignore:
Timestamp:
05/09/06 15:28:38 (14 years ago)
Author:
Walter Doerwald <walter@…>
Branch:
default
Tags:
rel-0-2
Message:

Drop addheaders(), xistpublish(), xistconvert() and expires().

Add Proc(), which wraps a ll.orasql procedure object maps Python arguments
to Oracle parameters and maps the returned field to response body and
heades.

Files:
2 modified

Legend:

Unmodified
Added
Removed
  • nightshade.py

    r7 r8  
    11# -*- coding: iso-8859-1 -*- 
    22 
    3 import datetime, thread 
     3import datetime, threading 
    44 
    55import cherrypy 
     
    1919 
    2020 
    21 def xistconvert(**convertargs): 
    22     """ 
    23     <function>xistconvert</function> can be used as a decorator with arguments 
    24     for CherryPy functions. It will return a function that will convert the result 
    25     of the decorated function (which must return an 
    26     <pyref module="ll.xist.xsc" class="Node">&xist; <class>Node</class></pyref> 
    27     by calling the <method>convert</method> method with <arg>convertargs</arg> 
    28     as the arguments. 
    29     """ 
    30     def decorator(func): 
    31         def wrapper(*args, **kwargs): 
    32             return func(*args, **kwargs).conv(**convertargs) 
    33         wrapper.__name__ = func.__name__ 
    34         wrapper.__doc__ = func.__doc__ 
    35         wrapper.__dict__.update(func.__dict__) 
    36         return wrapper 
    37     return decorator 
    38  
    39  
    40 def xistpublish(publisher=None, **publishargs): 
    41     """ 
    42     <function>xistpublish</function> can be used as a decorator with arguments for 
    43     CherryPy functions. It will return a function that will convert the return value 
    44     of the decorated function (which must be an 
    45     <pyref module="ll.xist.xsc" class="Node">&xist; <class>Node</class></pyref>) 
    46     into a string by calling the <method>asBytes</method> method with 
    47     <arg>publisher</arg> and <arg>publishargs</arg> as the arguments. 
    48     """ 
    49     def decorator(func): 
    50         def wrapper(*args, **kwargs): 
    51             return func(*args, **kwargs).asBytes(publisher, **publishargs) 
    52         wrapper.__name__ = func.__name__ 
    53         wrapper.__doc__ = func.__doc__ 
    54         wrapper.__dict__.update(func.__dict__) 
    55         return wrapper 
    56     return decorator 
    57  
    58  
    59 def cache(timedelta=None, **timedeltaargs): 
     21class cache(object): 
    6022    """ 
    6123    <function>cache</function> can be used as a decorator with arguments for 
     
    6729    <class>datetime.timedelta</class> object). 
    6830    """ 
    69     if timedelta is None: 
    70         timedelta = datetime.timedelta(**timedeltaargs) 
    71     def decorator(func): 
    72         cache = {} 
     31    def __init__(self, timedelta=None, **timedeltaargs): 
     32        if timedelta is None: 
     33            timedelta = datetime.timedelta(**timedeltaargs) 
     34        self.timedelta = timedelta 
     35        self.lock = threading.Lock() 
     36        self.cache = {} 
     37 
     38    def __call__(self, func): 
    7339        def wrapper(*args, **kwargs): 
    74             # Don't cache error responses and POST request 
    75             if cherrypy.request.method != "GET" or (cherrypy.response.status is not None and cherrypy.response.status != 200): 
     40            # Don't cache POST requests etc. 
     41            if cherrypy.request.method != "GET": 
    7642                return func(*args, **kwargs) 
    7743            now = datetime.datetime.utcnow() 
    78             cacheargs = (args, tuple(sorted(kwargs.iteritems()))) 
    79             fetch = False 
     44            cachekey = (args, tuple(sorted(kwargs.iteritems()))) 
     45            self.lock.acquire() 
    8046            try: 
    81                 (timestamp, content, headers) = cache[cacheargs] 
    82             except KeyError: 
    83                 fetch = True 
    84             else: 
    85                 if timestamp+timedelta < now: 
     47                fetch = False 
     48                try: 
     49                    (timestamp, content, headers) = self.cache[cachekey] 
     50                except KeyError: 
    8651                    fetch = True 
    8752                else: 
    88                     cherrypy.response.headers.update(headers) 
    89             if fetch: 
    90                 timestamp = datetime.datetime.utcnow() 
    91                 content = func(*args, **kwargs) 
    92                 cache[cacheargs] = (timestamp, content, cherrypy.response.headers.copy()) 
     53                    if timestamp+self.timedelta < now: 
     54                        fetch = True 
     55                    else: 
     56                        cherrypy.response.headers.update(headers) 
     57                if fetch: 
     58                    timestamp = datetime.datetime.utcnow() 
     59                    content = func(*args, **kwargs) 
     60                    # Don't cache error responses 
     61                    if cherrypy.response.status is None or 200 <= cherrypy.response.status <= 203: 
     62                        self.cache[cachekey] = (timestamp, content, cherrypy.response.headers.copy()) 
     63            finally: 
     64                self.lock.release() 
    9365            return content 
    9466        wrapper.__name__ = func.__name__ 
     
    9668        wrapper.__dict__.update(func.__dict__) 
    9769        return wrapper 
    98     return decorator 
    9970 
    10071 
     
    134105 
    135106 
    136 def expires(timedelta=None, **timedeltaargs): 
     107class Proc(object): 
    137108    """ 
    138     <function>expires</function> can be used as a decorator with arguments for 
    139     CherryPy functions. The returned wrapper function will add an <z>Expires</z> 
    140     header to the response. You can pass the timespan either via <arg>timedelta</arg> 
    141     (which must be a <class>datetime.timedelta</class> object), or via 
    142     <arg>timedeltaargs</arg> (which will be used as keyword arguments for creating 
    143     a <class>datetime.timedelta</class> object). 
     109    <class>Proc</class> wraps a procedure object from 
     110    <pyref module="ll.orasql"><module>ll.orasql</module></pyref> and make it 
     111    usable as a CherryPy handler. 
    144112    """ 
    145     if timedelta is None: 
    146         timedelta = datetime.timedelta(**timedeltaargs) 
    147     def decorator(func): 
    148         cache = {} 
    149         def wrapper(*args, **kwargs): 
    150             # Don't cache error responses and POST request 
    151             if cherrypy.request.method != "GET" or (cherrypy.response.status is not None and cherrypy.response.status != 200): 
    152                 return func(*args, **kwargs) 
    153             expires = datetime.datetime.utcnow() + timedelta 
    154             cherrypy.response.headers["Expires"] = httpdate(expires) 
    155             return func(*args, **kwargs) 
    156         wrapper.__name__ = func.__name__ 
    157         wrapper.__doc__ = func.__doc__ 
    158         wrapper.__dict__.update(func.__dict__) 
    159         return wrapper 
    160     return decorator 
     113    def __init__(self, pool, proc): 
     114        self.pool = pool 
     115        self.proc = proc 
     116        # Calculate parameter mapping now, so we don't get concurrency problems later 
     117        proc._calcrealargs(pool.acquire().cursor()) 
    161118 
     119    def __call__(self, *args, **kwargs): 
     120        """ 
     121        Call the procedure with the arguments <arg>args</arg> and <arg>kwargs</arg> 
     122        mapping Python function arguments to Oracle procedure arguments. On return 
     123        from the procedure the <lit>out</lit> parameter is mapped to the CherryPy 
     124        response body, and the parameters <lit>expires</lit> (the number of days 
     125        from now), <lit>lastmodified</lit> (a date in UTV), <lit>mimetype</lit> 
     126        (a string), <lit>encoding</lit> (a string) and <lit>etag</lit> (a string) 
     127        are mapped to the appropriate CherryPy response headers. If <lit>etag</lit> 
     128        is not specified a value is calculated. 
     129        """ 
     130         
     131        now = datetime.datetime.utcnow() 
     132        while 1: 
     133            connection = self.pool.acquire() 
     134            cursor = connection.cursor() 
     135            try: 
     136                result = self.proc(cursor, *args, **kwargs) 
     137            except cx_Oracle.DatabaseError, exc: 
     138                if "ORA-03114" in str(exc): 
     139                    # Drop dead connection and retry 
     140                    self.pool.drop(connection) 
     141                else: 
     142                    raise 
     143            else: 
     144                break 
    162145 
    163 def getcachelock(thread_index): 
    164     cherrypy.thread_data.lock = thread.allocate_lock() 
    165 cherrypy.server.on_start_thread_list.append(getcachelock) 
     146        # Set HTTP headers from parameters 
     147        expires = result.get("expires", None) 
     148        if expires is not None: 
     149            cherrypy.response.headers["Expire"] = httpdate(now + datetime.timedelta(days=expires)) 
     150        lastmodified = result.get("lastmodified", None) 
     151        if lastmodified is not None: 
     152            cherrypy.response.headers["Last-Modified"] = httpdate(lastmodified) 
     153        mimetype = result.get("mimetype", None) 
     154        if mimetype is not None: 
     155            encoding = result.get("encoding", None) 
     156            if encoding is not None: 
     157                cherrypy.response.headers["Content-Type"] = "%s; charset=%s" % (mimetype, encoding) 
     158            else: 
     159                cherrypy.response.headers["Content-Type"] = mimetype 
     160        hasetag = False 
     161        etag = result.get("etag", None) 
     162        if etag is not None: 
     163            cherrypy.response.headers["ETag"] = etag 
     164            hasetag = True 
    166165 
     166        # Get response body 
     167        if "out" in result: 
     168            result = result.out 
     169            if hasattr(result, "read"): 
     170                result = result.read() 
     171            if not hasetag: 
     172                cherrypy.response.headers["ETag"] = '"%x"' % hash(result) 
    167173 
    168 class Page(object): 
    169     def __init__(self, mimetype, encoding, expiretime=60*60, cachetime=20): 
    170         self.mimetype = mimetype 
    171         self.encoding = encoding 
    172         self.expiretime = datetime.timedelta(seconds=expiretime) 
    173         self.cachetime = datetime.timedelta(seconds=cachetime) 
    174         self.cache = {} 
    175  
    176     def getdata(self, *args, **kwargs): 
    177         """ 
    178         Gets passed all request data and must return the response data as 
    179         a <lit>(data, timestamp)</lit> tuple. <lit>data</lit> may be a string or a 
    180         LOB. If <lit>timestamp</lit> is <lit>None</lit> no <lit>Last-Modified</lit> 
    181         header will be returned and no <lit>If-Modified-Since</lit> handling will 
    182         be done. 
    183          
    184         If the requested resource is not available <lit>data</lit> must be 
    185         <lit>None</lit>. 
    186         """ 
    187  
    188     def getcachekey(self, *args, **kwargs): 
    189         """ 
    190         Gets passed all request data and must return the cache key 
    191         where the response data for this request data can be looked up. 
    192         """ 
    193         return (args, kwargs) 
    194  
    195     @cherrypy.expose 
    196     def default(self, *args, **kwargs): 
    197         now = datetime.datetime.utcnow() 
    198  
    199         lock = cherrypy.thread_data.lock 
    200         try: 
    201             lock.acquire() 
    202             try: 
    203                 cachekey = self.getcachekey(*args, **kwargs) 
    204             except TypeError: # probably wrong arguments 
    205                 raise cherrypy.NotFound 
    206      
    207             fetch = False 
    208             try: 
    209                 (timestamp, data, lastmodified) = self.cache[cachekey] 
    210             except KeyError: 
    211                 fetch = True 
    212             else: 
    213                 if timestamp+self.cachetime < now: 
    214                     fetch = True 
    215             if fetch: 
    216                 (data, lastmodified) = self.getdata(*args, **kwargs) 
    217                 if data is not None: 
    218                     if hasattr(data, "read"): 
    219                         data = data.read() 
    220                     self.cache[cachekey] = (now, data, lastmodified) 
    221         finally: 
    222             lock.release() 
    223  
    224         if data is None: 
    225             raise cherrypy.NotFound 
    226  
    227         if lastmodified is not None: 
    228             lastmodified = httpdate(lastmodified) 
    229  
    230         etag = '"%x"' % hash(data) 
    231  
    232         req_ifmodifiedsince = cherrypy.request.headerMap.get("If-Modified-Since", None) 
    233         req_ifnonematch = cherrypy.request.headerMap.get("If-None-Match", None) 
    234         modified = True 
    235         if req_ifmodifiedsince is not None or req_ifnonematch is not None: 
    236             modified = False 
    237             if req_ifmodifiedsince is not None and req_ifmodifiedsince != lastmodified: 
    238                 modified = True 
    239             if req_ifnonematch is not None and req_ifnonematch != etag: 
    240                 modified = True 
    241         if not modified: 
    242             cherrypy.response.status = "304 Not Modified" 
    243             cherrypy.response.body = [] 
    244             return "" 
    245         if self.encoding is not None: 
    246             cherrypy.response.headerMap["Content-Type"] = "%s; charset=%s" % (self.mimetype, self.encoding) 
    247         else: 
    248             cherrypy.response.headerMap["Content-Type"] = self.mimetype 
    249         if lastmodified is not None: 
    250             cherrypy.response.headerMap["Last-Modified"] = lastmodified 
    251  
    252         expires = httpdate(datetime.datetime.utcnow() + self.expiretime) 
    253         cherrypy.response.headerMap["Expires"] = expires 
    254  
    255         cherrypy.response.headerMap["ETag"] = etag 
    256         return data 
    257  
    258  
    259 class RSSPage(Page): 
    260     def __init__(self, pool, query, mimetype, encoding, expiretime=5*60*60, cachetime=20): 
    261         Page.__init__(self, mimetype=mimetype, encoding=encoding, expiretime=expiretime, cachetime=cachetime) 
    262         if isinstance(pool, basestring): 
    263             pool = cx_Oracle.SessionPool(connection, "", "", 1, 10, 3, None, True) 
    264         self.pool = pool 
    265         self.query = query 
    266  
    267     def getcachekey(self, identifier, maxcount=20): 
    268         try: 
    269             maxcount = int(maxcount) 
    270         except ValueError: 
    271             maxcount = 20 
    272         maxcount = max(1, min(maxcount, 100)) 
    273         return (identifier, maxcount) 
    274  
    275     def getdata(self, identifier, maxcount=20): 
    276         (identifier, maxcount) = self.getcachekey(identifier, maxcount) 
    277         c = self.pool.acquire().cursor() 
    278         c.execute(self.query, identifier=identifier, maxcount=maxcount, expires=self.expiretime.days*1440+self.expiretime.seconds/60) 
    279         rss = c.fetchone() 
    280         if rss is not None: 
    281             return rss 
    282         return (None, None) 
     174        return result 
  • setup.py

    r0 r8  
    22# -*- coding: iso-8859-1 -*- 
    33 
    4 # Setup script for ll-core 
     4# Setup script for ll-nighshade 
    55 
    66 
     
    5252tools.setup( 
    5353    name="ll-nightshade", 
    54     version="0.1", 
    55     description="Serve the output of TOXIC functions with CherryPy", 
     54    version="0.2", 
     55    description="Serve the output of TOXIC functions/procedures with CherryPy", 
    5656    long_description=DESCRIPTION, 
    5757    author=u"Walter Dörwald".encode("utf-8"), 
    5858    author_email="walter@livinglogic.de", 
    59     #url="http://www.livinglogic.de/Python/core/", 
    60     #download_url="http://www.livinglogic.de/Python/core/Download.html", 
     59    #url="http://www.livinglogic.de/Python/nightshade/", 
     60    #download_url="http://www.livinglogic.de/Python/nightshade/Download.html", 
    6161    license="Python", 
    6262    classifiers=[c for c in CLASSIFIERS.strip().splitlines() if c.strip() and not c.strip().startswith("#")],