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