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