• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Google Inc. All Rights Reserved.
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"""Classes to encapsulate a single HTTP request.
16
17The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actual HTTP request.
20"""
21from __future__ import absolute_import
22import six
23from six.moves import http_client
24from six.moves import range
25
26__author__ = 'jcgregorio@google.com (Joe Gregorio)'
27
28from six import BytesIO, StringIO
29from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
30
31import base64
32import copy
33import gzip
34import httplib2
35import json
36import logging
37import mimetypes
38import os
39import random
40import socket
41import sys
42import time
43import uuid
44
45# TODO(issue 221): Remove this conditional import jibbajabba.
46try:
47  import ssl
48except ImportError:
49  _ssl_SSLError = object()
50else:
51  _ssl_SSLError = ssl.SSLError
52
53from email.generator import Generator
54from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
56from email.parser import FeedParser
57
58from googleapiclient import _helpers as util
59
60from googleapiclient import _auth
61from googleapiclient.errors import BatchError
62from googleapiclient.errors import HttpError
63from googleapiclient.errors import InvalidChunkSizeError
64from googleapiclient.errors import ResumableUploadError
65from googleapiclient.errors import UnexpectedBodyError
66from googleapiclient.errors import UnexpectedMethodError
67from googleapiclient.model import JsonModel
68
69
70LOGGER = logging.getLogger(__name__)
71
72DEFAULT_CHUNK_SIZE = 100*1024*1024
73
74MAX_URI_LENGTH = 2048
75
76MAX_BATCH_LIMIT = 1000
77
78_TOO_MANY_REQUESTS = 429
79
80DEFAULT_HTTP_TIMEOUT_SEC = 60
81
82_LEGACY_BATCH_URI = 'https://www.googleapis.com/batch'
83
84
85def _should_retry_response(resp_status, content):
86  """Determines whether a response should be retried.
87
88  Args:
89    resp_status: The response status received.
90    content: The response content body.
91
92  Returns:
93    True if the response should be retried, otherwise False.
94  """
95  # Retry on 5xx errors.
96  if resp_status >= 500:
97    return True
98
99  # Retry on 429 errors.
100  if resp_status == _TOO_MANY_REQUESTS:
101    return True
102
103  # For 403 errors, we have to check for the `reason` in the response to
104  # determine if we should retry.
105  if resp_status == six.moves.http_client.FORBIDDEN:
106    # If there's no details about the 403 type, don't retry.
107    if not content:
108      return False
109
110    # Content is in JSON format.
111    try:
112      data = json.loads(content.decode('utf-8'))
113      if isinstance(data, dict):
114        reason = data['error']['errors'][0]['reason']
115      else:
116        reason = data[0]['error']['errors']['reason']
117    except (UnicodeDecodeError, ValueError, KeyError):
118      LOGGER.warning('Invalid JSON content from response: %s', content)
119      return False
120
121    LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
122
123    # Only retry on rate limit related failures.
124    if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
125      return True
126
127  # Everything else is a success or non-retriable so break.
128  return False
129
130
131def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
132                   **kwargs):
133  """Retries an HTTP request multiple times while handling errors.
134
135  If after all retries the request still fails, last error is either returned as
136  return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
137
138  Args:
139    http: Http object to be used to execute request.
140    num_retries: Maximum number of retries.
141    req_type: Type of the request (used for logging retries).
142    sleep, rand: Functions to sleep for random time between retries.
143    uri: URI to be requested.
144    method: HTTP method to be used.
145    args, kwargs: Additional arguments passed to http.request.
146
147  Returns:
148    resp, content - Response from the http request (may be HTTP 5xx).
149  """
150  resp = None
151  content = None
152  for retry_num in range(num_retries + 1):
153    if retry_num > 0:
154      # Sleep before retrying.
155      sleep_time = rand() * 2 ** retry_num
156      LOGGER.warning(
157          'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
158          sleep_time, retry_num, num_retries, req_type, method, uri,
159          resp.status if resp else exception)
160      sleep(sleep_time)
161
162    try:
163      exception = None
164      resp, content = http.request(uri, method, *args, **kwargs)
165    # Retry on SSL errors and socket timeout errors.
166    except _ssl_SSLError as ssl_error:
167      exception = ssl_error
168    except socket.timeout as socket_timeout:
169      # It's important that this be before socket.error as it's a subclass
170      # socket.timeout has no errorcode
171      exception = socket_timeout
172    except socket.error as socket_error:
173      # errno's contents differ by platform, so we have to match by name.
174      if socket.errno.errorcode.get(socket_error.errno) not in {
175        'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED'}:
176        raise
177      exception = socket_error
178    except httplib2.ServerNotFoundError as server_not_found_error:
179      exception = server_not_found_error
180
181    if exception:
182      if retry_num == num_retries:
183        raise exception
184      else:
185        continue
186
187    if not _should_retry_response(resp.status, content):
188      break
189
190  return resp, content
191
192
193class MediaUploadProgress(object):
194  """Status of a resumable upload."""
195
196  def __init__(self, resumable_progress, total_size):
197    """Constructor.
198
199    Args:
200      resumable_progress: int, bytes sent so far.
201      total_size: int, total bytes in complete upload, or None if the total
202        upload size isn't known ahead of time.
203    """
204    self.resumable_progress = resumable_progress
205    self.total_size = total_size
206
207  def progress(self):
208    """Percent of upload completed, as a float.
209
210    Returns:
211      the percentage complete as a float, returning 0.0 if the total size of
212      the upload is unknown.
213    """
214    if self.total_size is not None and self.total_size != 0:
215      return float(self.resumable_progress) / float(self.total_size)
216    else:
217      return 0.0
218
219
220class MediaDownloadProgress(object):
221  """Status of a resumable download."""
222
223  def __init__(self, resumable_progress, total_size):
224    """Constructor.
225
226    Args:
227      resumable_progress: int, bytes received so far.
228      total_size: int, total bytes in complete download.
229    """
230    self.resumable_progress = resumable_progress
231    self.total_size = total_size
232
233  def progress(self):
234    """Percent of download completed, as a float.
235
236    Returns:
237      the percentage complete as a float, returning 0.0 if the total size of
238      the download is unknown.
239    """
240    if self.total_size is not None and self.total_size != 0:
241      return float(self.resumable_progress) / float(self.total_size)
242    else:
243      return 0.0
244
245
246class MediaUpload(object):
247  """Describes a media object to upload.
248
249  Base class that defines the interface of MediaUpload subclasses.
250
251  Note that subclasses of MediaUpload may allow you to control the chunksize
252  when uploading a media object. It is important to keep the size of the chunk
253  as large as possible to keep the upload efficient. Other factors may influence
254  the size of the chunk you use, particularly if you are working in an
255  environment where individual HTTP requests may have a hardcoded time limit,
256  such as under certain classes of requests under Google App Engine.
257
258  Streams are io.Base compatible objects that support seek(). Some MediaUpload
259  subclasses support using streams directly to upload data. Support for
260  streaming may be indicated by a MediaUpload sub-class and if appropriate for a
261  platform that stream will be used for uploading the media object. The support
262  for streaming is indicated by has_stream() returning True. The stream() method
263  should return an io.Base object that supports seek(). On platforms where the
264  underlying httplib module supports streaming, for example Python 2.6 and
265  later, the stream will be passed into the http library which will result in
266  less memory being used and possibly faster uploads.
267
268  If you need to upload media that can't be uploaded using any of the existing
269  MediaUpload sub-class then you can sub-class MediaUpload for your particular
270  needs.
271  """
272
273  def chunksize(self):
274    """Chunk size for resumable uploads.
275
276    Returns:
277      Chunk size in bytes.
278    """
279    raise NotImplementedError()
280
281  def mimetype(self):
282    """Mime type of the body.
283
284    Returns:
285      Mime type.
286    """
287    return 'application/octet-stream'
288
289  def size(self):
290    """Size of upload.
291
292    Returns:
293      Size of the body, or None of the size is unknown.
294    """
295    return None
296
297  def resumable(self):
298    """Whether this upload is resumable.
299
300    Returns:
301      True if resumable upload or False.
302    """
303    return False
304
305  def getbytes(self, begin, end):
306    """Get bytes from the media.
307
308    Args:
309      begin: int, offset from beginning of file.
310      length: int, number of bytes to read, starting at begin.
311
312    Returns:
313      A string of bytes read. May be shorter than length if EOF was reached
314      first.
315    """
316    raise NotImplementedError()
317
318  def has_stream(self):
319    """Does the underlying upload support a streaming interface.
320
321    Streaming means it is an io.IOBase subclass that supports seek, i.e.
322    seekable() returns True.
323
324    Returns:
325      True if the call to stream() will return an instance of a seekable io.Base
326      subclass.
327    """
328    return False
329
330  def stream(self):
331    """A stream interface to the data being uploaded.
332
333    Returns:
334      The returned value is an io.IOBase subclass that supports seek, i.e.
335      seekable() returns True.
336    """
337    raise NotImplementedError()
338
339  @util.positional(1)
340  def _to_json(self, strip=None):
341    """Utility function for creating a JSON representation of a MediaUpload.
342
343    Args:
344      strip: array, An array of names of members to not include in the JSON.
345
346    Returns:
347       string, a JSON representation of this instance, suitable to pass to
348       from_json().
349    """
350    t = type(self)
351    d = copy.copy(self.__dict__)
352    if strip is not None:
353      for member in strip:
354        del d[member]
355    d['_class'] = t.__name__
356    d['_module'] = t.__module__
357    return json.dumps(d)
358
359  def to_json(self):
360    """Create a JSON representation of an instance of MediaUpload.
361
362    Returns:
363       string, a JSON representation of this instance, suitable to pass to
364       from_json().
365    """
366    return self._to_json()
367
368  @classmethod
369  def new_from_json(cls, s):
370    """Utility class method to instantiate a MediaUpload subclass from a JSON
371    representation produced by to_json().
372
373    Args:
374      s: string, JSON from to_json().
375
376    Returns:
377      An instance of the subclass of MediaUpload that was serialized with
378      to_json().
379    """
380    data = json.loads(s)
381    # Find and call the right classmethod from_json() to restore the object.
382    module = data['_module']
383    m = __import__(module, fromlist=module.split('.')[:-1])
384    kls = getattr(m, data['_class'])
385    from_json = getattr(kls, 'from_json')
386    return from_json(s)
387
388
389class MediaIoBaseUpload(MediaUpload):
390  """A MediaUpload for a io.Base objects.
391
392  Note that the Python file object is compatible with io.Base and can be used
393  with this class also.
394
395    fh = BytesIO('...Some data to upload...')
396    media = MediaIoBaseUpload(fh, mimetype='image/png',
397      chunksize=1024*1024, resumable=True)
398    farm.animals().insert(
399        id='cow',
400        name='cow.png',
401        media_body=media).execute()
402
403  Depending on the platform you are working on, you may pass -1 as the
404  chunksize, which indicates that the entire file should be uploaded in a single
405  request. If the underlying platform supports streams, such as Python 2.6 or
406  later, then this can be very efficient as it avoids multiple connections, and
407  also avoids loading the entire file into memory before sending it. Note that
408  Google App Engine has a 5MB limit on request size, so you should never set
409  your chunksize larger than 5MB, or to -1.
410  """
411
412  @util.positional(3)
413  def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
414      resumable=False):
415    """Constructor.
416
417    Args:
418      fd: io.Base or file object, The source of the bytes to upload. MUST be
419        opened in blocking mode, do not use streams opened in non-blocking mode.
420        The given stream must be seekable, that is, it must be able to call
421        seek() on fd.
422      mimetype: string, Mime-type of the file.
423      chunksize: int, File will be uploaded in chunks of this many bytes. Only
424        used if resumable=True. Pass in a value of -1 if the file is to be
425        uploaded as a single chunk. Note that Google App Engine has a 5MB limit
426        on request size, so you should never set your chunksize larger than 5MB,
427        or to -1.
428      resumable: bool, True if this is a resumable upload. False means upload
429        in a single request.
430    """
431    super(MediaIoBaseUpload, self).__init__()
432    self._fd = fd
433    self._mimetype = mimetype
434    if not (chunksize == -1 or chunksize > 0):
435      raise InvalidChunkSizeError()
436    self._chunksize = chunksize
437    self._resumable = resumable
438
439    self._fd.seek(0, os.SEEK_END)
440    self._size = self._fd.tell()
441
442  def chunksize(self):
443    """Chunk size for resumable uploads.
444
445    Returns:
446      Chunk size in bytes.
447    """
448    return self._chunksize
449
450  def mimetype(self):
451    """Mime type of the body.
452
453    Returns:
454      Mime type.
455    """
456    return self._mimetype
457
458  def size(self):
459    """Size of upload.
460
461    Returns:
462      Size of the body, or None of the size is unknown.
463    """
464    return self._size
465
466  def resumable(self):
467    """Whether this upload is resumable.
468
469    Returns:
470      True if resumable upload or False.
471    """
472    return self._resumable
473
474  def getbytes(self, begin, length):
475    """Get bytes from the media.
476
477    Args:
478      begin: int, offset from beginning of file.
479      length: int, number of bytes to read, starting at begin.
480
481    Returns:
482      A string of bytes read. May be shorted than length if EOF was reached
483      first.
484    """
485    self._fd.seek(begin)
486    return self._fd.read(length)
487
488  def has_stream(self):
489    """Does the underlying upload support a streaming interface.
490
491    Streaming means it is an io.IOBase subclass that supports seek, i.e.
492    seekable() returns True.
493
494    Returns:
495      True if the call to stream() will return an instance of a seekable io.Base
496      subclass.
497    """
498    return True
499
500  def stream(self):
501    """A stream interface to the data being uploaded.
502
503    Returns:
504      The returned value is an io.IOBase subclass that supports seek, i.e.
505      seekable() returns True.
506    """
507    return self._fd
508
509  def to_json(self):
510    """This upload type is not serializable."""
511    raise NotImplementedError('MediaIoBaseUpload is not serializable.')
512
513
514class MediaFileUpload(MediaIoBaseUpload):
515  """A MediaUpload for a file.
516
517  Construct a MediaFileUpload and pass as the media_body parameter of the
518  method. For example, if we had a service that allowed uploading images:
519
520    media = MediaFileUpload('cow.png', mimetype='image/png',
521      chunksize=1024*1024, resumable=True)
522    farm.animals().insert(
523        id='cow',
524        name='cow.png',
525        media_body=media).execute()
526
527  Depending on the platform you are working on, you may pass -1 as the
528  chunksize, which indicates that the entire file should be uploaded in a single
529  request. If the underlying platform supports streams, such as Python 2.6 or
530  later, then this can be very efficient as it avoids multiple connections, and
531  also avoids loading the entire file into memory before sending it. Note that
532  Google App Engine has a 5MB limit on request size, so you should never set
533  your chunksize larger than 5MB, or to -1.
534  """
535
536  @util.positional(2)
537  def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
538               resumable=False):
539    """Constructor.
540
541    Args:
542      filename: string, Name of the file.
543      mimetype: string, Mime-type of the file. If None then a mime-type will be
544        guessed from the file extension.
545      chunksize: int, File will be uploaded in chunks of this many bytes. Only
546        used if resumable=True. Pass in a value of -1 if the file is to be
547        uploaded in a single chunk. Note that Google App Engine has a 5MB limit
548        on request size, so you should never set your chunksize larger than 5MB,
549        or to -1.
550      resumable: bool, True if this is a resumable upload. False means upload
551        in a single request.
552    """
553    self._filename = filename
554    fd = open(self._filename, 'rb')
555    if mimetype is None:
556      # No mimetype provided, make a guess.
557      mimetype, _ = mimetypes.guess_type(filename)
558      if mimetype is None:
559        # Guess failed, use octet-stream.
560        mimetype = 'application/octet-stream'
561    super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
562                                          resumable=resumable)
563
564  def to_json(self):
565    """Creating a JSON representation of an instance of MediaFileUpload.
566
567    Returns:
568       string, a JSON representation of this instance, suitable to pass to
569       from_json().
570    """
571    return self._to_json(strip=['_fd'])
572
573  @staticmethod
574  def from_json(s):
575    d = json.loads(s)
576    return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
577                           chunksize=d['_chunksize'], resumable=d['_resumable'])
578
579
580class MediaInMemoryUpload(MediaIoBaseUpload):
581  """MediaUpload for a chunk of bytes.
582
583  DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
584  the stream.
585  """
586
587  @util.positional(2)
588  def __init__(self, body, mimetype='application/octet-stream',
589               chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
590    """Create a new MediaInMemoryUpload.
591
592  DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
593  the stream.
594
595  Args:
596    body: string, Bytes of body content.
597    mimetype: string, Mime-type of the file or default of
598      'application/octet-stream'.
599    chunksize: int, File will be uploaded in chunks of this many bytes. Only
600      used if resumable=True.
601    resumable: bool, True if this is a resumable upload. False means upload
602      in a single request.
603    """
604    fd = BytesIO(body)
605    super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
606                                              resumable=resumable)
607
608
609class MediaIoBaseDownload(object):
610  """"Download media resources.
611
612  Note that the Python file object is compatible with io.Base and can be used
613  with this class also.
614
615
616  Example:
617    request = farms.animals().get_media(id='cow')
618    fh = io.FileIO('cow.png', mode='wb')
619    downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
620
621    done = False
622    while done is False:
623      status, done = downloader.next_chunk()
624      if status:
625        print "Download %d%%." % int(status.progress() * 100)
626    print "Download Complete!"
627  """
628
629  @util.positional(3)
630  def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
631    """Constructor.
632
633    Args:
634      fd: io.Base or file object, The stream in which to write the downloaded
635        bytes.
636      request: googleapiclient.http.HttpRequest, the media request to perform in
637        chunks.
638      chunksize: int, File will be downloaded in chunks of this many bytes.
639    """
640    self._fd = fd
641    self._request = request
642    self._uri = request.uri
643    self._chunksize = chunksize
644    self._progress = 0
645    self._total_size = None
646    self._done = False
647
648    # Stubs for testing.
649    self._sleep = time.sleep
650    self._rand = random.random
651
652    self._headers = {}
653    for k, v in six.iteritems(request.headers):
654      # allow users to supply custom headers by setting them on the request
655      # but strip out the ones that are set by default on requests generated by
656      # API methods like Drive's files().get(fileId=...)
657      if not k.lower() in ('accept', 'accept-encoding', 'user-agent'):
658        self._headers[k] = v
659
660  @util.positional(1)
661  def next_chunk(self, num_retries=0):
662    """Get the next chunk of the download.
663
664    Args:
665      num_retries: Integer, number of times to retry with randomized
666            exponential backoff. If all retries fail, the raised HttpError
667            represents the last request. If zero (default), we attempt the
668            request only once.
669
670    Returns:
671      (status, done): (MediaDownloadProgress, boolean)
672         The value of 'done' will be True when the media has been fully
673         downloaded or the total size of the media is unknown.
674
675    Raises:
676      googleapiclient.errors.HttpError if the response was not a 2xx.
677      httplib2.HttpLib2Error if a transport error has occured.
678    """
679    headers = self._headers.copy()
680    headers['range'] = 'bytes=%d-%d' % (
681            self._progress, self._progress + self._chunksize)
682    http = self._request.http
683
684    resp, content = _retry_request(
685        http, num_retries, 'media download', self._sleep, self._rand, self._uri,
686        'GET', headers=headers)
687
688    if resp.status in [200, 206]:
689      if 'content-location' in resp and resp['content-location'] != self._uri:
690        self._uri = resp['content-location']
691      self._progress += len(content)
692      self._fd.write(content)
693
694      if 'content-range' in resp:
695        content_range = resp['content-range']
696        length = content_range.rsplit('/', 1)[1]
697        self._total_size = int(length)
698      elif 'content-length' in resp:
699        self._total_size = int(resp['content-length'])
700
701      if self._total_size is None or self._progress == self._total_size:
702        self._done = True
703      return MediaDownloadProgress(self._progress, self._total_size), self._done
704    else:
705      raise HttpError(resp, content, uri=self._uri)
706
707
708class _StreamSlice(object):
709  """Truncated stream.
710
711  Takes a stream and presents a stream that is a slice of the original stream.
712  This is used when uploading media in chunks. In later versions of Python a
713  stream can be passed to httplib in place of the string of data to send. The
714  problem is that httplib just blindly reads to the end of the stream. This
715  wrapper presents a virtual stream that only reads to the end of the chunk.
716  """
717
718  def __init__(self, stream, begin, chunksize):
719    """Constructor.
720
721    Args:
722      stream: (io.Base, file object), the stream to wrap.
723      begin: int, the seek position the chunk begins at.
724      chunksize: int, the size of the chunk.
725    """
726    self._stream = stream
727    self._begin = begin
728    self._chunksize = chunksize
729    self._stream.seek(begin)
730
731  def read(self, n=-1):
732    """Read n bytes.
733
734    Args:
735      n, int, the number of bytes to read.
736
737    Returns:
738      A string of length 'n', or less if EOF is reached.
739    """
740    # The data left available to read sits in [cur, end)
741    cur = self._stream.tell()
742    end = self._begin + self._chunksize
743    if n == -1 or cur + n > end:
744      n = end - cur
745    return self._stream.read(n)
746
747
748class HttpRequest(object):
749  """Encapsulates a single HTTP request."""
750
751  @util.positional(4)
752  def __init__(self, http, postproc, uri,
753               method='GET',
754               body=None,
755               headers=None,
756               methodId=None,
757               resumable=None):
758    """Constructor for an HttpRequest.
759
760    Args:
761      http: httplib2.Http, the transport object to use to make a request
762      postproc: callable, called on the HTTP response and content to transform
763                it into a data object before returning, or raising an exception
764                on an error.
765      uri: string, the absolute URI to send the request to
766      method: string, the HTTP method to use
767      body: string, the request body of the HTTP request,
768      headers: dict, the HTTP request headers
769      methodId: string, a unique identifier for the API method being called.
770      resumable: MediaUpload, None if this is not a resumbale request.
771    """
772    self.uri = uri
773    self.method = method
774    self.body = body
775    self.headers = headers or {}
776    self.methodId = methodId
777    self.http = http
778    self.postproc = postproc
779    self.resumable = resumable
780    self.response_callbacks = []
781    self._in_error_state = False
782
783    # The size of the non-media part of the request.
784    self.body_size = len(self.body or '')
785
786    # The resumable URI to send chunks to.
787    self.resumable_uri = None
788
789    # The bytes that have been uploaded.
790    self.resumable_progress = 0
791
792    # Stubs for testing.
793    self._rand = random.random
794    self._sleep = time.sleep
795
796  @util.positional(1)
797  def execute(self, http=None, num_retries=0):
798    """Execute the request.
799
800    Args:
801      http: httplib2.Http, an http object to be used in place of the
802            one the HttpRequest request object was constructed with.
803      num_retries: Integer, number of times to retry with randomized
804            exponential backoff. If all retries fail, the raised HttpError
805            represents the last request. If zero (default), we attempt the
806            request only once.
807
808    Returns:
809      A deserialized object model of the response body as determined
810      by the postproc.
811
812    Raises:
813      googleapiclient.errors.HttpError if the response was not a 2xx.
814      httplib2.HttpLib2Error if a transport error has occured.
815    """
816    if http is None:
817      http = self.http
818
819    if self.resumable:
820      body = None
821      while body is None:
822        _, body = self.next_chunk(http=http, num_retries=num_retries)
823      return body
824
825    # Non-resumable case.
826
827    if 'content-length' not in self.headers:
828      self.headers['content-length'] = str(self.body_size)
829    # If the request URI is too long then turn it into a POST request.
830    # Assume that a GET request never contains a request body.
831    if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
832      self.method = 'POST'
833      self.headers['x-http-method-override'] = 'GET'
834      self.headers['content-type'] = 'application/x-www-form-urlencoded'
835      parsed = urlparse(self.uri)
836      self.uri = urlunparse(
837          (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
838           None)
839          )
840      self.body = parsed.query
841      self.headers['content-length'] = str(len(self.body))
842
843    # Handle retries for server-side errors.
844    resp, content = _retry_request(
845          http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
846          method=str(self.method), body=self.body, headers=self.headers)
847
848    for callback in self.response_callbacks:
849      callback(resp)
850    if resp.status >= 300:
851      raise HttpError(resp, content, uri=self.uri)
852    return self.postproc(resp, content)
853
854  @util.positional(2)
855  def add_response_callback(self, cb):
856    """add_response_headers_callback
857
858    Args:
859      cb: Callback to be called on receiving the response headers, of signature:
860
861      def cb(resp):
862        # Where resp is an instance of httplib2.Response
863    """
864    self.response_callbacks.append(cb)
865
866  @util.positional(1)
867  def next_chunk(self, http=None, num_retries=0):
868    """Execute the next step of a resumable upload.
869
870    Can only be used if the method being executed supports media uploads and
871    the MediaUpload object passed in was flagged as using resumable upload.
872
873    Example:
874
875      media = MediaFileUpload('cow.png', mimetype='image/png',
876                              chunksize=1000, resumable=True)
877      request = farm.animals().insert(
878          id='cow',
879          name='cow.png',
880          media_body=media)
881
882      response = None
883      while response is None:
884        status, response = request.next_chunk()
885        if status:
886          print "Upload %d%% complete." % int(status.progress() * 100)
887
888
889    Args:
890      http: httplib2.Http, an http object to be used in place of the
891            one the HttpRequest request object was constructed with.
892      num_retries: Integer, number of times to retry with randomized
893            exponential backoff. If all retries fail, the raised HttpError
894            represents the last request. If zero (default), we attempt the
895            request only once.
896
897    Returns:
898      (status, body): (ResumableMediaStatus, object)
899         The body will be None until the resumable media is fully uploaded.
900
901    Raises:
902      googleapiclient.errors.HttpError if the response was not a 2xx.
903      httplib2.HttpLib2Error if a transport error has occured.
904    """
905    if http is None:
906      http = self.http
907
908    if self.resumable.size() is None:
909      size = '*'
910    else:
911      size = str(self.resumable.size())
912
913    if self.resumable_uri is None:
914      start_headers = copy.copy(self.headers)
915      start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
916      if size != '*':
917        start_headers['X-Upload-Content-Length'] = size
918      start_headers['content-length'] = str(self.body_size)
919
920      resp, content = _retry_request(
921          http, num_retries, 'resumable URI request', self._sleep, self._rand,
922          self.uri, method=self.method, body=self.body, headers=start_headers)
923
924      if resp.status == 200 and 'location' in resp:
925        self.resumable_uri = resp['location']
926      else:
927        raise ResumableUploadError(resp, content)
928    elif self._in_error_state:
929      # If we are in an error state then query the server for current state of
930      # the upload by sending an empty PUT and reading the 'range' header in
931      # the response.
932      headers = {
933          'Content-Range': 'bytes */%s' % size,
934          'content-length': '0'
935          }
936      resp, content = http.request(self.resumable_uri, 'PUT',
937                                   headers=headers)
938      status, body = self._process_response(resp, content)
939      if body:
940        # The upload was complete.
941        return (status, body)
942
943    if self.resumable.has_stream():
944      data = self.resumable.stream()
945      if self.resumable.chunksize() == -1:
946        data.seek(self.resumable_progress)
947        chunk_end = self.resumable.size() - self.resumable_progress - 1
948      else:
949        # Doing chunking with a stream, so wrap a slice of the stream.
950        data = _StreamSlice(data, self.resumable_progress,
951                            self.resumable.chunksize())
952        chunk_end = min(
953            self.resumable_progress + self.resumable.chunksize() - 1,
954            self.resumable.size() - 1)
955    else:
956      data = self.resumable.getbytes(
957          self.resumable_progress, self.resumable.chunksize())
958
959      # A short read implies that we are at EOF, so finish the upload.
960      if len(data) < self.resumable.chunksize():
961        size = str(self.resumable_progress + len(data))
962
963      chunk_end = self.resumable_progress + len(data) - 1
964
965    headers = {
966        'Content-Range': 'bytes %d-%d/%s' % (
967            self.resumable_progress, chunk_end, size),
968        # Must set the content-length header here because httplib can't
969        # calculate the size when working with _StreamSlice.
970        'Content-Length': str(chunk_end - self.resumable_progress + 1)
971        }
972
973    for retry_num in range(num_retries + 1):
974      if retry_num > 0:
975        self._sleep(self._rand() * 2**retry_num)
976        LOGGER.warning(
977            'Retry #%d for media upload: %s %s, following status: %d'
978            % (retry_num, self.method, self.uri, resp.status))
979
980      try:
981        resp, content = http.request(self.resumable_uri, method='PUT',
982                                     body=data,
983                                     headers=headers)
984      except:
985        self._in_error_state = True
986        raise
987      if not _should_retry_response(resp.status, content):
988        break
989
990    return self._process_response(resp, content)
991
992  def _process_response(self, resp, content):
993    """Process the response from a single chunk upload.
994
995    Args:
996      resp: httplib2.Response, the response object.
997      content: string, the content of the response.
998
999    Returns:
1000      (status, body): (ResumableMediaStatus, object)
1001         The body will be None until the resumable media is fully uploaded.
1002
1003    Raises:
1004      googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1005    """
1006    if resp.status in [200, 201]:
1007      self._in_error_state = False
1008      return None, self.postproc(resp, content)
1009    elif resp.status == 308:
1010      self._in_error_state = False
1011      # A "308 Resume Incomplete" indicates we are not done.
1012      try:
1013        self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1014      except KeyError:
1015        # If resp doesn't contain range header, resumable progress is 0
1016        self.resumable_progress = 0
1017      if 'location' in resp:
1018        self.resumable_uri = resp['location']
1019    else:
1020      self._in_error_state = True
1021      raise HttpError(resp, content, uri=self.uri)
1022
1023    return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1024            None)
1025
1026  def to_json(self):
1027    """Returns a JSON representation of the HttpRequest."""
1028    d = copy.copy(self.__dict__)
1029    if d['resumable'] is not None:
1030      d['resumable'] = self.resumable.to_json()
1031    del d['http']
1032    del d['postproc']
1033    del d['_sleep']
1034    del d['_rand']
1035
1036    return json.dumps(d)
1037
1038  @staticmethod
1039  def from_json(s, http, postproc):
1040    """Returns an HttpRequest populated with info from a JSON object."""
1041    d = json.loads(s)
1042    if d['resumable'] is not None:
1043      d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1044    return HttpRequest(
1045        http,
1046        postproc,
1047        uri=d['uri'],
1048        method=d['method'],
1049        body=d['body'],
1050        headers=d['headers'],
1051        methodId=d['methodId'],
1052        resumable=d['resumable'])
1053
1054
1055class BatchHttpRequest(object):
1056  """Batches multiple HttpRequest objects into a single HTTP request.
1057
1058  Example:
1059    from googleapiclient.http import BatchHttpRequest
1060
1061    def list_animals(request_id, response, exception):
1062      \"\"\"Do something with the animals list response.\"\"\"
1063      if exception is not None:
1064        # Do something with the exception.
1065        pass
1066      else:
1067        # Do something with the response.
1068        pass
1069
1070    def list_farmers(request_id, response, exception):
1071      \"\"\"Do something with the farmers list response.\"\"\"
1072      if exception is not None:
1073        # Do something with the exception.
1074        pass
1075      else:
1076        # Do something with the response.
1077        pass
1078
1079    service = build('farm', 'v2')
1080
1081    batch = BatchHttpRequest()
1082
1083    batch.add(service.animals().list(), list_animals)
1084    batch.add(service.farmers().list(), list_farmers)
1085    batch.execute(http=http)
1086  """
1087
1088  @util.positional(1)
1089  def __init__(self, callback=None, batch_uri=None):
1090    """Constructor for a BatchHttpRequest.
1091
1092    Args:
1093      callback: callable, A callback to be called for each response, of the
1094        form callback(id, response, exception). The first parameter is the
1095        request id, and the second is the deserialized response object. The
1096        third is an googleapiclient.errors.HttpError exception object if an HTTP error
1097        occurred while processing the request, or None if no error occurred.
1098      batch_uri: string, URI to send batch requests to.
1099    """
1100    if batch_uri is None:
1101      batch_uri = _LEGACY_BATCH_URI
1102
1103    if batch_uri == _LEGACY_BATCH_URI:
1104      LOGGER.warn(
1105        "You have constructed a BatchHttpRequest using the legacy batch "
1106        "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1107        "Please provide the API-specific endpoint or use "
1108        "service.new_batch_http_request(). For more details see "
1109        "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1110        "and https://developers.google.com/api-client-library/python/guide/batch.",
1111        _LEGACY_BATCH_URI)
1112    self._batch_uri = batch_uri
1113
1114    # Global callback to be called for each individual response in the batch.
1115    self._callback = callback
1116
1117    # A map from id to request.
1118    self._requests = {}
1119
1120    # A map from id to callback.
1121    self._callbacks = {}
1122
1123    # List of request ids, in the order in which they were added.
1124    self._order = []
1125
1126    # The last auto generated id.
1127    self._last_auto_id = 0
1128
1129    # Unique ID on which to base the Content-ID headers.
1130    self._base_id = None
1131
1132    # A map from request id to (httplib2.Response, content) response pairs
1133    self._responses = {}
1134
1135    # A map of id(Credentials) that have been refreshed.
1136    self._refreshed_credentials = {}
1137
1138  def _refresh_and_apply_credentials(self, request, http):
1139    """Refresh the credentials and apply to the request.
1140
1141    Args:
1142      request: HttpRequest, the request.
1143      http: httplib2.Http, the global http object for the batch.
1144    """
1145    # For the credentials to refresh, but only once per refresh_token
1146    # If there is no http per the request then refresh the http passed in
1147    # via execute()
1148    creds = None
1149    request_credentials = False
1150
1151    if request.http is not None:
1152      creds = _auth.get_credentials_from_http(request.http)
1153      request_credentials = True
1154
1155    if creds is None and http is not None:
1156      creds = _auth.get_credentials_from_http(http)
1157
1158    if creds is not None:
1159      if id(creds) not in self._refreshed_credentials:
1160        _auth.refresh_credentials(creds)
1161        self._refreshed_credentials[id(creds)] = 1
1162
1163    # Only apply the credentials if we are using the http object passed in,
1164    # otherwise apply() will get called during _serialize_request().
1165    if request.http is None or not request_credentials:
1166      _auth.apply_credentials(creds, request.headers)
1167
1168
1169  def _id_to_header(self, id_):
1170    """Convert an id to a Content-ID header value.
1171
1172    Args:
1173      id_: string, identifier of individual request.
1174
1175    Returns:
1176      A Content-ID header with the id_ encoded into it. A UUID is prepended to
1177      the value because Content-ID headers are supposed to be universally
1178      unique.
1179    """
1180    if self._base_id is None:
1181      self._base_id = uuid.uuid4()
1182
1183    # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1184    # line folding works properly on Python 3; see
1185    # https://github.com/google/google-api-python-client/issues/164
1186    return '<%s + %s>' % (self._base_id, quote(id_))
1187
1188  def _header_to_id(self, header):
1189    """Convert a Content-ID header value to an id.
1190
1191    Presumes the Content-ID header conforms to the format that _id_to_header()
1192    returns.
1193
1194    Args:
1195      header: string, Content-ID header value.
1196
1197    Returns:
1198      The extracted id value.
1199
1200    Raises:
1201      BatchError if the header is not in the expected format.
1202    """
1203    if header[0] != '<' or header[-1] != '>':
1204      raise BatchError("Invalid value for Content-ID: %s" % header)
1205    if '+' not in header:
1206      raise BatchError("Invalid value for Content-ID: %s" % header)
1207    base, id_ = header[1:-1].split(' + ', 1)
1208
1209    return unquote(id_)
1210
1211  def _serialize_request(self, request):
1212    """Convert an HttpRequest object into a string.
1213
1214    Args:
1215      request: HttpRequest, the request to serialize.
1216
1217    Returns:
1218      The request as a string in application/http format.
1219    """
1220    # Construct status line
1221    parsed = urlparse(request.uri)
1222    request_line = urlunparse(
1223        ('', '', parsed.path, parsed.params, parsed.query, '')
1224        )
1225    status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1226    major, minor = request.headers.get('content-type', 'application/json').split('/')
1227    msg = MIMENonMultipart(major, minor)
1228    headers = request.headers.copy()
1229
1230    if request.http is not None:
1231      credentials = _auth.get_credentials_from_http(request.http)
1232      if credentials is not None:
1233        _auth.apply_credentials(credentials, headers)
1234
1235    # MIMENonMultipart adds its own Content-Type header.
1236    if 'content-type' in headers:
1237      del headers['content-type']
1238
1239    for key, value in six.iteritems(headers):
1240      msg[key] = value
1241    msg['Host'] = parsed.netloc
1242    msg.set_unixfrom(None)
1243
1244    if request.body is not None:
1245      msg.set_payload(request.body)
1246      msg['content-length'] = str(len(request.body))
1247
1248    # Serialize the mime message.
1249    fp = StringIO()
1250    # maxheaderlen=0 means don't line wrap headers.
1251    g = Generator(fp, maxheaderlen=0)
1252    g.flatten(msg, unixfrom=False)
1253    body = fp.getvalue()
1254
1255    return status_line + body
1256
1257  def _deserialize_response(self, payload):
1258    """Convert string into httplib2 response and content.
1259
1260    Args:
1261      payload: string, headers and body as a string.
1262
1263    Returns:
1264      A pair (resp, content), such as would be returned from httplib2.request.
1265    """
1266    # Strip off the status line
1267    status_line, payload = payload.split('\n', 1)
1268    protocol, status, reason = status_line.split(' ', 2)
1269
1270    # Parse the rest of the response
1271    parser = FeedParser()
1272    parser.feed(payload)
1273    msg = parser.close()
1274    msg['status'] = status
1275
1276    # Create httplib2.Response from the parsed headers.
1277    resp = httplib2.Response(msg)
1278    resp.reason = reason
1279    resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1280
1281    content = payload.split('\r\n\r\n', 1)[1]
1282
1283    return resp, content
1284
1285  def _new_id(self):
1286    """Create a new id.
1287
1288    Auto incrementing number that avoids conflicts with ids already used.
1289
1290    Returns:
1291       string, a new unique id.
1292    """
1293    self._last_auto_id += 1
1294    while str(self._last_auto_id) in self._requests:
1295      self._last_auto_id += 1
1296    return str(self._last_auto_id)
1297
1298  @util.positional(2)
1299  def add(self, request, callback=None, request_id=None):
1300    """Add a new request.
1301
1302    Every callback added will be paired with a unique id, the request_id. That
1303    unique id will be passed back to the callback when the response comes back
1304    from the server. The default behavior is to have the library generate it's
1305    own unique id. If the caller passes in a request_id then they must ensure
1306    uniqueness for each request_id, and if they are not an exception is
1307    raised. Callers should either supply all request_ids or never supply a
1308    request id, to avoid such an error.
1309
1310    Args:
1311      request: HttpRequest, Request to add to the batch.
1312      callback: callable, A callback to be called for this response, of the
1313        form callback(id, response, exception). The first parameter is the
1314        request id, and the second is the deserialized response object. The
1315        third is an googleapiclient.errors.HttpError exception object if an HTTP error
1316        occurred while processing the request, or None if no errors occurred.
1317      request_id: string, A unique id for the request. The id will be passed
1318        to the callback with the response.
1319
1320    Returns:
1321      None
1322
1323    Raises:
1324      BatchError if a media request is added to a batch.
1325      KeyError is the request_id is not unique.
1326    """
1327
1328    if len(self._order) >= MAX_BATCH_LIMIT:
1329      raise BatchError("Exceeded the maximum calls(%d) in a single batch request."
1330                       % MAX_BATCH_LIMIT)
1331    if request_id is None:
1332      request_id = self._new_id()
1333    if request.resumable is not None:
1334      raise BatchError("Media requests cannot be used in a batch request.")
1335    if request_id in self._requests:
1336      raise KeyError("A request with this ID already exists: %s" % request_id)
1337    self._requests[request_id] = request
1338    self._callbacks[request_id] = callback
1339    self._order.append(request_id)
1340
1341  def _execute(self, http, order, requests):
1342    """Serialize batch request, send to server, process response.
1343
1344    Args:
1345      http: httplib2.Http, an http object to be used to make the request with.
1346      order: list, list of request ids in the order they were added to the
1347        batch.
1348      request: list, list of request objects to send.
1349
1350    Raises:
1351      httplib2.HttpLib2Error if a transport error has occured.
1352      googleapiclient.errors.BatchError if the response is the wrong format.
1353    """
1354    message = MIMEMultipart('mixed')
1355    # Message should not write out it's own headers.
1356    setattr(message, '_write_headers', lambda self: None)
1357
1358    # Add all the individual requests.
1359    for request_id in order:
1360      request = requests[request_id]
1361
1362      msg = MIMENonMultipart('application', 'http')
1363      msg['Content-Transfer-Encoding'] = 'binary'
1364      msg['Content-ID'] = self._id_to_header(request_id)
1365
1366      body = self._serialize_request(request)
1367      msg.set_payload(body)
1368      message.attach(msg)
1369
1370    # encode the body: note that we can't use `as_string`, because
1371    # it plays games with `From ` lines.
1372    fp = StringIO()
1373    g = Generator(fp, mangle_from_=False)
1374    g.flatten(message, unixfrom=False)
1375    body = fp.getvalue()
1376
1377    headers = {}
1378    headers['content-type'] = ('multipart/mixed; '
1379                               'boundary="%s"') % message.get_boundary()
1380
1381    resp, content = http.request(self._batch_uri, method='POST', body=body,
1382                                 headers=headers)
1383
1384    if resp.status >= 300:
1385      raise HttpError(resp, content, uri=self._batch_uri)
1386
1387    # Prepend with a content-type header so FeedParser can handle it.
1388    header = 'content-type: %s\r\n\r\n' % resp['content-type']
1389    # PY3's FeedParser only accepts unicode. So we should decode content
1390    # here, and encode each payload again.
1391    if six.PY3:
1392      content = content.decode('utf-8')
1393    for_parser = header + content
1394
1395    parser = FeedParser()
1396    parser.feed(for_parser)
1397    mime_response = parser.close()
1398
1399    if not mime_response.is_multipart():
1400      raise BatchError("Response not in multipart/mixed format.", resp=resp,
1401                       content=content)
1402
1403    for part in mime_response.get_payload():
1404      request_id = self._header_to_id(part['Content-ID'])
1405      response, content = self._deserialize_response(part.get_payload())
1406      # We encode content here to emulate normal http response.
1407      if isinstance(content, six.text_type):
1408        content = content.encode('utf-8')
1409      self._responses[request_id] = (response, content)
1410
1411  @util.positional(1)
1412  def execute(self, http=None):
1413    """Execute all the requests as a single batched HTTP request.
1414
1415    Args:
1416      http: httplib2.Http, an http object to be used in place of the one the
1417        HttpRequest request object was constructed with. If one isn't supplied
1418        then use a http object from the requests in this batch.
1419
1420    Returns:
1421      None
1422
1423    Raises:
1424      httplib2.HttpLib2Error if a transport error has occured.
1425      googleapiclient.errors.BatchError if the response is the wrong format.
1426    """
1427    # If we have no requests return
1428    if len(self._order) == 0:
1429      return None
1430
1431    # If http is not supplied use the first valid one given in the requests.
1432    if http is None:
1433      for request_id in self._order:
1434        request = self._requests[request_id]
1435        if request is not None:
1436          http = request.http
1437          break
1438
1439    if http is None:
1440      raise ValueError("Missing a valid http object.")
1441
1442    # Special case for OAuth2Credentials-style objects which have not yet been
1443    # refreshed with an initial access_token.
1444    creds = _auth.get_credentials_from_http(http)
1445    if creds is not None:
1446      if not _auth.is_valid(creds):
1447        LOGGER.info('Attempting refresh to obtain initial access_token')
1448        _auth.refresh_credentials(creds)
1449
1450    self._execute(http, self._order, self._requests)
1451
1452    # Loop over all the requests and check for 401s. For each 401 request the
1453    # credentials should be refreshed and then sent again in a separate batch.
1454    redo_requests = {}
1455    redo_order = []
1456
1457    for request_id in self._order:
1458      resp, content = self._responses[request_id]
1459      if resp['status'] == '401':
1460        redo_order.append(request_id)
1461        request = self._requests[request_id]
1462        self._refresh_and_apply_credentials(request, http)
1463        redo_requests[request_id] = request
1464
1465    if redo_requests:
1466      self._execute(http, redo_order, redo_requests)
1467
1468    # Now process all callbacks that are erroring, and raise an exception for
1469    # ones that return a non-2xx response? Or add extra parameter to callback
1470    # that contains an HttpError?
1471
1472    for request_id in self._order:
1473      resp, content = self._responses[request_id]
1474
1475      request = self._requests[request_id]
1476      callback = self._callbacks[request_id]
1477
1478      response = None
1479      exception = None
1480      try:
1481        if resp.status >= 300:
1482          raise HttpError(resp, content, uri=request.uri)
1483        response = request.postproc(resp, content)
1484      except HttpError as e:
1485        exception = e
1486
1487      if callback is not None:
1488        callback(request_id, response, exception)
1489      if self._callback is not None:
1490        self._callback(request_id, response, exception)
1491
1492
1493class HttpRequestMock(object):
1494  """Mock of HttpRequest.
1495
1496  Do not construct directly, instead use RequestMockBuilder.
1497  """
1498
1499  def __init__(self, resp, content, postproc):
1500    """Constructor for HttpRequestMock
1501
1502    Args:
1503      resp: httplib2.Response, the response to emulate coming from the request
1504      content: string, the response body
1505      postproc: callable, the post processing function usually supplied by
1506                the model class. See model.JsonModel.response() as an example.
1507    """
1508    self.resp = resp
1509    self.content = content
1510    self.postproc = postproc
1511    if resp is None:
1512      self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1513    if 'reason' in self.resp:
1514      self.resp.reason = self.resp['reason']
1515
1516  def execute(self, http=None):
1517    """Execute the request.
1518
1519    Same behavior as HttpRequest.execute(), but the response is
1520    mocked and not really from an HTTP request/response.
1521    """
1522    return self.postproc(self.resp, self.content)
1523
1524
1525class RequestMockBuilder(object):
1526  """A simple mock of HttpRequest
1527
1528    Pass in a dictionary to the constructor that maps request methodIds to
1529    tuples of (httplib2.Response, content, opt_expected_body) that should be
1530    returned when that method is called. None may also be passed in for the
1531    httplib2.Response, in which case a 200 OK response will be generated.
1532    If an opt_expected_body (str or dict) is provided, it will be compared to
1533    the body and UnexpectedBodyError will be raised on inequality.
1534
1535    Example:
1536      response = '{"data": {"id": "tag:google.c...'
1537      requestBuilder = RequestMockBuilder(
1538        {
1539          'plus.activities.get': (None, response),
1540        }
1541      )
1542      googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1543
1544    Methods that you do not supply a response for will return a
1545    200 OK with an empty string as the response content or raise an excpetion
1546    if check_unexpected is set to True. The methodId is taken from the rpcName
1547    in the discovery document.
1548
1549    For more details see the project wiki.
1550  """
1551
1552  def __init__(self, responses, check_unexpected=False):
1553    """Constructor for RequestMockBuilder
1554
1555    The constructed object should be a callable object
1556    that can replace the class HttpResponse.
1557
1558    responses - A dictionary that maps methodIds into tuples
1559                of (httplib2.Response, content). The methodId
1560                comes from the 'rpcName' field in the discovery
1561                document.
1562    check_unexpected - A boolean setting whether or not UnexpectedMethodError
1563                       should be raised on unsupplied method.
1564    """
1565    self.responses = responses
1566    self.check_unexpected = check_unexpected
1567
1568  def __call__(self, http, postproc, uri, method='GET', body=None,
1569               headers=None, methodId=None, resumable=None):
1570    """Implements the callable interface that discovery.build() expects
1571    of requestBuilder, which is to build an object compatible with
1572    HttpRequest.execute(). See that method for the description of the
1573    parameters and the expected response.
1574    """
1575    if methodId in self.responses:
1576      response = self.responses[methodId]
1577      resp, content = response[:2]
1578      if len(response) > 2:
1579        # Test the body against the supplied expected_body.
1580        expected_body = response[2]
1581        if bool(expected_body) != bool(body):
1582          # Not expecting a body and provided one
1583          # or expecting a body and not provided one.
1584          raise UnexpectedBodyError(expected_body, body)
1585        if isinstance(expected_body, str):
1586          expected_body = json.loads(expected_body)
1587        body = json.loads(body)
1588        if body != expected_body:
1589          raise UnexpectedBodyError(expected_body, body)
1590      return HttpRequestMock(resp, content, postproc)
1591    elif self.check_unexpected:
1592      raise UnexpectedMethodError(methodId=methodId)
1593    else:
1594      model = JsonModel(False)
1595      return HttpRequestMock(None, '{}', model.response)
1596
1597
1598class HttpMock(object):
1599  """Mock of httplib2.Http"""
1600
1601  def __init__(self, filename=None, headers=None):
1602    """
1603    Args:
1604      filename: string, absolute filename to read response from
1605      headers: dict, header to return with response
1606    """
1607    if headers is None:
1608      headers = {'status': '200'}
1609    if filename:
1610      f = open(filename, 'rb')
1611      self.data = f.read()
1612      f.close()
1613    else:
1614      self.data = None
1615    self.response_headers = headers
1616    self.headers = None
1617    self.uri = None
1618    self.method = None
1619    self.body = None
1620    self.headers = None
1621
1622
1623  def request(self, uri,
1624              method='GET',
1625              body=None,
1626              headers=None,
1627              redirections=1,
1628              connection_type=None):
1629    self.uri = uri
1630    self.method = method
1631    self.body = body
1632    self.headers = headers
1633    return httplib2.Response(self.response_headers), self.data
1634
1635
1636class HttpMockSequence(object):
1637  """Mock of httplib2.Http
1638
1639  Mocks a sequence of calls to request returning different responses for each
1640  call. Create an instance initialized with the desired response headers
1641  and content and then use as if an httplib2.Http instance.
1642
1643    http = HttpMockSequence([
1644      ({'status': '401'}, ''),
1645      ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1646      ({'status': '200'}, 'echo_request_headers'),
1647      ])
1648    resp, content = http.request("http://examples.com")
1649
1650  There are special values you can pass in for content to trigger
1651  behavours that are helpful in testing.
1652
1653  'echo_request_headers' means return the request headers in the response body
1654  'echo_request_headers_as_json' means return the request headers in
1655     the response body
1656  'echo_request_body' means return the request body in the response body
1657  'echo_request_uri' means return the request uri in the response body
1658  """
1659
1660  def __init__(self, iterable):
1661    """
1662    Args:
1663      iterable: iterable, a sequence of pairs of (headers, body)
1664    """
1665    self._iterable = iterable
1666    self.follow_redirects = True
1667
1668  def request(self, uri,
1669              method='GET',
1670              body=None,
1671              headers=None,
1672              redirections=1,
1673              connection_type=None):
1674    resp, content = self._iterable.pop(0)
1675    if content == 'echo_request_headers':
1676      content = headers
1677    elif content == 'echo_request_headers_as_json':
1678      content = json.dumps(headers)
1679    elif content == 'echo_request_body':
1680      if hasattr(body, 'read'):
1681        content = body.read()
1682      else:
1683        content = body
1684    elif content == 'echo_request_uri':
1685      content = uri
1686    if isinstance(content, six.text_type):
1687      content = content.encode('utf-8')
1688    return httplib2.Response(resp), content
1689
1690
1691def set_user_agent(http, user_agent):
1692  """Set the user-agent on every request.
1693
1694  Args:
1695     http - An instance of httplib2.Http
1696         or something that acts like it.
1697     user_agent: string, the value for the user-agent header.
1698
1699  Returns:
1700     A modified instance of http that was passed in.
1701
1702  Example:
1703
1704    h = httplib2.Http()
1705    h = set_user_agent(h, "my-app-name/6.0")
1706
1707  Most of the time the user-agent will be set doing auth, this is for the rare
1708  cases where you are accessing an unauthenticated endpoint.
1709  """
1710  request_orig = http.request
1711
1712  # The closure that will replace 'httplib2.Http.request'.
1713  def new_request(uri, method='GET', body=None, headers=None,
1714                  redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1715                  connection_type=None):
1716    """Modify the request headers to add the user-agent."""
1717    if headers is None:
1718      headers = {}
1719    if 'user-agent' in headers:
1720      headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1721    else:
1722      headers['user-agent'] = user_agent
1723    resp, content = request_orig(uri, method, body, headers,
1724                        redirections, connection_type)
1725    return resp, content
1726
1727  http.request = new_request
1728  return http
1729
1730
1731def tunnel_patch(http):
1732  """Tunnel PATCH requests over POST.
1733  Args:
1734     http - An instance of httplib2.Http
1735         or something that acts like it.
1736
1737  Returns:
1738     A modified instance of http that was passed in.
1739
1740  Example:
1741
1742    h = httplib2.Http()
1743    h = tunnel_patch(h, "my-app-name/6.0")
1744
1745  Useful if you are running on a platform that doesn't support PATCH.
1746  Apply this last if you are using OAuth 1.0, as changing the method
1747  will result in a different signature.
1748  """
1749  request_orig = http.request
1750
1751  # The closure that will replace 'httplib2.Http.request'.
1752  def new_request(uri, method='GET', body=None, headers=None,
1753                  redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1754                  connection_type=None):
1755    """Modify the request headers to add the user-agent."""
1756    if headers is None:
1757      headers = {}
1758    if method == 'PATCH':
1759      if 'oauth_token' in headers.get('authorization', ''):
1760        LOGGER.warning(
1761            'OAuth 1.0 request made with Credentials after tunnel_patch.')
1762      headers['x-http-method-override'] = "PATCH"
1763      method = 'POST'
1764    resp, content = request_orig(uri, method, body, headers,
1765                        redirections, connection_type)
1766    return resp, content
1767
1768  http.request = new_request
1769  return http
1770
1771
1772def build_http():
1773  """Builds httplib2.Http object
1774
1775  Returns:
1776  A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1777  To override default timeout call
1778
1779    socket.setdefaulttimeout(timeout_in_sec)
1780
1781  before interacting with this method.
1782  """
1783  if socket.getdefaulttimeout() is not None:
1784    http_timeout = socket.getdefaulttimeout()
1785  else:
1786    http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1787  return httplib2.Http(timeout=http_timeout)
1788