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

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

590 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 if hasattr(mtls, "should_use_client_cert"): 

653 use_client_cert = mtls.should_use_client_cert() 

654 else: 

655 # if unsupported, fallback to reading from env var 

656 use_client_cert_str = os.getenv( 

657 "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" 

658 ).lower() 

659 use_client_cert = use_client_cert_str == "true" 

660 if use_client_cert_str not in ("true", "false"): 

661 raise MutualTLSChannelError( 

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

663 ) 

664 if client_options and client_options.client_cert_source: 

665 raise MutualTLSChannelError( 

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

667 ) 

668 if use_client_cert: 

669 if ( 

670 client_options 

671 and hasattr(client_options, "client_encrypted_cert_source") 

672 and client_options.client_encrypted_cert_source 

673 ): 

674 client_cert_to_use = client_options.client_encrypted_cert_source 

675 elif ( 

676 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source() 

677 ): 

678 client_cert_to_use = mtls.default_client_encrypted_cert_source( 

679 adc_cert_path, adc_key_path 

680 ) 

681 if client_cert_to_use: 

682 cert_path, key_path, passphrase = client_cert_to_use() 

683 

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

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

686 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp. 

687 http_channel = ( 

688 http.http 

689 if google_auth_httplib2 

690 and isinstance(http, google_auth_httplib2.AuthorizedHttp) 

691 else http 

692 ) 

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

694 

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

696 # api endpoint to use. 

697 if "mtlsRootUrl" in service and ( 

698 not client_options or not client_options.api_endpoint 

699 ): 

700 mtls_endpoint = urllib.parse.urljoin( 

701 service["mtlsRootUrl"], service["servicePath"] 

702 ) 

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

704 

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

706 raise MutualTLSChannelError( 

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

708 ) 

709 

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

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

712 if use_mtls_endpoint == "always" or ( 

713 use_mtls_endpoint == "auto" and client_cert_to_use 

714 ): 

715 if HAS_UNIVERSE and universe_domain != universe.DEFAULT_UNIVERSE: 

716 raise MutualTLSChannelError( 

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

718 ) 

719 base = mtls_endpoint 

720 else: 

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

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

723 _check_api_core_compatible_with_credentials_universe(http_credentials) 

724 

725 if model is None: 

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

727 model = JsonModel("dataWrapper" in features) 

728 

729 return Resource( 

730 http=http, 

731 baseUrl=base, 

732 model=model, 

733 developerKey=developerKey, 

734 requestBuilder=requestBuilder, 

735 resourceDesc=service, 

736 rootDesc=service, 

737 schema=schema, 

738 universe_domain=universe_domain, 

739 ) 

740 

741 

742def _cast(value, schema_type): 

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

744 

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

746 JSON Schema. 

747 

748 Args: 

749 value: any, the value to convert 

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

751 

752 Returns: 

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

754 """ 

755 if schema_type == "string": 

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

757 return value 

758 else: 

759 return str(value) 

760 elif schema_type == "integer": 

761 return str(int(value)) 

762 elif schema_type == "number": 

763 return str(float(value)) 

764 elif schema_type == "boolean": 

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

766 else: 

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

768 return value 

769 else: 

770 return str(value) 

771 

772 

773def _media_size_to_long(maxSize): 

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

775 

776 Args: 

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

778 

779 Returns: 

780 The size as an integer value. 

781 """ 

782 if len(maxSize) < 2: 

783 return 0 

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

785 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) 

786 if bit_shift is not None: 

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

788 else: 

789 return int(maxSize) 

790 

791 

792def _media_path_url_from_info(root_desc, path_url): 

793 """Creates an absolute media path URL. 

794 

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

796 document and the relative path for the API method. 

797 

798 Args: 

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

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

801 root, which is specified in the discovery document. 

802 

803 Returns: 

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

805 """ 

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

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

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

809 "path": path_url, 

810 } 

811 

