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