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