812 

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

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

815 

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

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

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

819 APIs (these are listed in STACK_QUERY_PARAMETERS). 

820 

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

822 description. 

823 

824 Args: 

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

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

827 deserialized discovery document. 

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

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

830 in method_desc. 

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

832 

833 Returns: 

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

835 description dictionary. 

836 """ 

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

838 

839 # Add in the parameters common to all methods. 

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

841 parameters[name] = description 

842 

843 # Add in undocumented query parameters. 

844 for name in STACK_QUERY_PARAMETERS: 

845 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() 

846 

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

848 # a request payload. 

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

850 body = BODY_PARAMETER_DEFAULT_VALUE.copy() 

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

852 parameters["body"] = body 

853 

854 return parameters 

855 

856 

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

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

859 

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

861 'media_upload' key to parameters. 

862 

863 Args: 

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

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

866 deserialized discovery document. 

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

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

869 root, which is specified in the discovery document. 

870 parameters: A dictionary describing method parameters for method described 

871 in method_desc. 

872 

873 Returns: 

874 Triple (accept, max_size, media_path_url) where: 

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

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

877 discovery document. 

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

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

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

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

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

883 media upload is not supported, this is None. 

884 """ 

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

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

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

888 media_path_url = None 

889 

890 if media_upload: 

891 media_path_url = _media_path_url_from_info(root_desc, path_url) 

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

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

894 

895 return accept, max_size, media_path_url 

896 

897 

898def _fix_up_method_description(method_desc, root_desc, schema): 

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

900 

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

902 extra parameters which are used locally. 

903 

904 Args: 

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

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

907 deserialized discovery document. 

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

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

910 

911 Returns: 

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

913 where: 

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

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

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

917 described in the method description. 

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

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

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

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

922 discovery document. 

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

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

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

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

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

928 media upload is not supported, this is None. 

929 """ 

930 path_url = method_desc["path"] 

931 http_method = method_desc["httpMethod"] 

932 method_id = method_desc["id"] 

933 

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

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

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

937 # also sets a 'media_body' parameter. 

938 accept, max_size, media_path_url = _fix_up_media_upload( 

939 method_desc, root_desc, path_url, parameters 

940 ) 

941 

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

943 

944 

945def _fix_up_media_path_base_url(media_path_url, base_url): 

946 """ 

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

948 

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

950 client_options.api_endpoint. 

951 

952 Args: 

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

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

955 

956 Returns: 

957 String; the absolute URI for media upload. 

958 """ 

959 parsed_media_url = urllib.parse.urlparse(media_path_url) 

960 parsed_base_url = urllib.parse.urlparse(base_url) 

961 if parsed_media_url.netloc == parsed_base_url.netloc: 

962 return media_path_url 

963 return urllib.parse.urlunparse( 

964 parsed_media_url._replace(netloc=parsed_base_url.netloc) 

965 ) 

966 

967 

968def _urljoin(base, url): 

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

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

971 # the case of discovery documents, we know: 

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

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

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

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

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

977 # absolute url. 

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

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

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

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

982 return new_base + new_url 

983 

984 

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

986class ResourceMethodParameters(object): 

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

988 

989 Attributes: 

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

991 (string). 

992 required_params: List of required parameters (represented by parameter 

993 name as string). 

994 repeated_params: List of repeated parameters (represented by parameter 

995 name as string). 

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

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

998 value for that parameter must match the regular expression. 

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

1000 that will be used in the query string. 

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

1002 that will be used in the base URL path. 

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

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

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

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

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

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

1009 """ 

1010 

1011 def __init__(self, method_desc): 

1012 """Constructor for ResourceMethodParameters. 

1013 

1014 Sets default values and defers to set_parameters to populate. 

1015 

1016 Args: 

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

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

1019 the deserialized discovery document. 

1020 """ 

1021 self.argmap = {} 

1022 self.required_params = [] 

1023 self.repeated_params = [] 

