Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/googleapiclient/discovery.py: 57%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

586 statements  

1# Copyright 2014 Google Inc. All Rights Reserved. 

2# 

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""" 

19from __future__ import absolute_import 

20 

21__author__ = "jcgregorio@google.com (Joe Gregorio)" 

22__all__ = ["build", "build_from_document", "fix_method_name", "key2param"] 

23 

24from collections import OrderedDict 

25import collections.abc 

26 

27# Standard library imports 

28import copy 

29from email.generator import BytesGenerator 

30from email.mime.multipart import MIMEMultipart 

31from email.mime.nonmultipart import MIMENonMultipart 

32import http.client as http_client 

33import io 

34import json 

35import keyword 

36import logging 

37import mimetypes 

38import os 

39import re 

40import urllib 

41 

42import google.api_core.client_options 

43from google.auth.exceptions import MutualTLSChannelError 

44from google.auth.transport import mtls 

45from google.oauth2 import service_account 

46 

47# Third-party imports 

48import httplib2 

49import uritemplate 

50 

51try: 

52 import google_auth_httplib2 

53except ImportError: # pragma: NO COVER 

54 google_auth_httplib2 = None 

55 

56try: 

57 from google.api_core import universe 

58 

59 HAS_UNIVERSE = True 

60except ImportError: 

61 HAS_UNIVERSE = False 

62 

63# Local imports 

64from googleapiclient import _auth, mimeparse 

65from googleapiclient._helpers import _add_query_parameter, positional 

66from googleapiclient.errors import ( 

67 HttpError, 

68 InvalidJsonError, 

69 MediaUploadSizeError, 

70 UnacceptableMimeTypeError, 

71 UnknownApiNameOrVersion, 

72 UnknownFileType, 

73) 

74from googleapiclient.http import ( 

75 BatchHttpRequest, 

76 HttpMock, 

77 HttpMockSequence, 

78 HttpRequest, 

79 MediaFileUpload, 

80 MediaUpload, 

81 build_http, 

82) 

83from googleapiclient.model import JsonModel, MediaModel, RawModel 

84from googleapiclient.schema import Schemas 

85 

86# The client library requires a version of httplib2 that supports RETRIES. 

87httplib2.RETRIES = 1 

88 

89logger = logging.getLogger(__name__) 

90 

91URITEMPLATE = re.compile("{[^}]*}") 

92VARNAME = re.compile("[a-zA-Z0-9_-]+") 

93DISCOVERY_URI = ( 

94 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest" 

95) 

96V1_DISCOVERY_URI = DISCOVERY_URI 

97V2_DISCOVERY_URI = ( 

98 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}" 

99) 

100DEFAULT_METHOD_DOC = "A description of how to use this function" 

101HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"]) 

102 

103_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40} 

104BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"} 

105MEDIA_BODY_PARAMETER_DEFAULT_VALUE = { 

106 "description": ( 

107 "The filename of the media request body, or an instance " 

108 "of a MediaUpload object." 

109 ), 

110 "type": "string", 

111 "required": False, 

112} 

113MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = { 

114 "description": ( 

115 "The MIME type of the media request body, or an instance " 

116 "of a MediaUpload object." 

117 ), 

118 "type": "string", 

119 "required": False, 

120} 

121_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken") 

122 

123# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114. 

124GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE" 

125GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT" 

126GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN" 

127DEFAULT_UNIVERSE = "googleapis.com" 

128# Parameters accepted by the stack, but not visible via discovery. 

129# TODO(dhermes): Remove 'userip' in 'v2'. 

130STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"]) 

131STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"} 

132 

133 

134class APICoreVersionError(ValueError): 

135 def __init__(self): 

136 message = ( 

137 "google-api-core >= 2.18.0 is required to use the universe domain feature." 

138 ) 

139 super().__init__(message) 

140 

141 

142# Library-specific reserved words beyond Python keywords. 

143RESERVED_WORDS = frozenset(["body"]) 

144 

145# patch _write_lines to avoid munging '\r' into '\n' 

146# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 ) 

147class _BytesGenerator(BytesGenerator): 

148 _write_lines = BytesGenerator.write 

149 

150 

151def fix_method_name(name): 

152 """Fix method names to avoid '$' characters and reserved word conflicts. 

153 

154 Args: 

155 name: string, method name. 

156 

157 Returns: 

158 The name with '_' appended if the name is a reserved word and '$' and '-' 

159 replaced with '_'. 

160 """ 

161 name = name.replace("$", "_").replace("-", "_") 

162 if keyword.iskeyword(name) or name in RESERVED_WORDS: 

163 return name + "_" 

164 else: 

165 return name 

166 

167 

168def key2param(key): 

169 """Converts key names into parameter names. 

170 

171 For example, converting "max-results" -> "max_results" 

172 

173 Args: 

174 key: string, the method key name. 

175 

176 Returns: 

177 A safe method name based on the key name. 

178 """ 

179 result = [] 

180 key = list(key) 

181 if not key[0].isalpha(): 

182 result.append("x") 

183 for c in key: 

184 if c.isalnum(): 

185 result.append(c) 

186 else: 

187 result.append("_") 

188 

189 return "".join(result) 

190 

191 

192@positional(2) 

193def build( 

194 serviceName, 

195 version, 

196 http=None, 

197 discoveryServiceUrl=None, 

198 developerKey=None, 

199 model=None, 

200 requestBuilder=HttpRequest, 

201 credentials=None, 

202 cache_discovery=True, 

203 cache=None, 

204 client_options=None, 

205 adc_cert_path=None, 

206 adc_key_path=None, 

207 num_retries=1, 

208 static_discovery=None, 

209 always_use_jwt_access=False, 

210): 

211 """Construct a Resource for interacting with an API. 

212 

213 Construct a Resource object for interacting with an API. The serviceName and 

214 version are the names from the Discovery service. 

215 

216 Args: 

217 serviceName: string, name of the service. 

218 version: string, the version of the service. 

219 http: httplib2.Http, An instance of httplib2.Http or something that acts 

220 like it that HTTP requests will be made through. 

221 discoveryServiceUrl: string, a URI Template that points to the location of 

222 the discovery service. It should have two parameters {api} and 

223 {apiVersion} that when filled in produce an absolute URI to the discovery 

224 document for that service. 

225 developerKey: string, key obtained from 

226 https://code.google.com/apis/console. 

227 model: googleapiclient.Model, converts to and from the wire format. 

228 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP 

229 request. 

230 credentials: oauth2client.Credentials or 

231 google.auth.credentials.Credentials, credentials to be used for 

232 authentication. 

233 cache_discovery: Boolean, whether or not to cache the discovery doc. 

234 cache: googleapiclient.discovery_cache.base.CacheBase, an optional 

235 cache object for the discovery documents. 

236 client_options: Mapping object or google.api_core.client_options, client 

237 options to set user options on the client. 

238 (1) The API endpoint should be set through client_options. If API endpoint 

239 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used 

240 to control which endpoint to use. 

241 (2) client_cert_source is not supported, client cert should be provided using 

242 client_encrypted_cert_source instead. In order to use the provided client 

243 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be 

244 set to `true`. 

245 More details on the environment variables are here: 

246 https://google.aip.dev/auth/4114 

247 adc_cert_path: str, client certificate file path to save the application 

248 default client certificate for mTLS. This field is required if you want to 

