Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
grpc_helpers.py623 linesDownload Raw Back to api_core
1# Copyright 2017 Google LLC2#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 at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""Helpers for :mod:`grpc`."""16from typing import Generic, Iterator, Optional, TypeVar17 18import collections19import functools20import warnings21 22import grpc23 24from google.api_core import exceptions25import google.auth26import google.auth.credentials27import google.auth.transport.grpc28import google.auth.transport.requests29import google.protobuf30 31PROTOBUF_VERSION = google.protobuf.__version__32 33# The grpcio-gcp package only has support for protobuf < 434if PROTOBUF_VERSION[0:2] == "3.":  # pragma: NO COVER35    try:36        import grpc_gcp37 38        warnings.warn(39            """Support for grpcio-gcp is deprecated. This feature will be40            removed from `google-api-core` after January 1, 2024. If you need to41            continue to use this feature, please pin to a specific version of42            `google-api-core`.""",43            DeprecationWarning,44        )45        HAS_GRPC_GCP = True46    except ImportError:47        HAS_GRPC_GCP = False48else:49    HAS_GRPC_GCP = False50 51 52# The list of gRPC Callable interfaces that return iterators.53_STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable)54 55# denotes the proto response type for grpc calls56P = TypeVar("P")57 58 59def _patch_callable_name(callable_):60    """Fix-up gRPC callable attributes.61 62    gRPC callable lack the ``__name__`` attribute which causes63    :func:`functools.wraps` to error. This adds the attribute if needed.64    """65    if not hasattr(callable_, "__name__"):66        callable_.__name__ = callable_.__class__.__name__67 68 69def _wrap_unary_errors(callable_):70    """Map errors for Unary-Unary and Stream-Unary gRPC callables."""71    _patch_callable_name(callable_)72 73    @functools.wraps(callable_)74    def error_remapped_callable(*args, **kwargs):75        try:76            return callable_(*args, **kwargs)77        except grpc.RpcError as exc:78            raise exceptions.from_grpc_error(exc) from exc79 80    return error_remapped_callable81 82 83class _StreamingResponseIterator(Generic[P], grpc.Call):84    def __init__(self, wrapped, prefetch_first_result=True):85        self._wrapped = wrapped86 87        # This iterator is used in a retry context, and returned outside after init.88        # gRPC will not throw an exception until the stream is consumed, so we need89        # to retrieve the first result, in order to fail, in order to trigger a retry.90        try:91            if prefetch_first_result:92                self._stored_first_result = next(self._wrapped)93        except TypeError:94            # It is possible the wrapped method isn't an iterable (a grpc.Call95            # for instance). If this happens don't store the first result.96            pass97        except StopIteration:98            # ignore stop iteration at this time. This should be handled outside of retry.99            pass100 101    def __iter__(self) -> Iterator[P]:102        """This iterator is also an iterable that returns itself."""103        return self104 105    def __next__(self) -> P:106        """Get the next response from the stream.107 108        Returns:109            protobuf.Message: A single response from the stream.110        """111        try:112            if hasattr(self, "_stored_first_result"):113                result = self._stored_first_result114                del self._stored_first_result115                return result116            return next(self._wrapped)117        except grpc.RpcError as exc:118            # If the stream has already returned data, we cannot recover here.119            raise exceptions.from_grpc_error(exc) from exc120 121    # grpc.Call & grpc.RpcContext interface122 123    def add_callback(self, callback):124        return self._wrapped.add_callback(callback)125 126    def cancel(self):127        return self._wrapped.cancel()128 129    def code(self):130        return self._wrapped.code()131 132    def details(self):133        return self._wrapped.details()134 135    def initial_metadata(self):136        return self._wrapped.initial_metadata()137 138    def is_active(self):139        return self._wrapped.is_active()140 141    def time_remaining(self):142        return self._wrapped.time_remaining()143 144    def trailing_metadata(self):145        return self._wrapped.trailing_metadata()146 147 148# public type alias denoting the return type of streaming gapic calls149GrpcStream = _StreamingResponseIterator[P]150 151 152def _wrap_stream_errors(callable_):153    """Wrap errors for Unary-Stream and Stream-Stream gRPC callables.154 155    The callables that return iterators require a bit more logic to re-map156    errors when iterating. This wraps both the initial invocation and the157    iterator of the return value to re-map errors.158    """159    _patch_callable_name(callable_)160 161    @functools.wraps(callable_)162    def error_remapped_callable(*args, **kwargs):163        try:164            result = callable_(*args, **kwargs)165            # Auto-fetching the first result causes PubSub client's streaming pull166            # to hang when re-opening the stream, thus we need examine the hacky167            # hidden flag to see if pre-fetching is disabled.168            # https://github.com/googleapis/python-pubsub/issues/93#issuecomment-630762257169            prefetch_first = getattr(callable_, "_prefetch_first_result_", True)170            return _StreamingResponseIterator(171                result, prefetch_first_result=prefetch_first172            )173        except grpc.RpcError as exc:174            raise exceptions.from_grpc_error(exc) from exc175 176    return error_remapped_callable177 178 179def wrap_errors(callable_):180    """Wrap a gRPC callable and map :class:`grpc.RpcErrors` to friendly error181    classes.182 183    Errors raised by the gRPC callable are mapped to the appropriate184    :class:`google.api_core.exceptions.GoogleAPICallError` subclasses.185    The original `grpc.RpcError` (which is usually also a `grpc.Call`) is186    available from the ``response`` property on the mapped exception. This187    is useful for extracting metadata from the original error.188 189    Args:190        callable_ (Callable): A gRPC callable.191 192    Returns:193        Callable: The wrapped gRPC callable.194    """195    if isinstance(callable_, _STREAM_WRAP_CLASSES):196        return _wrap_stream_errors(callable_)197    else:198        return _wrap_unary_errors(callable_)199 200 201def _create_composite_credentials(202    credentials=None,203    credentials_file=None,204    default_scopes=None,205    scopes=None,206    ssl_credentials=None,207    quota_project_id=None,208    default_host=None,209):210    """Create the composite credentials for secure channels.211 212    Args:213        credentials (google.auth.credentials.Credentials): The credentials. If214            not specified, then this function will attempt to ascertain the215            credentials from the environment using :func:`google.auth.default`.216        credentials_file (str): A file with credentials that can be loaded with217            :func:`google.auth.load_credentials_from_file`. This argument is218            mutually exclusive with credentials.219        default_scopes (Sequence[str]): A optional list of scopes needed for this220            service. These are only used when credentials are not specified and221            are passed to :func:`google.auth.default`.222        scopes (Sequence[str]): A optional list of scopes needed for this223            service. These are only used when credentials are not specified and224            are passed to :func:`google.auth.default`.225        ssl_credentials (grpc.ChannelCredentials): Optional SSL channel226            credentials. This can be used to specify different certificates.227        quota_project_id (str): An optional project to use for billing and quota.228        default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".229 230    Returns:231        grpc.ChannelCredentials: The composed channel credentials object.232 233    Raises:234        google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.235    """236    if credentials and credentials_file:237        raise exceptions.DuplicateCredentialArgs(238            "'credentials' and 'credentials_file' are mutually exclusive."239        )240 241    if credentials_file:242        credentials, _ = google.auth.load_credentials_from_file(243            credentials_file, scopes=scopes, default_scopes=default_scopes244        )245    elif credentials:246        credentials = google.auth.credentials.with_scopes_if_required(247            credentials, scopes=scopes, default_scopes=default_scopes248        )249    else:250        credentials, _ = google.auth.default(251            scopes=scopes, default_scopes=default_scopes252        )253 254    if quota_project_id and isinstance(255        credentials, google.auth.credentials.CredentialsWithQuotaProject256    ):257        credentials = credentials.with_quota_project(quota_project_id)258 259    request = google.auth.transport.requests.Request()260 261    # Create the metadata plugin for inserting the authorization header.262    metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(263        credentials,264        request,265        default_host=default_host,266    )267 268    # Create a set of grpc.CallCredentials using the metadata plugin.269    google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)270 271    # if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of272    # `grpc.compute_engine_channel_credentials` as the former supports passing273    # `ssl_credentials` via `channel_credentials` which is needed for mTLS.274    if ssl_credentials:275        # Combine the ssl credentials and the authorization credentials.276        # See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials277        return grpc.composite_channel_credentials(278            ssl_credentials, google_auth_credentials279        )280    else:281        # Use grpc.compute_engine_channel_credentials in order to support Direct Path.282        # See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials283        # TODO(https://github.com/googleapis/python-api-core/issues/598):284        # Although `grpc.compute_engine_channel_credentials` returns channel credentials285        # outside of a Google Compute Engine environment (GCE), we should determine if286        # there is a way to reliably detect a GCE environment so that287        # `grpc.compute_engine_channel_credentials` is not called outside of GCE.288        return grpc.compute_engine_channel_credentials(google_auth_credentials)289 290 291def create_channel(292    target,293    credentials=None,294    scopes=None,295    ssl_credentials=None,296    credentials_file=None,297    quota_project_id=None,298    default_scopes=None,299    default_host=None,300    compression=None,301    attempt_direct_path: Optional[bool] = False,302    **kwargs,303):304    """Create a secure channel with credentials.305 306    Args:307        target (str): The target service address in the format 'hostname:port'.308        credentials (google.auth.credentials.Credentials): The credentials. If309            not specified, then this function will attempt to ascertain the310            credentials from the environment using :func:`google.auth.default`.311        scopes (Sequence[str]): A optional list of scopes needed for this312            service. These are only used when credentials are not specified and313            are passed to :func:`google.auth.default`.314        ssl_credentials (grpc.ChannelCredentials): Optional SSL channel315            credentials. This can be used to specify different certificates.316        credentials_file (str): A file with credentials that can be loaded with317            :func:`google.auth.load_credentials_from_file`. This argument is318            mutually exclusive with credentials.319        quota_project_id (str): An optional project to use for billing and quota.320        default_scopes (Sequence[str]): Default scopes passed by a Google client321            library. Use 'scopes' for user-defined scopes.322        default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".323        compression (grpc.Compression): An optional value indicating the324            compression method to be used over the lifetime of the channel.325        attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted326            when the request is made. Direct Path is only available within a Google327            Compute Engine (GCE) environment and provides a proxyless connection328            which increases the available throughput, reduces latency, and increases329            reliability. Note:330 331            - This argument should only be set in a GCE environment and for Services332              that are known to support Direct Path.333            - If this argument is set outside of GCE, then this request will fail334              unless the back-end service happens to have configured fall-back to DNS.335            - If the request causes a `ServiceUnavailable` response, it is recommended336              that the client repeat the request with `attempt_direct_path` set to337              `False` as the Service may not support Direct Path.338            - Using `ssl_credentials` with `attempt_direct_path` set to `True` will339              result in `ValueError` as this combination  is not yet supported.340 341        kwargs: Additional key-word args passed to342            :func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.343            Note: `grpc_gcp` is only supported in environments with protobuf < 4.0.0.344 345    Returns:346        grpc.Channel: The created channel.347 348    Raises:349        google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.350        ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.351    """352 353    # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,354    # raise ValueError as this is not yet supported.355    # See https://github.com/googleapis/python-api-core/issues/590356    if ssl_credentials and attempt_direct_path:357        raise ValueError("Using ssl_credentials with Direct Path is not supported")358 359    composite_credentials = _create_composite_credentials(360        credentials=credentials,361        credentials_file=credentials_file,362        default_scopes=default_scopes,363        scopes=scopes,364        ssl_credentials=ssl_credentials,365        quota_project_id=quota_project_id,366        default_host=default_host,367    )368 369    # Note that grpcio-gcp is deprecated370    if HAS_GRPC_GCP:  # pragma: NO COVER371        if compression is not None and compression != grpc.Compression.NoCompression:372            warnings.warn(373                "The `compression` argument is ignored for grpc_gcp.secure_channel creation.",374                DeprecationWarning,375            )376        if attempt_direct_path:377            warnings.warn(378                """The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation.""",379                DeprecationWarning,380            )381        return grpc_gcp.secure_channel(target, composite_credentials, **kwargs)382 383    if attempt_direct_path:384        target = _modify_target_for_direct_path(target)385 386    return grpc.secure_channel(387        target, composite_credentials, compression=compression, **kwargs388    )389 390 391def _modify_target_for_direct_path(target: str) -> str:392    """393    Given a target, return a modified version which is compatible with Direct Path.394 395    Args:396        target (str): The target service address in the format 'hostname[:port]' or397            'dns://hostname[:port]'.398 399    Returns:400        target (str): The target service address which is converted into a format compatible with Direct Path.401            If the target contains `dns:///` or does not contain `:///`, the target will be converted in402            a format compatible with Direct Path; otherwise the original target will be returned as the403            original target may already denote Direct Path.404    """405 406    # A DNS prefix may be included with the target to indicate the endpoint is living in the Internet,407    # outside of Google Cloud Platform.408    dns_prefix = "dns:///"409    # Remove "dns:///" if `attempt_direct_path` is set to True as410    # the Direct Path prefix `google-c2p:///` will be used instead.411    target = target.replace(dns_prefix, "")412 413    direct_path_separator = ":///"414    if direct_path_separator not in target:415        target_without_port = target.split(":")[0]416        # Modify the target to use Direct Path by adding the `google-c2p:///` prefix417        target = f"google-c2p{direct_path_separator}{target_without_port}"418    return target419 420 421_MethodCall = collections.namedtuple(422    "_MethodCall", ("request", "timeout", "metadata", "credentials", "compression")423)424 425_ChannelRequest = collections.namedtuple("_ChannelRequest", ("method", "request"))426 427 428class _CallableStub(object):429    """Stub for the grpc.*MultiCallable interfaces."""430 431    def __init__(self, method, channel):432        self._method = method433        self._channel = channel434        self.response = None435        """Union[protobuf.Message, Callable[protobuf.Message], exception]:436        The response to give when invoking this callable. If this is a437        callable, it will be invoked with the request protobuf. If it's an438        exception, the exception will be raised when this is invoked.439        """440        self.responses = None441        """Iterator[442            Union[protobuf.Message, Callable[protobuf.Message], exception]]:443        An iterator of responses. If specified, self.response will be populated444        on each invocation by calling ``next(self.responses)``."""445        self.requests = []446        """List[protobuf.Message]: All requests sent to this callable."""447        self.calls = []448        """List[Tuple]: All invocations of this callable. Each tuple is the449        request, timeout, metadata, compression, and credentials."""450 451    def __call__(452        self, request, timeout=None, metadata=None, credentials=None, compression=None453    ):454        self._channel.requests.append(_ChannelRequest(self._method, request))455        self.calls.append(456            _MethodCall(request, timeout, metadata, credentials, compression)457        )458        self.requests.append(request)459 460        response = self.response461        if self.responses is not None:462            if response is None:463                response = next(self.responses)464            else:465                raise ValueError(466                    "{method}.response and {method}.responses are mutually "467                    "exclusive.".format(method=self._method)468                )469 470        if callable(response):471            return response(request)472 473        if isinstance(response, Exception):474            raise response475 476        if response is not None:477            return response478 479        raise ValueError('Method stub for "{}" has no response.'.format(self._method))480 481 482def _simplify_method_name(method):483    """Simplifies a gRPC method name.484 485    When gRPC invokes the channel to create a callable, it gives a full486    method name like "/google.pubsub.v1.Publisher/CreateTopic". This487    returns just the name of the method, in this case "CreateTopic".488 489    Args:490        method (str): The name of the method.491 492    Returns:493        str: The simplified name of the method.494    """495    return method.rsplit("/", 1).pop()496 497 498class ChannelStub(grpc.Channel):499    """A testing stub for the grpc.Channel interface.500 501    This can be used to test any client that eventually uses a gRPC channel502    to communicate. By passing in a channel stub, you can configure which503    responses are returned and track which requests are made.504 505    For example:506 507    .. code-block:: python508 509        channel_stub = grpc_helpers.ChannelStub()510        client = FooClient(channel=channel_stub)511 512        channel_stub.GetFoo.response = foo_pb2.Foo(name='bar')513 514        foo = client.get_foo(labels=['baz'])515 516        assert foo.name == 'bar'517        assert channel_stub.GetFoo.requests[0].labels = ['baz']518 519    Each method on the stub can be accessed and configured on the channel.520    Here's some examples of various configurations:521 522    .. code-block:: python523 524        # Return a basic response:525 526        channel_stub.GetFoo.response = foo_pb2.Foo(name='bar')527        assert client.get_foo().name == 'bar'528 529        # Raise an exception:530        channel_stub.GetFoo.response = NotFound('...')531 532        with pytest.raises(NotFound):533            client.get_foo()534 535        # Use a sequence of responses:536        channel_stub.GetFoo.responses = iter([537            foo_pb2.Foo(name='bar'),538            foo_pb2.Foo(name='baz'),539        ])540 541        assert client.get_foo().name == 'bar'542        assert client.get_foo().name == 'baz'543 544        # Use a callable545 546        def on_get_foo(request):547            return foo_pb2.Foo(name='bar' + request.id)548 549        channel_stub.GetFoo.response = on_get_foo550 551        assert client.get_foo(id='123').name == 'bar123'552    """553 554    def __init__(self, responses=[]):555        self.requests = []556        """Sequence[Tuple[str, protobuf.Message]]: A list of all requests made557        on this channel in order. The tuple is of method name, request558        message."""559        self._method_stubs = {}560 561    def _stub_for_method(self, method):562        method = _simplify_method_name(method)563        self._method_stubs[method] = _CallableStub(method, self)564        return self._method_stubs[method]565 566    def __getattr__(self, key):567        try:568            return self._method_stubs[key]569        except KeyError:570            raise AttributeError571 572    def unary_unary(573        self,574        method,575        request_serializer=None,576        response_deserializer=None,577        _registered_method=False,578    ):579        """grpc.Channel.unary_unary implementation."""580        return self._stub_for_method(method)581 582    def unary_stream(583        self,584        method,585        request_serializer=None,586        response_deserializer=None,587        _registered_method=False,588    ):589        """grpc.Channel.unary_stream implementation."""590        return self._stub_for_method(method)591 592    def stream_unary(593        self,594        method,595        request_serializer=None,596        response_deserializer=None,597        _registered_method=False,598    ):599        """grpc.Channel.stream_unary implementation."""600        return self._stub_for_method(method)601 602    def stream_stream(603        self,604        method,605        request_serializer=None,606        response_deserializer=None,607        _registered_method=False,608    ):609        """grpc.Channel.stream_stream implementation."""610        return self._stub_for_method(method)611 612    def subscribe(self, callback, try_to_connect=False):613        """grpc.Channel.subscribe implementation."""614        pass615 616    def unsubscribe(self, callback):617        """grpc.Channel.unsubscribe implementation."""618        pass619 620    def close(self):621        """grpc.Channel.close implementation."""622        pass623 
codekingpro/portable-devtools · Team Ai