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 

2import struct 

3import zlib 

4from base64 import b64encode 

5from datetime import datetime, timedelta 

6from hashlib import md5 

7 

8from webob.byterange import ContentRange 

9from webob.cachecontrol import CacheControl, serialize_cache_control 

10from webob.compat import ( 

11 PY2, 

12 bytes_, 

13 native_, 

14 string_types, 

15 text_type, 

16 url_quote, 

17 urlparse, 

18) 

19from webob.cookies import Cookie, make_cookie 

20from webob.datetime_utils import ( 

21 parse_date_delta, 

22 serialize_date_delta, 

23 timedelta_to_seconds, 

24) 

25from webob.descriptors import ( 

26 CHARSET_RE, 

27 SCHEME_RE, 

28 converter, 

29 date_header, 

30 header_getter, 

31 list_header, 

32 parse_auth, 

33 parse_content_range, 

34 parse_etag_response, 

35 parse_int, 

36 parse_int_safe, 

37 serialize_auth, 

38 serialize_content_range, 

39 serialize_etag_response, 

40 serialize_int, 

41) 

42from webob.headers import ResponseHeaders 

43from webob.request import BaseRequest 

44from webob.util import status_generic_reasons, status_reasons, warn_deprecation 

45 

46try: 

47 import simplejson as json 

48except ImportError: 

49 import json 

50 

51__all__ = ['Response'] 

52 

53_PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I) 

54_OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I) 

55 

56_gzip_header = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff' 

57 

58_marker = object() 

59 

60class Response(object): 

61 """ 

62 Represents a WSGI response. 

63 

64 If no arguments are passed, creates a :class:`~Response` that uses a 

65 variety of defaults. The defaults may be changed by sub-classing the 

66 :class:`~Response`. See the :ref:`sub-classing notes 

67 <response_subclassing_notes>`. 

68 

69 :cvar ~Response.body: If ``body`` is a ``text_type``, then it will be 

70 encoded using either ``charset`` when provided or ``default_encoding`` 

71 when ``charset`` is not provided if the ``content_type`` allows for a 

72 ``charset``. This argument is mutually exclusive with ``app_iter``. 

73 

74 :vartype ~Response.body: bytes or text_type 

75 

76 :cvar ~Response.status: Either an :class:`int` or a string that is 

77 an integer followed by the status text. If it is an integer, it will be 

78 converted to a proper status that also includes the status text. Any 

79 existing status text will be kept. Non-standard values are allowed. 

80 

81 :vartype ~Response.status: int or str 

82 

83 :cvar ~Response.headerlist: A list of HTTP headers for the response. 

84 

85 :vartype ~Response.headerlist: list 

86 

87 :cvar ~Response.app_iter: An iterator that is used as the body of the 

88 response. Should conform to the WSGI requirements and should provide 

89 bytes. This argument is mutually exclusive with ``body``. 

90 

91 :vartype ~Response.app_iter: iterable 

92 

93 :cvar ~Response.content_type: Sets the ``Content-Type`` header. If no 

94 ``content_type`` is provided, and there is no ``headerlist``, the 

95 ``default_content_type`` will be automatically set. If ``headerlist`` 

96 is provided then this value is ignored. 

97 

98 :vartype ~Response.content_type: str or None 

99 

100 :cvar conditional_response: Used to change the behavior of the 

101 :class:`~Response` to check the original request for conditional 

102 response headers. See :meth:`~Response.conditional_response_app` for 

103 more information. 

104 

105 :vartype conditional_response: bool 

106 

107 :cvar ~Response.charset: Adds a ``charset`` ``Content-Type`` parameter. If 

108 no ``charset`` is provided and the ``Content-Type`` is text, then the 

109 ``default_charset`` will automatically be added. Currently the only 

110 ``Content-Type``'s that allow for a ``charset`` are defined to be 

111 ``text/*``, ``application/xml``, and ``*/*+xml``. Any other 

112 ``Content-Type``'s will not have a ``charset`` added. If a 

113 ``headerlist`` is provided this value is ignored. 

114 

115 :vartype ~Response.charset: str or None 

116 

117 All other response attributes may be set on the response by providing them 

118 as keyword arguments. A :exc:`TypeError` will be raised for any unexpected 

119 keywords. 

120 

121 .. _response_subclassing_notes: 

122 

123 **Sub-classing notes:** 

124 

125 * The ``default_content_type`` is used as the default for the 

126 ``Content-Type`` header that is returned on the response. It is 

127 ``text/html``. 

128 

129 * The ``default_charset`` is used as the default character set to return on 

130 the ``Content-Type`` header, if the ``Content-Type`` allows for a 

131 ``charset`` parameter. Currently the only ``Content-Type``'s that allow 

132 for a ``charset`` are defined to be: ``text/*``, ``application/xml``, and 

133 ``*/*+xml``. Any other ``Content-Type``'s will not have a ``charset`` 

134 added. 

135 

136 * The ``unicode_errors`` is set to ``strict``, and access on a 

137 :attr:`~Response.text` will raise an error if it fails to decode the 

138 :attr:`~Response.body`. 

139 

140 * ``default_conditional_response`` is set to ``False``. This flag may be 

141 set to ``True`` so that all ``Response`` objects will attempt to check 

142 the original request for conditional response headers. See 

143 :meth:`~Response.conditional_response_app` for more information. 

144 

145 * ``default_body_encoding`` is set to 'UTF-8' by default. It exists to 

146 allow users to get/set the ``Response`` object using ``.text``, even if 

147 no ``charset`` has been set for the ``Content-Type``. 

148 """ 

149 

150 default_content_type = 'text/html' 

151 default_charset = 'UTF-8' 

152 unicode_errors = 'strict' 

153 default_conditional_response = False 

154 default_body_encoding = 'UTF-8' 

155 

156 # These two are only around so that when people pass them into the 

157 # constructor they correctly get saved and set, however they are not used 

158 # by any part of the Response. See commit 

159 # 627593bbcd4ab52adc7ee569001cdda91c670d5d for rationale. 

160 request = None 

161 environ = None 

162 

163 # 

164 # __init__, from_file, copy 

165 # 

166 

167 def __init__(self, body=None, status=None, headerlist=None, app_iter=None, 

168 content_type=None, conditional_response=None, charset=_marker, 

169 **kw): 

170 # Do some sanity checking, and turn json_body into an actual body 

171 if app_iter is None and body is None and ('json_body' in kw or 'json' in kw): 

172 if 'json_body' in kw: 

173 json_body = kw.pop('json_body') 

174 else: 

175 json_body = kw.pop('json') 

176 body = json.dumps(json_body, separators=(',', ':')).encode('UTF-8') 

177 

178 if content_type is None: 

179 content_type = 'application/json' 

180 

181 if app_iter is None: 

182 if body is None: 

183 body = b'' 

184 elif body is not None: 

185 raise TypeError( 

186 "You may only give one of the body and app_iter arguments") 

187 

188 # Set up Response.status 

189 if status is None: 