249 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE` 

250 environment variable must be set to `true` in order to use this field, 

251 otherwise this field doesn't nothing. 

252 More details on the environment variables are here: 

253 https://google.aip.dev/auth/4114 

254 adc_key_path: str, client encrypted private key file path to save the 

255 application default client encrypted private key for mTLS. This field is 

256 required if you want to use the default client certificate. 

257 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to 

258 `true` in order to use this field, otherwise this field doesn't nothing. 

259 More details on the environment variables are here: 

260 https://google.aip.dev/auth/4114 

261 num_retries: Integer, number of times to retry discovery with 

262 randomized exponential backoff in case of intermittent/connection issues. 

263 static_discovery: Boolean, whether or not to use the static discovery docs 

264 included in the library. The default value for `static_discovery` depends 

265 on the value of `discoveryServiceUrl`. `static_discovery` will default to 

266 `True` when `discoveryServiceUrl` is also not provided, otherwise it will 

267 default to `False`. 

268 always_use_jwt_access: Boolean, whether always use self signed JWT for service 

269 account credentials. This only applies to 

270 google.oauth2.service_account.Credentials. 

271 

272 Returns: 

273 A Resource object with methods for interacting with the service. 

274 

275 Raises: 

276 google.auth.exceptions.MutualTLSChannelError: if there are any problems 

277 setting up mutual TLS channel. 

278 """ 

279 params = {"api": serviceName, "apiVersion": version} 

280 

281 # The default value for `static_discovery` depends on the value of 

282 # `discoveryServiceUrl`. `static_discovery` will default to `True` when 

283 # `discoveryServiceUrl` is also not provided, otherwise it will default to 

284 # `False`. This is added for backwards compatability with 

285 # google-api-python-client 1.x which does not support the `static_discovery` 

286 # parameter. 

287 if static_discovery is None: 

288 if discoveryServiceUrl is None: 

289 static_discovery = True 

290 else: 

291 static_discovery = False 

292 

293 if http is None: 

294 discovery_http = build_http() 

295 else: 

296 discovery_http = http 

297 

298 service = None 

299 

300 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version): 

301 requested_url = uritemplate.expand(discovery_url, params) 

302 

303 try: 

304 content = _retrieve_discovery_doc( 

305 requested_url, 

306 discovery_http, 

307 cache_discovery, 

308 serviceName, 

309 version, 

310 cache, 

311 developerKey, 

312 num_retries=num_retries, 

313 static_discovery=static_discovery, 

314 ) 

315 service = build_from_document( 

316 content, 

317 base=discovery_url, 

318 http=http, 

319 developerKey=developerKey, 

320 model=model, 

321 requestBuilder=requestBuilder, 

322 credentials=credentials, 

323 client_options=client_options, 

324 adc_cert_path=adc_cert_path, 

325 adc_key_path=adc_key_path, 

326 always_use_jwt_access=always_use_jwt_access, 

327 ) 

328 break # exit if a service was created 

329 except HttpError as e: 

330 if e.resp.status == http_client.NOT_FOUND: 

331 continue 

332 else: 

333 raise e 

334 

335 # If discovery_http was created by this function, we are done with it 

336 # and can safely close it 

337 if http is None: 

338 discovery_http.close() 

339 

340 if service is None: 

341 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version)) 

342 else: 

343 return service 

344 

345 

346def _discovery_service_uri_options(discoveryServiceUrl, version): 

347 """ 

348 Returns Discovery URIs to be used for attempting to build the API Resource. 

349 

350 Args: 

351 discoveryServiceUrl: 

352 string, the Original Discovery Service URL preferred by the customer. 

353 version: 

354 string, API Version requested 

355 

356 Returns: 

357 A list of URIs to be tried for the Service Discovery, in order. 

358 """ 

359 

360 if discoveryServiceUrl is not None: 

361 return [discoveryServiceUrl] 

362 if version is None: 

363 # V1 Discovery won't work if the requested version is None 

364 logger.warning( 

365 "Discovery V1 does not support empty versions. Defaulting to V2..." 

366 ) 

367 return [V2_DISCOVERY_URI] 

368 else: 

369 return [DISCOVERY_URI, V2_DISCOVERY_URI] 

370 

371 

372def _retrieve_discovery_doc( 

373 url, 

374 http, 

375 cache_discovery, 

376 serviceName, 

377 version, 

378 cache=None, 

379 developerKey=None, 

380 num_retries=1, 

381 static_discovery=True, 

382): 

383 """Retrieves the discovery_doc from cache or the internet. 

384 

385 Args: 

386 url: string, the URL of the discovery document. 

387 http: httplib2.Http, An instance of httplib2.Http or something that acts 

388 like it through which HTTP requests will be made. 

389 cache_discovery: Boolean, whether or not to cache the discovery doc. 

390 serviceName: string, name of the service. 

391 version: string, the version of the service. 

392 cache: googleapiclient.discovery_cache.base.Cache, an optional cache 

393 object for the discovery documents. 

394 developerKey: string, Key for controlling API usage, generated 

395 from the API Console. 

396 num_retries: Integer, number of times to retry discovery with 

397 randomized exponential backoff in case of intermittent/connection issues. 

398 static_discovery: Boolean, whether or not to use the static discovery docs 

399 included in the library. 

400 

401 Returns: 

402 A unicode string representation of the discovery document. 

403 """ 

404 from . import discovery_cache 

405 

406 if cache_discovery: 

407 if cache is None: 

408 cache = discovery_cache.autodetect() 

409 if cache: 

410 content = cache.get(url) 

411 if content: 

412 return content 

413 

414 # When `static_discovery=True`, use static discovery artifacts included 

415 # with the library 

416 if static_discovery: 

417 content = discovery_cache.get_static_doc(serviceName, version) 

418 if content: 

419 return content 

420 else: 

421 raise UnknownApiNameOrVersion( 

422 "name: %s version: %s" % (serviceName, version) 

423 ) 

424 

425 actual_url = url 

426 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment 

427 # variable that contains the network address of the client sending the 

428 # request. If it exists then add that to the request for the discovery 

429 # document to avoid exceeding the quota on discovery requests. 

430 if "REMOTE_ADDR" in os.environ: 

431 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) 

432 if developerKey: 

433 actual_url = _add_query_parameter(url, "key", developerKey) 

434 logger.debug("URL being requested: GET %s", actual_url) 

435 

436 # Execute this request with retries build into HttpRequest 

437 # Note that it will already raise an error if we don't get a 2xx response 

438 req = HttpRequest(http, HttpRequest.null_postproc, actual_url) 

439 resp, content = req.execute(num_retries=num_retries) 

440 

441 try: 

442 content = content.decode("utf-8") 

443 except AttributeError: 

444 pass 

445 

446 try: 

447 service = json.loads(content) 

448 except ValueError as e: 

449 logger.error("Failed to parse as JSON: " + content) 

450 raise InvalidJsonError() 

451 if cache_discovery and cache: 

452 cache.set(url, content) 

453 return content 

454 

455 

456def _check_api_core_compatible_with_credentials_universe(credentials): 

457 if not HAS_UNIVERSE: 

458 credentials_universe = getattr(credentials, "universe_domain", None) 

459 if credentials_universe and credentials_universe != DEFAULT_UNIVERSE: 

460 raise APICoreVersionError 

461 

462 

463@positional(1) 

464def build_from_document( 

465 service, 

466 base=None, 

467 future=None, 

468 http=None, 

469 developerKey=None, 

470 model=None, 

471 requestBuilder=HttpRequest, 

472 credentials=None, 

473 client_options=None, 

474 adc_cert_path=None, 

475 adc_key_path=None, 

476 always_use_jwt_access=False, 

477): 

478 """Create a Resource for interacting with an API. 

479 

480 Same as `build()`, but constructs the Resource object from a discovery 

481 document that is it given, as opposed to retrieving one over HTTP. 

482 

483 Args: 

484 service: string or object, the JSON discovery document describing the API. 

485 The value passed in may either be the JSON string or the deserialized 

486 JSON. 

487 base: string, base URI for all HTTP requests, usually the discovery URI. 

488 This parameter is no longer used as rootUrl and servicePath are included 

489 within the discovery document. (deprecated) 

490 future: string, discovery document with future capabilities (deprecated). 

491 http: httplib2.Http, An instance of httplib2.Http or something that acts 

492 like it that HTTP requests will be made through. 

493 developerKey: string, Key for controlling API usage, generated 

494 from the API Console. 

495 model: Model class instance that serializes and de-serializes requests and 

496 responses. 

497 requestBuilder: Takes an http request and packages it up to be executed. 

498 credentials: oauth2client.Credentials or 

499 google.auth.credentials.Credentials, credentials to be used for 

500 authentication. 

501 client_options: Mapping object or google.api_core.client_options, client 

502 options to set user options on the client. 

503 (1) The API endpoint should be set through client_options. If API endpoint 

504 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used 

505 to control which endpoint to use. 

506 (2) client_cert_source is not supported, client cert should be provided using 

507 client_encrypted_cert_source instead. In order to use the provided client 

508 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be 

509 set to `true`. 

510 More details on the environment variables are here: 

511 https://google.aip.dev/auth/4114 

512 adc_cert_path: str, client certificate file path to save the application 

513 default client certificate for mTLS. This field is required if you want to 

514 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE` 

