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