/circuits/web/headers.py

https://bitbucket.org/prologic/circuits/ · Python · 254 lines · 159 code · 18 blank · 77 comment · 17 complexity · b02731d7d99e7358eafc2649d9bc9cbd MD5 · raw file

  1. # Module: headers
  2. # Date: 1st February 2009 November 2008
  3. # Author: James Mills, prologic at shortcircuit dot net dot au
  4. """Headers Support
  5. This module implements support for parsing and handling headers.
  6. """
  7. import re
  8. from circuits.six import iteritems, u, b
  9. # Regular expression that matches `special' characters in parameters, the
  10. # existance of which force quoting of the parameter value.
  11. tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
  12. q_separator = re.compile(r'; *q *=')
  13. def _formatparam(param, value=None, quote=1):
  14. """Convenience function to format and return a key=value pair.
  15. This will quote the value if needed or if quote is true.
  16. """
  17. if value is not None and len(value) > 0:
  18. if quote or tspecials.search(value):
  19. value = value.replace('\\', '\\\\').replace('"', r'\"')
  20. return '%s="%s"' % (param, value)
  21. else:
  22. return '%s=%s' % (param, value)
  23. else:
  24. return param
  25. def header_elements(fieldname, fieldvalue):
  26. """Return a sorted HeaderElement list.
  27. Returns a sorted HeaderElement list
  28. from a comma-separated header string.
  29. """
  30. if not fieldvalue:
  31. return []
  32. result = []
  33. for element in fieldvalue.split(","):
  34. if fieldname.startswith("Accept") or fieldname == 'TE':
  35. hv = AcceptElement.from_str(element)
  36. else:
  37. hv = HeaderElement.from_str(element)
  38. result.append(hv)
  39. return list(reversed(sorted(result)))
  40. class HeaderElement(object):
  41. """An element (with parameters) from an HTTP header's element list."""
  42. def __init__(self, value, params=None):
  43. self.value = value
  44. if params is None:
  45. params = {}
  46. self.params = params
  47. def __eq__(self, other):
  48. return self.value == other.value
  49. def __lt__(self, other):
  50. return self.value < other.value
  51. def __str__(self):
  52. p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
  53. return "%s%s" % (self.value, "".join(p))
  54. def __bytes__(self):
  55. return b(self.__str__())
  56. def __unicode__(self):
  57. return u(self.__str__())
  58. def parse(elementstr):
  59. """Transform 'token;key=val' to ('token', {'key': 'val'})."""
  60. # Split the element into a value and parameters. The 'value' may
  61. # be of the form, "token=token", but we don't split that here.
  62. atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
  63. if not atoms:
  64. initial_value = ''
  65. else:
  66. initial_value = atoms.pop(0).strip()
  67. params = {}
  68. for atom in atoms:
  69. atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
  70. key = atom.pop(0)
  71. if atom:
  72. val = atom[0]
  73. else:
  74. val = ""
  75. params[key] = val
  76. return initial_value, params
  77. parse = staticmethod(parse)
  78. @classmethod
  79. def from_str(cls, elementstr):
  80. """Construct an instance from a string of the form 'token;key=val'."""
  81. ival, params = cls.parse(elementstr)
  82. return cls(ival, params)
  83. class AcceptElement(HeaderElement):
  84. """An element (with parameters) from an Accept* header's element list.
  85. AcceptElement objects are comparable; the more-preferred object will be
  86. "less than" the less-preferred object. They are also therefore sortable;
  87. if you sort a list of AcceptElement objects, they will be listed in
  88. priority order; the most preferred value will be first. Yes, it should
  89. have been the other way around, but it's too late to fix now.
  90. """
  91. @classmethod
  92. def from_str(cls, elementstr):
  93. qvalue = None
  94. # The first "q" parameter (if any) separates the initial
  95. # media-range parameter(s) (if any) from the accept-params.
  96. atoms = q_separator.split(elementstr, 1)
  97. media_range = atoms.pop(0).strip()
  98. if atoms:
  99. # The qvalue for an Accept header can have extensions. The other
  100. # headers cannot, but it's easier to parse them as if they did.
  101. qvalue = HeaderElement.from_str(atoms[0].strip())
  102. media_type, params = cls.parse(media_range)
  103. if qvalue is not None:
  104. params["q"] = qvalue
  105. return cls(media_type, params)
  106. def qvalue(self):
  107. val = self.params.get("q", "1")
  108. if isinstance(val, HeaderElement):
  109. val = val.value
  110. return float(val)
  111. qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
  112. def __eq__(self, other):
  113. return self.qvalue == other.qvalue
  114. def __lt__(self, other):
  115. if self.qvalue == other.qvalue:
  116. return str(self) < str(other)
  117. else:
  118. return self.qvalue < other.qvalue
  119. class CaseInsensitiveDict(dict):
  120. """A case-insensitive dict subclass.
  121. Each key is changed on entry to str(key).title().
  122. """
  123. def __init__(self, *args, **kwargs):
  124. d = dict(*args, **kwargs)
  125. for key, value in iteritems(d):
  126. dict.__setitem__(self, str(key).title(), value)
  127. dict.__init__(self)
  128. def __getitem__(self, key):
  129. return dict.__getitem__(self, str(key).title())
  130. def __setitem__(self, key, value):
  131. dict.__setitem__(self, str(key).title(), value)
  132. def __delitem__(self, key):
  133. dict.__delitem__(self, str(key).title())
  134. def __contains__(self, key):
  135. return dict.__contains__(self, str(key).title())
  136. def get(self, key, default=None):
  137. return dict.get(self, str(key).title(), default)
  138. def update(self, E):
  139. for k in E.keys():
  140. self[str(k).title()] = E[k]
  141. @classmethod
  142. def fromkeys(cls, seq, value=None):
  143. newdict = cls()
  144. for k in seq:
  145. newdict[k] = value
  146. return newdict
  147. def setdefault(self, key, x=None):
  148. key = str(key).title()
  149. try:
  150. return dict.__getitem__(self, key)
  151. except KeyError:
  152. self[key] = x
  153. return x
  154. def pop(self, key, default=None):
  155. return dict.pop(self, str(key).title(), default)
  156. class Headers(CaseInsensitiveDict):
  157. def elements(self, key):
  158. """Return a sorted list of HeaderElements for the given header."""
  159. return header_elements(key, self.get(key))
  160. def get_all(self, name):
  161. """Return a list of all the values for the named field."""
  162. return [val.strip() for val in self.get(name, '').split(',')]
  163. def __repr__(self):
  164. return "Headers(%s)" % repr(list(self.items()))
  165. def __str__(self):
  166. headers = ["%s: %s\r\n" % (k, v) for k, v in self.items()]
  167. return "".join(headers) + '\r\n'
  168. def __bytes__(self):
  169. return str(self).encode("latin1")
  170. def append(self, key, value):
  171. if not key in self:
  172. self[key] = value
  173. else:
  174. self[key] = ", ".join([self[key], value])
  175. def add_header(self, _name, _value, **_params):
  176. """Extended header setting.
  177. _name is the header field to add. keyword arguments can be used to set
  178. additional parameters for the header field, with underscores converted
  179. to dashes. Normally the parameter will be added as key="value" unless
  180. value is None, in which case only the key will be added.
  181. Example:
  182. h.add_header('content-disposition', 'attachment', filename='bud.gif')
  183. Note that unlike the corresponding 'email.Message' method, this does
  184. *not* handle '(charset, language, value)' tuples: all values must be
  185. strings or None.
  186. """
  187. parts = []
  188. if _value is not None:
  189. parts.append(_value)
  190. for k, v in list(_params.items()):
  191. k = k.replace('_', '-')
  192. if v is None:
  193. parts.append(k)
  194. else:
  195. parts.append(_formatparam(k, v))
  196. self.append(_name, "; ".join(parts))