515 environment variable must be set to `true` in order to use this field, 

516 otherwise this field doesn't nothing. 

517 More details on the environment variables are here: 

518 https://google.aip.dev/auth/4114 

519 adc_key_path: str, client encrypted private key file path to save the 

520 application default client encrypted private key for mTLS. This field is 

521 required if you want to use the default client certificate. 

522 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to 

523 `true` in order to use this field, otherwise this field doesn't nothing. 

524 More details on the environment variables are here: 

525 https://google.aip.dev/auth/4114 

526 always_use_jwt_access: Boolean, whether always use self signed JWT for service 

527 account credentials. This only applies to 

528 google.oauth2.service_account.Credentials. 

529 

530 Returns: 

531 A Resource object with methods for interacting with the service. 

532 

533 Raises: 

534 google.auth.exceptions.MutualTLSChannelError: if there are any problems 

535 setting up mutual TLS channel. 

536 """ 

537 

538 if client_options is None: 

539 client_options = google.api_core.client_options.ClientOptions() 

540 if isinstance(client_options, collections.abc.Mapping): 

541 client_options = google.api_core.client_options.from_dict(client_options) 

542 

543 if http is not None: 

544 # if http is passed, the user cannot provide credentials 

545 banned_options = [ 

546 (credentials, "credentials"), 

547 (client_options.credentials_file, "client_options.credentials_file"), 

548 ] 

549 for option, name in banned_options: 

550 if option is not None: 

551 raise ValueError( 

552 "Arguments http and {} are mutually exclusive".format(name) 

553 ) 

554 

555 if isinstance(service, str): 

556 service = json.loads(service) 

557 elif isinstance(service, bytes): 

558 service = json.loads(service.decode("utf-8")) 

559 

560 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)): 

561 logger.error( 

562 "You are using HttpMock or HttpMockSequence without" 

563 + "having the service discovery doc in cache. Try calling " 

564 + "build() without mocking once first to populate the " 

565 + "cache." 

566 ) 

567 raise InvalidJsonError() 

568 

569 # If an API Endpoint is provided on client options, use that as the base URL 

570 base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"]) 

571 universe_domain = None 

572 if HAS_UNIVERSE: 

573 universe_domain_env = os.getenv(GOOGLE_CLOUD_UNIVERSE_DOMAIN, None) 

574 universe_domain = universe.determine_domain( 

575 client_options.universe_domain, universe_domain_env 

576 ) 

577 base = base.replace(universe.DEFAULT_UNIVERSE, universe_domain) 

578 else: 

579 client_universe = getattr(client_options, "universe_domain", None) 

580 if client_universe: 

581 raise APICoreVersionError 

582 

583 audience_for_self_signed_jwt = base 

584 if client_options.api_endpoint: 

585 base = client_options.api_endpoint 

586 

587 schema = Schemas(service) 

588 

589 # If the http client is not specified, then we must construct an http client 

590 # to make requests. If the service has scopes, then we also need to setup 

591 # authentication. 

592 if http is None: 

593 # Does the service require scopes? 

594 scopes = list( 

595 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys() 

596 ) 

597 

598 # If so, then the we need to setup authentication if no developerKey is 

599 # specified. 

600 if scopes and not developerKey: 

601 # Make sure the user didn't pass multiple credentials 

602 if client_options.credentials_file and credentials: 

603 raise google.api_core.exceptions.DuplicateCredentialArgs( 

604 "client_options.credentials_file and credentials are mutually exclusive." 

605 ) 

606 # Check for credentials file via client options 

607 if client_options.credentials_file: 

608 credentials = _auth.credentials_from_file( 

609 client_options.credentials_file, 

610 scopes=client_options.scopes, 

611 quota_project_id=client_options.quota_project_id, 

612 ) 

613 # If the user didn't pass in credentials, attempt to acquire application 

614 # default credentials. 

615 if credentials is None: 

616 credentials = _auth.default_credentials( 

617 scopes=client_options.scopes, 

618 quota_project_id=client_options.quota_project_id, 

619 ) 

620 

621 # Check google-api-core >= 2.18.0 if credentials' universe != "googleapis.com". 

622 _check_api_core_compatible_with_credentials_universe(credentials) 

623 

624 # The credentials need to be scoped. 

625 # If the user provided scopes via client_options don't override them 

626 if not client_options.scopes: 

627 credentials = _auth.with_scopes(credentials, scopes) 

628 

629 # For google-auth service account credentials, enable self signed JWT if 

630 # always_use_jwt_access is true. 

631 if ( 

632 credentials 

633 and isinstance(credentials, service_account.Credentials) 

634 and always_use_jwt_access 

635 and hasattr(service_account.Credentials, "with_always_use_jwt_access") 

636 ): 

637 credentials = credentials.with_always_use_jwt_access(always_use_jwt_access) 

638 credentials._create_self_signed_jwt(audience_for_self_signed_jwt) 

639 

640 # If credentials are provided, create an authorized http instance; 

641 # otherwise, skip authentication. 

642 if credentials: 

643 http = _auth.authorized_http(credentials) 

644 

645 # If the service doesn't require scopes then there is no need for 

646 # authentication. 

647 else: 

648 http = build_http() 

649 

650 # Obtain client cert and create mTLS http channel if cert exists. 

651 client_cert_to_use = None 

652 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false") 

653 if not use_client_cert in ("true", "false"): 

654 raise MutualTLSChannelError( 

655 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false" 

656 ) 

657 if client_options and client_options.client_cert_source: 

658 raise MutualTLSChannelError( 

659 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source." 

660 ) 

661 if use_client_cert == "true": 

662 if ( 

663 client_options 

664 and hasattr(client_options, "client_encrypted_cert_source") 

665 and client_options.client_encrypted_cert_source 

666 ): 

667 client_cert_to_use = client_options.client_encrypted_cert_source 

668 elif ( 

669 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source() 

670 ): 

671 client_cert_to_use = mtls.default_client_encrypted_cert_source( 

672 adc_cert_path, adc_key_path 

673 ) 

674 if client_cert_to_use: 

675 cert_path, key_path, passphrase = client_cert_to_use() 

676 

677 # The http object we built could be google_auth_httplib2.AuthorizedHttp 

678 # or httplib2.Http. In the first case we need to extract the wrapped 

679 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp. 

680 http_channel = ( 

681 http.http 

682 if google_auth_httplib2 

683 and isinstance(http, google_auth_httplib2.AuthorizedHttp) 

684 else http 

685 ) 

686 http_channel.add_certificate(key_path, cert_path, "", passphrase) 

687 

688 # If user doesn't provide api endpoint via client options, decide which 

689 # api endpoint to use. 

690 if "mtlsRootUrl" in service and ( 

691 not client_options or not client_options.api_endpoint 

692 ): 

693 mtls_endpoint = urllib.parse.urljoin( 

694 service["mtlsRootUrl"], service["servicePath"] 

695 ) 

696 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto") 

697 

698 if not use_mtls_endpoint in ("never", "auto", "always"): 

699 raise MutualTLSChannelError( 

700 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always" 

701 ) 

702 

703 # Switch to mTLS endpoint, if environment variable is "always", or 

704 # environment varibable is "auto" and client cert exists. 

705 if use_mtls_endpoint == "always" or ( 

706 use_mtls_endpoint == "auto" and client_cert_to_use 

707 ): 

708 if HAS_UNIVERSE and universe_domain != universe.DEFAULT_UNIVERSE: 

709 raise MutualTLSChannelError( 

710 f"mTLS is not supported in any universe other than {universe.DEFAULT_UNIVERSE}." 

711 ) 

712 base = mtls_endpoint 

713 else: 

714 # Check google-api-core >= 2.18.0 if credentials' universe != "googleapis.com". 

715 http_credentials = getattr(http, "credentials", None) 

716 _check_api_core_compatible_with_credentials_universe(http_credentials) 

717 

718 if model is None: 

719 features = service.get("features", []) 

720 model = JsonModel("dataWrapper" in features) 

721 

722 return Resource( 

723 http=http, 

724 baseUrl=base, 

725 model=model, 

726 developerKey=developerKey, 

727 requestBuilder=requestBuilder, 

728 resourceDesc=service, 

729 rootDesc=service, 

730 schema=schema, 

731 universe_domain=universe_domain, 

732 ) 

733 

734 

735def _cast(value, schema_type): 

736 """Convert value to a string based on JSON Schema type. 

