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