1# Copyright 2017 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Support for downloading media from Google APIs."""
16
17import http
18
19import urllib3.response # type: ignore
20
21from google.resumable_media import _download, _helpers, common
22from google.resumable_media.requests import _request_helpers
23
24_CHECKSUM_MISMATCH = """\
25Checksum mismatch while downloading:
26
27 {}
28
29The X-Goog-Hash header indicated an {checksum_type} checksum of:
30
31 {}
32
33but the actual {checksum_type} checksum of the downloaded contents was:
34
35 {}
36"""
37
38_STREAM_SEEK_ERROR = """\
39Incomplete download for:
40{}
41Error writing to stream while handling a gzip-compressed file download.
42Please restart the download.
43"""
44
45_RESPONSE_HEADERS_INFO = """\
46
47The X-Goog-Stored-Content-Length is {}. The X-Goog-Stored-Content-Encoding is {}.
48
49The download request read {} bytes of data.
50If the download was incomplete, please check the network connection and restart the download.
51"""
52
53
54class Download(_request_helpers.RequestsMixin, _download.Download):
55 """Helper to manage downloading a resource from a Google API.
56
57 "Slices" of the resource can be retrieved by specifying a range
58 with ``start`` and / or ``end``. However, in typical usage, neither
59 ``start`` nor ``end`` is expected to be provided.
60
61 Args:
62 media_url (str): The URL containing the media to be downloaded.
63 stream (IO[bytes]): A write-able stream (i.e. file-like object) that
64 the downloaded resource can be written to.
65 start (int): The first byte in a range to be downloaded. If not
66 provided, but ``end`` is provided, will download from the
67 beginning to ``end`` of the media.
68 end (int): The last byte in a range to be downloaded. If not
69 provided, but ``start`` is provided, will download from the
70 ``start`` to the end of the media.
71 headers (Optional[Mapping[str, str]]): Extra headers that should
72 be sent with the request, e.g. headers for encrypted data.
73 checksum Optional([str]): The type of checksum to compute to verify
74 the integrity of the object. The response headers must contain
75 a checksum of the requested type. If the headers lack an
76 appropriate checksum (for instance in the case of transcoded or
77 ranged downloads where the remote service does not know the
78 correct checksum) an INFO-level log will be emitted. Supported
79 values are "md5", "crc32c" and None. The default is "md5".
80
81 Attributes:
82 media_url (str): The URL containing the media to be downloaded.
83 start (Optional[int]): The first byte in a range to be downloaded.
84 end (Optional[int]): The last byte in a range to be downloaded.
85 """
86
87 def _write_to_stream(self, response):
88 """Write response body to a write-able stream.
89
90 .. note:
91
92 This method assumes that the ``_stream`` attribute is set on the
93 current download.
94
95 Args:
96 response (~requests.Response): The HTTP response object.
97
98 Raises:
99 ~google.resumable_media.common.DataCorruption: If the download's
100 checksum doesn't agree with server-computed checksum.
101 """
102
103 # Retrieve the expected checksum only once for the download request,
104 # then compute and validate the checksum when the full download completes.
105 # Retried requests are range requests, and there's no way to detect
106 # data corruption for that byte range alone.
107 if self._expected_checksum is None and self._checksum_object is None:
108 # `_get_expected_checksum()` may return None even if a checksum was
109 # requested, in which case it will emit an info log _MISSING_CHECKSUM.
110 # If an invalid checksum type is specified, this will raise ValueError.
111 expected_checksum, checksum_object = _helpers._get_expected_checksum(
112 response, self._get_headers, self.media_url, checksum_type=self.checksum
113 )
114 self._expected_checksum = expected_checksum
115 self._checksum_object = checksum_object
116 else:
117 expected_checksum = self._expected_checksum
118 checksum_object = self._checksum_object
119
120 with response:
121 # NOTE: In order to handle compressed streams gracefully, we try
122 # to insert our checksum object into the decompression stream. If
123 # the stream is indeed compressed, this will delegate the checksum
124 # object to the decoder and return a _DoNothingHash here.
125 local_checksum_object = _add_decoder(response.raw, checksum_object)
126 body_iter = response.iter_content(
127 chunk_size=_request_helpers._SINGLE_GET_CHUNK_SIZE, decode_unicode=False
128 )
129 for chunk in body_iter:
130 self._stream.write(chunk)
131 self._bytes_downloaded += len(chunk)
132 local_checksum_object.update(chunk)
133
134 # Don't validate the checksum for partial responses.
135 if (
136 expected_checksum is not None
137 and response.status_code != http.client.PARTIAL_CONTENT
138 ):
139 actual_checksum = _helpers.prepare_checksum_digest(checksum_object.digest())
140
141 if actual_checksum != expected_checksum:
142 headers = self._get_headers(response)
143 x_goog_encoding = headers.get("x-goog-stored-content-encoding")
144 x_goog_length = headers.get("x-goog-stored-content-length")
145 content_length_msg = _RESPONSE_HEADERS_INFO.format(
146 x_goog_length, x_goog_encoding, self._bytes_downloaded
147 )
148 if (
149 x_goog_length
150 and self._bytes_downloaded < int(x_goog_length)
151 and x_goog_encoding != "gzip"
152 ):
153 # The library will attempt to trigger a retry by raising a ConnectionError, if
154 # (a) bytes_downloaded is less than response header x-goog-stored-content-length, and
155 # (b) the object is not gzip-compressed when stored in Cloud Storage.
156 raise ConnectionError(content_length_msg)
157 else:
158 msg = _CHECKSUM_MISMATCH.format(
159 self.media_url,
160 expected_checksum,
161 actual_checksum,
162 checksum_type=self.checksum.upper(),
163 )
164 msg += content_length_msg
165 raise common.DataCorruption(response, msg)
166
167 def consume(
168 self,
169 transport,
170 timeout=(
171 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
172 _request_helpers._DEFAULT_READ_TIMEOUT,
173 ),
174 ):
175 """Consume the resource to be downloaded.
176
177 If a ``stream`` is attached to this download, then the downloaded
178 resource will be written to the stream.
179
180 Args:
181 transport (~requests.Session): A ``requests`` object which can
182 make authenticated requests.
183 timeout (Optional[Union[float, Tuple[float, float]]]):
184 The number of seconds to wait for the server response.
185 Depending on the retry strategy, a request may be repeated
186 several times using the same timeout each time.
187
188 Can also be passed as a tuple (connect_timeout, read_timeout).
189 See :meth:`requests.Session.request` documentation for details.
190
191 Returns:
192 ~requests.Response: The HTTP response returned by ``transport``.
193
194 Raises:
195 ~google.resumable_media.common.DataCorruption: If the download's
196 checksum doesn't agree with server-computed checksum.
197 ValueError: If the current :class:`Download` has already
198 finished.
199 """
200 method, _, payload, headers = self._prepare_request()
201 # NOTE: We assume "payload is None" but pass it along anyway.
202 request_kwargs = {
203 "data": payload,
204 "headers": headers,
205 "timeout": timeout,
206 }
207 if self._stream is not None:
208 request_kwargs["stream"] = True
209
210 # Assign object generation if generation is specified in the media url.
211 if self._object_generation is None:
212 self._object_generation = _helpers._get_generation_from_url(self.media_url)
213
214 # Wrap the request business logic in a function to be retried.
215 def retriable_request():
216 url = self.media_url
217
218 # To restart an interrupted download, read from the offset of last byte
219 # received using a range request, and set object generation query param.
220 if self._bytes_downloaded > 0:
221 _download.add_bytes_range(
222 (self.start or 0) + self._bytes_downloaded, self.end, self._headers
223 )
224 request_kwargs["headers"] = self._headers
225
226 # Set object generation query param to ensure the same object content is requested.
227 if (
228 self._object_generation is not None
229 and _helpers._get_generation_from_url(self.media_url) is None
230 ):
231 query_param = {"generation": self._object_generation}
232 url = _helpers.add_query_parameters(self.media_url, query_param)
233
234 result = transport.request(method, url, **request_kwargs)
235
236 # If a generation hasn't been specified, and this is the first response we get, let's record the
237 # generation. In future requests we'll specify the generation query param to avoid data races.
238 if self._object_generation is None:
239 self._object_generation = _helpers._parse_generation_header(
240 result, self._get_headers
241 )
242
243 self._process_response(result)
244
245 # With decompressive transcoding, GCS serves back the whole file regardless of the range request,
246 # thus we reset the stream position to the start of the stream.
247 # See: https://cloud.google.com/storage/docs/transcoding#range
248 if self._stream is not None:
249 if _helpers._is_decompressive_transcoding(result, self._get_headers):
250 try:
251 self._stream.seek(0)
252 except Exception as exc:
253 msg = _STREAM_SEEK_ERROR.format(url)
254 raise Exception(msg) from exc
255 self._bytes_downloaded = 0
256
257 self._write_to_stream(result)
258
259 return result
260
261 return _request_helpers.wait_and_retry(
262 retriable_request, self._get_status_code, self._retry_strategy
263 )
264
265
266class RawDownload(_request_helpers.RawRequestsMixin, _download.Download):
267 """Helper to manage downloading a raw resource from a Google API.
268
269 "Slices" of the resource can be retrieved by specifying a range
270 with ``start`` and / or ``end``. However, in typical usage, neither
271 ``start`` nor ``end`` is expected to be provided.
272
273 Args:
274 media_url (str): The URL containing the media to be downloaded.
275 stream (IO[bytes]): A write-able stream (i.e. file-like object) that
276 the downloaded resource can be written to.
277 start (int): The first byte in a range to be downloaded. If not
278 provided, but ``end`` is provided, will download from the
279 beginning to ``end`` of the media.
280 end (int): The last byte in a range to be downloaded. If not
281 provided, but ``start`` is provided, will download from the
282 ``start`` to the end of the media.
283 headers (Optional[Mapping[str, str]]): Extra headers that should
284 be sent with the request, e.g. headers for encrypted data.
285 checksum Optional([str]): The type of checksum to compute to verify
286 the integrity of the object. The response headers must contain
287 a checksum of the requested type. If the headers lack an
288 appropriate checksum (for instance in the case of transcoded or
289 ranged downloads where the remote service does not know the
290 correct checksum) an INFO-level log will be emitted. Supported
291 values are "md5", "crc32c" and None. The default is "md5".
292 Attributes:
293 media_url (str): The URL containing the media to be downloaded.
294 start (Optional[int]): The first byte in a range to be downloaded.
295 end (Optional[int]): The last byte in a range to be downloaded.
296 """
297
298 def _write_to_stream(self, response):
299 """Write response body to a write-able stream.
300
301 .. note:
302
303 This method assumes that the ``_stream`` attribute is set on the
304 current download.
305
306 Args:
307 response (~requests.Response): The HTTP response object.
308
309 Raises:
310 ~google.resumable_media.common.DataCorruption: If the download's
311 checksum doesn't agree with server-computed checksum.
312 """
313 # Retrieve the expected checksum only once for the download request,
314 # then compute and validate the checksum when the full download completes.
315 # Retried requests are range requests, and there's no way to detect
316 # data corruption for that byte range alone.
317 if self._expected_checksum is None and self._checksum_object is None:
318 # `_get_expected_checksum()` may return None even if a checksum was
319 # requested, in which case it will emit an info log _MISSING_CHECKSUM.
320 # If an invalid checksum type is specified, this will raise ValueError.
321 expected_checksum, checksum_object = _helpers._get_expected_checksum(
322 response, self._get_headers, self.media_url, checksum_type=self.checksum
323 )
324 self._expected_checksum = expected_checksum
325 self._checksum_object = checksum_object
326 else:
327 expected_checksum = self._expected_checksum
328 checksum_object = self._checksum_object
329
330 with response:
331 body_iter = response.raw.stream(
332 _request_helpers._SINGLE_GET_CHUNK_SIZE, decode_content=False
333 )
334 for chunk in body_iter:
335 self._stream.write(chunk)
336 self._bytes_downloaded += len(chunk)
337 checksum_object.update(chunk)
338 response._content_consumed = True
339
340 # Don't validate the checksum for partial responses.
341 if (
342 expected_checksum is not None
343 and response.status_code != http.client.PARTIAL_CONTENT
344 ):
345 actual_checksum = _helpers.prepare_checksum_digest(checksum_object.digest())
346
347 if actual_checksum != expected_checksum:
348 headers = self._get_headers(response)
349 x_goog_encoding = headers.get("x-goog-stored-content-encoding")
350 x_goog_length = headers.get("x-goog-stored-content-length")
351 content_length_msg = _RESPONSE_HEADERS_INFO.format(
352 x_goog_length, x_goog_encoding, self._bytes_downloaded
353 )
354 if (
355 x_goog_length
356 and self._bytes_downloaded < int(x_goog_length)
357 and x_goog_encoding != "gzip"
358 ):
359 # The library will attempt to trigger a retry by raising a ConnectionError, if
360 # (a) bytes_downloaded is less than response header x-goog-stored-content-length, and
361 # (b) the object is not gzip-compressed when stored in Cloud Storage.
362 raise ConnectionError(content_length_msg)
363 else:
364 msg = _CHECKSUM_MISMATCH.format(
365 self.media_url,
366 expected_checksum,
367 actual_checksum,
368 checksum_type=self.checksum.upper(),
369 )
370 msg += content_length_msg
371 raise common.DataCorruption(response, msg)
372
373 def consume(
374 self,
375 transport,
376 timeout=(
377 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
378 _request_helpers._DEFAULT_READ_TIMEOUT,
379 ),
380 ):
381 """Consume the resource to be downloaded.
382
383 If a ``stream`` is attached to this download, then the downloaded
384 resource will be written to the stream.
385
386 Args:
387 transport (~requests.Session): A ``requests`` object which can
388 make authenticated requests.
389 timeout (Optional[Union[float, Tuple[float, float]]]):
390 The number of seconds to wait for the server response.
391 Depending on the retry strategy, a request may be repeated
392 several times using the same timeout each time.
393
394 Can also be passed as a tuple (connect_timeout, read_timeout).
395 See :meth:`requests.Session.request` documentation for details.
396
397 Returns:
398 ~requests.Response: The HTTP response returned by ``transport``.
399
400 Raises:
401 ~google.resumable_media.common.DataCorruption: If the download's
402 checksum doesn't agree with server-computed checksum.
403 ValueError: If the current :class:`Download` has already
404 finished.
405 """
406 method, _, payload, headers = self._prepare_request()
407 # NOTE: We assume "payload is None" but pass it along anyway.
408 request_kwargs = {
409 "data": payload,
410 "headers": headers,
411 "timeout": timeout,
412 "stream": True,
413 }
414
415 # Assign object generation if generation is specified in the media url.
416 if self._object_generation is None:
417 self._object_generation = _helpers._get_generation_from_url(self.media_url)
418
419 # Wrap the request business logic in a function to be retried.
420 def retriable_request():
421 url = self.media_url
422
423 # To restart an interrupted download, read from the offset of last byte
424 # received using a range request, and set object generation query param.
425 if self._bytes_downloaded > 0:
426 _download.add_bytes_range(
427 (self.start or 0) + self._bytes_downloaded, self.end, self._headers
428 )
429 request_kwargs["headers"] = self._headers
430
431 # Set object generation query param to ensure the same object content is requested.
432 if (
433 self._object_generation is not None
434 and _helpers._get_generation_from_url(self.media_url) is None
435 ):
436 query_param = {"generation": self._object_generation}
437 url = _helpers.add_query_parameters(self.media_url, query_param)
438
439 result = transport.request(method, url, **request_kwargs)
440
441 # If a generation hasn't been specified, and this is the first response we get, let's record the
442 # generation. In future requests we'll specify the generation query param to avoid data races.
443 if self._object_generation is None:
444 self._object_generation = _helpers._parse_generation_header(
445 result, self._get_headers
446 )
447
448 self._process_response(result)
449
450 # With decompressive transcoding, GCS serves back the whole file regardless of the range request,
451 # thus we reset the stream position to the start of the stream.
452 # See: https://cloud.google.com/storage/docs/transcoding#range
453 if self._stream is not None:
454 if _helpers._is_decompressive_transcoding(result, self._get_headers):
455 try:
456 self._stream.seek(0)
457 except Exception as exc:
458 msg = _STREAM_SEEK_ERROR.format(url)
459 raise Exception(msg) from exc
460 self._bytes_downloaded = 0
461
462 self._write_to_stream(result)
463
464 return result
465
466 return _request_helpers.wait_and_retry(
467 retriable_request, self._get_status_code, self._retry_strategy
468 )
469
470
471class ChunkedDownload(_request_helpers.RequestsMixin, _download.ChunkedDownload):
472 """Download a resource in chunks from a Google API.
473
474 Args:
475 media_url (str): The URL containing the media to be downloaded.
476 chunk_size (int): The number of bytes to be retrieved in each
477 request.
478 stream (IO[bytes]): A write-able stream (i.e. file-like object) that
479 will be used to concatenate chunks of the resource as they are
480 downloaded.
481 start (int): The first byte in a range to be downloaded. If not
482 provided, defaults to ``0``.
483 end (int): The last byte in a range to be downloaded. If not
484 provided, will download to the end of the media.
485 headers (Optional[Mapping[str, str]]): Extra headers that should
486 be sent with each request, e.g. headers for data encryption
487 key headers.
488
489 Attributes:
490 media_url (str): The URL containing the media to be downloaded.
491 start (Optional[int]): The first byte in a range to be downloaded.
492 end (Optional[int]): The last byte in a range to be downloaded.
493 chunk_size (int): The number of bytes to be retrieved in each request.
494
495 Raises:
496 ValueError: If ``start`` is negative.
497 """
498
499 def consume_next_chunk(
500 self,
501 transport,
502 timeout=(
503 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
504 _request_helpers._DEFAULT_READ_TIMEOUT,
505 ),
506 ):
507 """Consume the next chunk of the resource to be downloaded.
508
509 Args:
510 transport (~requests.Session): A ``requests`` object which can
511 make authenticated requests.
512 timeout (Optional[Union[float, Tuple[float, float]]]):
513 The number of seconds to wait for the server response.
514 Depending on the retry strategy, a request may be repeated
515 several times using the same timeout each time.
516
517 Can also be passed as a tuple (connect_timeout, read_timeout).
518 See :meth:`requests.Session.request` documentation for details.
519
520 Returns:
521 ~requests.Response: The HTTP response returned by ``transport``.
522
523 Raises:
524 ValueError: If the current download has finished.
525 """
526 method, url, payload, headers = self._prepare_request()
527
528 # Wrap the request business logic in a function to be retried.
529 def retriable_request():
530 # NOTE: We assume "payload is None" but pass it along anyway.
531 result = transport.request(
532 method,
533 url,
534 data=payload,
535 headers=headers,
536 timeout=timeout,
537 )
538 self._process_response(result)
539 return result
540
541 return _request_helpers.wait_and_retry(
542 retriable_request, self._get_status_code, self._retry_strategy
543 )
544
545
546class RawChunkedDownload(_request_helpers.RawRequestsMixin, _download.ChunkedDownload):
547 """Download a raw resource in chunks from a Google API.
548
549 Args:
550 media_url (str): The URL containing the media to be downloaded.
551 chunk_size (int): The number of bytes to be retrieved in each
552 request.
553 stream (IO[bytes]): A write-able stream (i.e. file-like object) that
554 will be used to concatenate chunks of the resource as they are
555 downloaded.
556 start (int): The first byte in a range to be downloaded. If not
557 provided, defaults to ``0``.
558 end (int): The last byte in a range to be downloaded. If not
559 provided, will download to the end of the media.
560 headers (Optional[Mapping[str, str]]): Extra headers that should
561 be sent with each request, e.g. headers for data encryption
562 key headers.
563
564 Attributes:
565 media_url (str): The URL containing the media to be downloaded.
566 start (Optional[int]): The first byte in a range to be downloaded.
567 end (Optional[int]): The last byte in a range to be downloaded.
568 chunk_size (int): The number of bytes to be retrieved in each request.
569
570 Raises:
571 ValueError: If ``start`` is negative.
572 """
573
574 def consume_next_chunk(
575 self,
576 transport,
577 timeout=(
578 _request_helpers._DEFAULT_CONNECT_TIMEOUT,
579 _request_helpers._DEFAULT_READ_TIMEOUT,
580 ),
581 ):
582 """Consume the next chunk of the resource to be downloaded.
583
584 Args:
585 transport (~requests.Session): A ``requests`` object which can
586 make authenticated requests.
587 timeout (Optional[Union[float, Tuple[float, float]]]):
588 The number of seconds to wait for the server response.
589 Depending on the retry strategy, a request may be repeated
590 several times using the same timeout each time.
591
592 Can also be passed as a tuple (connect_timeout, read_timeout).
593 See :meth:`requests.Session.request` documentation for details.
594
595 Returns:
596 ~requests.Response: The HTTP response returned by ``transport``.
597
598 Raises:
599 ValueError: If the current download has finished.
600 """
601 method, url, payload, headers = self._prepare_request()
602
603 # Wrap the request business logic in a function to be retried.
604 def retriable_request():
605 # NOTE: We assume "payload is None" but pass it along anyway.
606 result = transport.request(
607 method,
608 url,
609 data=payload,
610 headers=headers,
611 stream=True,
612 timeout=timeout,
613 )
614 self._process_response(result)
615 return result
616
617 return _request_helpers.wait_and_retry(
618 retriable_request, self._get_status_code, self._retry_strategy
619 )
620
621
622def _add_decoder(response_raw, checksum):
623 """Patch the ``_decoder`` on a ``urllib3`` response.
624
625 This is so that we can intercept the compressed bytes before they are
626 decoded.
627
628 Only patches if the content encoding is ``gzip`` or ``br``.
629
630 Args:
631 response_raw (urllib3.response.HTTPResponse): The raw response for
632 an HTTP request.
633 checksum (object):
634 A checksum which will be updated with compressed bytes.
635
636 Returns:
637 object: Either the original ``checksum`` if ``_decoder`` is not
638 patched, or a ``_DoNothingHash`` if the decoder is patched, since the
639 caller will no longer need to hash to decoded bytes.
640 """
641 encoding = response_raw.headers.get("content-encoding", "").lower()
642 if encoding == "gzip":
643 response_raw._decoder = _GzipDecoder(checksum)
644 return _helpers._DoNothingHash()
645 # Only activate if brotli is installed
646 elif encoding == "br" and _BrotliDecoder: # type: ignore
647 response_raw._decoder = _BrotliDecoder(checksum)
648 return _helpers._DoNothingHash()
649 else:
650 return checksum
651
652
653class _GzipDecoder(urllib3.response.GzipDecoder):
654 """Custom subclass of ``urllib3`` decoder for ``gzip``-ed bytes.
655
656 Allows a checksum function to see the compressed bytes before they are
657 decoded. This way the checksum of the compressed value can be computed.
658
659 Args:
660 checksum (object):
661 A checksum which will be updated with compressed bytes.
662 """
663
664 def __init__(self, checksum):
665 super().__init__()
666 self._checksum = checksum
667
668 def decompress(self, data, max_length=-1):
669 """Decompress the bytes.
670
671 Args:
672 data (bytes): The compressed bytes to be decompressed.
673 max_length (int): Maximum number of bytes to return. -1 for no
674 limit. Forwarded to the underlying decoder when supported.
675
676 Returns:
677 bytes: The decompressed bytes from ``data``.
678 """
679 self._checksum.update(data)
680 try:
681 return super().decompress(data, max_length=max_length)
682 except TypeError:
683 return super().decompress(data)
684
685
686# urllib3.response.BrotliDecoder might not exist depending on whether brotli is
687# installed.
688if hasattr(urllib3.response, "BrotliDecoder"):
689
690 class _BrotliDecoder:
691 """Handler for ``brotli`` encoded bytes.
692
693 Allows a checksum function to see the compressed bytes before they are
694 decoded. This way the checksum of the compressed value can be computed.
695
696 Because BrotliDecoder's decompress method is dynamically created in
697 urllib3, a subclass is not practical. Instead, this class creates a
698 captive urllib3.requests.BrotliDecoder instance and acts as a proxy.
699
700 Args:
701 checksum (object):
702 A checksum which will be updated with compressed bytes.
703 """
704
705 def __init__(self, checksum):
706 self._decoder = urllib3.response.BrotliDecoder()
707 self._checksum = checksum
708
709 def decompress(self, data, max_length=-1):
710 """Decompress the bytes.
711
712 Args:
713 data (bytes): The compressed bytes to be decompressed.
714 max_length (int): Maximum number of bytes to return. -1 for no
715 limit. Forwarded to the underlying decoder when supported.
716
717 Returns:
718 bytes: The decompressed bytes from ``data``.
719 """
720 self._checksum.update(data)
721 try:
722 return self._decoder.decompress(data, max_length=max_length)
723 except TypeError:
724 return self._decoder.decompress(data)
725
726 @property
727 def has_unconsumed_tail(self):
728 try:
729 return self._decoder.has_unconsumed_tail
730 except AttributeError:
731 return False
732
733 def flush(self):
734 return self._decoder.flush()
735
736else: # pragma: NO COVER
737 _BrotliDecoder = None # type: ignore # pragma: NO COVER