blob: 115609f3e4aa039a8903ee0285ca133362ffccd3 [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 )
444 if client_options and client_options.client_encrypted_cert_source:
445 client_cert_to_use = client_options.client_encrypted_cert_source
446 elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
447 client_cert_to_use = mtls.default_client_encrypted_cert_source(
448 adc_cert_path, adc_key_path
449 )
450 if client_cert_to_use:
451 cert_path, key_path, passphrase = client_cert_to_use()
452
453 # The http object we built could be google_auth_httplib2.AuthorizedHttp
454 # or httplib2.Http. In the first case we need to extract the wrapped
455 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
456 http_channel = (
457 http.http
458 if google_auth_httplib2
459 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
460 else http
461 )
462 http_channel.add_certificate(key_path, cert_path, "", passphrase)
463
464 # If user doesn't provide api endpoint via client options, decide which
465 # api endpoint to use.
466 if "mtlsRootUrl" in service and (
467 not client_options or not client_options.api_endpoint
468 ):
469 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
470 use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "Never")
471
472 if not use_mtls_env in ("Never", "Auto", "Always"):
473 raise MutualTLSChannelError(
474 "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: Never, Auto, Always"
475 )
476
477 # Switch to mTLS endpoint, if environment variable is "Always", or
478 # environment varibable is "Auto" and client cert exists.
479 if use_mtls_env == "Always" or (
480 use_mtls_env == "Auto" and client_cert_to_use
481 ):
482 base = mtls_endpoint
483
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700484 if model is None:
485 features = service.get("features", [])
486 model = JsonModel("dataWrapper" in features)
487
488 return Resource(
489 http=http,
490 baseUrl=base,
491 model=model,
492 developerKey=developerKey,
493 requestBuilder=requestBuilder,
494 resourceDesc=service,
495 rootDesc=service,
496 schema=schema,
497 )
John Asmuth864311d2014-04-24 15:46:08 -0400498
499
500def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700501 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400502
503 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
504 JSON Schema.
505
506 Args:
507 value: any, the value to convert
508 schema_type: string, the type that value should be interpreted as
509
510 Returns:
511 A string representation of 'value' based on the schema_type.
512 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700513 if schema_type == "string":
514 if type(value) == type("") or type(value) == type(u""):
515 return value
516 else:
517 return str(value)
518 elif schema_type == "integer":
519 return str(int(value))
520 elif schema_type == "number":
521 return str(float(value))
522 elif schema_type == "boolean":
523 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400524 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700525 if type(value) == type("") or type(value) == type(u""):
526 return value
527 else:
528 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400529
530
531def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700532 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400533
534 Args:
535 maxSize: string, size as a string, such as 2MB or 7GB.
536
537 Returns:
538 The size as an integer value.
539 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700540 if len(maxSize) < 2:
541 return 0
542 units = maxSize[-2:].upper()
543 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
544 if bit_shift is not None:
545 return int(maxSize[:-2]) << bit_shift
546 else:
547 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400548
549
550def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700551 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400552
553 Constructed using the API root URI and service path from the discovery
554 document and the relative path for the API method.
555
556 Args:
557 root_desc: Dictionary; the entire original deserialized discovery document.
558 path_url: String; the relative URL for the API method. Relative to the API
559 root, which is specified in the discovery document.
560
561 Returns:
562 String; the absolute URI for media upload for the API method.
563 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700564 return "%(root)supload/%(service_path)s%(path)s" % {
565 "root": root_desc["rootUrl"],
566 "service_path": root_desc["servicePath"],
567 "path": path_url,
568 }
John Asmuth864311d2014-04-24 15:46:08 -0400569
570
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900571def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700572 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400573
574 Specifically, adds whatever global parameters are specified by the API to the
575 parameters for the individual method. Also adds parameters which don't
576 appear in the discovery document, but are available to all discovery based
577 APIs (these are listed in STACK_QUERY_PARAMETERS).
578
579 SIDE EFFECTS: This updates the parameters dictionary object in the method
580 description.
581
582 Args:
583 method_desc: Dictionary with metadata describing an API method. Value comes
584 from the dictionary of methods stored in the 'methods' key in the
585 deserialized discovery document.
586 root_desc: Dictionary; the entire original deserialized discovery document.
587 http_method: String; the HTTP method used to call the API method described
588 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900589 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400590
591 Returns:
592 The updated Dictionary stored in the 'parameters' key of the method
593 description dictionary.
594 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700595 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400596
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700597 # Add in the parameters common to all methods.
598 for name, description in six.iteritems(root_desc.get("parameters", {})):
599 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400600
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700601 # Add in undocumented query parameters.
602 for name in STACK_QUERY_PARAMETERS:
603 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400604
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700605 # Add 'body' (our own reserved word) to parameters if the method supports
606 # a request payload.
607 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
608 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
609 body.update(method_desc["request"])
610 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400611
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700612 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400613
614
615def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700616 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400617
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700618 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
619 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400620
621 Args:
622 method_desc: Dictionary with metadata describing an API method. Value comes
623 from the dictionary of methods stored in the 'methods' key in the
624 deserialized discovery document.
625 root_desc: Dictionary; the entire original deserialized discovery document.
626 path_url: String; the relative URL for the API method. Relative to the API
627 root, which is specified in the discovery document.
628 parameters: A dictionary describing method parameters for method described
629 in method_desc.
630
631 Returns:
632 Triple (accept, max_size, media_path_url) where:
633 - accept is a list of strings representing what content types are
634 accepted for media upload. Defaults to empty list if not in the
635 discovery document.
636 - max_size is a long representing the max size in bytes allowed for a
637 media upload. Defaults to 0L if not in the discovery document.
638 - media_path_url is a String; the absolute URI for media upload for the
639 API method. Constructed using the API root URI and service path from
640 the discovery document and the relative path for the API method. If
641 media upload is not supported, this is None.
642 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700643 media_upload = method_desc.get("mediaUpload", {})
644 accept = media_upload.get("accept", [])
645 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
646 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400647
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700648 if media_upload:
649 media_path_url = _media_path_url_from_info(root_desc, path_url)
650 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
651 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400652
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700653 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400654
655
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900656def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700657 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400658
659 SIDE EFFECTS: Changes the parameters dictionary in the method description with
660 extra parameters which are used locally.
661
662 Args:
663 method_desc: Dictionary with metadata describing an API method. Value comes
664 from the dictionary of methods stored in the 'methods' key in the
665 deserialized discovery document.
666 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900667 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400668
669 Returns:
670 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
671 where:
672 - path_url is a String; the relative URL for the API method. Relative to
673 the API root, which is specified in the discovery document.
674 - http_method is a String; the HTTP method used to call the API method
675 described in the method description.
676 - method_id is a String; the name of the RPC method associated with the
677 API method, and is in the method description in the 'id' key.
678 - accept is a list of strings representing what content types are
679 accepted for media upload. Defaults to empty list if not in the
680 discovery document.
681 - max_size is a long representing the max size in bytes allowed for a
682 media upload. Defaults to 0L if not in the discovery document.
683 - media_path_url is a String; the absolute URI for media upload for the
684 API method. Constructed using the API root URI and service path from
685 the discovery document and the relative path for the API method. If
686 media upload is not supported, this is None.
687 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700688 path_url = method_desc["path"]
689 http_method = method_desc["httpMethod"]
690 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400691
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700692 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
693 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
694 # 'parameters' key and needs to know if there is a 'body' parameter because it
695 # also sets a 'media_body' parameter.
696 accept, max_size, media_path_url = _fix_up_media_upload(
697 method_desc, root_desc, path_url, parameters
698 )
John Asmuth864311d2014-04-24 15:46:08 -0400699
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700700 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400701
702
Craig Citro7ee535d2015-02-23 10:11:14 -0800703def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700704 """Custom urljoin replacement supporting : before / in url."""
705 # In general, it's unsafe to simply join base and url. However, for
706 # the case of discovery documents, we know:
707 # * base will never contain params, query, or fragment
708 # * url will never contain a scheme or net_loc.
709 # In general, this means we can safely join on /; we just need to
710 # ensure we end up with precisely one / joining base and url. The
711 # exception here is the case of media uploads, where url will be an
712 # absolute url.
713 if url.startswith("http://") or url.startswith("https://"):
714 return urljoin(base, url)
715 new_base = base if base.endswith("/") else base + "/"
716 new_url = url[1:] if url.startswith("/") else url
717 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800718
719
John Asmuth864311d2014-04-24 15:46:08 -0400720# TODO(dhermes): Convert this class to ResourceMethod and make it callable
721class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700722 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400723
724 Attributes:
725 argmap: Map from method parameter name (string) to query parameter name
726 (string).
727 required_params: List of required parameters (represented by parameter
728 name as string).
729 repeated_params: List of repeated parameters (represented by parameter
730 name as string).
731 pattern_params: Map from method parameter name (string) to regular
732 expression (as a string). If the pattern is set for a parameter, the
733 value for that parameter must match the regular expression.
734 query_params: List of parameters (represented by parameter name as string)
735 that will be used in the query string.
736 path_params: Set of parameters (represented by parameter name as string)
737 that will be used in the base URL path.
738 param_types: Map from method parameter name (string) to parameter type. Type
739 can be any valid JSON schema type; valid values are 'any', 'array',
740 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
741 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
742 enum_params: Map from method parameter name (string) to list of strings,
743 where each list of strings is the list of acceptable enum values.
744 """
745
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700746 def __init__(self, method_desc):
747 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400748
749 Sets default values and defers to set_parameters to populate.
750
751 Args:
752 method_desc: Dictionary with metadata describing an API method. Value
753 comes from the dictionary of methods stored in the 'methods' key in
754 the deserialized discovery document.
755 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700756 self.argmap = {}
757 self.required_params = []
758 self.repeated_params = []
759 self.pattern_params = {}
760 self.query_params = []
761 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
762 # parsing is gotten rid of.
763 self.path_params = set()
764 self.param_types = {}
765 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400766
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700767 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400768
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700769 def set_parameters(self, method_desc):
770 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400771
772 Iterates through each parameter for the method and parses the values from
773 the parameter dictionary.
774
775 Args:
776 method_desc: Dictionary with metadata describing an API method. Value
777 comes from the dictionary of methods stored in the 'methods' key in
778 the deserialized discovery document.
779 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700780 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
781 param = key2param(arg)
782 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400783
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700784 if desc.get("pattern"):
785 self.pattern_params[param] = desc["pattern"]
786 if desc.get("enum"):
787 self.enum_params[param] = desc["enum"]
788 if desc.get("required"):
789 self.required_params.append(param)
790 if desc.get("repeated"):
791 self.repeated_params.append(param)
792 if desc.get("location") == "query":
793 self.query_params.append(param)
794 if desc.get("location") == "path":
795 self.path_params.add(param)
796 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400797
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700798 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
799 # should have all path parameters already marked with
800 # 'location: path'.
801 for match in URITEMPLATE.finditer(method_desc["path"]):
802 for namematch in VARNAME.finditer(match.group(0)):
803 name = key2param(namematch.group(0))
804 self.path_params.add(name)
805 if name in self.query_params:
806 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400807
808
809def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700810 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400811
812 Args:
813 methodName: string, name of the method to use.
814 methodDesc: object, fragment of deserialized discovery document that
815 describes the method.
816 rootDesc: object, the entire deserialized discovery document.
817 schema: object, mapping of schema names to schema descriptions.
818 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700819 methodName = fix_method_name(methodName)
820 (
821 pathUrl,
822 httpMethod,
823 methodId,
824 accept,
825 maxSize,
826 mediaPathUrl,
827 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400828
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700829 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400830
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700831 def method(self, **kwargs):
832 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400833
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700834 for name in six.iterkeys(kwargs):
835 if name not in parameters.argmap:
836 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400837
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700838 # Remove args that have a value of None.
839 keys = list(kwargs.keys())
840 for name in keys:
841 if kwargs[name] is None:
842 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400843
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700844 for name in parameters.required_params:
845 if name not in kwargs:
846 # temporary workaround for non-paging methods incorrectly requiring
847 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
848 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
849 _methodProperties(methodDesc, schema, "response")
850 ):
851 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400852
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700853 for name, regex in six.iteritems(parameters.pattern_params):
854 if name in kwargs:
855 if isinstance(kwargs[name], six.string_types):
856 pvalues = [kwargs[name]]
857 else:
858 pvalues = kwargs[name]
859 for pvalue in pvalues:
860 if re.match(regex, pvalue) is None:
861 raise TypeError(
862 'Parameter "%s" value "%s" does not match the pattern "%s"'
863 % (name, pvalue, regex)
864 )
865
866 for name, enums in six.iteritems(parameters.enum_params):
867 if name in kwargs:
868 # We need to handle the case of a repeated enum
869 # name differently, since we want to handle both
870 # arg='value' and arg=['value1', 'value2']
871 if name in parameters.repeated_params and not isinstance(
872 kwargs[name], six.string_types
873 ):
874 values = kwargs[name]
875 else:
876 values = [kwargs[name]]
877 for value in values:
878 if value not in enums:
879 raise TypeError(
880 'Parameter "%s" value "%s" is not an allowed value in "%s"'
881 % (name, value, str(enums))
882 )
883
884 actual_query_params = {}
885 actual_path_params = {}
886 for key, value in six.iteritems(kwargs):
887 to_type = parameters.param_types.get(key, "string")
888 # For repeated parameters we cast each member of the list.
889 if key in parameters.repeated_params and type(value) == type([]):
890 cast_value = [_cast(x, to_type) for x in value]
891 else:
892 cast_value = _cast(value, to_type)
893 if key in parameters.query_params:
894 actual_query_params[parameters.argmap[key]] = cast_value
895 if key in parameters.path_params:
896 actual_path_params[parameters.argmap[key]] = cast_value
897 body_value = kwargs.get("body", None)
898 media_filename = kwargs.get("media_body", None)
899 media_mime_type = kwargs.get("media_mime_type", None)
900
901 if self._developerKey:
902 actual_query_params["key"] = self._developerKey
903
904 model = self._model
905 if methodName.endswith("_media"):
906 model = MediaModel()
907 elif "response" not in methodDesc:
908 model = RawModel()
909
910 headers = {}
911 headers, params, query, body = model.request(
912 headers, actual_path_params, actual_query_params, body_value
913 )
914
915 expanded_url = uritemplate.expand(pathUrl, params)
916 url = _urljoin(self._baseUrl, expanded_url + query)
917
918 resumable = None
919 multipart_boundary = ""
920
921 if media_filename:
922 # Ensure we end up with a valid MediaUpload object.
923 if isinstance(media_filename, six.string_types):
924 if media_mime_type is None:
925 logger.warning(
926 "media_mime_type argument not specified: trying to auto-detect for %s",
927 media_filename,
928 )
929 media_mime_type, _ = mimetypes.guess_type(media_filename)
930 if media_mime_type is None:
931 raise UnknownFileType(media_filename)
932 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
933 raise UnacceptableMimeTypeError(media_mime_type)
934 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
935 elif isinstance(media_filename, MediaUpload):
936 media_upload = media_filename
937 else:
938 raise TypeError("media_filename must be str or MediaUpload.")
939
940 # Check the maxSize
941 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
942 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
943
944 # Use the media path uri for media uploads
945 expanded_url = uritemplate.expand(mediaPathUrl, params)
946 url = _urljoin(self._baseUrl, expanded_url + query)
947 if media_upload.resumable():
948 url = _add_query_parameter(url, "uploadType", "resumable")
949
950 if media_upload.resumable():
951 # This is all we need to do for resumable, if the body exists it gets
952 # sent in the first request, otherwise an empty body is sent.
953 resumable = media_upload
954 else:
955 # A non-resumable upload
956 if body is None:
957 # This is a simple media upload
958 headers["content-type"] = media_upload.mimetype()
959 body = media_upload.getbytes(0, media_upload.size())
960 url = _add_query_parameter(url, "uploadType", "media")
961 else:
962 # This is a multipart/related upload.
963 msgRoot = MIMEMultipart("related")
964 # msgRoot should not write out it's own headers
965 setattr(msgRoot, "_write_headers", lambda self: None)
966
967 # attach the body as one part
968 msg = MIMENonMultipart(*headers["content-type"].split("/"))
969 msg.set_payload(body)
970 msgRoot.attach(msg)
971
972 # attach the media as the second part
973 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
974 msg["Content-Transfer-Encoding"] = "binary"
975
976 payload = media_upload.getbytes(0, media_upload.size())
977 msg.set_payload(payload)
978 msgRoot.attach(msg)
979 # encode the body: note that we can't use `as_string`, because
980 # it plays games with `From ` lines.
981 fp = BytesIO()
982 g = _BytesGenerator(fp, mangle_from_=False)
983 g.flatten(msgRoot, unixfrom=False)
984 body = fp.getvalue()
985
986 multipart_boundary = msgRoot.get_boundary()
987 headers["content-type"] = (
988 "multipart/related; " 'boundary="%s"'
989 ) % multipart_boundary
990 url = _add_query_parameter(url, "uploadType", "multipart")
991
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700992 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700993 return self._requestBuilder(
994 self._http,
995 model.response,
996 url,
997 method=httpMethod,
998 body=body,
999 headers=headers,
1000 methodId=methodId,
1001 resumable=resumable,
1002 )
1003
1004 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1005 if len(parameters.argmap) > 0:
1006 docs.append("Args:\n")
1007
1008 # Skip undocumented params and params common to all methods.
1009 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1010 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1011
1012 all_args = list(parameters.argmap.keys())
1013 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1014
1015 # Move body to the front of the line.
1016 if "body" in all_args:
1017 args_ordered.append("body")
1018
1019 for name in all_args:
1020 if name not in args_ordered:
1021 args_ordered.append(name)
1022
1023 for arg in args_ordered:
1024 if arg in skip_parameters:
1025 continue
1026
1027 repeated = ""
1028 if arg in parameters.repeated_params:
1029 repeated = " (repeated)"
1030 required = ""
1031 if arg in parameters.required_params:
1032 required = " (required)"
1033 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1034 paramdoc = paramdesc.get("description", "A parameter")
1035 if "$ref" in paramdesc:
1036 docs.append(
1037 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
1038 % (
1039 arg,
1040 paramdoc,
1041 required,
1042 repeated,
1043 schema.prettyPrintByName(paramdesc["$ref"]),
1044 )
1045 )
John Asmuth864311d2014-04-24 15:46:08 -04001046 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001047 paramtype = paramdesc.get("type", "string")
1048 docs.append(
1049 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1050 )
1051 enum = paramdesc.get("enum", [])
1052 enumDesc = paramdesc.get("enumDescriptions", [])
1053 if enum and enumDesc:
1054 docs.append(" Allowed values\n")
1055 for (name, desc) in zip(enum, enumDesc):
1056 docs.append(" %s - %s\n" % (name, desc))
1057 if "response" in methodDesc:
1058 if methodName.endswith("_media"):
1059 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001060 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001061 docs.append("\nReturns:\n An object of the form:\n\n ")
1062 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001063
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001064 setattr(method, "__doc__", "".join(docs))
1065 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001066
1067
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001068def createNextMethod(
1069 methodName,
1070 pageTokenName="pageToken",
1071 nextPageTokenName="nextPageToken",
1072 isPageTokenParameter=True,
1073):
1074 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001075
1076 The _next methods allow for easy iteration through list() responses.
1077
1078 Args:
1079 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001080 pageTokenName: string, name of request page token field.
1081 nextPageTokenName: string, name of response page token field.
1082 isPageTokenParameter: Boolean, True if request page token is a query
1083 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001084 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001085 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001086
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001087 def methodNext(self, previous_request, previous_response):
1088 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001089
1090Args:
1091 previous_request: The request for the previous page. (required)
1092 previous_response: The response from the request for the previous page. (required)
1093
1094Returns:
1095 A request object that you can call 'execute()' on to request the next
1096 page. Returns None if there are no more items in the collection.
1097 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001098 # Retrieve nextPageToken from previous_response
1099 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001100
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001101 nextPageToken = previous_response.get(nextPageTokenName, None)
1102 if not nextPageToken:
1103 return None
John Asmuth864311d2014-04-24 15:46:08 -04001104
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001105 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001106
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001107 if isPageTokenParameter:
1108 # Replace pageToken value in URI
1109 request.uri = _add_query_parameter(
1110 request.uri, pageTokenName, nextPageToken
1111 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001112 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001113 else:
1114 # Replace pageToken value in request body
1115 model = self._model
1116 body = model.deserialize(request.body)
1117 body[pageTokenName] = nextPageToken
1118 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001119 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001120
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001121 return request
John Asmuth864311d2014-04-24 15:46:08 -04001122
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001123 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001124
1125
1126class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001127 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001128
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001129 def __init__(
1130 self,
1131 http,
1132 baseUrl,
1133 model,
1134 requestBuilder,
1135 developerKey,
1136 resourceDesc,
1137 rootDesc,
1138 schema,
1139 ):
1140 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001141
1142 Args:
1143 http: httplib2.Http, Object to make http requests with.
1144 baseUrl: string, base URL for the API. All requests are relative to this
1145 URI.
1146 model: googleapiclient.Model, converts to and from the wire format.
1147 requestBuilder: class or callable that instantiates an
1148 googleapiclient.HttpRequest object.
1149 developerKey: string, key obtained from
1150 https://code.google.com/apis/console
1151 resourceDesc: object, section of deserialized discovery document that
1152 describes a resource. Note that the top level discovery document
1153 is considered a resource.
1154 rootDesc: object, the entire deserialized discovery document.
1155 schema: object, mapping of schema names to schema descriptions.
1156 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001157 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001158
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001159 self._http = http
1160 self._baseUrl = baseUrl
1161 self._model = model
1162 self._developerKey = developerKey
1163 self._requestBuilder = requestBuilder
1164 self._resourceDesc = resourceDesc
1165 self._rootDesc = rootDesc
1166 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001167
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001168 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001169
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001170 def _set_dynamic_attr(self, attr_name, value):
1171 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001172
1173 Args:
1174 attr_name: string; The name of the attribute to be set
1175 value: The value being set on the object and tracked in the dynamic cache.
1176 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001177 self._dynamic_attrs.append(attr_name)
1178 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001179
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001180 def __getstate__(self):
1181 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001182
1183 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1184 will be wiped and restored on pickle serialization.
1185 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001186 state_dict = copy.copy(self.__dict__)
1187 for dynamic_attr in self._dynamic_attrs:
1188 del state_dict[dynamic_attr]
1189 del state_dict["_dynamic_attrs"]
1190 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001191
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001192 def __setstate__(self, state):
1193 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001194
1195 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1196 will be wiped and restored on pickle serialization.
1197 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001198 self.__dict__.update(state)
1199 self._dynamic_attrs = []
1200 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001201
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001202 def _set_service_methods(self):
1203 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1204 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1205 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001206
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001207 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1208 # If this is the root Resource, add a new_batch_http_request() method.
1209 if resourceDesc == rootDesc:
1210 batch_uri = "%s%s" % (
1211 rootDesc["rootUrl"],
1212 rootDesc.get("batchPath", "batch"),
1213 )
1214
1215 def new_batch_http_request(callback=None):
1216 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001217
1218 Args:
1219 callback: callable, A callback to be called for each response, of the
1220 form callback(id, response, exception). The first parameter is the
1221 request id, and the second is the deserialized response object. The
1222 third is an apiclient.errors.HttpError exception object if an HTTP
1223 error occurred while processing the request, or None if no error
1224 occurred.
1225
1226 Returns:
1227 A BatchHttpRequest object based on the discovery document.
1228 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001229 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001230
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001231 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001232
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001233 # Add basic methods to Resource
1234 if "methods" in resourceDesc:
1235 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1236 fixedMethodName, method = createMethod(
1237 methodName, methodDesc, rootDesc, schema
1238 )
1239 self._set_dynamic_attr(
1240 fixedMethodName, method.__get__(self, self.__class__)
1241 )
1242 # Add in _media methods. The functionality of the attached method will
1243 # change when it sees that the method name ends in _media.
1244 if methodDesc.get("supportsMediaDownload", False):
1245 fixedMethodName, method = createMethod(
1246 methodName + "_media", methodDesc, rootDesc, schema
1247 )
1248 self._set_dynamic_attr(
1249 fixedMethodName, method.__get__(self, self.__class__)
1250 )
John Asmuth864311d2014-04-24 15:46:08 -04001251
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001252 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1253 # Add in nested resources
1254 if "resources" in resourceDesc:
1255
1256 def createResourceMethod(methodName, methodDesc):
1257 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001258
1259 Args:
1260 methodName: string, name of the method to use.
1261 methodDesc: object, fragment of deserialized discovery document that
1262 describes the method.
1263 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001264 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001266 def methodResource(self):
1267 return Resource(
1268 http=self._http,
1269 baseUrl=self._baseUrl,
1270 model=self._model,
1271 developerKey=self._developerKey,
1272 requestBuilder=self._requestBuilder,
1273 resourceDesc=methodDesc,
1274 rootDesc=rootDesc,
1275 schema=schema,
1276 )
John Asmuth864311d2014-04-24 15:46:08 -04001277
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001278 setattr(methodResource, "__doc__", "A collection resource.")
1279 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001280
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001281 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001282
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001283 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1284 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1285 self._set_dynamic_attr(
1286 fixedMethodName, method.__get__(self, self.__class__)
1287 )
John Asmuth864311d2014-04-24 15:46:08 -04001288
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001289 def _add_next_methods(self, resourceDesc, schema):
1290 # Add _next() methods if and only if one of the names 'pageToken' or
1291 # 'nextPageToken' occurs among the fields of both the method's response
1292 # type either the method's request (query parameters) or request body.
1293 if "methods" not in resourceDesc:
1294 return
1295 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1296 nextPageTokenName = _findPageTokenName(
1297 _methodProperties(methodDesc, schema, "response")
1298 )
1299 if not nextPageTokenName:
1300 continue
1301 isPageTokenParameter = True
1302 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1303 if not pageTokenName:
1304 isPageTokenParameter = False
1305 pageTokenName = _findPageTokenName(
1306 _methodProperties(methodDesc, schema, "request")
1307 )
1308 if not pageTokenName:
1309 continue
1310 fixedMethodName, method = createNextMethod(
1311 methodName + "_next",
1312 pageTokenName,
1313 nextPageTokenName,
1314 isPageTokenParameter,
1315 )
1316 self._set_dynamic_attr(
1317 fixedMethodName, method.__get__(self, self.__class__)
1318 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001319
1320
1321def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001322 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001323
1324 Args:
1325 fields: container of string, names of fields.
1326
1327 Returns:
1328 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1329 otherwise None.
1330 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001331 return next(
1332 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1333 )
1334
Thomas Coffee20af04d2017-02-10 15:24:44 -08001335
1336def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001337 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001338
1339 Args:
1340 methodDesc: object, fragment of deserialized discovery document that
1341 describes the method.
1342 schema: object, mapping of schema names to schema descriptions.
1343 name: string, name of top-level field in method description.
1344
1345 Returns:
1346 Object representing fragment of deserialized discovery document
1347 corresponding to 'properties' field of object corresponding to named field
1348 in method description, if it exists, otherwise empty dict.
1349 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001350 desc = methodDesc.get(name, {})
1351 if "$ref" in desc:
1352 desc = schema.get(desc["$ref"], {})
1353 return desc.get("properties", {})