737 

738 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on 

739 JSON Schema. 

740 

741 Args: 

742 value: any, the value to convert 

743 schema_type: string, the type that value should be interpreted as 

744 

745 Returns: 

746 A string representation of 'value' based on the schema_type. 

747 """ 

748 if schema_type == "string": 

749 if type(value) == type("") or type(value) == type(""): 

750 return value 

751 else: 

752 return str(value) 

753 elif schema_type == "integer": 

754 return str(int(value)) 

755 elif schema_type == "number": 

756 return str(float(value)) 

757 elif schema_type == "boolean": 

758 return str(bool(value)).lower() 

759 else: 

760 if type(value) == type("") or type(value) == type(""): 

761 return value 

762 else: 

763 return str(value) 

764 

765 

766def _media_size_to_long(maxSize): 

767 """Convert a string media size, such as 10GB or 3TB into an integer. 

768 

769 Args: 

770 maxSize: string, size as a string, such as 2MB or 7GB. 

771 

772 Returns: 

773 The size as an integer value. 

774 """ 

775 if len(maxSize) < 2: 

776 return 0 

777 units = maxSize[-2:].upper() 

778 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) 

779 if bit_shift is not None: 

780 return int(maxSize[:-2]) << bit_shift 

781 else: 

782 return int(maxSize) 

783 

784 

785def _media_path_url_from_info(root_desc, path_url): 

786 """Creates an absolute media path URL. 

787 

788 Constructed using the API root URI and service path from the discovery 

789 document and the relative path for the API method. 

790 

791 Args: 

792 root_desc: Dictionary; the entire original deserialized discovery document. 

793 path_url: String; the relative URL for the API method. Relative to the API 

794 root, which is specified in the discovery document. 

795 

796 Returns: 

797 String; the absolute URI for media upload for the API method. 

798 """ 

799 return "%(root)supload/%(service_path)s%(path)s" % { 

800 "root": root_desc["rootUrl"], 

801 "service_path": root_desc["servicePath"], 

802 "path": path_url, 

803 } 

804 

805 

806def _fix_up_parameters(method_desc, root_desc, http_method, schema): 

807 """Updates parameters of an API method with values specific to this library. 

808 

809 Specifically, adds whatever global parameters are specified by the API to the 

810 parameters for the individual method. Also adds parameters which don't 

811 appear in the discovery document, but are available to all discovery based 

812 APIs (these are listed in STACK_QUERY_PARAMETERS). 

813 

814 SIDE EFFECTS: This updates the parameters dictionary object in the method 

815 description. 

816 

817 Args: 

818 method_desc: Dictionary with metadata describing an API method. Value comes 

819 from the dictionary of methods stored in the 'methods' key in the 

820 deserialized discovery document. 

821 root_desc: Dictionary; the entire original deserialized discovery document. 

822 http_method: String; the HTTP method used to call the API method described 

823 in method_desc. 

824 schema: Object, mapping of schema names to schema descriptions. 

825 

826 Returns: 

827 The updated Dictionary stored in the 'parameters' key of the method 

828 description dictionary. 

829 """ 

830 parameters = method_desc.setdefault("parameters", {}) 

831 

832 # Add in the parameters common to all methods. 

833 for name, description in root_desc.get("parameters", {}).items(): 

834 parameters[name] = description 

835 

836 # Add in undocumented query parameters. 

837 for name in STACK_QUERY_PARAMETERS: 

838 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() 

839 

840 # Add 'body' (our own reserved word) to parameters if the method supports 

841 # a request payload. 

842 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc: 

843 body = BODY_PARAMETER_DEFAULT_VALUE.copy() 

844 body.update(method_desc["request"]) 

845 parameters["body"] = body 

846 

847 return parameters 

848 

849 

850def _fix_up_media_upload(method_desc, root_desc, path_url, parameters): 

851 """Adds 'media_body' and 'media_mime_type' parameters if supported by method. 

852 

853 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds 

854 'media_upload' key to parameters. 

855 

856 Args: 

857 method_desc: Dictionary with metadata describing an API method. Value comes 

858 from the dictionary of methods stored in the 'methods' key in the 

859 deserialized discovery document. 

860 root_desc: Dictionary; the entire original deserialized discovery document. 

861 path_url: String; the relative URL for the API method. Relative to the API 

862 root, which is specified in the discovery document. 

863 parameters: A dictionary describing method parameters for method described 

864 in method_desc. 

865 

866 Returns: 

867 Triple (accept, max_size, media_path_url) where: 

868 - accept is a list of strings representing what content types are 

869 accepted for media upload. Defaults to empty list if not in the 

870 discovery document. 

871 - max_size is a long representing the max size in bytes allowed for a 

872 media upload. Defaults to 0L if not in the discovery document. 

873 - media_path_url is a String; the absolute URI for media upload for the 

874 API method. Constructed using the API root URI and service path from 

875 the discovery document and the relative path for the API method. If 

876 media upload is not supported, this is None. 

877 """ 

878 media_upload = method_desc.get("mediaUpload", {}) 

879 accept = media_upload.get("accept", []) 

880 max_size = _media_size_to_long(media_upload.get("maxSize", "")) 

881 media_path_url = None 

882 

883 if media_upload: 

884 media_path_url = _media_path_url_from_info(root_desc, path_url) 

885 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy() 

886 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy() 

887 

888 return accept, max_size, media_path_url 

889 

890 

891def _fix_up_method_description(method_desc, root_desc, schema): 

892 """Updates a method description in a discovery document. 

