232 lines
6.6 KiB
Python
232 lines
6.6 KiB
Python
"""
|
|
Represents the Cache-Control header
|
|
"""
|
|
import re
|
|
|
|
class UpdateDict(dict):
|
|
"""
|
|
Dict that has a callback on all updates
|
|
"""
|
|
# these are declared as class attributes so that
|
|
# we don't need to override constructor just to
|
|
# set some defaults
|
|
updated = None
|
|
updated_args = None
|
|
|
|
def _updated(self):
|
|
"""
|
|
Assign to new_dict.updated to track updates
|
|
"""
|
|
updated = self.updated
|
|
if updated is not None:
|
|
args = self.updated_args
|
|
if args is None:
|
|
args = (self,)
|
|
updated(*args)
|
|
|
|
def __setitem__(self, key, item):
|
|
dict.__setitem__(self, key, item)
|
|
self._updated()
|
|
|
|
def __delitem__(self, key):
|
|
dict.__delitem__(self, key)
|
|
self._updated()
|
|
|
|
def clear(self):
|
|
dict.clear(self)
|
|
self._updated()
|
|
|
|
def update(self, *args, **kw):
|
|
dict.update(self, *args, **kw)
|
|
self._updated()
|
|
|
|
def setdefault(self, key, value=None):
|
|
val = dict.setdefault(self, key, value)
|
|
if val is value:
|
|
self._updated()
|
|
return val
|
|
|
|
def pop(self, *args):
|
|
v = dict.pop(self, *args)
|
|
self._updated()
|
|
return v
|
|
|
|
def popitem(self):
|
|
v = dict.popitem(self)
|
|
self._updated()
|
|
return v
|
|
|
|
|
|
token_re = re.compile(
|
|
r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?')
|
|
need_quote_re = re.compile(r'[^a-zA-Z0-9._-]')
|
|
|
|
|
|
class exists_property(object):
|
|
"""
|
|
Represents a property that either is listed in the Cache-Control
|
|
header, or is not listed (has no value)
|
|
"""
|
|
def __init__(self, prop, type=None):
|
|
self.prop = prop
|
|
self.type = type
|
|
|
|
def __get__(self, obj, type=None):
|
|
if obj is None:
|
|
return self
|
|
return self.prop in obj.properties
|
|
|
|
def __set__(self, obj, value):
|
|
if (self.type is not None
|
|
and self.type != obj.type):
|
|
raise AttributeError(
|
|
"The property %s only applies to %s Cache-Control" % (
|
|
self.prop, self.type))
|
|
|
|
if value:
|
|
obj.properties[self.prop] = None
|
|
else:
|
|
if self.prop in obj.properties:
|
|
del obj.properties[self.prop]
|
|
|
|
def __delete__(self, obj):
|
|
self.__set__(obj, False)
|
|
|
|
|
|
class value_property(object):
|
|
"""
|
|
Represents a property that has a value in the Cache-Control header.
|
|
|
|
When no value is actually given, the value of self.none is returned.
|
|
"""
|
|
def __init__(self, prop, default=None, none=None, type=None):
|
|
self.prop = prop
|
|
self.default = default
|
|
self.none = none
|
|
self.type = type
|
|
|
|
def __get__(self, obj, type=None):
|
|
if obj is None:
|
|
return self
|
|
if self.prop in obj.properties:
|
|
value = obj.properties[self.prop]
|
|
if value is None:
|
|
return self.none
|
|
else:
|
|
return value
|
|
else:
|
|
return self.default
|
|
|
|
def __set__(self, obj, value):
|
|
if (self.type is not None
|
|
and self.type != obj.type):
|
|
raise AttributeError(
|
|
"The property %s only applies to %s Cache-Control" % (
|
|
self.prop, self.type))
|
|
if value == self.default:
|
|
if self.prop in obj.properties:
|
|
del obj.properties[self.prop]
|
|
elif value is True:
|
|
obj.properties[self.prop] = None # Empty value, but present
|
|
else:
|
|
obj.properties[self.prop] = value
|
|
|
|
def __delete__(self, obj):
|
|
if self.prop in obj.properties:
|
|
del obj.properties[self.prop]
|
|
|
|
|
|
class CacheControl(object):
|
|
|
|
"""
|
|
Represents the Cache-Control header.
|
|
|
|
By giving a type of ``'request'`` or ``'response'`` you can
|
|
control what attributes are allowed (some Cache-Control values
|
|
only apply to requests or responses).
|
|
"""
|
|
|
|
update_dict = UpdateDict
|
|
|
|
def __init__(self, properties, type):
|
|
self.properties = properties
|
|
self.type = type
|
|
|
|
@classmethod
|
|
def parse(cls, header, updates_to=None, type=None):
|
|
"""
|
|
Parse the header, returning a CacheControl object.
|
|
|
|
The object is bound to the request or response object
|
|
``updates_to``, if that is given.
|
|
"""
|
|
if updates_to:
|
|
props = cls.update_dict()
|
|
props.updated = updates_to
|
|
else:
|
|
props = {}
|
|
for match in token_re.finditer(header):
|
|
name = match.group(1)
|
|
value = match.group(2) or match.group(3) or None
|
|
if value:
|
|
try:
|
|
value = int(value)
|
|
except ValueError:
|
|
pass
|
|
props[name] = value
|
|
obj = cls(props, type=type)
|
|
if updates_to:
|
|
props.updated_args = (obj,)
|
|
return obj
|
|
|
|
def __repr__(self):
|
|
return '<CacheControl %r>' % str(self)
|
|
|
|
# Request values:
|
|
# no-cache shared (below)
|
|
# no-store shared (below)
|
|
# max-age shared (below)
|
|
max_stale = value_property('max-stale', none='*', type='request')
|
|
min_fresh = value_property('min-fresh', type='request')
|
|
# no-transform shared (below)
|
|
only_if_cached = exists_property('only-if-cached', type='request')
|
|
|
|
# Response values:
|
|
public = exists_property('public', type='response')
|
|
private = value_property('private', none='*', type='response')
|
|
no_cache = value_property('no-cache', none='*')
|
|
no_store = exists_property('no-store')
|
|
no_transform = exists_property('no-transform')
|
|
must_revalidate = exists_property('must-revalidate', type='response')
|
|
proxy_revalidate = exists_property('proxy-revalidate', type='response')
|
|
max_age = value_property('max-age', none=-1)
|
|
s_maxage = value_property('s-maxage', type='response')
|
|
s_max_age = s_maxage
|
|
stale_while_revalidate = value_property(
|
|
'stale-while-revalidate', type='response')
|
|
stale_if_error = value_property('stale-if-error', type='response')
|
|
|
|
def __str__(self):
|
|
return serialize_cache_control(self.properties)
|
|
|
|
def copy(self):
|
|
"""
|
|
Returns a copy of this object.
|
|
"""
|
|
return self.__class__(self.properties.copy(), type=self.type)
|
|
|
|
|
|
def serialize_cache_control(properties):
|
|
if isinstance(properties, CacheControl):
|
|
properties = properties.properties
|
|
parts = []
|
|
for name, value in sorted(properties.items()):
|
|
if value is None:
|
|
parts.append(name)
|
|
continue
|
|
value = str(value)
|
|
if need_quote_re.search(value):
|
|
value = '"%s"' % value
|
|
parts.append('%s=%s' % (name, value))
|
|
return ', '.join(parts)
|