1024 self.pattern_params = {} 

1025 self.query_params = [] 

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

1027 # parsing is gotten rid of. 

1028 self.path_params = set() 

1029 self.param_types = {} 

1030 self.enum_params = {} 

1031 

1032 self.set_parameters(method_desc) 

1033 

1034 def set_parameters(self, method_desc): 

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

1036 

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

1038 the parameter dictionary. 

1039 

1040 Args: 

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

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

1043 the deserialized discovery document. 

1044 """ 

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

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

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

1048 param = key2param(arg) 

1049 self.argmap[param] = arg 

1050 

1051 if desc.get("pattern"): 

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

1053 if desc.get("enum"): 

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

1055 if desc.get("required"): 

1056 self.required_params.append(param) 

1057 if desc.get("repeated"): 

1058 self.repeated_params.append(param) 

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

1060 self.query_params.append(param) 

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

1062 self.path_params.add(param) 

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

1064 

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

1066 # should have all path parameters already marked with 

1067 # 'location: path'. 

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

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

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

1071 self.path_params.add(name) 

1072 if name in self.query_params: 

1073 self.query_params.remove(name) 

1074 

1075 

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

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

1078 

1079 Args: 

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

1081 methodDesc: object, fragment of deserialized discovery document that 

1082 describes the method. 

1083 rootDesc: object, the entire deserialized discovery document. 

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

1085 """ 

1086 methodName = fix_method_name(methodName) 

1087 ( 

1088 pathUrl, 

1089 httpMethod, 

1090 methodId, 

1091 accept, 

1092 maxSize, 

1093 mediaPathUrl, 

1094 ) = _fix_up_method_description(methodDesc, rootDesc, schema) 

1095 

1096 parameters = ResourceMethodParameters(methodDesc) 

1097 

1098 def method(self, **kwargs): 

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

1100 

1101 # Validate credentials for the configured universe. 

1102 self._validate_credentials() 

1103 

1104 for name in kwargs: 

1105 if name not in parameters.argmap: 

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

1107 

1108 # Remove args that have a value of None. 

1109 keys = list(kwargs.keys()) 

1110 for name in keys: 

1111 if kwargs[name] is None: 

1112 del kwargs[name] 

1113 

1114 for name in parameters.required_params: 

1115 if name not in kwargs: 

1116 # temporary workaround for non-paging methods incorrectly requiring 

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

1118 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName( 

1119 _methodProperties(methodDesc, schema, "response") 

1120 ): 

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

1122 

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

1124 if name in kwargs: 

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

1126 pvalues = [kwargs[name]] 

1127 else: 

1128 pvalues = kwargs[name] 

1129 for pvalue in pvalues: 

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

1131 raise TypeError( 

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

1133 % (name, pvalue, regex) 

1134 ) 

1135 

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

1137 if name in kwargs: 

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

1139 # name differently, since we want to handle both 

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

1141 if name in parameters.repeated_params and not isinstance( 

1142 kwargs[name], str 

1143 ): 

1144 values = kwargs[name] 

1145 else: 

1146 values = [kwargs[name]] 

1147 for value in values: 

1148 if value not in enums: 

1149 raise TypeError( 

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

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

1152 ) 

1153 

1154 actual_query_params = {} 

1155 actual_path_params = {} 

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

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

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

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

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

1161 else: 

1162 cast_value = _cast(value, to_type) 

1163 if key in parameters.query_params: 

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

1165 if key in parameters.path_params: 

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

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

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

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

1170 

1171 if self._developerKey: 

1172 actual_query_params["key"] = self._developerKey 

1173 

1174 model = self._model 

1175 if methodName.endswith("_media"): 

1176 model = MediaModel() 

1177 elif "response" not in methodDesc: 

1178 model = RawModel() 

1179 

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

1181 

1182 headers = {} 

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

1184 headers, actual_path_params, actual_query_params, body_value, api_version 