893 

894 SIDE EFFECTS: Changes the parameters dictionary in the method description with 

895 extra parameters which are used locally. 

896 

897 Args: 

898 method_desc: Dictionary with metadata describing an API method. Value comes 

899 from the dictionary of methods stored in the 'methods' key in the 

900 deserialized discovery document. 

901 root_desc: Dictionary; the entire original deserialized discovery document. 

902 schema: Object, mapping of schema names to schema descriptions. 

903 

904 Returns: 

905 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url) 

906 where: 

907 - path_url is a String; the relative URL for the API method. Relative to 

908 the API root, which is specified in the discovery document. 

909 - http_method is a String; the HTTP method used to call the API method 

910 described in the method description. 

911 - method_id is a String; the name of the RPC method associated with the 

912 API method, and is in the method description in the 'id' key. 

913 - accept is a list of strings representing what content types are 

914 accepted for media upload. Defaults to empty list if not in the 

915 discovery document. 

916 - max_size is a long representing the max size in bytes allowed for a 

917 media upload. Defaults to 0L if not in the discovery document. 

918 - media_path_url is a String; the absolute URI for media upload for the 

919 API method. Constructed using the API root URI and service path from 

920 the discovery document and the relative path for the API method. If 

921 media upload is not supported, this is None. 

922 """ 

923 path_url = method_desc["path"] 

924 http_method = method_desc["httpMethod"] 

925 method_id = method_desc["id"] 

926 

927 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema) 

928 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a 

929 # 'parameters' key and needs to know if there is a 'body' parameter because it 

930 # also sets a 'media_body' parameter. 

931 accept, max_size, media_path_url = _fix_up_media_upload( 

932 method_desc, root_desc, path_url, parameters 

933 ) 

934 

935 return path_url, http_method, method_id, accept, max_size, media_path_url 

936 

937 

938def _fix_up_media_path_base_url(media_path_url, base_url): 

939 """ 

940 Update the media upload base url if its netloc doesn't match base url netloc. 

941 

942 This can happen in case the base url was overridden by 

943 client_options.api_endpoint. 

944 

945 Args: 

946 media_path_url: String; the absolute URI for media upload. 

947 base_url: string, base URL for the API. All requests are relative to this URI. 

948 

949 Returns: 

950 String; the absolute URI for media upload. 

951 """ 

952 parsed_media_url = urllib.parse.urlparse(media_path_url) 

953 parsed_base_url = urllib.parse.urlparse(base_url) 

954 if parsed_media_url.netloc == parsed_base_url.netloc: 

955 return media_path_url 

956 return urllib.parse.urlunparse( 

957 parsed_media_url._replace(netloc=parsed_base_url.netloc) 

958 ) 

959 

960 

961def _urljoin(base, url): 

962 """Custom urljoin replacement supporting : before / in url.""" 

963 # In general, it's unsafe to simply join base and url. However, for 

964 # the case of discovery documents, we know: 

965 # * base will never contain params, query, or fragment 

966 # * url will never contain a scheme or net_loc. 

967 # In general, this means we can safely join on /; we just need to 

968 # ensure we end up with precisely one / joining base and url. The 

969 # exception here is the case of media uploads, where url will be an 

970 # absolute url. 

971 if url.startswith("http://") or url.startswith("https://"): 

972 return urllib.parse.urljoin(base, url) 

973 new_base = base if base.endswith("/") else base + "/" 

974 new_url = url[1:] if url.startswith("/") else url 

975 return new_base + new_url 

976 

977 

978# TODO(dhermes): Convert this class to ResourceMethod and make it callable 

979class ResourceMethodParameters(object): 

980 """Represents the parameters associated with a method. 

981 

982 Attributes: 

983 argmap: Map from method parameter name (string) to query parameter name 

984 (string). 

985 required_params: List of required parameters (represented by parameter 

986 name as string). 

987 repeated_params: List of repeated parameters (represented by parameter 

988 name as string). 

989 pattern_params: Map from method parameter name (string) to regular 

990 expression (as a string). If the pattern is set for a parameter, the 

991 value for that parameter must match the regular expression. 

992 query_params: List of parameters (represented by parameter name as string) 

993 that will be used in the query string. 

994 path_params: Set of parameters (represented by parameter name as string) 

995 that will be used in the base URL path. 

996 param_types: Map from method parameter name (string) to parameter type. Type 

997 can be any valid JSON schema type; valid values are 'any', 'array', 

998 'boolean', 'integer', 'number', 'object', or 'string'. Reference: 

999 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 

1000 enum_params: Map from method parameter name (string) to list of strings, 

1001 where each list of strings is the list of acceptable enum values. 

1002 """ 

1003 

1004 def __init__(self, method_desc): 

1005 """Constructor for ResourceMethodParameters. 

1006 

1007 Sets default values and defers to set_parameters to populate. 

1008 

1009 Args: 

1010 method_desc: Dictionary with metadata describing an API method. Value 

1011 comes from the dictionary of methods stored in the 'methods' key in 

1012 the deserialized discovery document. 

1013 """ 

1014 self.argmap = {} 

1015 self.required_params = [] 

1016 self.repeated_params = [] 

1017 self.pattern_params = {} 

1018 self.query_params = [] 

1019 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE 

1020 # parsing is gotten rid of. 

1021 self.path_params = set() 

1022 self.param_types = {} 

1023 self.enum_params = {} 

1024 

1025 self.set_parameters(method_desc) 

1026 

1027 def set_parameters(self, method_desc): 

1028 """Populates maps and lists based on method description. 

1029 

1030 Iterates through each parameter for the method and parses the values from 

1031 the parameter dictionary. 

1032 

1033 Args: 

1034 method_desc: Dictionary with metadata describing an API method. Value 

1035 comes from the dictionary of methods stored in the 'methods' key in 

1036 the deserialized discovery document. 

1037 """ 

1038 parameters = method_desc.get("parameters", {}) 

1039 sorted_parameters = OrderedDict(sorted(parameters.items())) 

1040 for arg, desc in sorted_parameters.items(): 

1041 param = key2param(arg) 

1042 self.argmap[param] = arg 

1043 

1044 if desc.get("pattern"): 

1045 self.pattern_params[param] = desc["pattern"] 

1046 if desc.get("enum"): 

1047 self.enum_params[param] = desc["enum"] 

1048 if desc.get("required"): 

1049 self.required_params.append(param) 

1050 if desc.get("repeated"): 

1051 self.repeated_params.append(param) 

1052 if desc.get("location") == "query": 

1053 self.query_params.append(param) 

1054 if desc.get("location") == "path": 

1055 self.path_params.add(param) 

1056 self.param_types[param] = desc.get("type", "string") 

1057 

1058 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs 

1059 # should have all path parameters already marked with 

1060 # 'location: path'. 

1061 for match in URITEMPLATE.finditer(method_desc["path"]): 

1062 for namematch in VARNAME.finditer(match.group(0)): 

1063 name = key2param(namematch.group(0)) 

1064 self.path_params.add(name) 

1065 if name in self.query_params: 

1066 self.query_params.remove(name) 

1067 

1068 

1069def createMethod(methodName, methodDesc, rootDesc, schema): 

1070 """Creates a method for attaching to a Resource. 

1071 

1072 Args: 

1073 methodName: string, name of the method to use. 

1074 methodDesc: object, fragment of deserialized discovery document that 

1075 describes the method. 

1076 rootDesc: object, the entire deserialized discovery document. 