190 self._status = '200 OK' 

191 else: 

192 self.status = status 

193 

194 # Initialize headers 

195 self._headers = None 

196 if headerlist is None: 

197 self._headerlist = [] 

198 else: 

199 self._headerlist = headerlist 

200 

201 # Set the encoding for the Response to charset, so if a charset is 

202 # passed but the Content-Type does not allow for a charset, we can 

203 # still encode text_type body's. 

204 # r = Response( 

205 # content_type='application/foo', 

206 # charset='UTF-8', 

207 # body=u'somebody') 

208 # Should work without issues, and the header will be correctly set to 

209 # Content-Type: application/foo with no charset on it. 

210 

211 encoding = None 

212 if charset is not _marker: 

213 encoding = charset 

214 

215 # Does the status code have a body or not? 

216 code_has_body = ( 

217 self._status[0] != '1' and 

218 self._status[:3] not in ('204', '205', '304') 

219 ) 

220 

221 # We only set the content_type to the one passed to the constructor or 

222 # the default content type if there is none that exists AND there was 

223 # no headerlist passed. If a headerlist was provided then most likely 

224 # the ommission of the Content-Type is on purpose and we shouldn't try 

225 # to be smart about it. 

226 # 

227 # Also allow creation of a empty Response with just the status set to a 

228 # Response with empty body, such as Response(status='204 No Content') 

229 # without the default content_type being set (since empty bodies have 

230 # no Content-Type) 

231 # 

232 # Check if content_type is set because default_content_type could be 

233 # None, in which case there is no content_type, and thus we don't need 

234 # to anything 

235 

236 content_type = content_type or self.default_content_type 

237 

238 if headerlist is None and code_has_body and content_type: 

239 # Set up the charset, if the content_type doesn't already have one 

240 

241 has_charset = 'charset=' in content_type 

242 

243 # If the Content-Type already has a charset, we don't set the user 

244 # provided charset on the Content-Type, so we shouldn't use it as 

245 # the encoding for text_type based body's. 

246 if has_charset: 

247 encoding = None 

248 

249 # Do not use the default_charset for the encoding because we 

250 # want things like 

251 # Response(content_type='image/jpeg',body=u'foo') to raise when 

252 # trying to encode the body. 

253 

254 new_charset = encoding 

255 

256 if ( 

257 not has_charset and 

258 charset is _marker and 

259 self.default_charset 

260 ): 

261 new_charset = self.default_charset 

262 

263 # Optimize for the default_content_type as shipped by 

264 # WebOb, becuase we know that 'text/html' has a charset, 

265 # otherwise add a charset if the content_type has a charset. 

266 # 

267 # Even if the user supplied charset explicitly, we do not add 

268 # it to the Content-Type unless it has has a charset, instead 

269 # the user supplied charset is solely used for encoding the 

270 # body if it is a text_type 

271 

272 if ( 

273 new_charset and 

274 ( 

275 content_type == 'text/html' or 

276 _content_type_has_charset(content_type) 

277 ) 

278 ): 

279 content_type += '; charset=' + new_charset 

280 

281 self._headerlist.append(('Content-Type', content_type)) 

282 

283 # Set up conditional response 

284 if conditional_response is None: 

285 self.conditional_response = self.default_conditional_response 

286 else: 

287 self.conditional_response = bool(conditional_response) 

288 

289 # Set up app_iter if the HTTP Status code has a body 

290 if app_iter is None and code_has_body: 

291 if isinstance(body, text_type): 

292 # Fall back to trying self.charset if encoding is not set. In 

293 # most cases encoding will be set to the default value. 

294 encoding = encoding or self.charset 

295 if encoding is None: 

296 raise TypeError( 

297 "You cannot set the body to a text value without a " 

298 "charset") 

299 body = body.encode(encoding) 

300 app_iter = [body] 

301 

302 if headerlist is not None: 

303 self._headerlist[:] = [ 

304 (k, v) 

305 for (k, v) 

306 in self._headerlist 

307 if k.lower() != 'content-length' 

308 ] 

309 self._headerlist.append(('Content-Length', str(len(body)))) 

310 elif app_iter is None and not code_has_body: 

311 app_iter = [b''] 

312 

313 self._app_iter = app_iter 

314 

315 # Loop through all the remaining keyword arguments 

316 for name, value in kw.items(): 

317 if not hasattr(self.__class__, name): 

318 # Not a basic attribute 

319 raise TypeError( 

320 "Unexpected keyword: %s=%r" % (name, value)) 

321 setattr(self, name, value) 

322 

323 @classmethod 

324 def from_file(cls, fp): 

325 """Reads a response from a file-like object (it must implement 

326 ``.read(size)`` and ``.readline()``). 

327 

328 It will read up to the end of the response, not the end of the 

329 file. 

330 

331 This reads the response as represented by ``str(resp)``; it 

332 may not read every valid HTTP response properly. Responses 

333 must have a ``Content-Length``.""" 

334 headerlist = [] 

335 status = fp.readline().strip() 

336 is_text = isinstance(status, text_type) 

337 

338 if is_text: 

339 _colon = ':' 

340 _http = 'HTTP/' 

341 else: 

342 _colon = b':' 

343 _http = b'HTTP/' 

344 

345 if status.startswith(_http): 

346 (http_ver, status_num, status_text) = status.split(None, 2) 

347 status = '%s %s' % (native_(status_num), native_(status_text)) 

348 

349 while 1: 

350 line = fp.readline().strip() 

351 if not line: 

352 # end of headers 

353 break 

354 try: 

355 header_name, value = line.split(_colon, 1) 

356 except ValueError: 

357 raise ValueError('Bad header line: %r' % line) 

358 value = value.strip() 

359 headerlist.append(( 

360 native_(header_name, 'latin-1'), 

361 native_(value, 'latin-1') 

362 )) 

363 r = cls( 

364 status=status, 

365 headerlist=headerlist, 

366 app_iter=(), 

367 ) 

368 body = fp.read(r.content_length or 0) 

369 if is_text: 

370 r.text = body 

371 else: 

372 r.body = body 

373 return r 

374 

375 def copy(self): 

376 """Makes a copy of the response.""" 

377 # we need to do this for app_iter to be reusable 

378 app_iter = list(self._app_iter) 

379 iter_close(self._app_iter) 

380 # and this to make sure app_iter instances are different 

381 self._app_iter = list(app_iter) 

382 return self.__class__( 

383 status=self._status, 

384 headerlist=self._headerlist[:], 

385 app_iter=app_iter, 

386 conditional_response=self.conditional_response) 

387 

388 # 

389 # __repr__, __str__ 

390 # 

391 

392 def __repr__(self): 

393 return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)), 

394 self.status) 

395 

396 def __str__(self, skip_body=False): 

397 parts = [self.status] 

398 if not skip_body: 

399 # Force enumeration of the body (to set content-length) 

400 self.body 

401 parts += map('%s: %s'.__mod__, self.headerlist) 

402 if not skip_body and self.body: 

