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