blob: 6363809f1e5bc5de0bd293b63699f37f33542a5e [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
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"""
INADA Naoki0bceb332014-08-20 15:27:52 +090019from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090020import six
21from six.moves import zip
John Asmuth864311d2014-04-24 15:46:08 -040022
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070023__author__ = "jcgregorio@google.com (Joe Gregorio)"
24__all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
John Asmuth864311d2014-04-24 15:46:08 -040025
Phil Ruffwind26178fc2015-10-13 19:00:33 -040026from six import BytesIO
Takashi Matsuo3772f9d2015-09-04 12:25:55 -070027from six.moves import http_client
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070028from six.moves.urllib.parse import urlencode, urlparse, urljoin, urlunparse, parse_qsl
John Asmuth864311d2014-04-24 15:46:08 -040029
30# Standard library imports
31import copy
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -070032from collections import OrderedDict
Bu Sun Kim790e7022020-09-11 20:18:06 -060033
Phil Ruffwind26178fc2015-10-13 19:00:33 -040034try:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070035 from email.generator import BytesGenerator
Phil Ruffwind26178fc2015-10-13 19:00:33 -040036except ImportError:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070037 from email.generator import Generator as BytesGenerator
John Asmuth864311d2014-04-24 15:46:08 -040038from email.mime.multipart import MIMEMultipart
39from email.mime.nonmultipart import MIMENonMultipart
Craig Citro6ae34d72014-08-18 23:10:09 -070040import json
John Asmuth864311d2014-04-24 15:46:08 -040041import keyword
42import logging
43import mimetypes
44import os
45import re
John Asmuth864311d2014-04-24 15:46:08 -040046
47# Third-party imports
48import httplib2
John Asmuth864311d2014-04-24 15:46:08 -040049import uritemplate
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -070050import google.api_core.client_options
arithmetic1728981eadf2020-06-02 10:20:10 -070051from google.auth.transport import mtls
52from google.auth.exceptions import MutualTLSChannelError
53
54try:
55 import google_auth_httplib2
56except ImportError: # pragma: NO COVER
57 google_auth_httplib2 = None
John Asmuth864311d2014-04-24 15:46:08 -040058
59# Local imports
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -080060from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080061from googleapiclient import mimeparse
John Asmuth864311d2014-04-24 15:46:08 -040062from googleapiclient.errors import HttpError
63from googleapiclient.errors import InvalidJsonError
64from googleapiclient.errors import MediaUploadSizeError
65from googleapiclient.errors import UnacceptableMimeTypeError
66from googleapiclient.errors import UnknownApiNameOrVersion
67from googleapiclient.errors import UnknownFileType
Igor Maravić22435292017-01-19 22:28:22 +010068from googleapiclient.http import build_http
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -040069from googleapiclient.http import BatchHttpRequest
Kostyantyn Leschenkobe8b1cb2016-10-17 12:57:21 +030070from googleapiclient.http import HttpMock
71from googleapiclient.http import HttpMockSequence
John Asmuth864311d2014-04-24 15:46:08 -040072from googleapiclient.http import HttpRequest
73from googleapiclient.http import MediaFileUpload
74from googleapiclient.http import MediaUpload
75from googleapiclient.model import JsonModel
76from googleapiclient.model import MediaModel
77from googleapiclient.model import RawModel
78from googleapiclient.schema import Schemas
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070079
Helen Koikede13e3b2018-04-26 16:05:16 -030080from googleapiclient._helpers import _add_query_parameter
81from googleapiclient._helpers import positional
John Asmuth864311d2014-04-24 15:46:08 -040082
83
84# The client library requires a version of httplib2 that supports RETRIES.
85httplib2.RETRIES = 1
86
87logger = logging.getLogger(__name__)
88
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070089URITEMPLATE = re.compile("{[^}]*}")
90VARNAME = re.compile("[a-zA-Z0-9_-]+")
91DISCOVERY_URI = (
92 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
93)
Ethan Bao12b7cd32016-03-14 14:25:10 -070094V1_DISCOVERY_URI = DISCOVERY_URI
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070095V2_DISCOVERY_URI = (
96 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
97)
98DEFAULT_METHOD_DOC = "A description of how to use this function"
99HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
Igor Maravić22435292017-01-19 22:28:22 +0100100
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700101_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
102BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
John Asmuth864311d2014-04-24 15:46:08 -0400103MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700104 "description": (
105 "The filename of the media request body, or an instance "
106 "of a MediaUpload object."
107 ),
108 "type": "string",
109 "required": False,
John Asmuth864311d2014-04-24 15:46:08 -0400110}
Brian J. Watson38051ac2016-10-25 07:53:08 -0700111MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700112 "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,
Brian J. Watson38051ac2016-10-25 07:53:08 -0700118}
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700119_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
John Asmuth864311d2014-04-24 15:46:08 -0400120
arithmetic17282fc5ca12020-08-27 14:08:12 -0700121# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114.
122GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
123GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
124
John Asmuth864311d2014-04-24 15:46:08 -0400125# Parameters accepted by the stack, but not visible via discovery.
126# TODO(dhermes): Remove 'userip' in 'v2'.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700127STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
128STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
John Asmuth864311d2014-04-24 15:46:08 -0400129
130# Library-specific reserved words beyond Python keywords.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700131RESERVED_WORDS = frozenset(["body"])
John Asmuth864311d2014-04-24 15:46:08 -0400132
Phil Ruffwind26178fc2015-10-13 19:00:33 -0400133# patch _write_lines to avoid munging '\r' into '\n'
134# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
135class _BytesGenerator(BytesGenerator):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700136 _write_lines = BytesGenerator.write
137
John Asmuth864311d2014-04-24 15:46:08 -0400138
139def fix_method_name(name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700140 """Fix method names to avoid '$' characters and reserved word conflicts.
John Asmuth864311d2014-04-24 15:46:08 -0400141
142 Args:
143 name: string, method name.
144
145 Returns:
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700146 The name with '_' appended if the name is a reserved word and '$' and '-'
arithmetic1728981eadf2020-06-02 10:20:10 -0700147 replaced with '_'.
John Asmuth864311d2014-04-24 15:46:08 -0400148 """
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700149 name = name.replace("$", "_").replace("-", "_")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700150 if keyword.iskeyword(name) or name in RESERVED_WORDS:
151 return name + "_"
152 else:
153 return name
John Asmuth864311d2014-04-24 15:46:08 -0400154
155
156def key2param(key):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700157 """Converts key names into parameter names.
John Asmuth864311d2014-04-24 15:46:08 -0400158
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700167 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("_")
John Asmuth864311d2014-04-24 15:46:08 -0400176
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700177 return "".join(result)
John Asmuth864311d2014-04-24 15:46:08 -0400178
179
180@positional(2)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700181def 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,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700192 client_options=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700193 adc_cert_path=None,
194 adc_key_path=None,
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700195 num_retries=1,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700196):
197 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400198
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.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800216 credentials: oauth2client.Credentials or
217 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700218 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700219 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.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300222 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700223 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
arithmetic1728981eadf2020-06-02 10:20:10 -0700233 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
arithmetic17282fc5ca12020-08-27 14:08:12 -0700235 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
arithmetic1728981eadf2020-06-02 10:20:10 -0700240 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.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700243 `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
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700247 num_retries: Integer, number of times to retry discovery with
248 randomized exponential backoff in case of intermittent/connection issues.
John Asmuth864311d2014-04-24 15:46:08 -0400249
250 Returns:
251 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700252
253 Raises:
254 google.auth.exceptions.MutualTLSChannelError: if there are any problems
255 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400256 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700257 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400258
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700259 if http is None:
260 discovery_http = build_http()
261 else:
262 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400263
Bu Sun Kim98888da2020-09-23 11:10:39 -0600264 service = None
265
Bu Sun Kim790e7022020-09-11 20:18:06 -0600266 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700267 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400268
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700269 try:
270 content = _retrieve_discovery_doc(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600271 requested_url,
272 discovery_http,
273 cache_discovery,
274 cache,
275 developerKey,
276 num_retries=num_retries,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700277 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600278 service = build_from_document(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700279 content,
280 base=discovery_url,
281 http=http,
282 developerKey=developerKey,
283 model=model,
284 requestBuilder=requestBuilder,
285 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700286 client_options=client_options,
287 adc_cert_path=adc_cert_path,
288 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700289 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600290 break # exit if a service was created
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700291 except HttpError as e:
292 if e.resp.status == http_client.NOT_FOUND:
293 continue
294 else:
295 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700296
Bu Sun Kim98888da2020-09-23 11:10:39 -0600297 # If discovery_http was created by this function, we are done with it
298 # and can safely close it
299 if http is None:
300 discovery_http.close()
301
302 if service is None:
303 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
304 else:
305 return service
Takashi Matsuo30125122015-08-19 11:42:32 -0700306
307
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700308def _discovery_service_uri_options(discoveryServiceUrl, version):
309 """
310 Returns Discovery URIs to be used for attemnting to build the API Resource.
311
312 Args:
313 discoveryServiceUrl:
314 string, the Original Discovery Service URL preferred by the customer.
315 version:
316 string, API Version requested
317
318 Returns:
319 A list of URIs to be tried for the Service Discovery, in order.
320 """
321
322 urls = [discoveryServiceUrl, V2_DISCOVERY_URI]
323 # V1 Discovery won't work if the requested version is None
324 if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
325 logger.warning(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600326 "Discovery V1 does not support empty versions. Defaulting to V2..."
327 )
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700328 urls.pop(0)
329 return list(OrderedDict.fromkeys(urls))
330
331
Bu Sun Kim790e7022020-09-11 20:18:06 -0600332def _retrieve_discovery_doc(
333 url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
334):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700335 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700336
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.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700344 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.
Takashi Matsuo30125122015-08-19 11:42:32 -0700348
349 Returns:
350 A unicode string representation of the discovery document.
351 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700352 if cache_discovery:
353 from . import discovery_cache
Takashi Matsuo30125122015-08-19 11:42:32 -0700354
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700355 if cache is None:
356 cache = discovery_cache.autodetect()
357 if cache:
358 content = cache.get(url)
359 if content:
360 return content
John Asmuth864311d2014-04-24 15:46:08 -0400361
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700362 actual_url = url
363 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
364 # variable that contains the network address of the client sending the
365 # request. If it exists then add that to the request for the discovery
366 # document to avoid exceeding the quota on discovery requests.
367 if "REMOTE_ADDR" in os.environ:
368 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
369 if developerKey:
370 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700371 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400372
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700373 # Execute this request with retries build into HttpRequest
374 # Note that it will already raise an error if we don't get a 2xx response
375 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
376 resp, content = req.execute(num_retries=num_retries)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800377
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700378 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
John Asmuth864311d2014-04-24 15:46:08 -0400391
392
393@positional(1)
394def build_from_document(
395 service,
396 base=None,
397 future=None,
398 http=None,
399 developerKey=None,
400 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700401 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700402 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700403 client_options=None,
404 adc_cert_path=None,
405 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700406):
407 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400408
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.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800427 credentials: oauth2client.Credentials or
428 google.auth.credentials.Credentials, credentials to be used for
429 authentication.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300430 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700431 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
arithmetic1728981eadf2020-06-02 10:20:10 -0700441 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
arithmetic17282fc5ca12020-08-27 14:08:12 -0700443 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
arithmetic1728981eadf2020-06-02 10:20:10 -0700448 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.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700451 `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
John Asmuth864311d2014-04-24 15:46:08 -0400455
456 Returns:
457 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700458
459 Raises:
460 google.auth.exceptions.MutualTLSChannelError: if there are any problems
461 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400462 """
463
Bu Sun Kim790e7022020-09-11 20:18:06 -0600464 if client_options is None:
465 client_options = google.api_core.client_options.ClientOptions()
466 if isinstance(client_options, six.moves.collections_abc.Mapping):
467 client_options = google.api_core.client_options.from_dict(client_options)
468
469 if http is not None:
470 # if http is passed, the user cannot provide credentials
471 banned_options = [
472 (credentials, "credentials"),
473 (client_options.credentials_file, "client_options.credentials_file"),
474 ]
475 for option, name in banned_options:
476 if option is not None:
477 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
John Asmuth864311d2014-04-24 15:46:08 -0400478
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700479 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"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400483
arithmetic1728981eadf2020-06-02 10:20:10 -0700484 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700485 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()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400492
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700493 # If an API Endpoint is provided on client options, use that as the base URL
arithmetic1728981eadf2020-06-02 10:20:10 -0700494 base = urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim790e7022020-09-11 20:18:06 -0600495 if client_options.api_endpoint:
496 base = client_options.api_endpoint
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700497
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700498 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400499
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700500 # If the http client is not specified, then we must construct an http client
501 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800502 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700503 if http is None:
504 # Does the service require scopes?
505 scopes = list(
506 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
507 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700508
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700509 # If so, then the we need to setup authentication if no developerKey is
510 # specified.
511 if scopes and not developerKey:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600512 # Make sure the user didn't pass multiple credentials
513 if client_options.credentials_file and credentials:
514 raise google.api_core.exceptions.DuplicateCredentialArgs(
515 "client_options.credentials_file and credentials are mutually exclusive."
516 )
517 # Check for credentials file via client options
518 if client_options.credentials_file:
519 credentials = _auth.credentials_from_file(
520 client_options.credentials_file,
521 scopes=client_options.scopes,
522 quota_project_id=client_options.quota_project_id,
523 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700524 # If the user didn't pass in credentials, attempt to acquire application
525 # default credentials.
526 if credentials is None:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600527 credentials = _auth.default_credentials(
528 scopes=client_options.scopes,
529 quota_project_id=client_options.quota_project_id,
530 )
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800531
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700532 # The credentials need to be scoped.
Bu Sun Kim790e7022020-09-11 20:18:06 -0600533 # If the user provided scopes via client_options don't override them
534 if not client_options.scopes:
535 credentials = _auth.with_scopes(credentials, scopes)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700536
537 # If credentials are provided, create an authorized http instance;
538 # otherwise, skip authentication.
539 if credentials:
540 http = _auth.authorized_http(credentials)
541
542 # If the service doesn't require scopes then there is no need for
543 # authentication.
544 else:
545 http = build_http()
546
arithmetic1728981eadf2020-06-02 10:20:10 -0700547 # Obtain client cert and create mTLS http channel if cert exists.
548 client_cert_to_use = None
arithmetic17282fc5ca12020-08-27 14:08:12 -0700549 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 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700554 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 )
arithmetic17282fc5ca12020-08-27 14:08:12 -0700558 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
Bu Sun Kim790e7022020-09-11 20:18:06 -0600565 elif (
566 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
567 ):
arithmetic17282fc5ca12020-08-27 14:08:12 -0700568 client_cert_to_use = mtls.default_client_encrypted_cert_source(
569 adc_cert_path, adc_key_path
570 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700571 if client_cert_to_use:
572 cert_path, key_path, passphrase = client_cert_to_use()
573
574 # The http object we built could be google_auth_httplib2.AuthorizedHttp
575 # or httplib2.Http. In the first case we need to extract the wrapped
576 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
577 http_channel = (
578 http.http
579 if google_auth_httplib2
580 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
581 else http
582 )
583 http_channel.add_certificate(key_path, cert_path, "", passphrase)
584
585 # If user doesn't provide api endpoint via client options, decide which
586 # api endpoint to use.
587 if "mtlsRootUrl" in service and (
588 not client_options or not client_options.api_endpoint
589 ):
590 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
arithmetic17282fc5ca12020-08-27 14:08:12 -0700591 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
arithmetic1728981eadf2020-06-02 10:20:10 -0700592
arithmetic17282fc5ca12020-08-27 14:08:12 -0700593 if not use_mtls_endpoint in ("never", "auto", "always"):
arithmetic1728981eadf2020-06-02 10:20:10 -0700594 raise MutualTLSChannelError(
arithmetic17282fc5ca12020-08-27 14:08:12 -0700595 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
arithmetic1728981eadf2020-06-02 10:20:10 -0700596 )
597
arithmetic172819908ed2020-06-09 22:32:43 -0700598 # Switch to mTLS endpoint, if environment variable is "always", or
599 # environment varibable is "auto" and client cert exists.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700600 if use_mtls_endpoint == "always" or (
601 use_mtls_endpoint == "auto" and client_cert_to_use
arithmetic1728981eadf2020-06-02 10:20:10 -0700602 ):
603 base = mtls_endpoint
604
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700605 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 )
John Asmuth864311d2014-04-24 15:46:08 -0400619
620
621def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700622 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400623
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700634 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()
John Asmuth864311d2014-04-24 15:46:08 -0400645 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700646 if type(value) == type("") or type(value) == type(u""):
647 return value
648 else:
649 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400650
651
652def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700653 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400654
655 Args:
656 maxSize: string, size as a string, such as 2MB or 7GB.
657
658 Returns:
659 The size as an integer value.
660 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700661 if len(maxSize) < 2:
662 return 0
663 units = maxSize[-2:].upper()
664 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
665 if bit_shift is not None:
666 return int(maxSize[:-2]) << bit_shift
667 else:
668 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400669
670
671def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700672 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400673
674 Constructed using the API root URI and service path from the discovery
675 document and the relative path for the API method.
676
677 Args:
678 root_desc: Dictionary; the entire original deserialized discovery document.
679 path_url: String; the relative URL for the API method. Relative to the API
680 root, which is specified in the discovery document.
681
682 Returns:
683 String; the absolute URI for media upload for the API method.
684 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700685 return "%(root)supload/%(service_path)s%(path)s" % {
686 "root": root_desc["rootUrl"],
687 "service_path": root_desc["servicePath"],
688 "path": path_url,
689 }
John Asmuth864311d2014-04-24 15:46:08 -0400690
691
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900692def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700693 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400694
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.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900710 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400711
712 Returns:
713 The updated Dictionary stored in the 'parameters' key of the method
714 description dictionary.
715 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700716 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400717
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700718 # Add in the parameters common to all methods.
719 for name, description in six.iteritems(root_desc.get("parameters", {})):
720 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400721
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700722 # Add in undocumented query parameters.
723 for name in STACK_QUERY_PARAMETERS:
724 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400725
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700726 # Add 'body' (our own reserved word) to parameters if the method supports
727 # a request payload.
728 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
729 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
730 body.update(method_desc["request"])
731 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400732
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700733 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400734
735
736def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700737 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400738
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700739 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
740 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400741
742 Args:
743 method_desc: Dictionary with metadata describing an API method. Value comes
744 from the dictionary of methods stored in the 'methods' key in the
745 deserialized discovery document.
746 root_desc: Dictionary; the entire original deserialized discovery document.
747 path_url: String; the relative URL for the API method. Relative to the API
748 root, which is specified in the discovery document.
749 parameters: A dictionary describing method parameters for method described
750 in method_desc.
751
752 Returns:
753 Triple (accept, max_size, media_path_url) where:
754 - accept is a list of strings representing what content types are
755 accepted for media upload. Defaults to empty list if not in the
756 discovery document.
757 - max_size is a long representing the max size in bytes allowed for a
758 media upload. Defaults to 0L if not in the discovery document.
759 - media_path_url is a String; the absolute URI for media upload for the
760 API method. Constructed using the API root URI and service path from
761 the discovery document and the relative path for the API method. If
762 media upload is not supported, this is None.
763 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700764 media_upload = method_desc.get("mediaUpload", {})
765 accept = media_upload.get("accept", [])
766 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
767 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400768
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700769 if media_upload:
770 media_path_url = _media_path_url_from_info(root_desc, path_url)
771 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
772 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400773
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700774 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400775
776
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900777def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700778 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400779
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.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900788 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400789
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700809 path_url = method_desc["path"]
810 http_method = method_desc["httpMethod"]
811 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400812
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700813 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
814 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
815 # 'parameters' key and needs to know if there is a 'body' parameter because it
816 # also sets a 'media_body' parameter.
817 accept, max_size, media_path_url = _fix_up_media_upload(
818 method_desc, root_desc, path_url, parameters
819 )
John Asmuth864311d2014-04-24 15:46:08 -0400820
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700821 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400822
823
Craig Citro7ee535d2015-02-23 10:11:14 -0800824def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700825 """Custom urljoin replacement supporting : before / in url."""
826 # In general, it's unsafe to simply join base and url. However, for
827 # the case of discovery documents, we know:
828 # * base will never contain params, query, or fragment
829 # * url will never contain a scheme or net_loc.
830 # In general, this means we can safely join on /; we just need to
831 # ensure we end up with precisely one / joining base and url. The
832 # exception here is the case of media uploads, where url will be an
833 # absolute url.
834 if url.startswith("http://") or url.startswith("https://"):
835 return urljoin(base, url)
836 new_base = base if base.endswith("/") else base + "/"
837 new_url = url[1:] if url.startswith("/") else url
838 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800839
840
John Asmuth864311d2014-04-24 15:46:08 -0400841# TODO(dhermes): Convert this class to ResourceMethod and make it callable
842class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700843 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400844
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700867 def __init__(self, method_desc):
868 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400869
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700877 self.argmap = {}
878 self.required_params = []
879 self.repeated_params = []
880 self.pattern_params = {}
881 self.query_params = []
882 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
883 # parsing is gotten rid of.
884 self.path_params = set()
885 self.param_types = {}
886 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400887
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700888 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400889
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700890 def set_parameters(self, method_desc):
891 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400892
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700901 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
902 param = key2param(arg)
903 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400904
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700905 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")
John Asmuth864311d2014-04-24 15:46:08 -0400918
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700919 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
920 # should have all path parameters already marked with
921 # 'location: path'.
922 for match in URITEMPLATE.finditer(method_desc["path"]):
923 for namematch in VARNAME.finditer(match.group(0)):
924 name = key2param(namematch.group(0))
925 self.path_params.add(name)
926 if name in self.query_params:
927 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400928
929
930def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700931 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400932
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700940 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)
John Asmuth864311d2014-04-24 15:46:08 -0400949
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700950 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400951
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700952 def method(self, **kwargs):
953 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400954
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700955 for name in six.iterkeys(kwargs):
956 if name not in parameters.argmap:
957 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400958
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700959 # Remove args that have a value of None.
960 keys = list(kwargs.keys())
961 for name in keys:
962 if kwargs[name] is None:
963 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400964
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700965 for name in parameters.required_params:
966 if name not in kwargs:
967 # temporary workaround for non-paging methods incorrectly requiring
968 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
969 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
970 _methodProperties(methodDesc, schema, "response")
971 ):
972 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400973
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700974 for name, regex in six.iteritems(parameters.pattern_params):
975 if name in kwargs:
976 if isinstance(kwargs[name], six.string_types):
977 pvalues = [kwargs[name]]
978 else:
979 pvalues = kwargs[name]
980 for pvalue in pvalues:
981 if re.match(regex, pvalue) is None:
982 raise TypeError(
983 'Parameter "%s" value "%s" does not match the pattern "%s"'
984 % (name, pvalue, regex)
985 )
986
987 for name, enums in six.iteritems(parameters.enum_params):
988 if name in kwargs:
989 # We need to handle the case of a repeated enum
990 # name differently, since we want to handle both
991 # arg='value' and arg=['value1', 'value2']
992 if name in parameters.repeated_params and not isinstance(
993 kwargs[name], six.string_types
994 ):
995 values = kwargs[name]
996 else:
997 values = [kwargs[name]]
998 for value in values:
999 if value not in enums:
1000 raise TypeError(
1001 'Parameter "%s" value "%s" is not an allowed value in "%s"'
1002 % (name, value, str(enums))
1003 )
1004
1005 actual_query_params = {}
1006 actual_path_params = {}
1007 for key, value in six.iteritems(kwargs):
1008 to_type = parameters.param_types.get(key, "string")
1009 # For repeated parameters we cast each member of the list.
1010 if key in parameters.repeated_params and type(value) == type([]):
1011 cast_value = [_cast(x, to_type) for x in value]
1012 else:
1013 cast_value = _cast(value, to_type)
1014 if key in parameters.query_params:
1015 actual_query_params[parameters.argmap[key]] = cast_value
1016 if key in parameters.path_params:
1017 actual_path_params[parameters.argmap[key]] = cast_value
1018 body_value = kwargs.get("body", None)
1019 media_filename = kwargs.get("media_body", None)
1020 media_mime_type = kwargs.get("media_mime_type", None)
1021
1022 if self._developerKey:
1023 actual_query_params["key"] = self._developerKey
1024
1025 model = self._model
1026 if methodName.endswith("_media"):
1027 model = MediaModel()
1028 elif "response" not in methodDesc:
1029 model = RawModel()
1030
1031 headers = {}
1032 headers, params, query, body = model.request(
1033 headers, actual_path_params, actual_query_params, body_value
1034 )
1035
1036 expanded_url = uritemplate.expand(pathUrl, params)
1037 url = _urljoin(self._baseUrl, expanded_url + query)
1038
1039 resumable = None
1040 multipart_boundary = ""
1041
1042 if media_filename:
1043 # Ensure we end up with a valid MediaUpload object.
1044 if isinstance(media_filename, six.string_types):
1045 if media_mime_type is None:
1046 logger.warning(
1047 "media_mime_type argument not specified: trying to auto-detect for %s",
1048 media_filename,
1049 )
1050 media_mime_type, _ = mimetypes.guess_type(media_filename)
1051 if media_mime_type is None:
1052 raise UnknownFileType(media_filename)
1053 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
1054 raise UnacceptableMimeTypeError(media_mime_type)
1055 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
1056 elif isinstance(media_filename, MediaUpload):
1057 media_upload = media_filename
1058 else:
1059 raise TypeError("media_filename must be str or MediaUpload.")
1060
1061 # Check the maxSize
1062 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
1063 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
1064
1065 # Use the media path uri for media uploads
1066 expanded_url = uritemplate.expand(mediaPathUrl, params)
1067 url = _urljoin(self._baseUrl, expanded_url + query)
1068 if media_upload.resumable():
1069 url = _add_query_parameter(url, "uploadType", "resumable")
1070
1071 if media_upload.resumable():
1072 # This is all we need to do for resumable, if the body exists it gets
1073 # sent in the first request, otherwise an empty body is sent.
1074 resumable = media_upload
1075 else:
1076 # A non-resumable upload
1077 if body is None:
1078 # This is a simple media upload
1079 headers["content-type"] = media_upload.mimetype()
1080 body = media_upload.getbytes(0, media_upload.size())
1081 url = _add_query_parameter(url, "uploadType", "media")
1082 else:
1083 # This is a multipart/related upload.
1084 msgRoot = MIMEMultipart("related")
1085 # msgRoot should not write out it's own headers
1086 setattr(msgRoot, "_write_headers", lambda self: None)
1087
1088 # attach the body as one part
1089 msg = MIMENonMultipart(*headers["content-type"].split("/"))
1090 msg.set_payload(body)
1091 msgRoot.attach(msg)
1092
1093 # attach the media as the second part
1094 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
1095 msg["Content-Transfer-Encoding"] = "binary"
1096
1097 payload = media_upload.getbytes(0, media_upload.size())
1098 msg.set_payload(payload)
1099 msgRoot.attach(msg)
1100 # encode the body: note that we can't use `as_string`, because
1101 # it plays games with `From ` lines.
1102 fp = BytesIO()
1103 g = _BytesGenerator(fp, mangle_from_=False)
1104 g.flatten(msgRoot, unixfrom=False)
1105 body = fp.getvalue()
1106
1107 multipart_boundary = msgRoot.get_boundary()
1108 headers["content-type"] = (
1109 "multipart/related; " 'boundary="%s"'
1110 ) % multipart_boundary
1111 url = _add_query_parameter(url, "uploadType", "multipart")
1112
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001113 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001114 return self._requestBuilder(
1115 self._http,
1116 model.response,
1117 url,
1118 method=httpMethod,
1119 body=body,
1120 headers=headers,
1121 methodId=methodId,
1122 resumable=resumable,
1123 )
1124
1125 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1126 if len(parameters.argmap) > 0:
1127 docs.append("Args:\n")
1128
1129 # Skip undocumented params and params common to all methods.
1130 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1131 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1132
1133 all_args = list(parameters.argmap.keys())
1134 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1135
1136 # Move body to the front of the line.
1137 if "body" in all_args:
1138 args_ordered.append("body")
1139
1140 for name in all_args:
1141 if name not in args_ordered:
1142 args_ordered.append(name)
1143
1144 for arg in args_ordered:
1145 if arg in skip_parameters:
1146 continue
1147
1148 repeated = ""
1149 if arg in parameters.repeated_params:
1150 repeated = " (repeated)"
1151 required = ""
1152 if arg in parameters.required_params:
1153 required = " (required)"
1154 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1155 paramdoc = paramdesc.get("description", "A parameter")
1156 if "$ref" in paramdesc:
1157 docs.append(
1158 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
1159 % (
1160 arg,
1161 paramdoc,
1162 required,
1163 repeated,
1164 schema.prettyPrintByName(paramdesc["$ref"]),
1165 )
1166 )
John Asmuth864311d2014-04-24 15:46:08 -04001167 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001168 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 ")
John Asmuth864311d2014-04-24 15:46:08 -04001181 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001182 docs.append("\nReturns:\n An object of the form:\n\n ")
1183 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001184
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001185 setattr(method, "__doc__", "".join(docs))
1186 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001187
1188
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001189def createNextMethod(
1190 methodName,
1191 pageTokenName="pageToken",
1192 nextPageTokenName="nextPageToken",
1193 isPageTokenParameter=True,
1194):
1195 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001196
1197 The _next methods allow for easy iteration through list() responses.
1198
1199 Args:
1200 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001201 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.
John Asmuth864311d2014-04-24 15:46:08 -04001205 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001206 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001207
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001208 def methodNext(self, previous_request, previous_response):
1209 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001210
1211Args:
1212 previous_request: The request for the previous page. (required)
1213 previous_response: The response from the request for the previous page. (required)
1214
1215Returns:
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001219 # Retrieve nextPageToken from previous_response
1220 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001221
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001222 nextPageToken = previous_response.get(nextPageTokenName, None)
1223 if not nextPageToken:
1224 return None
John Asmuth864311d2014-04-24 15:46:08 -04001225
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001226 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001227
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001228 if isPageTokenParameter:
1229 # Replace pageToken value in URI
1230 request.uri = _add_query_parameter(
1231 request.uri, pageTokenName, nextPageToken
1232 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001233 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001234 else:
1235 # Replace pageToken value in request body
1236 model = self._model
1237 body = model.deserialize(request.body)
1238 body[pageTokenName] = nextPageToken
1239 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001240 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001241
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001242 return request
John Asmuth864311d2014-04-24 15:46:08 -04001243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001244 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001245
1246
1247class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001248 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001249
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001250 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.
John Asmuth864311d2014-04-24 15:46:08 -04001262
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001278 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001279
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001280 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
John Asmuth864311d2014-04-24 15:46:08 -04001288
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001289 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001290
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001291 def _set_dynamic_attr(self, attr_name, value):
1292 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001293
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001298 self._dynamic_attrs.append(attr_name)
1299 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001300
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001301 def __getstate__(self):
1302 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001303
1304 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1305 will be wiped and restored on pickle serialization.
1306 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001307 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
John Asmuth864311d2014-04-24 15:46:08 -04001312
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001313 def __setstate__(self, state):
1314 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001315
1316 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1317 will be wiped and restored on pickle serialization.
1318 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001319 self.__dict__.update(state)
1320 self._dynamic_attrs = []
1321 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001322
Bu Sun Kim98888da2020-09-23 11:10:39 -06001323
1324 def __enter__(self):
1325 return self
1326
1327 def __exit__(self, exc_type, exc, exc_tb):
1328 self.close()
1329
1330 def close(self):
1331 """Close httplib2 connections."""
1332 # httplib2 leaves sockets open by default.
1333 # Cleanup using the `close` method.
1334 # https://github.com/httplib2/httplib2/issues/148
1335 self._http.http.close()
1336
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001337 def _set_service_methods(self):
1338 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1339 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1340 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001341
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001342 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1343 # If this is the root Resource, add a new_batch_http_request() method.
1344 if resourceDesc == rootDesc:
1345 batch_uri = "%s%s" % (
1346 rootDesc["rootUrl"],
1347 rootDesc.get("batchPath", "batch"),
1348 )
1349
1350 def new_batch_http_request(callback=None):
1351 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001352
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001364 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001365
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001366 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001367
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001368 # Add basic methods to Resource
1369 if "methods" in resourceDesc:
1370 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1371 fixedMethodName, method = createMethod(
1372 methodName, methodDesc, rootDesc, schema
1373 )
1374 self._set_dynamic_attr(
1375 fixedMethodName, method.__get__(self, self.__class__)
1376 )
1377 # Add in _media methods. The functionality of the attached method will
1378 # change when it sees that the method name ends in _media.
1379 if methodDesc.get("supportsMediaDownload", False):
1380 fixedMethodName, method = createMethod(
1381 methodName + "_media", methodDesc, rootDesc, schema
1382 )
1383 self._set_dynamic_attr(
1384 fixedMethodName, method.__get__(self, self.__class__)
1385 )
John Asmuth864311d2014-04-24 15:46:08 -04001386
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001387 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1388 # Add in nested resources
1389 if "resources" in resourceDesc:
1390
1391 def createResourceMethod(methodName, methodDesc):
1392 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001393
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001399 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001400
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001401 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 )
John Asmuth864311d2014-04-24 15:46:08 -04001412
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001413 setattr(methodResource, "__doc__", "A collection resource.")
1414 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001415
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001416 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001417
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001418 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 )
John Asmuth864311d2014-04-24 15:46:08 -04001423
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001424 def _add_next_methods(self, resourceDesc, schema):
1425 # Add _next() methods if and only if one of the names 'pageToken' or
1426 # 'nextPageToken' occurs among the fields of both the method's response
1427 # type either the method's request (query parameters) or request body.
1428 if "methods" not in resourceDesc:
1429 return
1430 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1431 nextPageTokenName = _findPageTokenName(
1432 _methodProperties(methodDesc, schema, "response")
1433 )
1434 if not nextPageTokenName:
1435 continue
1436 isPageTokenParameter = True
1437 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1438 if not pageTokenName:
1439 isPageTokenParameter = False
1440 pageTokenName = _findPageTokenName(
1441 _methodProperties(methodDesc, schema, "request")
1442 )
1443 if not pageTokenName:
1444 continue
1445 fixedMethodName, method = createNextMethod(
1446 methodName + "_next",
1447 pageTokenName,
1448 nextPageTokenName,
1449 isPageTokenParameter,
1450 )
1451 self._set_dynamic_attr(
1452 fixedMethodName, method.__get__(self, self.__class__)
1453 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001454
1455
1456def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001457 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001458
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001466 return next(
1467 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1468 )
1469
Thomas Coffee20af04d2017-02-10 15:24:44 -08001470
1471def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001472 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001473
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001485 desc = methodDesc.get(name, {})
1486 if "$ref" in desc:
1487 desc = schema.get(desc["$ref"], {})
1488 return desc.get("properties", {})