1077 schema: object, mapping of schema names to schema descriptions. 

1078 """ 

1079 methodName = fix_method_name(methodName) 

1080 ( 

1081 pathUrl, 

1082 httpMethod, 

1083 methodId, 

1084 accept, 

1085 maxSize, 

1086 mediaPathUrl, 

1087 ) = _fix_up_method_description(methodDesc, rootDesc, schema) 

1088 

1089 parameters = ResourceMethodParameters(methodDesc) 

1090 

1091 def method(self, **kwargs): 

1092 # Don't bother with doc string, it will be over-written by createMethod. 

1093 

1094 # Validate credentials for the configured universe. 

1095 self._validate_credentials() 

1096 

1097 for name in kwargs: 

1098 if name not in parameters.argmap: 

1099 raise TypeError("Got an unexpected keyword argument {}".format(name)) 

1100 

1101 # Remove args that have a value of None. 

1102 keys = list(kwargs.keys()) 

1103 for name in keys: 

1104 if kwargs[name] is None: 

1105 del kwargs[name] 

1106 

1107 for name in parameters.required_params: 

1108 if name not in kwargs: 

1109 # temporary workaround for non-paging methods incorrectly requiring 

1110 # page token parameter (cf. drive.changes.watch vs. drive.changes.list) 

1111 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName( 

1112 _methodProperties(methodDesc, schema, "response") 

1113 ): 

1114 raise TypeError('Missing required parameter "%s"' % name) 

1115 

1116 for name, regex in parameters.pattern_params.items(): 

1117 if name in kwargs: 

1118 if isinstance(kwargs[name], str): 

1119 pvalues = [kwargs[name]] 

1120 else: 

1121 pvalues = kwargs[name] 

1122 for pvalue in pvalues: 

1123 if re.match(regex, pvalue) is None: 

1124 raise TypeError( 

1125 'Parameter "%s" value "%s" does not match the pattern "%s"' 

1126 % (name, pvalue, regex) 

1127 ) 

1128 

1129 for name, enums in parameters.enum_params.items(): 

1130 if name in kwargs: 

1131 # We need to handle the case of a repeated enum 

1132 # name differently, since we want to handle both 

1133 # arg='value' and arg=['value1', 'value2'] 

1134 if name in parameters.repeated_params and not isinstance( 

1135 kwargs[name], str 

1136 ): 

1137 values = kwargs[name] 

1138 else: 

1139 values = [kwargs[name]] 

1140 for value in values: 

1141 if value not in enums: 

1142 raise TypeError( 

1143 'Parameter "%s" value "%s" is not an allowed value in "%s"' 

1144 % (name, value, str(enums)) 

1145 ) 

1146 

1147 actual_query_params = {} 

1148 actual_path_params = {} 

1149 for key, value in kwargs.items(): 

1150 to_type = parameters.param_types.get(key, "string") 

1151 # For repeated parameters we cast each member of the list. 

1152 if key in parameters.repeated_params and type(value) == type([]): 

1153 cast_value = [_cast(x, to_type) for x in value] 

1154 else: 

1155 cast_value = _cast(value, to_type) 

1156 if key in parameters.query_params: 

1157 actual_query_params[parameters.argmap[key]] = cast_value 

1158 if key in parameters.path_params: 

1159 actual_path_params[parameters.argmap[key]] = cast_value 

1160 body_value = kwargs.get("body", None) 

1161 media_filename = kwargs.get("media_body", None) 

1162 media_mime_type = kwargs.get("media_mime_type", None) 

1163 

1164 if self._developerKey: 

1165 actual_query_params["key"] = self._developerKey 

1166 

1167 model = self._model 

1168 if methodName.endswith("_media"): 

1169 model = MediaModel() 

1170 elif "response" not in methodDesc: 

1171 model = RawModel() 

1172 

1173 api_version = methodDesc.get("apiVersion", None) 

1174 

1175 headers = {} 

1176 headers, params, query, body = model.request( 

1177 headers, actual_path_params, actual_query_params, body_value, api_version 

1178 ) 

1179 

1180 expanded_url = uritemplate.expand(pathUrl, params) 

1181 url = _urljoin(self._baseUrl, expanded_url + query) 

1182 

1183 resumable = None 

1184 multipart_boundary = "" 

1185 

1186 if media_filename: 

1187 # Ensure we end up with a valid MediaUpload object. 

1188 if isinstance(media_filename, str): 

1189 if media_mime_type is None: 

1190 logger.warning( 

1191 "media_mime_type argument not specified: trying to auto-detect for %s", 

1192 media_filename, 

1193 ) 

1194 media_mime_type, _ = mimetypes.guess_type(media_filename) 

1195 if media_mime_type is None: 

1196 raise UnknownFileType(media_filename) 

1197 if not mimeparse.best_match([media_mime_type], ",".join(accept)): 

1198 raise UnacceptableMimeTypeError(media_mime_type) 

1199 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) 

1200 elif isinstance(media_filename, MediaUpload): 

1201 media_upload = media_filename 

1202 else: 

1203 raise TypeError("media_filename must be str or MediaUpload.") 

1204 

1205 # Check the maxSize 

1206 if media_upload.size() is not None and media_upload.size() > maxSize > 0: 

1207 raise MediaUploadSizeError("Media larger than: %s" % maxSize) 

1208 

1209 # Use the media path uri for media uploads 

1210 expanded_url = uritemplate.expand(mediaPathUrl, params) 

1211 url = _urljoin(self._baseUrl, expanded_url + query) 

1212 url = _fix_up_media_path_base_url(url, self._baseUrl) 

1213 if media_upload.resumable(): 

1214 url = _add_query_parameter(url, "uploadType", "resumable") 

1215 

1216 if media_upload.resumable(): 

1217 # This is all we need to do for resumable, if the body exists it gets 

1218 # sent in the first request, otherwise an empty body is sent. 

1219 resumable = media_upload 

1220 else: 

1221 # A non-resumable upload 

1222 if body is None: 

1223 # This is a simple media upload 

1224 headers["content-type"] = media_upload.mimetype() 

1225 body = media_upload.getbytes(0, media_upload.size()) 

1226 url = _add_query_parameter(url, "uploadType", "media") 

1227 else: 

1228 # This is a multipart/related upload. 

1229 msgRoot = MIMEMultipart("related") 

1230 # msgRoot should not write out it's own headers 

1231 setattr(msgRoot, "_write_headers", lambda self: None) 

1232 

1233 # attach the body as one part 

1234 msg = MIMENonMultipart(*headers["content-type"].split("/")) 

1235 msg.set_payload(body) 

1236 msgRoot.attach(msg) 

1237 

1238 # attach the media as the second part 

1239 msg = MIMENonMultipart(*media_upload.mimetype().split("/")) 

1240 msg["Content-Transfer-Encoding"] = "binary" 

1241 

1242 payload = media_upload.getbytes(0, media_upload.size()) 

1243 msg.set_payload(payload) 

1244 msgRoot.attach(msg) 

1245 # encode the body: note that we can't use `as_string`, because 

1246 # it plays games with `From ` lines. 

1247 fp = io.BytesIO() 

1248 g = _BytesGenerator(fp, mangle_from_=False) 

1249 g.flatten(msgRoot, unixfrom=False) 

1250 body = fp.getvalue() 

1251 

1252 multipart_boundary = msgRoot.get_boundary() 

1253 headers["content-type"] = ( 

1254 "multipart/related; " 'boundary="%s"' 

1255 ) % multipart_boundary 

1256 url = _add_query_parameter(url, "uploadType", "multipart") 

1257 

1258 logger.debug("URL being requested: %s %s" % (httpMethod, url)) 

1259 return self._requestBuilder( 

1260 self._http, 

1261 model.response, 

1262 url, 

1263 method=httpMethod, 

1264 body=body, 

1265 headers=headers, 

1266 methodId=methodId, 

1267 resumable=resumable, 

1268 ) 

1269 

1270 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"] 

1271 if len(parameters.argmap) > 0: 

1272 docs.append("Args:\n") 

1273 

1274 # Skip undocumented params and params common to all methods. 

1275 skip_parameters = list(rootDesc.get("parameters", {}).keys()) 

1276 skip_parameters.extend(STACK_QUERY_PARAMETERS) 

1277 

1278 all_args = list(parameters.argmap.keys()) 

1279 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])] 

1280 

1281 # Move body to the front of the line. 

1282 if "body" in all_args: 

1283 args_ordered.append("body") 

1284 

1285 for name in sorted(all_args): 

1286 if name not in args_ordered: 

1287 args_ordered.append(name) 

1288 

1289 for arg in args_ordered: 

1290 if arg in skip_parameters: 

1291 continue 

1292 

1293 repeated = "" 

1294 if arg in parameters.repeated_params: 

1295 repeated = " (repeated)" 

1296 required = "" 

1297 if arg in parameters.required_params: 

1298 required = " (required)" 

1299 paramdesc = methodDesc["parameters"][parameters.argmap[arg]] 

1300 paramdoc = paramdesc.get("description", "A parameter") 

1301 if "$ref" in paramdesc: 

1302 docs.append( 

1303 (" %s: object, %s%s%s\n The object takes the form of:\n\n%s\n\n") 

1304 % ( 

1305 arg, 

1306 paramdoc, 

1307 required, 

1308 repeated, 

1309 schema.prettyPrintByName(paramdesc["$ref"]), 

1310 ) 

1311 ) 

1312 else: 

1313 paramtype = paramdesc.get("type", "string") 

1314 docs.append( 

1315 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated) 

1316 ) 

1317 enum = paramdesc.get("enum", []) 

1318 enumDesc = paramdesc.get("enumDescriptions", []) 

1319 if enum and enumDesc: 

1320 docs.append(" Allowed values\n") 

1321 for (name, desc) in zip(enum, enumDesc): 

1322 docs.append(" %s - %s\n" % (name, desc)) 

1323 if "response" in methodDesc: 

1324 if methodName.endswith("_media"): 

1325 docs.append("\nReturns:\n The media object as a string.\n\n ") 

1326 else: 

1327 docs.append("\nReturns:\n An object of the form:\n\n ") 

1328 docs.append(schema.prettyPrintSchema(methodDesc["response"])) 

1329 

1330 setattr(method, "__doc__", "".join(docs)) 

1331 return (methodName, method) 

1332 

1333 

1334def createNextMethod( 

1335 methodName, 

1336 pageTokenName="pageToken", 

1337 nextPageTokenName="nextPageToken", 

1338 isPageTokenParameter=True, 

1339): 

1340 """Creates any _next methods for attaching to a Resource. 

