googleapiclient.discovery

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