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

Source Code for Module googleapiclient.discovery

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