class Range(object):
"""
Represents the Range header.
This only represents ``bytes`` ranges, which are the only kind
specified in HTTP. This can represent multiple sets of ranges,
but no place else is this multi-range facility supported.
"""
def __init__(self, ranges):
for begin, end in ranges:
assert end is None or end >= 0, "Bad ranges: %r" % ranges
self.ranges = ranges
def satisfiable(self, length):
"""
Returns true if this range can be satisfied by the resource
with the given byte length.
"""
for begin, end in self.ranges:
if end is not None and end >= length:
return False
return True
def range_for_length(self, length):
"""
*If* there is only one range, and *if* it is satisfiable by
the given length, then return a (begin, end) non-inclusive range
of bytes to serve. Otherwise return None
If length is None (unknown length), then the resulting range
may be (begin, None), meaning it should be served from that
point. If it's a range with a fixed endpoint we won't know if
it is satisfiable, so this will return None.
"""
if len(self.ranges) != 1:
return None
begin, end = self.ranges[0]
if length is None:
# Unknown; only works with ranges with no end-point
if end is None:
return (begin, end)
return None
if end >= length:
# Overshoots the end
return None
return (begin, end)
def content_range(self, length):
"""
Works like range_for_length; returns None or a ContentRange object
You can use it like::
response.content_range = req.range.content_range(response.content_length)
Though it's still up to you to actually serve that content range!
"""
range = self.range_for_length(length)
if range is None:
return None
return ContentRange(range[0], range[1], length)
def __str__(self):
return self.serialize_bytes('bytes', self.python_ranges_to_bytes(self.ranges))
def __repr__(self):
return '<%s ranges=%s>' % (
self.__class__.__name__,
', '.join(map(repr, self.ranges)))
#@classmethod
def parse(cls, header):
"""
Parse the header; may return None if header is invalid
"""
bytes = cls.parse_bytes(header)
if bytes is None:
return None
units, ranges = bytes
if units.lower() != 'bytes':
return None
ranges = cls.bytes_to_python_ranges(ranges)
if ranges is None:
return None
return cls(ranges)
parse = classmethod(parse)
#@staticmethod
def parse_bytes(header):
"""
Parse a Range header into (bytes, list_of_ranges). Note that the
ranges are *inclusive* (like in HTTP, not like in Python
typically).
Will return None if the header is invalid
"""
if not header:
raise TypeError(
"The header must not be empty")
ranges = []
last_end = 0
try:
(units, range) = header.split("=", 1)
units = units.strip().lower()
for item in range.split(","):
if '-' not in item:
raise ValueError()
if item.startswith('-'):
# This is a range asking for a trailing chunk
if last_end < 0:
raise ValueError('too many end ranges')
begin = int(item)
end = None
last_end = -1
else:
(begin, end) = item.split("-", 1)
begin = int(begin)
if begin < last_end or last_end < 0:
print begin, last_end
raise ValueError('begin