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