1185 ) 

1186 

1187 expanded_url = uritemplate.expand(pathUrl, params) 

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

1189 

1190 resumable = None 

1191 multipart_boundary = "" 

1192 

1193 if media_filename: 

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

1195 if isinstance(media_filename, str): 

1196 if media_mime_type is None: 

1197 logger.warning( 

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

1199 media_filename, 

1200 ) 

1201 media_mime_type, _ = mimetypes.guess_type(media_filename) 

1202 if media_mime_type is None: 

1203 raise UnknownFileType(media_filename) 

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

1205 raise UnacceptableMimeTypeError(media_mime_type) 

1206 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) 

1207 elif isinstance(media_filename, MediaUpload): 

1208 media_upload = media_filename 

1209 else: 

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

1211 

1212 # Check the maxSize 

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

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

1215 

1216 # Use the media path uri for media uploads 

1217 expanded_url = uritemplate.expand(mediaPathUrl, params) 

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

1219 url = _fix_up_media_path_base_url(url, self._baseUrl) 

1220 if media_upload.resumable(): 

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

1222 

1223 if media_upload.resumable(): 

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

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

1226 resumable = media_upload 

1227 else: 

1228 # A non-resumable upload 

1229 if body is None: 

1230 # This is a simple media upload 

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

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

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

1234 else: 

1235 # This is a multipart/related upload. 

1236 msgRoot = MIMEMultipart("related") 

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

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

1239 

1240 # attach the body as one part 

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

1242 msg.set_payload(body) 

1243 msgRoot.attach(msg) 

1244 

1245 # attach the media as the second part 

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

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

1248 

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

1250 msg.set_payload(payload) 

1251 msgRoot.attach(msg) 

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

1253 # it plays games with `From ` lines. 

1254 fp = io.BytesIO() 

1255 g = _BytesGenerator(fp, mangle_from_=False) 

1256 g.flatten(msgRoot, unixfrom=False) 

1257 body = fp.getvalue() 

1258 

1259 multipart_boundary = msgRoot.get_boundary() 

1260 headers["content-type"] = ( 

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

1262 ) % multipart_boundary 

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

1264 

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

1266 return self._requestBuilder( 

1267 self._http, 

1268 model.response, 

1269 url, 

1270 method=httpMethod, 

1271 body=body, 

1272 headers=headers, 

1273 methodId=methodId, 

1274 resumable=resumable, 

1275 ) 

1276 

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

1278 if len(parameters.argmap) > 0: 

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

1280 

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

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

1283 skip_parameters.extend(STACK_QUERY_PARAMETERS) 

1284 

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

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

1287 

1288 # Move body to the front of the line. 

1289 if "body" in all_args: 

1290 args_ordered.append("body") 

1291 

1292 for name in sorted(all_args): 

1293 if name not in args_ordered: 

1294 args_ordered.append(name) 

1295 

1296 for arg in args_ordered: 

1297 if arg in skip_parameters: 

1298 continue 

1299 

1300 repeated = "" 

1301 if arg in parameters.repeated_params: 

1302 repeated = " (repeated)" 

1303 required = "" 

1304 if arg in parameters.required_params: 

1305 required = " (required)" 

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

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

1308 if "$ref" in paramdesc: 

1309 docs.append( 

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

1311 % ( 

1312 arg, 

1313 paramdoc, 

1314 required, 

1315 repeated, 

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

1317 ) 

1318 ) 

1319 else: 

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

1321 docs.append( 

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

1323 ) 

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

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

1326 if enum and enumDesc: 

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

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

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

1330 if "response" in methodDesc: 

1331 if methodName.endswith("_media"): 

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

1333 else: 

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

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

1336 

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

1338 return (methodName, method) 

1339 

1340 

1341def createNextMethod( 

1342 methodName, 

1343 pageTokenName="pageToken", 

1344 nextPageTokenName="nextPageToken", 

1345 isPageTokenParameter=True, 

1346): 

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

