Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/api_core/grpc_helpers.py: 29%

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

176 statements  

1# Copyright 2017 Google LLC 

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"""Helpers for :mod:`grpc`.""" 

16 

17import collections 

18import functools 

19import warnings 

20from typing import Generic, Iterator, Optional, TypeVar 

21 

22import google.auth 

23import google.auth.credentials 

24import google.auth.transport.grpc 

25import google.auth.transport.requests 

26import google.protobuf 

27import grpc 

28 

29from google.api_core import exceptions, general_helpers 

30 

31# The list of gRPC Callable interfaces that return iterators. 

32_STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable) 

33 

34# denotes the proto response type for grpc calls 

35P = TypeVar("P") 

36 

37 

38def _patch_callable_name(callable_): 

39 """Fix-up gRPC callable attributes. 

40 

41 gRPC callable lack the ``__name__`` attribute which causes 

42 :func:`functools.wraps` to error. This adds the attribute if needed. 

43 """ 

44 if not hasattr(callable_, "__name__"): 

45 callable_.__name__ = callable_.__class__.__name__ 

46 

47 

48def _wrap_unary_errors(callable_): 

49 """Map errors for Unary-Unary and Stream-Unary gRPC callables.""" 

50 _patch_callable_name(callable_) 

51 

52 @functools.wraps(callable_) 

53 def error_remapped_callable(*args, **kwargs): 

54 try: 

55 return callable_(*args, **kwargs) 

56 except grpc.RpcError as exc: 

57 raise exceptions.from_grpc_error(exc) from exc 

58 

59 return error_remapped_callable 

60 

61 

62class _StreamingResponseIterator(Generic[P], grpc.Call): 

63 def __init__(self, wrapped, prefetch_first_result=True): 

64 self._wrapped = wrapped 

65 

66 # This iterator is used in a retry context, and returned outside after init. 

67 # gRPC will not throw an exception until the stream is consumed, so we need 

68 # to retrieve the first result, in order to fail, in order to trigger a retry. 

69 try: 

70 if prefetch_first_result: 

71 self._stored_first_result = next(self._wrapped) 

72 except TypeError: 

73 # It is possible the wrapped method isn't an iterable (a grpc.Call 

74 # for instance). If this happens don't store the first result. 

75 pass 

76 except StopIteration: 

77 # ignore stop iteration at this time. This should be handled outside of retry. 

78 pass 

79 

80 def __iter__(self) -> Iterator[P]: 

81 """This iterator is also an iterable that returns itself.""" 

82 return self 

83 

84 def __next__(self) -> P: 

85 """Get the next response from the stream. 

86 

87 Returns: 

88 protobuf.Message: A single response from the stream. 

89 """ 

90 try: 

91 if hasattr(self, "_stored_first_result"): 

92 result = self._stored_first_result 

93 del self._stored_first_result 

94 return result 

95 return next(self._wrapped) 

96 except grpc.RpcError as exc: 

97 # If the stream has already returned data, we cannot recover here. 

98 raise exceptions.from_grpc_error(exc) from exc 

99 

100 # grpc.Call & grpc.RpcContext interface 

101 

102 def add_callback(self, callback): 

103 return self._wrapped.add_callback(callback) 

104 

105 def cancel(self): 

106 return self._wrapped.cancel() 

107 

108 def code(self): 

109 return self._wrapped.code() 

110 

111 def details(self): 

112 return self._wrapped.details() 

113 

114 def initial_metadata(self): 

115 return self._wrapped.initial_metadata() 

116 

117 def is_active(self): 

118 return self._wrapped.is_active() 

119 

120 def time_remaining(self): 

121 return self._wrapped.time_remaining() 

122 

123 def trailing_metadata(self): 

124 return self._wrapped.trailing_metadata() 

125 

126 

127# public type alias denoting the return type of streaming gapic calls 

128GrpcStream = _StreamingResponseIterator[P] 

129 

130 

131def _wrap_stream_errors(callable_): 

132 """Wrap errors for Unary-Stream and Stream-Stream gRPC callables. 

133 

134 The callables that return iterators require a bit more logic to re-map 

135 errors when iterating. This wraps both the initial invocation and the 

136 iterator of the return value to re-map errors. 

137 """ 

138 _patch_callable_name(callable_) 

139 

140 @functools.wraps(callable_) 

141 def error_remapped_callable(*args, **kwargs): 

142 try: 

143 result = callable_(*args, **kwargs) 

144 # Auto-fetching the first result causes PubSub client's streaming pull 

