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