Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import re 

2 

3__all__ = ['Range', 'ContentRange'] 

4_rx_range = re.compile(r'bytes *= *(\d*) *- *(\d*)', flags=re.I) 

5_rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])') 

6 

7class Range(object): 

8 """ 

9 Represents the Range header. 

10 """ 

11 

12 def __init__(self, start, end): 

13 assert end is None or end >= 0, "Bad range end: %r" % end 

14 self.start = start 

15 self.end = end # non-inclusive 

16 

17 def range_for_length(self, length): 

18 """ 

19 *If* there is only one range, and *if* it is satisfiable by 

20 the given length, then return a (start, end) non-inclusive range 

21 of bytes to serve. Otherwise return None 

22 """ 

23 if length is None: 

24 return None 

25 start, end = self.start, self.end 

26 if end is None: 

27 end = length 

28 if start < 0: 

29 start += length 

30 if _is_content_range_valid(start, end, length): 

31 stop = min(end, length) 

32 return (start, stop) 

33 else: 

34 return None 

35 

36 def content_range(self, length): 

37 """ 

38 Works like range_for_length; returns None or a ContentRange object 

39 

40 You can use it like:: 

41 

42 response.content_range = req.range.content_range(response.content_length) 

43 

44 Though it's still up to you to actually serve that content range! 

45 """ 

46 range = self.range_for_length(length) 

47 if range is None: 

48 return None 

49 return ContentRange(range[0], range[1], length) 

50 

51 def __str__(self): 

52 s,e = self.start, self.end 

53 if e is None: 

54 r = 'bytes=%s' % s 

55 if s >= 0: 

56 r += '-' 

57 return r 

58 return 'bytes=%s-%s' % (s, e-1) 

59 

60 def __repr__(self): 

61 return '<%s bytes %r-%r>' % ( 

62 self.__class__.__name__, 

63 self.start, self.end) 

64 

65 def __iter__(self): 

66 return iter((self.start, self.end)) 

67 

68 @classmethod 

69 def parse(cls, header): 

70 """ 

71 Parse the header; may return None if header is invalid 

72 """ 

73 m = _rx_range.match(header or '') 

74 if not m: 

75 return None 

76 start, end = m.groups() 

77 if not start: 

78 return cls(-int(end), None) 

79 start = int(start) 

80 if not end: 

81 return cls(start, None) 

82 end = int(end) + 1 # return val is non-inclusive 

83 if start >= end: 

84 return None 

85 return cls(start, end) 

86 

87 

88class ContentRange(object): 

89 

90 """ 

91 Represents the Content-Range header 

92 

93 This header is ``start-stop/length``, where start-stop and length 

94 can be ``*`` (represented as None in the attributes). 

95 """ 

96 

97 def __init__(self, start, stop, length): 

98 if not _is_content_range_valid(start, stop, length): 

99 raise ValueError( 

100 "Bad start:stop/length: %r-%r/%r" % (start, stop, length)) 

101 self.start = start 

102 self.stop = stop # this is python-style range end (non-inclusive) 

103 self.length = length 

104 

105 def __repr__(self): 

106 return '<%s %s>' % (self.__class__.__name__, self) 

107 

108 def __str__(self): 

109 if self.length is None: 

110 length = '*' 

111 else: 

112 length = self.length 

113 if self.start is None: 

114 assert self.stop is None 

115 return 'bytes */%s' % length 

116 stop = self.stop - 1 # from non-inclusive to HTTP-style 

117 return 'bytes %s-%s/%s' % (self.start, stop, length) 

118 

119 def __iter__(self): 

120 """ 

121 Mostly so you can unpack this, like: 

122 

123 start, stop, length = res.content_range 

124 """ 

125 return iter([self.start, self.stop, self.length]) 

126 

127 @classmethod 

128 def parse(cls, value): 

129 """ 

130 Parse the header. May return None if it cannot parse. 

131 """ 

132 m = _rx_content_range.match(value or '') 

133 if not m: 

134 return None 

135 s, e, l = m.groups() 

136 if s: 

137 s = int(s) 

138 e = int(e) + 1 

139 l = l and int(l) 

140 if not _is_content_range_valid(s, e, l, response=True): 

141 return None 

142 return cls(s, e, l) 

143 

144 

145def _is_content_range_valid(start, stop, length, response=False): 

146 if (start is None) != (stop is None): 

147 return False 

148 elif start is None: 

149 return length is None or length >= 0 

150 elif length is None: 

151 return 0 <= start < stop 

152 elif start >= stop: 

153 return False 

154 elif response and stop > length: 

155 # "content-range: bytes 0-50/10" is invalid for a response 

156 # "range: bytes 0-50" is valid for a request to a 10-bytes entity 

157 return False 

158 else: 

159 return 0 <= start < length