145 # to hang when re-opening the stream, thus we need examine the hacky 

146 # hidden flag to see if pre-fetching is disabled. 

147 # https://github.com/googleapis/python-pubsub/issues/93#issuecomment-630762257 

148 prefetch_first = getattr(callable_, "_prefetch_first_result_", True) 

149 return _StreamingResponseIterator( 

150 result, prefetch_first_result=prefetch_first 

151 ) 

152 except grpc.RpcError as exc: 

153 raise exceptions.from_grpc_error(exc) from exc 

154 

155 return error_remapped_callable 

156 

157 

158def wrap_errors(callable_): 

159 """Wrap a gRPC callable and map :class:`grpc.RpcErrors` to friendly error 

160 classes. 

161 

162 Errors raised by the gRPC callable are mapped to the appropriate 

163 :class:`google.api_core.exceptions.GoogleAPICallError` subclasses. 

164 The original `grpc.RpcError` (which is usually also a `grpc.Call`) is 

165 available from the ``response`` property on the mapped exception. This 

166 is useful for extracting metadata from the original error. 

167 

168 Args: 

169 callable_ (Callable): A gRPC callable. 

170 

171 Returns: 

172 Callable: The wrapped gRPC callable. 

173 """ 

174 if isinstance(callable_, _STREAM_WRAP_CLASSES): 

175 return _wrap_stream_errors(callable_) 

176 else: 

177 return _wrap_unary_errors(callable_) 

178 

179 

180def _create_composite_credentials( 

181 credentials=None, 

182 credentials_file=None, 

183 default_scopes=None, 

184 scopes=None, 

185 ssl_credentials=None, 

186 quota_project_id=None, 

187 default_host=None, 

188): 

189 """Create the composite credentials for secure channels. 

190 

191 Args: 

192 credentials (google.auth.credentials.Credentials): The credentials. If 

193 not specified, then this function will attempt to ascertain the 

194 credentials from the environment using :func:`google.auth.default`. 

195 credentials_file (str): Deprecated. A file with credentials that can be loaded with 

196 :func:`google.auth.load_credentials_from_file`. This argument is 

197 mutually exclusive with credentials. This argument will be 

198 removed in the next major version of `google-api-core`. 

199 

200 .. warning:: 

201 Important: If you accept a credential configuration (credential JSON/File/Stream) 

202 from an external source for authentication to Google Cloud Platform, you must 

203 validate it before providing it to any Google API or client library. Providing an 

204 unvalidated credential configuration to Google APIs or libraries can compromise 

205 the security of your systems and data. For more information, refer to 

206 `Validate credential configurations from external sources`_. 

207 

208 .. _Validate credential configurations from external sources: 

209 

210 https://cloud.google.com/docs/authentication/external/externally-sourced-credentials 

211 default_scopes (Sequence[str]): A optional list of scopes needed for this 

212 service. These are only used when credentials are not specified and 

213 are passed to :func:`google.auth.default`. 

214 scopes (Sequence[str]): A optional list of scopes needed for this 

215 service. These are only used when credentials are not specified and 

216 are passed to :func:`google.auth.default`. 

217 ssl_credentials (grpc.ChannelCredentials): Optional SSL channel 

218 credentials. This can be used to specify different certificates. 

219 quota_project_id (str): An optional project to use for billing and quota. 

220 default_host (str): The default endpoint. e.g., "pubsub.googleapis.com". 

221 

222 Returns: 

223 grpc.ChannelCredentials: The composed channel credentials object. 

224 

225 Raises: 

226 google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed. 

227 """ 

228 if credentials_file is not None: 

229 warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) 

230 

231 if credentials and credentials_file: 

232 raise exceptions.DuplicateCredentialArgs( 

233 "'credentials' and 'credentials_file' are mutually exclusive." 

234 ) 

235 

236 if credentials_file: 

237 credentials, _ = google.auth.load_credentials_from_file( 

238 credentials_file, scopes=scopes, default_scopes=default_scopes 

239 ) 

240 elif credentials: 

241 credentials = google.auth.credentials.with_scopes_if_required( 

242 credentials, scopes=scopes, default_scopes=default_scopes 

243 ) 

244 else: 

245 credentials, _ = google.auth.default( 

246 scopes=scopes, default_scopes=default_scopes 

247 ) 

248 

249 if quota_project_id and isinstance( 

250 credentials, google.auth.credentials.CredentialsWithQuotaProject 

251 ): 

252 credentials = credentials.with_quota_project(quota_project_id) 

