source file: /Library/Python/2.3/site-packages/CherryPy-3.0.1-py2.3.egg/cherrypy/lib/caching.py
file stats: 120 lines, 17 executed: 14.2% covered
   1. import datetime
   2. import threading
   3. import time
   4. 
   5. import cherrypy
   6. from cherrypy.lib import cptools, http
   7. 
   8. 
   9. class MemoryCache:
  10. 
  11.     def __init__(self):
  12.         self.clear()
  13.         t = threading.Thread(target=self.expire_cache, name='expire_cache')
  14.         self.expiration_thread = t
  15.         t.setDaemon(True)
  16.         t.start()
  17. 
  18.     def clear(self):
  19.         """Reset the cache to its initial, empty state."""
  20.         self.cache = {}
  21.         self.expirations = {}
  22.         self.tot_puts = 0
  23.         self.tot_gets = 0
  24.         self.tot_hist = 0
  25.         self.tot_expires = 0
  26.         self.tot_non_modified = 0
  27.         self.cursize = 0
  28. 
  29.     def _key(self):
  30.         request = cherrypy.request
  31.         return request.config.get("tools.caching.key", cherrypy.url(qs=request.query_string))
  32.     key = property(_key)
  33. 
  34.     def expire_cache(self):
  35.         # expire_cache runs in a separate thread which the servers are
  36.         # not aware of. It's possible that "time" will be set to None
  37.         # arbitrarily, so we check "while time" to avoid exceptions.
  38.         # See tickets #99 and #180 for more information.
  39.         while time:
  40.             now = time.time()
  41.             for expiration_time, objects in self.expirations.items():
  42.                 if expiration_time <= now:
  43.                     for obj_size, obj_key in objects:
  44.                         try:
  45.                             del self.cache[obj_key]
  46.                             self.tot_expires += 1
  47.                             self.cursize -= obj_size
  48.                         except KeyError:
  49.                             # the key may have been deleted elsewhere
  50.                             pass
  51.                     del self.expirations[expiration_time]
  52.             time.sleep(0.1)
  53. 
  54.     def get(self):
  55.         """Return the object if in the cache, else None."""
  56.         self.tot_gets += 1
  57.         cache_item = self.cache.get(self.key, None)
  58.         if cache_item:
  59.             self.tot_hist += 1
  60.             return cache_item
  61.         else:
  62.             return None
  63. 
  64.     def put(self, obj):
  65.         conf = cherrypy.request.config.get
  66. 
  67.         if len(self.cache) < conf("tools.caching.maxobjects", 1000):
  68.             # Size check no longer includes header length
  69.             obj_size = len(obj[2])
  70.             maxobj_size = conf("tools.caching.maxobj_size", 100000)
  71. 
  72.             total_size = self.cursize + obj_size
  73.             maxsize = conf("tools.caching.maxsize", 10000000)
  74. 
  75.             # checks if there's space for the object
  76.             if (obj_size < maxobj_size and total_size < maxsize):
  77.                 # add to the expirations list and cache
  78.                 expiration_time = cherrypy.response.time + conf("tools.caching.delay", 600)
  79.                 obj_key = self.key
  80.                 bucket = self.expirations.setdefault(expiration_time, [])
  81.                 bucket.append((obj_size, obj_key))
  82.                 self.cache[obj_key] = obj
  83.                 self.tot_puts += 1
  84.                 self.cursize = total_size
  85. 
  86.     def delete(self):
  87.         self.cache.pop(self.key)
  88. 
  89. 
  90. def get(invalid_methods=("POST", "PUT", "DELETE"), cache_class=MemoryCache):
  91.     """Try to obtain cached output. If fresh enough, raise HTTPError(304).
  92. 
  93.     If POST, PUT, or DELETE:
  94.         * invalidates (deletes) any cached response for this resource
  95.         * sets request.cached = False
  96.         * sets request.cacheable = False
  97. 
  98.     else if a cached copy exists:
  99.         * sets request.cached = True
 100.         * sets request.cacheable = False
 101.         * sets response.headers to the cached values
 102.         * checks the cached Last-Modified response header against the
 103.             current If-(Un)Modified-Since request headers; raises 304
 104.             if necessary.
 105.         * sets response.status and response.body to the cached values
 106.         * returns True
 107. 
 108.     otherwise:
 109.         * sets request.cached = False
 110.         * sets request.cacheable = True
 111.         * returns False
 112.     """
 113.     if not hasattr(cherrypy, "_cache"):
 114.         cherrypy._cache = cache_class()
 115. 
 116.     request = cherrypy.request
 117. 
 118.     # POST, PUT, DELETE should invalidate (delete) the cached copy.
 119.     # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
 120.     if request.method in invalid_methods:
 121.         cherrypy._cache.delete()
 122.         request.cached = False
 123.         request.cacheable = False
 124.         return False
 125. 
 126.     cache_data = cherrypy._cache.get()
 127.     request.cached = c = bool(cache_data)
 128.     request.cacheable = not c
 129.     if c:
 130.         response = cherrypy.response
 131.         s, response.headers, b, create_time = cache_data
 132. 
 133.         # Add the required Age header
 134.         response.headers["Age"] = str(int(response.time - create_time))
 135. 
 136.         try:
 137.             # Note that validate_since depends on a Last-Modified header;
 138.             # this was put into the cached copy, and should have been
 139.             # resurrected just above (response.headers = cache_data[1]).
 140.             cptools.validate_since()
 141.         except cherrypy.HTTPError, x:
 142.             if x.status == 304:
 143.                 cherrypy._cache.tot_non_modified += 1
 144.             raise
 145. 
 146.         # serve it & get out from the request
 147.         response.status = s
 148.         response.body = b
 149.     return c
 150. 
 151. 
 152. def tee_output():
 153.     response = cherrypy.response
 154.     output = []
 155.     def tee(body):
 156.         """Tee response.body into a list."""
 157.         for chunk in body:
 158.             output.append(chunk)
 159.             yield chunk
 160.         # Might as well do this here; why cache if the body isn't consumed?
 161.         if response.headers.get('Pragma', None) != 'no-cache':
 162.             # save the cache data
 163.             body = ''.join([chunk for chunk in output])
 164.             cherrypy._cache.put((response.status, response.headers or {},
 165.                                  body, response.time))
 166.     response.body = tee(response.body)
 167. 
 168. 
 169. def expires(secs=0, force=False):
 170.     """Tool for influencing cache mechanisms using the 'Expires' header.
 171. 
 172.     'secs' must be either an int or a datetime.timedelta, and indicates the
 173.     number of seconds between response.time and when the response should
 174.     expire. The 'Expires' header will be set to (response.time + secs).
 175. 
 176.     If 'secs' is zero, the following "cache prevention" headers are also set:
 177.        'Pragma': 'no-cache'
 178.        'Cache-Control': 'no-cache, must-revalidate'
 179. 
 180.     If 'force' is False (the default), the following headers are checked:
 181.     'Etag', 'Last-Modified', 'Age', 'Expires'. If any are already present,
 182.     none of the above response headers are set.
 183.     """
 184. 
 185.     response = cherrypy.response
 186.     headers = response.headers
 187. 
 188.     cacheable = False
 189.     if not force:
 190.         # some header names that indicate that the response can be cached
 191.         for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
 192.             if indicator in headers:
 193.                 cacheable = True
 194.                 break
 195. 
 196.     if not cacheable:
 197.         if isinstance(secs, datetime.timedelta):
 198.             secs = (86400 * secs.days) + secs.seconds
 199. 
 200.         if secs == 0:
 201.             if force or "Pragma" not in headers:
 202.                 headers["Pragma"] = "no-cache"
 203.             if cherrypy.request.protocol >= (1, 1):
 204.                 if force or "Cache-Control" not in headers:
 205.                     headers["Cache-Control"] = "no-cache, must-revalidate"
 206. 
 207.         expiry = http.HTTPDate(response.time + secs)
 208.         if force or "Expires" not in headers:
 209.             headers["Expires"] = expiry