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            # Unknown; only works with ranges with no end-point
0042            if end is None:
0043                return (begin, end)
0044            return None
0045        if end >= length:
0046            # Overshoots the end
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    #@classmethod
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    #@staticmethod
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                    # This is a range asking for a trailing chunk
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            # In this case where the Range header is malformed,
0133            # section 14.16 says to treat the request as if the
0134            # Range header was not present.  How do I log this?
0135            print e
0136            return None
0137        return (units, ranges)
0138    parse_bytes = staticmethod(parse_bytes)
0139
0140    #@staticmethod
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    #@staticmethod
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    #@staticmethod
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    #@classmethod
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            # Unparseable
0267            return None
0268        value = value[len('bytes '):].strip()
0269        if '/' not in value:
0270            # Invalid, no length given
0271            return None
0272        range, length = value.split('/', 1)
0273        if '-' not in range:
0274            # Invalid, no range
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            # Parse problem
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)