403 parts += ['', self.body if PY2 else self.text] 

404 return '\r\n'.join(parts) 

405 

406 # 

407 # status, status_code/status_int 

408 # 

409 

410 def _status__get(self): 

411 """ 

412 The status string. 

413 """ 

414 return self._status 

415 

416 def _status__set(self, value): 

417 try: 

418 code = int(value) 

419 except (ValueError, TypeError): 

420 pass 

421 else: 

422 self.status_code = code 

423 return 

424 if not PY2: 

425 if isinstance(value, bytes): 

426 value = value.decode('ascii') 

427 elif isinstance(value, text_type): 

428 value = value.encode('ascii') 

429 if not isinstance(value, str): 

430 raise TypeError( 

431 "You must set status to a string or integer (not %s)" 

432 % type(value)) 

433 

434 # Attempt to get the status code itself, if this fails we should fail 

435 try: 

436 # We don't need this value anywhere, we just want to validate it's 

437 # an integer. So we are using the side-effect of int() raises a 

438 # ValueError as a test 

439 int(value.split()[0]) 

440 except ValueError: 

441 raise ValueError('Invalid status code, integer required.') 

442 self._status = value 

443 

444 status = property(_status__get, _status__set, doc=_status__get.__doc__) 

445 

446 def _status_code__get(self): 

447 """ 

448 The status as an integer. 

449 """ 

450 return int(self._status.split()[0]) 

451 

452 def _status_code__set(self, code): 

453 try: 

454 self._status = '%d %s' % (code, status_reasons[code]) 

455 except KeyError: 

456 self._status = '%d %s' % (code, status_generic_reasons[code // 100]) 

457 

458 status_code = status_int = property(_status_code__get, _status_code__set, 

459 doc=_status_code__get.__doc__) 

460 

461 # 

462 # headerslist, headers 

463 # 

464 

465 def _headerlist__get(self): 

466 """ 

467 The list of response headers. 

468 """ 

469 return self._headerlist 

470 

471 def _headerlist__set(self, value): 

472 self._headers = None 

473 if not isinstance(value, list): 

474 if hasattr(value, 'items'): 

475 value = value.items() 

476 value = list(value) 

477 self._headerlist = value 

478 

479 def _headerlist__del(self): 

480 self.headerlist = [] 

481 

482 headerlist = property(_headerlist__get, _headerlist__set, 

483 _headerlist__del, doc=_headerlist__get.__doc__) 

484 

485 def _headers__get(self): 

486 """ 

487 The headers in a dictionary-like object. 

488 """ 

489 if self._headers is None: 

490 self._headers = ResponseHeaders.view_list(self._headerlist) 

491 return self._headers 

492 

493 def _headers__set(self, value): 

494 if hasattr(value, 'items'): 

495 value = value.items() 

496 self.headerlist = value 

497 self._headers = None 

498 

499 headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) 

500 

501 # 

502 # body 

503 # 

504 

505 def _body__get(self): 

506 """ 

507 The body of the response, as a :class:`bytes`. This will read in 

508 the entire app_iter if necessary. 

509 """ 

510 app_iter = self._app_iter 

511# try: 

512# if len(app_iter) == 1: 

513# return app_iter[0] 

514# except: 

515# pass 

516 if isinstance(app_iter, list) and len(app_iter) == 1: 

517 return app_iter[0] 

518 if app_iter is None: 

519 raise AttributeError("No body has been set") 

520 try: 

521 body = b''.join(app_iter) 

522 finally: 

523 iter_close(app_iter) 

524 if isinstance(body, text_type): 

525 raise _error_unicode_in_app_iter(app_iter, body) 

526 self._app_iter = [body] 

527 if len(body) == 0: 

528 # if body-length is zero, we assume it's a HEAD response and 

529 # leave content_length alone 

530 pass 

531 elif self.content_length is None: 

532 self.content_length = len(body) 

533 elif self.content_length != len(body): 

534 raise AssertionError( 

535 "Content-Length is different from actual app_iter length " 

536 "(%r!=%r)" 

537 % (self.content_length, len(body)) 

538 ) 

539 return body 

540 

541 def _body__set(self, value=b''): 

542 if not isinstance(value, bytes): 

543 if isinstance(value, text_type): 

544 msg = ("You cannot set Response.body to a text object " 

545 "(use Response.text)") 

546 else: 

547 msg = ("You can only set the body to a binary type (not %s)" % 

548 type(value)) 

549 raise TypeError(msg) 

550 if self._app_iter is not None: 

551 self.content_md5 = None 

552 self._app_iter = [value] 

553 self.content_length = len(value) 

554 

555# def _body__del(self): 

556# self.body = '' 

557# #self.content_length = None 

558 

559 body = property(_body__get, _body__set, _body__set) 

560 

561 def _json_body__get(self): 

562 """ 

563 Set/get the body of the response as JSON. 

564 

565 .. note:: 

566 

567 This will automatically :meth:`~bytes.decode` the 

568 :attr:`~Response.body` as ``UTF-8`` on get, and 

569 :meth:`~str.encode` the :meth:`json.dumps` as ``UTF-8`` 

570 before assigning to :attr:`~Response.body`. 

571 

572 """ 

573 # Note: UTF-8 is a content-type specific default for JSON 

574 return json.loads(self.body.decode('UTF-8')) 

575 

576 def _json_body__set(self, value): 

577 self.body = json.dumps(value, separators=(',', ':')).encode('UTF-8') 

578 

579 def _json_body__del(self): 

580 del self.body 

581 

582 json = json_body = property(_json_body__get, _json_body__set, _json_body__del) 

583 

584 def _has_body__get(self): 

585 """ 

586 Determine if the the response has a :attr:`~Response.body`. In 

587 contrast to simply accessing :attr:`~Response.body`, this method 

588 will **not** read the underlying :attr:`~Response.app_iter`. 

589 """ 

590 

591 app_iter = self._app_iter 

592 

593 if isinstance(app_iter, list) and len(app_iter) == 1: 

594 if app_iter[0] != b'': 

595 return True 

596 else: 

597 return False 

598 

599 if app_iter is None: # pragma: no cover 

600 return False 

601 

602 return True 

603 

604 has_body = property(_has_body__get) 

605 

606 # 

607 # text, unicode_body, ubody 

608 # 

609 

610 def _text__get(self): 

611 """ 

612 Get/set the text value of the body using the ``charset`` of the 

613 ``Content-Type`` or the ``default_body_encoding``. 

614 """ 

615 if not self.charset and not self.default_body_encoding: 

616 raise AttributeError( 

617 "You cannot access Response.text unless charset or default_body_encoding" 

618 " is set" 

619 ) 

620 decoding = self.charset or self.default_body_encoding 

621 body = self.body 

622 return body.decode(decoding, self.unicode_errors) 

623 

624 def _text__set(self, value): 

625 if not self.charset and not self.default_body_encoding: 

626 raise AttributeError( 

627 "You cannot access Response.text unless charset or default_body_encoding" 

628 " is set" 

629 ) 

630 if not isinstance(value, text_type): 

631 raise TypeError( 

632 "You can only set Response.text to a unicode string " 

633 "(not %s)" % type(value)) 

634 encoding = self.charset or self.default_body_encoding 

635 self.body = value.encode(encoding) 

636 

637 def _text__del(self): 

638 del self.body 

639 

640 text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) 

