Skip to content

Instantly share code, notes, and snippets.

@seocam
Forked from Lukasa/headermap.py
Last active August 29, 2015 14:16
Show Gist options
  • Save seocam/ee5b3358f7f1aab53831 to your computer and use it in GitHub Desktop.
Save seocam/ee5b3358f7f1aab53831 to your computer and use it in GitHub Desktop.
import collections
class HTTPHeaderMap(collections.MutableMapping):
"""
A structure that contains HTTP headers.
HTTP headers are a curious beast. At the surface level they look roughly
like a name-value set, but in practice they have many variations that
make them tricky:
- duplicate keys are allowed
- keys are compared case-insensitively
- duplicate keys are isomorphic to comma-separated values, *except when
they aren't*!
- they logically contain a form of ordering
This data structure is an attempt to preserve all of that information
while being as user-friendly as possible.
"""
def __init__(self, *args, **kwargs):
# The meat of the structure. In practice, headers are an ordered list
# of tuples. This early version of the data structure simply uses this
# directly under the covers.
self._nvps = []
for arg in args:
self._nvps.extend(canonical_form(arg))
for header in kwargs.items():
self._nvps.extend(canonical_form(header))
def __getitem__(self, key):
"""
Unlike the dict __getitem__, this returns a list of items in the order
they were added.
"""
values = []
for k, v in self._nvps:
if _keys_equal(k, key):
values.append(v)
if not values:
raise KeyError()
return values
def __setitem__(self, key, value):
"""
Unlike the dict __setitem__, this appends to the list of NVPs. It also
splits out headers that can be split on the comma.
"""
self._nvps.extend(canonical_form((key, sub_val)))
def __delitem__(self, key):
"""
Sadly, __delitem__ is kind of stupid here, but the best we can do is
delete all headers with a given key. To correctly achieve the 'KeyError
on missing key' logic from dictionaries, we need to do this slowly.
"""
indices = []
for (i, (k, v)) in enumerate(self._nvps):
if _keys_equal(k, key):
indices.append(i)
if not indices:
raise KeyError()
for i in indices[::-1]:
self._nvps.pop(i)
def __iter__(self):
"""
This mapping iterates like the list of tuples it is.
"""
for pair in self._nvps:
yield pair
def __len__(self):
"""
The length of this mapping is the number of individual headers.
"""
return len(self._nvps)
def __contains__(self, key):
"""
If any header is present with this key, returns True.
"""
return any(_keys_equal(key, k) for k, _ in self._nvps)
def keys(self):
"""
Returns an iterable of the header keys in the mapping. This explicitly
does not filter duplicates, ensuring that it's the same length as
len().
"""
for n, _ in self._nvps:
yield n
def items(self):
"""
This mapping iterates like the list of tuples it is.
"""
for item in self:
yield item
def values(self):
"""
This is an almost nonsensical query on a header dictionary, but we
satisfy it in the exact same way we satisfy 'keys'.
"""
for _, v in self._nvps:
yield v
def get(self, name, default=None):
"""
Unlike the dict get, this returns a list of items in the order
they were added.
"""
try:
return self[name]
except KeyError:
return default
def __eq__(self, other):
return self._nvps == other._nvps
def __ne__(self, other):
return self._nvps != other._nvps
def canonical_form(k, v):
"""
Returns an iterable of key-value-pairs corresponding to the header in
canonical form. This means that the header is split on commas unless for
any reason it's a super-special snowflake (I'm looking at you Set-Cookie).
"""
SPECIAL_SNOWFLAKES = set('set-cookie', 'set-cookie2')
k = k.lower()
if k in SPECIAL_SNOWFLAKES:
yield k, v
else:
for sub_val in v.split(','):
yield k, sub_val
def _keys_equal(x, y):
"""
Returns 'True' if the two keys are equal by the laws of HTTP headers.
"""
return x.lower() == y.lower():
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment