1import re
2
3__all__ = ['Range', 'ContentRange']
4
5_rx_range = re.compile('bytes *= *(\d*) *- *(\d*)', flags=re.I)
6_rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])')
7
8class Range(object):
9    """
10        Represents the Range header.
11    """
12
13    def __init__(self, start, end):
14        assert end is None or end >= 0, "Bad range end: %r" % end
15        self.start = start
16        self.end = end # non-inclusive
17
18    def range_for_length(self, length):
19        """
20            *If* there is only one range, and *if* it is satisfiable by
21            the given length, then return a (start, end) non-inclusive range
22            of bytes to serve.  Otherwise return None
23        """
24        if length is None:
25            return None
26        start, end = self.start, self.end
27        if end is None:
28            end = length
29            if start < 0:
30                start += length
31        if _is_content_range_valid(start, end, length):
32            stop = min(end, length)
33            return (start, stop)
34        else:
35            return None
36
37    def content_range(self, length):
38        """
39            Works like range_for_length; returns None or a ContentRange object
40
41            You can use it like::
42
43                response.content_range = req.range.content_range(response.content_length)
44
45            Though it's still up to you to actually serve that content range!
46        """
47        range = self.range_for_length(length)
48        if range is None:
49            return None
50        return ContentRange(range[0], range[1], length)
51
52    def __str__(self):
53        s,e = self.start, self.end
54        if e is None:
55            r = 'bytes=%s' % s
56            if s >= 0:
57                r += '-'
58            return r
59        return 'bytes=%s-%s' % (s, e-1)
60
61    def __repr__(self):
62        return '%s(%r, %r)' % (
63            self.__class__.__name__,
64            self.start, self.end)
65
66    def __iter__(self):
67        return iter((self.start, self.end))
68
69    @classmethod
70    def parse(cls, header):
71        """
72            Parse the header; may return None if header is invalid
73        """
74        m = _rx_range.match(header or '')
75        if not m:
76            return None
77        start, end = m.groups()
78        if not start:
79            return cls(-int(end), None)
80        start = int(start)
81        if not end:
82            return cls(start, None)
83        end = int(end) + 1 # return val is non-inclusive
84        if start >= end:
85            return None
86        return cls(start, end)
87
88
89class ContentRange(object):
90
91    """
92    Represents the Content-Range header
93
94    This header is ``start-stop/length``, where start-stop and length
95    can be ``*`` (represented as None in the attributes).
96    """
97
98    def __init__(self, start, stop, length):
99        if not _is_content_range_valid(start, stop, length):
100            raise ValueError(
101                "Bad start:stop/length: %r-%r/%r" % (start, stop, length))
102        self.start = start
103        self.stop = stop # this is python-style range end (non-inclusive)
104        self.length = length
105
106    def __repr__(self):
107        return '<%s %s>' % (self.__class__.__name__, self)
108
109    def __str__(self):
110        if self.length is None:
111            length = '*'
112        else:
113            length = self.length
114        if self.start is None:
115            assert self.stop is None
116            return 'bytes */%s' % length
117        stop = self.stop - 1 # from non-inclusive to HTTP-style
118        return 'bytes %s-%s/%s' % (self.start, stop, length)
119
120    def __iter__(self):
121        """
122            Mostly so you can unpack this, like:
123
124                start, stop, length = res.content_range
125        """
126        return iter([self.start, self.stop, self.length])
127
128    @classmethod
129    def parse(cls, value):
130        """
131            Parse the header.  May return None if it cannot parse.
132        """
133        m = _rx_content_range.match(value or '')
134        if not m:
135            return None
136        s, e, l = m.groups()
137        if s:
138            s = int(s)
139            e = int(e) + 1
140        l = l and int(l)
141        if not _is_content_range_valid(s, e, l, response=True):
142            return None
143        return cls(s, e, l)
144
145
146def _is_content_range_valid(start, stop, length, response=False):
147    if (start is None) != (stop is None):
148        return False
149    elif start is None:
150        return length is None or length >= 0
151    elif length is None:
152        return 0 <= start < stop
153    elif start >= stop:
154        return False
155    elif response and stop > length:
156        # "content-range: bytes 0-50/10" is invalid for a response
157        # "range: bytes 0-50" is valid for a request to a 10-bytes entity
158        return False
159    else:
160        return 0 <= start < length
161