/circuits/web/headers.py
https://bitbucket.org/prologic/circuits/ · Python · 254 lines · 159 code · 18 blank · 77 comment · 17 complexity · b02731d7d99e7358eafc2649d9bc9cbd MD5 · raw file
- # Module: headers
- # Date: 1st February 2009 November 2008
- # Author: James Mills, prologic at shortcircuit dot net dot au
- """Headers Support
- This module implements support for parsing and handling headers.
- """
- import re
- from circuits.six import iteritems, u, b
- # Regular expression that matches `special' characters in parameters, the
- # existance of which force quoting of the parameter value.
- tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
- q_separator = re.compile(r'; *q *=')
- def _formatparam(param, value=None, quote=1):
- """Convenience function to format and return a key=value pair.
- This will quote the value if needed or if quote is true.
- """
- if value is not None and len(value) > 0:
- if quote or tspecials.search(value):
- value = value.replace('\\', '\\\\').replace('"', r'\"')
- return '%s="%s"' % (param, value)
- else:
- return '%s=%s' % (param, value)
- else:
- return param
- def header_elements(fieldname, fieldvalue):
- """Return a sorted HeaderElement list.
- Returns a sorted HeaderElement list
- from a comma-separated header string.
- """
- if not fieldvalue:
- return []
- result = []
- for element in fieldvalue.split(","):
- if fieldname.startswith("Accept") or fieldname == 'TE':
- hv = AcceptElement.from_str(element)
- else:
- hv = HeaderElement.from_str(element)
- result.append(hv)
- return list(reversed(sorted(result)))
- class HeaderElement(object):
- """An element (with parameters) from an HTTP header's element list."""
- def __init__(self, value, params=None):
- self.value = value
- if params is None:
- params = {}
- self.params = params
- def __eq__(self, other):
- return self.value == other.value
- def __lt__(self, other):
- return self.value < other.value
- def __str__(self):
- p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
- return "%s%s" % (self.value, "".join(p))
- def __bytes__(self):
- return b(self.__str__())
- def __unicode__(self):
- return u(self.__str__())
- def parse(elementstr):
- """Transform 'token;key=val' to ('token', {'key': 'val'})."""
- # Split the element into a value and parameters. The 'value' may
- # be of the form, "token=token", but we don't split that here.
- atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
- if not atoms:
- initial_value = ''
- else:
- initial_value = atoms.pop(0).strip()
- params = {}
- for atom in atoms:
- atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
- key = atom.pop(0)
- if atom:
- val = atom[0]
- else:
- val = ""
- params[key] = val
- return initial_value, params
- parse = staticmethod(parse)
- @classmethod
- def from_str(cls, elementstr):
- """Construct an instance from a string of the form 'token;key=val'."""
- ival, params = cls.parse(elementstr)
- return cls(ival, params)
- class AcceptElement(HeaderElement):
- """An element (with parameters) from an Accept* header's element list.
- AcceptElement objects are comparable; the more-preferred object will be
- "less than" the less-preferred object. They are also therefore sortable;
- if you sort a list of AcceptElement objects, they will be listed in
- priority order; the most preferred value will be first. Yes, it should
- have been the other way around, but it's too late to fix now.
- """
- @classmethod
- def from_str(cls, elementstr):
- qvalue = None
- # The first "q" parameter (if any) separates the initial
- # media-range parameter(s) (if any) from the accept-params.
- atoms = q_separator.split(elementstr, 1)
- media_range = atoms.pop(0).strip()
- if atoms:
- # The qvalue for an Accept header can have extensions. The other
- # headers cannot, but it's easier to parse them as if they did.
- qvalue = HeaderElement.from_str(atoms[0].strip())
- media_type, params = cls.parse(media_range)
- if qvalue is not None:
- params["q"] = qvalue
- return cls(media_type, params)
- def qvalue(self):
- val = self.params.get("q", "1")
- if isinstance(val, HeaderElement):
- val = val.value
- return float(val)
- qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
- def __eq__(self, other):
- return self.qvalue == other.qvalue
- def __lt__(self, other):
- if self.qvalue == other.qvalue:
- return str(self) < str(other)
- else:
- return self.qvalue < other.qvalue
- class CaseInsensitiveDict(dict):
- """A case-insensitive dict subclass.
- Each key is changed on entry to str(key).title().
- """
- def __init__(self, *args, **kwargs):
- d = dict(*args, **kwargs)
- for key, value in iteritems(d):
- dict.__setitem__(self, str(key).title(), value)
- dict.__init__(self)
- def __getitem__(self, key):
- return dict.__getitem__(self, str(key).title())
- def __setitem__(self, key, value):
- dict.__setitem__(self, str(key).title(), value)
- def __delitem__(self, key):
- dict.__delitem__(self, str(key).title())
- def __contains__(self, key):
- return dict.__contains__(self, str(key).title())
- def get(self, key, default=None):
- return dict.get(self, str(key).title(), default)
- def update(self, E):
- for k in E.keys():
- self[str(k).title()] = E[k]
- @classmethod
- def fromkeys(cls, seq, value=None):
- newdict = cls()
- for k in seq:
- newdict[k] = value
- return newdict
- def setdefault(self, key, x=None):
- key = str(key).title()
- try:
- return dict.__getitem__(self, key)
- except KeyError:
- self[key] = x
- return x
- def pop(self, key, default=None):
- return dict.pop(self, str(key).title(), default)
- class Headers(CaseInsensitiveDict):
- def elements(self, key):
- """Return a sorted list of HeaderElements for the given header."""
- return header_elements(key, self.get(key))
- def get_all(self, name):
- """Return a list of all the values for the named field."""
- return [val.strip() for val in self.get(name, '').split(',')]
- def __repr__(self):
- return "Headers(%s)" % repr(list(self.items()))
- def __str__(self):
- headers = ["%s: %s\r\n" % (k, v) for k, v in self.items()]
- return "".join(headers) + '\r\n'
- def __bytes__(self):
- return str(self).encode("latin1")
- def append(self, key, value):
- if not key in self:
- self[key] = value
- else:
- self[key] = ", ".join([self[key], value])
- def add_header(self, _name, _value, **_params):
- """Extended header setting.
- _name is the header field to add. keyword arguments can be used to set
- additional parameters for the header field, with underscores converted
- to dashes. Normally the parameter will be added as key="value" unless
- value is None, in which case only the key will be added.
- Example:
- h.add_header('content-disposition', 'attachment', filename='bud.gif')
- Note that unlike the corresponding 'email.Message' method, this does
- *not* handle '(charset, language, value)' tuples: all values must be
- strings or None.
- """
- parts = []
- if _value is not None:
- parts.append(_value)
- for k, v in list(_params.items()):
- k = k.replace('_', '-')
- if v is None:
- parts.append(k)
- else:
- parts.append(_formatparam(k, v))
- self.append(_name, "; ".join(parts))