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]