77cee0170481f0b83f38b8029ad9b3eb73c45b6d
[sdc/sdc-distribution-client.git] /
1 from __future__ import absolute_import
2 from collections import Mapping, MutableMapping
3 try:
4     from threading import RLock
5 except ImportError:  # Platform-specific: No threads available
6     class RLock:
7         def __enter__(self):
8             pass
9
10         def __exit__(self, exc_type, exc_value, traceback):
11             pass
12
13
14 try:  # Python 2.7+
15     from collections import OrderedDict
16 except ImportError:
17     from .packages.ordered_dict import OrderedDict
18 from .packages.six import iterkeys, itervalues, PY3
19
20
21 __all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
22
23
24 _Null = object()
25
26
27 class RecentlyUsedContainer(MutableMapping):
28     """
29     Provides a thread-safe dict-like container which maintains up to
30     ``maxsize`` keys while throwing away the least-recently-used keys beyond
31     ``maxsize``.
32
33     :param maxsize:
34         Maximum number of recent elements to retain.
35
36     :param dispose_func:
37         Every time an item is evicted from the container,
38         ``dispose_func(value)`` is called.  Callback which will get called
39     """
40
41     ContainerCls = OrderedDict
42
43     def __init__(self, maxsize=10, dispose_func=None):
44         self._maxsize = maxsize
45         self.dispose_func = dispose_func
46
47         self._container = self.ContainerCls()
48         self.lock = RLock()
49
50     def __getitem__(self, key):
51         # Re-insert the item, moving it to the end of the eviction line.
52         with self.lock:
53             item = self._container.pop(key)
54             self._container[key] = item
55             return item
56
57     def __setitem__(self, key, value):
58         evicted_value = _Null
59         with self.lock:
60             # Possibly evict the existing value of 'key'
61             evicted_value = self._container.get(key, _Null)
62             self._container[key] = value
63
64             # If we didn't evict an existing value, we might have to evict the
65             # least recently used item from the beginning of the container.
66             if len(self._container) > self._maxsize:
67                 _key, evicted_value = self._container.popitem(last=False)
68
69         if self.dispose_func and evicted_value is not _Null:
70             self.dispose_func(evicted_value)
71
72     def __delitem__(self, key):
73         with self.lock:
74             value = self._container.pop(key)
75
76         if self.dispose_func:
77             self.dispose_func(value)
78
79     def __len__(self):
80         with self.lock:
81             return len(self._container)
82
83     def __iter__(self):
84         raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.')
85
86     def clear(self):
87         with self.lock:
88             # Copy pointers to all values, then wipe the mapping
89             values = list(itervalues(self._container))
90             self._container.clear()
91
92         if self.dispose_func:
93             for value in values:
94                 self.dispose_func(value)
95
96     def keys(self):
97         with self.lock:
98             return list(iterkeys(self._container))
99
100
101 class HTTPHeaderDict(MutableMapping):
102     """
103     :param headers:
104         An iterable of field-value pairs. Must not contain multiple field names
105         when compared case-insensitively.
106
107     :param kwargs:
108         Additional field-value pairs to pass in to ``dict.update``.
109
110     A ``dict`` like container for storing HTTP Headers.
111
112     Field names are stored and compared case-insensitively in compliance with
113     RFC 7230. Iteration provides the first case-sensitive key seen for each
114     case-insensitive pair.
115
116     Using ``__setitem__`` syntax overwrites fields that compare equal
117     case-insensitively in order to maintain ``dict``'s api. For fields that
118     compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
119     in a loop.
120
121     If multiple fields that are equal case-insensitively are passed to the
122     constructor or ``.update``, the behavior is undefined and some will be
123     lost.
124
125     >>> headers = HTTPHeaderDict()
126     >>> headers.add('Set-Cookie', 'foo=bar')
127     >>> headers.add('set-cookie', 'baz=quxx')
128     >>> headers['content-length'] = '7'
129     >>> headers['SET-cookie']
130     'foo=bar, baz=quxx'
131     >>> headers['Content-Length']
132     '7'
133     """
134
135     def __init__(self, headers=None, **kwargs):
136         super(HTTPHeaderDict, self).__init__()
137         self._container = OrderedDict()
138         if headers is not None:
139             if isinstance(headers, HTTPHeaderDict):
140                 self._copy_from(headers)
141             else:
142                 self.extend(headers)
143         if kwargs:
144             self.extend(kwargs)
145
146     def __setitem__(self, key, val):
147         self._container[key.lower()] = (key, val)
148         return self._container[key.lower()]
149
150     def __getitem__(self, key):
151         val = self._container[key.lower()]
152         return ', '.join(val[1:])
153
154     def __delitem__(self, key):
155         del self._container[key.lower()]
156
157     def __contains__(self, key):
158         return key.lower() in self._container
159
160     def __eq__(self, other):
161         if not isinstance(other, Mapping) and not hasattr(other, 'keys'):
162             return False
163         if not isinstance(other, type(self)):
164             other = type(self)(other)
165         return (dict((k.lower(), v) for k, v in self.itermerged()) ==
166                 dict((k.lower(), v) for k, v in other.itermerged()))
167
168     def __ne__(self, other):
169         return not self.__eq__(other)
170
171     if not PY3:  # Python 2
172         iterkeys = MutableMapping.iterkeys
173         itervalues = MutableMapping.itervalues
174
175     __marker = object()
176
177     def __len__(self):
178         return len(self._container)
179
180     def __iter__(self):
181         # Only provide the originally cased names
182         for vals in self._container.values():
183             yield vals[0]
184
185     def pop(self, key, default=__marker):
186         '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
187           If key is not found, d is returned if given, otherwise KeyError is raised.
188         '''
189         # Using the MutableMapping function directly fails due to the private marker.
190         # Using ordinary dict.pop would expose the internal structures.
191         # So let's reinvent the wheel.
192         try:
193             value = self[key]
194         except KeyError:
195             if default is self.__marker:
196                 raise
197             return default
198         else:
199             del self[key]
200             return value
201
202     def discard(self, key):
203         try:
204             del self[key]
205         except KeyError:
206             pass
207
208     def add(self, key, val):
209         """Adds a (name, value) pair, doesn't overwrite the value if it already
210         exists.
211
212         >>> headers = HTTPHeaderDict(foo='bar')
213         >>> headers.add('Foo', 'baz')
214         >>> headers['foo']
215         'bar, baz'
216         """
217         key_lower = key.lower()
218         new_vals = key, val
219         # Keep the common case aka no item present as fast as possible
220         vals = self._container.setdefault(key_lower, new_vals)
221         if new_vals is not vals:
222             # new_vals was not inserted, as there was a previous one
223             if isinstance(vals, list):
224                 # If already several items got inserted, we have a list
225                 vals.append(val)
226             else:
227                 # vals should be a tuple then, i.e. only one item so far
228                 # Need to convert the tuple to list for further extension
229                 self._container[key_lower] = [vals[0], vals[1], val]
230
231     def extend(self, *args, **kwargs):
232         """Generic import function for any type of header-like object.
233         Adapted version of MutableMapping.update in order to insert items
234         with self.add instead of self.__setitem__
235         """
236         if len(args) > 1:
237             raise TypeError("extend() takes at most 1 positional "
238                             "arguments ({0} given)".format(len(args)))
239         other = args[0] if len(args) >= 1 else ()
240
241         if isinstance(other, HTTPHeaderDict):
242             for key, val in other.iteritems():
243                 self.add(key, val)
244         elif isinstance(other, Mapping):
245             for key in other:
246                 self.add(key, other[key])
247         elif hasattr(other, "keys"):
248             for key in other.keys():
249                 self.add(key, other[key])
250         else:
251             for key, value in other:
252                 self.add(key, value)
253
254         for key, value in kwargs.items():
255             self.add(key, value)
256
257     def getlist(self, key):
258         """Returns a list of all the values for the named field. Returns an
259         empty list if the key doesn't exist."""
260         try:
261             vals = self._container[key.lower()]
262         except KeyError:
263             return []
264         else:
265             if isinstance(vals, tuple):
266                 return [vals[1]]
267             else:
268                 return vals[1:]
269
270     # Backwards compatibility for httplib
271     getheaders = getlist
272     getallmatchingheaders = getlist
273     iget = getlist
274
275     def __repr__(self):
276         return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
277
278     def _copy_from(self, other):
279         for key in other:
280             val = other.getlist(key)
281             if isinstance(val, list):
282                 # Don't need to convert tuples
283                 val = list(val)
284             self._container[key.lower()] = [key] + val
285
286     def copy(self):
287         clone = type(self)()
288         clone._copy_from(self)
289         return clone
290
291     def iteritems(self):
292         """Iterate over all header lines, including duplicate ones."""
293         for key in self:
294             vals = self._container[key.lower()]
295             for val in vals[1:]:
296                 yield vals[0], val
297
298     def itermerged(self):
299         """Iterate over all headers, merging duplicate ones together."""
300         for key in self:
301             val = self._container[key.lower()]
302             yield val[0], ', '.join(val[1:])
303
304     def items(self):
305         return list(self.iteritems())
306
307     @classmethod
308     def from_httplib(cls, message):  # Python 2
309         """Read headers from a Python 2 httplib message object."""
310         # python2.7 does not expose a proper API for exporting multiheaders
311         # efficiently. This function re-reads raw lines from the message
312         # object and extracts the multiheaders properly.
313         headers = []
314
315         for line in message.headers:
316             if line.startswith((' ', '\t')):
317                 key, value = headers[-1]
318                 headers[-1] = (key, value + '\r\n' + line.rstrip())
319                 continue
320
321             key, value = line.split(':', 1)
322             headers.append((key, value.strip()))
323
324         return cls(headers)