blob: c7981157f221a225e8419b6a61361e84e36de523 [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,
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700190 num_retries=1,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700191):
192 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400193
194 Construct a Resource object for interacting with an API. The serviceName and
195 version are the names from the Discovery service.
196
197 Args:
198 serviceName: string, name of the service.
199 version: string, the version of the service.
200 http: httplib2.Http, An instance of httplib2.Http or something that acts
201 like it that HTTP requests will be made through.
202 discoveryServiceUrl: string, a URI Template that points to the location of
203 the discovery service. It should have two parameters {api} and
204 {apiVersion} that when filled in produce an absolute URI to the discovery
205 document for that service.
206 developerKey: string, key obtained from
207 https://code.google.com/apis/console.
208 model: googleapiclient.Model, converts to and from the wire format.
209 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
210 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800211 credentials: oauth2client.Credentials or
212 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700213 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700214 cache_discovery: Boolean, whether or not to cache the discovery doc.
215 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
216 cache object for the discovery documents.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300217 client_options: Mapping object or google.api_core.client_options, client
218 options to set user options on the client. The API endpoint should be set
219 through client_options. client_cert_source is not supported, client cert
220 should be provided using client_encrypted_cert_source instead.
arithmetic1728981eadf2020-06-02 10:20:10 -0700221 adc_cert_path: str, client certificate file path to save the application
222 default client certificate for mTLS. This field is required if you want to
223 use the default client certificate.
224 adc_key_path: str, client encrypted private key file path to save the
225 application default client encrypted private key for mTLS. This field is
226 required if you want to use the default client certificate.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700227 num_retries: Integer, number of times to retry discovery with
228 randomized exponential backoff in case of intermittent/connection issues.
John Asmuth864311d2014-04-24 15:46:08 -0400229
230 Returns:
231 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700232
233 Raises:
234 google.auth.exceptions.MutualTLSChannelError: if there are any problems
235 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400236 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700237 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400238
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700239 if http is None:
240 discovery_http = build_http()
241 else:
242 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700244 for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI):
245 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400246
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700247 try:
248 content = _retrieve_discovery_doc(
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700249 requested_url, discovery_http, cache_discovery, cache,
250 developerKey, num_retries=num_retries
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700251 )
252 return build_from_document(
253 content,
254 base=discovery_url,
255 http=http,
256 developerKey=developerKey,
257 model=model,
258 requestBuilder=requestBuilder,
259 credentials=credentials,
arithmetic1728981eadf2020-06-02 10:20:10 -0700260 client_options=client_options,
261 adc_cert_path=adc_cert_path,
262 adc_key_path=adc_key_path,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700263 )
264 except HttpError as e:
265 if e.resp.status == http_client.NOT_FOUND:
266 continue
267 else:
268 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700269
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700270 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
Takashi Matsuo30125122015-08-19 11:42:32 -0700271
272
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700273def _retrieve_discovery_doc(url, http, cache_discovery,
274 cache=None, developerKey=None, num_retries=1):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700275 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700276
277 Args:
278 url: string, the URL of the discovery document.
279 http: httplib2.Http, An instance of httplib2.Http or something that acts
280 like it through which HTTP requests will be made.
281 cache_discovery: Boolean, whether or not to cache the discovery doc.
282 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
283 object for the discovery documents.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700284 developerKey: string, Key for controlling API usage, generated
285 from the API Console.
286 num_retries: Integer, number of times to retry discovery with
287 randomized exponential backoff in case of intermittent/connection issues.
Takashi Matsuo30125122015-08-19 11:42:32 -0700288
289 Returns:
290 A unicode string representation of the discovery document.
291 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700292 if cache_discovery:
293 from . import discovery_cache
Takashi Matsuo30125122015-08-19 11:42:32 -0700294
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700295 if cache is None:
296 cache = discovery_cache.autodetect()
297 if cache:
298 content = cache.get(url)
299 if content:
300 return content
John Asmuth864311d2014-04-24 15:46:08 -0400301
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 actual_url = url
303 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
304 # variable that contains the network address of the client sending the
305 # request. If it exists then add that to the request for the discovery
306 # document to avoid exceeding the quota on discovery requests.
307 if "REMOTE_ADDR" in os.environ:
308 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
309 if developerKey:
310 actual_url = _add_query_parameter(url, "key", developerKey)
Bu Sun Kim3bf27812020-04-28 09:39:09 -0700311 logger.debug("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400312
Dmitry Frenkelf3348f92020-07-15 13:05:58 -0700313 # Execute this request with retries build into HttpRequest
314 # Note that it will already raise an error if we don't get a 2xx response
315 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
316 resp, content = req.execute(num_retries=num_retries)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800317
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700318 try:
319 content = content.decode("utf-8")
320 except AttributeError:
321 pass
322
323 try:
324 service = json.loads(content)
325 except ValueError as e:
326 logger.error("Failed to parse as JSON: " + content)
327 raise InvalidJsonError()
328 if cache_discovery and cache:
329 cache.set(url, content)
330 return content
John Asmuth864311d2014-04-24 15:46:08 -0400331
332
333@positional(1)
334def build_from_document(
335 service,
336 base=None,
337 future=None,
338 http=None,
339 developerKey=None,
340 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700341 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700342 credentials=None,
arithmetic1728981eadf2020-06-02 10:20:10 -0700343 client_options=None,
344 adc_cert_path=None,
345 adc_key_path=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700346):
347 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400348
349 Same as `build()`, but constructs the Resource object from a discovery
350 document that is it given, as opposed to retrieving one over HTTP.
351
352 Args:
353 service: string or object, the JSON discovery document describing the API.
354 The value passed in may either be the JSON string or the deserialized
355 JSON.
356 base: string, base URI for all HTTP requests, usually the discovery URI.
357 This parameter is no longer used as rootUrl and servicePath are included
358 within the discovery document. (deprecated)
359 future: string, discovery document with future capabilities (deprecated).
360 http: httplib2.Http, An instance of httplib2.Http or something that acts
361 like it that HTTP requests will be made through.
362 developerKey: string, Key for controlling API usage, generated
363 from the API Console.
364 model: Model class instance that serializes and de-serializes requests and
365 responses.
366 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800367 credentials: oauth2client.Credentials or
368 google.auth.credentials.Credentials, credentials to be used for
369 authentication.
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300370 client_options: Mapping object or google.api_core.client_options, client
371 options to set user options on the client. The API endpoint should be set
372 through client_options. client_cert_source is not supported, client cert
373 should be provided using client_encrypted_cert_source instead.
arithmetic1728981eadf2020-06-02 10:20:10 -0700374 adc_cert_path: str, client certificate file path to save the application
375 default client certificate for mTLS. This field is required if you want to
376 use the default client certificate.
377 adc_key_path: str, client encrypted private key file path to save the
378 application default client encrypted private key for mTLS. This field is
379 required if you want to use the default client certificate.
John Asmuth864311d2014-04-24 15:46:08 -0400380
381 Returns:
382 A Resource object with methods for interacting with the service.
arithmetic1728981eadf2020-06-02 10:20:10 -0700383
384 Raises:
385 google.auth.exceptions.MutualTLSChannelError: if there are any problems
386 setting up mutual TLS channel.
John Asmuth864311d2014-04-24 15:46:08 -0400387 """
388
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700389 if http is not None and credentials is not None:
390 raise ValueError("Arguments http and credentials are mutually exclusive.")
John Asmuth864311d2014-04-24 15:46:08 -0400391
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700392 if isinstance(service, six.string_types):
393 service = json.loads(service)
394 elif isinstance(service, six.binary_type):
395 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400396
arithmetic1728981eadf2020-06-02 10:20:10 -0700397 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700398 logger.error(
399 "You are using HttpMock or HttpMockSequence without"
400 + "having the service discovery doc in cache. Try calling "
401 + "build() without mocking once first to populate the "
402 + "cache."
403 )
404 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400405
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700406 # If an API Endpoint is provided on client options, use that as the base URL
arithmetic1728981eadf2020-06-02 10:20:10 -0700407 base = urljoin(service["rootUrl"], service["servicePath"])
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700408 if client_options:
Pavel Kiselev21af37b2020-06-18 19:50:03 +0300409 if isinstance(client_options, six.moves.collections_abc.Mapping):
arithmetic1728981eadf2020-06-02 10:20:10 -0700410 client_options = google.api_core.client_options.from_dict(client_options)
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700411 if client_options.api_endpoint:
412 base = client_options.api_endpoint
413
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700414 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400415
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700416 # If the http client is not specified, then we must construct an http client
417 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800418 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700419 if http is None:
420 # Does the service require scopes?
421 scopes = list(
422 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
423 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700424
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700425 # If so, then the we need to setup authentication if no developerKey is
426 # specified.
427 if scopes and not developerKey:
428 # If the user didn't pass in credentials, attempt to acquire application
429 # default credentials.
430 if credentials is None:
431 credentials = _auth.default_credentials()
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800432
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700433 # The credentials need to be scoped.
434 credentials = _auth.with_scopes(credentials, scopes)
435
436 # If credentials are provided, create an authorized http instance;
437 # otherwise, skip authentication.
438 if credentials:
439 http = _auth.authorized_http(credentials)
440
441 # If the service doesn't require scopes then there is no need for
442 # authentication.
443 else:
444 http = build_http()
445
arithmetic1728981eadf2020-06-02 10:20:10 -0700446 # Obtain client cert and create mTLS http channel if cert exists.
447 client_cert_to_use = None
448 if client_options and client_options.client_cert_source:
449 raise MutualTLSChannelError(
450 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
451 )
arithmetic172842028ed2020-06-02 11:24:04 -0700452 if (
453 client_options
454 and hasattr(client_options, "client_encrypted_cert_source")
455 and client_options.client_encrypted_cert_source
456 ):
arithmetic1728981eadf2020-06-02 10:20:10 -0700457 client_cert_to_use = client_options.client_encrypted_cert_source
458 elif adc_cert_path and adc_key_path and mtls.has_default_client_cert_source():
459 client_cert_to_use = mtls.default_client_encrypted_cert_source(
460 adc_cert_path, adc_key_path
461 )
462 if client_cert_to_use:
463 cert_path, key_path, passphrase = client_cert_to_use()
464
465 # The http object we built could be google_auth_httplib2.AuthorizedHttp
466 # or httplib2.Http. In the first case we need to extract the wrapped
467 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
468 http_channel = (
469 http.http
470 if google_auth_httplib2
471 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
472 else http
473 )
474 http_channel.add_certificate(key_path, cert_path, "", passphrase)
475
476 # If user doesn't provide api endpoint via client options, decide which
477 # api endpoint to use.
478 if "mtlsRootUrl" in service and (
479 not client_options or not client_options.api_endpoint
480 ):
481 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
arithmetic172819908ed2020-06-09 22:32:43 -0700482 use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS", "never")
arithmetic1728981eadf2020-06-02 10:20:10 -0700483
arithmetic172819908ed2020-06-09 22:32:43 -0700484 if not use_mtls_env in ("never", "auto", "always"):
arithmetic1728981eadf2020-06-02 10:20:10 -0700485 raise MutualTLSChannelError(
arithmetic172819908ed2020-06-09 22:32:43 -0700486 "Unsupported GOOGLE_API_USE_MTLS value. Accepted values: never, auto, always"
arithmetic1728981eadf2020-06-02 10:20:10 -0700487 )
488
arithmetic172819908ed2020-06-09 22:32:43 -0700489 # Switch to mTLS endpoint, if environment variable is "always", or
490 # environment varibable is "auto" and client cert exists.
491 if use_mtls_env == "always" or (
492 use_mtls_env == "auto" and client_cert_to_use
arithmetic1728981eadf2020-06-02 10:20:10 -0700493 ):
494 base = mtls_endpoint
495
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700496 if model is None:
497 features = service.get("features", [])
498 model = JsonModel("dataWrapper" in features)
499
500 return Resource(
501 http=http,
502 baseUrl=base,
503 model=model,
504 developerKey=developerKey,
505 requestBuilder=requestBuilder,
506 resourceDesc=service,
507 rootDesc=service,
508 schema=schema,
509 )
John Asmuth864311d2014-04-24 15:46:08 -0400510
511
512def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700513 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400514
515 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
516 JSON Schema.
517
518 Args:
519 value: any, the value to convert
520 schema_type: string, the type that value should be interpreted as
521
522 Returns:
523 A string representation of 'value' based on the schema_type.
524 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700525 if schema_type == "string":
526 if type(value) == type("") or type(value) == type(u""):
527 return value
528 else:
529 return str(value)
530 elif schema_type == "integer":
531 return str(int(value))
532 elif schema_type == "number":
533 return str(float(value))
534 elif schema_type == "boolean":
535 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400536 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700537 if type(value) == type("") or type(value) == type(u""):
538 return value
539 else:
540 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400541
542
543def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700544 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400545
546 Args:
547 maxSize: string, size as a string, such as 2MB or 7GB.
548
549 Returns:
550 The size as an integer value.
551 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700552 if len(maxSize) < 2:
553 return 0
554 units = maxSize[-2:].upper()
555 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
556 if bit_shift is not None:
557 return int(maxSize[:-2]) << bit_shift
558 else:
559 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400560
561
562def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700563 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400564
565 Constructed using the API root URI and service path from the discovery
566 document and the relative path for the API method.
567
568 Args:
569 root_desc: Dictionary; the entire original deserialized discovery document.
570 path_url: String; the relative URL for the API method. Relative to the API
571 root, which is specified in the discovery document.
572
573 Returns:
574 String; the absolute URI for media upload for the API method.
575 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700576 return "%(root)supload/%(service_path)s%(path)s" % {
577 "root": root_desc["rootUrl"],
578 "service_path": root_desc["servicePath"],
579 "path": path_url,
580 }
John Asmuth864311d2014-04-24 15:46:08 -0400581
582
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900583def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700584 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400585
586 Specifically, adds whatever global parameters are specified by the API to the
587 parameters for the individual method. Also adds parameters which don't
588 appear in the discovery document, but are available to all discovery based
589 APIs (these are listed in STACK_QUERY_PARAMETERS).
590
591 SIDE EFFECTS: This updates the parameters dictionary object in the method
592 description.
593
594 Args:
595 method_desc: Dictionary with metadata describing an API method. Value comes
596 from the dictionary of methods stored in the 'methods' key in the
597 deserialized discovery document.
598 root_desc: Dictionary; the entire original deserialized discovery document.
599 http_method: String; the HTTP method used to call the API method described
600 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900601 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400602
603 Returns:
604 The updated Dictionary stored in the 'parameters' key of the method
605 description dictionary.
606 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700607 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400608
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700609 # Add in the parameters common to all methods.
610 for name, description in six.iteritems(root_desc.get("parameters", {})):
611 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400612
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700613 # Add in undocumented query parameters.
614 for name in STACK_QUERY_PARAMETERS:
615 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400616
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700617 # Add 'body' (our own reserved word) to parameters if the method supports
618 # a request payload.
619 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
620 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
621 body.update(method_desc["request"])
622 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400623
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700624 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400625
626
627def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700628 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400629
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700630 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
631 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400632
633 Args:
634 method_desc: Dictionary with metadata describing an API method. Value comes
635 from the dictionary of methods stored in the 'methods' key in the
636 deserialized discovery document.
637 root_desc: Dictionary; the entire original deserialized discovery document.
638 path_url: String; the relative URL for the API method. Relative to the API
639 root, which is specified in the discovery document.
640 parameters: A dictionary describing method parameters for method described
641 in method_desc.
642
643 Returns:
644 Triple (accept, max_size, media_path_url) where:
645 - accept is a list of strings representing what content types are
646 accepted for media upload. Defaults to empty list if not in the
647 discovery document.
648 - max_size is a long representing the max size in bytes allowed for a
649 media upload. Defaults to 0L if not in the discovery document.
650 - media_path_url is a String; the absolute URI for media upload for the
651 API method. Constructed using the API root URI and service path from
652 the discovery document and the relative path for the API method. If
653 media upload is not supported, this is None.
654 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700655 media_upload = method_desc.get("mediaUpload", {})
656 accept = media_upload.get("accept", [])
657 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
658 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400659
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700660 if media_upload:
661 media_path_url = _media_path_url_from_info(root_desc, path_url)
662 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
663 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400664
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700665 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400666
667
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900668def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700669 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400670
671 SIDE EFFECTS: Changes the parameters dictionary in the method description with
672 extra parameters which are used locally.
673
674 Args:
675 method_desc: Dictionary with metadata describing an API method. Value comes
676 from the dictionary of methods stored in the 'methods' key in the
677 deserialized discovery document.
678 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900679 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400680
681 Returns:
682 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
683 where:
684 - path_url is a String; the relative URL for the API method. Relative to
685 the API root, which is specified in the discovery document.
686 - http_method is a String; the HTTP method used to call the API method
687 described in the method description.
688 - method_id is a String; the name of the RPC method associated with the
689 API method, and is in the method description in the 'id' key.
690 - accept is a list of strings representing what content types are
691 accepted for media upload. Defaults to empty list if not in the
692 discovery document.
693 - max_size is a long representing the max size in bytes allowed for a
694 media upload. Defaults to 0L if not in the discovery document.
695 - media_path_url is a String; the absolute URI for media upload for the
696 API method. Constructed using the API root URI and service path from
697 the discovery document and the relative path for the API method. If
698 media upload is not supported, this is None.
699 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700700 path_url = method_desc["path"]
701 http_method = method_desc["httpMethod"]
702 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400703
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700704 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
705 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
706 # 'parameters' key and needs to know if there is a 'body' parameter because it
707 # also sets a 'media_body' parameter.
708 accept, max_size, media_path_url = _fix_up_media_upload(
709 method_desc, root_desc, path_url, parameters
710 )
John Asmuth864311d2014-04-24 15:46:08 -0400711
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700712 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400713
714
Craig Citro7ee535d2015-02-23 10:11:14 -0800715def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700716 """Custom urljoin replacement supporting : before / in url."""
717 # In general, it's unsafe to simply join base and url. However, for
718 # the case of discovery documents, we know:
719 # * base will never contain params, query, or fragment
720 # * url will never contain a scheme or net_loc.
721 # In general, this means we can safely join on /; we just need to
722 # ensure we end up with precisely one / joining base and url. The
723 # exception here is the case of media uploads, where url will be an
724 # absolute url.
725 if url.startswith("http://") or url.startswith("https://"):
726 return urljoin(base, url)
727 new_base = base if base.endswith("/") else base + "/"
728 new_url = url[1:] if url.startswith("/") else url
729 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800730
731
John Asmuth864311d2014-04-24 15:46:08 -0400732# TODO(dhermes): Convert this class to ResourceMethod and make it callable
733class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700734 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400735
736 Attributes:
737 argmap: Map from method parameter name (string) to query parameter name
738 (string).
739 required_params: List of required parameters (represented by parameter
740 name as string).
741 repeated_params: List of repeated parameters (represented by parameter
742 name as string).
743 pattern_params: Map from method parameter name (string) to regular
744 expression (as a string). If the pattern is set for a parameter, the
745 value for that parameter must match the regular expression.
746 query_params: List of parameters (represented by parameter name as string)
747 that will be used in the query string.
748 path_params: Set of parameters (represented by parameter name as string)
749 that will be used in the base URL path.
750 param_types: Map from method parameter name (string) to parameter type. Type
751 can be any valid JSON schema type; valid values are 'any', 'array',
752 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
753 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
754 enum_params: Map from method parameter name (string) to list of strings,
755 where each list of strings is the list of acceptable enum values.
756 """
757
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700758 def __init__(self, method_desc):
759 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400760
761 Sets default values and defers to set_parameters to populate.
762
763 Args:
764 method_desc: Dictionary with metadata describing an API method. Value
765 comes from the dictionary of methods stored in the 'methods' key in
766 the deserialized discovery document.
767 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700768 self.argmap = {}
769 self.required_params = []
770 self.repeated_params = []
771 self.pattern_params = {}
772 self.query_params = []
773 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
774 # parsing is gotten rid of.
775 self.path_params = set()
776 self.param_types = {}
777 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400778
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700779 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400780
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700781 def set_parameters(self, method_desc):
782 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400783
784 Iterates through each parameter for the method and parses the values from
785 the parameter dictionary.
786
787 Args:
788 method_desc: Dictionary with metadata describing an API method. Value
789 comes from the dictionary of methods stored in the 'methods' key in
790 the deserialized discovery document.
791 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700792 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
793 param = key2param(arg)
794 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400795
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700796 if desc.get("pattern"):
797 self.pattern_params[param] = desc["pattern"]
798 if desc.get("enum"):
799 self.enum_params[param] = desc["enum"]
800 if desc.get("required"):
801 self.required_params.append(param)
802 if desc.get("repeated"):
803 self.repeated_params.append(param)
804 if desc.get("location") == "query":
805 self.query_params.append(param)
806 if desc.get("location") == "path":
807 self.path_params.add(param)
808 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400809
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700810 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
811 # should have all path parameters already marked with
812 # 'location: path'.
813 for match in URITEMPLATE.finditer(method_desc["path"]):
814 for namematch in VARNAME.finditer(match.group(0)):
815 name = key2param(namematch.group(0))
816 self.path_params.add(name)
817 if name in self.query_params:
818 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400819
820
821def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700822 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400823
824 Args:
825 methodName: string, name of the method to use.
826 methodDesc: object, fragment of deserialized discovery document that
827 describes the method.
828 rootDesc: object, the entire deserialized discovery document.
829 schema: object, mapping of schema names to schema descriptions.
830 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700831 methodName = fix_method_name(methodName)
832 (
833 pathUrl,
834 httpMethod,
835 methodId,
836 accept,
837 maxSize,
838 mediaPathUrl,
839 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400840
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700841 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400842
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700843 def method(self, **kwargs):
844 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400845
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700846 for name in six.iterkeys(kwargs):
847 if name not in parameters.argmap:
848 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400849
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700850 # Remove args that have a value of None.
851 keys = list(kwargs.keys())
852 for name in keys:
853 if kwargs[name] is None:
854 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400855
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700856 for name in parameters.required_params:
857 if name not in kwargs:
858 # temporary workaround for non-paging methods incorrectly requiring
859 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
860 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
861 _methodProperties(methodDesc, schema, "response")
862 ):
863 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400864
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700865 for name, regex in six.iteritems(parameters.pattern_params):
866 if name in kwargs:
867 if isinstance(kwargs[name], six.string_types):
868 pvalues = [kwargs[name]]
869 else:
870 pvalues = kwargs[name]
871 for pvalue in pvalues:
872 if re.match(regex, pvalue) is None:
873 raise TypeError(
874 'Parameter "%s" value "%s" does not match the pattern "%s"'
875 % (name, pvalue, regex)
876 )
877
878 for name, enums in six.iteritems(parameters.enum_params):
879 if name in kwargs:
880 # We need to handle the case of a repeated enum
881 # name differently, since we want to handle both
882 # arg='value' and arg=['value1', 'value2']
883 if name in parameters.repeated_params and not isinstance(
884 kwargs[name], six.string_types
885 ):
886 values = kwargs[name]
887 else:
888 values = [kwargs[name]]
889 for value in values:
890 if value not in enums:
891 raise TypeError(
892 'Parameter "%s" value "%s" is not an allowed value in "%s"'
893 % (name, value, str(enums))
894 )
895
896 actual_query_params = {}
897 actual_path_params = {}
898 for key, value in six.iteritems(kwargs):
899 to_type = parameters.param_types.get(key, "string")
900 # For repeated parameters we cast each member of the list.
901 if key in parameters.repeated_params and type(value) == type([]):
902 cast_value = [_cast(x, to_type) for x in value]
903 else:
904 cast_value = _cast(value, to_type)
905 if key in parameters.query_params:
906 actual_query_params[parameters.argmap[key]] = cast_value
907 if key in parameters.path_params:
908 actual_path_params[parameters.argmap[key]] = cast_value
909 body_value = kwargs.get("body", None)
910 media_filename = kwargs.get("media_body", None)
911 media_mime_type = kwargs.get("media_mime_type", None)
912
913 if self._developerKey:
914 actual_query_params["key"] = self._developerKey
915
916 model = self._model
917 if methodName.endswith("_media"):
918 model = MediaModel()
919 elif "response" not in methodDesc:
920 model = RawModel()
921
922 headers = {}
923 headers, params, query, body = model.request(
924 headers, actual_path_params, actual_query_params, body_value
925 )
926
927 expanded_url = uritemplate.expand(pathUrl, params)
928 url = _urljoin(self._baseUrl, expanded_url + query)
929
930 resumable = None
931 multipart_boundary = ""
932
933 if media_filename:
934 # Ensure we end up with a valid MediaUpload object.
935 if isinstance(media_filename, six.string_types):
936 if media_mime_type is None:
937 logger.warning(
938 "media_mime_type argument not specified: trying to auto-detect for %s",
939 media_filename,
940 )
941 media_mime_type, _ = mimetypes.guess_type(media_filename)
942 if media_mime_type is None:
943 raise UnknownFileType(media_filename)
944 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
945 raise UnacceptableMimeTypeError(media_mime_type)
946 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
947 elif isinstance(media_filename, MediaUpload):
948 media_upload = media_filename
949 else:
950 raise TypeError("media_filename must be str or MediaUpload.")
951
952 # Check the maxSize
953 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
954 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
955
956 # Use the media path uri for media uploads
957 expanded_url = uritemplate.expand(mediaPathUrl, params)
958 url = _urljoin(self._baseUrl, expanded_url + query)
959 if media_upload.resumable():
960 url = _add_query_parameter(url, "uploadType", "resumable")
961
962 if media_upload.resumable():
963 # This is all we need to do for resumable, if the body exists it gets
964 # sent in the first request, otherwise an empty body is sent.
965 resumable = media_upload
966 else:
967 # A non-resumable upload
968 if body is None:
969 # This is a simple media upload
970 headers["content-type"] = media_upload.mimetype()
971 body = media_upload.getbytes(0, media_upload.size())
972 url = _add_query_parameter(url, "uploadType", "media")
973 else:
974 # This is a multipart/related upload.
975 msgRoot = MIMEMultipart("related")
976 # msgRoot should not write out it's own headers
977 setattr(msgRoot, "_write_headers", lambda self: None)
978
979 # attach the body as one part
980 msg = MIMENonMultipart(*headers["content-type"].split("/"))
981 msg.set_payload(body)
982 msgRoot.attach(msg)
983
984 # attach the media as the second part
985 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
986 msg["Content-Transfer-Encoding"] = "binary"
987
988 payload = media_upload.getbytes(0, media_upload.size())
989 msg.set_payload(payload)
990 msgRoot.attach(msg)
991 # encode the body: note that we can't use `as_string`, because
992 # it plays games with `From ` lines.
993 fp = BytesIO()
994 g = _BytesGenerator(fp, mangle_from_=False)
995 g.flatten(msgRoot, unixfrom=False)
996 body = fp.getvalue()
997
998 multipart_boundary = msgRoot.get_boundary()
999 headers["content-type"] = (
1000 "multipart/related; " 'boundary="%s"'
1001 ) % multipart_boundary
1002 url = _add_query_parameter(url, "uploadType", "multipart")
1003
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001004 logger.debug("URL being requested: %s %s" % (httpMethod, url))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001005 return self._requestBuilder(
1006 self._http,
1007 model.response,
1008 url,
1009 method=httpMethod,
1010 body=body,
1011 headers=headers,
1012 methodId=methodId,
1013 resumable=resumable,
1014 )
1015
1016 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1017 if len(parameters.argmap) > 0:
1018 docs.append("Args:\n")
1019
1020 # Skip undocumented params and params common to all methods.
1021 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1022 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1023
1024 all_args = list(parameters.argmap.keys())
1025 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1026
1027 # Move body to the front of the line.
1028 if "body" in all_args:
1029 args_ordered.append("body")
1030
1031 for name in all_args:
1032 if name not in args_ordered:
1033 args_ordered.append(name)
1034
1035 for arg in args_ordered:
1036 if arg in skip_parameters:
1037 continue
1038
1039 repeated = ""
1040 if arg in parameters.repeated_params:
1041 repeated = " (repeated)"
1042 required = ""
1043 if arg in parameters.required_params:
1044 required = " (required)"
1045 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1046 paramdoc = paramdesc.get("description", "A parameter")
1047 if "$ref" in paramdesc:
1048 docs.append(
1049 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
1050 % (
1051 arg,
1052 paramdoc,
1053 required,
1054 repeated,
1055 schema.prettyPrintByName(paramdesc["$ref"]),
1056 )
1057 )
John Asmuth864311d2014-04-24 15:46:08 -04001058 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001059 paramtype = paramdesc.get("type", "string")
1060 docs.append(
1061 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1062 )
1063 enum = paramdesc.get("enum", [])
1064 enumDesc = paramdesc.get("enumDescriptions", [])
1065 if enum and enumDesc:
1066 docs.append(" Allowed values\n")
1067 for (name, desc) in zip(enum, enumDesc):
1068 docs.append(" %s - %s\n" % (name, desc))
1069 if "response" in methodDesc:
1070 if methodName.endswith("_media"):
1071 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -04001072 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001073 docs.append("\nReturns:\n An object of the form:\n\n ")
1074 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -04001075
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001076 setattr(method, "__doc__", "".join(docs))
1077 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -04001078
1079
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001080def createNextMethod(
1081 methodName,
1082 pageTokenName="pageToken",
1083 nextPageTokenName="nextPageToken",
1084 isPageTokenParameter=True,
1085):
1086 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001087
1088 The _next methods allow for easy iteration through list() responses.
1089
1090 Args:
1091 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001092 pageTokenName: string, name of request page token field.
1093 nextPageTokenName: string, name of response page token field.
1094 isPageTokenParameter: Boolean, True if request page token is a query
1095 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001096 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001097 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001098
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001099 def methodNext(self, previous_request, previous_response):
1100 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001101
1102Args:
1103 previous_request: The request for the previous page. (required)
1104 previous_response: The response from the request for the previous page. (required)
1105
1106Returns:
1107 A request object that you can call 'execute()' on to request the next
1108 page. Returns None if there are no more items in the collection.
1109 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001110 # Retrieve nextPageToken from previous_response
1111 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001112
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001113 nextPageToken = previous_response.get(nextPageTokenName, None)
1114 if not nextPageToken:
1115 return None
John Asmuth864311d2014-04-24 15:46:08 -04001116
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001117 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001118
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001119 if isPageTokenParameter:
1120 # Replace pageToken value in URI
1121 request.uri = _add_query_parameter(
1122 request.uri, pageTokenName, nextPageToken
1123 )
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001124 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001125 else:
1126 # Replace pageToken value in request body
1127 model = self._model
1128 body = model.deserialize(request.body)
1129 body[pageTokenName] = nextPageToken
1130 request.body = model.serialize(body)
Bu Sun Kim3bf27812020-04-28 09:39:09 -07001131 logger.debug("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001132
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001133 return request
John Asmuth864311d2014-04-24 15:46:08 -04001134
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001135 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001136
1137
1138class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001139 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001140
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001141 def __init__(
1142 self,
1143 http,
1144 baseUrl,
1145 model,
1146 requestBuilder,
1147 developerKey,
1148 resourceDesc,
1149 rootDesc,
1150 schema,
1151 ):
1152 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001153
1154 Args:
1155 http: httplib2.Http, Object to make http requests with.
1156 baseUrl: string, base URL for the API. All requests are relative to this
1157 URI.
1158 model: googleapiclient.Model, converts to and from the wire format.
1159 requestBuilder: class or callable that instantiates an
1160 googleapiclient.HttpRequest object.
1161 developerKey: string, key obtained from
1162 https://code.google.com/apis/console
1163 resourceDesc: object, section of deserialized discovery document that
1164 describes a resource. Note that the top level discovery document
1165 is considered a resource.
1166 rootDesc: object, the entire deserialized discovery document.
1167 schema: object, mapping of schema names to schema descriptions.
1168 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001169 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001170
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001171 self._http = http
1172 self._baseUrl = baseUrl
1173 self._model = model
1174 self._developerKey = developerKey
1175 self._requestBuilder = requestBuilder
1176 self._resourceDesc = resourceDesc
1177 self._rootDesc = rootDesc
1178 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001179
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001180 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001181
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001182 def _set_dynamic_attr(self, attr_name, value):
1183 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001184
1185 Args:
1186 attr_name: string; The name of the attribute to be set
1187 value: The value being set on the object and tracked in the dynamic cache.
1188 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001189 self._dynamic_attrs.append(attr_name)
1190 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001191
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001192 def __getstate__(self):
1193 """Trim the state down to something that can be 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 state_dict = copy.copy(self.__dict__)
1199 for dynamic_attr in self._dynamic_attrs:
1200 del state_dict[dynamic_attr]
1201 del state_dict["_dynamic_attrs"]
1202 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001203
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001204 def __setstate__(self, state):
1205 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001206
1207 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1208 will be wiped and restored on pickle serialization.
1209 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001210 self.__dict__.update(state)
1211 self._dynamic_attrs = []
1212 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001213
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001214 def _set_service_methods(self):
1215 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1216 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1217 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001218
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001219 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1220 # If this is the root Resource, add a new_batch_http_request() method.
1221 if resourceDesc == rootDesc:
1222 batch_uri = "%s%s" % (
1223 rootDesc["rootUrl"],
1224 rootDesc.get("batchPath", "batch"),
1225 )
1226
1227 def new_batch_http_request(callback=None):
1228 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001229
1230 Args:
1231 callback: callable, A callback to be called for each response, of the
1232 form callback(id, response, exception). The first parameter is the
1233 request id, and the second is the deserialized response object. The
1234 third is an apiclient.errors.HttpError exception object if an HTTP
1235 error occurred while processing the request, or None if no error
1236 occurred.
1237
1238 Returns:
1239 A BatchHttpRequest object based on the discovery document.
1240 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001241 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001242
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001243 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001244
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001245 # Add basic methods to Resource
1246 if "methods" in resourceDesc:
1247 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1248 fixedMethodName, method = createMethod(
1249 methodName, methodDesc, rootDesc, schema
1250 )
1251 self._set_dynamic_attr(
1252 fixedMethodName, method.__get__(self, self.__class__)
1253 )
1254 # Add in _media methods. The functionality of the attached method will
1255 # change when it sees that the method name ends in _media.
1256 if methodDesc.get("supportsMediaDownload", False):
1257 fixedMethodName, method = createMethod(
1258 methodName + "_media", methodDesc, rootDesc, schema
1259 )
1260 self._set_dynamic_attr(
1261 fixedMethodName, method.__get__(self, self.__class__)
1262 )
John Asmuth864311d2014-04-24 15:46:08 -04001263
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001264 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1265 # Add in nested resources
1266 if "resources" in resourceDesc:
1267
1268 def createResourceMethod(methodName, methodDesc):
1269 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001270
1271 Args:
1272 methodName: string, name of the method to use.
1273 methodDesc: object, fragment of deserialized discovery document that
1274 describes the method.
1275 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001276 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001277
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001278 def methodResource(self):
1279 return Resource(
1280 http=self._http,
1281 baseUrl=self._baseUrl,
1282 model=self._model,
1283 developerKey=self._developerKey,
1284 requestBuilder=self._requestBuilder,
1285 resourceDesc=methodDesc,
1286 rootDesc=rootDesc,
1287 schema=schema,
1288 )
John Asmuth864311d2014-04-24 15:46:08 -04001289
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001290 setattr(methodResource, "__doc__", "A collection resource.")
1291 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001292
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001293 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001294
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001295 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1296 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1297 self._set_dynamic_attr(
1298 fixedMethodName, method.__get__(self, self.__class__)
1299 )
John Asmuth864311d2014-04-24 15:46:08 -04001300
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001301 def _add_next_methods(self, resourceDesc, schema):
1302 # Add _next() methods if and only if one of the names 'pageToken' or
1303 # 'nextPageToken' occurs among the fields of both the method's response
1304 # type either the method's request (query parameters) or request body.
1305 if "methods" not in resourceDesc:
1306 return
1307 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1308 nextPageTokenName = _findPageTokenName(
1309 _methodProperties(methodDesc, schema, "response")
1310 )
1311 if not nextPageTokenName:
1312 continue
1313 isPageTokenParameter = True
1314 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1315 if not pageTokenName:
1316 isPageTokenParameter = False
1317 pageTokenName = _findPageTokenName(
1318 _methodProperties(methodDesc, schema, "request")
1319 )
1320 if not pageTokenName:
1321 continue
1322 fixedMethodName, method = createNextMethod(
1323 methodName + "_next",
1324 pageTokenName,
1325 nextPageTokenName,
1326 isPageTokenParameter,
1327 )
1328 self._set_dynamic_attr(
1329 fixedMethodName, method.__get__(self, self.__class__)
1330 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001331
1332
1333def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001334 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001335
1336 Args:
1337 fields: container of string, names of fields.
1338
1339 Returns:
1340 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1341 otherwise None.
1342 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001343 return next(
1344 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1345 )
1346
Thomas Coffee20af04d2017-02-10 15:24:44 -08001347
1348def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001349 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001350
1351 Args:
1352 methodDesc: object, fragment of deserialized discovery document that
1353 describes the method.
1354 schema: object, mapping of schema names to schema descriptions.
1355 name: string, name of top-level field in method description.
1356
1357 Returns:
1358 Object representing fragment of deserialized discovery document
1359 corresponding to 'properties' field of object corresponding to named field
1360 in method description, if it exists, otherwise empty dict.
1361 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001362 desc = methodDesc.get(name, {})
1363 if "$ref" in desc:
1364 desc = schema.get(desc["$ref"], {})
1365 return desc.get("properties", {})