blob: 1b7aedd6fd18867782554d1655da97cd9623ef30 [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
John Asmuth864311d2014-04-24 15:46:08 -040020
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070021__author__ = "jcgregorio@google.com (Joe Gregorio)"
22__all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
John Asmuth864311d2014-04-24 15:46:08 -040023
John Asmuth864311d2014-04-24 15:46:08 -040024# Standard library imports
25import copy
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -070026from collections import OrderedDict
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -040027import collections.abc
28from email.generator import BytesGenerator
John Asmuth864311d2014-04-24 15:46:08 -040029from email.mime.multipart import MIMEMultipart
30from email.mime.nonmultipart import MIMENonMultipart
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -040031import http.client as http_client
32import io
Craig Citro6ae34d72014-08-18 23:10:09 -070033import json
John Asmuth864311d2014-04-24 15:46:08 -040034import keyword
35import logging
36import mimetypes
37import os
38import re
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -040039import urllib
John Asmuth864311d2014-04-24 15:46:08 -040040
41# Third-party imports
42import httplib2
John Asmuth864311d2014-04-24 15:46:08 -040043import uritemplate
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -070044import google.api_core.client_options
arithmetic1728981eadf2020-06-02 10:20:10 -070045from google.auth.transport import mtls
46from google.auth.exceptions import MutualTLSChannelError
47
48try:
49 import google_auth_httplib2
50except ImportError: # pragma: NO COVER
51 google_auth_httplib2 = None
John Asmuth864311d2014-04-24 15:46:08 -040052
53# Local imports
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -080054from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080055from googleapiclient import mimeparse
John Asmuth864311d2014-04-24 15:46:08 -040056from googleapiclient.errors import HttpError
57from googleapiclient.errors import InvalidJsonError
58from googleapiclient.errors import MediaUploadSizeError
59from googleapiclient.errors import UnacceptableMimeTypeError
60from googleapiclient.errors import UnknownApiNameOrVersion
61from googleapiclient.errors import UnknownFileType
Igor Maravić22435292017-01-19 22:28:22 +010062from googleapiclient.http import build_http
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -040063from googleapiclient.http import BatchHttpRequest
Kostyantyn Leschenkobe8b1cb2016-10-17 12:57:21 +030064from googleapiclient.http import HttpMock
65from googleapiclient.http import HttpMockSequence
John Asmuth864311d2014-04-24 15:46:08 -040066from googleapiclient.http import HttpRequest
67from googleapiclient.http import MediaFileUpload
68from googleapiclient.http import MediaUpload
69from googleapiclient.model import JsonModel
70from googleapiclient.model import MediaModel
71from googleapiclient.model import RawModel
72from googleapiclient.schema import Schemas
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070073
Helen Koikede13e3b2018-04-26 16:05:16 -030074from googleapiclient._helpers import _add_query_parameter
75from googleapiclient._helpers import positional
John Asmuth864311d2014-04-24 15:46:08 -040076
77
78# The client library requires a version of httplib2 that supports RETRIES.
79httplib2.RETRIES = 1
80
81logger = logging.getLogger(__name__)
82
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070083URITEMPLATE = re.compile("{[^}]*}")
84VARNAME = re.compile("[a-zA-Z0-9_-]+")
85DISCOVERY_URI = (
86 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
87)
Ethan Bao12b7cd32016-03-14 14:25:10 -070088V1_DISCOVERY_URI = DISCOVERY_URI
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070089V2_DISCOVERY_URI = (
90 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
91)
92DEFAULT_METHOD_DOC = "A description of how to use this function"
93HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
Igor Maravić22435292017-01-19 22:28:22 +010094
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070095_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
96BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
John Asmuth864311d2014-04-24 15:46:08 -040097MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070098 "description": (
99 "The filename of the media request body, or an instance "
100 "of a MediaUpload object."
101 ),
102 "type": "string",
103 "required": False,
John Asmuth864311d2014-04-24 15:46:08 -0400104}
Brian J. Watson38051ac2016-10-25 07:53:08 -0700105MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700106 "description": (
107 "The MIME type of the media request body, or an instance "
108 "of a MediaUpload object."
109 ),
110 "type": "string",
111 "required": False,
Brian J. Watson38051ac2016-10-25 07:53:08 -0700112}
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700113_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
John Asmuth864311d2014-04-24 15:46:08 -0400114
arithmetic17282fc5ca12020-08-27 14:08:12 -0700115# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114.
116GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
117GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
118
John Asmuth864311d2014-04-24 15:46:08 -0400119# Parameters accepted by the stack, but not visible via discovery.
120# TODO(dhermes): Remove 'userip' in 'v2'.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700121STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
122STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
John Asmuth864311d2014-04-24 15:46:08 -0400123
124# Library-specific reserved words beyond Python keywords.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700125RESERVED_WORDS = frozenset(["body"])
John Asmuth864311d2014-04-24 15:46:08 -0400126
Phil Ruffwind26178fc2015-10-13 19:00:33 -0400127# patch _write_lines to avoid munging '\r' into '\n'
128# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
129class _BytesGenerator(BytesGenerator):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700130 _write_lines = BytesGenerator.write
131
John Asmuth864311d2014-04-24 15:46:08 -0400132
133def fix_method_name(name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700134 """Fix method names to avoid '$' characters and reserved word conflicts.
John Asmuth864311d2014-04-24 15:46:08 -0400135
136 Args:
137 name: string, method name.
138
139 Returns:
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700140 The name with '_' appended if the name is a reserved word and '$' and '-'
arithmetic1728981eadf2020-06-02 10:20:10 -0700141 replaced with '_'.
John Asmuth864311d2014-04-24 15:46:08 -0400142 """
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700143 name = name.replace("$", "_").replace("-", "_")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700144 if keyword.iskeyword(name) or name in RESERVED_WORDS:
145 return name + "_"
146 else:
147 return name
John Asmuth864311d2014-04-24 15:46:08 -0400148
149
150def key2param(key):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700151 """Converts key names into parameter names.
John Asmuth864311d2014-04-24 15:46:08 -0400152
153 For example, converting "max-results" -> "max_results"
154
155 Args:
156 key: string, the method key name.
157
158 Returns:
159 A safe method name based on the key name.
160 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700161 result = []
162 key = list(key)
163 if not key[0].isalpha():
164 result.append("x")
165 for c in key:
166 if c.isalnum():
167 result.append(c)
168 else:
169 result.append("_")
John Asmuth864311d2014-04-24 15:46:08 -0400170
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700171 return "".join(result)
John Asmuth864311d2014-04-24 15:46:08 -0400172
173
174@positional(2)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700175def build(
176 serviceName,
177 version,
178 http=None,
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400179 discoveryServiceUrl=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700180 developerKey=None,
181 model=None,
182 requestBuilder=HttpRequest,
183 credentials=None,
184 cache_discovery=True,
185 cache=None,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700186 client_options=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700187 adc_cert_path=None,
188 adc_key_path=None,
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700189 num_retries=1,
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400190 static_discovery=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700191):
192 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400193
194 Construct a Resource object for interacting with an API. The serviceName and
195 version are the names from the Discovery service.
196
197 Args:
198 serviceName: string, name of the service.
199 version: string, the version of the service.
200 http: httplib2.Http, An instance of httplib2.Http or something that acts
201 like it that HTTP requests will be made through.
202 discoveryServiceUrl: string, a URI Template that points to the location of
203 the discovery service. It should have two parameters {api} and
204 {apiVersion} that when filled in produce an absolute URI to the discovery
205 document for that service.
206 developerKey: string, key obtained from
207 https://code.google.com/apis/console.
208 model: googleapiclient.Model, converts to and from the wire format.
209 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
210 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800211 credentials: oauth2client.Credentials or
212 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700213 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700214 cache_discovery: Boolean, whether or not to cache the discovery doc.
215 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
216 cache object for the discovery documents.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300217 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700218 options to set user options on the client.
219 (1) The API endpoint should be set through client_options. If API endpoint
220 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
221 to control which endpoint to use.
222 (2) client_cert_source is not supported, client cert should be provided using
223 client_encrypted_cert_source instead. In order to use the provided client
224 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
225 set to `true`.
226 More details on the environment variables are here:
227 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700228 adc_cert_path: str, client certificate file path to save the application
229 default client certificate for mTLS. This field is required if you want to
arithmetic17282fc5ca12020-08-27 14:08:12 -0700230 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
231 environment variable must be set to `true` in order to use this field,
232 otherwise this field doesn't nothing.
233 More details on the environment variables are here:
234 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700235 adc_key_path: str, client encrypted private key file path to save the
236 application default client encrypted private key for mTLS. This field is
237 required if you want to use the default client certificate.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700238 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
239 `true` in order to use this field, otherwise this field doesn't nothing.
240 More details on the environment variables are here:
241 https://google.aip.dev/auth/4114
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700242 num_retries: Integer, number of times to retry discovery with
243 randomized exponential backoff in case of intermittent/connection issues.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500244 static_discovery: Boolean, whether or not to use the static discovery docs
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400245 included in the library. The default value for `static_discovery` depends
246 on the value of `discoveryServiceUrl`. `static_discovery` will default to
247 `True` when `discoveryServiceUrl` is also not provided, otherwise it will
248 default to `False`.
John Asmuth864311d2014-04-24 15:46:08 -0400249
250 Returns:
251 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700252
253 Raises:
254 google.auth.exceptions.MutualTLSChannelError: if there are any problems
255 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400256 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700257 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400258
Anthonios Partheniou3b4f2e22021-03-19 11:36:01 -0400259 # The default value for `static_discovery` depends on the value of
260 # `discoveryServiceUrl`. `static_discovery` will default to `True` when
261 # `discoveryServiceUrl` is also not provided, otherwise it will default to
262 # `False`. This is added for backwards compatability with
263 # google-api-python-client 1.x which does not support the `static_discovery`
264 # parameter.
265 if static_discovery is None:
266 if discoveryServiceUrl is None:
267 static_discovery = True
268 else:
269 static_discovery = False
270
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700271 if http is None:
272 discovery_http = build_http()
273 else:
274 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400275
Bu Sun Kim98888da2020-09-23 11:10:39 -0600276 service = None
277
Bu Sun Kim790e7022020-09-11 20:18:06 -0600278 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700279 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400280
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700281 try:
282 content = _retrieve_discovery_doc(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600283 requested_url,
284 discovery_http,
285 cache_discovery,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500286 serviceName,
287 version,
Bu Sun Kim790e7022020-09-11 20:18:06 -0600288 cache,
289 developerKey,
290 num_retries=num_retries,
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500291 static_discovery=static_discovery,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700292 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600293 service = build_from_document(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700294 content,
295 base=discovery_url,
296 http=http,
297 developerKey=developerKey,
298 model=model,
299 requestBuilder=requestBuilder,
300 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700301 client_options=client_options,
302 adc_cert_path=adc_cert_path,
303 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700304 )
Bu Sun Kim98888da2020-09-23 11:10:39 -0600305 break # exit if a service was created
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700306 except HttpError as e:
307 if e.resp.status == http_client.NOT_FOUND:
308 continue
309 else:
310 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700311
Bu Sun Kim98888da2020-09-23 11:10:39 -0600312 # If discovery_http was created by this function, we are done with it
313 # and can safely close it
314 if http is None:
315 discovery_http.close()
316
317 if service is None:
318 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
319 else:
320 return service
Takashi Matsuo30125122015-08-19 11:42:32 -0700321
322
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700323def _discovery_service_uri_options(discoveryServiceUrl, version):
324 """
325 Returns Discovery URIs to be used for attemnting to build the API Resource.
326
327 Args:
328 discoveryServiceUrl:
329 string, the Original Discovery Service URL preferred by the customer.
330 version:
331 string, API Version requested
332
333 Returns:
334 A list of URIs to be tried for the Service Discovery, in order.
335 """
336
Alex1c4d1992021-04-29 04:04:06 -0700337 if discoveryServiceUrl is not None:
338 return [discoveryServiceUrl]
339 if version is None:
340 # V1 Discovery won't work if the requested version is None
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700341 logger.warning(
Bu Sun Kim790e7022020-09-11 20:18:06 -0600342 "Discovery V1 does not support empty versions. Defaulting to V2..."
343 )
Alex1c4d1992021-04-29 04:04:06 -0700344 return [V2_DISCOVERY_URI]
345 else:
346 return [DISCOVERY_URI, V2_DISCOVERY_URI]
Dmitry Frenkelcd4e8f42020-07-30 12:34:03 -0700347
348
Bu Sun Kim790e7022020-09-11 20:18:06 -0600349def _retrieve_discovery_doc(
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500350 url,
351 http,
352 cache_discovery,
353 serviceName,
354 version,
355 cache=None,
356 developerKey=None,
357 num_retries=1,
358 static_discovery=True
Bu Sun Kim790e7022020-09-11 20:18:06 -0600359):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700360 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700361
362 Args:
363 url: string, the URL of the discovery document.
364 http: httplib2.Http, An instance of httplib2.Http or something that acts
365 like it through which HTTP requests will be made.
366 cache_discovery: Boolean, whether or not to cache the discovery doc.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500367 serviceName: string, name of the service.
368 version: string, the version of the service.
Takashi Matsuo30125122015-08-19 11:42:32 -0700369 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
370 object for the discovery documents.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700371 developerKey: string, Key for controlling API usage, generated
372 from the API Console.
373 num_retries: Integer, number of times to retry discovery with
374 randomized exponential backoff in case of intermittent/connection issues.
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500375 static_discovery: Boolean, whether or not to use the static discovery docs
376 included in the library.
Takashi Matsuo30125122015-08-19 11:42:32 -0700377
378 Returns:
379 A unicode string representation of the discovery document.
380 """
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500381 from . import discovery_cache
Takashi Matsuo30125122015-08-19 11:42:32 -0700382
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500383 if cache_discovery:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700384 if cache is None:
385 cache = discovery_cache.autodetect()
386 if cache:
387 content = cache.get(url)
388 if content:
389 return content
John Asmuth864311d2014-04-24 15:46:08 -0400390
Anthonios Partheniou32d1c592021-01-14 18:48:59 -0500391 # When `static_discovery=True`, use static discovery artifacts included
392 # with the library
393 if static_discovery:
394 content = discovery_cache.get_static_doc(serviceName, version)
395 if content:
396 return content
397 else:
398 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
399
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700400 actual_url = url
401 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
402 # variable that contains the network address of the client sending the
403 # request. If it exists then add that to the request for the discovery
404 # document to avoid exceeding the quota on discovery requests.
405 if "REMOTE_ADDR" in os.environ:
406 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
407 if developerKey:
408 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700409 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400410
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700411 # Execute this request with retries build into HttpRequest
412 # Note that it will already raise an error if we don't get a 2xx response
413 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
414 resp, content = req.execute(num_retries=num_retries)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800415
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700416 try:
417 content = content.decode("utf-8")
418 except AttributeError:
419 pass
420
421 try:
422 service = json.loads(content)
423 except ValueError as e:
424 logger.error("Failed to parse as JSON: " + content)
425 raise InvalidJsonError()
426 if cache_discovery and cache:
427 cache.set(url, content)
428 return content
John Asmuth864311d2014-04-24 15:46:08 -0400429
430
431@positional(1)
432def build_from_document(
433 service,
434 base=None,
435 future=None,
436 http=None,
437 developerKey=None,
438 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700439 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700440 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700441 client_options=None,
442 adc_cert_path=None,
443 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700444):
445 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400446
447 Same as `build()`, but constructs the Resource object from a discovery
448 document that is it given, as opposed to retrieving one over HTTP.
449
450 Args:
451 service: string or object, the JSON discovery document describing the API.
452 The value passed in may either be the JSON string or the deserialized
453 JSON.
454 base: string, base URI for all HTTP requests, usually the discovery URI.
455 This parameter is no longer used as rootUrl and servicePath are included
456 within the discovery document. (deprecated)
457 future: string, discovery document with future capabilities (deprecated).
458 http: httplib2.Http, An instance of httplib2.Http or something that acts
459 like it that HTTP requests will be made through.
460 developerKey: string, Key for controlling API usage, generated
461 from the API Console.
462 model: Model class instance that serializes and de-serializes requests and
463 responses.
464 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800465 credentials: oauth2client.Credentials or
466 google.auth.credentials.Credentials, credentials to be used for
467 authentication.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300468 client_options: Mapping object or google.api_core.client_options, client
arithmetic17282fc5ca12020-08-27 14:08:12 -0700469 options to set user options on the client.
470 (1) The API endpoint should be set through client_options. If API endpoint
471 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
472 to control which endpoint to use.
473 (2) client_cert_source is not supported, client cert should be provided using
474 client_encrypted_cert_source instead. In order to use the provided client
475 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
476 set to `true`.
477 More details on the environment variables are here:
478 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700479 adc_cert_path: str, client certificate file path to save the application
480 default client certificate for mTLS. This field is required if you want to
arithmetic17282fc5ca12020-08-27 14:08:12 -0700481 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
482 environment variable must be set to `true` in order to use this field,
483 otherwise this field doesn't nothing.
484 More details on the environment variables are here:
485 https://google.aip.dev/auth/4114
arithmetic1728981eadf2020-06-02 10:20:10 -0700486 adc_key_path: str, client encrypted private key file path to save the
487 application default client encrypted private key for mTLS. This field is
488 required if you want to use the default client certificate.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700489 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
490 `true` in order to use this field, otherwise this field doesn't nothing.
491 More details on the environment variables are here:
492 https://google.aip.dev/auth/4114
John Asmuth864311d2014-04-24 15:46:08 -0400493
494 Returns:
495 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700496
497 Raises:
498 google.auth.exceptions.MutualTLSChannelError: if there are any problems
499 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400500 """
501
Bu Sun Kim790e7022020-09-11 20:18:06 -0600502 if client_options is None:
503 client_options = google.api_core.client_options.ClientOptions()
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400504 if isinstance(client_options, collections.abc.Mapping):
Bu Sun Kim790e7022020-09-11 20:18:06 -0600505 client_options = google.api_core.client_options.from_dict(client_options)
506
507 if http is not None:
508 # if http is passed, the user cannot provide credentials
509 banned_options = [
510 (credentials, "credentials"),
511 (client_options.credentials_file, "client_options.credentials_file"),
512 ]
513 for option, name in banned_options:
514 if option is not None:
515 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
John Asmuth864311d2014-04-24 15:46:08 -0400516
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400517 if isinstance(service, str):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700518 service = json.loads(service)
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400519 elif isinstance(service, bytes):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700520 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400521
arithmetic1728981eadf2020-06-02 10:20:10 -0700522 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700523 logger.error(
524 "You are using HttpMock or HttpMockSequence without"
525 + "having the service discovery doc in cache. Try calling "
526 + "build() without mocking once first to populate the "
527 + "cache."
528 )
529 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400530
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700531 # If an API Endpoint is provided on client options, use that as the base URL
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400532 base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim790e7022020-09-11 20:18:06 -0600533 if client_options.api_endpoint:
534 base = client_options.api_endpoint
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700535
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700536 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400537
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700538 # If the http client is not specified, then we must construct an http client
539 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800540 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700541 if http is None:
542 # Does the service require scopes?
543 scopes = list(
544 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
545 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700546
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700547 # If so, then the we need to setup authentication if no developerKey is
548 # specified.
549 if scopes and not developerKey:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600550 # Make sure the user didn't pass multiple credentials
551 if client_options.credentials_file and credentials:
552 raise google.api_core.exceptions.DuplicateCredentialArgs(
553 "client_options.credentials_file and credentials are mutually exclusive."
554 )
555 # Check for credentials file via client options
556 if client_options.credentials_file:
557 credentials = _auth.credentials_from_file(
558 client_options.credentials_file,
559 scopes=client_options.scopes,
560 quota_project_id=client_options.quota_project_id,
561 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700562 # If the user didn't pass in credentials, attempt to acquire application
563 # default credentials.
564 if credentials is None:
Bu Sun Kim790e7022020-09-11 20:18:06 -0600565 credentials = _auth.default_credentials(
566 scopes=client_options.scopes,
567 quota_project_id=client_options.quota_project_id,
568 )
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800569
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700570 # The credentials need to be scoped.
Bu Sun Kim790e7022020-09-11 20:18:06 -0600571 # If the user provided scopes via client_options don't override them
572 if not client_options.scopes:
573 credentials = _auth.with_scopes(credentials, scopes)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700574
575 # If credentials are provided, create an authorized http instance;
576 # otherwise, skip authentication.
577 if credentials:
578 http = _auth.authorized_http(credentials)
579
580 # If the service doesn't require scopes then there is no need for
581 # authentication.
582 else:
583 http = build_http()
584
arithmetic1728981eadf2020-06-02 10:20:10 -0700585 # Obtain client cert and create mTLS http channel if cert exists.
586 client_cert_to_use = None
arithmetic17282fc5ca12020-08-27 14:08:12 -0700587 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
588 if not use_client_cert in ("true", "false"):
589 raise MutualTLSChannelError(
590 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
591 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700592 if client_options and client_options.client_cert_source:
593 raise MutualTLSChannelError(
594 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
595 )
arithmetic17282fc5ca12020-08-27 14:08:12 -0700596 if use_client_cert == "true":
597 if (
598 client_options
599 and hasattr(client_options, "client_encrypted_cert_source")
600 and client_options.client_encrypted_cert_source
601 ):
602 client_cert_to_use = client_options.client_encrypted_cert_source
Bu Sun Kim790e7022020-09-11 20:18:06 -0600603 elif (
604 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
605 ):
arithmetic17282fc5ca12020-08-27 14:08:12 -0700606 client_cert_to_use = mtls.default_client_encrypted_cert_source(
607 adc_cert_path, adc_key_path
608 )
arithmetic1728981eadf2020-06-02 10:20:10 -0700609 if client_cert_to_use:
610 cert_path, key_path, passphrase = client_cert_to_use()
611
612 # The http object we built could be google_auth_httplib2.AuthorizedHttp
613 # or httplib2.Http. In the first case we need to extract the wrapped
614 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
615 http_channel = (
616 http.http
617 if google_auth_httplib2
618 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
619 else http
620 )
621 http_channel.add_certificate(key_path, cert_path, "", passphrase)
622
623 # If user doesn't provide api endpoint via client options, decide which
624 # api endpoint to use.
625 if "mtlsRootUrl" in service and (
626 not client_options or not client_options.api_endpoint
627 ):
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400628 mtls_endpoint = urllib.parse.urljoin(service["mtlsRootUrl"], service["servicePath"])
arithmetic17282fc5ca12020-08-27 14:08:12 -0700629 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
arithmetic1728981eadf2020-06-02 10:20:10 -0700630
arithmetic17282fc5ca12020-08-27 14:08:12 -0700631 if not use_mtls_endpoint in ("never", "auto", "always"):
arithmetic1728981eadf2020-06-02 10:20:10 -0700632 raise MutualTLSChannelError(
arithmetic17282fc5ca12020-08-27 14:08:12 -0700633 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
arithmetic1728981eadf2020-06-02 10:20:10 -0700634 )
635
arithmetic172819908ed2020-06-09 22:32:43 -0700636 # Switch to mTLS endpoint, if environment variable is "always", or
637 # environment varibable is "auto" and client cert exists.
arithmetic17282fc5ca12020-08-27 14:08:12 -0700638 if use_mtls_endpoint == "always" or (
639 use_mtls_endpoint == "auto" and client_cert_to_use
arithmetic1728981eadf2020-06-02 10:20:10 -0700640 ):
641 base = mtls_endpoint
642
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700643 if model is None:
644 features = service.get("features", [])
645 model = JsonModel("dataWrapper" in features)
646
647 return Resource(
648 http=http,
649 baseUrl=base,
650 model=model,
651 developerKey=developerKey,
652 requestBuilder=requestBuilder,
653 resourceDesc=service,
654 rootDesc=service,
655 schema=schema,
656 )
John Asmuth864311d2014-04-24 15:46:08 -0400657
658
659def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700660 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400661
662 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
663 JSON Schema.
664
665 Args:
666 value: any, the value to convert
667 schema_type: string, the type that value should be interpreted as
668
669 Returns:
670 A string representation of 'value' based on the schema_type.
671 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700672 if schema_type == "string":
673 if type(value) == type("") or type(value) == type(u""):
674 return value
675 else:
676 return str(value)
677 elif schema_type == "integer":
678 return str(int(value))
679 elif schema_type == "number":
680 return str(float(value))
681 elif schema_type == "boolean":
682 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400683 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700684 if type(value) == type("") or type(value) == type(u""):
685 return value
686 else:
687 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400688
689
690def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700691 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400692
693 Args:
694 maxSize: string, size as a string, such as 2MB or 7GB.
695
696 Returns:
697 The size as an integer value.
698 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700699 if len(maxSize) < 2:
700 return 0
701 units = maxSize[-2:].upper()
702 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
703 if bit_shift is not None:
704 return int(maxSize[:-2]) << bit_shift
705 else:
706 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400707
708
709def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700710 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400711
712 Constructed using the API root URI and service path from the discovery
713 document and the relative path for the API method.
714
715 Args:
716 root_desc: Dictionary; the entire original deserialized discovery document.
717 path_url: String; the relative URL for the API method. Relative to the API
718 root, which is specified in the discovery document.
719
720 Returns:
721 String; the absolute URI for media upload for the API method.
722 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700723 return "%(root)supload/%(service_path)s%(path)s" % {
724 "root": root_desc["rootUrl"],
725 "service_path": root_desc["servicePath"],
726 "path": path_url,
727 }
John Asmuth864311d2014-04-24 15:46:08 -0400728
729
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900730def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700731 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400732
733 Specifically, adds whatever global parameters are specified by the API to the
734 parameters for the individual method. Also adds parameters which don't
735 appear in the discovery document, but are available to all discovery based
736 APIs (these are listed in STACK_QUERY_PARAMETERS).
737
738 SIDE EFFECTS: This updates the parameters dictionary object in the method
739 description.
740
741 Args:
742 method_desc: Dictionary with metadata describing an API method. Value comes
743 from the dictionary of methods stored in the 'methods' key in the
744 deserialized discovery document.
745 root_desc: Dictionary; the entire original deserialized discovery document.
746 http_method: String; the HTTP method used to call the API method described
747 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900748 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400749
750 Returns:
751 The updated Dictionary stored in the 'parameters' key of the method
752 description dictionary.
753 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700754 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400755
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700756 # Add in the parameters common to all methods.
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400757 for name, description in root_desc.get("parameters", {}).items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700758 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400759
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700760 # Add in undocumented query parameters.
761 for name in STACK_QUERY_PARAMETERS:
762 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400763
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700764 # Add 'body' (our own reserved word) to parameters if the method supports
765 # a request payload.
766 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
767 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
768 body.update(method_desc["request"])
769 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400770
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700771 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400772
773
774def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700775 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400776
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700777 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
778 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400779
780 Args:
781 method_desc: Dictionary with metadata describing an API method. Value comes
782 from the dictionary of methods stored in the 'methods' key in the
783 deserialized discovery document.
784 root_desc: Dictionary; the entire original deserialized discovery document.
785 path_url: String; the relative URL for the API method. Relative to the API
786 root, which is specified in the discovery document.
787 parameters: A dictionary describing method parameters for method described
788 in method_desc.
789
790 Returns:
791 Triple (accept, max_size, media_path_url) where:
792 - accept is a list of strings representing what content types are
793 accepted for media upload. Defaults to empty list if not in the
794 discovery document.
795 - max_size is a long representing the max size in bytes allowed for a
796 media upload. Defaults to 0L if not in the discovery document.
797 - media_path_url is a String; the absolute URI for media upload for the
798 API method. Constructed using the API root URI and service path from
799 the discovery document and the relative path for the API method. If
800 media upload is not supported, this is None.
801 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700802 media_upload = method_desc.get("mediaUpload", {})
803 accept = media_upload.get("accept", [])
804 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
805 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400806
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700807 if media_upload:
808 media_path_url = _media_path_url_from_info(root_desc, path_url)
809 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
810 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400811
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700812 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400813
814
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900815def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700816 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400817
818 SIDE EFFECTS: Changes the parameters dictionary in the method description with
819 extra parameters which are used locally.
820
821 Args:
822 method_desc: Dictionary with metadata describing an API method. Value comes
823 from the dictionary of methods stored in the 'methods' key in the
824 deserialized discovery document.
825 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900826 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400827
828 Returns:
829 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
830 where:
831 - path_url is a String; the relative URL for the API method. Relative to
832 the API root, which is specified in the discovery document.
833 - http_method is a String; the HTTP method used to call the API method
834 described in the method description.
835 - method_id is a String; the name of the RPC method associated with the
836 API method, and is in the method description in the 'id' key.
837 - accept is a list of strings representing what content types are
838 accepted for media upload. Defaults to empty list if not in the
839 discovery document.
840 - max_size is a long representing the max size in bytes allowed for a
841 media upload. Defaults to 0L if not in the discovery document.
842 - media_path_url is a String; the absolute URI for media upload for the
843 API method. Constructed using the API root URI and service path from
844 the discovery document and the relative path for the API method. If
845 media upload is not supported, this is None.
846 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700847 path_url = method_desc["path"]
848 http_method = method_desc["httpMethod"]
849 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400850
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700851 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
852 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
853 # 'parameters' key and needs to know if there is a 'body' parameter because it
854 # also sets a 'media_body' parameter.
855 accept, max_size, media_path_url = _fix_up_media_upload(
856 method_desc, root_desc, path_url, parameters
857 )
John Asmuth864311d2014-04-24 15:46:08 -0400858
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700859 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400860
861
Craig Citro7ee535d2015-02-23 10:11:14 -0800862def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700863 """Custom urljoin replacement supporting : before / in url."""
864 # In general, it's unsafe to simply join base and url. However, for
865 # the case of discovery documents, we know:
866 # * base will never contain params, query, or fragment
867 # * url will never contain a scheme or net_loc.
868 # In general, this means we can safely join on /; we just need to
869 # ensure we end up with precisely one / joining base and url. The
870 # exception here is the case of media uploads, where url will be an
871 # absolute url.
872 if url.startswith("http://") or url.startswith("https://"):
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400873 return urllib.parse.urljoin(base, url)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700874 new_base = base if base.endswith("/") else base + "/"
875 new_url = url[1:] if url.startswith("/") else url
876 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800877
878
John Asmuth864311d2014-04-24 15:46:08 -0400879# TODO(dhermes): Convert this class to ResourceMethod and make it callable
880class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700881 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400882
883 Attributes:
884 argmap: Map from method parameter name (string) to query parameter name
885 (string).
886 required_params: List of required parameters (represented by parameter
887 name as string).
888 repeated_params: List of repeated parameters (represented by parameter
889 name as string).
890 pattern_params: Map from method parameter name (string) to regular
891 expression (as a string). If the pattern is set for a parameter, the
892 value for that parameter must match the regular expression.
893 query_params: List of parameters (represented by parameter name as string)
894 that will be used in the query string.
895 path_params: Set of parameters (represented by parameter name as string)
896 that will be used in the base URL path.
897 param_types: Map from method parameter name (string) to parameter type. Type
898 can be any valid JSON schema type; valid values are 'any', 'array',
899 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
900 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
901 enum_params: Map from method parameter name (string) to list of strings,
902 where each list of strings is the list of acceptable enum values.
903 """
904
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700905 def __init__(self, method_desc):
906 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400907
908 Sets default values and defers to set_parameters to populate.
909
910 Args:
911 method_desc: Dictionary with metadata describing an API method. Value
912 comes from the dictionary of methods stored in the 'methods' key in
913 the deserialized discovery document.
914 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700915 self.argmap = {}
916 self.required_params = []
917 self.repeated_params = []
918 self.pattern_params = {}
919 self.query_params = []
920 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
921 # parsing is gotten rid of.
922 self.path_params = set()
923 self.param_types = {}
924 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400925
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700926 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400927
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700928 def set_parameters(self, method_desc):
929 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400930
931 Iterates through each parameter for the method and parses the values from
932 the parameter dictionary.
933
934 Args:
935 method_desc: Dictionary with metadata describing an API method. Value
936 comes from the dictionary of methods stored in the 'methods' key in
937 the deserialized discovery document.
938 """
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -0500939 parameters = method_desc.get("parameters", {})
940 sorted_parameters = OrderedDict(sorted(parameters.items()))
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400941 for arg, desc in sorted_parameters.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700942 param = key2param(arg)
943 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400944
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700945 if desc.get("pattern"):
946 self.pattern_params[param] = desc["pattern"]
947 if desc.get("enum"):
948 self.enum_params[param] = desc["enum"]
949 if desc.get("required"):
950 self.required_params.append(param)
951 if desc.get("repeated"):
952 self.repeated_params.append(param)
953 if desc.get("location") == "query":
954 self.query_params.append(param)
955 if desc.get("location") == "path":
956 self.path_params.add(param)
957 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400958
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700959 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
960 # should have all path parameters already marked with
961 # 'location: path'.
962 for match in URITEMPLATE.finditer(method_desc["path"]):
963 for namematch in VARNAME.finditer(match.group(0)):
964 name = key2param(namematch.group(0))
965 self.path_params.add(name)
966 if name in self.query_params:
967 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400968
969
970def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700971 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400972
973 Args:
974 methodName: string, name of the method to use.
975 methodDesc: object, fragment of deserialized discovery document that
976 describes the method.
977 rootDesc: object, the entire deserialized discovery document.
978 schema: object, mapping of schema names to schema descriptions.
979 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700980 methodName = fix_method_name(methodName)
981 (
982 pathUrl,
983 httpMethod,
984 methodId,
985 accept,
986 maxSize,
987 mediaPathUrl,
988 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400989
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700990 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400991
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700992 def method(self, **kwargs):
993 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400994
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400995 for name in kwargs:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700996 if name not in parameters.argmap:
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400997 raise TypeError('Got an unexpected keyword argument {}'.format(name))
John Asmuth864311d2014-04-24 15:46:08 -0400998
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700999 # Remove args that have a value of None.
1000 keys = list(kwargs.keys())
1001 for name in keys:
1002 if kwargs[name] is None:
1003 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -04001004
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001005 for name in parameters.required_params:
1006 if name not in kwargs:
1007 # temporary workaround for non-paging methods incorrectly requiring
1008 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
1009 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
1010 _methodProperties(methodDesc, schema, "response")
1011 ):
1012 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -04001013
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001014 for name, regex in parameters.pattern_params.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001015 if name in kwargs:
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001016 if isinstance(kwargs[name], str):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001017 pvalues = [kwargs[name]]
1018 else:
1019 pvalues = kwargs[name]
1020 for pvalue in pvalues:
1021 if re.match(regex, pvalue) is None:
1022 raise TypeError(
1023 'Parameter "%s" value "%s" does not match the pattern "%s"'
1024 % (name, pvalue, regex)
1025 )
1026
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001027 for name, enums in parameters.enum_params.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001028 if name in kwargs:
1029 # We need to handle the case of a repeated enum
1030 # name differently, since we want to handle both
1031 # arg='value' and arg=['value1', 'value2']
1032 if name in parameters.repeated_params and not isinstance(
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001033 kwargs[name], str
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001034 ):
1035 values = kwargs[name]
1036 else:
1037 values = [kwargs[name]]
1038 for value in values:
1039 if value not in enums:
1040 raise TypeError(
1041 'Parameter "%s" value "%s" is not an allowed value in "%s"'
1042 % (name, value, str(enums))
1043 )
1044
1045 actual_query_params = {}
1046 actual_path_params = {}
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001047 for key, value in kwargs.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001048 to_type = parameters.param_types.get(key, "string")
1049 # For repeated parameters we cast each member of the list.
1050 if key in parameters.repeated_params and type(value) == type([]):
1051 cast_value = [_cast(x, to_type) for x in value]
1052 else:
1053 cast_value = _cast(value, to_type)
1054 if key in parameters.query_params:
1055 actual_query_params[parameters.argmap[key]] = cast_value
1056 if key in parameters.path_params:
1057 actual_path_params[parameters.argmap[key]] = cast_value
1058 body_value = kwargs.get("body", None)
1059 media_filename = kwargs.get("media_body", None)
1060 media_mime_type = kwargs.get("media_mime_type", None)
1061
1062 if self._developerKey:
1063 actual_query_params["key"] = self._developerKey
1064
1065 model = self._model
1066 if methodName.endswith("_media"):
1067 model = MediaModel()
1068 elif "response" not in methodDesc:
1069 model = RawModel()
1070
1071 headers = {}
1072 headers, params, query, body = model.request(
1073 headers, actual_path_params, actual_query_params, body_value
1074 )
1075
1076 expanded_url = uritemplate.expand(pathUrl, params)
1077 url = _urljoin(self._baseUrl, expanded_url + query)
1078
1079 resumable = None
1080 multipart_boundary = ""
1081
1082 if media_filename:
1083 # Ensure we end up with a valid MediaUpload object.
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001084 if isinstance(media_filename, str):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001085 if media_mime_type is None:
1086 logger.warning(
1087 "media_mime_type argument not specified: trying to auto-detect for %s",
1088 media_filename,
1089 )
1090 media_mime_type, _ = mimetypes.guess_type(media_filename)
1091 if media_mime_type is None:
1092 raise UnknownFileType(media_filename)
1093 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
1094 raise UnacceptableMimeTypeError(media_mime_type)
1095 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
1096 elif isinstance(media_filename, MediaUpload):
1097 media_upload = media_filename
1098 else:
1099 raise TypeError("media_filename must be str or MediaUpload.")
1100
1101 # Check the maxSize
1102 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
1103 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
1104
1105 # Use the media path uri for media uploads
1106 expanded_url = uritemplate.expand(mediaPathUrl, params)
1107 url = _urljoin(self._baseUrl, expanded_url + query)
1108 if media_upload.resumable():
1109 url = _add_query_parameter(url, "uploadType", "resumable")
1110
1111 if media_upload.resumable():
1112 # This is all we need to do for resumable, if the body exists it gets
1113 # sent in the first request, otherwise an empty body is sent.
1114 resumable = media_upload
1115 else:
1116 # A non-resumable upload
1117 if body is None:
1118 # This is a simple media upload
1119 headers["content-type"] = media_upload.mimetype()
1120 body = media_upload.getbytes(0, media_upload.size())
1121 url = _add_query_parameter(url, "uploadType", "media")
1122 else:
1123 # This is a multipart/related upload.
1124 msgRoot = MIMEMultipart("related")
1125 # msgRoot should not write out it's own headers
1126 setattr(msgRoot, "_write_headers", lambda self: None)
1127
1128 # attach the body as one part
1129 msg = MIMENonMultipart(*headers["content-type"].split("/"))
1130 msg.set_payload(body)
1131 msgRoot.attach(msg)
1132
1133 # attach the media as the second part
1134 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
1135 msg["Content-Transfer-Encoding"] = "binary"
1136
1137 payload = media_upload.getbytes(0, media_upload.size())
1138 msg.set_payload(payload)
1139 msgRoot.attach(msg)
1140 # encode the body: note that we can't use `as_string`, because
1141 # it plays games with `From ` lines.
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001142 fp = io.BytesIO()
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001143 g = _BytesGenerator(fp, mangle_from_=False)
1144 g.flatten(msgRoot, unixfrom=False)
1145 body = fp.getvalue()
1146
1147 multipart_boundary = msgRoot.get_boundary()
1148 headers["content-type"] = (
1149 "multipart/related; " 'boundary="%s"'
1150 ) % multipart_boundary
1151 url = _add_query_parameter(url, "uploadType", "multipart")
1152
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001153 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001154 return self._requestBuilder(
1155 self._http,
1156 model.response,
1157 url,
1158 method=httpMethod,
1159 body=body,
1160 headers=headers,
1161 methodId=methodId,
1162 resumable=resumable,
1163 )
1164
1165 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1166 if len(parameters.argmap) > 0:
1167 docs.append("Args:\n")
1168
1169 # Skip undocumented params and params common to all methods.
1170 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1171 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1172
1173 all_args = list(parameters.argmap.keys())
1174 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1175
1176 # Move body to the front of the line.
1177 if "body" in all_args:
1178 args_ordered.append("body")
1179
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001180 for name in sorted(all_args):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001181 if name not in args_ordered:
1182 args_ordered.append(name)
1183
1184 for arg in args_ordered:
1185 if arg in skip_parameters:
1186 continue
1187
1188 repeated = ""
1189 if arg in parameters.repeated_params:
1190 repeated = " (repeated)"
1191 required = ""
1192 if arg in parameters.required_params:
1193 required = " (required)"
1194 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1195 paramdoc = paramdesc.get("description", "A parameter")
1196 if "$ref" in paramdesc:
1197 docs.append(
Anthonios Parthenioub1b0c832020-12-14 14:24:19 -05001198 (" %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 -07001199 % (
1200 arg,
1201 paramdoc,
1202 required,
1203 repeated,
1204 schema.prettyPrintByName(paramdesc["$ref"]),
1205 )
1206 )
John Asmuth864311d2014-04-24 15:46:08 -04001207 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001208 paramtype = paramdesc.get("type", "string")
1209 docs.append(
1210 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1211 )
1212 enum = paramdesc.get("enum", [])
1213 enumDesc = paramdesc.get("enumDescriptions", [])
1214 if enum and enumDesc:
1215 docs.append(" Allowed values\n")
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001216 for (name, desc) in zip(enum, enumDesc):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001217 docs.append(" %s - %s\n" % (name, desc))
1218 if "response" in methodDesc:
1219 if methodName.endswith("_media"):
1220 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001221 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001222 docs.append("\nReturns:\n An object of the form:\n\n ")
1223 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001224
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001225 setattr(method, "__doc__", "".join(docs))
1226 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001227
1228
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001229def createNextMethod(
1230 methodName,
1231 pageTokenName="pageToken",
1232 nextPageTokenName="nextPageToken",
1233 isPageTokenParameter=True,
1234):
1235 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001236
1237 The _next methods allow for easy iteration through list() responses.
1238
1239 Args:
1240 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001241 pageTokenName: string, name of request page token field.
1242 nextPageTokenName: string, name of response page token field.
1243 isPageTokenParameter: Boolean, True if request page token is a query
1244 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001245 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001246 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001247
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001248 def methodNext(self, previous_request, previous_response):
1249 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001250
1251Args:
1252 previous_request: The request for the previous page. (required)
1253 previous_response: The response from the request for the previous page. (required)
1254
1255Returns:
1256 A request object that you can call 'execute()' on to request the next
1257 page. Returns None if there are no more items in the collection.
1258 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001259 # Retrieve nextPageToken from previous_response
1260 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001261
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001262 nextPageToken = previous_response.get(nextPageTokenName, None)
1263 if not nextPageToken:
1264 return None
John Asmuth864311d2014-04-24 15:46:08 -04001265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001266 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001267
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001268 if isPageTokenParameter:
1269 # Replace pageToken value in URI
1270 request.uri = _add_query_parameter(
1271 request.uri, pageTokenName, nextPageToken
1272 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001273 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001274 else:
1275 # Replace pageToken value in request body
1276 model = self._model
1277 body = model.deserialize(request.body)
1278 body[pageTokenName] = nextPageToken
1279 request.body = model.serialize(body)
David Schweikert8019f2f2021-06-08 16:54:44 +02001280 request.body_size = len(request.body)
1281 if "content-length" in request.headers:
1282 del request.headers["content-length"]
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001283 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001284
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001285 return request
John Asmuth864311d2014-04-24 15:46:08 -04001286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001287 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001288
1289
1290class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001291 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001292
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001293 def __init__(
1294 self,
1295 http,
1296 baseUrl,
1297 model,
1298 requestBuilder,
1299 developerKey,
1300 resourceDesc,
1301 rootDesc,
1302 schema,
1303 ):
1304 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001305
1306 Args:
1307 http: httplib2.Http, Object to make http requests with.
1308 baseUrl: string, base URL for the API. All requests are relative to this
1309 URI.
1310 model: googleapiclient.Model, converts to and from the wire format.
1311 requestBuilder: class or callable that instantiates an
1312 googleapiclient.HttpRequest object.
1313 developerKey: string, key obtained from
1314 https://code.google.com/apis/console
1315 resourceDesc: object, section of deserialized discovery document that
1316 describes a resource. Note that the top level discovery document
1317 is considered a resource.
1318 rootDesc: object, the entire deserialized discovery document.
1319 schema: object, mapping of schema names to schema descriptions.
1320 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001321 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001322
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001323 self._http = http
1324 self._baseUrl = baseUrl
1325 self._model = model
1326 self._developerKey = developerKey
1327 self._requestBuilder = requestBuilder
1328 self._resourceDesc = resourceDesc
1329 self._rootDesc = rootDesc
1330 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001331
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001332 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001333
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001334 def _set_dynamic_attr(self, attr_name, value):
1335 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001336
1337 Args:
1338 attr_name: string; The name of the attribute to be set
1339 value: The value being set on the object and tracked in the dynamic cache.
1340 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001341 self._dynamic_attrs.append(attr_name)
1342 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001344 def __getstate__(self):
1345 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001346
1347 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1348 will be wiped and restored on pickle serialization.
1349 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001350 state_dict = copy.copy(self.__dict__)
1351 for dynamic_attr in self._dynamic_attrs:
1352 del state_dict[dynamic_attr]
1353 del state_dict["_dynamic_attrs"]
1354 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001355
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001356 def __setstate__(self, state):
1357 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001358
1359 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1360 will be wiped and restored on pickle serialization.
1361 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001362 self.__dict__.update(state)
1363 self._dynamic_attrs = []
1364 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001365
Bu Sun Kim98888da2020-09-23 11:10:39 -06001366
1367 def __enter__(self):
1368 return self
1369
1370 def __exit__(self, exc_type, exc, exc_tb):
1371 self.close()
1372
1373 def close(self):
1374 """Close httplib2 connections."""
1375 # httplib2 leaves sockets open by default.
1376 # Cleanup using the `close` method.
1377 # https://github.com/httplib2/httplib2/issues/148
Bu Sun Kima9583f72021-03-15 09:12:02 -06001378 self._http.close()
Bu Sun Kim98888da2020-09-23 11:10:39 -06001379
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001380 def _set_service_methods(self):
1381 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1382 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1383 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001384
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001385 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1386 # If this is the root Resource, add a new_batch_http_request() method.
1387 if resourceDesc == rootDesc:
1388 batch_uri = "%s%s" % (
1389 rootDesc["rootUrl"],
1390 rootDesc.get("batchPath", "batch"),
1391 )
1392
1393 def new_batch_http_request(callback=None):
1394 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001395
1396 Args:
1397 callback: callable, A callback to be called for each response, of the
1398 form callback(id, response, exception). The first parameter is the
1399 request id, and the second is the deserialized response object. The
1400 third is an apiclient.errors.HttpError exception object if an HTTP
1401 error occurred while processing the request, or None if no error
1402 occurred.
1403
1404 Returns:
1405 A BatchHttpRequest object based on the discovery document.
1406 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001407 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001408
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001409 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001410
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001411 # Add basic methods to Resource
1412 if "methods" in resourceDesc:
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001413 for methodName, methodDesc in resourceDesc["methods"].items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001414 fixedMethodName, method = createMethod(
1415 methodName, methodDesc, rootDesc, schema
1416 )
1417 self._set_dynamic_attr(
1418 fixedMethodName, method.__get__(self, self.__class__)
1419 )
1420 # Add in _media methods. The functionality of the attached method will
1421 # change when it sees that the method name ends in _media.
1422 if methodDesc.get("supportsMediaDownload", False):
1423 fixedMethodName, method = createMethod(
1424 methodName + "_media", methodDesc, rootDesc, schema
1425 )
1426 self._set_dynamic_attr(
1427 fixedMethodName, method.__get__(self, self.__class__)
1428 )
John Asmuth864311d2014-04-24 15:46:08 -04001429
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001430 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1431 # Add in nested resources
1432 if "resources" in resourceDesc:
1433
1434 def createResourceMethod(methodName, methodDesc):
1435 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001436
1437 Args:
1438 methodName: string, name of the method to use.
1439 methodDesc: object, fragment of deserialized discovery document that
1440 describes the method.
1441 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001442 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001443
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001444 def methodResource(self):
1445 return Resource(
1446 http=self._http,
1447 baseUrl=self._baseUrl,
1448 model=self._model,
1449 developerKey=self._developerKey,
1450 requestBuilder=self._requestBuilder,
1451 resourceDesc=methodDesc,
1452 rootDesc=rootDesc,
1453 schema=schema,
1454 )
John Asmuth864311d2014-04-24 15:46:08 -04001455
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001456 setattr(methodResource, "__doc__", "A collection resource.")
1457 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001458
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001459 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001460
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001461 for methodName, methodDesc in resourceDesc["resources"].items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001462 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1463 self._set_dynamic_attr(
1464 fixedMethodName, method.__get__(self, self.__class__)
1465 )
John Asmuth864311d2014-04-24 15:46:08 -04001466
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001467 def _add_next_methods(self, resourceDesc, schema):
1468 # Add _next() methods if and only if one of the names 'pageToken' or
1469 # 'nextPageToken' occurs among the fields of both the method's response
1470 # type either the method's request (query parameters) or request body.
1471 if "methods" not in resourceDesc:
1472 return
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -04001473 for methodName, methodDesc in resourceDesc["methods"].items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001474 nextPageTokenName = _findPageTokenName(
1475 _methodProperties(methodDesc, schema, "response")
1476 )
1477 if not nextPageTokenName:
1478 continue
1479 isPageTokenParameter = True
1480 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1481 if not pageTokenName:
1482 isPageTokenParameter = False
1483 pageTokenName = _findPageTokenName(
1484 _methodProperties(methodDesc, schema, "request")
1485 )
1486 if not pageTokenName:
1487 continue
1488 fixedMethodName, method = createNextMethod(
1489 methodName + "_next",
1490 pageTokenName,
1491 nextPageTokenName,
1492 isPageTokenParameter,
1493 )
1494 self._set_dynamic_attr(
1495 fixedMethodName, method.__get__(self, self.__class__)
1496 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001497
1498
1499def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001500 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001501
1502 Args:
1503 fields: container of string, names of fields.
1504
1505 Returns:
1506 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1507 otherwise None.
1508 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001509 return next(
1510 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1511 )
1512
Thomas Coffee20af04d2017-02-10 15:24:44 -08001513
1514def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001515 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001516
1517 Args:
1518 methodDesc: object, fragment of deserialized discovery document that
1519 describes the method.
1520 schema: object, mapping of schema names to schema descriptions.
1521 name: string, name of top-level field in method description.
1522
1523 Returns:
1524 Object representing fragment of deserialized discovery document
1525 corresponding to 'properties' field of object corresponding to named field
1526 in method description, if it exists, otherwise empty dict.
1527 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001528 desc = methodDesc.get(name, {})
1529 if "$ref" in desc:
1530 desc = schema.get(desc["$ref"], {})
1531 return desc.get("properties", {})