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