import re __all__ = ['Range', 'ContentRange'] _rx_range = re.compile('bytes *= *(\d*) *- *(\d*)', flags=re.I) _rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])') class Range(object): """ Represents the Range header. """ def __init__(self, start, end): assert end is None or end >= 0, "Bad range end: %r" % end self.start = start self.end = end # non-inclusive def range_for_length(self, length): """ *If* there is only one range, and *if* it is satisfiable by the given length, then return a (start, end) non-inclusive range of bytes to serve. Otherwise return None """ if length is None: return None start, end = self.start, self.end if end is None: end = length if start < 0: start += length if _is_content_range_valid(start, end, length): stop = min(end, length) return (start, stop) else: return None 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): s,e = self.start, self.end if e is None: r = 'bytes=%s' % s if s >= 0: r += '-' return r return 'bytes=%s-%s' % (s, e-1) def __repr__(self): return '%s(%r, %r)' % ( self.__class__.__name__, self.start, self.end) def __iter__(self): return iter((self.start, self.end)) @classmethod def parse(cls, header): """ Parse the header; may return None if header is invalid """ m = _rx_range.match(header or '') if not m: return None start, end = m.groups() if not start: return cls(-int(end), None) start = int(start) if not end: return cls(start, None) end = int(end) + 1 # return val is non-inclusive if start >= end: return None return cls(start, end) class ContentRange(object): """ Represents the Content-Range header This header is ``start-stop/length``, where start-stop and length can be ``*`` (represented as None in the attributes). """ def __init__(self, start, stop, length): if not _is_content_range_valid(start, stop, length): raise ValueError( "Bad start:stop/length: %r-%r/%r" % (start, stop, length)) self.start = start self.stop = stop # this is python-style range end (non-inclusive) self.length = length def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self) def __str__(self): if self.length is None: length = '*' else: length = self.length if self.start is None: assert self.stop is None return 'bytes */%s' % length stop = self.stop - 1 # from non-inclusive to HTTP-style 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. """ m = _rx_content_range.match(value or '') if not m: return None s, e, l = m.groups() if s: s = int(s) e = int(e) + 1 l = l and int(l) if not _is_content_range_valid(s, e, l, response=True): return None return cls(s, e, l) def _is_content_range_valid(start, stop, length, response=False): if (start is None) != (stop is None): return False elif start is None: return length is None or length >= 0 elif length is None: return 0 <= start < stop elif start >= stop: return False elif response and stop > length: # "content-range: bytes 0-50/10" is invalid for a response # "range: bytes 0-50" is valid for a request to a 10-bytes entity return False else: return 0 <= start < length