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