Package googleapiclient :: Module http |
[hide private]
[frames] | no frames]

# Source Code for Module googleapiclient.http

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