accept.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """Functions for generating and parsing HTTP Accept: headers for
  2. supporting server-directed content negotiation.
  3. """
  4. def generateAcceptHeader(*elements):
  5. """Generate an accept header value
  6. [str or (str, float)] -> str
  7. """
  8. parts = []
  9. for element in elements:
  10. if type(element) is str:
  11. qs = "1.0"
  12. mtype = element
  13. else:
  14. mtype, q = element
  15. q = float(q)
  16. if q > 1 or q <= 0:
  17. raise ValueError('Invalid preference factor: %r' % q)
  18. qs = '%0.1f' % (q,)
  19. parts.append((qs, mtype))
  20. parts.sort()
  21. chunks = []
  22. for q, mtype in parts:
  23. if q == '1.0':
  24. chunks.append(mtype)
  25. else:
  26. chunks.append('%s; q=%s' % (mtype, q))
  27. return ', '.join(chunks)
  28. def parseAcceptHeader(value):
  29. """Parse an accept header, ignoring any accept-extensions
  30. returns a list of tuples containing main MIME type, MIME subtype,
  31. and quality markdown.
  32. str -> [(str, str, float)]
  33. """
  34. chunks = [chunk.strip() for chunk in value.split(',')]
  35. accept = []
  36. for chunk in chunks:
  37. parts = [s.strip() for s in chunk.split(';')]
  38. mtype = parts.pop(0)
  39. if '/' not in mtype:
  40. # This is not a MIME type, so ignore the bad data
  41. continue
  42. main, sub = mtype.split('/', 1)
  43. for ext in parts:
  44. if '=' in ext:
  45. k, v = ext.split('=', 1)
  46. if k == 'q':
  47. try:
  48. q = float(v)
  49. break
  50. except ValueError:
  51. # Ignore poorly formed q-values
  52. pass
  53. else:
  54. q = 1.0
  55. accept.append((q, main, sub))
  56. accept.sort()
  57. accept.reverse()
  58. return [(main, sub, q) for (q, main, sub) in accept]
  59. def matchTypes(accept_types, have_types):
  60. """Given the result of parsing an Accept: header, and the
  61. available MIME types, return the acceptable types with their
  62. quality markdowns.
  63. For example:
  64. >>> acceptable = parseAcceptHeader('text/html, text/plain; q=0.5')
  65. >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg'])
  66. [('text/html', 1.0), ('text/plain', 0.5)]
  67. Type signature: ([(str, str, float)], [str]) -> [(str, float)]
  68. """
  69. if not accept_types:
  70. # Accept all of them
  71. default = 1
  72. else:
  73. default = 0
  74. match_main = {}
  75. match_sub = {}
  76. for (main, sub, q) in accept_types:
  77. if main == '*':
  78. default = max(default, q)
  79. continue
  80. elif sub == '*':
  81. match_main[main] = max(match_main.get(main, 0), q)
  82. else:
  83. match_sub[(main, sub)] = max(match_sub.get((main, sub), 0), q)
  84. accepted_list = []
  85. order_maintainer = 0
  86. for mtype in have_types:
  87. main, sub = mtype.split('/')
  88. if (main, sub) in match_sub:
  89. q = match_sub[(main, sub)]
  90. else:
  91. q = match_main.get(main, default)
  92. if q:
  93. accepted_list.append((1 - q, order_maintainer, q, mtype))
  94. order_maintainer += 1
  95. accepted_list.sort()
  96. return [(mtype, q) for (_, _, q, mtype) in accepted_list]
  97. def getAcceptable(accept_header, have_types):
  98. """Parse the accept header and return a list of available types in
  99. preferred order. If a type is unacceptable, it will not be in the
  100. resulting list.
  101. This is a convenience wrapper around matchTypes and
  102. parseAcceptHeader.
  103. (str, [str]) -> [str]
  104. """
  105. accepted = parseAcceptHeader(accept_header)
  106. preferred = matchTypes(accepted, have_types)
  107. return [mtype for (mtype, _) in preferred]