641 

642 unicode_body = ubody = property(_text__get, _text__set, _text__del, 

643 "Deprecated alias for .text") 

644 

645 # 

646 # body_file, write(text) 

647 # 

648 

649 def _body_file__get(self): 

650 """ 

651 A file-like object that can be used to write to the 

652 body. If you passed in a list ``app_iter``, that ``app_iter`` will be 

653 modified by writes. 

654 """ 

655 return ResponseBodyFile(self) 

656 

657 def _body_file__set(self, file): 

658 self.app_iter = iter_file(file) 

659 

660 def _body_file__del(self): 

661 del self.body 

662 

663 body_file = property(_body_file__get, _body_file__set, _body_file__del, 

664 doc=_body_file__get.__doc__) 

665 

666 def write(self, text): 

667 if not isinstance(text, bytes): 

668 if not isinstance(text, text_type): 

669 msg = "You can only write str to a Response.body_file, not %s" 

670 raise TypeError(msg % type(text)) 

671 if not self.charset: 

672 msg = ("You can only write text to Response if charset has " 

673 "been set") 

674 raise TypeError(msg) 

675 text = text.encode(self.charset) 

676 app_iter = self._app_iter 

677 if not isinstance(app_iter, list): 

678 try: 

679 new_app_iter = self._app_iter = list(app_iter) 

680 finally: 

681 iter_close(app_iter) 

682 app_iter = new_app_iter 

683 self.content_length = sum(len(chunk) for chunk in app_iter) 

684 app_iter.append(text) 

685 if self.content_length is not None: 

686 self.content_length += len(text) 

687 

688 # 

689 # app_iter 

690 # 

691 

692 def _app_iter__get(self): 

693 """ 

694 Returns the ``app_iter`` of the response. 

695 

696 If ``body`` was set, this will create an ``app_iter`` from that 

697 ``body`` (a single-item list). 

698 """ 

699 return self._app_iter 

700 

701 def _app_iter__set(self, value): 

702 if self._app_iter is not None: 

703 # Undo the automatically-set content-length 

704 self.content_length = None 

705 self._app_iter = value 

706 

707 def _app_iter__del(self): 

708 self._app_iter = [] 

709 self.content_length = None 

710 

711 app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, 

712 doc=_app_iter__get.__doc__) 

713 

714 # 

715 # headers attrs 

716 # 

717 

718 allow = list_header('Allow', '14.7') 

719 # TODO: (maybe) support response.vary += 'something' 

720 # TODO: same thing for all listy headers 

721 vary = list_header('Vary', '14.44') 

722 

723 content_length = converter( 

724 header_getter('Content-Length', '14.17'), 

725 parse_int, serialize_int, 'int') 

726 

727 content_encoding = header_getter('Content-Encoding', '14.11') 

728 content_language = list_header('Content-Language', '14.12') 

729 content_location = header_getter('Content-Location', '14.14') 

730 content_md5 = header_getter('Content-MD5', '14.14') 

731 content_disposition = header_getter('Content-Disposition', '19.5.1') 

732 

733 accept_ranges = header_getter('Accept-Ranges', '14.5') 

734 content_range = converter( 

735 header_getter('Content-Range', '14.16'), 

736 parse_content_range, serialize_content_range, 'ContentRange object') 

737 

738 date = date_header('Date', '14.18') 

739 expires = date_header('Expires', '14.21') 

740 last_modified = date_header('Last-Modified', '14.29') 

741 

742 _etag_raw = header_getter('ETag', '14.19') 

743 etag = converter( 

744 _etag_raw, 

745 parse_etag_response, serialize_etag_response, 

746 'Entity tag' 

747 ) 

748 @property 

749 def etag_strong(self): 

750 return parse_etag_response(self._etag_raw, strong=True) 

751 

752 location = header_getter('Location', '14.30') 

753 pragma = header_getter('Pragma', '14.32') 

754 age = converter( 

755 header_getter('Age', '14.6'), 

756 parse_int_safe, serialize_int, 'int') 

757 

