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