199 lines
6.1 KiB
Python
199 lines
6.1 KiB
Python
from collections import Mapping, MutableMapping
|
|
try:
|
|
from threading import RLock
|
|
except ImportError: # Platform-specific: No threads available
|
|
class RLock:
|
|
def __enter__(self):
|
|
pass
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
pass
|
|
|
|
|
|
try: # Python 2.7+
|
|
from collections import OrderedDict
|
|
except ImportError:
|
|
from .packages.ordered_dict import OrderedDict
|
|
from .packages.six import iterkeys, itervalues
|
|
|
|
|
|
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
|
|
|
|
|
|
_Null = object()
|
|
|
|
|
|
class RecentlyUsedContainer(MutableMapping):
|
|
"""
|
|
Provides a thread-safe dict-like container which maintains up to
|
|
``maxsize`` keys while throwing away the least-recently-used keys beyond
|
|
``maxsize``.
|
|
|
|
:param maxsize:
|
|
Maximum number of recent elements to retain.
|
|
|
|
:param dispose_func:
|
|
Every time an item is evicted from the container,
|
|
``dispose_func(value)`` is called. Callback which will get called
|
|
"""
|
|
|
|
ContainerCls = OrderedDict
|
|
|
|
def __init__(self, maxsize=10, dispose_func=None):
|
|
self._maxsize = maxsize
|
|
self.dispose_func = dispose_func
|
|
|
|
self._container = self.ContainerCls()
|
|
self.lock = RLock()
|
|
|
|
def __getitem__(self, key):
|
|
# Re-insert the item, moving it to the end of the eviction line.
|
|
with self.lock:
|
|
item = self._container.pop(key)
|
|
self._container[key] = item
|
|
return item
|
|
|
|
def __setitem__(self, key, value):
|
|
evicted_value = _Null
|
|
with self.lock:
|
|
# Possibly evict the existing value of 'key'
|
|
evicted_value = self._container.get(key, _Null)
|
|
self._container[key] = value
|
|
|
|
# If we didn't evict an existing value, we might have to evict the
|
|
# least recently used item from the beginning of the container.
|
|
if len(self._container) > self._maxsize:
|
|
_key, evicted_value = self._container.popitem(last=False)
|
|
|
|
if self.dispose_func and evicted_value is not _Null:
|
|
self.dispose_func(evicted_value)
|
|
|
|
def __delitem__(self, key):
|
|
with self.lock:
|
|
value = self._container.pop(key)
|
|
|
|
if self.dispose_func:
|
|
self.dispose_func(value)
|
|
|
|
def __len__(self):
|
|
with self.lock:
|
|
return len(self._container)
|
|
|
|
def __iter__(self):
|
|
raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.')
|
|
|
|
def clear(self):
|
|
with self.lock:
|
|
# Copy pointers to all values, then wipe the mapping
|
|
values = list(itervalues(self._container))
|
|
self._container.clear()
|
|
|
|
if self.dispose_func:
|
|
for value in values:
|
|
self.dispose_func(value)
|
|
|
|
def keys(self):
|
|
with self.lock:
|
|
return list(iterkeys(self._container))
|
|
|
|
|
|
class HTTPHeaderDict(MutableMapping):
|
|
"""
|
|
:param headers:
|
|
An iterable of field-value pairs. Must not contain multiple field names
|
|
when compared case-insensitively.
|
|
|
|
:param kwargs:
|
|
Additional field-value pairs to pass in to ``dict.update``.
|
|
|
|
A ``dict`` like container for storing HTTP Headers.
|
|
|
|
Field names are stored and compared case-insensitively in compliance with
|
|
RFC 7230. Iteration provides the first case-sensitive key seen for each
|
|
case-insensitive pair.
|
|
|
|
Using ``__setitem__`` syntax overwrites fields that compare equal
|
|
case-insensitively in order to maintain ``dict``'s api. For fields that
|
|
compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
|
|
in a loop.
|
|
|
|
If multiple fields that are equal case-insensitively are passed to the
|
|
constructor or ``.update``, the behavior is undefined and some will be
|
|
lost.
|
|
|
|
>>> headers = HTTPHeaderDict()
|
|
>>> headers.add('Set-Cookie', 'foo=bar')
|
|
>>> headers.add('set-cookie', 'baz=quxx')
|
|
>>> headers['content-length'] = '7'
|
|
>>> headers['SET-cookie']
|
|
'foo=bar, baz=quxx'
|
|
>>> headers['Content-Length']
|
|
'7'
|
|
|
|
If you want to access the raw headers with their original casing
|
|
for debugging purposes you can access the private ``._data`` attribute
|
|
which is a normal python ``dict`` that maps the case-insensitive key to a
|
|
list of tuples stored as (case-sensitive-original-name, value). Using the
|
|
structure from above as our example:
|
|
|
|
>>> headers._data
|
|
{'set-cookie': [('Set-Cookie', 'foo=bar'), ('set-cookie', 'baz=quxx')],
|
|
'content-length': [('content-length', '7')]}
|
|
"""
|
|
|
|
def __init__(self, headers=None, **kwargs):
|
|
self._data = {}
|
|
if headers is None:
|
|
headers = {}
|
|
self.update(headers, **kwargs)
|
|
|
|
def add(self, key, value):
|
|
"""Adds a (name, value) pair, doesn't overwrite the value if it already
|
|
exists.
|
|
|
|
>>> headers = HTTPHeaderDict(foo='bar')
|
|
>>> headers.add('Foo', 'baz')
|
|
>>> headers['foo']
|
|
'bar, baz'
|
|
"""
|
|
self._data.setdefault(key.lower(), []).append((key, value))
|
|
|
|
def getlist(self, key):
|
|
"""Returns a list of all the values for the named field. Returns an
|
|
empty list if the key doesn't exist."""
|
|
return self[key].split(', ') if key in self else []
|
|
|
|
def copy(self):
|
|
h = HTTPHeaderDict()
|
|
for key in self._data:
|
|
for rawkey, value in self._data[key]:
|
|
h.add(rawkey, value)
|
|
return h
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Mapping):
|
|
return False
|
|
other = HTTPHeaderDict(other)
|
|
return dict((k1, self[k1]) for k1 in self._data) == \
|
|
dict((k2, other[k2]) for k2 in other._data)
|
|
|
|
def __getitem__(self, key):
|
|
values = self._data[key.lower()]
|
|
return ', '.join(value[1] for value in values)
|
|
|
|
def __setitem__(self, key, value):
|
|
self._data[key.lower()] = [(key, value)]
|
|
|
|
def __delitem__(self, key):
|
|
del self._data[key.lower()]
|
|
|
|
def __len__(self):
|
|
return len(self._data)
|
|
|
|
def __iter__(self):
|
|
for headers in itervalues(self._data):
|
|
yield headers[0][0]
|
|
|
|
def __repr__(self):
|
|
return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
|