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