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