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