1348 

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

1350 

1351 Args: 

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

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

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

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

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

1357 """ 

1358 methodName = fix_method_name(methodName) 

1359 

1360 def methodNext(self, previous_request, previous_response): 

1361 """Retrieves the next page of results. 

1362 

1363 Args: 

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

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

1366 

1367 Returns: 

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

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

1370 """ 

1371 # Retrieve nextPageToken from previous_response 

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

1373 

1374 nextPageToken = previous_response.get(nextPageTokenName, None) 

1375 if not nextPageToken: 

1376 return None 

1377 

1378 request = copy.copy(previous_request) 

1379 

1380 if isPageTokenParameter: 

1381 # Replace pageToken value in URI 

1382 request.uri = _add_query_parameter( 

1383 request.uri, pageTokenName, nextPageToken 

1384 ) 

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

1386 else: 

1387 # Replace pageToken value in request body 

1388 model = self._model 

1389 body = model.deserialize(request.body) 

1390 body[pageTokenName] = nextPageToken 

1391 request.body = model.serialize(body) 

1392 request.body_size = len(request.body) 

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

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

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

1396 

1397 return request 

1398 

1399 return (methodName, methodNext) 

1400 

1401 

1402class Resource(object): 

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

1404 

1405 def __init__( 

1406 self, 

1407 http, 

1408 baseUrl, 

1409 model, 

1410 requestBuilder, 

1411 developerKey, 

1412 resourceDesc, 

1413 rootDesc, 

1414 schema, 

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

1416 ): 

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

1418 

1419 Args: 

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

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

1422 URI. 

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

1424 requestBuilder: class or callable that instantiates an 

1425 googleapiclient.HttpRequest object. 

1426 developerKey: string, key obtained from 

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

1428 resourceDesc: object, section of deserialized discovery document that 

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

1430 is considered a resource. 

1431 rootDesc: object, the entire deserialized discovery document. 

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

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

1434 is "googleapis.com". 

1435 """ 

1436 self._dynamic_attrs = [] 

1437 

1438 self._http = http 

1439 self._baseUrl = baseUrl 

1440 self._model = model 

1441 self._developerKey = developerKey 

1442 self._requestBuilder = requestBuilder 

1443 self._resourceDesc = resourceDesc 

1444 self._rootDesc = rootDesc 

1445 self._schema = schema 

1446 self._universe_domain = universe_domain 

1447 self._credentials_validated = False 

1448 

1449 self._set_service_methods() 

1450 

1451 def _set_dynamic_attr(self, attr_name, value): 

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

1453 

1454 Args: 

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

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

1457 """ 

1458 self._dynamic_attrs.append(attr_name) 

1459 self.__dict__[attr_name] = value 

1460 

1461 def __getstate__(self): 

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

1463 

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

1465 will be wiped and restored on pickle serialization. 

1466 """ 

1467 state_dict = copy.copy(self.__dict__) 

1468 for dynamic_attr in self._dynamic_attrs: 

1469 del state_dict[dynamic_attr] 

1470 del state_dict["_dynamic_attrs"] 

1471 return state_dict 

1472 

1473 def __setstate__(self, state): 

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

1475 

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

1477 will be wiped and restored on pickle serialization. 

1478 """ 

1479 self.__dict__.update(state) 

1480 self._dynamic_attrs = [] 

1481 self._set_service_methods() 

1482 

1483 def __enter__(self): 

1484 return self 

1485 

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

1487 self.close() 

1488 

1489 def close(self): 

1490 """Close httplib2 connections.""" 

1491 # httplib2 leaves sockets open by default. 

1492 # Cleanup using the `close` method. 

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

1494 self._http.close() 

1495 

1496 def _set_service_methods(self): 

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

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

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

1500 

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

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

1503 if resourceDesc == rootDesc: 

1504 batch_uri = "%s%s" % ( 

1505 rootDesc["rootUrl"], 

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

1507 ) 

1508 

1509 def new_batch_http_request(callback=None): 

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

1511 

1512 Args: 

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

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

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

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

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

1518 occurred. 

1519 

1520 Returns: 

1521 A BatchHttpRequest object based on the discovery document. 

1522 """ 

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

