1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/ContentTypeSupport.py Sat Jan 19 16:08:29 2013 +0100
1.3 @@ -0,0 +1,297 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - ContentTypeSupport library
1.7 +
1.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
1.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 +"""
1.11 +
1.12 +import re
1.13 +
1.14 +# Content type parsing.
1.15 +
1.16 +encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?'
1.17 +encoding_regexp = re.compile(encoding_regexp_str)
1.18 +
1.19 +# Accept header parsing.
1.20 +
1.21 +accept_regexp_str = ur';\s*q='
1.22 +accept_regexp = re.compile(accept_regexp_str)
1.23 +
1.24 +# Content/media type and preferences support.
1.25 +
1.26 +class MediaRange:
1.27 +
1.28 + "A content/media type value which supports whole categories of data."
1.29 +
1.30 + def __init__(self, media_range, accept_parameters=None):
1.31 + self.media_range = media_range
1.32 + self.accept_parameters = accept_parameters or {}
1.33 +
1.34 + parts = media_range.split(";")
1.35 + self.media_type = parts[0]
1.36 + self.parameters = getMappingFromParameterStrings(parts[1:])
1.37 +
1.38 + # The media type is divided into category and subcategory.
1.39 +
1.40 + parts = self.media_type.split("/")
1.41 + self.category = parts[0]
1.42 + self.subcategory = "/".join(parts[1:])
1.43 +
1.44 + def get_parts(self):
1.45 +
1.46 + "Return the category, subcategory parts."
1.47 +
1.48 + return self.category, self.subcategory
1.49 +
1.50 + def get_specificity(self):
1.51 +
1.52 + """
1.53 + Return the specificity of the media type in terms of the scope of the
1.54 + category and subcategory, and also in terms of any qualifying
1.55 + parameters.
1.56 + """
1.57 +
1.58 + if "*" in self.get_parts():
1.59 + return -list(self.get_parts()).count("*")
1.60 + else:
1.61 + return len(self.parameters)
1.62 +
1.63 + def permits(self, other):
1.64 +
1.65 + """
1.66 + Return whether this media type permits the use of the 'other' media type
1.67 + if suggested as suitable content.
1.68 + """
1.69 +
1.70 + if not isinstance(other, MediaRange):
1.71 + other = MediaRange(other)
1.72 +
1.73 + category = categoryPermits(self.category, other.category)
1.74 + subcategory = categoryPermits(self.subcategory, other.subcategory)
1.75 +
1.76 + if category and subcategory:
1.77 + if "*" not in (category, subcategory):
1.78 + return not self.parameters or self.parameters == other.parameters
1.79 + else:
1.80 + return True
1.81 + else:
1.82 + return False
1.83 +
1.84 + def __eq__(self, other):
1.85 +
1.86 + """
1.87 + Return whether this media type is effectively the same as the 'other'
1.88 + media type.
1.89 + """
1.90 +
1.91 + if not isinstance(other, MediaRange):
1.92 + other = MediaRange(other)
1.93 +
1.94 + category = categoryMatches(self.category, other.category)
1.95 + subcategory = categoryMatches(self.subcategory, other.subcategory)
1.96 +
1.97 + if category and subcategory:
1.98 + if "*" not in (category, subcategory):
1.99 + return self.parameters == other.parameters or \
1.100 + not self.parameters or not other.parameters
1.101 + else:
1.102 + return True
1.103 + else:
1.104 + return False
1.105 +
1.106 + def __ne__(self, other):
1.107 + return not self.__eq__(other)
1.108 +
1.109 + def __hash__(self):
1.110 + return hash(self.media_range)
1.111 +
1.112 + def __repr__(self):
1.113 + return "MediaRange(%r)" % self.media_range
1.114 +
1.115 +def categoryMatches(this, that):
1.116 +
1.117 + """
1.118 + Return the basis of a match between 'this' and 'that' or False if the given
1.119 + categories do not match.
1.120 + """
1.121 +
1.122 + return (this == "*" or this == that) and this or \
1.123 + that == "*" and that or False
1.124 +
1.125 +def categoryPermits(this, that):
1.126 +
1.127 + """
1.128 + Return whether 'this' category permits 'that' category. Where 'this' is a
1.129 + wildcard ("*"), 'that' should always match. A value of False is returned if
1.130 + the categories do not otherwise match.
1.131 + """
1.132 +
1.133 + return (this == "*" or this == that) and this or False
1.134 +
1.135 +def getMappingFromParameterStrings(l):
1.136 +
1.137 + """
1.138 + Return a mapping representing the list of "name=value" strings given by 'l'.
1.139 + """
1.140 +
1.141 + parameters = {}
1.142 +
1.143 + for parameter in l:
1.144 + parts = parameter.split("=")
1.145 + name = parts[0].strip()
1.146 + value = "=".join(parts[1:]).strip()
1.147 + parameters[name] = value
1.148 +
1.149 + return parameters
1.150 +
1.151 +def getContentPreferences(accept):
1.152 +
1.153 + """
1.154 + Return a mapping from media types to parameters for content/media types
1.155 + extracted from the given 'accept' header value. The mapping is returned in
1.156 + the form of a list of (media type, parameters) tuples.
1.157 +
1.158 + See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
1.159 + """
1.160 +
1.161 + preferences = []
1.162 +
1.163 + for field in accept.split(","):
1.164 +
1.165 + # The media type with parameters (defined by the "media-range") is
1.166 + # separated from any other parameters (defined as "accept-extension"
1.167 + # parameters) by a quality parameter.
1.168 +
1.169 + fparts = accept_regexp.split(field)
1.170 +
1.171 + # The first part is always the media type.
1.172 +
1.173 + media_type = fparts[0].strip()
1.174 +
1.175 + # Any other parts can be interpreted as extension parameters.
1.176 +
1.177 + if len(fparts) > 1:
1.178 + fparts = ("q=" + ";q=".join(fparts[1:])).split(";")
1.179 + else:
1.180 + fparts = []
1.181 +
1.182 + # Each field in the preferences can incorporate parameters separated by
1.183 + # semicolon characters.
1.184 +
1.185 + parameters = getMappingFromParameterStrings(fparts)
1.186 + media_range = MediaRange(media_type, parameters)
1.187 + preferences.append(media_range)
1.188 +
1.189 + return ContentPreferences(preferences)
1.190 +
1.191 +class ContentPreferences:
1.192 +
1.193 + "A wrapper around content preference information."
1.194 +
1.195 + def __init__(self, preferences):
1.196 + self.preferences = preferences
1.197 +
1.198 + def __iter__(self):
1.199 + return iter(self.preferences)
1.200 +
1.201 + def get_ordered(self, by_quality=0):
1.202 +
1.203 + """
1.204 + Return a list of content/media types in descending order of preference.
1.205 + If 'by_quality' is set to a true value, the "q" value will be used as
1.206 + the primary measure of preference; otherwise, only the specificity will
1.207 + be considered.
1.208 + """
1.209 +
1.210 + ordered = {}
1.211 +
1.212 + for media_range in self.preferences:
1.213 + specificity = media_range.get_specificity()
1.214 +
1.215 + if by_quality:
1.216 + q = float(media_range.accept_parameters.get("q", "1"))
1.217 + key = q, specificity
1.218 + else:
1.219 + key = specificity
1.220 +
1.221 + if not ordered.has_key(key):
1.222 + ordered[key] = []
1.223 +
1.224 + ordered[key].append(media_range)
1.225 +
1.226 + # Return the preferences in descending order of quality and specificity.
1.227 +
1.228 + keys = ordered.keys()
1.229 + keys.sort(reverse=True)
1.230 + return [ordered[key] for key in keys]
1.231 +
1.232 + def get_acceptable_types(self, available):
1.233 +
1.234 + """
1.235 + Return content/media types from those in the 'available' list supported
1.236 + by the known preferences grouped by preference level in descending order
1.237 + of preference.
1.238 + """
1.239 +
1.240 + matches = {}
1.241 + available = set(available[:])
1.242 +
1.243 + for level in self.get_ordered():
1.244 + for media_range in level:
1.245 +
1.246 + # Attempt to match available types.
1.247 +
1.248 + found = set()
1.249 + for available_type in available:
1.250 + if media_range.permits(available_type):
1.251 + q = float(media_range.accept_parameters.get("q", "1"))
1.252 + if not matches.has_key(q):
1.253 + matches[q] = []
1.254 + matches[q].append(available_type)
1.255 + found.add(available_type)
1.256 +
1.257 + # Stop looking for matches for matched available types.
1.258 +
1.259 + if found:
1.260 + available.difference_update(found)
1.261 +
1.262 + # Sort the matches in descending order of quality.
1.263 +
1.264 + all_q = matches.keys()
1.265 +
1.266 + if all_q:
1.267 + all_q.sort(reverse=True)
1.268 + return [matches[q] for q in all_q]
1.269 + else:
1.270 + return []
1.271 +
1.272 + def get_preferred_types(self, available):
1.273 +
1.274 + """
1.275 + Return the preferred content/media types from those in the 'available'
1.276 + list, given the known preferences.
1.277 + """
1.278 +
1.279 + preferred = self.get_acceptable_types(available)
1.280 + if preferred:
1.281 + return preferred[0]
1.282 + else:
1.283 + return []
1.284 +
1.285 +# Content type parsing.
1.286 +
1.287 +def getContentTypeAndEncoding(content_type):
1.288 +
1.289 + """
1.290 + Return a tuple with the content/media type and encoding, extracted from the
1.291 + given 'content_type' header value.
1.292 + """
1.293 +
1.294 + m = encoding_regexp.search(content_type)
1.295 + if m:
1.296 + return m.group("content_type"), m.group("encoding")
1.297 + else:
1.298 + return None, None
1.299 +
1.300 +# vim: tabstop=4 expandtab shiftwidth=4