""" 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 '' % 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)