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