blob: 44473dc3cbdec87f86718e8e6b14b3120e63383f [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,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500196 static_discovery=True,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700197):
198 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400199
200 Construct a Resource object for interacting with an API. The serviceName and
201 version are the names from the Discovery service.
202
203 Args:
204 serviceName: string, name of the service.
205 version: string, the version of the service.
206 http: httplib2.Http, An instance of httplib2.Http or something that acts
207 like it that HTTP requests will be made through.
208 discoveryServiceUrl: string, a URI Template that points to the location of
209 the discovery service. It should have two parameters {api} and
210 {apiVersion} that when filled in produce an absolute URI to the discovery
211 document for that service.
212 developerKey: string, key obtained from
213 https://code.google.com/apis/console.
214 model: googleapiclient.Model, converts to and from the wire format.
215 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
216 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800217 credentials: oauth2client.Credentials or
218 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700219 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700220 cache_discovery: Boolean, whether or not to cache the discovery doc.
221 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
222 cache object for the discovery documents.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300223 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700224 options to set user options on the client.
225 (1) The API endpoint should be set through client_options. If API endpoint
226 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
227 to control which endpoint to use.
228 (2) client_cert_source is not supported, client cert should be provided using
229 client_encrypted_cert_source instead. In order to use the provided client
230 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
231 set to `true`.
232 More details on the environment variables are here:
233 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700234 adc_cert_path: str, client certificate file path to save the application
235 default client certificate for mTLS. This field is required if you want to
arithmetic17282fc5ca12020-08-27 14:08:12 -0700236 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
237 environment variable must be set to `true` in order to use this field,
238 otherwise this field doesn't nothing.
239 More details on the environment variables are here:
240 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700241 adc_key_path: str, client encrypted private key file path to save the
242 application default client encrypted private key for mTLS. This field is
243 required if you want to use the default client certificate.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700244 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
245 `true` in order to use this field, otherwise this field doesn't nothing.
246 More details on the environment variables are here:
247 https://google.aip.dev/auth/4114
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700248 num_retries: Integer, number of times to retry discovery with
249 randomized exponential backoff in case of intermittent/connection issues.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500250 static_discovery: Boolean, whether or not to use the static discovery docs
251 included in the library.
John Asmuth864311d2014-04-24 15:46:08 -0400252
253 Returns:
254 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700255
256 Raises:
257 google.auth.exceptions.MutualTLSChannelError: if there are any problems
258 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400259 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700260 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400261
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700262 if http is None:
263 discovery_http = build_http()
264 else:
265 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400266
Bu Sun Kim98888da2020-09-23 11:10:39 -0600267 service = None
268
Bu Sun Kim790e7022020-09-11 20:18:06 -0600269 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700270 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400271
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700272 try:
273 content = _retrieve_discovery_doc(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600274 requested_url,
275 discovery_http,
276 cache_discovery,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500277 serviceName,
278 version,
Bu Sun Kim790e7022020-09-11 20:18:06 -0600279 cache,
280 developerKey,
281 num_retries=num_retries,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500282 static_discovery=static_discovery,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700283 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600284 service = build_from_document(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700285 content,
286 base=discovery_url,
287 http=http,
288 developerKey=developerKey,
289 model=model,
290 requestBuilder=requestBuilder,
291 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700292 client_options=client_options,
293 adc_cert_path=adc_cert_path,
294 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700295 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600296 break # exit if a service was created
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700297 except HttpError as e:
298 if e.resp.status == http_client.NOT_FOUND:
299 continue
300 else:
301 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700302
Bu Sun Kim98888da2020-09-23 11:10:39 -0600303 # If discovery_http was created by this function, we are done with it
304 # and can safely close it
305 if http is None:
306 discovery_http.close()
307
308 if service is None:
309 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
310 else:
311 return service
Takashi Matsuo30125122015-08-19 11:42:32 -0700312
313
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700314def _discovery_service_uri_options(discoveryServiceUrl, version):
315 """
316 Returns Discovery URIs to be used for attemnting to build the API Resource.
317
318 Args:
319 discoveryServiceUrl:
320 string, the Original Discovery Service URL preferred by the customer.
321 version:
322 string, API Version requested
323
324 Returns:
325 A list of URIs to be tried for the Service Discovery, in order.
326 """
327
328 urls = [discoveryServiceUrl, V2_DISCOVERY_URI]
329 # V1 Discovery won't work if the requested version is None
330 if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
331 logger.warning(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600332 "Discovery V1 does not support empty versions. Defaulting to V2..."
333 )
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700334 urls.pop(0)
335 return list(OrderedDict.fromkeys(urls))
336
337
Bu Sun Kim790e7022020-09-11 20:18:06 -0600338def _retrieve_discovery_doc(
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500339 url,
340 http,
341 cache_discovery,
342 serviceName,
343 version,
344 cache=None,
345 developerKey=None,
346 num_retries=1,
347 static_discovery=True
Bu Sun Kim790e7022020-09-11 20:18:06 -0600348):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700349 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700350
351 Args:
352 url: string, the URL of the discovery document.
353 http: httplib2.Http, An instance of httplib2.Http or something that acts
354 like it through which HTTP requests will be made.
355 cache_discovery: Boolean, whether or not to cache the discovery doc.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500356 serviceName: string, name of the service.
357 version: string, the version of the service.
Takashi Matsuo30125122015-08-19 11:42:32 -0700358 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
359 object for the discovery documents.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700360 developerKey: string, Key for controlling API usage, generated
361 from the API Console.
362 num_retries: Integer, number of times to retry discovery with
363 randomized exponential backoff in case of intermittent/connection issues.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500364 static_discovery: Boolean, whether or not to use the static discovery docs
365 included in the library.
Takashi Matsuo30125122015-08-19 11:42:32 -0700366
367 Returns:
368 A unicode string representation of the discovery document.
369 """
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500370 from . import discovery_cache
Takashi Matsuo30125122015-08-19 11:42:32 -0700371
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500372 if cache_discovery:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700373 if cache is None:
374 cache = discovery_cache.autodetect()
375 if cache:
376 content = cache.get(url)
377 if content:
378 return content
John Asmuth864311d2014-04-24 15:46:08 -0400379
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500380 # When `static_discovery=True`, use static discovery artifacts included
381 # with the library
382 if static_discovery:
383 content = discovery_cache.get_static_doc(serviceName, version)
384 if content:
385 return content
386 else:
387 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
388
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700389 actual_url = url
390 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
391 # variable that contains the network address of the client sending the
392 # request. If it exists then add that to the request for the discovery
393 # document to avoid exceeding the quota on discovery requests.
394 if "REMOTE_ADDR" in os.environ:
395 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
396 if developerKey:
397 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700398 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400399
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700400 # Execute this request with retries build into HttpRequest
401 # Note that it will already raise an error if we don't get a 2xx response
402 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
403 resp, content = req.execute(num_retries=num_retries)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800404
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700405 try:
406 content = content.decode("utf-8")
407 except AttributeError:
408 pass
409
410 try:
411 service = json.loads(content)
412 except ValueError as e:
413 logger.error("Failed to parse as JSON: " + content)
414 raise InvalidJsonError()
415 if cache_discovery and cache:
416 cache.set(url, content)
417 return content
John Asmuth864311d2014-04-24 15:46:08 -0400418
419
420@positional(1)
421def build_from_document(
422 service,
423 base=None,
424 future=None,
425 http=None,
426 developerKey=None,
427 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700428 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700429 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700430 client_options=None,
431 adc_cert_path=None,
432 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700433):
434 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400435
436 Same as `build()`, but constructs the Resource object from a discovery
437 document that is it given, as opposed to retrieving one over HTTP.
438
439 Args:
440 service: string or object, the JSON discovery document describing the API.
441 The value passed in may either be the JSON string or the deserialized
442 JSON.
443 base: string, base URI for all HTTP requests, usually the discovery URI.
444 This parameter is no longer used as rootUrl and servicePath are included
445 within the discovery document. (deprecated)
446 future: string, discovery document with future capabilities (deprecated).
447 http: httplib2.Http, An instance of httplib2.Http or something that acts
448 like it that HTTP requests will be made through.
449 developerKey: string, Key for controlling API usage, generated
450 from the API Console.
451 model: Model class instance that serializes and de-serializes requests and
452 responses.
453 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800454 credentials: oauth2client.Credentials or
455 google.auth.credentials.Credentials, credentials to be used for
456 authentication.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300457 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700458 options to set user options on the client.
459 (1) The API endpoint should be set through client_options. If API endpoint
460 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
461 to control which endpoint to use.
462 (2) client_cert_source is not supported, client cert should be provided using
463 client_encrypted_cert_source instead. In order to use the provided client
464 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
465 set to `true`.
466 More details on the environment variables are here:
467 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700468 adc_cert_path: str, client certificate file path to save the application
469 default client certificate for mTLS. This field is required if you want to
arithmetic17282fc5ca12020-08-27 14:08:12 -0700470 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
471 environment variable must be set to `true` in order to use this field,
472 otherwise this field doesn't nothing.
473 More details on the environment variables are here:
474 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700475 adc_key_path: str, client encrypted private key file path to save the
476 application default client encrypted private key for mTLS. This field is
477 required if you want to use the default client certificate.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700478 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
479 `true` in order to use this field, otherwise this field doesn't nothing.
480 More details on the environment variables are here:
481 https://google.aip.dev/auth/4114
John Asmuth864311d2014-04-24 15:46:08 -0400482
483 Returns:
484 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700485
486 Raises:
487 google.auth.exceptions.MutualTLSChannelError: if there are any problems
488 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400489 """
490
Bu Sun Kim790e7022020-09-11 20:18:06 -0600491 if client_options is None:
492 client_options = google.api_core.client_options.ClientOptions()
493 if isinstance(client_options, six.moves.collections_abc.Mapping):
494 client_options = google.api_core.client_options.from_dict(client_options)
495
496 if http is not None:
497 # if http is passed, the user cannot provide credentials
498 banned_options = [
499 (credentials, "credentials"),
500 (client_options.credentials_file, "client_options.credentials_file"),
501 ]
502 for option, name in banned_options:
503 if option is not None:
504 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
John Asmuth864311d2014-04-24 15:46:08 -0400505
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700506 if isinstance(service, six.string_types):
507 service = json.loads(service)
508 elif isinstance(service, six.binary_type):
509 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400510
arithmetic1728981eadf2020-06-02 10:20:10 -0700511 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700512 logger.error(
513 "You are using HttpMock or HttpMockSequence without"
514 + "having the service discovery doc in cache. Try calling "
515 + "build() without mocking once first to populate the "
516 + "cache."
517 )
518 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400519
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700520 # If an API Endpoint is provided on client options, use that as the base URL
arithmetic1728981eadf2020-06-02 10:20:10 -0700521 base = urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim790e7022020-09-11 20:18:06 -0600522 if client_options.api_endpoint:
523 base = client_options.api_endpoint
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700524
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700525 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400526
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700527 # If the http client is not specified, then we must construct an http client
528 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800529 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700530 if http is None:
531 # Does the service require scopes?
532 scopes = list(
533 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
534 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700535
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700536 # If so, then the we need to setup authentication if no developerKey is
537 # specified.
538 if scopes and not developerKey:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600539 # Make sure the user didn't pass multiple credentials
540 if client_options.credentials_file and credentials:
541 raise google.api_core.exceptions.DuplicateCredentialArgs(
542 "client_options.credentials_file and credentials are mutually exclusive."
543 )
544 # Check for credentials file via client options
545 if client_options.credentials_file:
546 credentials = _auth.credentials_from_file(
547 client_options.credentials_file,
548 scopes=client_options.scopes,
549 quota_project_id=client_options.quota_project_id,
550 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700551 # If the user didn't pass in credentials, attempt to acquire application
552 # default credentials.
553 if credentials is None:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600554 credentials = _auth.default_credentials(
555 scopes=client_options.scopes,
556 quota_project_id=client_options.quota_project_id,
557 )
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800558
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700559 # The credentials need to be scoped.
Bu Sun Kim790e7022020-09-11 20:18:06 -0600560 # If the user provided scopes via client_options don't override them
561 if not client_options.scopes:
562 credentials = _auth.with_scopes(credentials, scopes)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700563
564 # If credentials are provided, create an authorized http instance;
565 # otherwise, skip authentication.
566 if credentials:
567 http = _auth.authorized_http(credentials)
568
569 # If the service doesn't require scopes then there is no need for
570 # authentication.
571 else:
572 http = build_http()
573
arithmetic1728981eadf2020-06-02 10:20:10 -0700574 # Obtain client cert and create mTLS http channel if cert exists.
575 client_cert_to_use = None
arithmetic17282fc5ca12020-08-27 14:08:12 -0700576 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
577 if not use_client_cert in ("true", "false"):
578 raise MutualTLSChannelError(
579 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
580 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700581 if client_options and client_options.client_cert_source:
582 raise MutualTLSChannelError(
583 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
584 )
arithmetic17282fc5ca12020-08-27 14:08:12 -0700585 if use_client_cert == "true":
586 if (
587 client_options
588 and hasattr(client_options, "client_encrypted_cert_source")
589 and client_options.client_encrypted_cert_source
590 ):
591 client_cert_to_use = client_options.client_encrypted_cert_source
Bu Sun Kim790e7022020-09-11 20:18:06 -0600592 elif (
593 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
594 ):
arithmetic17282fc5ca12020-08-27 14:08:12 -0700595 client_cert_to_use = mtls.default_client_encrypted_cert_source(
596 adc_cert_path, adc_key_path
597 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700598 if client_cert_to_use:
599 cert_path, key_path, passphrase = client_cert_to_use()
600
601 # The http object we built could be google_auth_httplib2.AuthorizedHttp
602 # or httplib2.Http. In the first case we need to extract the wrapped
603 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
604 http_channel = (
605 http.http
606 if google_auth_httplib2
607 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
608 else http
609 )
610 http_channel.add_certificate(key_path, cert_path, "", passphrase)
611
612 # If user doesn't provide api endpoint via client options, decide which
613 # api endpoint to use.
614 if "mtlsRootUrl" in service and (
615 not client_options or not client_options.api_endpoint
616 ):
617 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
arithmetic17282fc5ca12020-08-27 14:08:12 -0700618 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
arithmetic1728981eadf2020-06-02 10:20:10 -0700619
arithmetic17282fc5ca12020-08-27 14:08:12 -0700620 if not use_mtls_endpoint in ("never", "auto", "always"):
arithmetic1728981eadf2020-06-02 10:20:10 -0700621 raise MutualTLSChannelError(
arithmetic17282fc5ca12020-08-27 14:08:12 -0700622 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
arithmetic1728981eadf2020-06-02 10:20:10 -0700623 )
624
arithmetic172819908ed2020-06-09 22:32:43 -0700625 # Switch to mTLS endpoint, if environment variable is "always", or
626 # environment varibable is "auto" and client cert exists.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700627 if use_mtls_endpoint == "always" or (
628 use_mtls_endpoint == "auto" and client_cert_to_use
arithmetic1728981eadf2020-06-02 10:20:10 -0700629 ):
630 base = mtls_endpoint
631
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700632 if model is None:
633 features = service.get("features", [])
634 model = JsonModel("dataWrapper" in features)
635
636 return Resource(
637 http=http,
638 baseUrl=base,
639 model=model,
640 developerKey=developerKey,
641 requestBuilder=requestBuilder,
642 resourceDesc=service,
643 rootDesc=service,
644 schema=schema,
645 )
John Asmuth864311d2014-04-24 15:46:08 -0400646
647
648def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700649 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400650
651 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
652 JSON Schema.
653
654 Args:
655 value: any, the value to convert
656 schema_type: string, the type that value should be interpreted as
657
658 Returns:
659 A string representation of 'value' based on the schema_type.
660 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700661 if schema_type == "string":
662 if type(value) == type("") or type(value) == type(u""):
663 return value
664 else:
665 return str(value)
666 elif schema_type == "integer":
667 return str(int(value))
668 elif schema_type == "number":
669 return str(float(value))
670 elif schema_type == "boolean":
671 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400672 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700673 if type(value) == type("") or type(value) == type(u""):
674 return value
675 else:
676 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400677
678
679def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700680 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400681
682 Args:
683 maxSize: string, size as a string, such as 2MB or 7GB.
684
685 Returns:
686 The size as an integer value.
687 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700688 if len(maxSize) < 2:
689 return 0
690 units = maxSize[-2:].upper()
691 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
692 if bit_shift is not None:
693 return int(maxSize[:-2]) << bit_shift
694 else:
695 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400696
697
698def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700699 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400700
701 Constructed using the API root URI and service path from the discovery
702 document and the relative path for the API method.
703
704 Args:
705 root_desc: Dictionary; the entire original deserialized discovery document.
706 path_url: String; the relative URL for the API method. Relative to the API
707 root, which is specified in the discovery document.
708
709 Returns:
710 String; the absolute URI for media upload for the API method.
711 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700712 return "%(root)supload/%(service_path)s%(path)s" % {
713 "root": root_desc["rootUrl"],
714 "service_path": root_desc["servicePath"],
715 "path": path_url,
716 }
John Asmuth864311d2014-04-24 15:46:08 -0400717
718
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900719def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700720 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400721
722 Specifically, adds whatever global parameters are specified by the API to the
723 parameters for the individual method. Also adds parameters which don't
724 appear in the discovery document, but are available to all discovery based
725 APIs (these are listed in STACK_QUERY_PARAMETERS).
726
727 SIDE EFFECTS: This updates the parameters dictionary object in the method
728 description.
729
730 Args:
731 method_desc: Dictionary with metadata describing an API method. Value comes
732 from the dictionary of methods stored in the 'methods' key in the
733 deserialized discovery document.
734 root_desc: Dictionary; the entire original deserialized discovery document.
735 http_method: String; the HTTP method used to call the API method described
736 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900737 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400738
739 Returns:
740 The updated Dictionary stored in the 'parameters' key of the method
741 description dictionary.
742 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700743 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400744
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700745 # Add in the parameters common to all methods.
746 for name, description in six.iteritems(root_desc.get("parameters", {})):
747 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400748
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700749 # Add in undocumented query parameters.
750 for name in STACK_QUERY_PARAMETERS:
751 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400752
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700753 # Add 'body' (our own reserved word) to parameters if the method supports
754 # a request payload.
755 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
756 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
757 body.update(method_desc["request"])
758 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400759
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700760 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400761
762
763def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700764 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400765
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700766 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
767 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400768
769 Args:
770 method_desc: Dictionary with metadata describing an API method. Value comes
771 from the dictionary of methods stored in the 'methods' key in the
772 deserialized discovery document.
773 root_desc: Dictionary; the entire original deserialized discovery document.
774 path_url: String; the relative URL for the API method. Relative to the API
775 root, which is specified in the discovery document.
776 parameters: A dictionary describing method parameters for method described
777 in method_desc.
778
779 Returns:
780 Triple (accept, max_size, media_path_url) where:
781 - accept is a list of strings representing what content types are
782 accepted for media upload. Defaults to empty list if not in the
783 discovery document.
784 - max_size is a long representing the max size in bytes allowed for a
785 media upload. Defaults to 0L if not in the discovery document.
786 - media_path_url is a String; the absolute URI for media upload for the
787 API method. Constructed using the API root URI and service path from
788 the discovery document and the relative path for the API method. If
789 media upload is not supported, this is None.
790 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700791 media_upload = method_desc.get("mediaUpload", {})
792 accept = media_upload.get("accept", [])
793 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
794 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400795
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700796 if media_upload:
797 media_path_url = _media_path_url_from_info(root_desc, path_url)
798 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
799 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400800
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700801 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400802
803
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900804def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700805 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400806
807 SIDE EFFECTS: Changes the parameters dictionary in the method description with
808 extra parameters which are used locally.
809
810 Args:
811 method_desc: Dictionary with metadata describing an API method. Value comes
812 from the dictionary of methods stored in the 'methods' key in the
813 deserialized discovery document.
814 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900815 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400816
817 Returns:
818 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
819 where:
820 - path_url is a String; the relative URL for the API method. Relative to
821 the API root, which is specified in the discovery document.
822 - http_method is a String; the HTTP method used to call the API method
823 described in the method description.
824 - method_id is a String; the name of the RPC method associated with the
825 API method, and is in the method description in the 'id' key.
826 - accept is a list of strings representing what content types are
827 accepted for media upload. Defaults to empty list if not in the
828 discovery document.
829 - max_size is a long representing the max size in bytes allowed for a
830 media upload. Defaults to 0L if not in the discovery document.
831 - media_path_url is a String; the absolute URI for media upload for the
832 API method. Constructed using the API root URI and service path from
833 the discovery document and the relative path for the API method. If
834 media upload is not supported, this is None.
835 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700836 path_url = method_desc["path"]
837 http_method = method_desc["httpMethod"]
838 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400839
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700840 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
841 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
842 # 'parameters' key and needs to know if there is a 'body' parameter because it
843 # also sets a 'media_body' parameter.
844 accept, max_size, media_path_url = _fix_up_media_upload(
845 method_desc, root_desc, path_url, parameters
846 )
John Asmuth864311d2014-04-24 15:46:08 -0400847
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700848 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400849
850
Craig Citro7ee535d2015-02-23 10:11:14 -0800851def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700852 """Custom urljoin replacement supporting : before / in url."""
853 # In general, it's unsafe to simply join base and url. However, for
854 # the case of discovery documents, we know:
855 # * base will never contain params, query, or fragment
856 # * url will never contain a scheme or net_loc.
857 # In general, this means we can safely join on /; we just need to
858 # ensure we end up with precisely one / joining base and url. The
859 # exception here is the case of media uploads, where url will be an
860 # absolute url.
861 if url.startswith("http://") or url.startswith("https://"):
862 return urljoin(base, url)
863 new_base = base if base.endswith("/") else base + "/"
864 new_url = url[1:] if url.startswith("/") else url
865 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800866
867
John Asmuth864311d2014-04-24 15:46:08 -0400868# TODO(dhermes): Convert this class to ResourceMethod and make it callable
869class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700870 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400871
872 Attributes:
873 argmap: Map from method parameter name (string) to query parameter name
874 (string).
875 required_params: List of required parameters (represented by parameter
876 name as string).
877 repeated_params: List of repeated parameters (represented by parameter
878 name as string).
879 pattern_params: Map from method parameter name (string) to regular
880 expression (as a string). If the pattern is set for a parameter, the
881 value for that parameter must match the regular expression.
882 query_params: List of parameters (represented by parameter name as string)
883 that will be used in the query string.
884 path_params: Set of parameters (represented by parameter name as string)
885 that will be used in the base URL path.
886 param_types: Map from method parameter name (string) to parameter type. Type
887 can be any valid JSON schema type; valid values are 'any', 'array',
888 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
889 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
890 enum_params: Map from method parameter name (string) to list of strings,
891 where each list of strings is the list of acceptable enum values.
892 """
893
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700894 def __init__(self, method_desc):
895 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400896
897 Sets default values and defers to set_parameters to populate.
898
899 Args:
900 method_desc: Dictionary with metadata describing an API method. Value
901 comes from the dictionary of methods stored in the 'methods' key in
902 the deserialized discovery document.
903 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700904 self.argmap = {}
905 self.required_params = []
906 self.repeated_params = []
907 self.pattern_params = {}
908 self.query_params = []
909 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
910 # parsing is gotten rid of.
911 self.path_params = set()
912 self.param_types = {}
913 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400914
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700915 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400916
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700917 def set_parameters(self, method_desc):
918 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400919
920 Iterates through each parameter for the method and parses the values from
921 the parameter dictionary.
922
923 Args:
924 method_desc: Dictionary with metadata describing an API method. Value
925 comes from the dictionary of methods stored in the 'methods' key in
926 the deserialized discovery document.
927 """
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500928 parameters = method_desc.get("parameters", {})
929 sorted_parameters = OrderedDict(sorted(parameters.items()))
930 for arg, desc in six.iteritems(sorted_parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700931 param = key2param(arg)
932 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400933
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700934 if desc.get("pattern"):
935 self.pattern_params[param] = desc["pattern"]
936 if desc.get("enum"):
937 self.enum_params[param] = desc["enum"]
938 if desc.get("required"):
939 self.required_params.append(param)
940 if desc.get("repeated"):
941 self.repeated_params.append(param)
942 if desc.get("location") == "query":
943 self.query_params.append(param)
944 if desc.get("location") == "path":
945 self.path_params.add(param)
946 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400947
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700948 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
949 # should have all path parameters already marked with
950 # 'location: path'.
951 for match in URITEMPLATE.finditer(method_desc["path"]):
952 for namematch in VARNAME.finditer(match.group(0)):
953 name = key2param(namematch.group(0))
954 self.path_params.add(name)
955 if name in self.query_params:
956 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400957
958
959def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700960 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400961
962 Args:
963 methodName: string, name of the method to use.
964 methodDesc: object, fragment of deserialized discovery document that
965 describes the method.
966 rootDesc: object, the entire deserialized discovery document.
967 schema: object, mapping of schema names to schema descriptions.
968 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700969 methodName = fix_method_name(methodName)
970 (
971 pathUrl,
972 httpMethod,
973 methodId,
974 accept,
975 maxSize,
976 mediaPathUrl,
977 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400978
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700979 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400980
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700981 def method(self, **kwargs):
982 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400983
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700984 for name in six.iterkeys(kwargs):
985 if name not in parameters.argmap:
986 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400987
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700988 # Remove args that have a value of None.
989 keys = list(kwargs.keys())
990 for name in keys:
991 if kwargs[name] is None:
992 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400993
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700994 for name in parameters.required_params:
995 if name not in kwargs:
996 # temporary workaround for non-paging methods incorrectly requiring
997 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
998 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
999 _methodProperties(methodDesc, schema, "response")
1000 ):
1001 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -04001002
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001003 for name, regex in six.iteritems(parameters.pattern_params):
1004 if name in kwargs:
1005 if isinstance(kwargs[name], six.string_types):
1006 pvalues = [kwargs[name]]
1007 else:
1008 pvalues = kwargs[name]
1009 for pvalue in pvalues:
1010 if re.match(regex, pvalue) is None:
1011 raise TypeError(
1012 'Parameter "%s" value "%s" does not match the pattern "%s"'
1013 % (name, pvalue, regex)
1014 )
1015
1016 for name, enums in six.iteritems(parameters.enum_params):
1017 if name in kwargs:
1018 # We need to handle the case of a repeated enum
1019 # name differently, since we want to handle both
1020 # arg='value' and arg=['value1', 'value2']
1021 if name in parameters.repeated_params and not isinstance(
1022 kwargs[name], six.string_types
1023 ):
1024 values = kwargs[name]
1025 else:
1026 values = [kwargs[name]]
1027 for value in values:
1028 if value not in enums:
1029 raise TypeError(
1030 'Parameter "%s" value "%s" is not an allowed value in "%s"'
1031 % (name, value, str(enums))
1032 )
1033
1034 actual_query_params = {}
1035 actual_path_params = {}
1036 for key, value in six.iteritems(kwargs):
1037 to_type = parameters.param_types.get(key, "string")
1038 # For repeated parameters we cast each member of the list.
1039 if key in parameters.repeated_params and type(value) == type([]):
1040 cast_value = [_cast(x, to_type) for x in value]
1041 else:
1042 cast_value = _cast(value, to_type)
1043 if key in parameters.query_params:
1044 actual_query_params[parameters.argmap[key]] = cast_value
1045 if key in parameters.path_params:
1046 actual_path_params[parameters.argmap[key]] = cast_value
1047 body_value = kwargs.get("body", None)
1048 media_filename = kwargs.get("media_body", None)
1049 media_mime_type = kwargs.get("media_mime_type", None)
1050
1051 if self._developerKey:
1052 actual_query_params["key"] = self._developerKey
1053
1054 model = self._model
1055 if methodName.endswith("_media"):
1056 model = MediaModel()
1057 elif "response" not in methodDesc:
1058 model = RawModel()
1059
1060 headers = {}
1061 headers, params, query, body = model.request(
1062 headers, actual_path_params, actual_query_params, body_value
1063 )
1064
1065 expanded_url = uritemplate.expand(pathUrl, params)
1066 url = _urljoin(self._baseUrl, expanded_url + query)
1067
1068 resumable = None
1069 multipart_boundary = ""
1070
1071 if media_filename:
1072 # Ensure we end up with a valid MediaUpload object.
1073 if isinstance(media_filename, six.string_types):
1074 if media_mime_type is None:
1075 logger.warning(
1076 "media_mime_type argument not specified: trying to auto-detect for %s",
1077 media_filename,
1078 )
1079 media_mime_type, _ = mimetypes.guess_type(media_filename)
1080 if media_mime_type is None:
1081 raise UnknownFileType(media_filename)
1082 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
1083 raise UnacceptableMimeTypeError(media_mime_type)
1084 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
1085 elif isinstance(media_filename, MediaUpload):
1086 media_upload = media_filename
1087 else:
1088 raise TypeError("media_filename must be str or MediaUpload.")
1089
1090 # Check the maxSize
1091 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
1092 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
1093
1094 # Use the media path uri for media uploads
1095 expanded_url = uritemplate.expand(mediaPathUrl, params)
1096 url = _urljoin(self._baseUrl, expanded_url + query)
1097 if media_upload.resumable():
1098 url = _add_query_parameter(url, "uploadType", "resumable")
1099
1100 if media_upload.resumable():
1101 # This is all we need to do for resumable, if the body exists it gets
1102 # sent in the first request, otherwise an empty body is sent.
1103 resumable = media_upload
1104 else:
1105 # A non-resumable upload
1106 if body is None:
1107 # This is a simple media upload
1108 headers["content-type"] = media_upload.mimetype()
1109 body = media_upload.getbytes(0, media_upload.size())
1110 url = _add_query_parameter(url, "uploadType", "media")
1111 else:
1112 # This is a multipart/related upload.
1113 msgRoot = MIMEMultipart("related")
1114 # msgRoot should not write out it's own headers
1115 setattr(msgRoot, "_write_headers", lambda self: None)
1116
1117 # attach the body as one part
1118 msg = MIMENonMultipart(*headers["content-type"].split("/"))
1119 msg.set_payload(body)
1120 msgRoot.attach(msg)
1121
1122 # attach the media as the second part
1123 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
1124 msg["Content-Transfer-Encoding"] = "binary"
1125
1126 payload = media_upload.getbytes(0, media_upload.size())
1127 msg.set_payload(payload)
1128 msgRoot.attach(msg)
1129 # encode the body: note that we can't use `as_string`, because
1130 # it plays games with `From ` lines.
1131 fp = BytesIO()
1132 g = _BytesGenerator(fp, mangle_from_=False)
1133 g.flatten(msgRoot, unixfrom=False)
1134 body = fp.getvalue()
1135
1136 multipart_boundary = msgRoot.get_boundary()
1137 headers["content-type"] = (
1138 "multipart/related; " 'boundary="%s"'
1139 ) % multipart_boundary
1140 url = _add_query_parameter(url, "uploadType", "multipart")
1141
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001142 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001143 return self._requestBuilder(
1144 self._http,
1145 model.response,
1146 url,
1147 method=httpMethod,
1148 body=body,
1149 headers=headers,
1150 methodId=methodId,
1151 resumable=resumable,
1152 )
1153
1154 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1155 if len(parameters.argmap) > 0:
1156 docs.append("Args:\n")
1157
1158 # Skip undocumented params and params common to all methods.
1159 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1160 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1161
1162 all_args = list(parameters.argmap.keys())
1163 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1164
1165 # Move body to the front of the line.
1166 if "body" in all_args:
1167 args_ordered.append("body")
1168
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001169 for name in sorted(all_args):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001170 if name not in args_ordered:
1171 args_ordered.append(name)
1172
1173 for arg in args_ordered:
1174 if arg in skip_parameters:
1175 continue
1176
1177 repeated = ""
1178 if arg in parameters.repeated_params:
1179 repeated = " (repeated)"
1180 required = ""
1181 if arg in parameters.required_params:
1182 required = " (required)"
1183 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1184 paramdoc = paramdesc.get("description", "A parameter")
1185 if "$ref" in paramdesc:
1186 docs.append(
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001187 (" %s: object, %s%s%s\n The object takes the form of:\n\n%s\n\n")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001188 % (
1189 arg,
1190 paramdoc,
1191 required,
1192 repeated,
1193 schema.prettyPrintByName(paramdesc["$ref"]),
1194 )
1195 )
John Asmuth864311d2014-04-24 15:46:08 -04001196 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001197 paramtype = paramdesc.get("type", "string")
1198 docs.append(
1199 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1200 )
1201 enum = paramdesc.get("enum", [])
1202 enumDesc = paramdesc.get("enumDescriptions", [])
1203 if enum and enumDesc:
1204 docs.append(" Allowed values\n")
1205 for (name, desc) in zip(enum, enumDesc):
1206 docs.append(" %s - %s\n" % (name, desc))
1207 if "response" in methodDesc:
1208 if methodName.endswith("_media"):
1209 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001210 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001211 docs.append("\nReturns:\n An object of the form:\n\n ")
1212 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001213
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001214 setattr(method, "__doc__", "".join(docs))
1215 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001216
1217
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001218def createNextMethod(
1219 methodName,
1220 pageTokenName="pageToken",
1221 nextPageTokenName="nextPageToken",
1222 isPageTokenParameter=True,
1223):
1224 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001225
1226 The _next methods allow for easy iteration through list() responses.
1227
1228 Args:
1229 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001230 pageTokenName: string, name of request page token field.
1231 nextPageTokenName: string, name of response page token field.
1232 isPageTokenParameter: Boolean, True if request page token is a query
1233 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001234 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001235 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001236
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001237 def methodNext(self, previous_request, previous_response):
1238 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001239
1240Args:
1241 previous_request: The request for the previous page. (required)
1242 previous_response: The response from the request for the previous page. (required)
1243
1244Returns:
1245 A request object that you can call 'execute()' on to request the next
1246 page. Returns None if there are no more items in the collection.
1247 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001248 # Retrieve nextPageToken from previous_response
1249 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001250
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001251 nextPageToken = previous_response.get(nextPageTokenName, None)
1252 if not nextPageToken:
1253 return None
John Asmuth864311d2014-04-24 15:46:08 -04001254
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001255 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001256
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001257 if isPageTokenParameter:
1258 # Replace pageToken value in URI
1259 request.uri = _add_query_parameter(
1260 request.uri, pageTokenName, nextPageToken
1261 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001262 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001263 else:
1264 # Replace pageToken value in request body
1265 model = self._model
1266 body = model.deserialize(request.body)
1267 body[pageTokenName] = nextPageToken
1268 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001269 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001270
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001271 return request
John Asmuth864311d2014-04-24 15:46:08 -04001272
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001273 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001274
1275
1276class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001277 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001278
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001279 def __init__(
1280 self,
1281 http,
1282 baseUrl,
1283 model,
1284 requestBuilder,
1285 developerKey,
1286 resourceDesc,
1287 rootDesc,
1288 schema,
1289 ):
1290 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001291
1292 Args:
1293 http: httplib2.Http, Object to make http requests with.
1294 baseUrl: string, base URL for the API. All requests are relative to this
1295 URI.
1296 model: googleapiclient.Model, converts to and from the wire format.
1297 requestBuilder: class or callable that instantiates an
1298 googleapiclient.HttpRequest object.
1299 developerKey: string, key obtained from
1300 https://code.google.com/apis/console
1301 resourceDesc: object, section of deserialized discovery document that
1302 describes a resource. Note that the top level discovery document
1303 is considered a resource.
1304 rootDesc: object, the entire deserialized discovery document.
1305 schema: object, mapping of schema names to schema descriptions.
1306 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001307 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001308
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001309 self._http = http
1310 self._baseUrl = baseUrl
1311 self._model = model
1312 self._developerKey = developerKey
1313 self._requestBuilder = requestBuilder
1314 self._resourceDesc = resourceDesc
1315 self._rootDesc = rootDesc
1316 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001317
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001318 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001320 def _set_dynamic_attr(self, attr_name, value):
1321 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001322
1323 Args:
1324 attr_name: string; The name of the attribute to be set
1325 value: The value being set on the object and tracked in the dynamic cache.
1326 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001327 self._dynamic_attrs.append(attr_name)
1328 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001329
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001330 def __getstate__(self):
1331 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001332
1333 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1334 will be wiped and restored on pickle serialization.
1335 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001336 state_dict = copy.copy(self.__dict__)
1337 for dynamic_attr in self._dynamic_attrs:
1338 del state_dict[dynamic_attr]
1339 del state_dict["_dynamic_attrs"]
1340 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001341
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001342 def __setstate__(self, state):
1343 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001344
1345 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1346 will be wiped and restored on pickle serialization.
1347 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001348 self.__dict__.update(state)
1349 self._dynamic_attrs = []
1350 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001351
Bu Sun Kim98888da2020-09-23 11:10:39 -06001352
1353 def __enter__(self):
1354 return self
1355
1356 def __exit__(self, exc_type, exc, exc_tb):
1357 self.close()
1358
1359 def close(self):
1360 """Close httplib2 connections."""
1361 # httplib2 leaves sockets open by default.
1362 # Cleanup using the `close` method.
1363 # https://github.com/httplib2/httplib2/issues/148
1364 self._http.http.close()
1365
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001366 def _set_service_methods(self):
1367 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1368 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1369 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001370
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001371 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1372 # If this is the root Resource, add a new_batch_http_request() method.
1373 if resourceDesc == rootDesc:
1374 batch_uri = "%s%s" % (
1375 rootDesc["rootUrl"],
1376 rootDesc.get("batchPath", "batch"),
1377 )
1378
1379 def new_batch_http_request(callback=None):
1380 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001381
1382 Args:
1383 callback: callable, A callback to be called for each response, of the
1384 form callback(id, response, exception). The first parameter is the
1385 request id, and the second is the deserialized response object. The
1386 third is an apiclient.errors.HttpError exception object if an HTTP
1387 error occurred while processing the request, or None if no error
1388 occurred.
1389
1390 Returns:
1391 A BatchHttpRequest object based on the discovery document.
1392 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001393 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001394
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001395 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001396
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001397 # Add basic methods to Resource
1398 if "methods" in resourceDesc:
1399 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1400 fixedMethodName, method = createMethod(
1401 methodName, methodDesc, rootDesc, schema
1402 )
1403 self._set_dynamic_attr(
1404 fixedMethodName, method.__get__(self, self.__class__)
1405 )
1406 # Add in _media methods. The functionality of the attached method will
1407 # change when it sees that the method name ends in _media.
1408 if methodDesc.get("supportsMediaDownload", False):
1409 fixedMethodName, method = createMethod(
1410 methodName + "_media", methodDesc, rootDesc, schema
1411 )
1412 self._set_dynamic_attr(
1413 fixedMethodName, method.__get__(self, self.__class__)
1414 )
John Asmuth864311d2014-04-24 15:46:08 -04001415
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001416 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1417 # Add in nested resources
1418 if "resources" in resourceDesc:
1419
1420 def createResourceMethod(methodName, methodDesc):
1421 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001422
1423 Args:
1424 methodName: string, name of the method to use.
1425 methodDesc: object, fragment of deserialized discovery document that
1426 describes the method.
1427 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001428 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001429
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001430 def methodResource(self):
1431 return Resource(
1432 http=self._http,
1433 baseUrl=self._baseUrl,
1434 model=self._model,
1435 developerKey=self._developerKey,
1436 requestBuilder=self._requestBuilder,
1437 resourceDesc=methodDesc,
1438 rootDesc=rootDesc,
1439 schema=schema,
1440 )
John Asmuth864311d2014-04-24 15:46:08 -04001441
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001442 setattr(methodResource, "__doc__", "A collection resource.")
1443 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001444
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001445 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001446
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001447 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1448 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1449 self._set_dynamic_attr(
1450 fixedMethodName, method.__get__(self, self.__class__)
1451 )
John Asmuth864311d2014-04-24 15:46:08 -04001452
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001453 def _add_next_methods(self, resourceDesc, schema):
1454 # Add _next() methods if and only if one of the names 'pageToken' or
1455 # 'nextPageToken' occurs among the fields of both the method's response
1456 # type either the method's request (query parameters) or request body.
1457 if "methods" not in resourceDesc:
1458 return
1459 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1460 nextPageTokenName = _findPageTokenName(
1461 _methodProperties(methodDesc, schema, "response")
1462 )
1463 if not nextPageTokenName:
1464 continue
1465 isPageTokenParameter = True
1466 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1467 if not pageTokenName:
1468 isPageTokenParameter = False
1469 pageTokenName = _findPageTokenName(
1470 _methodProperties(methodDesc, schema, "request")
1471 )
1472 if not pageTokenName:
1473 continue
1474 fixedMethodName, method = createNextMethod(
1475 methodName + "_next",
1476 pageTokenName,
1477 nextPageTokenName,
1478 isPageTokenParameter,
1479 )
1480 self._set_dynamic_attr(
1481 fixedMethodName, method.__get__(self, self.__class__)
1482 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001483
1484
1485def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001486 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001487
1488 Args:
1489 fields: container of string, names of fields.
1490
1491 Returns:
1492 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1493 otherwise None.
1494 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001495 return next(
1496 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1497 )
1498
Thomas Coffee20af04d2017-02-10 15:24:44 -08001499
1500def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001501 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001502
1503 Args:
1504 methodDesc: object, fragment of deserialized discovery document that
1505 describes the method.
1506 schema: object, mapping of schema names to schema descriptions.
1507 name: string, name of top-level field in method description.
1508
1509 Returns:
1510 Object representing fragment of deserialized discovery document
1511 corresponding to 'properties' field of object corresponding to named field
1512 in method description, if it exists, otherwise empty dict.
1513 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001514 desc = methodDesc.get(name, {})
1515 if "$ref" in desc:
1516 desc = schema.get(desc["$ref"], {})
1517 return desc.get("properties", {})