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<last_end, or last_end<0')
if not end.strip():
end = None
else:
end = int(end)
if end is not None and begin > end:
raise ValueError('begin>end')
last_end = end
ranges.append((begin, end))
except ValueError, e:
# In this case where the Range header is malformed,
# section 14.16 says to treat the request as if the
# Range header was not present. How do I log this?
print e
return None
return (units, ranges)
parse_bytes = staticmethod(parse_bytes)
#@staticmethod
def serialize_bytes(units, ranges):
"""
Takes the output of parse_bytes and turns it into a header
"""
parts = []
for begin, end in ranges:
if end is None:
if begin >= 0:
parts.append('%s-' % begin)
else:
parts.append(str(begin))
else:
if begin < 0:
raise ValueError(
"(%r, %r) should have a non-negative first value" % (begin, end))
if end < 0:
raise ValueError(
"(%r, %r) should have a non-negative second value" % (begin, end))
parts.append('%s-%s' % (begin, end))
return '%s=%s' % (units, ','.join(parts))
serialize_bytes = staticmethod(serialize_bytes)
#@staticmethod
def bytes_to_python_ranges(ranges, length=None):
"""
Converts the list-of-ranges from parse_bytes() to a Python-style
list of ranges (non-inclusive end points)
In the list of ranges, the last item can be None to indicate that
it should go to the end of the file, and the first item can be
negative to indicate that it should start from an offset from the
end. If you give a length then this will not occur (negative
numbers and offsets will be resolved).
If length is given, and any range is not value, then None is
returned.
"""
result = []
for begin, end in ranges:
if begin < 0:
if length is None:
result.append((begin, None))
continue
else:
begin = length - begin
end = length
if begin is None:
begin = 0
if end is None and length is not None:
end = length
if length is not None and end is not None and end > length:
return None
if end is not None:
end -= 1
result.append((begin, end))
return result
bytes_to_python_ranges = staticmethod(bytes_to_python_ranges)
#@staticmethod
def python_ranges_to_bytes(ranges):
"""
Converts a Python-style list of ranges to what serialize_bytes
expects.
This is the inverse of bytes_to_python_ranges
"""
result = []
for begin, end in ranges:
if end is None:
result.append((begin, None))
else:
result.append((begin, end+1))
return result
python_ranges_to_bytes = staticmethod(python_ranges_to_bytes)
class ContentRange(object):
"""
Represents the Content-Range header
This header is ``start-stop/length``, where stop and length can be
``*`` (represented as None in the attributes).
"""
def __init__(self, start, stop, length):
assert start >= 0, "Bad start: %r" % start
assert stop is None or (stop >= 0 and stop >= start), (
"Bad stop: %r" % stop)
self.start = start
self.stop = stop
self.length = length
def __repr__(self):
return '<%s %s>' % (
self.__class__.__name__,
self)
def __str__(self):
if self.stop is None:
stop = '*'
else:
stop = self.stop + 1
if self.length is None:
length = '*'
else:
length = self.length
return 'bytes %s-%s/%s' % (self.start, stop, length)
def __iter__(self):
"""
Mostly so you can unpack this, like:
start, stop, length = res.content_range
"""
return iter([self.start, self.stop, self.length])
#@classmethod
def parse(cls, value):
"""
Parse the header. May return None if it cannot parse.
"""
if value is None:
return None
value = value.strip()
if not value.startswith('bytes '):
# Unparseable
return None
value = value[len('bytes '):].strip()
if '/' not in value:
# Invalid, no length given
return None
range, length = value.split('/', 1)
if '-' not in range:
# Invalid, no range
return None
start, end = range.split('-', 1)
try:
start = int(start)
if end == '*':
end = None
else:
end = int(end)
if length == '*':
length = None
else:
length = int(length)
except ValueError:
# Parse problem
return None
if end is None:
return cls(start, None, length)
else:
return cls(start, end-1, length)
parse = classmethod(parse)