Package googleapiclient :: Module discovery |
[hide private]
[frames] | no frames]

# Source Code for Module googleapiclient.discovery

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