253 

254 request = google.auth.transport.requests.Request() 

255 

256 # Create the metadata plugin for inserting the authorization header. 

257 try: 

258 metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( 

259 credentials, 

260 request, 

261 default_host=default_host, 

262 suppress_metrics_header=True, 

263 ) 

264 except TypeError: 

265 # Support older versions of google-auth that do not accept suppress_metrics_header 

266 metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( 

267 credentials, 

268 request, 

269 default_host=default_host, 

270 ) 

271 

272 # Create a set of grpc.CallCredentials using the metadata plugin. 

273 google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) 

274 

275 # if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of 

276 # `grpc.compute_engine_channel_credentials` as the former supports passing 

277 # `ssl_credentials` via `channel_credentials` which is needed for mTLS. 

278 if ssl_credentials: 

279 # Combine the ssl credentials and the authorization credentials. 

280 # See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials 

281 return grpc.composite_channel_credentials( 

282 ssl_credentials, google_auth_credentials 

283 ) 

284 else: 

285 # Use grpc.compute_engine_channel_credentials in order to support Direct Path. 

286 # See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials 

287 # TODO(https://github.com/googleapis/python-api-core/issues/598): 

288 # Although `grpc.compute_engine_channel_credentials` returns channel credentials 

289 # outside of a Google Compute Engine environment (GCE), we should determine if 

290 # there is a way to reliably detect a GCE environment so that 

291 # `grpc.compute_engine_channel_credentials` is not called outside of GCE. 

292 return grpc.compute_engine_channel_credentials(google_auth_credentials) 

293 

294 

295def create_channel( 

296 target, 

297 credentials=None, 

298 scopes=None, 

299 ssl_credentials=None, 

300 credentials_file=None, 

301 quota_project_id=None, 

302 default_scopes=None, 

303 default_host=None, 

304 compression=None, 

305 attempt_direct_path: Optional[bool] = False, 

306 **kwargs, 

307): 

308 """Create a secure channel with credentials. 

309 

310 Args: 

311 target (str): The target service address in the format 'hostname:port'. 

312 credentials (google.auth.credentials.Credentials): The credentials. If 

313 not specified, then this function will attempt to ascertain the 

314 credentials from the environment using :func:`google.auth.default`. 

315 scopes (Sequence[str]): A optional list of scopes needed for this 

316 service. These are only used when credentials are not specified and 

317 are passed to :func:`google.auth.default`. 

318 ssl_credentials (grpc.ChannelCredentials): Optional SSL channel 

319 credentials. This can be used to specify different certificates. 

320 credentials_file (str): A file with credentials that can be loaded with 

321 :func:`google.auth.load_credentials_from_file`. This argument is 

322 mutually exclusive with credentials. 

323 

324 .. warning:: 

325 Important: If you accept a credential configuration (credential JSON/File/Stream) 

326 from an external source for authentication to Google Cloud Platform, you must 

327 validate it before providing it to any Google API or client library. Providing an 

328 unvalidated credential configuration to Google APIs or libraries can compromise 

329 the security of your systems and data. For more information, refer to 

330 `Validate credential configurations from external sources`_. 

331 

332 .. _Validate credential configurations from external sources: 

333 

334 https://cloud.google.com/docs/authentication/external/externally-sourced-credentials 

335 quota_project_id (str): An optional project to use for billing and quota. 

336 default_scopes (Sequence[str]): Default scopes passed by a Google client 

337 library. Use 'scopes' for user-defined scopes. 

338 default_host (str): The default endpoint. e.g., "pubsub.googleapis.com". 

339 compression (grpc.Compression): An optional value indicating the 

340 compression method to be used over the lifetime of the channel. 

341 attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted 

342 when the request is made. Direct Path is only available within a Google 

343 Compute Engine (GCE) environment and provides a proxyless connection 

344 which increases the available throughput, reduces latency, and increases 

345 reliability. Note: 

346 

347 - This argument should only be set in a GCE environment and for Services 

348 that are known to support Direct Path. 

349 - If this argument is set outside of GCE, then this request will fail 

350 unless the back-end service happens to have configured fall-back to DNS. 

351 - If the request causes a `ServiceUnavailable` response, it is recommended 

352 that the client repeat the request with `attempt_direct_path` set to 

353 `False` as the Service may not support Direct Path. 

354 - Using `ssl_credentials` with `attempt_direct_path` set to `True` will 

355 result in `ValueError` as this combination is not yet supported. 

356 

357 kwargs: Additional key-word args passed to 

358 :func:`grpc.secure_channel`. 

359 

360 Returns: 

361 grpc.Channel: The created channel. 

362 

363 Raises: 

364 google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed. 

365 ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`. 

366 """ 

