138 lines
3.7 KiB
Python
138 lines
3.7 KiB
Python
"""Functions for generating and parsing HTTP Accept: headers for
|
|
supporting server-directed content negotiation.
|
|
"""
|
|
|
|
|
|
def generateAcceptHeader(*elements):
|
|
"""Generate an accept header value
|
|
|
|
[str or (str, float)] -> str
|
|
"""
|
|
parts = []
|
|
for element in elements:
|
|
if type(element) is str:
|
|
qs = "1.0"
|
|
mtype = element
|
|
else:
|
|
mtype, q = element
|
|
q = float(q)
|
|
if q > 1 or q <= 0:
|
|
raise ValueError('Invalid preference factor: %r' % q)
|
|
|
|
qs = '%0.1f' % (q, )
|
|
|
|
parts.append((qs, mtype))
|
|
|
|
parts.sort()
|
|
chunks = []
|
|
for q, mtype in parts:
|
|
if q == '1.0':
|
|
chunks.append(mtype)
|
|
else:
|
|
chunks.append('%s; q=%s' % (mtype, q))
|
|
|
|
return ', '.join(chunks)
|
|
|
|
|
|
def parseAcceptHeader(value):
|
|
"""Parse an accept header, ignoring any accept-extensions
|
|
|
|
returns a list of tuples containing main MIME type, MIME subtype,
|
|
and quality markdown.
|
|
|
|
str -> [(str, str, float)]
|
|
"""
|
|
chunks = [chunk.strip() for chunk in value.split(',')]
|
|
accept = []
|
|
for chunk in chunks:
|
|
parts = [s.strip() for s in chunk.split(';')]
|
|
|
|
mtype = parts.pop(0)
|
|
if '/' not in mtype:
|
|
# This is not a MIME type, so ignore the bad data
|
|
continue
|
|
|
|
main, sub = mtype.split('/', 1)
|
|
|
|
for ext in parts:
|
|
if '=' in ext:
|
|
k, v = ext.split('=', 1)
|
|
if k == 'q':
|
|
try:
|
|
q = float(v)
|
|
break
|
|
except ValueError:
|
|
# Ignore poorly formed q-values
|
|
pass
|
|
else:
|
|
q = 1.0
|
|
|
|
accept.append((q, main, sub))
|
|
|
|
accept.sort()
|
|
accept.reverse()
|
|
return [(main, sub, q) for (q, main, sub) in accept]
|
|
|
|
|
|
def matchTypes(accept_types, have_types):
|
|
"""Given the result of parsing an Accept: header, and the
|
|
available MIME types, return the acceptable types with their
|
|
quality markdowns.
|
|
|
|
For example:
|
|
|
|
>>> acceptable = parseAcceptHeader('text/html, text/plain; q=0.5')
|
|
>>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg'])
|
|
[('text/html', 1.0), ('text/plain', 0.5)]
|
|
|
|
|
|
Type signature: ([(str, str, float)], [str]) -> [(str, float)]
|
|
"""
|
|
if not accept_types:
|
|
# Accept all of them
|
|
default = 1
|
|
else:
|
|
default = 0
|
|
|
|
match_main = {}
|
|
match_sub = {}
|
|
for (main, sub, q) in accept_types:
|
|
if main == '*':
|
|
default = max(default, q)
|
|
continue
|
|
elif sub == '*':
|
|
match_main[main] = max(match_main.get(main, 0), q)
|
|
else:
|
|
match_sub[(main, sub)] = max(match_sub.get((main, sub), 0), q)
|
|
|
|
accepted_list = []
|
|
order_maintainer = 0
|
|
for mtype in have_types:
|
|
main, sub = mtype.split('/')
|
|
if (main, sub) in match_sub:
|
|
q = match_sub[(main, sub)]
|
|
else:
|
|
q = match_main.get(main, default)
|
|
|
|
if q:
|
|
accepted_list.append((1 - q, order_maintainer, q, mtype))
|
|
order_maintainer += 1
|
|
|
|
accepted_list.sort()
|
|
return [(mtype, q) for (_, _, q, mtype) in accepted_list]
|
|
|
|
|
|
def getAcceptable(accept_header, have_types):
|
|
"""Parse the accept header and return a list of available types in
|
|
preferred order. If a type is unacceptable, it will not be in the
|
|
resulting list.
|
|
|
|
This is a convenience wrapper around matchTypes and
|
|
parseAcceptHeader.
|
|
|
|
(str, [str]) -> [str]
|
|
"""
|
|
accepted = parseAcceptHeader(accept_header)
|
|
preferred = matchTypes(accepted, have_types)
|
|
return [mtype for (mtype, _) in preferred]
|