blob: 2ea74a63d2bc9c34b115ebdcd0faf8e337d9e8d9 [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,
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400185 discoveryServiceUrl=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700186 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 Partheniou3b4f2e22021-03-19 11:36:01 -0400196 static_discovery=None,
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
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400251 included in the library. The default value for `static_discovery` depends
252 on the value of `discoveryServiceUrl`. `static_discovery` will default to
253 `True` when `discoveryServiceUrl` is also not provided, otherwise it will
254 default to `False`.
John Asmuth864311d2014-04-24 15:46:08 -0400255
256 Returns:
257 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700258
259 Raises:
260 google.auth.exceptions.MutualTLSChannelError: if there are any problems
261 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400262 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700263 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400264
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400265 # The default value for `static_discovery` depends on the value of
266 # `discoveryServiceUrl`. `static_discovery` will default to `True` when
267 # `discoveryServiceUrl` is also not provided, otherwise it will default to
268 # `False`. This is added for backwards compatability with
269 # google-api-python-client 1.x which does not support the `static_discovery`
270 # parameter.
271 if static_discovery is None:
272 if discoveryServiceUrl is None:
273 static_discovery = True
274 else:
275 static_discovery = False
276
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700277 if http is None:
278 discovery_http = build_http()
279 else:
280 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400281
Bu Sun Kim98888da2020-09-23 11:10:39 -0600282 service = None
283
Bu Sun Kim790e7022020-09-11 20:18:06 -0600284 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700285 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700287 try:
288 content = _retrieve_discovery_doc(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600289 requested_url,
290 discovery_http,
291 cache_discovery,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500292 serviceName,
293 version,
Bu Sun Kim790e7022020-09-11 20:18:06 -0600294 cache,
295 developerKey,
296 num_retries=num_retries,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500297 static_discovery=static_discovery,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700298 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600299 service = build_from_document(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700300 content,
301 base=discovery_url,
302 http=http,
303 developerKey=developerKey,
304 model=model,
305 requestBuilder=requestBuilder,
306 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700307 client_options=client_options,
308 adc_cert_path=adc_cert_path,
309 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700310 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600311 break # exit if a service was created
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700312 except HttpError as e:
313 if e.resp.status == http_client.NOT_FOUND:
314 continue
315 else:
316 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700317
Bu Sun Kim98888da2020-09-23 11:10:39 -0600318 # If discovery_http was created by this function, we are done with it
319 # and can safely close it
320 if http is None:
321 discovery_http.close()
322
323 if service is None:
324 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
325 else:
326 return service
Takashi Matsuo30125122015-08-19 11:42:32 -0700327
328
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700329def _discovery_service_uri_options(discoveryServiceUrl, version):
330 """
331 Returns Discovery URIs to be used for attemnting to build the API Resource.
332
333 Args:
334 discoveryServiceUrl:
335 string, the Original Discovery Service URL preferred by the customer.
336 version:
337 string, API Version requested
338
339 Returns:
340 A list of URIs to be tried for the Service Discovery, in order.
341 """
342
Alex1c4d1992021-04-29 04:04:06 -0700343 if discoveryServiceUrl is not None:
344 return [discoveryServiceUrl]
345 if version is None:
346 # V1 Discovery won't work if the requested version is None
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700347 logger.warning(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600348 "Discovery V1 does not support empty versions. Defaulting to V2..."
349 )
Alex1c4d1992021-04-29 04:04:06 -0700350 return [V2_DISCOVERY_URI]
351 else:
352 return [DISCOVERY_URI, V2_DISCOVERY_URI]
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700353
354
Bu Sun Kim790e7022020-09-11 20:18:06 -0600355def _retrieve_discovery_doc(
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500356 url,
357 http,
358 cache_discovery,
359 serviceName,
360 version,
361 cache=None,
362 developerKey=None,
363 num_retries=1,
364 static_discovery=True
Bu Sun Kim790e7022020-09-11 20:18:06 -0600365):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700366 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700367
368 Args:
369 url: string, the URL of the discovery document.
370 http: httplib2.Http, An instance of httplib2.Http or something that acts
371 like it through which HTTP requests will be made.
372 cache_discovery: Boolean, whether or not to cache the discovery doc.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500373 serviceName: string, name of the service.
374 version: string, the version of the service.
Takashi Matsuo30125122015-08-19 11:42:32 -0700375 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
376 object for the discovery documents.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700377 developerKey: string, Key for controlling API usage, generated
378 from the API Console.
379 num_retries: Integer, number of times to retry discovery with
380 randomized exponential backoff in case of intermittent/connection issues.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500381 static_discovery: Boolean, whether or not to use the static discovery docs
382 included in the library.
Takashi Matsuo30125122015-08-19 11:42:32 -0700383
384 Returns:
385 A unicode string representation of the discovery document.
386 """
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500387 from . import discovery_cache
Takashi Matsuo30125122015-08-19 11:42:32 -0700388
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500389 if cache_discovery:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700390 if cache is None:
391 cache = discovery_cache.autodetect()
392 if cache:
393 content = cache.get(url)
394 if content:
395 return content
John Asmuth864311d2014-04-24 15:46:08 -0400396
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500397 # When `static_discovery=True`, use static discovery artifacts included
398 # with the library
399 if static_discovery:
400 content = discovery_cache.get_static_doc(serviceName, version)
401 if content:
402 return content
403 else:
404 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
405
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700406 actual_url = url
407 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
408 # variable that contains the network address of the client sending the
409 # request. If it exists then add that to the request for the discovery
410 # document to avoid exceeding the quota on discovery requests.
411 if "REMOTE_ADDR" in os.environ:
412 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
413 if developerKey:
414 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700415 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400416
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700417 # Execute this request with retries build into HttpRequest
418 # Note that it will already raise an error if we don't get a 2xx response
419 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
420 resp, content = req.execute(num_retries=num_retries)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800421
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700422 try:
423 content = content.decode("utf-8")
424 except AttributeError:
425 pass
426
427 try:
428 service = json.loads(content)
429 except ValueError as e:
430 logger.error("Failed to parse as JSON: " + content)
431 raise InvalidJsonError()
432 if cache_discovery and cache:
433 cache.set(url, content)
434 return content
John Asmuth864311d2014-04-24 15:46:08 -0400435
436
437@positional(1)
438def build_from_document(
439 service,
440 base=None,
441 future=None,
442 http=None,
443 developerKey=None,
444 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700445 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700446 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700447 client_options=None,
448 adc_cert_path=None,
449 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700450):
451 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400452
453 Same as `build()`, but constructs the Resource object from a discovery
454 document that is it given, as opposed to retrieving one over HTTP.
455
456 Args:
457 service: string or object, the JSON discovery document describing the API.
458 The value passed in may either be the JSON string or the deserialized
459 JSON.
460 base: string, base URI for all HTTP requests, usually the discovery URI.
461 This parameter is no longer used as rootUrl and servicePath are included
462 within the discovery document. (deprecated)
463 future: string, discovery document with future capabilities (deprecated).
464 http: httplib2.Http, An instance of httplib2.Http or something that acts
465 like it that HTTP requests will be made through.
466 developerKey: string, Key for controlling API usage, generated
467 from the API Console.
468 model: Model class instance that serializes and de-serializes requests and
469 responses.
470 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800471 credentials: oauth2client.Credentials or
472 google.auth.credentials.Credentials, credentials to be used for
473 authentication.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300474 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700475 options to set user options on the client.
476 (1) The API endpoint should be set through client_options. If API endpoint
477 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
478 to control which endpoint to use.
479 (2) client_cert_source is not supported, client cert should be provided using
480 client_encrypted_cert_source instead. In order to use the provided client
481 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
482 set to `true`.
483 More details on the environment variables are here:
484 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700485 adc_cert_path: str, client certificate file path to save the application
486 default client certificate for mTLS. This field is required if you want to
arithmetic17282fc5ca12020-08-27 14:08:12 -0700487 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
488 environment variable must be set to `true` in order to use this field,
489 otherwise this field doesn't nothing.
490 More details on the environment variables are here:
491 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700492 adc_key_path: str, client encrypted private key file path to save the
493 application default client encrypted private key for mTLS. This field is
494 required if you want to use the default client certificate.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700495 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
496 `true` in order to use this field, otherwise this field doesn't nothing.
497 More details on the environment variables are here:
498 https://google.aip.dev/auth/4114
John Asmuth864311d2014-04-24 15:46:08 -0400499
500 Returns:
501 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700502
503 Raises:
504 google.auth.exceptions.MutualTLSChannelError: if there are any problems
505 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400506 """
507
Bu Sun Kim790e7022020-09-11 20:18:06 -0600508 if client_options is None:
509 client_options = google.api_core.client_options.ClientOptions()
510 if isinstance(client_options, six.moves.collections_abc.Mapping):
511 client_options = google.api_core.client_options.from_dict(client_options)
512
513 if http is not None:
514 # if http is passed, the user cannot provide credentials
515 banned_options = [
516 (credentials, "credentials"),
517 (client_options.credentials_file, "client_options.credentials_file"),
518 ]
519 for option, name in banned_options:
520 if option is not None:
521 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
John Asmuth864311d2014-04-24 15:46:08 -0400522
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700523 if isinstance(service, six.string_types):
524 service = json.loads(service)
525 elif isinstance(service, six.binary_type):
526 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400527
arithmetic1728981eadf2020-06-02 10:20:10 -0700528 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700529 logger.error(
530 "You are using HttpMock or HttpMockSequence without"
531 + "having the service discovery doc in cache. Try calling "
532 + "build() without mocking once first to populate the "
533 + "cache."
534 )
535 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400536
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700537 # If an API Endpoint is provided on client options, use that as the base URL
arithmetic1728981eadf2020-06-02 10:20:10 -0700538 base = urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim790e7022020-09-11 20:18:06 -0600539 if client_options.api_endpoint:
540 base = client_options.api_endpoint
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700541
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700542 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400543
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700544 # If the http client is not specified, then we must construct an http client
545 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800546 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700547 if http is None:
548 # Does the service require scopes?
549 scopes = list(
550 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
551 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700552
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700553 # If so, then the we need to setup authentication if no developerKey is
554 # specified.
555 if scopes and not developerKey:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600556 # Make sure the user didn't pass multiple credentials
557 if client_options.credentials_file and credentials:
558 raise google.api_core.exceptions.DuplicateCredentialArgs(
559 "client_options.credentials_file and credentials are mutually exclusive."
560 )
561 # Check for credentials file via client options
562 if client_options.credentials_file:
563 credentials = _auth.credentials_from_file(
564 client_options.credentials_file,
565 scopes=client_options.scopes,
566 quota_project_id=client_options.quota_project_id,
567 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700568 # If the user didn't pass in credentials, attempt to acquire application
569 # default credentials.
570 if credentials is None:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600571 credentials = _auth.default_credentials(
572 scopes=client_options.scopes,
573 quota_project_id=client_options.quota_project_id,
574 )
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800575
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700576 # The credentials need to be scoped.
Bu Sun Kim790e7022020-09-11 20:18:06 -0600577 # If the user provided scopes via client_options don't override them
578 if not client_options.scopes:
579 credentials = _auth.with_scopes(credentials, scopes)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700580
581 # If credentials are provided, create an authorized http instance;
582 # otherwise, skip authentication.
583 if credentials:
584 http = _auth.authorized_http(credentials)
585
586 # If the service doesn't require scopes then there is no need for
587 # authentication.
588 else:
589 http = build_http()
590
arithmetic1728981eadf2020-06-02 10:20:10 -0700591 # Obtain client cert and create mTLS http channel if cert exists.
592 client_cert_to_use = None
arithmetic17282fc5ca12020-08-27 14:08:12 -0700593 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
594 if not use_client_cert in ("true", "false"):
595 raise MutualTLSChannelError(
596 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
597 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700598 if client_options and client_options.client_cert_source:
599 raise MutualTLSChannelError(
600 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
601 )
arithmetic17282fc5ca12020-08-27 14:08:12 -0700602 if use_client_cert == "true":
603 if (
604 client_options
605 and hasattr(client_options, "client_encrypted_cert_source")
606 and client_options.client_encrypted_cert_source
607 ):
608 client_cert_to_use = client_options.client_encrypted_cert_source
Bu Sun Kim790e7022020-09-11 20:18:06 -0600609 elif (
610 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
611 ):
arithmetic17282fc5ca12020-08-27 14:08:12 -0700612 client_cert_to_use = mtls.default_client_encrypted_cert_source(
613 adc_cert_path, adc_key_path
614 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700615 if client_cert_to_use:
616 cert_path, key_path, passphrase = client_cert_to_use()
617
618 # The http object we built could be google_auth_httplib2.AuthorizedHttp
619 # or httplib2.Http. In the first case we need to extract the wrapped
620 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
621 http_channel = (
622 http.http
623 if google_auth_httplib2
624 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
625 else http
626 )
627 http_channel.add_certificate(key_path, cert_path, "", passphrase)
628
629 # If user doesn't provide api endpoint via client options, decide which
630 # api endpoint to use.
631 if "mtlsRootUrl" in service and (
632 not client_options or not client_options.api_endpoint
633 ):
634 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
arithmetic17282fc5ca12020-08-27 14:08:12 -0700635 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
arithmetic1728981eadf2020-06-02 10:20:10 -0700636
arithmetic17282fc5ca12020-08-27 14:08:12 -0700637 if not use_mtls_endpoint in ("never", "auto", "always"):
arithmetic1728981eadf2020-06-02 10:20:10 -0700638 raise MutualTLSChannelError(
arithmetic17282fc5ca12020-08-27 14:08:12 -0700639 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
arithmetic1728981eadf2020-06-02 10:20:10 -0700640 )
641
arithmetic172819908ed2020-06-09 22:32:43 -0700642 # Switch to mTLS endpoint, if environment variable is "always", or
643 # environment varibable is "auto" and client cert exists.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700644 if use_mtls_endpoint == "always" or (
645 use_mtls_endpoint == "auto" and client_cert_to_use
arithmetic1728981eadf2020-06-02 10:20:10 -0700646 ):
647 base = mtls_endpoint
648
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700649 if model is None:
650 features = service.get("features", [])
651 model = JsonModel("dataWrapper" in features)
652
653 return Resource(
654 http=http,
655 baseUrl=base,
656 model=model,
657 developerKey=developerKey,
658 requestBuilder=requestBuilder,
659 resourceDesc=service,
660 rootDesc=service,
661 schema=schema,
662 )
John Asmuth864311d2014-04-24 15:46:08 -0400663
664
665def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700666 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400667
668 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
669 JSON Schema.
670
671 Args:
672 value: any, the value to convert
673 schema_type: string, the type that value should be interpreted as
674
675 Returns:
676 A string representation of 'value' based on the schema_type.
677 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700678 if schema_type == "string":
679 if type(value) == type("") or type(value) == type(u""):
680 return value
681 else:
682 return str(value)
683 elif schema_type == "integer":
684 return str(int(value))
685 elif schema_type == "number":
686 return str(float(value))
687 elif schema_type == "boolean":
688 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400689 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700690 if type(value) == type("") or type(value) == type(u""):
691 return value
692 else:
693 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400694
695
696def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700697 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400698
699 Args:
700 maxSize: string, size as a string, such as 2MB or 7GB.
701
702 Returns:
703 The size as an integer value.
704 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700705 if len(maxSize) < 2:
706 return 0
707 units = maxSize[-2:].upper()
708 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
709 if bit_shift is not None:
710 return int(maxSize[:-2]) << bit_shift
711 else:
712 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400713
714
715def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700716 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400717
718 Constructed using the API root URI and service path from the discovery
719 document and the relative path for the API method.
720
721 Args:
722 root_desc: Dictionary; the entire original deserialized discovery document.
723 path_url: String; the relative URL for the API method. Relative to the API
724 root, which is specified in the discovery document.
725
726 Returns:
727 String; the absolute URI for media upload for the API method.
728 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700729 return "%(root)supload/%(service_path)s%(path)s" % {
730 "root": root_desc["rootUrl"],
731 "service_path": root_desc["servicePath"],
732 "path": path_url,
733 }
John Asmuth864311d2014-04-24 15:46:08 -0400734
735
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900736def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700737 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400738
739 Specifically, adds whatever global parameters are specified by the API to the
740 parameters for the individual method. Also adds parameters which don't
741 appear in the discovery document, but are available to all discovery based
742 APIs (these are listed in STACK_QUERY_PARAMETERS).
743
744 SIDE EFFECTS: This updates the parameters dictionary object in the method
745 description.
746
747 Args:
748 method_desc: Dictionary with metadata describing an API method. Value comes
749 from the dictionary of methods stored in the 'methods' key in the
750 deserialized discovery document.
751 root_desc: Dictionary; the entire original deserialized discovery document.
752 http_method: String; the HTTP method used to call the API method described
753 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900754 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400755
756 Returns:
757 The updated Dictionary stored in the 'parameters' key of the method
758 description dictionary.
759 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700760 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400761
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700762 # Add in the parameters common to all methods.
763 for name, description in six.iteritems(root_desc.get("parameters", {})):
764 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400765
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700766 # Add in undocumented query parameters.
767 for name in STACK_QUERY_PARAMETERS:
768 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400769
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700770 # Add 'body' (our own reserved word) to parameters if the method supports
771 # a request payload.
772 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
773 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
774 body.update(method_desc["request"])
775 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400776
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700777 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400778
779
780def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700781 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400782
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700783 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
784 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400785
786 Args:
787 method_desc: Dictionary with metadata describing an API method. Value comes
788 from the dictionary of methods stored in the 'methods' key in the
789 deserialized discovery document.
790 root_desc: Dictionary; the entire original deserialized discovery document.
791 path_url: String; the relative URL for the API method. Relative to the API
792 root, which is specified in the discovery document.
793 parameters: A dictionary describing method parameters for method described
794 in method_desc.
795
796 Returns:
797 Triple (accept, max_size, media_path_url) where:
798 - accept is a list of strings representing what content types are
799 accepted for media upload. Defaults to empty list if not in the
800 discovery document.
801 - max_size is a long representing the max size in bytes allowed for a
802 media upload. Defaults to 0L if not in the discovery document.
803 - media_path_url is a String; the absolute URI for media upload for the
804 API method. Constructed using the API root URI and service path from
805 the discovery document and the relative path for the API method. If
806 media upload is not supported, this is None.
807 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700808 media_upload = method_desc.get("mediaUpload", {})
809 accept = media_upload.get("accept", [])
810 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
811 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400812
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700813 if media_upload:
814 media_path_url = _media_path_url_from_info(root_desc, path_url)
815 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
816 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400817
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700818 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400819
820
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900821def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700822 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400823
824 SIDE EFFECTS: Changes the parameters dictionary in the method description with
825 extra parameters which are used locally.
826
827 Args:
828 method_desc: Dictionary with metadata describing an API method. Value comes
829 from the dictionary of methods stored in the 'methods' key in the
830 deserialized discovery document.
831 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900832 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400833
834 Returns:
835 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
836 where:
837 - path_url is a String; the relative URL for the API method. Relative to
838 the API root, which is specified in the discovery document.
839 - http_method is a String; the HTTP method used to call the API method
840 described in the method description.
841 - method_id is a String; the name of the RPC method associated with the
842 API method, and is in the method description in the 'id' key.
843 - accept is a list of strings representing what content types are
844 accepted for media upload. Defaults to empty list if not in the
845 discovery document.
846 - max_size is a long representing the max size in bytes allowed for a
847 media upload. Defaults to 0L if not in the discovery document.
848 - media_path_url is a String; the absolute URI for media upload for the
849 API method. Constructed using the API root URI and service path from
850 the discovery document and the relative path for the API method. If
851 media upload is not supported, this is None.
852 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700853 path_url = method_desc["path"]
854 http_method = method_desc["httpMethod"]
855 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400856
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700857 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
858 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
859 # 'parameters' key and needs to know if there is a 'body' parameter because it
860 # also sets a 'media_body' parameter.
861 accept, max_size, media_path_url = _fix_up_media_upload(
862 method_desc, root_desc, path_url, parameters
863 )
John Asmuth864311d2014-04-24 15:46:08 -0400864
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700865 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400866
867
Craig Citro7ee535d2015-02-23 10:11:14 -0800868def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700869 """Custom urljoin replacement supporting : before / in url."""
870 # In general, it's unsafe to simply join base and url. However, for
871 # the case of discovery documents, we know:
872 # * base will never contain params, query, or fragment
873 # * url will never contain a scheme or net_loc.
874 # In general, this means we can safely join on /; we just need to
875 # ensure we end up with precisely one / joining base and url. The
876 # exception here is the case of media uploads, where url will be an
877 # absolute url.
878 if url.startswith("http://") or url.startswith("https://"):
879 return urljoin(base, url)
880 new_base = base if base.endswith("/") else base + "/"
881 new_url = url[1:] if url.startswith("/") else url
882 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800883
884
John Asmuth864311d2014-04-24 15:46:08 -0400885# TODO(dhermes): Convert this class to ResourceMethod and make it callable
886class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700887 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400888
889 Attributes:
890 argmap: Map from method parameter name (string) to query parameter name
891 (string).
892 required_params: List of required parameters (represented by parameter
893 name as string).
894 repeated_params: List of repeated parameters (represented by parameter
895 name as string).
896 pattern_params: Map from method parameter name (string) to regular
897 expression (as a string). If the pattern is set for a parameter, the
898 value for that parameter must match the regular expression.
899 query_params: List of parameters (represented by parameter name as string)
900 that will be used in the query string.
901 path_params: Set of parameters (represented by parameter name as string)
902 that will be used in the base URL path.
903 param_types: Map from method parameter name (string) to parameter type. Type
904 can be any valid JSON schema type; valid values are 'any', 'array',
905 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
906 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
907 enum_params: Map from method parameter name (string) to list of strings,
908 where each list of strings is the list of acceptable enum values.
909 """
910
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700911 def __init__(self, method_desc):
912 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400913
914 Sets default values and defers to set_parameters to populate.
915
916 Args:
917 method_desc: Dictionary with metadata describing an API method. Value
918 comes from the dictionary of methods stored in the 'methods' key in
919 the deserialized discovery document.
920 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700921 self.argmap = {}
922 self.required_params = []
923 self.repeated_params = []
924 self.pattern_params = {}
925 self.query_params = []
926 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
927 # parsing is gotten rid of.
928 self.path_params = set()
929 self.param_types = {}
930 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400931
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700932 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400933
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700934 def set_parameters(self, method_desc):
935 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400936
937 Iterates through each parameter for the method and parses the values from
938 the parameter dictionary.
939
940 Args:
941 method_desc: Dictionary with metadata describing an API method. Value
942 comes from the dictionary of methods stored in the 'methods' key in
943 the deserialized discovery document.
944 """
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500945 parameters = method_desc.get("parameters", {})
946 sorted_parameters = OrderedDict(sorted(parameters.items()))
947 for arg, desc in six.iteritems(sorted_parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700948 param = key2param(arg)
949 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400950
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700951 if desc.get("pattern"):
952 self.pattern_params[param] = desc["pattern"]
953 if desc.get("enum"):
954 self.enum_params[param] = desc["enum"]
955 if desc.get("required"):
956 self.required_params.append(param)
957 if desc.get("repeated"):
958 self.repeated_params.append(param)
959 if desc.get("location") == "query":
960 self.query_params.append(param)
961 if desc.get("location") == "path":
962 self.path_params.add(param)
963 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400964
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700965 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
966 # should have all path parameters already marked with
967 # 'location: path'.
968 for match in URITEMPLATE.finditer(method_desc["path"]):
969 for namematch in VARNAME.finditer(match.group(0)):
970 name = key2param(namematch.group(0))
971 self.path_params.add(name)
972 if name in self.query_params:
973 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400974
975
976def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700977 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400978
979 Args:
980 methodName: string, name of the method to use.
981 methodDesc: object, fragment of deserialized discovery document that
982 describes the method.
983 rootDesc: object, the entire deserialized discovery document.
984 schema: object, mapping of schema names to schema descriptions.
985 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700986 methodName = fix_method_name(methodName)
987 (
988 pathUrl,
989 httpMethod,
990 methodId,
991 accept,
992 maxSize,
993 mediaPathUrl,
994 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400995
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700996 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400997
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700998 def method(self, **kwargs):
999 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -04001000
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001001 for name in six.iterkeys(kwargs):
1002 if name not in parameters.argmap:
1003 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -04001004
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001005 # Remove args that have a value of None.
1006 keys = list(kwargs.keys())
1007 for name in keys:
1008 if kwargs[name] is None:
1009 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -04001010
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001011 for name in parameters.required_params:
1012 if name not in kwargs:
1013 # temporary workaround for non-paging methods incorrectly requiring
1014 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
1015 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
1016 _methodProperties(methodDesc, schema, "response")
1017 ):
1018 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -04001019
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001020 for name, regex in six.iteritems(parameters.pattern_params):
1021 if name in kwargs:
1022 if isinstance(kwargs[name], six.string_types):
1023 pvalues = [kwargs[name]]
1024 else:
1025 pvalues = kwargs[name]
1026 for pvalue in pvalues:
1027 if re.match(regex, pvalue) is None:
1028 raise TypeError(
1029 'Parameter "%s" value "%s" does not match the pattern "%s"'
1030 % (name, pvalue, regex)
1031 )
1032
1033 for name, enums in six.iteritems(parameters.enum_params):
1034 if name in kwargs:
1035 # We need to handle the case of a repeated enum
1036 # name differently, since we want to handle both
1037 # arg='value' and arg=['value1', 'value2']
1038 if name in parameters.repeated_params and not isinstance(
1039 kwargs[name], six.string_types
1040 ):
1041 values = kwargs[name]
1042 else:
1043 values = [kwargs[name]]
1044 for value in values:
1045 if value not in enums:
1046 raise TypeError(
1047 'Parameter "%s" value "%s" is not an allowed value in "%s"'
1048 % (name, value, str(enums))
1049 )
1050
1051 actual_query_params = {}
1052 actual_path_params = {}
1053 for key, value in six.iteritems(kwargs):
1054 to_type = parameters.param_types.get(key, "string")
1055 # For repeated parameters we cast each member of the list.
1056 if key in parameters.repeated_params and type(value) == type([]):
1057 cast_value = [_cast(x, to_type) for x in value]
1058 else:
1059 cast_value = _cast(value, to_type)
1060 if key in parameters.query_params:
1061 actual_query_params[parameters.argmap[key]] = cast_value
1062 if key in parameters.path_params:
1063 actual_path_params[parameters.argmap[key]] = cast_value
1064 body_value = kwargs.get("body", None)
1065 media_filename = kwargs.get("media_body", None)
1066 media_mime_type = kwargs.get("media_mime_type", None)
1067
1068 if self._developerKey:
1069 actual_query_params["key"] = self._developerKey
1070
1071 model = self._model
1072 if methodName.endswith("_media"):
1073 model = MediaModel()
1074 elif "response" not in methodDesc:
1075 model = RawModel()
1076
1077 headers = {}
1078 headers, params, query, body = model.request(
1079 headers, actual_path_params, actual_query_params, body_value
1080 )
1081
1082 expanded_url = uritemplate.expand(pathUrl, params)
1083 url = _urljoin(self._baseUrl, expanded_url + query)
1084
1085 resumable = None
1086 multipart_boundary = ""
1087
1088 if media_filename:
1089 # Ensure we end up with a valid MediaUpload object.
1090 if isinstance(media_filename, six.string_types):
1091 if media_mime_type is None:
1092 logger.warning(
1093 "media_mime_type argument not specified: trying to auto-detect for %s",
1094 media_filename,
1095 )
1096 media_mime_type, _ = mimetypes.guess_type(media_filename)
1097 if media_mime_type is None:
1098 raise UnknownFileType(media_filename)
1099 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
1100 raise UnacceptableMimeTypeError(media_mime_type)
1101 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
1102 elif isinstance(media_filename, MediaUpload):
1103 media_upload = media_filename
1104 else:
1105 raise TypeError("media_filename must be str or MediaUpload.")
1106
1107 # Check the maxSize
1108 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
1109 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
1110
1111 # Use the media path uri for media uploads
1112 expanded_url = uritemplate.expand(mediaPathUrl, params)
1113 url = _urljoin(self._baseUrl, expanded_url + query)
1114 if media_upload.resumable():
1115 url = _add_query_parameter(url, "uploadType", "resumable")
1116
1117 if media_upload.resumable():
1118 # This is all we need to do for resumable, if the body exists it gets
1119 # sent in the first request, otherwise an empty body is sent.
1120 resumable = media_upload
1121 else:
1122 # A non-resumable upload
1123 if body is None:
1124 # This is a simple media upload
1125 headers["content-type"] = media_upload.mimetype()
1126 body = media_upload.getbytes(0, media_upload.size())
1127 url = _add_query_parameter(url, "uploadType", "media")
1128 else:
1129 # This is a multipart/related upload.
1130 msgRoot = MIMEMultipart("related")
1131 # msgRoot should not write out it's own headers
1132 setattr(msgRoot, "_write_headers", lambda self: None)
1133
1134 # attach the body as one part
1135 msg = MIMENonMultipart(*headers["content-type"].split("/"))
1136 msg.set_payload(body)
1137 msgRoot.attach(msg)
1138
1139 # attach the media as the second part
1140 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
1141 msg["Content-Transfer-Encoding"] = "binary"
1142
1143 payload = media_upload.getbytes(0, media_upload.size())
1144 msg.set_payload(payload)
1145 msgRoot.attach(msg)
1146 # encode the body: note that we can't use `as_string`, because
1147 # it plays games with `From ` lines.
1148 fp = BytesIO()
1149 g = _BytesGenerator(fp, mangle_from_=False)
1150 g.flatten(msgRoot, unixfrom=False)
1151 body = fp.getvalue()
1152
1153 multipart_boundary = msgRoot.get_boundary()
1154 headers["content-type"] = (
1155 "multipart/related; " 'boundary="%s"'
1156 ) % multipart_boundary
1157 url = _add_query_parameter(url, "uploadType", "multipart")
1158
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001159 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001160 return self._requestBuilder(
1161 self._http,
1162 model.response,
1163 url,
1164 method=httpMethod,
1165 body=body,
1166 headers=headers,
1167 methodId=methodId,
1168 resumable=resumable,
1169 )
1170
1171 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1172 if len(parameters.argmap) > 0:
1173 docs.append("Args:\n")
1174
1175 # Skip undocumented params and params common to all methods.
1176 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1177 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1178
1179 all_args = list(parameters.argmap.keys())
1180 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1181
1182 # Move body to the front of the line.
1183 if "body" in all_args:
1184 args_ordered.append("body")
1185
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001186 for name in sorted(all_args):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001187 if name not in args_ordered:
1188 args_ordered.append(name)
1189
1190 for arg in args_ordered:
1191 if arg in skip_parameters:
1192 continue
1193
1194 repeated = ""
1195 if arg in parameters.repeated_params:
1196 repeated = " (repeated)"
1197 required = ""
1198 if arg in parameters.required_params:
1199 required = " (required)"
1200 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1201 paramdoc = paramdesc.get("description", "A parameter")
1202 if "$ref" in paramdesc:
1203 docs.append(
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001204 (" %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 -07001205 % (
1206 arg,
1207 paramdoc,
1208 required,
1209 repeated,
1210 schema.prettyPrintByName(paramdesc["$ref"]),
1211 )
1212 )
John Asmuth864311d2014-04-24 15:46:08 -04001213 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001214 paramtype = paramdesc.get("type", "string")
1215 docs.append(
1216 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1217 )
1218 enum = paramdesc.get("enum", [])
1219 enumDesc = paramdesc.get("enumDescriptions", [])
1220 if enum and enumDesc:
1221 docs.append(" Allowed values\n")
1222 for (name, desc) in zip(enum, enumDesc):
1223 docs.append(" %s - %s\n" % (name, desc))
1224 if "response" in methodDesc:
1225 if methodName.endswith("_media"):
1226 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001227 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001228 docs.append("\nReturns:\n An object of the form:\n\n ")
1229 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001230
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001231 setattr(method, "__doc__", "".join(docs))
1232 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001233
1234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001235def createNextMethod(
1236 methodName,
1237 pageTokenName="pageToken",
1238 nextPageTokenName="nextPageToken",
1239 isPageTokenParameter=True,
1240):
1241 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001242
1243 The _next methods allow for easy iteration through list() responses.
1244
1245 Args:
1246 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001247 pageTokenName: string, name of request page token field.
1248 nextPageTokenName: string, name of response page token field.
1249 isPageTokenParameter: Boolean, True if request page token is a query
1250 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001251 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001252 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001253
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001254 def methodNext(self, previous_request, previous_response):
1255 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001256
1257Args:
1258 previous_request: The request for the previous page. (required)
1259 previous_response: The response from the request for the previous page. (required)
1260
1261Returns:
1262 A request object that you can call 'execute()' on to request the next
1263 page. Returns None if there are no more items in the collection.
1264 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001265 # Retrieve nextPageToken from previous_response
1266 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001267
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001268 nextPageToken = previous_response.get(nextPageTokenName, None)
1269 if not nextPageToken:
1270 return None
John Asmuth864311d2014-04-24 15:46:08 -04001271
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001272 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001273
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001274 if isPageTokenParameter:
1275 # Replace pageToken value in URI
1276 request.uri = _add_query_parameter(
1277 request.uri, pageTokenName, nextPageToken
1278 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001279 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001280 else:
1281 # Replace pageToken value in request body
1282 model = self._model
1283 body = model.deserialize(request.body)
1284 body[pageTokenName] = nextPageToken
1285 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001286 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001287
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001288 return request
John Asmuth864311d2014-04-24 15:46:08 -04001289
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001290 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001291
1292
1293class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001294 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001295
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001296 def __init__(
1297 self,
1298 http,
1299 baseUrl,
1300 model,
1301 requestBuilder,
1302 developerKey,
1303 resourceDesc,
1304 rootDesc,
1305 schema,
1306 ):
1307 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001308
1309 Args:
1310 http: httplib2.Http, Object to make http requests with.
1311 baseUrl: string, base URL for the API. All requests are relative to this
1312 URI.
1313 model: googleapiclient.Model, converts to and from the wire format.
1314 requestBuilder: class or callable that instantiates an
1315 googleapiclient.HttpRequest object.
1316 developerKey: string, key obtained from
1317 https://code.google.com/apis/console
1318 resourceDesc: object, section of deserialized discovery document that
1319 describes a resource. Note that the top level discovery document
1320 is considered a resource.
1321 rootDesc: object, the entire deserialized discovery document.
1322 schema: object, mapping of schema names to schema descriptions.
1323 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001324 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001325
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001326 self._http = http
1327 self._baseUrl = baseUrl
1328 self._model = model
1329 self._developerKey = developerKey
1330 self._requestBuilder = requestBuilder
1331 self._resourceDesc = resourceDesc
1332 self._rootDesc = rootDesc
1333 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001334
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001335 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001336
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001337 def _set_dynamic_attr(self, attr_name, value):
1338 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001339
1340 Args:
1341 attr_name: string; The name of the attribute to be set
1342 value: The value being set on the object and tracked in the dynamic cache.
1343 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001344 self._dynamic_attrs.append(attr_name)
1345 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001346
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001347 def __getstate__(self):
1348 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001349
1350 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1351 will be wiped and restored on pickle serialization.
1352 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001353 state_dict = copy.copy(self.__dict__)
1354 for dynamic_attr in self._dynamic_attrs:
1355 del state_dict[dynamic_attr]
1356 del state_dict["_dynamic_attrs"]
1357 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001358
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001359 def __setstate__(self, state):
1360 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001361
1362 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1363 will be wiped and restored on pickle serialization.
1364 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001365 self.__dict__.update(state)
1366 self._dynamic_attrs = []
1367 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001368
Bu Sun Kim98888da2020-09-23 11:10:39 -06001369
1370 def __enter__(self):
1371 return self
1372
1373 def __exit__(self, exc_type, exc, exc_tb):
1374 self.close()
1375
1376 def close(self):
1377 """Close httplib2 connections."""
1378 # httplib2 leaves sockets open by default.
1379 # Cleanup using the `close` method.
1380 # https://github.com/httplib2/httplib2/issues/148
Bu Sun Kima9583f72021-03-15 09:12:02 -06001381 self._http.close()
Bu Sun Kim98888da2020-09-23 11:10:39 -06001382
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001383 def _set_service_methods(self):
1384 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1385 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1386 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001387
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001388 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1389 # If this is the root Resource, add a new_batch_http_request() method.
1390 if resourceDesc == rootDesc:
1391 batch_uri = "%s%s" % (
1392 rootDesc["rootUrl"],
1393 rootDesc.get("batchPath", "batch"),
1394 )
1395
1396 def new_batch_http_request(callback=None):
1397 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001398
1399 Args:
1400 callback: callable, A callback to be called for each response, of the
1401 form callback(id, response, exception). The first parameter is the
1402 request id, and the second is the deserialized response object. The
1403 third is an apiclient.errors.HttpError exception object if an HTTP
1404 error occurred while processing the request, or None if no error
1405 occurred.
1406
1407 Returns:
1408 A BatchHttpRequest object based on the discovery document.
1409 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001410 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001411
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001412 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001413
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001414 # Add basic methods to Resource
1415 if "methods" in resourceDesc:
1416 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1417 fixedMethodName, method = createMethod(
1418 methodName, methodDesc, rootDesc, schema
1419 )
1420 self._set_dynamic_attr(
1421 fixedMethodName, method.__get__(self, self.__class__)
1422 )
1423 # Add in _media methods. The functionality of the attached method will
1424 # change when it sees that the method name ends in _media.
1425 if methodDesc.get("supportsMediaDownload", False):
1426 fixedMethodName, method = createMethod(
1427 methodName + "_media", methodDesc, rootDesc, schema
1428 )
1429 self._set_dynamic_attr(
1430 fixedMethodName, method.__get__(self, self.__class__)
1431 )
John Asmuth864311d2014-04-24 15:46:08 -04001432
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001433 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1434 # Add in nested resources
1435 if "resources" in resourceDesc:
1436
1437 def createResourceMethod(methodName, methodDesc):
1438 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001439
1440 Args:
1441 methodName: string, name of the method to use.
1442 methodDesc: object, fragment of deserialized discovery document that
1443 describes the method.
1444 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001445 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001446
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001447 def methodResource(self):
1448 return Resource(
1449 http=self._http,
1450 baseUrl=self._baseUrl,
1451 model=self._model,
1452 developerKey=self._developerKey,
1453 requestBuilder=self._requestBuilder,
1454 resourceDesc=methodDesc,
1455 rootDesc=rootDesc,
1456 schema=schema,
1457 )
John Asmuth864311d2014-04-24 15:46:08 -04001458
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001459 setattr(methodResource, "__doc__", "A collection resource.")
1460 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001461
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001462 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001463
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001464 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1465 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1466 self._set_dynamic_attr(
1467 fixedMethodName, method.__get__(self, self.__class__)
1468 )
John Asmuth864311d2014-04-24 15:46:08 -04001469
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001470 def _add_next_methods(self, resourceDesc, schema):
1471 # Add _next() methods if and only if one of the names 'pageToken' or
1472 # 'nextPageToken' occurs among the fields of both the method's response
1473 # type either the method's request (query parameters) or request body.
1474 if "methods" not in resourceDesc:
1475 return
1476 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1477 nextPageTokenName = _findPageTokenName(
1478 _methodProperties(methodDesc, schema, "response")
1479 )
1480 if not nextPageTokenName:
1481 continue
1482 isPageTokenParameter = True
1483 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1484 if not pageTokenName:
1485 isPageTokenParameter = False
1486 pageTokenName = _findPageTokenName(
1487 _methodProperties(methodDesc, schema, "request")
1488 )
1489 if not pageTokenName:
1490 continue
1491 fixedMethodName, method = createNextMethod(
1492 methodName + "_next",
1493 pageTokenName,
1494 nextPageTokenName,
1495 isPageTokenParameter,
1496 )
1497 self._set_dynamic_attr(
1498 fixedMethodName, method.__get__(self, self.__class__)
1499 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001500
1501
1502def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001503 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001504
1505 Args:
1506 fields: container of string, names of fields.
1507
1508 Returns:
1509 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1510 otherwise None.
1511 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001512 return next(
1513 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1514 )
1515
Thomas Coffee20af04d2017-02-10 15:24:44 -08001516
1517def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001518 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001519
1520 Args:
1521 methodDesc: object, fragment of deserialized discovery document that
1522 describes the method.
1523 schema: object, mapping of schema names to schema descriptions.
1524 name: string, name of top-level field in method description.
1525
1526 Returns:
1527 Object representing fragment of deserialized discovery document
1528 corresponding to 'properties' field of object corresponding to named field
1529 in method description, if it exists, otherwise empty dict.
1530 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001531 desc = methodDesc.get(name, {})
1532 if "$ref" in desc:
1533 desc = schema.get(desc["$ref"], {})
1534 return desc.get("properties", {})