367 

368 # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`, 

369 # raise ValueError as this is not yet supported. 

370 # See https://github.com/googleapis/python-api-core/issues/590 

371 if ssl_credentials and attempt_direct_path: 

372 raise ValueError("Using ssl_credentials with Direct Path is not supported") 

373 

374 composite_credentials = _create_composite_credentials( 

375 credentials=credentials, 

376 credentials_file=credentials_file, 

377 default_scopes=default_scopes, 

378 scopes=scopes, 

379 ssl_credentials=ssl_credentials, 

380 quota_project_id=quota_project_id, 

381 default_host=default_host, 

382 ) 

383 

384 if attempt_direct_path: 

385 target = _modify_target_for_direct_path(target) 

386 

387 return grpc.secure_channel( 

388 target, composite_credentials, compression=compression, **kwargs 

389 ) 

390 

391 

392def _modify_target_for_direct_path(target: str) -> str: 

393 """ 

394 Given a target, return a modified version which is compatible with Direct Path. 

395 

396 Args: 

397 target (str): The target service address in the format 'hostname[:port]' or 

398 'dns://hostname[:port]'. 

399 

400 Returns: 

401 target (str): The target service address which is converted into a format compatible with Direct Path. 

402 If the target contains `dns:///` or does not contain `:///`, the target will be converted in 

403 a format compatible with Direct Path; otherwise the original target will be returned as the 

404 original target may already denote Direct Path. 

405 """ 

406 

407 # A DNS prefix may be included with the target to indicate the endpoint is living in the Internet, 

408 # outside of Google Cloud Platform. 

409 dns_prefix = "dns:///" 

410 # Remove "dns:///" if `attempt_direct_path` is set to True as 

411 # the Direct Path prefix `google-c2p:///` will be used instead. 

412 target = target.replace(dns_prefix, "") 

413 

414 direct_path_separator = ":///" 

415 if direct_path_separator not in target: 

416 target_without_port = target.split(":")[0] 

417 # Modify the target to use Direct Path by adding the `google-c2p:///` prefix 

418 target = f"google-c2p{direct_path_separator}{target_without_port}" 

419 return target 

420 

421 

422_MethodCall = collections.namedtuple( 

423 "_MethodCall", ("request", "timeout", "metadata", "credentials", "compression") 

424) 

425 

426_ChannelRequest = collections.namedtuple("_ChannelRequest", ("method", "request")) 

427 

428 

429class _CallableStub(object): 

430 """Stub for the grpc.*MultiCallable interfaces.""" 

431 

432 def __init__(self, method, channel): 

433 self._method = method 

434 self._channel = channel 

435 self.response = None 

436 """Union[protobuf.Message, Callable[protobuf.Message], exception]: 

437 The response to give when invoking this callable. If this is a 

438 callable, it will be invoked with the request protobuf. If it's an 

439 exception, the exception will be raised when this is invoked. 

440 """ 

441 self.responses = None 

442 """Iterator[ 

443 Union[protobuf.Message, Callable[protobuf.Message], exception]]: 

444 An iterator of responses. If specified, self.response will be populated 

445 on each invocation by calling ``next(self.responses)``.""" 

446 self.requests = [] 

447 """List[protobuf.Message]: All requests sent to this callable.""" 

448 self.calls = [] 

449 """List[Tuple]: All invocations of this callable. Each tuple is the 

450 request, timeout, metadata, compression, and credentials.""" 

451 

452 def __call__( 

453 self, request, timeout=None, metadata=None, credentials=None, compression=None 

454 ): 

455 self._channel.requests.append(_ChannelRequest(self._method, request)) 

456 self.calls.append( 

457 _MethodCall(request, timeout, metadata, credentials, compression) 

458 ) 

459 self.requests.append(request) 

460 

461 response = self.response 

462 if self.responses is not None: 

463 if response is None: 

464 response = next(self.responses) 

465 else: 

466 raise ValueError( 

467 "{method}.response and {method}.responses are mutually " 

468 "exclusive.".format(method=self._method) 

469 ) 

470 

471 if callable(response): 

472 return response(request) 

473 

474 if isinstance(response, Exception): 

475 raise response 

476 

477 if response is not None: 

478 return response 

479 

480 raise ValueError('Method stub for "{}" has no response.'.format(self._method)) 