758 retry_after = converter( 

759 header_getter('Retry-After', '14.37'), 

760 parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds') 

761 

762 server = header_getter('Server', '14.38') 

763 

764 # TODO: the standard allows this to be a list of challenges 

765 www_authenticate = converter( 

766 header_getter('WWW-Authenticate', '14.47'), 

767 parse_auth, serialize_auth, 

768 ) 

769 

770 # 

771 # charset 

772 # 

773 

774 def _charset__get(self): 

775 """ 

776 Get/set the ``charset`` specified in ``Content-Type``. 

777 

778 There is no checking to validate that a ``content_type`` actually 

779 allows for a ``charset`` parameter. 

780 """ 

781 header = self.headers.get('Content-Type') 

782 if not header: 

783 return None 

784 match = CHARSET_RE.search(header) 

785 if match: 

786 return match.group(1) 

787 return None 

788 

789 def _charset__set(self, charset): 

790 if charset is None: 

791 self._charset__del() 

792 return 

793 header = self.headers.get('Content-Type', None) 

794 if header is None: 

795 raise AttributeError("You cannot set the charset when no " 

796 "content-type is defined") 

797 match = CHARSET_RE.search(header) 

798 if match: 

799 header = header[:match.start()] + header[match.end():] 

800 header += '; charset=%s' % charset 

801 self.headers['Content-Type'] = header 

802 

803 def _charset__del(self): 

804 header = self.headers.pop('Content-Type', None) 

805 if header is None: 

806 # Don't need to remove anything 

807 return 

808 match = CHARSET_RE.search(header) 

809 if match: 

810 header = header[:match.start()] + header[match.end():] 

811 self.headers['Content-Type'] = header 

812 

813 charset = property(_charset__get, _charset__set, _charset__del, 

814 doc=_charset__get.__doc__) 

815 

816 # 

817 # content_type 

818 # 

819 

820 def _content_type__get(self): 

821 """ 

822 Get/set the ``Content-Type`` header. If no ``Content-Type`` header is 

823 set, this will return ``None``. 

824 

825 .. versionchanged:: 1.7 

826 

827 Setting a new ``Content-Type`` will remove all ``Content-Type`` 

828 parameters and reset the ``charset`` to the default if the 

829 ``Content-Type`` is ``text/*`` or XML (``application/xml`` or 

830 ``*/*+xml``). 

831 

832 To preserve all ``Content-Type`` parameters, you may use the 

833 following code: 

834 

835 .. code-block:: python 

836 

837 resp = Response() 

838 params = resp.content_type_params 

839 resp.content_type = 'application/something' 

840 resp.content_type_params = params 

841 """ 

842 header = self.headers.get('Content-Type') 

843 if not header: 

844 return None 

845 return header.split(';', 1)[0] 

846 

847 def _content_type__set(self, value): 

848 if not value: 

849 self._content_type__del() 

850 return 

851 else: 

852 if PY2 and isinstance(value, text_type): 

853 value = value.encode("latin-1") 

854 

855 if not isinstance(value, string_types): 

856 raise TypeError("content_type requires value to be of string_types") 

857 

858 content_type = value 

859 

860 # Set up the charset if the content-type doesn't have one 

861 

862 has_charset = 'charset=' in content_type 

863 

864 new_charset = None 

865 

866 if ( 

867 not has_charset and 

868 self.default_charset 

869 ): 

870 new_charset = self.default_charset 

871 

872 # Optimize for the default_content_type as shipped by 

873 # WebOb, becuase we know that 'text/html' has a charset, 

874 # otherwise add a charset if the content_type has a charset. 

875 # 

876 # We add the default charset if the content-type is "texty". 

877 if ( 

878 new_charset and 

879 ( 

880 content_type == 'text/html' or 

881 _content_type_has_charset(content_type) 

882 ) 

883 ): 

884 content_type += '; charset=' + new_charset 

885 

886 self.headers['Content-Type'] = content_type 

887 

888 def _content_type__del(self): 

889 self.headers.pop('Content-Type', None) 

890 

891 content_type = property(_content_type__get, _content_type__set, 

892 _content_type__del, doc=_content_type__get.__doc__) 

893 

894 # 

895 # content_type_params 

896 # 

897 

898 def _content_type_params__get(self): 

899 """ 

900 A dictionary of all the parameters in the content type. 

901 

902 (This is not a view, set to change, modifications of the dict will not 

903 be applied otherwise.) 

904 """ 

905 params = self.headers.get('Content-Type', '') 

906 if ';' not in params: 

907 return {} 

908 params = params.split(';', 1)[1] 

909 result = {} 

910 for match in _PARAM_RE.finditer(params): 

911 result[match.group(1)] = match.group(2) or match.group(3) or '' 

912 return result 

913 

914 def _content_type_params__set(self, value_dict): 

915 if not value_dict: 

916 self._content_type_params__del() 

917 return 

918 

919 params = [] 

920 for k, v in sorted(value_dict.items()): 

921 if not _OK_PARAM_RE.search(v): 

922 v = '"%s"' % v.replace('"', '\\"') 

923 params.append('; %s=%s' % (k, v)) 

924 ct = self.headers.pop('Content-Type', '').split(';', 1)[0] 

925 ct += ''.join(params) 

926 self.headers['Content-Type'] = ct 

927 

928 def _content_type_params__del(self): 

929 self.headers['Content-Type'] = self.headers.get( 

930 'Content-Type', '').split(';', 1)[0] 

931 

932 content_type_params = property( 

933 _content_type_params__get, 

934 _content_type_params__set, 

935 _content_type_params__del, 

936 _content_type_params__get.__doc__ 

937 ) 

938 

939 # 

940 # set_cookie, unset_cookie, delete_cookie, merge_cookies 

941 # 

942 

943 def set_cookie(self, name, value='', max_age=None, 

944 path='/', domain=None, secure=False, httponly=False, 

945 comment=None, expires=None, overwrite=False, 

946 samesite=None): 

947 """ 

948 Set (add) a cookie for the response. 

949 

950 Arguments are: 

951 

952 ``name`` 

953 

954 The cookie name. 

955 

956 ``value`` 

957 

958 The cookie value, which should be a string or ``None``. If 

959 ``value`` is ``None``, it's equivalent to calling the 

960 :meth:`webob.response.Response.unset_cookie` method for this 

961 cookie key (it effectively deletes the cookie on the client). 

962 

963 ``max_age`` 

964 

965 An integer representing a number of seconds, ``datetime.timedelta``, 

966 or ``None``. This value is used as the ``Max-Age`` of the generated 

967 cookie. If ``expires`` is not passed and this value is not 

968 ``None``, the ``max_age`` value will also influence the ``Expires`` 

969 value of the cookie (``Expires`` will be set to ``now`` + 

970 ``max_age``). If this value is ``None``, the cookie will not have a 

971 ``Max-Age`` value (unless ``expires`` is set). If both ``max_age`` 

972 and ``expires`` are set, this value takes precedence. 

973 

974 ``path`` 

975 

976 A string representing the cookie ``Path`` value. It defaults to 

977 ``/``. 

978 

979 ``domain`` 

980 

981 A string representing the cookie ``Domain``, or ``None``. If 

982 domain is ``None``, no ``Domain`` value will be sent in the 

983 cookie. 

984 

985 ``secure`` 

986 

987 A boolean. If it's ``True``, the ``secure`` flag will be sent in 

988 the cookie, if it's ``False``, the ``secure`` flag will not be 

989 sent in the cookie. 

990 

991 ``httponly`` 

992 

993 A boolean. If it's ``True``, the ``HttpOnly`` flag will be sent 

994 in the cookie, if it's ``False``, the ``HttpOnly`` flag will not 

995 be sent in the cookie. 

996 

997 ``samesite`` 

998 

999 A string representing the ``SameSite`` attribute of the cookie or 

1000 ``None``. If samesite is ``None`` no ``SameSite`` value will be sent 

1001 in the cookie. Should only be ``"strict"``, ``"lax"``, or ``"none"``. 

1002 

1003 ``comment`` 

1004 

1005 A string representing the cookie ``Comment`` value, or ``None``. 

1006 If ``comment`` is ``None``, no ``Comment`` value will be sent in 

1007 the cookie. 

1008 

1009 ``expires`` 

1010 

1011 A ``datetime.timedelta`` object representing an amount of time, 

1012 ``datetime.datetime`` or ``None``. A non-``None`` value is used to 

1013 generate the ``Expires`` value of the generated cookie. If 

1014 ``max_age`` is not passed, but this value is not ``None``, it will 

1015 influence the ``Max-Age`` header. If this value is ``None``, the 

1016 ``Expires`` cookie value will be unset (unless ``max_age`` is set). 

1017 If ``max_age`` is set, it will be used to generate the ``expires`` 

1018 and this value is ignored. 

1019 

1020 If a ``datetime.datetime`` is provided it has to either be timezone 

1021 aware or be based on UTC. ``datetime.datetime`` objects that are 

1022 local time are not supported. Timezone aware ``datetime.datetime`` 

1023 objects are converted to UTC. 

1024 

1025 This argument will be removed in future versions of WebOb (version 

1026 1.9). 

1027 

1028 ``overwrite`` 

1029 

1030 If this key is ``True``, before setting the cookie, unset any 

1031 existing cookie. 

1032 

1033 """ 

1034 

1035 # Remove in WebOb 1.10 

1036 if expires: 

1037 warn_deprecation('Argument "expires" will be removed in a future ' 

1038 'version of WebOb, please use "max_age".', 1.10, 1) 

1039 

1040 if overwrite: 

1041 self.unset_cookie(name, strict=False) 

1042 

1043 # If expires is set, but not max_age we set max_age to expires 

1044 if not max_age and isinstance(expires, timedelta): 

1045 max_age = expires 

1046 

1047 # expires can also be a datetime 

1048 if not max_age and isinstance(expires, datetime): 

1049 

1050 # If expires has a timezone attached, convert it to UTC 

1051 if expires.tzinfo and expires.utcoffset(): 

1052 expires = (expires - expires.utcoffset()).replace(tzinfo=None) 

1053 

1054 max_age = expires - datetime.utcnow() 

1055 

1056 value = bytes_(value, 'utf-8') 

1057 

1058 cookie = make_cookie(name, value, max_age=max_age, path=path, 

1059 domain=domain, secure=secure, httponly=httponly, 

1060 comment=comment, samesite=samesite) 

1061 self.headerlist.append(('Set-Cookie', cookie)) 

1062 

1063 def delete_cookie(self, name, path='/', domain=None): 

1064 """ 

1065 Delete a cookie from the client. Note that ``path`` and ``domain`` 

1066 must match how the cookie was originally set. 

1067 

1068 This sets the cookie to the empty string, and ``max_age=0`` so 

1069 that it should expire immediately. 

1070 """ 

1071 self.set_cookie(name, None, path=path, domain=domain) 

1072 

1073 def unset_cookie(self, name, strict=True): 

1074 """ 

1075 Unset a cookie with the given name (remove it from the response). 

1076 """ 

1077 existing = self.headers.getall('Set-Cookie') 

1078 if not existing and not strict: 

1079 return 

1080 cookies = Cookie() 

1081 for header in existing: 

1082 cookies.load(header) 

1083 if isinstance(name, text_type): 

1084 name = name.encode('utf8') 

1085 if name in cookies: 

1086 del cookies[name] 

1087 del self.headers['Set-Cookie'] 

1088 for m in cookies.values(): 

1089 self.headerlist.append(('Set-Cookie', m.serialize())) 

1090 elif strict: 

1091 raise KeyError("No cookie has been set with the name %r" % name) 

1092 

1093 def merge_cookies(self, resp): 

1094 """Merge the cookies that were set on this response with the 

1095 given ``resp`` object (which can be any WSGI application). 

1096 

1097 If the ``resp`` is a :class:`webob.Response` object, then the 

1098 other object will be modified in-place. 

1099 """ 

1100 if not self.headers.get('Set-Cookie'): 

1101 return resp 

1102 if isinstance(resp, Response): 

1103 for header in self.headers.getall('Set-Cookie'): 

1104 resp.headers.add('Set-Cookie', header) 

1105 return resp 

1106 else: 

1107 c_headers = [h for h in self.headerlist if 

1108 h[0].lower() == 'set-cookie'] 

1109 def repl_app(environ, start_response): 

1110 def repl_start_response(status, headers, exc_info=None): 

1111 return start_response(status, headers + c_headers, 

1112 exc_info=exc_info) 

1113 return resp(environ, repl_start_response) 

1114 return repl_app 

1115 

1116 # 

1117 # cache_control 

1118 # 

1119 

1120 _cache_control_obj = None 

1121 

1122 def _cache_control__get(self): 

1123 """ 

1124 Get/set/modify the Cache-Control header (`HTTP spec section 14.9 

1125 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_). 

1126 """ 

1127 value = self.headers.get('cache-control', '') 

1128 if self._cache_control_obj is None: 

1129 self._cache_control_obj = CacheControl.parse( 

1130 value, updates_to=self._update_cache_control, type='response') 

1131 self._cache_control_obj.header_value = value 

1132 if self._cache_control_obj.header_value != value: 

1133 new_obj = CacheControl.parse(value, type='response') 

1134 self._cache_control_obj.properties.clear() 

1135 self._cache_control_obj.properties.update(new_obj.properties) 

1136 self._cache_control_obj.header_value = value 

1137 return self._cache_control_obj 

1138 

1139 def _cache_control__set(self, value): 

1140 # This actually becomes a copy 

1141 if not value: 

1142 value = "" 

1143 if isinstance(value, dict): 

1144 value = CacheControl(value, 'response') 

1145 if isinstance(value, text_type): 

1146 value = str(value) 

1147 if isinstance(value, str): 

1148 if self._cache_control_obj is None: 

1149 self.headers['Cache-Control'] = value 

1150 return 

1151 value = CacheControl.parse(value, 'response') 

1152 cache = self.cache_control 

1153 cache.properties.clear() 

1154 cache.properties.update(value.properties) 

1155 

1156 def _cache_control__del(self): 

1157 self.cache_control = {} 

1158 

1159 def _update_cache_control(self, prop_dict): 

1160 value = serialize_cache_control(prop_dict) 

1161 if not value: 

1162 if 'Cache-Control' in self.headers: 

1163 del self.headers['Cache-Control'] 

1164 else: 

1165 self.headers['Cache-Control'] = value 

1166 

1167 cache_control = property( 

1168 _cache_control__get, _cache_control__set, 

1169 _cache_control__del, doc=_cache_control__get.__doc__) 

1170 

1171 # 

1172 # cache_expires 

1173 # 

1174 

1175 def _cache_expires(self, seconds=0, **kw): 

1176 """ 

1177 Set expiration on this request. This sets the response to 

1178 expire in the given seconds, and any other attributes are used 

1179 for ``cache_control`` (e.g., ``private=True``). 

1180 """ 

1181 if seconds is True: 

1182 seconds = 0 

1183 elif isinstance(seconds, timedelta): 

1184 seconds = timedelta_to_seconds(seconds) 

1185 cache_control = self.cache_control 

1186 if seconds is None: 

1187 pass 

1188 elif not seconds: 

1189 # To really expire something, you have to force a 

1190 # bunch of these cache control attributes, and IE may 

1191 # not pay attention to those still so we also set 

1192 # Expires. 

1193 cache_control.no_store = True 

1194 cache_control.no_cache = True 

1195 cache_control.must_revalidate = True 

1196 cache_control.max_age = 0 

1197 cache_control.post_check = 0 

1198 cache_control.pre_check = 0 

1199 self.expires = datetime.utcnow() 

1200 if 'last-modified' not in self.headers: 

1201 self.last_modified = datetime.utcnow() 

1202 self.pragma = 'no-cache' 

1203 else: 

1204 cache_control.properties.clear() 

1205 cache_control.max_age = seconds 

1206 self.expires = datetime.utcnow() + timedelta(seconds=seconds) 

1207 self.pragma = None 

1208 for name, value in kw.items(): 

1209 setattr(cache_control, name, value) 

1210 

1211 cache_expires = property(lambda self: self._cache_expires, _cache_expires) 

1212 

1213 # 

1214 # encode_content, decode_content, md5_etag 

1215 # 

1216 

1217 def encode_content(self, encoding='gzip', lazy=False): 

1218 """ 

1219 Encode the content with the given encoding (only ``gzip`` and 

1220 ``identity`` are supported). 

1221 """ 

1222 assert encoding in ('identity', 'gzip'), \ 

1223 "Unknown encoding: %r" % encoding 

1224 if encoding == 'identity': 

1225 self.decode_content() 

1226 return 

1227 if self.content_encoding == 'gzip': 

1228 return 

1229 if lazy: 

1230 self.app_iter = gzip_app_iter(self._app_iter) 

1231 self.content_length = None 

1232 else: 

1233 self.app_iter = list(gzip_app_iter(self._app_iter)) 

1234 self.content_length = sum(map(len, self._app_iter)) 

1235 self.content_encoding = 'gzip' 

1236 

1237 def decode_content(self): 

1238 content_encoding = self.content_encoding or 'identity' 

1239 if content_encoding == 'identity': 

1240 return 

1241 if content_encoding not in ('gzip', 'deflate'): 

1242 raise ValueError( 

1243 "I don't know how to decode the content %s" % content_encoding) 

1244 if content_encoding == 'gzip': 

1245 from gzip import GzipFile 

1246 from io import BytesIO 

1247 gzip_f = GzipFile(filename='', mode='r', fileobj=BytesIO(self.body)) 

1248 self.body = gzip_f.read() 

1249 self.content_encoding = None 

1250 gzip_f.close() 

1251 else: 

1252 # Weird feature: http://bugs.python.org/issue5784 

1253 self.body = zlib.decompress(self.body, -15) 

1254 self.content_encoding = None 

1255 

1256 def md5_etag(self, body=None, set_content_md5=False): 

1257 """ 

1258 Generate an etag for the response object using an MD5 hash of 

1259 the body (the ``body`` parameter, or ``self.body`` if not given). 

1260 

1261 Sets ``self.etag``. 

1262 

1263 If ``set_content_md5`` is ``True``, sets ``self.content_md5`` as well. 

1264 """ 

1265 if body is None: 

1266 body = self.body 

1267 md5_digest = md5(body).digest() 

1268 md5_digest = b64encode(md5_digest) 

1269 md5_digest = md5_digest.replace(b'\n', b'') 

1270 md5_digest = native_(md5_digest) 

1271 self.etag = md5_digest.strip('=') 

1272 if set_content_md5: 

1273 self.content_md5 = md5_digest 

1274 

1275 @staticmethod 

1276 def _make_location_absolute(environ, value): 

1277 if SCHEME_RE.search(value): 

1278 return value 

1279 

1280 new_location = urlparse.urljoin(_request_uri(environ), value) 

1281 return new_location 

1282 

1283 def _abs_headerlist(self, environ): 

1284 # Build the headerlist, if we have a Location header, make it absolute 

1285 return [ 

1286 (k, v) if k.lower() != 'location' 

1287 else (k, self._make_location_absolute(environ, v)) 

1288 for (k, v) 

1289 in self._headerlist 

1290 ] 

1291 

1292 # 

1293 # __call__, conditional_response_app 

1294 # 

1295 

1296 def __call__(self, environ, start_response): 

1297 """ 

1298 WSGI application interface 

1299 """ 

1300 if self.conditional_response: 

1301 return self.conditional_response_app(environ, start_response) 

1302 

1303 headerlist = self._abs_headerlist(environ) 

1304 

1305 start_response(self.status, headerlist) 

1306 if environ['REQUEST_METHOD'] == 'HEAD': 

1307 # Special case here... 

1308 return EmptyResponse(self._app_iter) 

1309 return self._app_iter 

1310 

1311 _safe_methods = ('GET', 'HEAD') 

1312 

1313 def conditional_response_app(self, environ, start_response): 

1314 """ 

1315 Like the normal ``__call__`` interface, but checks conditional headers: 

1316 

1317 * ``If-Modified-Since`` (``304 Not Modified``; only on ``GET``, 

1318 ``HEAD``) 

1319 * ``If-None-Match`` (``304 Not Modified``; only on ``GET``, 

1320 ``HEAD``) 

1321 * ``Range`` (``406 Partial Content``; only on ``GET``, 

1322 ``HEAD``) 

1323 """ 

1324 req = BaseRequest(environ) 

1325 

1326 headerlist = self._abs_headerlist(environ) 

1327 

1328 method = environ.get('REQUEST_METHOD', 'GET') 

1329 if method in self._safe_methods: 

1330 status304 = False 

1331 if req.if_none_match and self.etag: 

1332 status304 = self.etag in req.if_none_match 

1333 elif req.if_modified_since and self.last_modified: 

1334 status304 = self.last_modified <= req.if_modified_since 

1335 if status304: 

1336 start_response('304 Not Modified', filter_headers(headerlist)) 

1337 return EmptyResponse(self._app_iter) 

1338 if ( 

1339 req.range and self in req.if_range and 

1340 self.content_range is None and 

1341 method in ('HEAD', 'GET') and 

1342 self.status_code == 200 and 

1343 self.content_length is not None 

1344 ): 

1345 content_range = req.range.content_range(self.content_length) 

1346 if content_range is None: 

1347 iter_close(self._app_iter) 

1348 body = bytes_("Requested range not satisfiable: %s" % req.range) 

1349 headerlist = [ 

1350 ('Content-Length', str(len(body))), 

1351 ('Content-Range', str(ContentRange(None, None, 

1352 self.content_length))), 

1353 ('Content-Type', 'text/plain'), 

1354 ] + filter_headers(headerlist) 

1355 start_response('416 Requested Range Not Satisfiable', 

1356 headerlist) 

1357 if method == 'HEAD': 

1358 return () 

1359 return [body] 

1360 else: 

1361 app_iter = self.app_iter_range(content_range.start, 

1362 content_range.stop) 

1363 if app_iter is not None: 

1364 # the following should be guaranteed by 

1365 # Range.range_for_length(length) 

1366 assert content_range.start is not None 

1367 headerlist = [ 

1368 ('Content-Length', 

1369 str(content_range.stop - content_range.start)), 

1370 ('Content-Range', str(content_range)), 

1371 ] + filter_headers(headerlist, ('content-length',)) 

1372 start_response('206 Partial Content', headerlist) 

1373 if method == 'HEAD': 

1374 return EmptyResponse(app_iter) 

1375 return app_iter 

1376 

1377 start_response(self.status, headerlist) 

1378 if method == 'HEAD': 

1379 return EmptyResponse(self._app_iter) 

1380 return self._app_iter 

1381 

1382 def app_iter_range(self, start, stop): 

1383 """ 

1384 Return a new ``app_iter`` built from the response ``app_iter``, that 

1385 serves up only the given ``start:stop`` range. 

1386 """ 

1387 app_iter = self._app_iter 

1388 if hasattr(app_iter, 'app_iter_range'): 

1389 return app_iter.app_iter_range(start, stop) 

1390 return AppIterRange(app_iter, start, stop) 

1391 

1392 

1393def filter_headers(hlist, remove_headers=('content-length', 'content-type')): 

1394 return [h for h in hlist if (h[0].lower() not in remove_headers)] 

1395 

1396 

1397def iter_file(file, block_size=1 << 18): # 256Kb 

1398 while True: 

1399 data = file.read(block_size) 

1400 if not data: 

1401 break 

1402 yield data 

1403 

1404class ResponseBodyFile(object): 

1405 mode = 'wb' 

1406 closed = False 

1407 

1408 def __init__(self, response): 

1409 """ 

1410 Represents a :class:`~Response` as a file like object. 

1411 """ 

1412 self.response = response 

1413 self.write = response.write 

1414 

1415 def __repr__(self): 

1416 return '<body_file for %r>' % self.response 

1417 

1418 encoding = property( 

1419 lambda self: self.response.charset, 

1420 doc="The encoding of the file (inherited from response.charset)" 

1421 ) 

1422 

1423 def writelines(self, seq): 

1424 """ 

1425 Write a sequence of lines to the response. 

1426 """ 

1427 for item in seq: 

1428 self.write(item) 

1429 

1430 def close(self): 

1431 raise NotImplementedError("Response bodies cannot be closed") 

1432 

1433 def flush(self): 

1434 pass 

1435 

1436 def tell(self): 

1437 """ 

1438 Provide the current location where we are going to start writing. 

1439 """ 

1440 if not self.response.has_body: 

1441 return 0 

1442 

1443 return sum([len(chunk) for chunk in self.response.app_iter]) 

1444 

1445 

1446class AppIterRange(object): 

1447 """ 

1448 Wraps an ``app_iter``, returning just a range of bytes. 

1449 """ 

1450 

1451 def __init__(self, app_iter, start, stop): 

1452 assert start >= 0, "Bad start: %r" % start 

1453 assert stop is None or (stop >= 0 and stop >= start), ( 

1454 "Bad stop: %r" % stop) 

1455 self.app_iter = iter(app_iter) 

1456 self._pos = 0 # position in app_iter 

1457 self.start = start 

1458 self.stop = stop 

1459 

1460 def __iter__(self): 

1461 return self 

1462 

1463 def _skip_start(self): 

1464 start, stop = self.start, self.stop 

1465 for chunk in self.app_iter: 

1466 self._pos += len(chunk) 

1467 if self._pos < start: 

1468 continue 

1469 elif self._pos == start: 

1470 return b'' 

1471 else: 

1472 chunk = chunk[start - self._pos:] 

1473 if stop is not None and self._pos > stop: 

1474 chunk = chunk[:stop - self._pos] 

1475 assert len(chunk) == stop - start 

1476 return chunk 

1477 else: 

1478 raise StopIteration() 

1479 

1480 def next(self): 

1481 if self._pos < self.start: 

1482 # need to skip some leading bytes 

1483 return self._skip_start() 

1484 stop = self.stop 

1485 if stop is not None and self._pos >= stop: 

1486 raise StopIteration 

1487 

1488 chunk = next(self.app_iter) 

1489 self._pos += len(chunk) 

1490 

1491 if stop is None or self._pos <= stop: 

1492 return chunk 

1493 else: 

1494 return chunk[:stop - self._pos] 

1495 

1496 __next__ = next # py3 

1497 

1498 def close(self): 

1499 iter_close(self.app_iter) 

1500 

1501 

1502class EmptyResponse(object): 

1503 """ 

1504 An empty WSGI response. 

1505 

1506 An iterator that immediately stops. Optionally provides a close 

1507 method to close an underlying ``app_iter`` it replaces. 

1508 """ 

1509 

1510 def __init__(self, app_iter=None): 

1511 if app_iter is not None and hasattr(app_iter, 'close'): 

1512 self.close = app_iter.close 

1513 

1514 def __iter__(self): 

1515 return self 

1516 

1517 def __len__(self): 

1518 return 0 

1519 

1520 def next(self): 

1521 raise StopIteration() 

1522 

1523 __next__ = next # py3 

1524 

1525def _is_xml(content_type): 

1526 return ( 

1527 content_type.startswith('application/xml') or 

1528 ( 

1529 content_type.startswith('application/') and 

1530 content_type.endswith('+xml') 

1531 ) or 

1532 ( 

1533 content_type.startswith('image/') and 

1534 content_type.endswith('+xml') 

1535 ) 

1536 ) 

1537 

1538def _content_type_has_charset(content_type): 

1539 return ( 

1540 content_type.startswith('text/') or 

1541 _is_xml(content_type) 

1542 ) 

1543 

1544def _request_uri(environ): 

1545 """Like ``wsgiref.url.request_uri``, except eliminates ``:80`` ports. 

1546 

1547 Returns the full request URI.""" 

1548 url = environ['wsgi.url_scheme'] + '://' 

1549 

1550 if environ.get('HTTP_HOST'): 

1551 url += environ['HTTP_HOST'] 

1552 else: 

1553 url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT'] 

1554 if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http': 

1555 url = url[:-3] 

1556 elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https': 

1557 url = url[:-4] 

1558 

1559 if PY2: 

1560 script_name = environ.get('SCRIPT_NAME', '/') 

1561 path_info = environ.get('PATH_INFO', '') 

1562 else: 

1563 script_name = bytes_(environ.get('SCRIPT_NAME', '/'), 'latin-1') 

1564 path_info = bytes_(environ.get('PATH_INFO', ''), 'latin-1') 

1565 

1566 url += url_quote(script_name) 

1567 qpath_info = url_quote(path_info) 

1568 if 'SCRIPT_NAME' not in environ: 

1569 url += qpath_info[1:] 

1570 else: 

1571 url += qpath_info 

1572 return url 

1573 

1574 

1575def iter_close(iter): 

1576 if hasattr(iter, 'close'): 

1577 iter.close() 

1578 

1579def gzip_app_iter(app_iter): 

1580 size = 0 

1581 crc = zlib.crc32(b"") & 0xffffffff 

1582 compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS, 

1583 zlib.DEF_MEM_LEVEL, 0) 

1584 

1585 yield _gzip_header 

1586 for item in app_iter: 

1587 size += len(item) 

1588 crc = zlib.crc32(item, crc) & 0xffffffff 

1589 

1590 # The compress function may return zero length bytes if the input is 

1591 # small enough; it buffers the input for the next iteration or for a 

1592 # flush. 

1593 result = compress.compress(item) 

1594 if result: 

1595 yield result 

1596 

1597 # Similarly, flush may also not yield a value. 

1598 result = compress.flush() 

1599 if result: 

1600 yield result 

1601 yield struct.pack("<2L", crc, size & 0xffffffff) 

1602 

1603def _error_unicode_in_app_iter(app_iter, body): 

1604 app_iter_repr = repr(app_iter) 

1605 if len(app_iter_repr) > 50: 

1606 app_iter_repr = ( 

1607 app_iter_repr[:30] + '...' + app_iter_repr[-10:]) 

1608 raise TypeError( 

1609 'An item of the app_iter (%s) was text, causing a ' 

1610 'text body: %r' % (app_iter_repr, body))