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  # TODO(dhermes): Convert this class to ResourceMethod and make it callable
    495 -class ResourceMethodParameters(object):
    496    """Represents the parameters associated with a method.
    497
    498    Attributes:
    499      argmap: Map from method parameter name (string) to query parameter name
    500          (string).
    501      required_params: List of required parameters (represented by parameter
    502          name as string).
    503      repeated_params: List of repeated parameters (represented by parameter
    504          name as string).
    505      pattern_params: Map from method parameter name (string) to regular
    506          expression (as a string). If the pattern is set for a parameter, the
    507          value for that parameter must match the regular expression.
    508      query_params: List of parameters (represented by parameter name as string)
    509          that will be used in the query string.
    510      path_params: Set of parameters (represented by parameter name as string)
    511          that will be used in the base URL path.
    512      param_types: Map from method parameter name (string) to parameter type. Type
    513          can be any valid JSON schema type; valid values are 'any', 'array',
    514          'boolean', 'integer', 'number', 'object', or 'string'. Reference:
    515          http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
    516      enum_params: Map from method parameter name (string) to list of strings,
    517         where each list of strings is the list of acceptable enum values.
    518    """
    519
    520 -  def __init__(self, method_desc):
    521      """Constructor for ResourceMethodParameters.
    522
    523      Sets default values and defers to set_parameters to populate.
    524
    525      Args:
    526        method_desc: Dictionary with metadata describing an API method. Value
    527            comes from the dictionary of methods stored in the 'methods' key in
    528            the deserialized discovery document.
    529      """
    530      self.argmap = {}
    531      self.required_params = []
    532      self.repeated_params = []
    533      self.pattern_params = {}
    534      self.query_params = []
    535      # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
    536      #                parsing is gotten rid of.
    537      self.path_params = set()
    538      self.param_types = {}
    539      self.enum_params = {}
    540
    541      self.set_parameters(method_desc)
    542
    543 -  def set_parameters(self, method_desc):
    544      """Populates maps and lists based on method description.
    545
    546      Iterates through each parameter for the method and parses the values from
    547      the parameter dictionary.
    548
    549      Args:
    550        method_desc: Dictionary with metadata describing an API method. Value
    551            comes from the dictionary of methods stored in the 'methods' key in
    552            the deserialized discovery document.
    553      """
    554      for arg, desc in method_desc.get('parameters', {}).iteritems():
    555        param = key2param(arg)
    556        self.argmap[param] = arg
    557
    558        if desc.get('pattern'):
    559          self.pattern_params[param] = desc['pattern']
    560        if desc.get('enum'):
    561          self.enum_params[param] = desc['enum']
    562        if desc.get('required'):
    563          self.required_params.append(param)
    564        if desc.get('repeated'):
    565          self.repeated_params.append(param)
    566        if desc.get('location') == 'query':
    567          self.query_params.append(param)
    568        if desc.get('location') == 'path':
    569          self.path_params.add(param)
    570        self.param_types[param] = desc.get('type', 'string')
    571
    572      # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
    573      #                should have all path parameters already marked with
    574      #                'location: path'.
    575      for match in URITEMPLATE.finditer(method_desc['path']):
    576        for namematch in VARNAME.finditer(match.group(0)):
    577          name = key2param(namematch.group(0))
    578          self.path_params.add(name)
    579          if name in self.query_params:
    580            self.query_params.remove(name)
    581
    582
    583 -def createMethod(methodName, methodDesc, rootDesc, schema):
    584    """Creates a method for attaching to a Resource.
    585
    586    Args:
    587      methodName: string, name of the method to use.
    588      methodDesc: object, fragment of deserialized discovery document that
    589        describes the method.
    590      rootDesc: object, the entire deserialized discovery document.
    591      schema: object, mapping of schema names to schema descriptions.
    592    """
    593    methodName = fix_method_name(methodName)
    594    (pathUrl, httpMethod, methodId, accept,
    595     maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
    596
    597    parameters = ResourceMethodParameters(methodDesc)
    598
    599    def method(self, **kwargs):
    600      # Don't bother with doc string, it will be over-written by createMethod.
    601
    602      for name in kwargs.iterkeys():
    603        if name not in parameters.argmap:
    604          raise TypeError('Got an unexpected keyword argument "%s"' % name)
    605
    606      # Remove args that have a value of None.
    607      keys = kwargs.keys()
    608      for name in keys:
    609        if kwargs[name] is None:
    610          del kwargs[name]
    611
    612      for name in parameters.required_params:
    613        if name not in kwargs:
    614          raise TypeError('Missing required parameter "%s"' % name)
    615
    616      for name, regex in parameters.pattern_params.iteritems():
    617        if name in kwargs:
    618          if isinstance(kwargs[name], basestring):
    619            pvalues = [kwargs[name]]
    620          else:
    621            pvalues = kwargs[name]
    622          for pvalue in pvalues:
    623            if re.match(regex, pvalue) is None:
    624              raise TypeError(
    625                  'Parameter "%s" value "%s" does not match the pattern "%s"' %
    626                  (name, pvalue, regex))
    627
    628      for name, enums in parameters.enum_params.iteritems():
    629        if name in kwargs:
    630          # We need to handle the case of a repeated enum
    631          # name differently, since we want to handle both
    632          # arg='value' and arg=['value1', 'value2']
    633          if (name in parameters.repeated_params and
    634              not isinstance(kwargs[name], basestring)):
    635            values = kwargs[name]
    636          else:
    637            values = [kwargs[name]]
    638          for value in values:
    639            if value not in enums:
    640              raise TypeError(
    641                  'Parameter "%s" value "%s" is not an allowed value in "%s"' %
    642                  (name, value, str(enums)))
    643
    644      actual_query_params = {}
    645      actual_path_params = {}
    646      for key, value in kwargs.iteritems():
    647        to_type = parameters.param_types.get(key, 'string')
    648        # For repeated parameters we cast each member of the list.
    649        if key in parameters.repeated_params and type(value) == type([]):
    650          cast_value = [_cast(x, to_type) for x in value]
    651        else:
    652          cast_value = _cast(value, to_type)
    653        if key in parameters.query_params:
    654          actual_query_params[parameters.argmap[key]] = cast_value
    655        if key in parameters.path_params:
    656          actual_path_params[parameters.argmap[key]] = cast_value
    657      body_value = kwargs.get('body', None)
    658      media_filename = kwargs.get('media_body', None)
    659
    660      if self._developerKey:
    661        actual_query_params['key'] = self._developerKey
    662
    663      model = self._model
    664      if methodName.endswith('_media'):
    665        model = MediaModel()
    666      elif 'response' not in methodDesc:
    667        model = RawModel()
    668
    669      headers = {}
    670      headers, params, query, body = model.request(headers,
    671          actual_path_params, actual_query_params, body_value)
    672
    673      expanded_url = uritemplate.expand(pathUrl, params)
    674      url = urlparse.urljoin(self._baseUrl, expanded_url + query)
    675
    676      resumable = None
    677      multipart_boundary = ''
    678
    679      if media_filename:
    680        # Ensure we end up with a valid MediaUpload object.
    681        if isinstance(media_filename, basestring):
    682          (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
    683          if media_mime_type is None:
    684            raise UnknownFileType(media_filename)
    685          if not mimeparse.best_match([media_mime_type], ','.join(accept)):
    686            raise UnacceptableMimeTypeError(media_mime_type)
    687          media_upload = MediaFileUpload(media_filename,
    688                                         mimetype=media_mime_type)
    689        elif isinstance(media_filename, MediaUpload):
    690          media_upload = media_filename
    691        else:
    692          raise TypeError('media_filename must be str or MediaUpload.')
    693
    694        # Check the maxSize
    695        if maxSize > 0 and media_upload.size() > maxSize:
    696          raise MediaUploadSizeError("Media larger than: %s" % maxSize)
    697
    698        # Use the media path uri for media uploads
    699        expanded_url = uritemplate.expand(mediaPathUrl, params)
    700        url = urlparse.urljoin(self._baseUrl, expanded_url + query)
    701        if media_upload.resumable():
    702          url = _add_query_parameter(url, 'uploadType', 'resumable')
    703
    704        if media_upload.resumable():
    705          # This is all we need to do for resumable, if the body exists it gets
    706          # sent in the first request, otherwise an empty body is sent.
    707          resumable = media_upload
    708        else:
    709          # A non-resumable upload
    710          if body is None:
    711            # This is a simple media upload
    712            headers['content-type'] = media_upload.mimetype()
    713            body = media_upload.getbytes(0, media_upload.size())
    714            url = _add_query_parameter(url, 'uploadType', 'media')
    715          else:
    716            # This is a multipart/related upload.
    717            msgRoot = MIMEMultipart('related')
    718            # msgRoot should not write out it's own headers
    719            setattr(msgRoot, '_write_headers', lambda self: None)
    720
    721            # attach the body as one part
    722            msg = MIMENonMultipart(*headers['content-type'].split('/'))
    723            msg.set_payload(body)
    724            msgRoot.attach(msg)
    725
    726            # attach the media as the second part
    727            msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
    728            msg['Content-Transfer-Encoding'] = 'binary'
    729
    730            payload = media_upload.getbytes(0, media_upload.size())
    731            msg.set_payload(payload)
    732            msgRoot.attach(msg)
    733            # encode the body: note that we can't use `as_string`, because
    734            # it plays games with `From ` lines.
    735            fp = StringIO.StringIO()
    736            g = Generator(fp, mangle_from_=False)
    737            g.flatten(msgRoot, unixfrom=False)
    738            body = fp.getvalue()
    739
    740            multipart_boundary = msgRoot.get_boundary()
    741            headers['content-type'] = ('multipart/related; '
    742                                       'boundary="%s"') % multipart_boundary
    743            url = _add_query_parameter(url, 'uploadType', 'multipart')
    744
    745      logger.info('URL being requested: %s %s' % (httpMethod,url))
    746      return self._requestBuilder(self._http,
    747                                  model.response,
    748                                  url,
    749                                  method=httpMethod,
    750                                  body=body,
    751                                  headers=headers,
    752                                  methodId=methodId,
    753                                  resumable=resumable)
    754
    755    docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
    756    if len(parameters.argmap) > 0:
    757      docs.append('Args:\n')
    758
    759    # Skip undocumented params and params common to all methods.
    760    skip_parameters = rootDesc.get('parameters', {}).keys()
    761    skip_parameters.extend(STACK_QUERY_PARAMETERS)
    762
    763    all_args = parameters.argmap.keys()
    764    args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
    765
    766    # Move body to the front of the line.
    767    if 'body' in all_args:
    768      args_ordered.append('body')
    769
    770    for name in all_args:
    771      if name not in args_ordered:
    772        args_ordered.append(name)
    773
    774    for arg in args_ordered:
    775      if arg in skip_parameters:
    776        continue
    777
    778      repeated = ''
    779      if arg in parameters.repeated_params:
    780        repeated = ' (repeated)'
    781      required = ''
    782      if arg in parameters.required_params:
    783        required = ' (required)'
    784      paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
    785      paramdoc = paramdesc.get('description', 'A parameter')
    786      if '$ref' in paramdesc:
    787        docs.append(
    788            ('  %s: object, %s%s%s\n    The object takes the'
    789            ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
    790              schema.prettyPrintByName(paramdesc['$ref'])))
    791      else:
    792        paramtype = paramdesc.get('type', 'string')
    793        docs.append('  %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
    794                                            repeated))
    795      enum = paramdesc.get('enum', [])
    796      enumDesc = paramdesc.get('enumDescriptions', [])
    797      if enum and enumDesc:
    798        docs.append('    Allowed values\n')
    799        for (name, desc) in zip(enum, enumDesc):
    800          docs.append('      %s - %s\n' % (name, desc))
    801    if 'response' in methodDesc:
    802      if methodName.endswith('_media'):
    803        docs.append('\nReturns:\n  The media object as a string.\n\n    ')
    804      else:
    805        docs.append('\nReturns:\n  An object of the form:\n\n    ')
    806        docs.append(schema.prettyPrintSchema(methodDesc['response']))
    807
    808    setattr(method, '__doc__', ''.join(docs))
    809    return (methodName, method)
    810
    811
    812 -def createNextMethod(methodName):
    813    """Creates any _next methods for attaching to a Resource.
    814
    815    The _next methods allow for easy iteration through list() responses.
    816
    817    Args:
    818      methodName: string, name of the method to use.
    819    """
    820    methodName = fix_method_name(methodName)
    821
    822    def methodNext(self, previous_request, previous_response):
    823      """Retrieves the next page of results.
    824
    825  Args:
    826    previous_request: The request for the previous page. (required)
    827    previous_response: The response from the request for the previous page. (required)
    828
    829  Returns:
    830    A request object that you can call 'execute()' on to request the next
    831    page. Returns None if there are no more items in the collection.
    832      """
    833      # Retrieve nextPageToken from previous_response
    834      # Use as pageToken in previous_request to create new request.
    835
    836      if 'nextPageToken' not in previous_response:
    837        return None
    838
    839      request = copy.copy(previous_request)
    840
    841      pageToken = previous_response['nextPageToken']
    842      parsed = list(urlparse.urlparse(request.uri))
    843      q = parse_qsl(parsed[4])
    844
    845      # Find and remove old 'pageToken' value from URI
    846      newq = [(key, value) for (key, value) in q if key != 'pageToken']
    847      newq.append(('pageToken', pageToken))
    848      parsed[4] = urllib.urlencode(newq)
    849      uri = urlparse.urlunparse(parsed)
    850
    851      request.uri = uri
    852
    853      logger.info('URL being requested: %s %s' % (methodName,uri))
    854
    855      return request
    856
    857    return (methodName, methodNext)
    858
    859
    860 -class Resource(object):
    861    """A class for interacting with a resource."""
    862
    863 -  def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
    864                 resourceDesc, rootDesc, schema):
    865      """Build a Resource from the API description.
    866
    867      Args:
    868        http: httplib2.Http, Object to make http requests with.
    869        baseUrl: string, base URL for the API. All requests are relative to this
    870            URI.
    871        model: googleapiclient.Model, converts to and from the wire format.
    872        requestBuilder: class or callable that instantiates an
    873            googleapiclient.HttpRequest object.
    874        developerKey: string, key obtained from
    875            https://code.google.com/apis/console
    876        resourceDesc: object, section of deserialized discovery document that
    877            describes a resource. Note that the top level discovery document
    878            is considered a resource.
    879        rootDesc: object, the entire deserialized discovery document.
    880        schema: object, mapping of schema names to schema descriptions.
    881      """
    882      self._dynamic_attrs = []
    883
    884      self._http = http
    885      self._baseUrl = baseUrl
    886      self._model = model
    887      self._developerKey = developerKey
    888      self._requestBuilder = requestBuilder
    889      self._resourceDesc = resourceDesc
    890      self._rootDesc = rootDesc
    891      self._schema = schema
    892
    893      self._set_service_methods()
    894
    895 -  def _set_dynamic_attr(self, attr_name, value):
    896      """Sets an instance attribute and tracks it in a list of dynamic attributes.
    897
    898      Args:
    899        attr_name: string; The name of the attribute to be set
    900        value: The value being set on the object and tracked in the dynamic cache.
    901      """
    902      self._dynamic_attrs.append(attr_name)
    903      self.__dict__[attr_name] = value
    904
    905 -  def __getstate__(self):
    906      """Trim the state down to something that can be pickled.
    907
    908      Uses the fact that the instance variable _dynamic_attrs holds attrs that
    909      will be wiped and restored on pickle serialization.
    910      """
    911      state_dict = copy.copy(self.__dict__)
    912      for dynamic_attr in self._dynamic_attrs:
    913        del state_dict[dynamic_attr]
    914      del state_dict['_dynamic_attrs']
    915      return state_dict
    916
    917 -  def __setstate__(self, state):
    918      """Reconstitute the state of the object from being pickled.
    919
    920      Uses the fact that the instance variable _dynamic_attrs holds attrs that
    921      will be wiped and restored on pickle serialization.
    922      """
    923      self.__dict__.update(state)
    924      self._dynamic_attrs = []
    925      self._set_service_methods()
    926
    927 -  def _set_service_methods(self):
    928      self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
    929      self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
    930      self._add_next_methods(self._resourceDesc, self._schema)
    931
    932 -  def _add_basic_methods(self, resourceDesc, rootDesc, schema):
    933      # Add basic methods to Resource
    934      if 'methods' in resourceDesc:
    935        for methodName, methodDesc in resourceDesc['methods'].iteritems():
    936          fixedMethodName, method = createMethod(
    937              methodName, methodDesc, rootDesc, schema)
    938          self._set_dynamic_attr(fixedMethodName,
    939                                 method.__get__(self, self.__class__))
    940          # Add in _media methods. The functionality of the attached method will
    941          # change when it sees that the method name ends in _media.
    942          if methodDesc.get('supportsMediaDownload', False):
    943            fixedMethodName, method = createMethod(
    944                methodName + '_media', methodDesc, rootDesc, schema)
    945            self._set_dynamic_attr(fixedMethodName,
    946                                   method.__get__(self, self.__class__))
    947
    948 -  def _add_nested_resources(self, resourceDesc, rootDesc, schema):
    949      # Add in nested resources
    950      if 'resources' in resourceDesc:
    951
    952        def createResourceMethod(methodName, methodDesc):
    953          """Create a method on the Resource to access a nested Resource.
    954
    955          Args:
    956            methodName: string, name of the method to use.
    957            methodDesc: object, fragment of deserialized discovery document that
    958              describes the method.
    959          """
    960          methodName = fix_method_name(methodName)
    961
    962          def methodResource(self):
    963            return Resource(http=self._http, baseUrl=self._baseUrl,
    964                            model=self._model, developerKey=self._developerKey,
    965                            requestBuilder=self._requestBuilder,
    966                            resourceDesc=methodDesc, rootDesc=rootDesc,
    967                            schema=schema)
    968
    969          setattr(methodResource, '__doc__', 'A collection resource.')
    970          setattr(methodResource, '__is_resource__', True)
    971
    972          return (methodName, methodResource)
    973
    974        for methodName, methodDesc in resourceDesc['resources'].iteritems():
    975          fixedMethodName, method = createResourceMethod(methodName, methodDesc)
    976          self._set_dynamic_attr(fixedMethodName,
    977                                 method.__get__(self, self.__class__))
    978
    979 -  def _add_next_methods(self, resourceDesc, schema):
    980      # Add _next() methods
    981      # Look for response bodies in schema that contain nextPageToken, and methods
    982      # that take a pageToken parameter.
    983      if 'methods' in resourceDesc:
    984        for methodName, methodDesc in resourceDesc['methods'].iteritems():
    985          if 'response' in methodDesc:
    986            responseSchema = methodDesc['response']
    987            if '$ref' in responseSchema:
    988              responseSchema = schema.get(responseSchema['$ref'])
    989            hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
    990                                                                     {})
    991            hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
    992            if hasNextPageToken and hasPageToken:
    993              fixedMethodName, method = createNextMethod(methodName + '_next')
    994              self._set_dynamic_attr(fixedMethodName,
    995                                     method.__get__(self, self.__class__))
    996