| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962 |
- # Copyright 2014 Google Inc. All Rights Reserved.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """Classes to encapsulate a single HTTP request.
- The classes implement a command pattern, with every
- object supporting an execute() method that does the
- actual HTTP request.
- """
- from __future__ import absolute_import
- __author__ = "jcgregorio@google.com (Joe Gregorio)"
- import copy
- import http.client as http_client
- import io
- import json
- import logging
- import mimetypes
- import os
- import random
- import socket
- import time
- import urllib
- import uuid
- import httplib2
- # TODO(issue 221): Remove this conditional import jibbajabba.
- try:
- import ssl
- except ImportError:
- _ssl_SSLError = object()
- else:
- _ssl_SSLError = ssl.SSLError
- from email.generator import Generator
- from email.mime.multipart import MIMEMultipart
- from email.mime.nonmultipart import MIMENonMultipart
- from email.parser import FeedParser
- from googleapiclient import _auth
- from googleapiclient import _helpers as util
- from googleapiclient.errors import (
- BatchError,
- HttpError,
- InvalidChunkSizeError,
- ResumableUploadError,
- UnexpectedBodyError,
- UnexpectedMethodError,
- )
- from googleapiclient.model import JsonModel
- LOGGER = logging.getLogger(__name__)
- DEFAULT_CHUNK_SIZE = 100 * 1024 * 1024
- MAX_URI_LENGTH = 2048
- MAX_BATCH_LIMIT = 1000
- _TOO_MANY_REQUESTS = 429
- DEFAULT_HTTP_TIMEOUT_SEC = 60
- _LEGACY_BATCH_URI = "https://www.googleapis.com/batch"
- def _should_retry_response(resp_status, content):
- """Determines whether a response should be retried.
- Args:
- resp_status: The response status received.
- content: The response content body.
- Returns:
- True if the response should be retried, otherwise False.
- """
- reason = None
- # Retry on 5xx errors.
- if resp_status >= 500:
- return True
- # Retry on 429 errors.
- if resp_status == _TOO_MANY_REQUESTS:
- return True
- # For 403 errors, we have to check for the `reason` in the response to
- # determine if we should retry.
- if resp_status == http_client.FORBIDDEN:
- # If there's no details about the 403 type, don't retry.
- if not content:
- return False
- # Content is in JSON format.
- try:
- data = json.loads(content.decode("utf-8"))
- if isinstance(data, dict):
- # There are many variations of the error json so we need
- # to determine the keyword which has the error detail. Make sure
- # that the order of the keywords below isn't changed as it can
- # break user code. If the "errors" key exists, we must use that
- # first.
- # See Issue #1243
- # https://github.com/googleapis/google-api-python-client/issues/1243
- error_detail_keyword = next(
- (
- kw
- for kw in ["errors", "status", "message"]
- if kw in data["error"]
- ),
- "",
- )
- if error_detail_keyword:
- reason = data["error"][error_detail_keyword]
- if isinstance(reason, list) and len(reason) > 0:
- reason = reason[0]
- if "reason" in reason:
- reason = reason["reason"]
- else:
- reason = data[0]["error"]["errors"]["reason"]
- except (UnicodeDecodeError, ValueError, KeyError):
- LOGGER.warning("Invalid JSON content from response: %s", content)
- return False
- LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
- # Only retry on rate limit related failures.
- if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
- return True
- # Everything else is a success or non-retriable so break.
- return False
- def _retry_request(
- http, num_retries, req_type, sleep, rand, uri, method, *args, **kwargs
- ):
- """Retries an HTTP request multiple times while handling errors.
- If after all retries the request still fails, last error is either returned as
- return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
- Args:
- http: Http object to be used to execute request.
- num_retries: Maximum number of retries.
- req_type: Type of the request (used for logging retries).
- sleep, rand: Functions to sleep for random time between retries.
- uri: URI to be requested.
- method: HTTP method to be used.
- args, kwargs: Additional arguments passed to http.request.
- Returns:
- resp, content - Response from the http request (may be HTTP 5xx).
- """
- resp = None
- content = None
- exception = None
- for retry_num in range(num_retries + 1):
- if retry_num > 0:
- # Sleep before retrying.
- sleep_time = rand() * 2**retry_num
- LOGGER.warning(
- "Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s",
- sleep_time,
- retry_num,
- num_retries,
- req_type,
- method,
- uri,
- resp.status if resp else exception,
- )
- sleep(sleep_time)
- try:
- exception = None
- resp, content = http.request(uri, method, *args, **kwargs)
- # Retry on SSL errors and socket timeout errors.
- except _ssl_SSLError as ssl_error:
- exception = ssl_error
- except socket.timeout as socket_timeout:
- # Needs to be before socket.error as it's a subclass of OSError
- # socket.timeout has no errorcode
- exception = socket_timeout
- except ConnectionError as connection_error:
- # Needs to be before socket.error as it's a subclass of OSError
- exception = connection_error
- except OSError as socket_error:
- # errno's contents differ by platform, so we have to match by name.
- # Some of these same errors may have been caught above, e.g. ECONNRESET *should* be
- # raised as a ConnectionError, but some libraries will raise it as a socket.error
- # with an errno corresponding to ECONNRESET
- if socket.errno.errorcode.get(socket_error.errno) not in {
- "WSAETIMEDOUT",
- "ETIMEDOUT",
- "EPIPE",
- "ECONNABORTED",
- "ECONNREFUSED",
- "ECONNRESET",
- }:
- raise
- exception = socket_error
- except httplib2.ServerNotFoundError as server_not_found_error:
- exception = server_not_found_error
- if exception:
- if retry_num == num_retries:
- raise exception
- else:
- continue
- if not _should_retry_response(resp.status, content):
- break
- return resp, content
- class MediaUploadProgress(object):
- """Status of a resumable upload."""
- def __init__(self, resumable_progress, total_size):
- """Constructor.
- Args:
- resumable_progress: int, bytes sent so far.
- total_size: int, total bytes in complete upload, or None if the total
- upload size isn't known ahead of time.
- """
- self.resumable_progress = resumable_progress
- self.total_size = total_size
- def progress(self):
- """Percent of upload completed, as a float.
- Returns:
- the percentage complete as a float, returning 0.0 if the total size of
- the upload is unknown.
- """
- if self.total_size is not None and self.total_size != 0:
- return float(self.resumable_progress) / float(self.total_size)
- else:
- return 0.0
- class MediaDownloadProgress(object):
- """Status of a resumable download."""
- def __init__(self, resumable_progress, total_size):
- """Constructor.
- Args:
- resumable_progress: int, bytes received so far.
- total_size: int, total bytes in complete download.
- """
- self.resumable_progress = resumable_progress
- self.total_size = total_size
- def progress(self):
- """Percent of download completed, as a float.
- Returns:
- the percentage complete as a float, returning 0.0 if the total size of
- the download is unknown.
- """
- if self.total_size is not None and self.total_size != 0:
- return float(self.resumable_progress) / float(self.total_size)
- else:
- return 0.0
- class MediaUpload(object):
- """Describes a media object to upload.
- Base class that defines the interface of MediaUpload subclasses.
- Note that subclasses of MediaUpload may allow you to control the chunksize
- when uploading a media object. It is important to keep the size of the chunk
- as large as possible to keep the upload efficient. Other factors may influence
- the size of the chunk you use, particularly if you are working in an
- environment where individual HTTP requests may have a hardcoded time limit,
- such as under certain classes of requests under Google App Engine.
- Streams are io.Base compatible objects that support seek(). Some MediaUpload
- subclasses support using streams directly to upload data. Support for
- streaming may be indicated by a MediaUpload sub-class and if appropriate for a
- platform that stream will be used for uploading the media object. The support
- for streaming is indicated by has_stream() returning True. The stream() method
- should return an io.Base object that supports seek(). On platforms where the
- underlying httplib module supports streaming, for example Python 2.6 and
- later, the stream will be passed into the http library which will result in
- less memory being used and possibly faster uploads.
- If you need to upload media that can't be uploaded using any of the existing
- MediaUpload sub-class then you can sub-class MediaUpload for your particular
- needs.
- """
- def chunksize(self):
- """Chunk size for resumable uploads.
- Returns:
- Chunk size in bytes.
- """
- raise NotImplementedError()
- def mimetype(self):
- """Mime type of the body.
- Returns:
- Mime type.
- """
- return "application/octet-stream"
- def size(self):
- """Size of upload.
- Returns:
- Size of the body, or None of the size is unknown.
- """
- return None
- def resumable(self):
- """Whether this upload is resumable.
- Returns:
- True if resumable upload or False.
- """
- return False
- def getbytes(self, begin, end):
- """Get bytes from the media.
- Args:
- begin: int, offset from beginning of file.
- length: int, number of bytes to read, starting at begin.
- Returns:
- A string of bytes read. May be shorter than length if EOF was reached
- first.
- """
- raise NotImplementedError()
- def has_stream(self):
- """Does the underlying upload support a streaming interface.
- Streaming means it is an io.IOBase subclass that supports seek, i.e.
- seekable() returns True.
- Returns:
- True if the call to stream() will return an instance of a seekable io.Base
- subclass.
- """
- return False
- def stream(self):
- """A stream interface to the data being uploaded.
- Returns:
- The returned value is an io.IOBase subclass that supports seek, i.e.
- seekable() returns True.
- """
- raise NotImplementedError()
- @util.positional(1)
- def _to_json(self, strip=None):
- """Utility function for creating a JSON representation of a MediaUpload.
- Args:
- strip: array, An array of names of members to not include in the JSON.
- Returns:
- string, a JSON representation of this instance, suitable to pass to
- from_json().
- """
- t = type(self)
- d = copy.copy(self.__dict__)
- if strip is not None:
- for member in strip:
- del d[member]
- d["_class"] = t.__name__
- d["_module"] = t.__module__
- return json.dumps(d)
- def to_json(self):
- """Create a JSON representation of an instance of MediaUpload.
- Returns:
- string, a JSON representation of this instance, suitable to pass to
- from_json().
- """
- return self._to_json()
- @classmethod
- def new_from_json(cls, s):
- """Utility class method to instantiate a MediaUpload subclass from a JSON
- representation produced by to_json().
- Args:
- s: string, JSON from to_json().
- Returns:
- An instance of the subclass of MediaUpload that was serialized with
- to_json().
- """
- data = json.loads(s)
- # Find and call the right classmethod from_json() to restore the object.
- module = data["_module"]
- m = __import__(module, fromlist=module.split(".")[:-1])
- kls = getattr(m, data["_class"])
- from_json = getattr(kls, "from_json")
- return from_json(s)
- class MediaIoBaseUpload(MediaUpload):
- """A MediaUpload for a io.Base objects.
- Note that the Python file object is compatible with io.Base and can be used
- with this class also.
- fh = BytesIO('...Some data to upload...')
- media = MediaIoBaseUpload(fh, mimetype='image/png',
- chunksize=1024*1024, resumable=True)
- farm.animals().insert(
- id='cow',
- name='cow.png',
- media_body=media).execute()
- Depending on the platform you are working on, you may pass -1 as the
- chunksize, which indicates that the entire file should be uploaded in a single
- request. If the underlying platform supports streams, such as Python 2.6 or
- later, then this can be very efficient as it avoids multiple connections, and
- also avoids loading the entire file into memory before sending it. Note that
- Google App Engine has a 5MB limit on request size, so you should never set
- your chunksize larger than 5MB, or to -1.
- """
- @util.positional(3)
- def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
- """Constructor.
- Args:
- fd: io.Base or file object, The source of the bytes to upload. MUST be
- opened in blocking mode, do not use streams opened in non-blocking mode.
- The given stream must be seekable, that is, it must be able to call
- seek() on fd.
- mimetype: string, Mime-type of the file.
- chunksize: int, File will be uploaded in chunks of this many bytes. Only
- used if resumable=True. Pass in a value of -1 if the file is to be
- uploaded as a single chunk. Note that Google App Engine has a 5MB limit
- on request size, so you should never set your chunksize larger than 5MB,
- or to -1.
- resumable: bool, True if this is a resumable upload. False means upload
- in a single request.
- """
- super(MediaIoBaseUpload, self).__init__()
- self._fd = fd
- self._mimetype = mimetype
- if not (chunksize == -1 or chunksize > 0):
- raise InvalidChunkSizeError()
- self._chunksize = chunksize
- self._resumable = resumable
- self._fd.seek(0, os.SEEK_END)
- self._size = self._fd.tell()
- def chunksize(self):
- """Chunk size for resumable uploads.
- Returns:
- Chunk size in bytes.
- """
- return self._chunksize
- def mimetype(self):
- """Mime type of the body.
- Returns:
- Mime type.
- """
- return self._mimetype
- def size(self):
- """Size of upload.
- Returns:
- Size of the body, or None of the size is unknown.
- """
- return self._size
- def resumable(self):
- """Whether this upload is resumable.
- Returns:
- True if resumable upload or False.
- """
- return self._resumable
- def getbytes(self, begin, length):
- """Get bytes from the media.
- Args:
- begin: int, offset from beginning of file.
- length: int, number of bytes to read, starting at begin.
- Returns:
- A string of bytes read. May be shorted than length if EOF was reached
- first.
- """
- self._fd.seek(begin)
- return self._fd.read(length)
- def has_stream(self):
- """Does the underlying upload support a streaming interface.
- Streaming means it is an io.IOBase subclass that supports seek, i.e.
- seekable() returns True.
- Returns:
- True if the call to stream() will return an instance of a seekable io.Base
- subclass.
- """
- return True
- def stream(self):
- """A stream interface to the data being uploaded.
- Returns:
- The returned value is an io.IOBase subclass that supports seek, i.e.
- seekable() returns True.
- """
- return self._fd
- def to_json(self):
- """This upload type is not serializable."""
- raise NotImplementedError("MediaIoBaseUpload is not serializable.")
- class MediaFileUpload(MediaIoBaseUpload):
- """A MediaUpload for a file.
- Construct a MediaFileUpload and pass as the media_body parameter of the
- method. For example, if we had a service that allowed uploading images:
- media = MediaFileUpload('cow.png', mimetype='image/png',
- chunksize=1024*1024, resumable=True)
- farm.animals().insert(
- id='cow',
- name='cow.png',
- media_body=media).execute()
- Depending on the platform you are working on, you may pass -1 as the
- chunksize, which indicates that the entire file should be uploaded in a single
- request. If the underlying platform supports streams, such as Python 2.6 or
- later, then this can be very efficient as it avoids multiple connections, and
- also avoids loading the entire file into memory before sending it. Note that
- Google App Engine has a 5MB limit on request size, so you should never set
- your chunksize larger than 5MB, or to -1.
- """
- @util.positional(2)
- def __init__(
- self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False
- ):
- """Constructor.
- Args:
- filename: string, Name of the file.
- mimetype: string, Mime-type of the file. If None then a mime-type will be
- guessed from the file extension.
- chunksize: int, File will be uploaded in chunks of this many bytes. Only
- used if resumable=True. Pass in a value of -1 if the file is to be
- uploaded in a single chunk. Note that Google App Engine has a 5MB limit
- on request size, so you should never set your chunksize larger than 5MB,
- or to -1.
- resumable: bool, True if this is a resumable upload. False means upload
- in a single request.
- """
- self._fd = None
- self._filename = filename
- self._fd = open(self._filename, "rb")
- if mimetype is None:
- # No mimetype provided, make a guess.
- mimetype, _ = mimetypes.guess_type(filename)
- if mimetype is None:
- # Guess failed, use octet-stream.
- mimetype = "application/octet-stream"
- super(MediaFileUpload, self).__init__(
- self._fd, mimetype, chunksize=chunksize, resumable=resumable
- )
- def __del__(self):
- if self._fd:
- self._fd.close()
- def to_json(self):
- """Creating a JSON representation of an instance of MediaFileUpload.
- Returns:
- string, a JSON representation of this instance, suitable to pass to
- from_json().
- """
- return self._to_json(strip=["_fd"])
- @staticmethod
- def from_json(s):
- d = json.loads(s)
- return MediaFileUpload(
- d["_filename"],
- mimetype=d["_mimetype"],
- chunksize=d["_chunksize"],
- resumable=d["_resumable"],
- )
- class MediaInMemoryUpload(MediaIoBaseUpload):
- """MediaUpload for a chunk of bytes.
- DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or io.StringIO for
- the stream.
- """
- @util.positional(2)
- def __init__(
- self,
- body,
- mimetype="application/octet-stream",
- chunksize=DEFAULT_CHUNK_SIZE,
- resumable=False,
- ):
- """Create a new MediaInMemoryUpload.
- DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or io.StringIO for
- the stream.
- Args:
- body: string, Bytes of body content.
- mimetype: string, Mime-type of the file or default of
- 'application/octet-stream'.
- chunksize: int, File will be uploaded in chunks of this many bytes. Only
- used if resumable=True.
- resumable: bool, True if this is a resumable upload. False means upload
- in a single request.
- """
- fd = io.BytesIO(body)
- super(MediaInMemoryUpload, self).__init__(
- fd, mimetype, chunksize=chunksize, resumable=resumable
- )
- class MediaIoBaseDownload(object):
- """ "Download media resources.
- Note that the Python file object is compatible with io.Base and can be used
- with this class also.
- Example:
- request = farms.animals().get_media(id='cow')
- fh = io.FileIO('cow.png', mode='wb')
- downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
- done = False
- while done is False:
- status, done = downloader.next_chunk()
- if status:
- print "Download %d%%." % int(status.progress() * 100)
- print "Download Complete!"
- """
- @util.positional(3)
- def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
- """Constructor.
- Args:
- fd: io.Base or file object, The stream in which to write the downloaded
- bytes.
- request: googleapiclient.http.HttpRequest, the media request to perform in
- chunks.
- chunksize: int, File will be downloaded in chunks of this many bytes.
- """
- self._fd = fd
- self._request = request
- self._uri = request.uri
- self._chunksize = chunksize
- self._progress = 0
- self._total_size = None
- self._done = False
- # Stubs for testing.
- self._sleep = time.sleep
- self._rand = random.random
- self._headers = {}
- for k, v in request.headers.items():
- # allow users to supply custom headers by setting them on the request
- # but strip out the ones that are set by default on requests generated by
- # API methods like Drive's files().get(fileId=...)
- if not k.lower() in ("accept", "accept-encoding", "user-agent"):
- self._headers[k] = v
- @util.positional(1)
- def next_chunk(self, num_retries=0):
- """Get the next chunk of the download.
- Args:
- num_retries: Integer, number of times to retry with randomized
- exponential backoff. If all retries fail, the raised HttpError
- represents the last request. If zero (default), we attempt the
- request only once.
- Returns:
- (status, done): (MediaDownloadProgress, boolean)
- The value of 'done' will be True when the media has been fully
- downloaded or the total size of the media is unknown.
- Raises:
- googleapiclient.errors.HttpError if the response was not a 2xx.
- httplib2.HttpLib2Error if a transport error has occurred.
- """
- headers = self._headers.copy()
- headers["range"] = "bytes=%d-%d" % (
- self._progress,
- self._progress + self._chunksize - 1,
- )
- http = self._request.http
- resp, content = _retry_request(
- http,
- num_retries,
- "media download",
- self._sleep,
- self._rand,
- self._uri,
- "GET",
- headers=headers,
- )
- if resp.status in [200, 206]:
- if "content-location" in resp and resp["content-location"] != self._uri:
- self._uri = resp["content-location"]
- self._progress += len(content)
- self._fd.write(content)
- if "content-range" in resp:
- content_range = resp["content-range"]
- length = content_range.rsplit("/", 1)[1]
- self._total_size = int(length)
- elif "content-length" in resp:
- self._total_size = int(resp["content-length"])
- if self._total_size is None or self._progress == self._total_size:
- self._done = True
- return MediaDownloadProgress(self._progress, self._total_size), self._done
- elif resp.status == 416:
- # 416 is Range Not Satisfiable
- # This typically occurs with a zero byte file
- content_range = resp["content-range"]
- length = content_range.rsplit("/", 1)[1]
- self._total_size = int(length)
- if self._total_size == 0:
- self._done = True
- return (
- MediaDownloadProgress(self._progress, self._total_size),
- self._done,
- )
- raise HttpError(resp, content, uri=self._uri)
- class _StreamSlice(object):
- """Truncated stream.
- Takes a stream and presents a stream that is a slice of the original stream.
- This is used when uploading media in chunks. In later versions of Python a
- stream can be passed to httplib in place of the string of data to send. The
- problem is that httplib just blindly reads to the end of the stream. This
- wrapper presents a virtual stream that only reads to the end of the chunk.
- """
- def __init__(self, stream, begin, chunksize):
- """Constructor.
- Args:
- stream: (io.Base, file object), the stream to wrap.
- begin: int, the seek position the chunk begins at.
- chunksize: int, the size of the chunk.
- """
- self._stream = stream
- self._begin = begin
- self._chunksize = chunksize
- self._stream.seek(begin)
- def read(self, n=-1):
- """Read n bytes.
- Args:
- n, int, the number of bytes to read.
- Returns:
- A string of length 'n', or less if EOF is reached.
- """
- # The data left available to read sits in [cur, end)
- cur = self._stream.tell()
- end = self._begin + self._chunksize
- if n == -1 or cur + n > end:
- n = end - cur
- return self._stream.read(n)
- class HttpRequest(object):
- """Encapsulates a single HTTP request."""
- @util.positional(4)
- def __init__(
- self,
- http,
- postproc,
- uri,
- method="GET",
- body=None,
- headers=None,
- methodId=None,
- resumable=None,
- ):
- """Constructor for an HttpRequest.
- Args:
- http: httplib2.Http, the transport object to use to make a request
- postproc: callable, called on the HTTP response and content to transform
- it into a data object before returning, or raising an exception
- on an error.
- uri: string, the absolute URI to send the request to
- method: string, the HTTP method to use
- body: string, the request body of the HTTP request,
- headers: dict, the HTTP request headers
- methodId: string, a unique identifier for the API method being called.
- resumable: MediaUpload, None if this is not a resumbale request.
- """
- self.uri = uri
- self.method = method
- self.body = body
- self.headers = headers or {}
- self.methodId = methodId
- self.http = http
- self.postproc = postproc
- self.resumable = resumable
- self.response_callbacks = []
- self._in_error_state = False
- # The size of the non-media part of the request.
- self.body_size = len(self.body or "")
- # The resumable URI to send chunks to.
- self.resumable_uri = None
- # The bytes that have been uploaded.
- self.resumable_progress = 0
- # Stubs for testing.
- self._rand = random.random
- self._sleep = time.sleep
- @util.positional(1)
- def execute(self, http=None, num_retries=0):
- """Execute the request.
- Args:
- http: httplib2.Http, an http object to be used in place of the
- one the HttpRequest request object was constructed with.
- num_retries: Integer, number of times to retry with randomized
- exponential backoff. If all retries fail, the raised HttpError
- represents the last request. If zero (default), we attempt the
- request only once.
- Returns:
- A deserialized object model of the response body as determined
- by the postproc.
- Raises:
- googleapiclient.errors.HttpError if the response was not a 2xx.
- httplib2.HttpLib2Error if a transport error has occurred.
- """
- if http is None:
- http = self.http
- if self.resumable:
- body = None
- while body is None:
- _, body = self.next_chunk(http=http, num_retries=num_retries)
- return body
- # Non-resumable case.
- if "content-length" not in self.headers:
- self.headers["content-length"] = str(self.body_size)
- # If the request URI is too long then turn it into a POST request.
- # Assume that a GET request never contains a request body.
- if len(self.uri) > MAX_URI_LENGTH and self.method == "GET":
- self.method = "POST"
- self.headers["x-http-method-override"] = "GET"
- self.headers["content-type"] = "application/x-www-form-urlencoded"
- parsed = urllib.parse.urlparse(self.uri)
- self.uri = urllib.parse.urlunparse(
- (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
- )
- self.body = parsed.query
- self.headers["content-length"] = str(len(self.body))
- # Handle retries for server-side errors.
- resp, content = _retry_request(
- http,
- num_retries,
- "request",
- self._sleep,
- self._rand,
- str(self.uri),
- method=str(self.method),
- body=self.body,
- headers=self.headers,
- )
- for callback in self.response_callbacks:
- callback(resp)
- if resp.status >= 300:
- raise HttpError(resp, content, uri=self.uri)
- return self.postproc(resp, content)
- @util.positional(2)
- def add_response_callback(self, cb):
- """add_response_headers_callback
- Args:
- cb: Callback to be called on receiving the response headers, of signature:
- def cb(resp):
- # Where resp is an instance of httplib2.Response
- """
- self.response_callbacks.append(cb)
- @util.positional(1)
- def next_chunk(self, http=None, num_retries=0):
- """Execute the next step of a resumable upload.
- Can only be used if the method being executed supports media uploads and
- the MediaUpload object passed in was flagged as using resumable upload.
- Example:
- media = MediaFileUpload('cow.png', mimetype='image/png',
- chunksize=1000, resumable=True)
- request = farm.animals().insert(
- id='cow',
- name='cow.png',
- media_body=media)
- response = None
- while response is None:
- status, response = request.next_chunk()
- if status:
- print "Upload %d%% complete." % int(status.progress() * 100)
- Args:
- http: httplib2.Http, an http object to be used in place of the
- one the HttpRequest request object was constructed with.
- num_retries: Integer, number of times to retry with randomized
- exponential backoff. If all retries fail, the raised HttpError
- represents the last request. If zero (default), we attempt the
- request only once.
- Returns:
- (status, body): (ResumableMediaStatus, object)
- The body will be None until the resumable media is fully uploaded.
- Raises:
- googleapiclient.errors.HttpError if the response was not a 2xx.
- httplib2.HttpLib2Error if a transport error has occurred.
- """
- if http is None:
- http = self.http
- if self.resumable.size() is None:
- size = "*"
- else:
- size = str(self.resumable.size())
- if self.resumable_uri is None:
- start_headers = copy.copy(self.headers)
- start_headers["X-Upload-Content-Type"] = self.resumable.mimetype()
- if size != "*":
- start_headers["X-Upload-Content-Length"] = size
- start_headers["content-length"] = str(self.body_size)
- resp, content = _retry_request(
- http,
- num_retries,
- "resumable URI request",
- self._sleep,
- self._rand,
- self.uri,
- method=self.method,
- body=self.body,
- headers=start_headers,
- )
- if resp.status == 200 and "location" in resp:
- self.resumable_uri = resp["location"]
- else:
- raise ResumableUploadError(resp, content)
- elif self._in_error_state:
- # If we are in an error state then query the server for current state of
- # the upload by sending an empty PUT and reading the 'range' header in
- # the response.
- headers = {"Content-Range": "bytes */%s" % size, "content-length": "0"}
- resp, content = http.request(self.resumable_uri, "PUT", headers=headers)
- status, body = self._process_response(resp, content)
- if body:
- # The upload was complete.
- return (status, body)
- if self.resumable.has_stream():
- data = self.resumable.stream()
- if self.resumable.chunksize() == -1:
- data.seek(self.resumable_progress)
- chunk_end = self.resumable.size() - self.resumable_progress - 1
- else:
- # Doing chunking with a stream, so wrap a slice of the stream.
- data = _StreamSlice(
- data, self.resumable_progress, self.resumable.chunksize()
- )
- chunk_end = min(
- self.resumable_progress + self.resumable.chunksize() - 1,
- self.resumable.size() - 1,
- )
- else:
- data = self.resumable.getbytes(
- self.resumable_progress, self.resumable.chunksize()
- )
- # A short read implies that we are at EOF, so finish the upload.
- if len(data) < self.resumable.chunksize():
- size = str(self.resumable_progress + len(data))
- chunk_end = self.resumable_progress + len(data) - 1
- headers = {
- # Must set the content-length header here because httplib can't
- # calculate the size when working with _StreamSlice.
- "Content-Length": str(chunk_end - self.resumable_progress + 1),
- }
- # An empty file results in chunk_end = -1 and size = 0
- # sending "bytes 0--1/0" results in an invalid request
- # Only add header "Content-Range" if chunk_end != -1
- if chunk_end != -1:
- headers["Content-Range"] = "bytes %d-%d/%s" % (
- self.resumable_progress,
- chunk_end,
- size,
- )
- for retry_num in range(num_retries + 1):
- if retry_num > 0:
- self._sleep(self._rand() * 2**retry_num)
- LOGGER.warning(
- "Retry #%d for media upload: %s %s, following status: %d"
- % (retry_num, self.method, self.uri, resp.status)
- )
- try:
- resp, content = http.request(
- self.resumable_uri, method="PUT", body=data, headers=headers
- )
- except:
- self._in_error_state = True
- raise
- if not _should_retry_response(resp.status, content):
- break
- return self._process_response(resp, content)
- def _process_response(self, resp, content):
- """Process the response from a single chunk upload.
- Args:
- resp: httplib2.Response, the response object.
- content: string, the content of the response.
- Returns:
- (status, body): (ResumableMediaStatus, object)
- The body will be None until the resumable media is fully uploaded.
- Raises:
- googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
- """
- if resp.status in [200, 201]:
- self._in_error_state = False
- return None, self.postproc(resp, content)
- elif resp.status == 308:
- self._in_error_state = False
- # A "308 Resume Incomplete" indicates we are not done.
- try:
- self.resumable_progress = int(resp["range"].split("-")[1]) + 1
- except KeyError:
- # If resp doesn't contain range header, resumable progress is 0
- self.resumable_progress = 0
- if "location" in resp:
- self.resumable_uri = resp["location"]
- else:
- self._in_error_state = True
- raise HttpError(resp, content, uri=self.uri)
- return (
- MediaUploadProgress(self.resumable_progress, self.resumable.size()),
- None,
- )
- def to_json(self):
- """Returns a JSON representation of the HttpRequest."""
- d = copy.copy(self.__dict__)
- if d["resumable"] is not None:
- d["resumable"] = self.resumable.to_json()
- del d["http"]
- del d["postproc"]
- del d["_sleep"]
- del d["_rand"]
- return json.dumps(d)
- @staticmethod
- def from_json(s, http, postproc):
- """Returns an HttpRequest populated with info from a JSON object."""
- d = json.loads(s)
- if d["resumable"] is not None:
- d["resumable"] = MediaUpload.new_from_json(d["resumable"])
- return HttpRequest(
- http,
- postproc,
- uri=d["uri"],
- method=d["method"],
- body=d["body"],
- headers=d["headers"],
- methodId=d["methodId"],
- resumable=d["resumable"],
- )
- @staticmethod
- def null_postproc(resp, contents):
- return resp, contents
- class BatchHttpRequest(object):
- """Batches multiple HttpRequest objects into a single HTTP request.
- Example:
- from googleapiclient.http import BatchHttpRequest
- def list_animals(request_id, response, exception):
- \"\"\"Do something with the animals list response.\"\"\"
- if exception is not None:
- # Do something with the exception.
- pass
- else:
- # Do something with the response.
- pass
- def list_farmers(request_id, response, exception):
- \"\"\"Do something with the farmers list response.\"\"\"
- if exception is not None:
- # Do something with the exception.
- pass
- else:
- # Do something with the response.
- pass
- service = build('farm', 'v2')
- batch = BatchHttpRequest()
- batch.add(service.animals().list(), list_animals)
- batch.add(service.farmers().list(), list_farmers)
- batch.execute(http=http)
- """
- @util.positional(1)
- def __init__(self, callback=None, batch_uri=None):
- """Constructor for a BatchHttpRequest.
- Args:
- callback: callable, A callback to be called for each response, of the
- form callback(id, response, exception). The first parameter is the
- request id, and the second is the deserialized response object. The
- third is an googleapiclient.errors.HttpError exception object if an HTTP error
- occurred while processing the request, or None if no error occurred.
- batch_uri: string, URI to send batch requests to.
- """
- if batch_uri is None:
- batch_uri = _LEGACY_BATCH_URI
- if batch_uri == _LEGACY_BATCH_URI:
- LOGGER.warning(
- "You have constructed a BatchHttpRequest using the legacy batch "
- "endpoint %s. This endpoint will be turned down on August 12, 2020. "
- "Please provide the API-specific endpoint or use "
- "service.new_batch_http_request(). For more details see "
- "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
- "and https://developers.google.com/api-client-library/python/guide/batch.",
- _LEGACY_BATCH_URI,
- )
- self._batch_uri = batch_uri
- # Global callback to be called for each individual response in the batch.
- self._callback = callback
- # A map from id to request.
- self._requests = {}
- # A map from id to callback.
- self._callbacks = {}
- # List of request ids, in the order in which they were added.
- self._order = []
- # The last auto generated id.
- self._last_auto_id = 0
- # Unique ID on which to base the Content-ID headers.
- self._base_id = None
- # A map from request id to (httplib2.Response, content) response pairs
- self._responses = {}
- # A map of id(Credentials) that have been refreshed.
- self._refreshed_credentials = {}
- def _refresh_and_apply_credentials(self, request, http):
- """Refresh the credentials and apply to the request.
- Args:
- request: HttpRequest, the request.
- http: httplib2.Http, the global http object for the batch.
- """
- # For the credentials to refresh, but only once per refresh_token
- # If there is no http per the request then refresh the http passed in
- # via execute()
- creds = None
- request_credentials = False
- if request.http is not None:
- creds = _auth.get_credentials_from_http(request.http)
- request_credentials = True
- if creds is None and http is not None:
- creds = _auth.get_credentials_from_http(http)
- if creds is not None:
- if id(creds) not in self._refreshed_credentials:
- _auth.refresh_credentials(creds)
- self._refreshed_credentials[id(creds)] = 1
- # Only apply the credentials if we are using the http object passed in,
- # otherwise apply() will get called during _serialize_request().
- if request.http is None or not request_credentials:
- _auth.apply_credentials(creds, request.headers)
- def _id_to_header(self, id_):
- """Convert an id to a Content-ID header value.
- Args:
- id_: string, identifier of individual request.
- Returns:
- A Content-ID header with the id_ encoded into it. A UUID is prepended to
- the value because Content-ID headers are supposed to be universally
- unique.
- """
- if self._base_id is None:
- self._base_id = uuid.uuid4()
- # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
- # line folding works properly on Python 3; see
- # https://github.com/googleapis/google-api-python-client/issues/164
- return "<%s + %s>" % (self._base_id, urllib.parse.quote(id_))
- def _header_to_id(self, header):
- """Convert a Content-ID header value to an id.
- Presumes the Content-ID header conforms to the format that _id_to_header()
- returns.
- Args:
- header: string, Content-ID header value.
- Returns:
- The extracted id value.
- Raises:
- BatchError if the header is not in the expected format.
- """
- if header[0] != "<" or header[-1] != ">":
- raise BatchError("Invalid value for Content-ID: %s" % header)
- if "+" not in header:
- raise BatchError("Invalid value for Content-ID: %s" % header)
- base, id_ = header[1:-1].split(" + ", 1)
- return urllib.parse.unquote(id_)
- def _serialize_request(self, request):
- """Convert an HttpRequest object into a string.
- Args:
- request: HttpRequest, the request to serialize.
- Returns:
- The request as a string in application/http format.
- """
- # Construct status line
- parsed = urllib.parse.urlparse(request.uri)
- request_line = urllib.parse.urlunparse(
- ("", "", parsed.path, parsed.params, parsed.query, "")
- )
- status_line = request.method + " " + request_line + " HTTP/1.1\n"
- major, minor = request.headers.get("content-type", "application/json").split(
- "/"
- )
- msg = MIMENonMultipart(major, minor)
- headers = request.headers.copy()
- if request.http is not None:
- credentials = _auth.get_credentials_from_http(request.http)
- if credentials is not None:
- _auth.apply_credentials(credentials, headers)
- # MIMENonMultipart adds its own Content-Type header.
- if "content-type" in headers:
- del headers["content-type"]
- for key, value in headers.items():
- msg[key] = value
- msg["Host"] = parsed.netloc
- msg.set_unixfrom(None)
- if request.body is not None:
- msg.set_payload(request.body)
- msg["content-length"] = str(len(request.body))
- # Serialize the mime message.
- fp = io.StringIO()
- # maxheaderlen=0 means don't line wrap headers.
- g = Generator(fp, maxheaderlen=0)
- g.flatten(msg, unixfrom=False)
- body = fp.getvalue()
- return status_line + body
- def _deserialize_response(self, payload):
- """Convert string into httplib2 response and content.
- Args:
- payload: string, headers and body as a string.
- Returns:
- A pair (resp, content), such as would be returned from httplib2.request.
- """
- # Strip off the status line
- status_line, payload = payload.split("\n", 1)
- protocol, status, reason = status_line.split(" ", 2)
- # Parse the rest of the response
- parser = FeedParser()
- parser.feed(payload)
- msg = parser.close()
- msg["status"] = status
- # Create httplib2.Response from the parsed headers.
- resp = httplib2.Response(msg)
- resp.reason = reason
- resp.version = int(protocol.split("/", 1)[1].replace(".", ""))
- content = payload.split("\r\n\r\n", 1)[1]
- return resp, content
- def _new_id(self):
- """Create a new id.
- Auto incrementing number that avoids conflicts with ids already used.
- Returns:
- string, a new unique id.
- """
- self._last_auto_id += 1
- while str(self._last_auto_id) in self._requests:
- self._last_auto_id += 1
- return str(self._last_auto_id)
- @util.positional(2)
- def add(self, request, callback=None, request_id=None):
- """Add a new request.
- Every callback added will be paired with a unique id, the request_id. That
- unique id will be passed back to the callback when the response comes back
- from the server. The default behavior is to have the library generate it's
- own unique id. If the caller passes in a request_id then they must ensure
- uniqueness for each request_id, and if they are not an exception is
- raised. Callers should either supply all request_ids or never supply a
- request id, to avoid such an error.
- Args:
- request: HttpRequest, Request to add to the batch.
- callback: callable, A callback to be called for this response, of the
- form callback(id, response, exception). The first parameter is the
- request id, and the second is the deserialized response object. The
- third is an googleapiclient.errors.HttpError exception object if an HTTP error
- occurred while processing the request, or None if no errors occurred.
- request_id: string, A unique id for the request. The id will be passed
- to the callback with the response.
- Returns:
- None
- Raises:
- BatchError if a media request is added to a batch.
- KeyError is the request_id is not unique.
- """
- if len(self._order) >= MAX_BATCH_LIMIT:
- raise BatchError(
- "Exceeded the maximum calls(%d) in a single batch request."
- % MAX_BATCH_LIMIT
- )
- if request_id is None:
- request_id = self._new_id()
- if request.resumable is not None:
- raise BatchError("Media requests cannot be used in a batch request.")
- if request_id in self._requests:
- raise KeyError("A request with this ID already exists: %s" % request_id)
- self._requests[request_id] = request
- self._callbacks[request_id] = callback
- self._order.append(request_id)
- def _execute(self, http, order, requests):
- """Serialize batch request, send to server, process response.
- Args:
- http: httplib2.Http, an http object to be used to make the request with.
- order: list, list of request ids in the order they were added to the
- batch.
- requests: list, list of request objects to send.
- Raises:
- httplib2.HttpLib2Error if a transport error has occurred.
- googleapiclient.errors.BatchError if the response is the wrong format.
- """
- message = MIMEMultipart("mixed")
- # Message should not write out it's own headers.
- setattr(message, "_write_headers", lambda self: None)
- # Add all the individual requests.
- for request_id in order:
- request = requests[request_id]
- msg = MIMENonMultipart("application", "http")
- msg["Content-Transfer-Encoding"] = "binary"
- msg["Content-ID"] = self._id_to_header(request_id)
- body = self._serialize_request(request)
- msg.set_payload(body)
- message.attach(msg)
- # encode the body: note that we can't use `as_string`, because
- # it plays games with `From ` lines.
- fp = io.StringIO()
- g = Generator(fp, mangle_from_=False)
- g.flatten(message, unixfrom=False)
- body = fp.getvalue()
- headers = {}
- headers["content-type"] = (
- "multipart/mixed; " 'boundary="%s"'
- ) % message.get_boundary()
- resp, content = http.request(
- self._batch_uri, method="POST", body=body, headers=headers
- )
- if resp.status >= 300:
- raise HttpError(resp, content, uri=self._batch_uri)
- # Prepend with a content-type header so FeedParser can handle it.
- header = "content-type: %s\r\n\r\n" % resp["content-type"]
- # PY3's FeedParser only accepts unicode. So we should decode content
- # here, and encode each payload again.
- content = content.decode("utf-8")
- for_parser = header + content
- parser = FeedParser()
- parser.feed(for_parser)
- mime_response = parser.close()
- if not mime_response.is_multipart():
- raise BatchError(
- "Response not in multipart/mixed format.", resp=resp, content=content
- )
- for part in mime_response.get_payload():
- request_id = self._header_to_id(part["Content-ID"])
- response, content = self._deserialize_response(part.get_payload())
- # We encode content here to emulate normal http response.
- if isinstance(content, str):
- content = content.encode("utf-8")
- self._responses[request_id] = (response, content)
- @util.positional(1)
- def execute(self, http=None):
- """Execute all the requests as a single batched HTTP request.
- Args:
- http: httplib2.Http, an http object to be used in place of the one the
- HttpRequest request object was constructed with. If one isn't supplied
- then use a http object from the requests in this batch.
- Returns:
- None
- Raises:
- httplib2.HttpLib2Error if a transport error has occurred.
- googleapiclient.errors.BatchError if the response is the wrong format.
- """
- # If we have no requests return
- if len(self._order) == 0:
- return None
- # If http is not supplied use the first valid one given in the requests.
- if http is None:
- for request_id in self._order:
- request = self._requests[request_id]
- if request is not None:
- http = request.http
- break
- if http is None:
- raise ValueError("Missing a valid http object.")
- # Special case for OAuth2Credentials-style objects which have not yet been
- # refreshed with an initial access_token.
- creds = _auth.get_credentials_from_http(http)
- if creds is not None:
- if not _auth.is_valid(creds):
- LOGGER.info("Attempting refresh to obtain initial access_token")
- _auth.refresh_credentials(creds)
- self._execute(http, self._order, self._requests)
- # Loop over all the requests and check for 401s. For each 401 request the
- # credentials should be refreshed and then sent again in a separate batch.
- redo_requests = {}
- redo_order = []
- for request_id in self._order:
- resp, content = self._responses[request_id]
- if resp["status"] == "401":
- redo_order.append(request_id)
- request = self._requests[request_id]
- self._refresh_and_apply_credentials(request, http)
- redo_requests[request_id] = request
- if redo_requests:
- self._execute(http, redo_order, redo_requests)
- # Now process all callbacks that are erroring, and raise an exception for
- # ones that return a non-2xx response? Or add extra parameter to callback
- # that contains an HttpError?
- for request_id in self._order:
- resp, content = self._responses[request_id]
- request = self._requests[request_id]
- callback = self._callbacks[request_id]
- response = None
- exception = None
- try:
- if resp.status >= 300:
- raise HttpError(resp, content, uri=request.uri)
- response = request.postproc(resp, content)
- except HttpError as e:
- exception = e
- if callback is not None:
- callback(request_id, response, exception)
- if self._callback is not None:
- self._callback(request_id, response, exception)
- class HttpRequestMock(object):
- """Mock of HttpRequest.
- Do not construct directly, instead use RequestMockBuilder.
- """
- def __init__(self, resp, content, postproc):
- """Constructor for HttpRequestMock
- Args:
- resp: httplib2.Response, the response to emulate coming from the request
- content: string, the response body
- postproc: callable, the post processing function usually supplied by
- the model class. See model.JsonModel.response() as an example.
- """
- self.resp = resp
- self.content = content
- self.postproc = postproc
- if resp is None:
- self.resp = httplib2.Response({"status": 200, "reason": "OK"})
- if "reason" in self.resp:
- self.resp.reason = self.resp["reason"]
- def execute(self, http=None):
- """Execute the request.
- Same behavior as HttpRequest.execute(), but the response is
- mocked and not really from an HTTP request/response.
- """
- return self.postproc(self.resp, self.content)
- class RequestMockBuilder(object):
- """A simple mock of HttpRequest
- Pass in a dictionary to the constructor that maps request methodIds to
- tuples of (httplib2.Response, content, opt_expected_body) that should be
- returned when that method is called. None may also be passed in for the
- httplib2.Response, in which case a 200 OK response will be generated.
- If an opt_expected_body (str or dict) is provided, it will be compared to
- the body and UnexpectedBodyError will be raised on inequality.
- Example:
- response = '{"data": {"id": "tag:google.c...'
- requestBuilder = RequestMockBuilder(
- {
- 'plus.activities.get': (None, response),
- }
- )
- googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
- Methods that you do not supply a response for will return a
- 200 OK with an empty string as the response content or raise an excpetion
- if check_unexpected is set to True. The methodId is taken from the rpcName
- in the discovery document.
- For more details see the project wiki.
- """
- def __init__(self, responses, check_unexpected=False):
- """Constructor for RequestMockBuilder
- The constructed object should be a callable object
- that can replace the class HttpResponse.
- responses - A dictionary that maps methodIds into tuples
- of (httplib2.Response, content). The methodId
- comes from the 'rpcName' field in the discovery
- document.
- check_unexpected - A boolean setting whether or not UnexpectedMethodError
- should be raised on unsupplied method.
- """
- self.responses = responses
- self.check_unexpected = check_unexpected
- def __call__(
- self,
- http,
- postproc,
- uri,
- method="GET",
- body=None,
- headers=None,
- methodId=None,
- resumable=None,
- ):
- """Implements the callable interface that discovery.build() expects
- of requestBuilder, which is to build an object compatible with
- HttpRequest.execute(). See that method for the description of the
- parameters and the expected response.
- """
- if methodId in self.responses:
- response = self.responses[methodId]
- resp, content = response[:2]
- if len(response) > 2:
- # Test the body against the supplied expected_body.
- expected_body = response[2]
- if bool(expected_body) != bool(body):
- # Not expecting a body and provided one
- # or expecting a body and not provided one.
- raise UnexpectedBodyError(expected_body, body)
- if isinstance(expected_body, str):
- expected_body = json.loads(expected_body)
- body = json.loads(body)
- if body != expected_body:
- raise UnexpectedBodyError(expected_body, body)
- return HttpRequestMock(resp, content, postproc)
- elif self.check_unexpected:
- raise UnexpectedMethodError(methodId=methodId)
- else:
- model = JsonModel(False)
- return HttpRequestMock(None, "{}", model.response)
- class HttpMock(object):
- """Mock of httplib2.Http"""
- def __init__(self, filename=None, headers=None):
- """
- Args:
- filename: string, absolute filename to read response from
- headers: dict, header to return with response
- """
- if headers is None:
- headers = {"status": "200"}
- if filename:
- with open(filename, "rb") as f:
- self.data = f.read()
- else:
- self.data = None
- self.response_headers = headers
- self.headers = None
- self.uri = None
- self.method = None
- self.body = None
- self.headers = None
- def request(
- self,
- uri,
- method="GET",
- body=None,
- headers=None,
- redirections=1,
- connection_type=None,
- ):
- self.uri = uri
- self.method = method
- self.body = body
- self.headers = headers
- return httplib2.Response(self.response_headers), self.data
- def close(self):
- return None
- class HttpMockSequence(object):
- """Mock of httplib2.Http
- Mocks a sequence of calls to request returning different responses for each
- call. Create an instance initialized with the desired response headers
- and content and then use as if an httplib2.Http instance.
- http = HttpMockSequence([
- ({'status': '401'}, ''),
- ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
- ({'status': '200'}, 'echo_request_headers'),
- ])
- resp, content = http.request("http://examples.com")
- There are special values you can pass in for content to trigger
- behavours that are helpful in testing.
- 'echo_request_headers' means return the request headers in the response body
- 'echo_request_headers_as_json' means return the request headers in
- the response body
- 'echo_request_body' means return the request body in the response body
- 'echo_request_uri' means return the request uri in the response body
- """
- def __init__(self, iterable):
- """
- Args:
- iterable: iterable, a sequence of pairs of (headers, body)
- """
- self._iterable = iterable
- self.follow_redirects = True
- self.request_sequence = list()
- def request(
- self,
- uri,
- method="GET",
- body=None,
- headers=None,
- redirections=1,
- connection_type=None,
- ):
- # Remember the request so after the fact this mock can be examined
- self.request_sequence.append((uri, method, body, headers))
- resp, content = self._iterable.pop(0)
- if isinstance(content, str):
- content = content.encode("utf-8")
- if content == b"echo_request_headers":
- content = headers
- elif content == b"echo_request_headers_as_json":
- content = json.dumps(headers)
- elif content == b"echo_request_body":
- if hasattr(body, "read"):
- content = body.read()
- else:
- content = body
- elif content == b"echo_request_uri":
- content = uri
- if isinstance(content, str):
- content = content.encode("utf-8")
- return httplib2.Response(resp), content
- def set_user_agent(http, user_agent):
- """Set the user-agent on every request.
- Args:
- http - An instance of httplib2.Http
- or something that acts like it.
- user_agent: string, the value for the user-agent header.
- Returns:
- A modified instance of http that was passed in.
- Example:
- h = httplib2.Http()
- h = set_user_agent(h, "my-app-name/6.0")
- Most of the time the user-agent will be set doing auth, this is for the rare
- cases where you are accessing an unauthenticated endpoint.
- """
- request_orig = http.request
- # The closure that will replace 'httplib2.Http.request'.
- def new_request(
- uri,
- method="GET",
- body=None,
- headers=None,
- redirections=httplib2.DEFAULT_MAX_REDIRECTS,
- connection_type=None,
- ):
- """Modify the request headers to add the user-agent."""
- if headers is None:
- headers = {}
- if "user-agent" in headers:
- headers["user-agent"] = user_agent + " " + headers["user-agent"]
- else:
- headers["user-agent"] = user_agent
- resp, content = request_orig(
- uri,
- method=method,
- body=body,
- headers=headers,
- redirections=redirections,
- connection_type=connection_type,
- )
- return resp, content
- http.request = new_request
- return http
- def tunnel_patch(http):
- """Tunnel PATCH requests over POST.
- Args:
- http - An instance of httplib2.Http
- or something that acts like it.
- Returns:
- A modified instance of http that was passed in.
- Example:
- h = httplib2.Http()
- h = tunnel_patch(h, "my-app-name/6.0")
- Useful if you are running on a platform that doesn't support PATCH.
- Apply this last if you are using OAuth 1.0, as changing the method
- will result in a different signature.
- """
- request_orig = http.request
- # The closure that will replace 'httplib2.Http.request'.
- def new_request(
- uri,
- method="GET",
- body=None,
- headers=None,
- redirections=httplib2.DEFAULT_MAX_REDIRECTS,
- connection_type=None,
- ):
- """Modify the request headers to add the user-agent."""
- if headers is None:
- headers = {}
- if method == "PATCH":
- if "oauth_token" in headers.get("authorization", ""):
- LOGGER.warning(
- "OAuth 1.0 request made with Credentials after tunnel_patch."
- )
- headers["x-http-method-override"] = "PATCH"
- method = "POST"
- resp, content = request_orig(
- uri,
- method=method,
- body=body,
- headers=headers,
- redirections=redirections,
- connection_type=connection_type,
- )
- return resp, content
- http.request = new_request
- return http
- def build_http():
- """Builds httplib2.Http object
- Returns:
- A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
- To override default timeout call
- socket.setdefaulttimeout(timeout_in_sec)
- before interacting with this method.
- """
- if socket.getdefaulttimeout() is not None:
- http_timeout = socket.getdefaulttimeout()
- else:
- http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
- http = httplib2.Http(timeout=http_timeout)
- # 308's are used by several Google APIs (Drive, YouTube)
- # for Resumable Uploads rather than Permanent Redirects.
- # This asks httplib2 to exclude 308s from the status codes
- # it treats as redirects
- try:
- http.redirect_codes = http.redirect_codes - {308}
- except AttributeError:
- # Apache Beam tests depend on this library and cannot
- # currently upgrade their httplib2 version
- # http.redirect_codes does not exist in previous versions
- # of httplib2, so pass
- pass
- return http
|