1""" 2Represents the Cache-Control header 3""" 4import re 5 6class UpdateDict(dict): 7 """ 8 Dict that has a callback on all updates 9 """ 10 # these are declared as class attributes so that 11 # we don't need to override constructor just to 12 # set some defaults 13 updated = None 14 updated_args = None 15 16 def _updated(self): 17 """ 18 Assign to new_dict.updated to track updates 19 """ 20 updated = self.updated 21 if updated is not None: 22 args = self.updated_args 23 if args is None: 24 args = (self,) 25 updated(*args) 26 27 def __setitem__(self, key, item): 28 dict.__setitem__(self, key, item) 29 self._updated() 30 31 def __delitem__(self, key): 32 dict.__delitem__(self, key) 33 self._updated() 34 35 def clear(self): 36 dict.clear(self) 37 self._updated() 38 39 def update(self, *args, **kw): 40 dict.update(self, *args, **kw) 41 self._updated() 42 43 def setdefault(self, key, value=None): 44 val = dict.setdefault(self, key, value) 45 if val is value: 46 self._updated() 47 return val 48 49 def pop(self, *args): 50 v = dict.pop(self, *args) 51 self._updated() 52 return v 53 54 def popitem(self): 55 v = dict.popitem(self) 56 self._updated() 57 return v 58 59 60token_re = re.compile( 61 r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?') 62need_quote_re = re.compile(r'[^a-zA-Z0-9._-]') 63 64 65class exists_property(object): 66 """ 67 Represents a property that either is listed in the Cache-Control 68 header, or is not listed (has no value) 69 """ 70 def __init__(self, prop, type=None): 71 self.prop = prop 72 self.type = type 73 74 def __get__(self, obj, type=None): 75 if obj is None: 76 return self 77 return self.prop in obj.properties 78 79 def __set__(self, obj, value): 80 if (self.type is not None 81 and self.type != obj.type): 82 raise AttributeError( 83 "The property %s only applies to %s Cache-Control" % ( 84 self.prop, self.type)) 85 86 if value: 87 obj.properties[self.prop] = None 88 else: 89 if self.prop in obj.properties: 90 del obj.properties[self.prop] 91 92 def __delete__(self, obj): 93 self.__set__(obj, False) 94 95 96class value_property(object): 97 """ 98 Represents a property that has a value in the Cache-Control header. 99 100 When no value is actually given, the value of self.none is returned. 101 """ 102 def __init__(self, prop, default=None, none=None, type=None): 103 self.prop = prop 104 self.default = default 105 self.none = none 106 self.type = type 107 108 def __get__(self, obj, type=None): 109 if obj is None: 110 return self 111 if self.prop in obj.properties: 112 value = obj.properties[self.prop] 113 if value is None: 114 return self.none 115 else: 116 return value 117 else: 118 return self.default 119 120 def __set__(self, obj, value): 121 if (self.type is not None 122 and self.type != obj.type): 123 raise AttributeError( 124 "The property %s only applies to %s Cache-Control" % ( 125 self.prop, self.type)) 126 if value == self.default: 127 if self.prop in obj.properties: 128 del obj.properties[self.prop] 129 elif value is True: 130 obj.properties[self.prop] = None # Empty value, but present 131 else: 132 obj.properties[self.prop] = value 133 134 def __delete__(self, obj): 135 if self.prop in obj.properties: 136 del obj.properties[self.prop] 137 138 139class CacheControl(object): 140 141 """ 142 Represents the Cache-Control header. 143 144 By giving a type of ``'request'`` or ``'response'`` you can 145 control what attributes are allowed (some Cache-Control values 146 only apply to requests or responses). 147 """ 148 149 update_dict = UpdateDict 150 151 def __init__(self, properties, type): 152 self.properties = properties 153 self.type = type 154 155 @classmethod 156 def parse(cls, header, updates_to=None, type=None): 157 """ 158 Parse the header, returning a CacheControl object. 159 160 The object is bound to the request or response object 161 ``updates_to``, if that is given. 162 """ 163 if updates_to: 164 props = cls.update_dict() 165 props.updated = updates_to 166 else: 167 props = {} 168 for match in token_re.finditer(header): 169 name = match.group(1) 170 value = match.group(2) or match.group(3) or None 171 if value: 172 try: 173 value = int(value) 174 except ValueError: 175 pass 176 props[name] = value 177 obj = cls(props, type=type) 178 if updates_to: 179 props.updated_args = (obj,) 180 return obj 181 182 def __repr__(self): 183 return '<CacheControl %r>' % str(self) 184 185 # Request values: 186 # no-cache shared (below) 187 # no-store shared (below) 188 # max-age shared (below) 189 max_stale = value_property('max-stale', none='*', type='request') 190 min_fresh = value_property('min-fresh', type='request') 191 # no-transform shared (below) 192 only_if_cached = exists_property('only-if-cached', type='request') 193 194 # Response values: 195 public = exists_property('public', type='response') 196 private = value_property('private', none='*', type='response') 197 no_cache = value_property('no-cache', none='*') 198 no_store = exists_property('no-store') 199 no_transform = exists_property('no-transform') 200 must_revalidate = exists_property('must-revalidate', type='response') 201 proxy_revalidate = exists_property('proxy-revalidate', type='response') 202 max_age = value_property('max-age', none=-1) 203 s_maxage = value_property('s-maxage', type='response') 204 s_max_age = s_maxage 205 stale_while_revalidate = value_property( 206 'stale-while-revalidate', type='response') 207 stale_if_error = value_property('stale-if-error', type='response') 208 209 def __str__(self): 210 return serialize_cache_control(self.properties) 211 212 def copy(self): 213 """ 214 Returns a copy of this object. 215 """ 216 return self.__class__(self.properties.copy(), type=self.type) 217 218 219def serialize_cache_control(properties): 220 if isinstance(properties, CacheControl): 221 properties = properties.properties 222 parts = [] 223 for name, value in sorted(properties.items()): 224 if value is None: 225 parts.append(name) 226 continue 227 value = str(value) 228 if need_quote_re.search(value): 229 value = '"%s"' % value 230 parts.append('%s=%s' % (name, value)) 231 return ', '.join(parts) 232