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