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