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

# Source Code for Module apiclient.discovery

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