1341 

1342 The _next methods allow for easy iteration through list() responses. 

1343 

1344 Args: 

1345 methodName: string, name of the method to use. 

1346 pageTokenName: string, name of request page token field. 

1347 nextPageTokenName: string, name of response page token field. 

1348 isPageTokenParameter: Boolean, True if request page token is a query 

1349 parameter, False if request page token is a field of the request body. 

1350 """ 

1351 methodName = fix_method_name(methodName) 

1352 

1353 def methodNext(self, previous_request, previous_response): 

1354 """Retrieves the next page of results. 

1355 

1356 Args: 

1357 previous_request: The request for the previous page. (required) 

1358 previous_response: The response from the request for the previous page. (required) 

1359 

1360 Returns: 

1361 A request object that you can call 'execute()' on to request the next 

1362 page. Returns None if there are no more items in the collection. 

1363 """ 

1364 # Retrieve nextPageToken from previous_response 

1365 # Use as pageToken in previous_request to create new request. 

1366 

1367 nextPageToken = previous_response.get(nextPageTokenName, None) 

1368 if not nextPageToken: 

1369 return None 

1370 

1371 request = copy.copy(previous_request) 

1372 

1373 if isPageTokenParameter: 

1374 # Replace pageToken value in URI 

1375 request.uri = _add_query_parameter( 

1376 request.uri, pageTokenName, nextPageToken 

1377 ) 

1378 logger.debug("Next page request URL: %s %s" % (methodName, request.uri)) 

1379 else: 

1380 # Replace pageToken value in request body 

1381 model = self._model 

1382 body = model.deserialize(request.body) 

1383 body[pageTokenName] = nextPageToken 

1384 request.body = model.serialize(body) 

1385 request.body_size = len(request.body) 

1386 if "content-length" in request.headers: 

1387 del request.headers["content-length"] 

1388 logger.debug("Next page request body: %s %s" % (methodName, body)) 

1389 

1390 return request 

1391 

1392 return (methodName, methodNext) 

1393 

1394 

1395class Resource(object): 

1396 """A class for interacting with a resource.""" 

1397 

1398 def __init__( 

1399 self, 

1400 http, 

1401 baseUrl, 

1402 model, 

1403 requestBuilder, 

1404 developerKey, 

1405 resourceDesc, 

1406 rootDesc, 

1407 schema, 

1408 universe_domain=universe.DEFAULT_UNIVERSE if HAS_UNIVERSE else "", 

1409 ): 

1410 """Build a Resource from the API description. 

1411 

1412 Args: 

1413 http: httplib2.Http, Object to make http requests with. 

1414 baseUrl: string, base URL for the API. All requests are relative to this 

1415 URI. 

1416 model: googleapiclient.Model, converts to and from the wire format. 

1417 requestBuilder: class or callable that instantiates an 

1418 googleapiclient.HttpRequest object. 

1419 developerKey: string, key obtained from 

1420 https://code.google.com/apis/console 

1421 resourceDesc: object, section of deserialized discovery document that 

1422 describes a resource. Note that the top level discovery document 

1423 is considered a resource. 

1424 rootDesc: object, the entire deserialized discovery document. 

1425 schema: object, mapping of schema names to schema descriptions. 

1426 universe_domain: string, the universe for the API. The default universe 

1427 is "googleapis.com". 

1428 """ 

1429 self._dynamic_attrs = [] 

1430 

1431 self._http = http 

1432 self._baseUrl = baseUrl 

1433 self._model = model 

1434 self._developerKey = developerKey 

1435 self._requestBuilder = requestBuilder 

1436 self._resourceDesc = resourceDesc 

1437 self._rootDesc = rootDesc 

1438 self._schema = schema 

1439 self._universe_domain = universe_domain 

1440 self._credentials_validated = False 

1441 

1442 self._set_service_methods() 

1443 

1444 def _set_dynamic_attr(self, attr_name, value): 

1445 """Sets an instance attribute and tracks it in a list of dynamic attributes. 

1446 

1447 Args: 

1448 attr_name: string; The name of the attribute to be set 

1449 value: The value being set on the object and tracked in the dynamic cache. 

1450 """ 

1451 self._dynamic_attrs.append(attr_name) 

1452 self.__dict__[attr_name] = value 

1453 

1454 def __getstate__(self): 

1455 """Trim the state down to something that can be pickled. 

1456 

1457 Uses the fact that the instance variable _dynamic_attrs holds attrs that 

1458 will be wiped and restored on pickle serialization. 

