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