481 

482 

483def _simplify_method_name(method): 

484 """Simplifies a gRPC method name. 

485 

486 When gRPC invokes the channel to create a callable, it gives a full 

487 method name like "/google.pubsub.v1.Publisher/CreateTopic". This 

488 returns just the name of the method, in this case "CreateTopic". 

489 

490 Args: 

491 method (str): The name of the method. 

492 

493 Returns: 

494 str: The simplified name of the method. 

495 """ 

496 return method.rsplit("/", 1).pop() 

497 

498 

499class ChannelStub(grpc.Channel): 

500 """A testing stub for the grpc.Channel interface. 

501 

502 This can be used to test any client that eventually uses a gRPC channel 

503 to communicate. By passing in a channel stub, you can configure which 

504 responses are returned and track which requests are made. 

505 

506 For example: 

507 

508 .. code-block:: python 

509 

510 channel_stub = grpc_helpers.ChannelStub() 

511 client = FooClient(channel=channel_stub) 

512 

513 channel_stub.GetFoo.response = foo_pb2.Foo(name='bar') 

514 

515 foo = client.get_foo(labels=['baz']) 

516 

517 assert foo.name == 'bar' 

518 assert channel_stub.GetFoo.requests[0].labels = ['baz'] 

519 

520 Each method on the stub can be accessed and configured on the channel. 

521 Here's some examples of various configurations: 

522 

523 .. code-block:: python 

524 

525 # Return a basic response: 

526 

527 channel_stub.GetFoo.response = foo_pb2.Foo(name='bar') 

528 assert client.get_foo().name == 'bar' 

529 

530 # Raise an exception: 

531 channel_stub.GetFoo.response = NotFound('...') 

532 

533 with pytest.raises(NotFound): 

534 client.get_foo() 

535 

536 # Use a sequence of responses: 

537 channel_stub.GetFoo.responses = iter([ 

538 foo_pb2.Foo(name='bar'), 

539 foo_pb2.Foo(name='baz'), 

540 ]) 

541 

542 assert client.get_foo().name == 'bar' 

543 assert client.get_foo().name == 'baz' 

544 

545 # Use a callable 

546 

547 def on_get_foo(request): 

548 return foo_pb2.Foo(name='bar' + request.id) 

549 

550 channel_stub.GetFoo.response = on_get_foo 

551 

552 assert client.get_foo(id='123').name == 'bar123' 

553 """ 

554 

555 def __init__(self, responses=[]): 

556 self.requests = [] 

557 """Sequence[Tuple[str, protobuf.Message]]: A list of all requests made 

558 on this channel in order. The tuple is of method name, request 

559 message.""" 

560 self._method_stubs = {} 

561 

562 def _stub_for_method(self, method): 

563 method = _simplify_method_name(method) 

564 self._method_stubs[method] = _CallableStub(method, self) 

565 return self._method_stubs[method] 

566 

567 def __getattr__(self, key): 

568 try: 

569 return self._method_stubs[key] 

570 except KeyError: 

571 raise AttributeError 

572 

573 def unary_unary( 

574 self, 

575 method, 

576 request_serializer=None, 

577 response_deserializer=None, 

578 _registered_method=False, 

579 ): 

580 """grpc.Channel.unary_unary implementation.""" 

581 return self._stub_for_method(method) 

582 

583 def unary_stream( 

584 self, 

585 method, 

586 request_serializer=None, 

587 response_deserializer=None, 

588 _registered_method=False, 

589 ): 

590 """grpc.Channel.unary_stream implementation.""" 

591 return self._stub_for_method(method) 

592 

593 def stream_unary( 

594 self, 

595 method, 

596 request_serializer=None, 

597 response_deserializer=None, 

598 _registered_method=False, 

599 ): 

600 """grpc.Channel.stream_unary implementation.""" 

601 return self._stub_for_method(method) 

602 

603 def stream_stream( 

604 self, 

605 method, 

606 request_serializer=None, 

607 response_deserializer=None, 

608 _registered_method=False, 

609 ): 

610 """grpc.Channel.stream_stream implementation.""" 

611 return self._stub_for_method(method) 

612 

613 def subscribe(self, callback, try_to_connect=False): 

614 """grpc.Channel.subscribe implementation.""" 

615 pass 

616 

617 def unsubscribe(self, callback): 

618 """grpc.Channel.unsubscribe implementation.""" 

619 pass 

620 

621 def close(self): 

622 """grpc.Channel.close implementation.""" 

623 pass