blob: 55367c0a4917d9bc8f43dbc9fe076e5fbf5ff15a [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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070032
Phil Ruffwind26178fc2015-10-13 19:00:33 -040033try:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070034 from email.generator import BytesGenerator
Phil Ruffwind26178fc2015-10-13 19:00:33 -040035except ImportError:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070036 from email.generator import Generator as BytesGenerator
John Asmuth864311d2014-04-24 15:46:08 -040037from email.mime.multipart import MIMEMultipart
38from email.mime.nonmultipart import MIMENonMultipart
Craig Citro6ae34d72014-08-18 23:10:09 -070039import json
John Asmuth864311d2014-04-24 15:46:08 -040040import keyword
41import logging
42import mimetypes
43import os
44import re
John Asmuth864311d2014-04-24 15:46:08 -040045
46# Third-party imports
47import httplib2
John Asmuth864311d2014-04-24 15:46:08 -040048import uritemplate
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -070049import google.api_core.client_options
arithmetic1728981eadf2020-06-02 10:20:10 -070050from google.auth.transport import mtls
51from google.auth.exceptions import MutualTLSChannelError
52
53try:
54 import google_auth_httplib2
55except ImportError: # pragma: NO COVER
56 google_auth_httplib2 = None
John Asmuth864311d2014-04-24 15:46:08 -040057
58# Local imports
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -080059from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080060from googleapiclient import mimeparse
John Asmuth864311d2014-04-24 15:46:08 -040061from googleapiclient.errors import HttpError
62from googleapiclient.errors import InvalidJsonError
63from googleapiclient.errors import MediaUploadSizeError
64from googleapiclient.errors import UnacceptableMimeTypeError
65from googleapiclient.errors import UnknownApiNameOrVersion
66from googleapiclient.errors import UnknownFileType
Igor Maravić22435292017-01-19 22:28:22 +010067from googleapiclient.http import build_http
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -040068from googleapiclient.http import BatchHttpRequest
Kostyantyn Leschenkobe8b1cb2016-10-17 12:57:21 +030069from googleapiclient.http import HttpMock
70from googleapiclient.http import HttpMockSequence
John Asmuth864311d2014-04-24 15:46:08 -040071from googleapiclient.http import HttpRequest
72from googleapiclient.http import MediaFileUpload
73from googleapiclient.http import MediaUpload
74from googleapiclient.model import JsonModel
75from googleapiclient.model import MediaModel
76from googleapiclient.model import RawModel
77from googleapiclient.schema import Schemas
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070078
Helen Koikede13e3b2018-04-26 16:05:16 -030079from googleapiclient._helpers import _add_query_parameter
80from googleapiclient._helpers import positional
John Asmuth864311d2014-04-24 15:46:08 -040081
82
83# The client library requires a version of httplib2 that supports RETRIES.
84httplib2.RETRIES = 1
85
86logger = logging.getLogger(__name__)
87
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070088URITEMPLATE = re.compile("{[^}]*}")
89VARNAME = re.compile("[a-zA-Z0-9_-]+")
90DISCOVERY_URI = (
91 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
92)
Ethan Bao12b7cd32016-03-14 14:25:10 -070093V1_DISCOVERY_URI = DISCOVERY_URI
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070094V2_DISCOVERY_URI = (
95 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
96)
97DEFAULT_METHOD_DOC = "A description of how to use this function"
98HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
Igor Maravić22435292017-01-19 22:28:22 +010099
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700100_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
101BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
John Asmuth864311d2014-04-24 15:46:08 -0400102MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700103 "description": (
104 "The filename of the media request body, or an instance "
105 "of a MediaUpload object."
106 ),
107 "type": "string",
108 "required": False,
John Asmuth864311d2014-04-24 15:46:08 -0400109}
Brian J. Watson38051ac2016-10-25 07:53:08 -0700110MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700111 "description": (
112 "The MIME type of the media request body, or an instance "
113 "of a MediaUpload object."
114 ),
115 "type": "string",
116 "required": False,
Brian J. Watson38051ac2016-10-25 07:53:08 -0700117}
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700118_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
John Asmuth864311d2014-04-24 15:46:08 -0400119
120# Parameters accepted by the stack, but not visible via discovery.
121# TODO(dhermes): Remove 'userip' in 'v2'.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700122STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
123STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
John Asmuth864311d2014-04-24 15:46:08 -0400124
125# Library-specific reserved words beyond Python keywords.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700126RESERVED_WORDS = frozenset(["body"])
John Asmuth864311d2014-04-24 15:46:08 -0400127
Phil Ruffwind26178fc2015-10-13 19:00:33 -0400128# patch _write_lines to avoid munging '\r' into '\n'
129# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
130class _BytesGenerator(BytesGenerator):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700131 _write_lines = BytesGenerator.write
132
John Asmuth864311d2014-04-24 15:46:08 -0400133
134def fix_method_name(name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700135 """Fix method names to avoid '$' characters and reserved word conflicts.
John Asmuth864311d2014-04-24 15:46:08 -0400136
137 Args:
138 name: string, method name.
139
140 Returns:
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700141 The name with '_' appended if the name is a reserved word and '$' and '-'
arithmetic1728981eadf2020-06-02 10:20:10 -0700142 replaced with '_'.
John Asmuth864311d2014-04-24 15:46:08 -0400143 """
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700144 name = name.replace("$", "_").replace("-", "_")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700145 if keyword.iskeyword(name) or name in RESERVED_WORDS:
146 return name + "_"
147 else:
148 return name
John Asmuth864311d2014-04-24 15:46:08 -0400149
150
151def key2param(key):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700152 """Converts key names into parameter names.
John Asmuth864311d2014-04-24 15:46:08 -0400153
154 For example, converting "max-results" -> "max_results"
155
156 Args:
157 key: string, the method key name.
158
159 Returns:
160 A safe method name based on the key name.
161 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700162 result = []
163 key = list(key)
164 if not key[0].isalpha():
165 result.append("x")
166 for c in key:
167 if c.isalnum():
168 result.append(c)
169 else:
170 result.append("_")
John Asmuth864311d2014-04-24 15:46:08 -0400171
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700172 return "".join(result)
John Asmuth864311d2014-04-24 15:46:08 -0400173
174
175@positional(2)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700176def build(
177 serviceName,
178 version,
179 http=None,
180 discoveryServiceUrl=DISCOVERY_URI,
181 developerKey=None,
182 model=None,
183 requestBuilder=HttpRequest,
184 credentials=None,
185 cache_discovery=True,
186 cache=None,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700187 client_options=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700188 adc_cert_path=None,
189 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700190):
191 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400192
193 Construct a Resource object for interacting with an API. The serviceName and
194 version are the names from the Discovery service.
195
196 Args:
197 serviceName: string, name of the service.
198 version: string, the version of the service.
199 http: httplib2.Http, An instance of httplib2.Http or something that acts
200 like it that HTTP requests will be made through.
201 discoveryServiceUrl: string, a URI Template that points to the location of
202 the discovery service. It should have two parameters {api} and
203 {apiVersion} that when filled in produce an absolute URI to the discovery
204 document for that service.
205 developerKey: string, key obtained from
206 https://code.google.com/apis/console.
207 model: googleapiclient.Model, converts to and from the wire format.
208 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
209 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800210 credentials: oauth2client.Credentials or
211 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700212 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700213 cache_discovery: Boolean, whether or not to cache the discovery doc.
214 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
215 cache object for the discovery documents.
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700216 client_options: Dictionary or google.api_core.client_options, Client options to set user
217 options on the client. API endpoint should be set through client_options.
arithmetic1728981eadf2020-06-02 10:20:10 -0700218 client_cert_source is not supported, client cert should be provided using
219 client_encrypted_cert_source instead.
220 adc_cert_path: str, client certificate file path to save the application
221 default client certificate for mTLS. This field is required if you want to
222 use the default client certificate.
223 adc_key_path: str, client encrypted private key file path to save the
224 application default client encrypted private key for mTLS. This field is
225 required if you want to use the default client certificate.
John Asmuth864311d2014-04-24 15:46:08 -0400226
227 Returns:
228 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700229
230 Raises:
231 google.auth.exceptions.MutualTLSChannelError: if there are any problems
232 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400233 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700234 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400235
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700236 if http is None:
237 discovery_http = build_http()
238 else:
239 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400240
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700241 for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI):
242 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700244 try:
245 content = _retrieve_discovery_doc(
246 requested_url, discovery_http, cache_discovery, cache, developerKey
247 )
248 return build_from_document(
249 content,
250 base=discovery_url,
251 http=http,
252 developerKey=developerKey,
253 model=model,
254 requestBuilder=requestBuilder,
255 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700256 client_options=client_options,
257 adc_cert_path=adc_cert_path,
258 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700259 )
260 except HttpError as e:
261 if e.resp.status == http_client.NOT_FOUND:
262 continue
263 else:
264 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700266 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
Takashi Matsuo30125122015-08-19 11:42:32 -0700267
268
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700269def _retrieve_discovery_doc(url, http, cache_discovery, cache=None, developerKey=None):
270 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700271
272 Args:
273 url: string, the URL of the discovery document.
274 http: httplib2.Http, An instance of httplib2.Http or something that acts
275 like it through which HTTP requests will be made.
276 cache_discovery: Boolean, whether or not to cache the discovery doc.
277 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
278 object for the discovery documents.
279
280 Returns:
281 A unicode string representation of the discovery document.
282 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700283 if cache_discovery:
284 from . import discovery_cache
285 from .discovery_cache import base
Takashi Matsuo30125122015-08-19 11:42:32 -0700286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700287 if cache is None:
288 cache = discovery_cache.autodetect()
289 if cache:
290 content = cache.get(url)
291 if content:
292 return content
John Asmuth864311d2014-04-24 15:46:08 -0400293
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700294 actual_url = url
295 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
296 # variable that contains the network address of the client sending the
297 # request. If it exists then add that to the request for the discovery
298 # document to avoid exceeding the quota on discovery requests.
299 if "REMOTE_ADDR" in os.environ:
300 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
301 if developerKey:
302 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700303 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400304
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700305 resp, content = http.request(actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400306
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700307 if resp.status >= 400:
308 raise HttpError(resp, content, uri=actual_url)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800309
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700310 try:
311 content = content.decode("utf-8")
312 except AttributeError:
313 pass
314
315 try:
316 service = json.loads(content)
317 except ValueError as e:
318 logger.error("Failed to parse as JSON: " + content)
319 raise InvalidJsonError()
320 if cache_discovery and cache:
321 cache.set(url, content)
322 return content
John Asmuth864311d2014-04-24 15:46:08 -0400323
324
325@positional(1)
326def build_from_document(
327 service,
328 base=None,
329 future=None,
330 http=None,
331 developerKey=None,
332 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700333 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700334 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700335 client_options=None,
336 adc_cert_path=None,
337 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700338):
339 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400340
341 Same as `build()`, but constructs the Resource object from a discovery
342 document that is it given, as opposed to retrieving one over HTTP.
343
344 Args:
345 service: string or object, the JSON discovery document describing the API.
346 The value passed in may either be the JSON string or the deserialized
347 JSON.
348 base: string, base URI for all HTTP requests, usually the discovery URI.
349 This parameter is no longer used as rootUrl and servicePath are included
350 within the discovery document. (deprecated)
351 future: string, discovery document with future capabilities (deprecated).
352 http: httplib2.Http, An instance of httplib2.Http or something that acts
353 like it that HTTP requests will be made through.
354 developerKey: string, Key for controlling API usage, generated
355 from the API Console.
356 model: Model class instance that serializes and de-serializes requests and
357 responses.
358 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800359 credentials: oauth2client.Credentials or
360 google.auth.credentials.Credentials, credentials to be used for
361 authentication.
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700362 client_options: Dictionary or google.api_core.client_options, Client options to set user
363 options on the client. API endpoint should be set through client_options.
arithmetic1728981eadf2020-06-02 10:20:10 -0700364 client_cert_source is not supported, client cert should be provided using
365 client_encrypted_cert_source instead.
366 adc_cert_path: str, client certificate file path to save the application
367 default client certificate for mTLS. This field is required if you want to
368 use the default client certificate.
369 adc_key_path: str, client encrypted private key file path to save the
370 application default client encrypted private key for mTLS. This field is
371 required if you want to use the default client certificate.
John Asmuth864311d2014-04-24 15:46:08 -0400372
373 Returns:
374 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700375
376 Raises:
377 google.auth.exceptions.MutualTLSChannelError: if there are any problems
378 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400379 """
380
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700381 if http is not None and credentials is not None:
382 raise ValueError("Arguments http and credentials are mutually exclusive.")
John Asmuth864311d2014-04-24 15:46:08 -0400383
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700384 if isinstance(service, six.string_types):
385 service = json.loads(service)
386 elif isinstance(service, six.binary_type):
387 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400388
arithmetic1728981eadf2020-06-02 10:20:10 -0700389 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700390 logger.error(
391 "You are using HttpMock or HttpMockSequence without"
392 + "having the service discovery doc in cache. Try calling "
393 + "build() without mocking once first to populate the "
394 + "cache."
395 )
396 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400397
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700398 # If an API Endpoint is provided on client options, use that as the base URL
arithmetic1728981eadf2020-06-02 10:20:10 -0700399 base = urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700400 if client_options:
401 if type(client_options) == dict:
arithmetic1728981eadf2020-06-02 10:20:10 -0700402 client_options = google.api_core.client_options.from_dict(client_options)
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700403 if client_options.api_endpoint:
404 base = client_options.api_endpoint
405
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700406 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400407
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700408 # If the http client is not specified, then we must construct an http client
409 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800410 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700411 if http is None:
412 # Does the service require scopes?
413 scopes = list(
414 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
415 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700416
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700417 # If so, then the we need to setup authentication if no developerKey is
418 # specified.
419 if scopes and not developerKey:
420 # If the user didn't pass in credentials, attempt to acquire application
421 # default credentials.
422 if credentials is None:
423 credentials = _auth.default_credentials()
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800424
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700425 # The credentials need to be scoped.
426 credentials = _auth.with_scopes(credentials, scopes)
427
428 # If credentials are provided, create an authorized http instance;
429 # otherwise, skip authentication.
430 if credentials:
431 http = _auth.authorized_http(credentials)
432
433 # If the service doesn't require scopes then there is no need for
434 # authentication.
435 else:
436 http = build_http()
437
arithmetic1728981eadf2020-06-02 10:20:10 -0700438 # Obtain client cert and create mTLS http channel if cert exists.
439 client_cert_to_use = None
440 if client_options and client_options.client_cert_source:
441 raise MutualTLSChannelError(
442 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
443 )
arithmetic172842028ed2020-06-02 11:24:04 -0700444 if (
445 client_options
446 and hasattr(client_options, "client_encrypted_cert_source")
447 and client_options.client_encrypted_cert_source
448 ):
arithmetic1728981eadf2020-06-02 10:20:10 -0700449 client_cert_to_use = client_options.client_encrypted_cert_source
450 elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
451 client_cert_to_use = mtls.default_client_encrypted_cert_source(
452 adc_cert_path, adc_key_path
453 )
454 if client_cert_to_use:
455 cert_path, key_path, passphrase = client_cert_to_use()
456
457 # The http object we built could be google_auth_httplib2.AuthorizedHttp
458 # or httplib2.Http. In the first case we need to extract the wrapped
459 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
460 http_channel = (
461 http.http
462 if google_auth_httplib2
463 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
464 else http
465 )
466 http_channel.add_certificate(key_path, cert_path, "", passphrase)
467
468 # If user doesn't provide api endpoint via client options, decide which
469 # api endpoint to use.
470 if "mtlsRootUrl" in service and (
471 not client_options or not client_options.api_endpoint
472 ):
473 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
474 use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "Never")
475
476 if not use_mtls_env in ("Never", "Auto", "Always"):
477 raise MutualTLSChannelError(
478 "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: Never, Auto, Always"
479 )
480
481 # Switch to mTLS endpoint, if environment variable is "Always", or
482 # environment varibable is "Auto" and client cert exists.
483 if use_mtls_env == "Always" or (
484 use_mtls_env == "Auto" and client_cert_to_use
485 ):
486 base = mtls_endpoint
487
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700488 if model is None:
489 features = service.get("features", [])
490 model = JsonModel("dataWrapper" in features)
491
492 return Resource(
493 http=http,
494 baseUrl=base,
495 model=model,
496 developerKey=developerKey,
497 requestBuilder=requestBuilder,
498 resourceDesc=service,
499 rootDesc=service,
500 schema=schema,
501 )
John Asmuth864311d2014-04-24 15:46:08 -0400502
503
504def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700505 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400506
507 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
508 JSON Schema.
509
510 Args:
511 value: any, the value to convert
512 schema_type: string, the type that value should be interpreted as
513
514 Returns:
515 A string representation of 'value' based on the schema_type.
516 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700517 if schema_type == "string":
518 if type(value) == type("") or type(value) == type(u""):
519 return value
520 else:
521 return str(value)
522 elif schema_type == "integer":
523 return str(int(value))
524 elif schema_type == "number":
525 return str(float(value))
526 elif schema_type == "boolean":
527 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400528 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700529 if type(value) == type("") or type(value) == type(u""):
530 return value
531 else:
532 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400533
534
535def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700536 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400537
538 Args:
539 maxSize: string, size as a string, such as 2MB or 7GB.
540
541 Returns:
542 The size as an integer value.
543 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700544 if len(maxSize) < 2:
545 return 0
546 units = maxSize[-2:].upper()
547 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
548 if bit_shift is not None:
549 return int(maxSize[:-2]) << bit_shift
550 else:
551 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400552
553
554def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700555 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400556
557 Constructed using the API root URI and service path from the discovery
558 document and the relative path for the API method.
559
560 Args:
561 root_desc: Dictionary; the entire original deserialized discovery document.
562 path_url: String; the relative URL for the API method. Relative to the API
563 root, which is specified in the discovery document.
564
565 Returns:
566 String; the absolute URI for media upload for the API method.
567 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700568 return "%(root)supload/%(service_path)s%(path)s" % {
569 "root": root_desc["rootUrl"],
570 "service_path": root_desc["servicePath"],
571 "path": path_url,
572 }
John Asmuth864311d2014-04-24 15:46:08 -0400573
574
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900575def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700576 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400577
578 Specifically, adds whatever global parameters are specified by the API to the
579 parameters for the individual method. Also adds parameters which don't
580 appear in the discovery document, but are available to all discovery based
581 APIs (these are listed in STACK_QUERY_PARAMETERS).
582
583 SIDE EFFECTS: This updates the parameters dictionary object in the method
584 description.
585
586 Args:
587 method_desc: Dictionary with metadata describing an API method. Value comes
588 from the dictionary of methods stored in the 'methods' key in the
589 deserialized discovery document.
590 root_desc: Dictionary; the entire original deserialized discovery document.
591 http_method: String; the HTTP method used to call the API method described
592 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900593 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400594
595 Returns:
596 The updated Dictionary stored in the 'parameters' key of the method
597 description dictionary.
598 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700599 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400600
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700601 # Add in the parameters common to all methods.
602 for name, description in six.iteritems(root_desc.get("parameters", {})):
603 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400604
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700605 # Add in undocumented query parameters.
606 for name in STACK_QUERY_PARAMETERS:
607 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400608
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700609 # Add 'body' (our own reserved word) to parameters if the method supports
610 # a request payload.
611 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
612 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
613 body.update(method_desc["request"])
614 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400615
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700616 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400617
618
619def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700620 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400621
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700622 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
623 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400624
625 Args:
626 method_desc: Dictionary with metadata describing an API method. Value comes
627 from the dictionary of methods stored in the 'methods' key in the
628 deserialized discovery document.
629 root_desc: Dictionary; the entire original deserialized discovery document.
630 path_url: String; the relative URL for the API method. Relative to the API
631 root, which is specified in the discovery document.
632 parameters: A dictionary describing method parameters for method described
633 in method_desc.
634
635 Returns:
636 Triple (accept, max_size, media_path_url) where:
637 - accept is a list of strings representing what content types are
638 accepted for media upload. Defaults to empty list if not in the
639 discovery document.
640 - max_size is a long representing the max size in bytes allowed for a
641 media upload. Defaults to 0L if not in the discovery document.
642 - media_path_url is a String; the absolute URI for media upload for the
643 API method. Constructed using the API root URI and service path from
644 the discovery document and the relative path for the API method. If
645 media upload is not supported, this is None.
646 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700647 media_upload = method_desc.get("mediaUpload", {})
648 accept = media_upload.get("accept", [])
649 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
650 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400651
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700652 if media_upload:
653 media_path_url = _media_path_url_from_info(root_desc, path_url)
654 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
655 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400656
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700657 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400658
659
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900660def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700661 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400662
663 SIDE EFFECTS: Changes the parameters dictionary in the method description with
664 extra parameters which are used locally.
665
666 Args:
667 method_desc: Dictionary with metadata describing an API method. Value comes
668 from the dictionary of methods stored in the 'methods' key in the
669 deserialized discovery document.
670 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900671 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400672
673 Returns:
674 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
675 where:
676 - path_url is a String; the relative URL for the API method. Relative to
677 the API root, which is specified in the discovery document.
678 - http_method is a String; the HTTP method used to call the API method
679 described in the method description.
680 - method_id is a String; the name of the RPC method associated with the
681 API method, and is in the method description in the 'id' key.
682 - accept is a list of strings representing what content types are
683 accepted for media upload. Defaults to empty list if not in the
684 discovery document.
685 - max_size is a long representing the max size in bytes allowed for a
686 media upload. Defaults to 0L if not in the discovery document.
687 - media_path_url is a String; the absolute URI for media upload for the
688 API method. Constructed using the API root URI and service path from
689 the discovery document and the relative path for the API method. If
690 media upload is not supported, this is None.
691 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700692 path_url = method_desc["path"]
693 http_method = method_desc["httpMethod"]
694 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400695
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700696 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
697 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
698 # 'parameters' key and needs to know if there is a 'body' parameter because it
699 # also sets a 'media_body' parameter.
700 accept, max_size, media_path_url = _fix_up_media_upload(
701 method_desc, root_desc, path_url, parameters
702 )
John Asmuth864311d2014-04-24 15:46:08 -0400703
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700704 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400705
706
Craig Citro7ee535d2015-02-23 10:11:14 -0800707def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700708 """Custom urljoin replacement supporting : before / in url."""
709 # In general, it's unsafe to simply join base and url. However, for
710 # the case of discovery documents, we know:
711 # * base will never contain params, query, or fragment
712 # * url will never contain a scheme or net_loc.
713 # In general, this means we can safely join on /; we just need to
714 # ensure we end up with precisely one / joining base and url. The
715 # exception here is the case of media uploads, where url will be an
716 # absolute url.
717 if url.startswith("http://") or url.startswith("https://"):
718 return urljoin(base, url)
719 new_base = base if base.endswith("/") else base + "/"
720 new_url = url[1:] if url.startswith("/") else url
721 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800722
723
John Asmuth864311d2014-04-24 15:46:08 -0400724# TODO(dhermes): Convert this class to ResourceMethod and make it callable
725class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700726 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400727
728 Attributes:
729 argmap: Map from method parameter name (string) to query parameter name
730 (string).
731 required_params: List of required parameters (represented by parameter
732 name as string).
733 repeated_params: List of repeated parameters (represented by parameter
734 name as string).
735 pattern_params: Map from method parameter name (string) to regular
736 expression (as a string). If the pattern is set for a parameter, the
737 value for that parameter must match the regular expression.
738 query_params: List of parameters (represented by parameter name as string)
739 that will be used in the query string.
740 path_params: Set of parameters (represented by parameter name as string)
741 that will be used in the base URL path.
742 param_types: Map from method parameter name (string) to parameter type. Type
743 can be any valid JSON schema type; valid values are 'any', 'array',
744 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
745 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
746 enum_params: Map from method parameter name (string) to list of strings,
747 where each list of strings is the list of acceptable enum values.
748 """
749
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700750 def __init__(self, method_desc):
751 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400752
753 Sets default values and defers to set_parameters to populate.
754
755 Args:
756 method_desc: Dictionary with metadata describing an API method. Value
757 comes from the dictionary of methods stored in the 'methods' key in
758 the deserialized discovery document.
759 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700760 self.argmap = {}
761 self.required_params = []
762 self.repeated_params = []
763 self.pattern_params = {}
764 self.query_params = []
765 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
766 # parsing is gotten rid of.
767 self.path_params = set()
768 self.param_types = {}
769 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400770
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700771 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400772
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700773 def set_parameters(self, method_desc):
774 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400775
776 Iterates through each parameter for the method and parses the values from
777 the parameter dictionary.
778
779 Args:
780 method_desc: Dictionary with metadata describing an API method. Value
781 comes from the dictionary of methods stored in the 'methods' key in
782 the deserialized discovery document.
783 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700784 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
785 param = key2param(arg)
786 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400787
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700788 if desc.get("pattern"):
789 self.pattern_params[param] = desc["pattern"]
790 if desc.get("enum"):
791 self.enum_params[param] = desc["enum"]
792 if desc.get("required"):
793 self.required_params.append(param)
794 if desc.get("repeated"):
795 self.repeated_params.append(param)
796 if desc.get("location") == "query":
797 self.query_params.append(param)
798 if desc.get("location") == "path":
799 self.path_params.add(param)
800 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400801
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700802 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
803 # should have all path parameters already marked with
804 # 'location: path'.
805 for match in URITEMPLATE.finditer(method_desc["path"]):
806 for namematch in VARNAME.finditer(match.group(0)):
807 name = key2param(namematch.group(0))
808 self.path_params.add(name)
809 if name in self.query_params:
810 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400811
812
813def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700814 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400815
816 Args:
817 methodName: string, name of the method to use.
818 methodDesc: object, fragment of deserialized discovery document that
819 describes the method.
820 rootDesc: object, the entire deserialized discovery document.
821 schema: object, mapping of schema names to schema descriptions.
822 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700823 methodName = fix_method_name(methodName)
824 (
825 pathUrl,
826 httpMethod,
827 methodId,
828 accept,
829 maxSize,
830 mediaPathUrl,
831 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400832
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700833 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400834
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700835 def method(self, **kwargs):
836 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400837
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700838 for name in six.iterkeys(kwargs):
839 if name not in parameters.argmap:
840 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400841
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700842 # Remove args that have a value of None.
843 keys = list(kwargs.keys())
844 for name in keys:
845 if kwargs[name] is None:
846 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400847
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700848 for name in parameters.required_params:
849 if name not in kwargs:
850 # temporary workaround for non-paging methods incorrectly requiring
851 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
852 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
853 _methodProperties(methodDesc, schema, "response")
854 ):
855 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400856
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700857 for name, regex in six.iteritems(parameters.pattern_params):
858 if name in kwargs:
859 if isinstance(kwargs[name], six.string_types):
860 pvalues = [kwargs[name]]
861 else:
862 pvalues = kwargs[name]
863 for pvalue in pvalues:
864 if re.match(regex, pvalue) is None:
865 raise TypeError(
866 'Parameter "%s" value "%s" does not match the pattern "%s"'
867 % (name, pvalue, regex)
868 )
869
870 for name, enums in six.iteritems(parameters.enum_params):
871 if name in kwargs:
872 # We need to handle the case of a repeated enum
873 # name differently, since we want to handle both
874 # arg='value' and arg=['value1', 'value2']
875 if name in parameters.repeated_params and not isinstance(
876 kwargs[name], six.string_types
877 ):
878 values = kwargs[name]
879 else:
880 values = [kwargs[name]]
881 for value in values:
882 if value not in enums:
883 raise TypeError(
884 'Parameter "%s" value "%s" is not an allowed value in "%s"'
885 % (name, value, str(enums))
886 )
887
888 actual_query_params = {}
889 actual_path_params = {}
890 for key, value in six.iteritems(kwargs):
891 to_type = parameters.param_types.get(key, "string")
892 # For repeated parameters we cast each member of the list.
893 if key in parameters.repeated_params and type(value) == type([]):
894 cast_value = [_cast(x, to_type) for x in value]
895 else:
896 cast_value = _cast(value, to_type)
897 if key in parameters.query_params:
898 actual_query_params[parameters.argmap[key]] = cast_value
899 if key in parameters.path_params:
900 actual_path_params[parameters.argmap[key]] = cast_value
901 body_value = kwargs.get("body", None)
902 media_filename = kwargs.get("media_body", None)
903 media_mime_type = kwargs.get("media_mime_type", None)
904
905 if self._developerKey:
906 actual_query_params["key"] = self._developerKey
907
908 model = self._model
909 if methodName.endswith("_media"):
910 model = MediaModel()
911 elif "response" not in methodDesc:
912 model = RawModel()
913
914 headers = {}
915 headers, params, query, body = model.request(
916 headers, actual_path_params, actual_query_params, body_value
917 )
918
919 expanded_url = uritemplate.expand(pathUrl, params)
920 url = _urljoin(self._baseUrl, expanded_url + query)
921
922 resumable = None
923 multipart_boundary = ""
924
925 if media_filename:
926 # Ensure we end up with a valid MediaUpload object.
927 if isinstance(media_filename, six.string_types):
928 if media_mime_type is None:
929 logger.warning(
930 "media_mime_type argument not specified: trying to auto-detect for %s",
931 media_filename,
932 )
933 media_mime_type, _ = mimetypes.guess_type(media_filename)
934 if media_mime_type is None:
935 raise UnknownFileType(media_filename)
936 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
937 raise UnacceptableMimeTypeError(media_mime_type)
938 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
939 elif isinstance(media_filename, MediaUpload):
940 media_upload = media_filename
941 else:
942 raise TypeError("media_filename must be str or MediaUpload.")
943
944 # Check the maxSize
945 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
946 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
947
948 # Use the media path uri for media uploads
949 expanded_url = uritemplate.expand(mediaPathUrl, params)
950 url = _urljoin(self._baseUrl, expanded_url + query)
951 if media_upload.resumable():
952 url = _add_query_parameter(url, "uploadType", "resumable")
953
954 if media_upload.resumable():
955 # This is all we need to do for resumable, if the body exists it gets
956 # sent in the first request, otherwise an empty body is sent.
957 resumable = media_upload
958 else:
959 # A non-resumable upload
960 if body is None:
961 # This is a simple media upload
962 headers["content-type"] = media_upload.mimetype()
963 body = media_upload.getbytes(0, media_upload.size())
964 url = _add_query_parameter(url, "uploadType", "media")
965 else:
966 # This is a multipart/related upload.
967 msgRoot = MIMEMultipart("related")
968 # msgRoot should not write out it's own headers
969 setattr(msgRoot, "_write_headers", lambda self: None)
970
971 # attach the body as one part
972 msg = MIMENonMultipart(*headers["content-type"].split("/"))
973 msg.set_payload(body)
974 msgRoot.attach(msg)
975
976 # attach the media as the second part
977 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
978 msg["Content-Transfer-Encoding"] = "binary"
979
980 payload = media_upload.getbytes(0, media_upload.size())
981 msg.set_payload(payload)
982 msgRoot.attach(msg)
983 # encode the body: note that we can't use `as_string`, because
984 # it plays games with `From ` lines.
985 fp = BytesIO()
986 g = _BytesGenerator(fp, mangle_from_=False)
987 g.flatten(msgRoot, unixfrom=False)
988 body = fp.getvalue()
989
990 multipart_boundary = msgRoot.get_boundary()
991 headers["content-type"] = (
992 "multipart/related; " 'boundary="%s"'
993 ) % multipart_boundary
994 url = _add_query_parameter(url, "uploadType", "multipart")
995
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700996 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700997 return self._requestBuilder(
998 self._http,
999 model.response,
1000 url,
1001 method=httpMethod,
1002 body=body,
1003 headers=headers,
1004 methodId=methodId,
1005 resumable=resumable,
1006 )
1007
1008 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1009 if len(parameters.argmap) > 0:
1010 docs.append("Args:\n")
1011
1012 # Skip undocumented params and params common to all methods.
1013 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1014 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1015
1016 all_args = list(parameters.argmap.keys())
1017 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1018
1019 # Move body to the front of the line.
1020 if "body" in all_args:
1021 args_ordered.append("body")
1022
1023 for name in all_args:
1024 if name not in args_ordered:
1025 args_ordered.append(name)
1026
1027 for arg in args_ordered:
1028 if arg in skip_parameters:
1029 continue
1030
1031 repeated = ""
1032 if arg in parameters.repeated_params:
1033 repeated = " (repeated)"
1034 required = ""
1035 if arg in parameters.required_params:
1036 required = " (required)"
1037 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1038 paramdoc = paramdesc.get("description", "A parameter")
1039 if "$ref" in paramdesc:
1040 docs.append(
1041 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
1042 % (
1043 arg,
1044 paramdoc,
1045 required,
1046 repeated,
1047 schema.prettyPrintByName(paramdesc["$ref"]),
1048 )
1049 )
John Asmuth864311d2014-04-24 15:46:08 -04001050 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001051 paramtype = paramdesc.get("type", "string")
1052 docs.append(
1053 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1054 )
1055 enum = paramdesc.get("enum", [])
1056 enumDesc = paramdesc.get("enumDescriptions", [])
1057 if enum and enumDesc:
1058 docs.append(" Allowed values\n")
1059 for (name, desc) in zip(enum, enumDesc):
1060 docs.append(" %s - %s\n" % (name, desc))
1061 if "response" in methodDesc:
1062 if methodName.endswith("_media"):
1063 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001064 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001065 docs.append("\nReturns:\n An object of the form:\n\n ")
1066 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001067
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001068 setattr(method, "__doc__", "".join(docs))
1069 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001070
1071
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001072def createNextMethod(
1073 methodName,
1074 pageTokenName="pageToken",
1075 nextPageTokenName="nextPageToken",
1076 isPageTokenParameter=True,
1077):
1078 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001079
1080 The _next methods allow for easy iteration through list() responses.
1081
1082 Args:
1083 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001084 pageTokenName: string, name of request page token field.
1085 nextPageTokenName: string, name of response page token field.
1086 isPageTokenParameter: Boolean, True if request page token is a query
1087 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001088 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001089 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001090
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001091 def methodNext(self, previous_request, previous_response):
1092 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001093
1094Args:
1095 previous_request: The request for the previous page. (required)
1096 previous_response: The response from the request for the previous page. (required)
1097
1098Returns:
1099 A request object that you can call 'execute()' on to request the next
1100 page. Returns None if there are no more items in the collection.
1101 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001102 # Retrieve nextPageToken from previous_response
1103 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001104
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001105 nextPageToken = previous_response.get(nextPageTokenName, None)
1106 if not nextPageToken:
1107 return None
John Asmuth864311d2014-04-24 15:46:08 -04001108
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001109 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001110
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001111 if isPageTokenParameter:
1112 # Replace pageToken value in URI
1113 request.uri = _add_query_parameter(
1114 request.uri, pageTokenName, nextPageToken
1115 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001116 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001117 else:
1118 # Replace pageToken value in request body
1119 model = self._model
1120 body = model.deserialize(request.body)
1121 body[pageTokenName] = nextPageToken
1122 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001123 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001124
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001125 return request
John Asmuth864311d2014-04-24 15:46:08 -04001126
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001127 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001128
1129
1130class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001131 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001132
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001133 def __init__(
1134 self,
1135 http,
1136 baseUrl,
1137 model,
1138 requestBuilder,
1139 developerKey,
1140 resourceDesc,
1141 rootDesc,
1142 schema,
1143 ):
1144 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001145
1146 Args:
1147 http: httplib2.Http, Object to make http requests with.
1148 baseUrl: string, base URL for the API. All requests are relative to this
1149 URI.
1150 model: googleapiclient.Model, converts to and from the wire format.
1151 requestBuilder: class or callable that instantiates an
1152 googleapiclient.HttpRequest object.
1153 developerKey: string, key obtained from
1154 https://code.google.com/apis/console
1155 resourceDesc: object, section of deserialized discovery document that
1156 describes a resource. Note that the top level discovery document
1157 is considered a resource.
1158 rootDesc: object, the entire deserialized discovery document.
1159 schema: object, mapping of schema names to schema descriptions.
1160 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001161 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001162
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001163 self._http = http
1164 self._baseUrl = baseUrl
1165 self._model = model
1166 self._developerKey = developerKey
1167 self._requestBuilder = requestBuilder
1168 self._resourceDesc = resourceDesc
1169 self._rootDesc = rootDesc
1170 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001171
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001172 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001173
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001174 def _set_dynamic_attr(self, attr_name, value):
1175 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001176
1177 Args:
1178 attr_name: string; The name of the attribute to be set
1179 value: The value being set on the object and tracked in the dynamic cache.
1180 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001181 self._dynamic_attrs.append(attr_name)
1182 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001183
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001184 def __getstate__(self):
1185 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001186
1187 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1188 will be wiped and restored on pickle serialization.
1189 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001190 state_dict = copy.copy(self.__dict__)
1191 for dynamic_attr in self._dynamic_attrs:
1192 del state_dict[dynamic_attr]
1193 del state_dict["_dynamic_attrs"]
1194 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001195
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001196 def __setstate__(self, state):
1197 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001198
1199 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1200 will be wiped and restored on pickle serialization.
1201 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001202 self.__dict__.update(state)
1203 self._dynamic_attrs = []
1204 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001205
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001206 def _set_service_methods(self):
1207 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1208 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1209 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001210
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001211 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1212 # If this is the root Resource, add a new_batch_http_request() method.
1213 if resourceDesc == rootDesc:
1214 batch_uri = "%s%s" % (
1215 rootDesc["rootUrl"],
1216 rootDesc.get("batchPath", "batch"),
1217 )
1218
1219 def new_batch_http_request(callback=None):
1220 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001221
1222 Args:
1223 callback: callable, A callback to be called for each response, of the
1224 form callback(id, response, exception). The first parameter is the
1225 request id, and the second is the deserialized response object. The
1226 third is an apiclient.errors.HttpError exception object if an HTTP
1227 error occurred while processing the request, or None if no error
1228 occurred.
1229
1230 Returns:
1231 A BatchHttpRequest object based on the discovery document.
1232 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001233 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001235 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001236
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001237 # Add basic methods to Resource
1238 if "methods" in resourceDesc:
1239 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1240 fixedMethodName, method = createMethod(
1241 methodName, methodDesc, rootDesc, schema
1242 )
1243 self._set_dynamic_attr(
1244 fixedMethodName, method.__get__(self, self.__class__)
1245 )
1246 # Add in _media methods. The functionality of the attached method will
1247 # change when it sees that the method name ends in _media.
1248 if methodDesc.get("supportsMediaDownload", False):
1249 fixedMethodName, method = createMethod(
1250 methodName + "_media", methodDesc, rootDesc, schema
1251 )
1252 self._set_dynamic_attr(
1253 fixedMethodName, method.__get__(self, self.__class__)
1254 )
John Asmuth864311d2014-04-24 15:46:08 -04001255
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001256 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1257 # Add in nested resources
1258 if "resources" in resourceDesc:
1259
1260 def createResourceMethod(methodName, methodDesc):
1261 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001262
1263 Args:
1264 methodName: string, name of the method to use.
1265 methodDesc: object, fragment of deserialized discovery document that
1266 describes the method.
1267 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001268 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001269
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001270 def methodResource(self):
1271 return Resource(
1272 http=self._http,
1273 baseUrl=self._baseUrl,
1274 model=self._model,
1275 developerKey=self._developerKey,
1276 requestBuilder=self._requestBuilder,
1277 resourceDesc=methodDesc,
1278 rootDesc=rootDesc,
1279 schema=schema,
1280 )
John Asmuth864311d2014-04-24 15:46:08 -04001281
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001282 setattr(methodResource, "__doc__", "A collection resource.")
1283 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001284
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001285 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001287 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1288 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1289 self._set_dynamic_attr(
1290 fixedMethodName, method.__get__(self, self.__class__)
1291 )
John Asmuth864311d2014-04-24 15:46:08 -04001292
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001293 def _add_next_methods(self, resourceDesc, schema):
1294 # Add _next() methods if and only if one of the names 'pageToken' or
1295 # 'nextPageToken' occurs among the fields of both the method's response
1296 # type either the method's request (query parameters) or request body.
1297 if "methods" not in resourceDesc:
1298 return
1299 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1300 nextPageTokenName = _findPageTokenName(
1301 _methodProperties(methodDesc, schema, "response")
1302 )
1303 if not nextPageTokenName:
1304 continue
1305 isPageTokenParameter = True
1306 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1307 if not pageTokenName:
1308 isPageTokenParameter = False
1309 pageTokenName = _findPageTokenName(
1310 _methodProperties(methodDesc, schema, "request")
1311 )
1312 if not pageTokenName:
1313 continue
1314 fixedMethodName, method = createNextMethod(
1315 methodName + "_next",
1316 pageTokenName,
1317 nextPageTokenName,
1318 isPageTokenParameter,
1319 )
1320 self._set_dynamic_attr(
1321 fixedMethodName, method.__get__(self, self.__class__)
1322 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001323
1324
1325def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001326 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001327
1328 Args:
1329 fields: container of string, names of fields.
1330
1331 Returns:
1332 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1333 otherwise None.
1334 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001335 return next(
1336 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1337 )
1338
Thomas Coffee20af04d2017-02-10 15:24:44 -08001339
1340def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001341 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001342
1343 Args:
1344 methodDesc: object, fragment of deserialized discovery document that
1345 describes the method.
1346 schema: object, mapping of schema names to schema descriptions.
1347 name: string, name of top-level field in method description.
1348
1349 Returns:
1350 Object representing fragment of deserialized discovery document
1351 corresponding to 'properties' field of object corresponding to named field
1352 in method description, if it exists, otherwise empty dict.
1353 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001354 desc = methodDesc.get(name, {})
1355 if "$ref" in desc:
1356 desc = schema.get(desc["$ref"], {})
1357 return desc.get("properties", {})