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

# Source Code for Module googleapiclient.discovery

       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  """Client for discovery based APIs.
      16
      17  A client library for Google's discovery based APIs.
      18  """
      19  from __future__ import absolute_import
      20  import six
      21  from six.moves import zip
      22
      23  __author__ = "jcgregorio@google.com (Joe Gregorio)"
      24  __all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
      25
      26  from six import BytesIO
      27  from six.moves import http_client
      28  from six.moves.urllib.parse import urlencode, urlparse, urljoin, urlunparse, parse_qsl
      29
      30  # Standard library imports
      31  import copy
      32  from collections import OrderedDict
      33
      34  try:
      35      from email.generator import BytesGenerator
      36  except ImportError:
      37      from email.generator import Generator as BytesGenerator
      38  from email.mime.multipart import MIMEMultipart
      39  from email.mime.nonmultipart import MIMENonMultipart
      40  import json
      41  import keyword
      42  import logging
      43  import mimetypes
      44  import os
      45  import re
      46
      47  # Third-party imports
      48  import httplib2
      49  import uritemplate
      50  import google.api_core.client_options
      51  from google.auth.transport import mtls
      52  from google.auth.exceptions import MutualTLSChannelError
      53
      54  try:
      55      import google_auth_httplib2
      56  except ImportError:  # pragma: NO COVER
      57      google_auth_httplib2 = None
      58
      59  # Local imports
      60  from googleapiclient import _auth
      61  from googleapiclient import mimeparse
      62  from googleapiclient.errors import HttpError
      63  from googleapiclient.errors import InvalidJsonError
      64  from googleapiclient.errors import MediaUploadSizeError
      65  from googleapiclient.errors import UnacceptableMimeTypeError
      66  from googleapiclient.errors import UnknownApiNameOrVersion
      67  from googleapiclient.errors import UnknownFileType
      68  from googleapiclient.http import build_http
      69  from googleapiclient.http import BatchHttpRequest
      70  from googleapiclient.http import HttpMock
      71  from googleapiclient.http import HttpMockSequence
      72  from googleapiclient.http import HttpRequest
      73  from googleapiclient.http import MediaFileUpload
      74  from googleapiclient.http import MediaUpload
      75  from googleapiclient.model import JsonModel
      76  from googleapiclient.model import MediaModel
      77  from googleapiclient.model import RawModel
      78  from googleapiclient.schema import Schemas
      79
      80  from googleapiclient._helpers import _add_query_parameter
      81  from googleapiclient._helpers import positional
      82
      83
      84  # The client library requires a version of httplib2 that supports RETRIES.
      85  httplib2.RETRIES = 1
      86
      87  logger = logging.getLogger(__name__)
      88
      89  URITEMPLATE = re.compile("{[^}]*}")
      90  VARNAME = re.compile("[a-zA-Z0-9_-]+")
      91  DISCOVERY_URI = (
      92      "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
      93  )
      94  V1_DISCOVERY_URI = DISCOVERY_URI
      95  V2_DISCOVERY_URI = (
      96      "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
      97  )
      98  DEFAULT_METHOD_DOC = "A description of how to use this function"
      99  HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
     100
     101  _MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
     102  BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
     103  MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
     104      "description": (
     105          "The filename of the media request body, or an instance "
     106          "of a MediaUpload object."
     107      ),
     108      "type": "string",
     109      "required": False,
     110  }
     111  MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
     112      "description": (
     113          "The MIME type of the media request body, or an instance "
     114          "of a MediaUpload object."
     115      ),
     116      "type": "string",
     117      "required": False,
     118  }
     119  _PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
     120
     121  # Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114.
     122  GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
     123  GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
     124
     125  # Parameters accepted by the stack, but not visible via discovery.
     126  # TODO(dhermes): Remove 'userip' in 'v2'.
     127  STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
     128  STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
     129
     130  # Library-specific reserved words beyond Python keywords.
     131  RESERVED_WORDS = frozenset(["body"])
     132
     133  # patch _write_lines to avoid munging '\r' into '\n'
     134  # ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
     135 -class _BytesGenerator(BytesGenerator):
     136      _write_lines = BytesGenerator.write
     137
     138
     139 -def fix_method_name(name):
     140      """Fix method names to avoid '$' characters and reserved word conflicts.
     141
     142    Args:
     143      name: string, method name.
     144
     145    Returns:
     146      The name with '_' appended if the name is a reserved word and '$' and '-'
     147      replaced with '_'.
     148    """
     149      name = name.replace("$", "_").replace("-", "_")
     150      if keyword.iskeyword(name) or name in RESERVED_WORDS:
     151          return name + "_"
     152      else:
     153          return name
     154
     155
     156 -def key2param(key):
     157      """Converts key names into parameter names.
     158
     159    For example, converting "max-results" -> "max_results"
     160
     161    Args:
     162      key: string, the method key name.
     163
     164    Returns:
     165      A safe method name based on the key name.
     166    """
     167      result = []
     168      key = list(key)
     169      if not key[0].isalpha():
     170          result.append("x")
     171      for c in key:
     172          if c.isalnum():
     173              result.append(c)
     174          else:
     175              result.append("_")
     176
     177      return "".join(result)
     178
     179
     180  @positional(2)
     181 -def build(
     182      serviceName,
     183      version,
     184      http=None,
     185      discoveryServiceUrl=DISCOVERY_URI,
     186      developerKey=None,
     187      model=None,
     188      requestBuilder=HttpRequest,
     189      credentials=None,
     190      cache_discovery=True,
     191      cache=None,
     192      client_options=None,
     193      adc_cert_path=None,
     194      adc_key_path=None,
     195      num_retries=1,
     196  ):
     197      """Construct a Resource for interacting with an API.
     198
     199    Construct a Resource object for interacting with an API. The serviceName and
     200    version are the names from the Discovery service.
     201
     202    Args:
     203      serviceName: string, name of the service.
     204      version: string, the version of the service.
     205      http: httplib2.Http, An instance of httplib2.Http or something that acts
     206        like it that HTTP requests will be made through.
     207      discoveryServiceUrl: string, a URI Template that points to the location of
     208        the discovery service. It should have two parameters {api} and
     209        {apiVersion} that when filled in produce an absolute URI to the discovery
     210        document for that service.
     211      developerKey: string, key obtained from
     212        https://code.google.com/apis/console.
     213      model: googleapiclient.Model, converts to and from the wire format.
     214      requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
     215        request.
     216      credentials: oauth2client.Credentials or
     217        google.auth.credentials.Credentials, credentials to be used for
     218        authentication.
     219      cache_discovery: Boolean, whether or not to cache the discovery doc.
     220      cache: googleapiclient.discovery_cache.base.CacheBase, an optional
     221        cache object for the discovery documents.
     222      client_options: Mapping object or google.api_core.client_options, client
     223        options to set user options on the client.
     224        (1) The API endpoint should be set through client_options. If API endpoint
     225        is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
     226        to control which endpoint to use.
     227        (2) client_cert_source is not supported, client cert should be provided using
     228        client_encrypted_cert_source instead. In order to use the provided client
     229        cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
     230        set to `true`.
     231        More details on the environment variables are here:
     232        https://google.aip.dev/auth/4114
     233      adc_cert_path: str, client certificate file path to save the application
     234        default client certificate for mTLS. This field is required if you want to
     235        use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
     236        environment variable must be set to `true` in order to use this field,
     237        otherwise this field doesn't nothing.
     238        More details on the environment variables are here:
     239        https://google.aip.dev/auth/4114
     240      adc_key_path: str, client encrypted private key file path to save the
     241        application default client encrypted private key for mTLS. This field is
     242        required if you want to use the default client certificate.
     243        `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
     244        `true` in order to use this field, otherwise this field doesn't nothing.
     245        More details on the environment variables are here:
     246        https://google.aip.dev/auth/4114
     247      num_retries: Integer, number of times to retry discovery with
     248        randomized exponential backoff in case of intermittent/connection issues.
     249
     250    Returns:
     251      A Resource object with methods for interacting with the service.
     252
     253    Raises:
     254      google.auth.exceptions.MutualTLSChannelError: if there are any problems
     255        setting up mutual TLS channel.
     256    """
     257      params = {"api": serviceName, "apiVersion": version}
     258
     259      if http is None:
     260          discovery_http = build_http()
     261      else:
     262          discovery_http = http
     263
     264      service = None
     265
     266      for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
     267          requested_url = uritemplate.expand(discovery_url, params)
     268
     269          try:
     270              content = _retrieve_discovery_doc(
     271                  requested_url,
     272                  discovery_http,
     273                  cache_discovery,
     274                  cache,
     275                  developerKey,
     276                  num_retries=num_retries,
     277              )
     278              service = build_from_document(
     279                  content,
     280                  base=discovery_url,
     281                  http=http,
     282                  developerKey=developerKey,
     283                  model=model,
     284                  requestBuilder=requestBuilder,
     285                  credentials=credentials,
     286                  client_options=client_options,
     287                  adc_cert_path=adc_cert_path,
     288                  adc_key_path=adc_key_path,
     289              )
     290              break  # exit if a service was created
     291          except HttpError as e:
     292              if e.resp.status == http_client.NOT_FOUND:
     293                  continue
     294              else:
     295                  raise e
     296
     297      # If discovery_http was created by this function, we are done with it
     298      # and can safely close it
     299      if http is None:
     300          discovery_http.close()
     301
     302      if service is None:
     303          raise UnknownApiNameOrVersion("name: %s  version: %s" % (serviceName, version))
     304      else:
     305          return service
     306
     307
     308 -def _discovery_service_uri_options(discoveryServiceUrl, version):
     309      """
     310      Returns Discovery URIs to be used for attemnting to build the API Resource.
     311
     312    Args:
     313      discoveryServiceUrl:
     314          string, the Original Discovery Service URL preferred by the customer.
     315      version:
     316          string, API Version requested
     317
     318    Returns:
     319        A list of URIs to be tried for the Service Discovery, in order.
     320      """
     321
     322      urls = [discoveryServiceUrl, V2_DISCOVERY_URI]
     323      # V1 Discovery won't work if the requested version is None
     324      if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
     325          logger.warning(
     326              "Discovery V1 does not support empty versions. Defaulting to V2..."
     327          )
     328          urls.pop(0)
     329      return list(OrderedDict.fromkeys(urls))
     330
     331
     332 -def _retrieve_discovery_doc(
     333      url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
     334  ):
     335      """Retrieves the discovery_doc from cache or the internet.
     336
     337    Args:
     338      url: string, the URL of the discovery document.
     339      http: httplib2.Http, An instance of httplib2.Http or something that acts
     340        like it through which HTTP requests will be made.
     341      cache_discovery: Boolean, whether or not to cache the discovery doc.
     342      cache: googleapiclient.discovery_cache.base.Cache, an optional cache
     343        object for the discovery documents.
     344      developerKey: string, Key for controlling API usage, generated
     345        from the API Console.
     346      num_retries: Integer, number of times to retry discovery with
     347        randomized exponential backoff in case of intermittent/connection issues.
     348
     349    Returns:
     350      A unicode string representation of the discovery document.
     351    """
     352      if cache_discovery:
     353          from . import discovery_cache
     354
     355          if cache is None:
     356              cache = discovery_cache.autodetect()
     357          if cache:
     358              content = cache.get(url)
     359              if content:
     360                  return content
     361
     362      actual_url = url
     363      # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
     364      # variable that contains the network address of the client sending the
     365      # request. If it exists then add that to the request for the discovery
     366      # document to avoid exceeding the quota on discovery requests.
     367      if "REMOTE_ADDR" in os.environ:
     368          actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
     369      if developerKey:
     370          actual_url = _add_query_parameter(url, "key", developerKey)
     371      logger.debug("URL being requested: GET %s", actual_url)
     372
     373      # Execute this request with retries build into HttpRequest
     374      # Note that it will already raise an error if we don't get a 2xx response
     375      req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
     376      resp, content = req.execute(num_retries=num_retries)
     377
     378      try:
     379          content = content.decode("utf-8")
     380      except AttributeError:
     381          pass
     382
     383      try:
     384          service = json.loads(content)
     385      except ValueError as e:
     386          logger.error("Failed to parse as JSON: " + content)
     387          raise InvalidJsonError()
     388      if cache_discovery and cache:
     389          cache.set(url, content)
     390      return content
     391
     392
     393  @positional(1)
     394 -def build_from_document(
     395      service,
     396      base=None,
     397      future=None,
     398      http=None,
     399      developerKey=None,
     400      model=None,
     401      requestBuilder=HttpRequest,
     402      credentials=None,
     403      client_options=None,
     404      adc_cert_path=None,
     405      adc_key_path=None,
     406  ):
     407      """Create a Resource for interacting with an API.
     408
     409    Same as `build()`, but constructs the Resource object from a discovery
     410    document that is it given, as opposed to retrieving one over HTTP.
     411
     412    Args:
     413      service: string or object, the JSON discovery document describing the API.
     414        The value passed in may either be the JSON string or the deserialized
     415        JSON.
     416      base: string, base URI for all HTTP requests, usually the discovery URI.
     417        This parameter is no longer used as rootUrl and servicePath are included
     418        within the discovery document. (deprecated)
     419      future: string, discovery document with future capabilities (deprecated).
     420      http: httplib2.Http, An instance of httplib2.Http or something that acts
     421        like it that HTTP requests will be made through.
     422      developerKey: string, Key for controlling API usage, generated
     423        from the API Console.
     424      model: Model class instance that serializes and de-serializes requests and
     425        responses.
     426      requestBuilder: Takes an http request and packages it up to be executed.
     427      credentials: oauth2client.Credentials or
     428        google.auth.credentials.Credentials, credentials to be used for
     429        authentication.
     430      client_options: Mapping object or google.api_core.client_options, client
     431        options to set user options on the client.
     432        (1) The API endpoint should be set through client_options. If API endpoint
     433        is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
     434        to control which endpoint to use.
     435        (2) client_cert_source is not supported, client cert should be provided using
     436        client_encrypted_cert_source instead. In order to use the provided client
     437        cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
     438        set to `true`.
     439        More details on the environment variables are here:
     440        https://google.aip.dev/auth/4114
     441      adc_cert_path: str, client certificate file path to save the application
     442        default client certificate for mTLS. This field is required if you want to
     443        use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
     444        environment variable must be set to `true` in order to use this field,
     445        otherwise this field doesn't nothing.
     446        More details on the environment variables are here:
     447        https://google.aip.dev/auth/4114
     448      adc_key_path: str, client encrypted private key file path to save the
     449        application default client encrypted private key for mTLS. This field is
     450        required if you want to use the default client certificate.
     451        `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
     452        `true` in order to use this field, otherwise this field doesn't nothing.
     453        More details on the environment variables are here:
     454        https://google.aip.dev/auth/4114
     455
     456    Returns:
     457      A Resource object with methods for interacting with the service.
     458
     459    Raises:
     460      google.auth.exceptions.MutualTLSChannelError: if there are any problems
     461        setting up mutual TLS channel.
     462    """
     463
     464      if client_options is None:
     465          client_options = google.api_core.client_options.ClientOptions()
     466      if isinstance(client_options, six.moves.collections_abc.Mapping):
     467          client_options = google.api_core.client_options.from_dict(client_options)
     468
     469      if http is not None:
     470          # if http is passed, the user cannot provide credentials
     471          banned_options = [
     472              (credentials, "credentials"),
     473              (client_options.credentials_file, "client_options.credentials_file"),
     474          ]
     475          for option, name in banned_options:
     476              if option is not None:
     477                  raise ValueError("Arguments http and {} are mutually exclusive".format(name))
     478
     479      if isinstance(service, six.string_types):
     480          service = json.loads(service)
     481      elif isinstance(service, six.binary_type):
     482          service = json.loads(service.decode("utf-8"))
     483
     484      if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
     485          logger.error(
     486              "You are using HttpMock or HttpMockSequence without"
     487              + "having the service discovery doc in cache. Try calling "
     488              + "build() without mocking once first to populate the "
     489              + "cache."
     490          )
     491          raise InvalidJsonError()
     492
     493      # If an API Endpoint is provided on client options, use that as the base URL
     494      base = urljoin(service["rootUrl"], service["servicePath"])
     495      if client_options.api_endpoint:
     496          base = client_options.api_endpoint
     497
     498      schema = Schemas(service)
     499
     500      # If the http client is not specified, then we must construct an http client
     501      # to make requests. If the service has scopes, then we also need to setup
     502      # authentication.
     503      if http is None:
     504          # Does the service require scopes?
     505          scopes = list(
     506              service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
     507          )
     508
     509          # If so, then the we need to setup authentication if no developerKey is
     510          # specified.
     511          if scopes and not developerKey:
     512              # Make sure the user didn't pass multiple credentials
     513              if client_options.credentials_file and credentials:
     514                  raise google.api_core.exceptions.DuplicateCredentialArgs(
     515                      "client_options.credentials_file and credentials are mutually exclusive."
     516              )
     517              # Check for credentials file via client options
     518              if client_options.credentials_file:
     519                  credentials = _auth.credentials_from_file(
     520                      client_options.credentials_file,
     521                      scopes=client_options.scopes,
     522                      quota_project_id=client_options.quota_project_id,
     523                  )
     524              # If the user didn't pass in credentials, attempt to acquire application
     525              # default credentials.
     526              if credentials is None:
     527                  credentials = _auth.default_credentials(
     528                      scopes=client_options.scopes,
     529                      quota_project_id=client_options.quota_project_id,
     530                  )
     531
     532              # The credentials need to be scoped.
     533              # If the user provided scopes via client_options don't override them
     534              if not client_options.scopes:
     535                  credentials = _auth.with_scopes(credentials, scopes)
     536
     537          # If credentials are provided, create an authorized http instance;
     538          # otherwise, skip authentication.
     539          if credentials:
     540              http = _auth.authorized_http(credentials)
     541
     542          # If the service doesn't require scopes then there is no need for
     543          # authentication.
     544          else:
     545              http = build_http()
     546
     547          # Obtain client cert and create mTLS http channel if cert exists.
     548          client_cert_to_use = None
     549          use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
     550          if not use_client_cert in ("true", "false"):
     551              raise MutualTLSChannelError(
     552                  "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
     553              )
     554          if client_options and client_options.client_cert_source:
     555              raise MutualTLSChannelError(
     556                  "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
     557              )
     558          if use_client_cert == "true":
     559              if (
     560                  client_options
     561                  and hasattr(client_options, "client_encrypted_cert_source")
     562                  and client_options.client_encrypted_cert_source
     563              ):
     564                  client_cert_to_use = client_options.client_encrypted_cert_source
     565              elif (
     566                  adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
     567              ):
     568                  client_cert_to_use = mtls.default_client_encrypted_cert_source(
     569                      adc_cert_path, adc_key_path
     570                  )
     571          if client_cert_to_use:
     572              cert_path, key_path, passphrase = client_cert_to_use()
     573
     574              # The http object we built could be google_auth_httplib2.AuthorizedHttp
     575              # or httplib2.Http. In the first case we need to extract the wrapped
     576              # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
     577              http_channel = (
     578                  http.http
     579                  if google_auth_httplib2
     580                  and isinstance(http, google_auth_httplib2.AuthorizedHttp)
     581                  else http
     582              )
     583              http_channel.add_certificate(key_path, cert_path, "", passphrase)
     584
     585          # If user doesn't provide api endpoint via client options, decide which
     586          # api endpoint to use.
     587          if "mtlsRootUrl" in service and (
     588              not client_options or not client_options.api_endpoint
     589          ):
     590              mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
     591              use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
     592
     593              if not use_mtls_endpoint in ("never", "auto", "always"):
     594                  raise MutualTLSChannelError(
     595                      "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
     596                  )
     597
     598              # Switch to mTLS endpoint, if environment variable is "always", or
     599              # environment varibable is "auto" and client cert exists.
     600              if use_mtls_endpoint == "always" or (
     601                  use_mtls_endpoint == "auto" and client_cert_to_use
     602              ):
     603                  base = mtls_endpoint
     604
     605      if model is None:
     606          features = service.get("features", [])
     607          model = JsonModel("dataWrapper" in features)
     608
     609      return Resource(
     610          http=http,
     611          baseUrl=base,
     612          model=model,
     613          developerKey=developerKey,
     614          requestBuilder=requestBuilder,
     615          resourceDesc=service,
     616          rootDesc=service,
     617          schema=schema,
     618      )
     619
     620
     621 -def _cast(value, schema_type):
     622      """Convert value to a string based on JSON Schema type.
     623
     624    See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
     625    JSON Schema.
     626
     627    Args:
     628      value: any, the value to convert
     629      schema_type: string, the type that value should be interpreted as
     630
     631    Returns:
     632      A string representation of 'value' based on the schema_type.
     633    """
     634      if schema_type == "string":
     635          if type(value) == type("") or type(value) == type(u""):
     636              return value
     637          else:
     638              return str(value)
     639      elif schema_type == "integer":
     640          return str(int(value))
     641      elif schema_type == "number":
     642          return str(float(value))
     643      elif schema_type == "boolean":
     644          return str(bool(value)).lower()
     645      else:
     646          if type(value) == type("") or type(value) == type(u""):
     647              return value
     648          else:
     649              return str(value)
     650
     651
     652 -def _media_size_to_long(maxSize):
     653      """Convert a string media size, such as 10GB or 3TB into an integer.
     654
     655    Args:
     656      maxSize: string, size as a string, such as 2MB or 7GB.
     657
     658    Returns:
     659      The size as an integer value.
     660    """
     661      if len(maxSize) < 2:
     662          return 0
     663      units = maxSize[-2:].upper()
     664      bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
     665      if bit_shift is not None:
     666          return int(maxSize[:-2]) << bit_shift
     667      else:
     668          return int(maxSize)
     669
     670
     671 -def _media_path_url_from_info(root_desc, path_url):
     672      """Creates an absolute media path URL.
     673
     674    Constructed using the API root URI and service path from the discovery
     675    document and the relative path for the API method.
     676
     677    Args:
     678      root_desc: Dictionary; the entire original deserialized discovery document.
     679      path_url: String; the relative URL for the API method. Relative to the API
     680          root, which is specified in the discovery document.
     681
     682    Returns:
     683      String; the absolute URI for media upload for the API method.
     684    """
     685      return "%(root)supload/%(service_path)s%(path)s" % {
     686          "root": root_desc["rootUrl"],
     687          "service_path": root_desc["servicePath"],
     688          "path": path_url,
     689      }
     690
     691
     692 -def _fix_up_parameters(method_desc, root_desc, http_method, schema):
     693      """Updates parameters of an API method with values specific to this library.
     694
     695    Specifically, adds whatever global parameters are specified by the API to the
     696    parameters for the individual method. Also adds parameters which don't
     697    appear in the discovery document, but are available to all discovery based
     698    APIs (these are listed in STACK_QUERY_PARAMETERS).
     699
     700    SIDE EFFECTS: This updates the parameters dictionary object in the method
     701    description.
     702
     703    Args:
     704      method_desc: Dictionary with metadata describing an API method. Value comes
     705          from the dictionary of methods stored in the 'methods' key in the
     706          deserialized discovery document.
     707      root_desc: Dictionary; the entire original deserialized discovery document.
     708      http_method: String; the HTTP method used to call the API method described
     709          in method_desc.
     710      schema: Object, mapping of schema names to schema descriptions.
     711
     712    Returns:
     713      The updated Dictionary stored in the 'parameters' key of the method
     714          description dictionary.
     715    """
     716      parameters = method_desc.setdefault("parameters", {})
     717
     718      # Add in the parameters common to all methods.
     719      for name, description in six.iteritems(root_desc.get("parameters", {})):
     720          parameters[name] = description
     721
     722      # Add in undocumented query parameters.
     723      for name in STACK_QUERY_PARAMETERS:
     724          parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
     725
     726      # Add 'body' (our own reserved word) to parameters if the method supports
     727      # a request payload.
     728      if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
     729          body = BODY_PARAMETER_DEFAULT_VALUE.copy()
     730          body.update(method_desc["request"])
     731          parameters["body"] = body
     732
     733      return parameters
     734
     735
     736 -def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
     737      """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
     738
     739    SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
     740    'media_upload' key to parameters.
     741
     742    Args:
     743      method_desc: Dictionary with metadata describing an API method. Value comes
     744          from the dictionary of methods stored in the 'methods' key in the
     745          deserialized discovery document.
     746      root_desc: Dictionary; the entire original deserialized discovery document.
     747      path_url: String; the relative URL for the API method. Relative to the API
     748          root, which is specified in the discovery document.
     749      parameters: A dictionary describing method parameters for method described
     750          in method_desc.
     751
     752    Returns:
     753      Triple (accept, max_size, media_path_url) where:
     754        - accept is a list of strings representing what content types are
     755          accepted for media upload. Defaults to empty list if not in the
     756          discovery document.
     757        - max_size is a long representing the max size in bytes allowed for a
     758          media upload. Defaults to 0L if not in the discovery document.
     759        - media_path_url is a String; the absolute URI for media upload for the
     760          API method. Constructed using the API root URI and service path from
     761          the discovery document and the relative path for the API method. If
     762          media upload is not supported, this is None.
     763    """
     764      media_upload = method_desc.get("mediaUpload", {})
     765      accept = media_upload.get("accept", [])
     766      max_size = _media_size_to_long(media_upload.get("maxSize", ""))
     767      media_path_url = None
     768
     769      if media_upload:
     770          media_path_url = _media_path_url_from_info(root_desc, path_url)
     771          parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
     772          parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
     773
     774      return accept, max_size, media_path_url
     775
     776
     777 -def _fix_up_method_description(method_desc, root_desc, schema):
     778      """Updates a method description in a discovery document.
     779
     780    SIDE EFFECTS: Changes the parameters dictionary in the method description with
     781    extra parameters which are used locally.
     782
     783    Args:
     784      method_desc: Dictionary with metadata describing an API method. Value comes
     785          from the dictionary of methods stored in the 'methods' key in the
     786          deserialized discovery document.
     787      root_desc: Dictionary; the entire original deserialized discovery document.
     788      schema: Object, mapping of schema names to schema descriptions.
     789
     790    Returns:
     791      Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
     792      where:
     793        - path_url is a String; the relative URL for the API method. Relative to
     794          the API root, which is specified in the discovery document.
     795        - http_method is a String; the HTTP method used to call the API method
     796          described in the method description.
     797        - method_id is a String; the name of the RPC method associated with the
     798          API method, and is in the method description in the 'id' key.
     799        - accept is a list of strings representing what content types are
     800          accepted for media upload. Defaults to empty list if not in the
     801          discovery document.
     802        - max_size is a long representing the max size in bytes allowed for a
     803          media upload. Defaults to 0L if not in the discovery document.
     804        - media_path_url is a String; the absolute URI for media upload for the
     805          API method. Constructed using the API root URI and service path from
     806          the discovery document and the relative path for the API method. If
     807          media upload is not supported, this is None.
     808    """
     809      path_url = method_desc["path"]
     810      http_method = method_desc["httpMethod"]
     811      method_id = method_desc["id"]
     812
     813      parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
     814      # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
     815      # 'parameters' key and needs to know if there is a 'body' parameter because it
     816      # also sets a 'media_body' parameter.
     817      accept, max_size, media_path_url = _fix_up_media_upload(
     818          method_desc, root_desc, path_url, parameters
     819      )
     820
     821      return path_url, http_method, method_id, accept, max_size, media_path_url
     822
     823
     824 -def _urljoin(base, url):
     825      """Custom urljoin replacement supporting : before / in url."""
     826      # In general, it's unsafe to simply join base and url. However, for
     827      # the case of discovery documents, we know:
     828      #  * base will never contain params, query, or fragment
     829      #  * url will never contain a scheme or net_loc.
     830      # In general, this means we can safely join on /; we just need to
     831      # ensure we end up with precisely one / joining base and url. The
     832      # exception here is the case of media uploads, where url will be an
     833      # absolute url.
     834      if url.startswith("http://") or url.startswith("https://"):
     835          return urljoin(base, url)
     836      new_base = base if base.endswith("/") else base + "/"
     837      new_url = url[1:] if url.startswith("/") else url
     838      return new_base + new_url
     839
     840
     841  # TODO(dhermes): Convert this class to ResourceMethod and make it callable
     842 -class ResourceMethodParameters(object):
     843      """Represents the parameters associated with a method.
     844
     845    Attributes:
     846      argmap: Map from method parameter name (string) to query parameter name
     847          (string).
     848      required_params: List of required parameters (represented by parameter
     849          name as string).
     850      repeated_params: List of repeated parameters (represented by parameter
     851          name as string).
     852      pattern_params: Map from method parameter name (string) to regular
     853          expression (as a string). If the pattern is set for a parameter, the
     854          value for that parameter must match the regular expression.
     855      query_params: List of parameters (represented by parameter name as string)
     856          that will be used in the query string.
     857      path_params: Set of parameters (represented by parameter name as string)
     858          that will be used in the base URL path.
     859      param_types: Map from method parameter name (string) to parameter type. Type
     860          can be any valid JSON schema type; valid values are 'any', 'array',
     861          'boolean', 'integer', 'number', 'object', or 'string'. Reference:
     862          http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
     863      enum_params: Map from method parameter name (string) to list of strings,
     864         where each list of strings is the list of acceptable enum values.
     865    """
     866
     867 -    def __init__(self, method_desc):
     868          """Constructor for ResourceMethodParameters.
     869
     870      Sets default values and defers to set_parameters to populate.
     871
     872      Args:
     873        method_desc: Dictionary with metadata describing an API method. Value
     874            comes from the dictionary of methods stored in the 'methods' key in
     875            the deserialized discovery document.
     876      """
     877          self.argmap = {}
     878          self.required_params = []
     879          self.repeated_params = []
     880          self.pattern_params = {}
     881          self.query_params = []
     882          # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
     883          #                parsing is gotten rid of.
     884          self.path_params = set()
     885          self.param_types = {}
     886          self.enum_params = {}
     887
     888          self.set_parameters(method_desc)
     889
     890 -    def set_parameters(self, method_desc):
     891          """Populates maps and lists based on method description.
     892
     893      Iterates through each parameter for the method and parses the values from
     894      the parameter dictionary.
     895
     896      Args:
     897        method_desc: Dictionary with metadata describing an API method. Value
     898            comes from the dictionary of methods stored in the 'methods' key in
     899            the deserialized discovery document.
     900      """
     901          for arg, desc in six.iteritems(method_desc.get("parameters", {})):
     902              param = key2param(arg)
     903              self.argmap[param] = arg
     904
     905              if desc.get("pattern"):
     906                  self.pattern_params[param] = desc["pattern"]
     907              if desc.get("enum"):
     908                  self.enum_params[param] = desc["enum"]
     909              if desc.get("required"):
     910                  self.required_params.append(param)
     911              if desc.get("repeated"):
     912                  self.repeated_params.append(param)
     913              if desc.get("location") == "query":
     914                  self.query_params.append(param)
     915              if desc.get("location") == "path":
     916                  self.path_params.add(param)
     917              self.param_types[param] = desc.get("type", "string")
     918
     919          # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
     920          #                should have all path parameters already marked with
     921          #                'location: path'.
     922          for match in URITEMPLATE.finditer(method_desc["path"]):
     923              for namematch in VARNAME.finditer(match.group(0)):
     924                  name = key2param(namematch.group(0))
     925                  self.path_params.add(name)
     926                  if name in self.query_params:
     927                      self.query_params.remove(name)
     928
     929
     930 -def createMethod(methodName, methodDesc, rootDesc, schema):
     931      """Creates a method for attaching to a Resource.
     932
     933    Args:
     934      methodName: string, name of the method to use.
     935      methodDesc: object, fragment of deserialized discovery document that
     936        describes the method.
     937      rootDesc: object, the entire deserialized discovery document.
     938      schema: object, mapping of schema names to schema descriptions.
     939    """
     940      methodName = fix_method_name(methodName)
     941      (
     942          pathUrl,
     943          httpMethod,
     944          methodId,
     945          accept,
     946          maxSize,
     947          mediaPathUrl,
     948      ) = _fix_up_method_description(methodDesc, rootDesc, schema)
     949
     950      parameters = ResourceMethodParameters(methodDesc)
     951
     952      def method(self, **kwargs):
     953          # Don't bother with doc string, it will be over-written by createMethod.
     954
     955          for name in six.iterkeys(kwargs):
     956              if name not in parameters.argmap:
     957                  raise TypeError('Got an unexpected keyword argument "%s"' % name)
     958
     959          # Remove args that have a value of None.
     960          keys = list(kwargs.keys())
     961          for name in keys:
     962              if kwargs[name] is None:
     963                  del kwargs[name]
     964
     965          for name in parameters.required_params:
     966              if name not in kwargs:
     967                  # temporary workaround for non-paging methods incorrectly requiring
     968                  # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
     969                  if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
     970                      _methodProperties(methodDesc, schema, "response")
     971                  ):
     972                      raise TypeError('Missing required parameter "%s"' % name)
     973
     974          for name, regex in six.iteritems(parameters.pattern_params):
     975              if name in kwargs:
     976                  if isinstance(kwargs[name], six.string_types):
     977                      pvalues = [kwargs[name]]
     978                  else:
     979                      pvalues = kwargs[name]
     980                  for pvalue in pvalues:
     981                      if re.match(regex, pvalue) is None:
     982                          raise TypeError(
     983                              'Parameter "%s" value "%s" does not match the pattern "%s"'
     984                              % (name, pvalue, regex)
     985                          )
     986
     987          for name, enums in six.iteritems(parameters.enum_params):
     988              if name in kwargs:
     989                  # We need to handle the case of a repeated enum
     990                  # name differently, since we want to handle both
     991                  # arg='value' and arg=['value1', 'value2']
     992                  if name in parameters.repeated_params and not isinstance(
     993                      kwargs[name], six.string_types
     994                  ):
     995                      values = kwargs[name]
     996                  else:
     997                      values = [kwargs[name]]
     998                  for value in values:
     999                      if value not in enums:
    1000                          raise TypeError(
    1001                              'Parameter "%s" value "%s" is not an allowed value in "%s"'
    1002                              % (name, value, str(enums))
    1003                          )
    1004
    1005          actual_query_params = {}
    1006          actual_path_params = {}
    1007          for key, value in six.iteritems(kwargs):
    1008              to_type = parameters.param_types.get(key, "string")
    1009              # For repeated parameters we cast each member of the list.
    1010              if key in parameters.repeated_params and type(value) == type([]):
    1011                  cast_value = [_cast(x, to_type) for x in value]
    1012              else:
    1013                  cast_value = _cast(value, to_type)
    1014              if key in parameters.query_params:
    1015                  actual_query_params[parameters.argmap[key]] = cast_value
    1016              if key in parameters.path_params:
    1017                  actual_path_params[parameters.argmap[key]] = cast_value
    1018          body_value = kwargs.get("body", None)
    1019          media_filename = kwargs.get("media_body", None)
    1020          media_mime_type = kwargs.get("media_mime_type", None)
    1021
    1022          if self._developerKey:
    1023              actual_query_params["key"] = self._developerKey
    1024
    1025          model = self._model
    1026          if methodName.endswith("_media"):
    1027              model = MediaModel()
    1028          elif "response" not in methodDesc:
    1029              model = RawModel()
    1030
    1031          headers = {}
    1032          headers, params, query, body = model.request(
    1033              headers, actual_path_params, actual_query_params, body_value
    1034          )
    1035
    1036          expanded_url = uritemplate.expand(pathUrl, params)
    1037          url = _urljoin(self._baseUrl, expanded_url + query)
    1038
    1039          resumable = None
    1040          multipart_boundary = ""
    1041
    1042          if media_filename:
    1043              # Ensure we end up with a valid MediaUpload object.
    1044              if isinstance(media_filename, six.string_types):
    1045                  if media_mime_type is None:
    1046                      logger.warning(
    1047                          "media_mime_type argument not specified: trying to auto-detect for %s",
    1048                          media_filename,
    1049                      )
    1050                      media_mime_type, _ = mimetypes.guess_type(media_filename)
    1051                  if media_mime_type is None:
    1052                      raise UnknownFileType(media_filename)
    1053                  if not mimeparse.best_match([media_mime_type], ",".join(accept)):
    1054                      raise UnacceptableMimeTypeError(media_mime_type)
    1055                  media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
    1056              elif isinstance(media_filename, MediaUpload):
    1057                  media_upload = media_filename
    1058              else:
    1059                  raise TypeError("media_filename must be str or MediaUpload.")
    1060
    1061              # Check the maxSize
    1062              if media_upload.size() is not None and media_upload.size() > maxSize > 0:
    1063                  raise MediaUploadSizeError("Media larger than: %s" % maxSize)
    1064
    1065              # Use the media path uri for media uploads
    1066              expanded_url = uritemplate.expand(mediaPathUrl, params)
    1067              url = _urljoin(self._baseUrl, expanded_url + query)
    1068              if media_upload.resumable():
    1069                  url = _add_query_parameter(url, "uploadType", "resumable")
    1070
    1071              if media_upload.resumable():
    1072                  # This is all we need to do for resumable, if the body exists it gets
    1073                  # sent in the first request, otherwise an empty body is sent.
    1074                  resumable = media_upload
    1075              else:
    1076                  # A non-resumable upload
    1077                  if body is None:
    1078                      # This is a simple media upload
    1079                      headers["content-type"] = media_upload.mimetype()
    1080                      body = media_upload.getbytes(0, media_upload.size())
    1081                      url = _add_query_parameter(url, "uploadType", "media")
    1082                  else:
    1083                      # This is a multipart/related upload.
    1084                      msgRoot = MIMEMultipart("related")
    1085                      # msgRoot should not write out it's own headers
    1086                      setattr(msgRoot, "_write_headers", lambda self: None)
    1087
    1088                      # attach the body as one part
    1089                      msg = MIMENonMultipart(*headers["content-type"].split("/"))
    1090                      msg.set_payload(body)
    1091                      msgRoot.attach(msg)
    1092
    1093                      # attach the media as the second part
    1094                      msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
    1095                      msg["Content-Transfer-Encoding"] = "binary"
    1096
    1097                      payload = media_upload.getbytes(0, media_upload.size())
    1098                      msg.set_payload(payload)
    1099                      msgRoot.attach(msg)
    1100                      # encode the body: note that we can't use `as_string`, because
    1101                      # it plays games with `From ` lines.
    1102                      fp = BytesIO()
    1103                      g = _BytesGenerator(fp, mangle_from_=False)
    1104                      g.flatten(msgRoot, unixfrom=False)
    1105                      body = fp.getvalue()
    1106
    1107                      multipart_boundary = msgRoot.get_boundary()
    1108                      headers["content-type"] = (
    1109                          "multipart/related; " 'boundary="%s"'
    1110                      ) % multipart_boundary
    1111                      url = _add_query_parameter(url, "uploadType", "multipart")
    1112
    1113          logger.debug("URL being requested: %s %s" % (httpMethod, url))
    1114          return self._requestBuilder(
    1115              self._http,
    1116              model.response,
    1117              url,
    1118              method=httpMethod,
    1119              body=body,
    1120              headers=headers,
    1121              methodId=methodId,
    1122              resumable=resumable,
    1123          )
    1124
    1125      docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
    1126      if len(parameters.argmap) > 0:
    1127          docs.append("Args:\n")
    1128
    1129      # Skip undocumented params and params common to all methods.
    1130      skip_parameters = list(rootDesc.get("parameters", {}).keys())
    1131      skip_parameters.extend(STACK_QUERY_PARAMETERS)
    1132
    1133      all_args = list(parameters.argmap.keys())
    1134      args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
    1135
    1136      # Move body to the front of the line.
    1137      if "body" in all_args:
    1138          args_ordered.append("body")
    1139
    1140      for name in all_args:
    1141          if name not in args_ordered:
    1142              args_ordered.append(name)
    1143
    1144      for arg in args_ordered:
    1145          if arg in skip_parameters:
    1146              continue
    1147
    1148          repeated = ""
    1149          if arg in parameters.repeated_params:
    1150              repeated = " (repeated)"
    1151          required = ""
    1152          if arg in parameters.required_params:
    1153              required = " (required)"
    1154          paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
    1155          paramdoc = paramdesc.get("description", "A parameter")
    1156          if "$ref" in paramdesc:
    1157              docs.append(
    1158                  ("  %s: object, %s%s%s\n    The object takes the" " form of:\n\n%s\n\n")
    1159                  % (
    1160                      arg,
    1161                      paramdoc,
    1162                      required,
    1163                      repeated,
    1164                      schema.prettyPrintByName(paramdesc["$ref"]),
    1165                  )
    1166              )
    1167          else:
    1168              paramtype = paramdesc.get("type", "string")
    1169              docs.append(
    1170                  "  %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
    1171              )
    1172          enum = paramdesc.get("enum", [])
    1173          enumDesc = paramdesc.get("enumDescriptions", [])
    1174          if enum and enumDesc:
    1175              docs.append("    Allowed values\n")
    1176              for (name, desc) in zip(enum, enumDesc):
    1177                  docs.append("      %s - %s\n" % (name, desc))
    1178      if "response" in methodDesc:
    1179          if methodName.endswith("_media"):
    1180              docs.append("\nReturns:\n  The media object as a string.\n\n    ")
    1181          else:
    1182              docs.append("\nReturns:\n  An object of the form:\n\n    ")
    1183              docs.append(schema.prettyPrintSchema(methodDesc["response"]))
    1184
    1185      setattr(method, "__doc__", "".join(docs))
    1186      return (methodName, method)
    1187
    1188
    1189 -def createNextMethod(
    1190      methodName,
    1191      pageTokenName="pageToken",
    1192      nextPageTokenName="nextPageToken",
    1193      isPageTokenParameter=True,
    1194  ):
    1195      """Creates any _next methods for attaching to a Resource.
    1196
    1197    The _next methods allow for easy iteration through list() responses.
    1198
    1199    Args:
    1200      methodName: string, name of the method to use.
    1201      pageTokenName: string, name of request page token field.
    1202      nextPageTokenName: string, name of response page token field.
    1203      isPageTokenParameter: Boolean, True if request page token is a query
    1204          parameter, False if request page token is a field of the request body.
    1205    """
    1206      methodName = fix_method_name(methodName)
    1207
    1208      def methodNext(self, previous_request, previous_response):
    1209          """Retrieves the next page of results.
    1210
    1211  Args:
    1212    previous_request: The request for the previous page. (required)
    1213    previous_response: The response from the request for the previous page. (required)
    1214
    1215  Returns:
    1216    A request object that you can call 'execute()' on to request the next
    1217    page. Returns None if there are no more items in the collection.
    1218      """
    1219          # Retrieve nextPageToken from previous_response
    1220          # Use as pageToken in previous_request to create new request.
    1221
    1222          nextPageToken = previous_response.get(nextPageTokenName, None)
    1223          if not nextPageToken:
    1224              return None
    1225
    1226          request = copy.copy(previous_request)
    1227
    1228          if isPageTokenParameter:
    1229              # Replace pageToken value in URI
    1230              request.uri = _add_query_parameter(
    1231                  request.uri, pageTokenName, nextPageToken
    1232              )
    1233              logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
    1234          else:
    1235              # Replace pageToken value in request body
    1236              model = self._model
    1237              body = model.deserialize(request.body)
    1238              body[pageTokenName] = nextPageToken
    1239              request.body = model.serialize(body)
    1240              logger.debug("Next page request body: %s %s" % (methodName, body))
    1241
    1242          return request
    1243
    1244      return (methodName, methodNext)
    1245
    1246
    1247 -class Resource(object):
    1248      """A class for interacting with a resource."""
    1249
    1250 -    def __init__(
    1251          self,
    1252          http,
    1253          baseUrl,
    1254          model,
    1255          requestBuilder,
    1256          developerKey,
    1257          resourceDesc,
    1258          rootDesc,
    1259          schema,
    1260      ):
    1261          """Build a Resource from the API description.
    1262
    1263      Args:
    1264        http: httplib2.Http, Object to make http requests with.
    1265        baseUrl: string, base URL for the API. All requests are relative to this
    1266            URI.
    1267        model: googleapiclient.Model, converts to and from the wire format.
    1268        requestBuilder: class or callable that instantiates an
    1269            googleapiclient.HttpRequest object.
    1270        developerKey: string, key obtained from
    1271            https://code.google.com/apis/console
    1272        resourceDesc: object, section of deserialized discovery document that
    1273            describes a resource. Note that the top level discovery document
    1274            is considered a resource.
    1275        rootDesc: object, the entire deserialized discovery document.
    1276        schema: object, mapping of schema names to schema descriptions.
    1277      """
    1278          self._dynamic_attrs = []
    1279
    1280          self._http = http
    1281          self._baseUrl = baseUrl
    1282          self._model = model
    1283          self._developerKey = developerKey
    1284          self._requestBuilder = requestBuilder
    1285          self._resourceDesc = resourceDesc
    1286          self._rootDesc = rootDesc
    1287          self._schema = schema
    1288
    1289          self._set_service_methods()
    1290
    1291 -    def _set_dynamic_attr(self, attr_name, value):
    1292          """Sets an instance attribute and tracks it in a list of dynamic attributes.
    1293
    1294      Args:
    1295        attr_name: string; The name of the attribute to be set
    1296        value: The value being set on the object and tracked in the dynamic cache.
    1297      """
    1298          self._dynamic_attrs.append(attr_name)
    1299          self.__dict__[attr_name] = value
    1300
    1301 -    def __getstate__(self):
    1302          """Trim the state down to something that can be pickled.
    1303
    1304      Uses the fact that the instance variable _dynamic_attrs holds attrs that
    1305      will be wiped and restored on pickle serialization.
    1306      """
    1307          state_dict = copy.copy(self.__dict__)
    1308          for dynamic_attr in self._dynamic_attrs:
    1309              del state_dict[dynamic_attr]
    1310          del state_dict["_dynamic_attrs"]
    1311          return state_dict
    1312
    1313 -    def __setstate__(self, state):
    1314          """Reconstitute the state of the object from being pickled.
    1315
    1316      Uses the fact that the instance variable _dynamic_attrs holds attrs that
    1317      will be wiped and restored on pickle serialization.
    1318      """
    1319          self.__dict__.update(state)
    1320          self._dynamic_attrs = []
    1321          self._set_service_methods()
    1322
    1323
    1324 -    def __enter__(self):
    1325          return self
    1326
    1327 -    def __exit__(self, exc_type, exc, exc_tb):
    1328          self.close()
    1329
    1330 -    def close(self):
    1331          """Close httplib2 connections."""
    1332          # httplib2 leaves sockets open by default.
    1333          # Cleanup using the `close` method.
    1334          # https://github.com/httplib2/httplib2/issues/148
    1335          self._http.http.close()
    1336
    1337 -    def _set_service_methods(self):
    1338          self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
    1339          self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
    1340          self._add_next_methods(self._resourceDesc, self._schema)
    1341
    1342 -    def _add_basic_methods(self, resourceDesc, rootDesc, schema):
    1343          # If this is the root Resource, add a new_batch_http_request() method.
    1344          if resourceDesc == rootDesc:
    1345              batch_uri = "%s%s" % (
    1346                  rootDesc["rootUrl"],
    1347                  rootDesc.get("batchPath", "batch"),
    1348              )
    1349
    1350              def new_batch_http_request(callback=None):
    1351                  """Create a BatchHttpRequest object based on the discovery document.
    1352
    1353          Args:
    1354            callback: callable, A callback to be called for each response, of the
    1355              form callback(id, response, exception). The first parameter is the
    1356              request id, and the second is the deserialized response object. The
    1357              third is an apiclient.errors.HttpError exception object if an HTTP
    1358              error occurred while processing the request, or None if no error
    1359              occurred.
    1360
    1361          Returns:
    1362            A BatchHttpRequest object based on the discovery document.
    1363          """
    1364                  return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
    1365
    1366              self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
    1367
    1368          # Add basic methods to Resource
    1369          if "methods" in resourceDesc:
    1370              for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
    1371                  fixedMethodName, method = createMethod(
    1372                      methodName, methodDesc, rootDesc, schema
    1373                  )
    1374                  self._set_dynamic_attr(
    1375                      fixedMethodName, method.__get__(self, self.__class__)
    1376                  )
    1377                  # Add in _media methods. The functionality of the attached method will
    1378                  # change when it sees that the method name ends in _media.
    1379                  if methodDesc.get("supportsMediaDownload", False):
    1380                      fixedMethodName, method = createMethod(
    1381                          methodName + "_media", methodDesc, rootDesc, schema
    1382                      )
    1383                      self._set_dynamic_attr(
    1384                          fixedMethodName, method.__get__(self, self.__class__)
    1385                      )
    1386
    1387 -    def _add_nested_resources(self, resourceDesc, rootDesc, schema):
    1388          # Add in nested resources
    1389          if "resources" in resourceDesc:
    1390
    1391              def createResourceMethod(methodName, methodDesc):
    1392                  """Create a method on the Resource to access a nested Resource.
    1393
    1394          Args:
    1395            methodName: string, name of the method to use.
    1396            methodDesc: object, fragment of deserialized discovery document that
    1397              describes the method.
    1398          """
    1399                  methodName = fix_method_name(methodName)
    1400
    1401                  def methodResource(self):
    1402                      return Resource(
    1403                          http=self._http,
    1404                          baseUrl=self._baseUrl,
    1405                          model=self._model,
    1406                          developerKey=self._developerKey,
    1407                          requestBuilder=self._requestBuilder,
    1408                          resourceDesc=methodDesc,
    1409                          rootDesc=rootDesc,
    1410                          schema=schema,
    1411                      )
    1412
    1413                  setattr(methodResource, "__doc__", "A collection resource.")
    1414                  setattr(methodResource, "__is_resource__", True)
    1415
    1416                  return (methodName, methodResource)
    1417
    1418              for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
    1419                  fixedMethodName, method = createResourceMethod(methodName, methodDesc)
    1420                  self._set_dynamic_attr(
    1421                      fixedMethodName, method.__get__(self, self.__class__)
    1422                  )
    1423
    1424 -    def _add_next_methods(self, resourceDesc, schema):
    1425          # Add _next() methods if and only if one of the names 'pageToken' or
    1426          # 'nextPageToken' occurs among the fields of both the method's response
    1427          # type either the method's request (query parameters) or request body.
    1428          if "methods" not in resourceDesc:
    1429              return
    1430          for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
    1431              nextPageTokenName = _findPageTokenName(
    1432                  _methodProperties(methodDesc, schema, "response")
    1433              )
    1434              if not nextPageTokenName:
    1435                  continue
    1436              isPageTokenParameter = True
    1437              pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
    1438              if not pageTokenName:
    1439                  isPageTokenParameter = False
    1440                  pageTokenName = _findPageTokenName(
    1441                      _methodProperties(methodDesc, schema, "request")
    1442                  )
    1443              if not pageTokenName:
    1444                  continue
    1445              fixedMethodName, method = createNextMethod(
    1446                  methodName + "_next",
    1447                  pageTokenName,
    1448                  nextPageTokenName,
    1449                  isPageTokenParameter,
    1450              )
    1451              self._set_dynamic_attr(
    1452                  fixedMethodName, method.__get__(self, self.__class__)
    1453              )
    1454
    1455
    1456 -def _findPageTokenName(fields):
    1457      """Search field names for one like a page token.
    1458
    1459    Args:
    1460      fields: container of string, names of fields.
    1461
    1462    Returns:
    1463      First name that is either 'pageToken' or 'nextPageToken' if one exists,
    1464      otherwise None.
    1465    """
    1466      return next(
    1467          (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
    1468      )
    1469
    1470
    1471 -def _methodProperties(methodDesc, schema, name):
    1472      """Get properties of a field in a method description.
    1473
    1474    Args:
    1475      methodDesc: object, fragment of deserialized discovery document that
    1476        describes the method.
    1477      schema: object, mapping of schema names to schema descriptions.
    1478      name: string, name of top-level field in method description.
    1479
    1480    Returns:
    1481      Object representing fragment of deserialized discovery document
    1482      corresponding to 'properties' field of object corresponding to named field
    1483      in method description, if it exists, otherwise empty dict.
    1484    """
    1485      desc = methodDesc.get(name, {})
    1486      if "$ref" in desc:
    1487          desc = schema.get(desc["$ref"], {})
    1488      return desc.get("properties", {})
    1489