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):
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/tests/test_mediarange.py Mon Jan 23 23:23:20 2012 +0100
2.3 @@ -0,0 +1,42 @@
2.4 +#!/usr/bin/env python
2.5 +
2.6 +"""
2.7 +A test of content preferences using examples from the HTTP 1.1 specification.
2.8 +See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
2.9 +"""
2.10 +
2.11 +from MoinSupport import MediaRange
2.12 +
2.13 +mr1 = MediaRange("audio/*")
2.14 +mr2 = MediaRange("audio/basic")
2.15 +print mr1 == mr2, ":", mr1, "==", mr2
2.16 +print mr1.permits(mr2), ":", mr1, "permits", mr2
2.17 +
2.18 +mr1 = MediaRange("text/plain")
2.19 +mr2 = MediaRange("text/html")
2.20 +print mr1 != mr2, ":", mr1, "!=", mr2
2.21 +print not mr1.permits(mr2), ":", mr1, "does not permit", mr2
2.22 +
2.23 +mr1 = MediaRange("text/*")
2.24 +mr2 = MediaRange("text/html")
2.25 +mr3 = MediaRange("text/html;level=1")
2.26 +mr4 = MediaRange("*/*")
2.27 +mr5 = MediaRange("text/html;level=2")
2.28 +print mr1 == mr2, ":", mr1, "==", mr2
2.29 +print mr1.permits(mr2), ":", mr1, "permits", mr2
2.30 +print not mr2.permits(mr1), ":", mr2, "does not permit", mr1
2.31 +print mr1 == mr3, ":", mr1, "==", mr3
2.32 +print mr1.permits(mr3), ":", mr1, "permits", mr3
2.33 +print mr1 == mr4, ":", mr1, "==", mr4
2.34 +print mr1 == mr5, ":", mr1, "==", mr5
2.35 +print mr2 == mr3, ":", mr2, "==", mr3
2.36 +print mr2.permits(mr3), ":", mr2, "permits", mr3
2.37 +print mr2 == mr4, ":", mr2, "==", mr4
2.38 +print mr2 == mr5, ":", mr2, "==", mr5
2.39 +print mr3 == mr4, ":", mr3, "==", mr4
2.40 +print mr3 != mr5, ":", mr3, "!=", mr5
2.41 +print mr4 == mr5, ":", mr4, "==", mr5
2.42 +print mr4.permits(mr5), ":", mr4, "permits", mr5
2.43 +print not mr5.permits(mr4), ":", mr5, "does not permit", mr4
2.44 +
2.45 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/tests/test_preferences.py Mon Jan 23 23:23:20 2012 +0100
3.3 @@ -0,0 +1,73 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +A test of content preferences using examples from the HTTP 1.1 specification.
3.8 +See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
3.9 +"""
3.10 +
3.11 +from MoinSupport import getContentPreferences
3.12 +
3.13 +s0 = "audio/*; q=0.2, audio/basic"
3.14 +prefs = getContentPreferences(s0)
3.15 +oprefs = prefs.get_ordered(True)
3.16 +expected = [
3.17 + ["audio/basic"],
3.18 + ["audio/*"]
3.19 + ]
3.20 +print oprefs == expected, ":", oprefs, "==", expected
3.21 +
3.22 +s1 = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"
3.23 +prefs = getContentPreferences(s1)
3.24 +oprefs = prefs.get_ordered(True)
3.25 +expected = [
3.26 + ["text/html", "text/x-c"], # equal quality, specificity
3.27 + ["text/x-dvi"],
3.28 + ["text/plain"]
3.29 + ]
3.30 +print oprefs == expected, ":", oprefs, "==", expected
3.31 +
3.32 +s2 = "text/*, text/html, text/html;level=1, */*"
3.33 +prefs = getContentPreferences(s2)
3.34 +oprefs = prefs.get_ordered(True)
3.35 +expected = [
3.36 + ["text/html;level=1"], # specificity is 1
3.37 + ["text/html"], # specificity is 0
3.38 + ["text/*"], # specificity is -1
3.39 + ["*/*"] # specificity is -2
3.40 + ]
3.41 +print oprefs == expected, ":", oprefs, "==", expected
3.42 +
3.43 +s3 = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
3.44 +prefs = getContentPreferences(s3)
3.45 +oprefs = prefs.get_ordered(True)
3.46 +expected = [
3.47 + ["text/html;level=1"],
3.48 + ["text/html"], # specificity is 1
3.49 + ["*/*"], # specificity is -2
3.50 + ["text/html;level=2"],
3.51 + ["text/*"]
3.52 + ]
3.53 +print oprefs == expected, ":", oprefs, "==", expected
3.54 +
3.55 +available = [
3.56 + "text/html;level=1",
3.57 + "text/html"
3.58 + ]
3.59 +expected = ["text/html;level=1"]
3.60 +print prefs.get_preferred_type(available) == expected, ":", prefs.get_preferred_type(available), "==", expected
3.61 +
3.62 +available = [
3.63 + "text/plain",
3.64 + "image/jpeg"
3.65 + ]
3.66 +expected = ["image/jpeg"]
3.67 +print prefs.get_preferred_type(available) == expected, ":", prefs.get_preferred_type(available), "==", expected
3.68 +
3.69 +available = [
3.70 + "text/html;level=2",
3.71 + "text/html;level=3"
3.72 + ]
3.73 +expected = ["text/html;level=3"]
3.74 +print prefs.get_preferred_type(available) == expected, ":", prefs.get_preferred_type(available), "==", expected
3.75 +
3.76 +# vim: tabstop=4 expandtab shiftwidth=4