1459 """ 

1460 state_dict = copy.copy(self.__dict__) 

1461 for dynamic_attr in self._dynamic_attrs: 

1462 del state_dict[dynamic_attr] 

1463 del state_dict["_dynamic_attrs"] 

1464 return state_dict 

1465 

1466 def __setstate__(self, state): 

1467 """Reconstitute the state of the object from being pickled. 

1468 

1469 Uses the fact that the instance variable _dynamic_attrs holds attrs that 

1470 will be wiped and restored on pickle serialization. 

1471 """ 

1472 self.__dict__.update(state) 

1473 self._dynamic_attrs = [] 

1474 self._set_service_methods() 

1475 

1476 def __enter__(self): 

1477 return self 

1478 

1479 def __exit__(self, exc_type, exc, exc_tb): 

1480 self.close() 

1481 

1482 def close(self): 

1483 """Close httplib2 connections.""" 

1484 # httplib2 leaves sockets open by default. 

1485 # Cleanup using the `close` method. 

1486 # https://github.com/httplib2/httplib2/issues/148 

1487 self._http.close() 

1488 

1489 def _set_service_methods(self): 

1490 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) 

1491 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) 

1492 self._add_next_methods(self._resourceDesc, self._schema) 

1493 

1494 def _add_basic_methods(self, resourceDesc, rootDesc, schema): 

1495 # If this is the root Resource, add a new_batch_http_request() method. 

1496 if resourceDesc == rootDesc: 

1497 batch_uri = "%s%s" % ( 

1498 rootDesc["rootUrl"], 

1499 rootDesc.get("batchPath", "batch"), 

1500 ) 

1501 

1502 def new_batch_http_request(callback=None): 

1503 """Create a BatchHttpRequest object based on the discovery document. 

1504 

1505 Args: 

1506 callback: callable, A callback to be called for each response, of the 

1507 form callback(id, response, exception). The first parameter is the 

1508 request id, and the second is the deserialized response object. The 

1509 third is an apiclient.errors.HttpError exception object if an HTTP 

1510 error occurred while processing the request, or None if no error 

1511 occurred. 

1512 

1513 Returns: 

1514 A BatchHttpRequest object based on the discovery document. 

1515 """ 

1516 return BatchHttpRequest(callback=callback, batch_uri=batch_uri) 

1517 

1518 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request) 

1519 

1520 # Add basic methods to Resource 

1521 if "methods" in resourceDesc: 

1522 for methodName, methodDesc in resourceDesc["methods"].items(): 

1523 fixedMethodName, method = createMethod( 

1524 methodName, methodDesc, rootDesc, schema 

1525 ) 

1526 self._set_dynamic_attr( 

1527 fixedMethodName, method.__get__(self, self.__class__) 

1528 ) 

1529 # Add in _media methods. The functionality of the attached method will 

1530 # change when it sees that the method name ends in _media. 

1531 if methodDesc.get("supportsMediaDownload", False): 

1532 fixedMethodName, method = createMethod( 

1533 methodName + "_media", methodDesc, rootDesc, schema 

1534 ) 

1535 self._set_dynamic_attr( 

1536 fixedMethodName, method.__get__(self, self.__class__) 

1537 ) 

1538 

1539 def _add_nested_resources(self, resourceDesc, rootDesc, schema): 

1540 # Add in nested resources 

1541 if "resources" in resourceDesc: 

1542 

1543 def createResourceMethod(methodName, methodDesc): 

1544 """Create a method on the Resource to access a nested Resource. 

1545 

1546 Args: 

1547 methodName: string, name of the method to use. 

1548 methodDesc: object, fragment of deserialized discovery document that 

1549 describes the method. 

1550 """ 

1551 methodName = fix_method_name(methodName) 

1552 

1553 def methodResource(self): 

1554 return Resource( 

1555 http=self._http, 

1556 baseUrl=self._baseUrl, 

1557 model=self._model, 

1558 developerKey=self._developerKey, 

1559 requestBuilder=self._requestBuilder, 

1560 resourceDesc=methodDesc, 

1561 rootDesc=rootDesc, 

1562 schema=schema, 

1563 universe_domain=self._universe_domain, 

1564 ) 

1565 

1566 setattr(methodResource, "__doc__", "A collection resource.") 

1567 setattr(methodResource, "__is_resource__", True) 

1568 

1569 return (methodName, methodResource) 

1570 

1571 for methodName, methodDesc in resourceDesc["resources"].items(): 

1572 fixedMethodName, method = createResourceMethod(methodName, methodDesc) 

1573 self._set_dynamic_attr( 

1574 fixedMethodName, method.__get__(self, self.__class__) 

1575 ) 

1576 

1577 def _add_next_methods(self, resourceDesc, schema): 

1578 # Add _next() methods if and only if one of the names 'pageToken' or 

1579 # 'nextPageToken' occurs among the fields of both the method's response 

1580 # type either the method's request (query parameters) or request body. 

1581 if "methods" not in resourceDesc: 

1582 return 

1583 for methodName, methodDesc in resourceDesc["methods"].items(): 

1584 nextPageTokenName = _findPageTokenName( 

1585 _methodProperties(methodDesc, schema, "response") 

1586 ) 

1587 if not nextPageTokenName: 

1588 continue 

1589 isPageTokenParameter = True 

1590 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {})) 

1591 if not pageTokenName: 

1592 isPageTokenParameter = False 

1593 pageTokenName = _findPageTokenName( 

1594 _methodProperties(methodDesc, schema, "request") 

1595 ) 

1596 if not pageTokenName: 

1597 continue 

1598 fixedMethodName, method = createNextMethod( 

1599 methodName + "_next", 

1600 pageTokenName, 

1601 nextPageTokenName, 

1602 isPageTokenParameter, 

1603 ) 

1604 self._set_dynamic_attr( 

1605 fixedMethodName, method.__get__(self, self.__class__) 

1606 ) 

1607 

1608 def _validate_credentials(self): 

1609 """Validates client's and credentials' universe domains are consistent. 

1610 

1611 Returns: 

1612 bool: True iff the configured universe domain is valid. 

1613 

1614 Raises: 

1615 UniverseMismatchError: If the configured universe domain is not valid. 

1616 """ 

1617 credentials = getattr(self._http, "credentials", None) 

1618 

1619 self._credentials_validated = ( 

1620 ( 

1621 self._credentials_validated 

1622 or universe.compare_domains(self._universe_domain, credentials) 

1623 ) 

1624 if HAS_UNIVERSE 

1625 else True 

1626 ) 

1627 return self._credentials_validated 

1628 

1629 

1630def _findPageTokenName(fields): 

1631 """Search field names for one like a page token. 

1632 

1633 Args: 

1634 fields: container of string, names of fields. 

1635 

1636 Returns: 

1637 First name that is either 'pageToken' or 'nextPageToken' if one exists, 

1638 otherwise None. 

1639 """ 

1640 return next( 

1641 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None 

1642 ) 

1643 

1644 

1645def _methodProperties(methodDesc, schema, name): 

1646 """Get properties of a field in a method description. 

1647 

1648 Args: 

1649 methodDesc: object, fragment of deserialized discovery document that 

1650 describes the method. 

1651 schema: object, mapping of schema names to schema descriptions. 

1652 name: string, name of top-level field in method description. 

1653 

1654 Returns: 

1655 Object representing fragment of deserialized discovery document 

1656 corresponding to 'properties' field of object corresponding to named field 

1657 in method description, if it exists, otherwise empty dict. 

1658 """ 

1659 desc = methodDesc.get(name, {}) 

1660 if "$ref" in desc: 

1661 desc = schema.get(desc["$ref"], {}) 

1662 return desc.get("properties", {})