0001class Range(object):
0002
0003 """
0004 Represents the Range header.
0005
0006 This only represents ``bytes`` ranges, which are the only kind
0007 specified in HTTP. This can represent multiple sets of ranges,
0008 but no place else is this multi-range facility supported.
0009 """
0010
0011 def __init__(self, ranges):
0012 for begin, end in ranges:
0013 assert end is None or end >= 0, "Bad ranges: %r" % ranges
0014 self.ranges = ranges
0015
0016 def satisfiable(self, length):
0017 """
0018 Returns true if this range can be satisfied by the resource
0019 with the given byte length.
0020 """
0021 for begin, end in self.ranges:
0022 if end is not None and end >= length:
0023 return False
0024 return True
0025
0026 def range_for_length(self, length):
0027 """
0028 *If* there is only one range, and *if* it is satisfiable by
0029 the given length, then return a (begin, end) non-inclusive range
0030 of bytes to serve. Otherwise return None
0031
0032 If length is None (unknown length), then the resulting range
0033 may be (begin, None), meaning it should be served from that
0034 point. If it's a range with a fixed endpoint we won't know if
0035 it is satisfiable, so this will return None.
0036 """
0037 if len(self.ranges) != 1:
0038 return None
0039 begin, end = self.ranges[0]
0040 if length is None:
0041
0042 if end is None:
0043 return (begin, end)
0044 return None
0045 if end >= length:
0046
0047 return None
0048 return (begin, end)
0049
0050 def content_range(self, length):
0051 """
0052 Works like range_for_length; returns None or a ContentRange object
0053
0054 You can use it like::
0055
0056 response.content_range = req.range.content_range(response.content_length)
0057
0058 Though it's still up to you to actually serve that content range!
0059 """
0060 range = self.range_for_length(length)
0061 if range is None:
0062 return None
0063 return ContentRange(range[0], range[1], length)
0064
0065 def __str__(self):
0066 return self.serialize_bytes('bytes', self.python_ranges_to_bytes(self.ranges))
0067
0068 def __repr__(self):
0069 return '<%s ranges=%s>' % (
0070 self.__class__.__name__,
0071 ', '.join(map(repr, self.ranges)))
0072
0073
0074 def parse(cls, header):
0075 """
0076 Parse the header; may return None if header is invalid
0077 """
0078 bytes = cls.parse_bytes(header)
0079 if bytes is None:
0080 return None
0081 units, ranges = bytes
0082 if units.lower() != 'bytes':
0083 return None
0084 ranges = cls.bytes_to_python_ranges(ranges)
0085 if ranges is None:
0086 return None
0087 return cls(ranges)
0088 parse = classmethod(parse)
0089
0090
0091 def parse_bytes(header):
0092 """
0093 Parse a Range header into (bytes, list_of_ranges). Note that the
0094 ranges are *inclusive* (like in HTTP, not like in Python
0095 typically).
0096
0097 Will return None if the header is invalid
0098 """
0099 if not header:
0100 raise TypeError(
0101 "The header must not be empty")
0102 ranges = []
0103 last_end = 0
0104 try:
0105 (units, range) = header.split("=", 1)
0106 units = units.strip().lower()
0107 for item in range.split(","):
0108 if '-' not in item:
0109 raise ValueError()
0110 if item.startswith('-'):
0111
0112 if last_end < 0:
0113 raise ValueError('too many end ranges')
0114 begin = int(item)
0115 end = None
0116 last_end = -1
0117 else:
0118 (begin, end) = item.split("-", 1)
0119 begin = int(begin)
0120 if begin < last_end or last_end < 0:
0121 print begin, last_end
0122 raise ValueError('begin<last_end, or last_end<0')
0123 if not end.strip():
0124 end = None
0125 else:
0126 end = int(end)
0127 if end is not None and begin > end:
0128 raise ValueError('begin>end')
0129 last_end = end
0130 ranges.append((begin, end))
0131 except ValueError, e:
0132
0133
0134
0135 print e
0136 return None
0137 return (units, ranges)
0138 parse_bytes = staticmethod(parse_bytes)
0139
0140
0141 def serialize_bytes(units, ranges):
0142 """
0143 Takes the output of parse_bytes and turns it into a header
0144 """
0145 parts = []
0146 for begin, end in ranges:
0147 if end is None:
0148 if begin >= 0:
0149 parts.append('%s-' % begin)
0150 else:
0151 parts.append(str(begin))
0152 else:
0153 if begin < 0:
0154 raise ValueError(
0155 "(%r, %r) should have a non-negative first value" % (begin, end))
0156 if end < 0:
0157 raise ValueError(
0158 "(%r, %r) should have a non-negative second value" % (begin, end))
0159 parts.append('%s-%s' % (begin, end))
0160 return '%s=%s' % (units, ','.join(parts))
0161 serialize_bytes = staticmethod(serialize_bytes)
0162
0163
0164 def bytes_to_python_ranges(ranges, length=None):
0165 """
0166 Converts the list-of-ranges from parse_bytes() to a Python-style
0167 list of ranges (non-inclusive end points)
0168
0169 In the list of ranges, the last item can be None to indicate that
0170 it should go to the end of the file, and the first item can be
0171 negative to indicate that it should start from an offset from the
0172 end. If you give a length then this will not occur (negative
0173 numbers and offsets will be resolved).
0174
0175 If length is given, and any range is not value, then None is
0176 returned.
0177 """
0178 result = []
0179 for begin, end in ranges:
0180 if begin < 0:
0181 if length is None:
0182 result.append((begin, None))
0183 continue
0184 else:
0185 begin = length - begin
0186 end = length
0187 if begin is None:
0188 begin = 0
0189 if end is None and length is not None:
0190 end = length
0191 if length is not None and end is not None and end > length:
0192 return None
0193 if end is not None:
0194 end -= 1
0195 result.append((begin, end))
0196 return result
0197 bytes_to_python_ranges = staticmethod(bytes_to_python_ranges)
0198
0199
0200 def python_ranges_to_bytes(ranges):
0201 """
0202 Converts a Python-style list of ranges to what serialize_bytes
0203 expects.
0204
0205 This is the inverse of bytes_to_python_ranges
0206 """
0207 result = []
0208 for begin, end in ranges:
0209 if end is None:
0210 result.append((begin, None))
0211 else:
0212 result.append((begin, end+1))
0213 return result
0214 python_ranges_to_bytes = staticmethod(python_ranges_to_bytes)
0215
0216class ContentRange(object):
0217
0218 """
0219 Represents the Content-Range header
0220
0221 This header is ``start-stop/length``, where stop and length can be
0222 ``*`` (represented as None in the attributes).
0223 """
0224
0225 def __init__(self, start, stop, length):
0226 assert start >= 0, "Bad start: %r" % start
0227 assert stop is None or (stop >= 0 and stop >= start), (
0228 "Bad stop: %r" % stop)
0229 self.start = start
0230 self.stop = stop
0231 self.length = length
0232
0233 def __repr__(self):
0234 return '<%s %s>' % (
0235 self.__class__.__name__,
0236 self)
0237
0238 def __str__(self):
0239 if self.stop is None:
0240 stop = '*'
0241 else:
0242 stop = self.stop + 1
0243 if self.length is None:
0244 length = '*'
0245 else:
0246 length = self.length
0247 return 'bytes %s-%s/%s' % (self.start, stop, length)
0248
0249 def __iter__(self):
0250 """
0251 Mostly so you can unpack this, like:
0252
0253 start, stop, length = res.content_range
0254 """
0255 return iter([self.start, self.stop, self.length])
0256
0257
0258 def parse(cls, value):
0259 """
0260 Parse the header. May return None if it cannot parse.
0261 """
0262 if value is None:
0263 return None
0264 value = value.strip()
0265 if not value.startswith('bytes '):
0266
0267 return None
0268 value = value[len('bytes '):].strip()
0269 if '/' not in value:
0270
0271 return None
0272 range, length = value.split('/', 1)
0273 if '-' not in range:
0274
0275 return None
0276 start, end = range.split('-', 1)
0277 try:
0278 start = int(start)
0279 if end == '*':
0280 end = None
0281 else:
0282 end = int(end)
0283 if length == '*':
0284 length = None
0285 else:
0286 length = int(length)
0287 except ValueError:
0288
0289 return None
0290 if end is None:
0291 return cls(start, None, length)
0292 else:
0293 return cls(start, end-1, length)
0294 parse = classmethod(parse)