1524 

1525 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request) 

1526 

1527 # Add basic methods to Resource 

1528 if "methods" in resourceDesc: 

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

1530 fixedMethodName, method = createMethod( 

1531 methodName, methodDesc, rootDesc, schema 

1532 ) 

1533 self._set_dynamic_attr( 

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

1535 ) 

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

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

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

1539 fixedMethodName, method = createMethod( 

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

1541 ) 

1542 self._set_dynamic_attr( 

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

1544 ) 

1545 

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

1547 # Add in nested resources 

1548 if "resources" in resourceDesc: 

1549 

1550 def createResourceMethod(methodName, methodDesc): 

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

1552 

1553 Args: 

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

1555 methodDesc: object, fragment of deserialized discovery document that 

1556 describes the method. 

1557 """ 

1558 methodName = fix_method_name(methodName) 

1559 

1560 def methodResource(self): 

1561 return Resource( 

1562 http=self._http, 

1563 baseUrl=self._baseUrl, 

1564 model=self._model, 

1565 developerKey=self._developerKey, 

1566 requestBuilder=self._requestBuilder, 

1567 resourceDesc=methodDesc, 

1568 rootDesc=rootDesc, 

1569 schema=schema, 

1570 universe_domain=self._universe_domain, 

1571 ) 

1572 

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

1574 setattr(methodResource, "__is_resource__", True) 

1575 

1576 return (methodName, methodResource) 

1577 

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

1579 fixedMethodName, method = createResourceMethod(methodName, methodDesc) 

1580 self._set_dynamic_attr( 

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

1582 ) 

1583 

1584 def _add_next_methods(self, resourceDesc, schema): 

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

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

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

1588 if "methods" not in resourceDesc: 

1589 return 

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

1591 nextPageTokenName = _findPageTokenName( 

1592 _methodProperties(methodDesc, schema, "response") 

1593 ) 

1594 if not nextPageTokenName: 

1595 continue 

1596 isPageTokenParameter = True 

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

1598 if not pageTokenName: 

1599 isPageTokenParameter = False 

1600 pageTokenName = _findPageTokenName( 

1601 _methodProperties(methodDesc, schema, "request") 

1602 ) 

1603 if not pageTokenName: 

1604 continue 

1605 fixedMethodName, method = createNextMethod( 

1606 methodName + "_next", 

1607 pageTokenName, 

1608 nextPageTokenName, 

1609 isPageTokenParameter, 

1610 ) 

1611 self._set_dynamic_attr( 

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

1613 ) 

1614 

1615 def _validate_credentials(self): 

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

1617 

1618 Returns: 

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

1620 

1621 Raises: 

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

1623 """ 

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

1625 

1626 self._credentials_validated = ( 

1627 ( 

1628 self._credentials_validated 

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

1630 ) 

1631 if HAS_UNIVERSE 

1632 else True 

1633 ) 

1634 return self._credentials_validated 

1635 

1636 

1637def _findPageTokenName(fields): 

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

1639 

1640 Args: 

1641 fields: container of string, names of fields. 

1642 

1643 Returns: 

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

1645 otherwise None. 

1646 """ 

1647 return next( 

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

1649 ) 

1650 

1651 

1652def _methodProperties(methodDesc, schema, name): 

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

1654 

1655 Args: 

1656 methodDesc: object, fragment of deserialized discovery document that 

1657 describes the method. 

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

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

1660 

1661 Returns: 

1662 Object representing fragment of deserialized discovery document 

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

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

1665 """ 

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

1667 if "$ref" in desc: 

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

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