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