From 18af90a8b51159da85f3f93bd37248a58ede4ab3 Mon Sep 17 00:00:00 2001 From: Aza Tulepbergenov Date: Tue, 24 May 2022 11:14:42 -0700 Subject: [PATCH] feat: adds LRO mixin. (#1304) Adds support for LRO to existing MixIns implementation. It extends client interface with get_operation, wait_operation, cancel_operation, delete_operation, list_operations methods (if they were defined in service yaml). --- .../%sub/services/%service/_mixins.py.j2 | 269 ++++++++ .../%sub/services/%service/client.py.j2 | 3 + .../%service/transports/_mixins.py.j2 | 104 +++ .../services/%service/transports/base.py.j2 | 60 ++ .../services/%service/transports/grpc.py.j2 | 3 + .../%name_%version/%sub/_test_mixins.py.j2 | 341 +++++++++ .../%name_%version/%sub/test_%service.py.j2 | 37 +- gapic/schema/api.py | 4 +- .../services/%service/_async_mixins.py.j2 | 271 ++++++++ .../%sub/services/%service/_mixins.py.j2 | 269 ++++++++ .../%sub/services/%service/async_client.py.j2 | 3 + .../%sub/services/%service/client.py.j2 | 4 +- .../%service/transports/_mixins.py.j2 | 104 +++ .../services/%service/transports/base.py.j2 | 60 ++ .../services/%service/transports/grpc.py.j2 | 3 + .../%service/transports/grpc_asyncio.py.j2 | 3 + .../%name_%version/%sub/_test_mixins.py.j2 | 648 ++++++++++++++++++ .../%name_%version/%sub/test_%service.py.j2 | 37 +- tests/unit/schema/test_api.py | 81 +++ 19 files changed, 2300 insertions(+), 4 deletions(-) diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_mixins.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_mixins.py.j2 index b398283458..a5ef010df5 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_mixins.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_mixins.py.j2 @@ -1,3 +1,272 @@ + {# LRO mixins #} + {% if api.has_operations_mixin %} + {% if "ListOperations" in api.mixin_api_methods %} + def list_operations( + self, + request: operations_pb2.ListOperationsRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.ListOperationsResponse: + r"""Lists operations that match the specified filter in the request. + + Args: + request (:class:`~.operations_pb2.ListOperationsRequest`): + The request object. Request message for + `ListOperations` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.ListOperationsResponse: + Response message for ``ListOperations`` method. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.ListOperationsRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.list_operations, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + def get_operation( + self, + request: operations_pb2.GetOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Gets the latest state of a long-running operation. + + Args: + request (:class:`~.operations_pb2.GetOperationRequest`): + The request object. Request message for + `GetOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.GetOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.get_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + def delete_operation( + self, + request: operations_pb2.DeleteOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Deletes a long-running operation. + + This method indicates that the client is no longer interested + in the operation result. It does not cancel the operation. + If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.DeleteOperationRequest`): + The request object. Request message for + `DeleteOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.DeleteOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.delete_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + def cancel_operation( + self, + request: operations_pb2.CancelOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Starts asynchronous cancellation on a long-running operation. + + The server makes a best effort to cancel the operation, but success + is not guaranteed. If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.CancelOperationRequest`): + The request object. Request message for + `CancelOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.CancelOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.cancel_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + def wait_operation( + self, + request: operations_pb2.WaitOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Waits until the specified long-running operation is done or reaches at most + a specified timeout, returning the latest state. + + If the operation is already done, the latest state is immediately returned. + If the timeout specified is greater than the default HTTP/RPC timeout, the HTTP/RPC + timeout is used. If the server does not support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.WaitOperationRequest`): + The request object. Request message for + `WaitOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.WaitOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.wait_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + metadata = tuple(metadata) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + {% endif %} {# LRO #} + {# IAM mixins #} {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2 index a00aa8f256..e1db165909 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2 @@ -39,6 +39,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .transports.base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO {% if 'grpc' in opts.transport %} diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/_mixins.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/_mixins.py.j2 index d2842263a4..00b0d53b71 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/_mixins.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/_mixins.py.j2 @@ -1,5 +1,109 @@ {% if "grpc" in opts.transport %} + {% if api.has_operations_mixin %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + @property + def delete_operation( + self, + ) -> Callable[[operations_pb2.DeleteOperationRequest], None]: + r"""Return a callable for the delete_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "delete_operation" not in self._stubs: + self._stubs["delete_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/DeleteOperation", + request_serializer=operations_pb2.DeleteOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["delete_operation"] + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + @property + def cancel_operation( + self, + ) -> Callable[[operations_pb2.CancelOperationRequest], None]: + r"""Return a callable for the cancel_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "cancel_operation" not in self._stubs: + self._stubs["cancel_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/CancelOperation", + request_serializer=operations_pb2.CancelOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["cancel_operation"] + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + @property + def wait_operation( + self, + ) -> Callable[[operations_pb2.WaitOperationRequest], None]: + r"""Return a callable for the wait_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "delete_operation" not in self._stubs: + self._stubs["wait_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/WaitOperation", + request_serializer=operations_pb2.WaitOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["wait_operation"] + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + @property + def get_operation( + self, + ) -> Callable[[operations_pb2.GetOperationRequest], operations_pb2.Operation]: + r"""Return a callable for the get_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "get_operation" not in self._stubs: + self._stubs["get_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/GetOperation", + request_serializer=operations_pb2.GetOperationRequest.SerializeToString, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["get_operation"] + {% endif %} + + {% if "ListOperations" in api.mixin_api_methods %} + @property + def list_operations( + self, + ) -> Callable[[operations_pb2.ListOperationsRequest], operations_pb2.ListOperationsResponse]: + r"""Return a callable for the list_operations method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "list_operations" not in self._stubs: + self._stubs["list_operations"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/ListOperations", + request_serializer=operations_pb2.ListOperationsRequest.SerializeToString, + response_deserializer=operations_pb2.ListOperationsResponse.FromString, + ) + return self._stubs["list_operations"] + {% endif %} + + {% endif %} {# LRO #} + {% if api.has_location_mixin %} {% if "ListLocations" in api.mixin_api_methods %} diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 index cb1bf8eaa0..19f6f2f93b 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 @@ -29,6 +29,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} try: @@ -172,6 +175,63 @@ class {{ service.name }}Transport(abc.ABC): raise NotImplementedError() {% endfor %} + {% if api.has_operations_mixin %} + {% if "ListOperations" in api.mixin_api_methods %} + @property + def list_operations( + self, + ) -> Callable[ + [operations_pb2.ListOperationsRequest], + Union[operations_pb2.ListOperationsResponse, Awaitable[operations_pb2.ListOperationsResponse]], + ]: + raise NotImplementedError() + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + @property + def get_operation( + self, + ) -> Callable[ + [operations_pb2.GetOperationRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + @property + def cancel_operation( + self, + ) -> Callable[ + [operations_pb2.CancelOperationRequest], + None, + ]: + raise NotImplementedError() + {% endif %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + @property + def delete_operation( + self, + ) -> Callable[ + [operations_pb2.DeleteOperationRequest], + None, + ]: + raise NotImplementedError() + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + @property + def wait_operation( + self, + ) -> Callable[ + [operations_pb2.WaitOperationRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + {% endif %} + {% endif %} + {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} {% if not opts.add_iam_methods and api.has_iam_mixin %} {% if "SetIamPolicy" in api.mixin_api_methods %} diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 index fdb71fc98b..b9d46f1cd7 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 @@ -28,6 +28,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 index a9f7aa7b9e..8175f2e0e5 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 @@ -1,3 +1,344 @@ +{% if api.has_operations_mixin and 'grpc' in opts.transport %} + +{% if "DeleteOperation" in api.mixin_api_methods %} +def test_delete_operation(transport: str = "grpc"): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.DeleteOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = None + response = client.delete_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert response is None + +def test_delete_operation_field_headers(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.DeleteOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + call.return_value = None + + client.delete_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] + +def test_delete_operation_from_dict(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = None + + response = client.delete_operation( + request={ + "name": "locations", + } + ) + call.assert_called() + +{% endif %} {# DeleteOperation #} + +{% if "CancelOperation" in api.mixin_api_methods %} +def test_cancel_operation(transport: str = "grpc"): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.CancelOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = None + response = client.cancel_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert response is None + +def test_cancel_operation_field_headers(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.CancelOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + call.return_value = None + + client.cancel_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] + +def test_cancel_operation_from_dict(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = None + + response = client.cancel_operation( + request={ + "name": "locations", + } + ) + call.assert_called() +{% endif %} {# CancelOperation #} + +{% if "WaitOperation" in api.mixin_api_methods %} + +def test_wait_operation(transport: str = "grpc"): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.WaitOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation() + response = client.wait_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.Operation) + +def test_wait_operation_field_headers(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.WaitOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + call.return_value = operations_pb2.Operation() + + client.wait_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] + +def test_wait_operation_from_dict(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation() + + response = client.wait_operation( + request={ + "name": "locations", + } + ) + call.assert_called() + +{% endif %} {# WaitOperation #} + +{% if "GetOperation" in api.mixin_api_methods %} + +def test_get_operation(transport: str = "grpc"): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.GetOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation() + response = client.get_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.Operation) + +def test_get_operation_field_headers(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.GetOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + call.return_value = operations_pb2.Operation() + + client.get_operation(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] + +def test_get_operation_from_dict(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.Operation() + + response = client.get_operation( + request={ + "name": "locations", + } + ) + + call.assert_called() + +{% endif %} {# GetOperation #} + +{% if "ListOperations" in api.mixin_api_methods %} +def test_list_operations(transport: str = "grpc"): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.ListOperationsRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.ListOperationsResponse() + response = client.list_operations(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.ListOperationsResponse) + +def test_list_operations_field_headers(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.ListOperationsRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + call.return_value = operations_pb2.ListOperationsResponse() + + client.list_operations(request) + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] + + +def test_list_operations_from_dict(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = operations_pb2.ListOperationsResponse() + + response = client.list_operations( + request={ + "name": "locations", + } + ) + + call.assert_called() + +{% endif %} {# ListOperations #} + +{% endif %} {# LRO #} + {% if api.has_location_mixin and 'grpc' in opts.transport %} {# ListLocation #} diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index dbaa5115f3..7b71403073 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -51,6 +51,9 @@ from google.protobuf import json_format {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} from google.api_core import gapic_v1 {% for method in service.methods.values() %} {% for ref_type in method.ref_types @@ -1687,15 +1690,47 @@ def test_{{ service.name|snake_case }}_base_transport(): '{{ method.transport_safe_name|snake_case }}', {% endfor %} {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} - {% if opts.add_iam_methods or api.has_iam_mixin %} + {% if opts.add_iam_methods %} 'set_iam_policy', 'get_iam_policy', 'test_iam_permissions', {% endif %} + {% if not opts.add_iam_methods and api.has_iam_mixin %} + {% if "SetIamPolicy" in api.mixin_api_methods %} + 'set_iam_policy', + {% endif %} + {% if "GetIamPolicy" in api.mixin_api_methods %} + 'get_iam_policy', + {% endif %} + {% if "TestIamPermissions" in api.mixin_api_methods %} + 'test_iam_permissions', + {% endif %} + {% endif %} {# has_iam_mixin #} {% if api.has_location_mixin %} + {% if "GetLocation" in api.mixin_api_methods %} 'get_location', + {% endif %} + {% if "ListLocations" in api.mixin_api_methods %} 'list_locations', {% endif %} + {% endif %} {# Location Mixin #} + {% if api.has_operations_mixin %} + {% if "GetOperation" in api.mixin_api_methods %} + 'get_operation', + {% endif %} + {% if "WaitOperation" in api.mixin_api_methods %} + 'wait_operation', + {% endif %} + {% if "CancelOperation" in api.mixin_api_methods %} + 'cancel_operation', + {% endif %} + {% if "DeleteOperation" in api.mixin_api_methods %} + 'delete_operation', + {% endif %} + {% if "ListOperations" in api.mixin_api_methods %} + 'list_operations', + {% endif %} + {% endif %} {# has_operations_mixin #} ) for method in methods: with pytest.raises(NotImplementedError): diff --git a/gapic/schema/api.py b/gapic/schema/api.py index afed380455..e5132f3384 100644 --- a/gapic/schema/api.py +++ b/gapic/schema/api.py @@ -524,7 +524,9 @@ def mixin_api_methods(self) -> Dict[str, MethodDescriptorProto]: if not self._has_iam_overrides and self.has_iam_mixin: methods = {**methods, ** self._get_methods_from_service(iam_policy_pb2)} - # For LRO, expose operations client instead. + if self.has_operations_mixin: + methods = {**methods, ** + self._get_methods_from_service(operations_pb2)} return methods @cached_property diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_async_mixins.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_async_mixins.py.j2 index aa2a3c561c..3259a653aa 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_async_mixins.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_async_mixins.py.j2 @@ -1,3 +1,274 @@ + {# LRO mixins #} + + {% if api.has_operations_mixin %} + + {% if "ListOperations" in api.mixin_api_methods %} + async def list_operations( + self, + request: operations_pb2.ListOperationsRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.ListOperationsResponse: + r"""Lists operations that match the specified filter in the request. + + Args: + request (:class:`~.operations_pb2.ListOperationsRequest`): + The request object. Request message for + `ListOperations` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.ListOperationsResponse: + Response message for ``ListOperations`` method. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.ListOperationsRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._client._transport.list_operations, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = await rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + async def get_operation( + self, + request: operations_pb2.GetOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Gets the latest state of a long-running operation. + + Args: + request (:class:`~.operations_pb2.GetOperationRequest`): + The request object. Request message for + `GetOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.GetOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._client._transport.get_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = await rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + async def delete_operation( + self, + request: operations_pb2.DeleteOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Deletes a long-running operation. + + This method indicates that the client is no longer interested + in the operation result. It does not cancel the operation. + If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.DeleteOperationRequest`): + The request object. Request message for + `DeleteOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.DeleteOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._client._transport.delete_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + async def cancel_operation( + self, + request: operations_pb2.CancelOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Starts asynchronous cancellation on a long-running operation. + + The server makes a best effort to cancel the operation, but success + is not guaranteed. If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.CancelOperationRequest`): + The request object. Request message for + `CancelOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.CancelOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._client._transport.cancel_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + async def wait_operation( + self, + request: operations_pb2.WaitOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Waits until the specified long-running operation is done or reaches at most + a specified timeout, returning the latest state. + + If the operation is already done, the latest state is immediately returned. + If the timeout specified is greater than the default HTTP/RPC timeout, the HTTP/RPC + timeout is used. If the server does not support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.WaitOperationRequest`): + The request object. Request message for + `WaitOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.WaitOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._client._transport.wait_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + metadata = tuple(metadata) + + # Send the request. + response = await rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + {% endif %} {# LRO #} + {# IAM mixins #} {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 index b398283458..5336f9a95d 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 @@ -1,3 +1,272 @@ + {# LRO mixins #} + {% if api.has_operations_mixin %} + {% if "ListOperations" in api.mixin_api_methods %} + def list_operations( + self, + request: operations_pb2.ListOperationsRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.ListOperationsResponse: + r"""Lists operations that match the specified filter in the request. + + Args: + request (:class:`~.operations_pb2.ListOperationsRequest`): + The request object. Request message for + `ListOperations` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.ListOperationsResponse: + Response message for ``ListOperations`` method. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.ListOperationsRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.list_operations, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + def get_operation( + self, + request: operations_pb2.GetOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Gets the latest state of a long-running operation. + + Args: + request (:class:`~.operations_pb2.GetOperationRequest`): + The request object. Request message for + `GetOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.GetOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.get_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + def delete_operation( + self, + request: operations_pb2.DeleteOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Deletes a long-running operation. + + This method indicates that the client is no longer interested + in the operation result. It does not cancel the operation. + If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.DeleteOperationRequest`): + The request object. Request message for + `DeleteOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.DeleteOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.delete_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + def cancel_operation( + self, + request: operations_pb2.CancelOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Starts asynchronous cancellation on a long-running operation. + + The server makes a best effort to cancel the operation, but success + is not guaranteed. If the server doesn't support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.CancelOperationRequest`): + The request object. Request message for + `CancelOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + None + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.CancelOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.cancel_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("name", request.name),)), + ) + + # Send the request. + rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + def wait_operation( + self, + request: operations_pb2.WaitOperationRequest = None, + *, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Waits until the specified long-running operation is done or reaches at most + a specified timeout, returning the latest state. + + If the operation is already done, the latest state is immediately returned. + If the timeout specified is greater than the default HTTP/RPC timeout, the HTTP/RPC + timeout is used. If the server does not support this method, it returns + `google.rpc.Code.UNIMPLEMENTED`. + + Args: + request (:class:`~.operations_pb2.WaitOperationRequest`): + The request object. Request message for + `WaitOperation` method. + retry (google.api_core.retry.Retry): Designation of what errors, + if any, should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + Returns: + ~.operations_pb2.Operation: + An ``Operation`` object. + """ + # Create or coerce a protobuf request object. + # The request isn't a proto-plus wrapped type, + # so it must be constructed via keyword expansion. + if isinstance(request, dict): + request = operations_pb2.WaitOperationRequest(**request) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method.wrap_method( + self._transport.wait_operation, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + metadata = tuple(metadata) + + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + {% endif %} + {% endif %} {# LRO #} + {# IAM mixins #} {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 index 6bfb20f587..df2298abb7 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 @@ -37,6 +37,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .transports.base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO from .transports.grpc_asyncio import {{ service.grpc_asyncio_transport_name }} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 89fe4e9b33..8bdef4b848 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -47,6 +47,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .transports.base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO {% if 'grpc' in opts.transport %} @@ -661,7 +664,6 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): # Done; return the response. return response {% endif %} - try: DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=pkg_resources.get_distribution( diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2 index d2842263a4..ff57376812 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2 @@ -1,5 +1,109 @@ {% if "grpc" in opts.transport %} + {% if api.has_operations_mixin %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + @property + def delete_operation( + self, + ) -> Callable[[operations_pb2.DeleteOperationRequest], None]: + r"""Return a callable for the delete_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "delete_operation" not in self._stubs: + self._stubs["delete_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/DeleteOperation", + request_serializer=operations_pb2.DeleteOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["delete_operation"] + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + @property + def cancel_operation( + self, + ) -> Callable[[operations_pb2.CancelOperationRequest], None]: + r"""Return a callable for the cancel_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "cancel_operation" not in self._stubs: + self._stubs["cancel_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/CancelOperation", + request_serializer=operations_pb2.CancelOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["cancel_operation"] + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + @property + def wait_operation( + self, + ) -> Callable[[operations_pb2.WaitOperationRequest], None]: + r"""Return a callable for the wait_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "delete_operation" not in self._stubs: + self._stubs["wait_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/WaitOperation", + request_serializer=operations_pb2.WaitOperationRequest.SerializeToString, + response_deserializer=None, + ) + return self._stubs["wait_operation"] + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + @property + def get_operation( + self, + ) -> Callable[[operations_pb2.GetOperationRequest], operations_pb2.Operation]: + r"""Return a callable for the get_operation method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "get_operation" not in self._stubs: + self._stubs["get_operation"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/GetOperation", + request_serializer=operations_pb2.GetOperationRequest.SerializeToString, + response_deserializer=operations_pb2.Operation.FromString, + ) + return self._stubs["get_operation"] + {% endif %} + + {% if "ListOperations" in api.mixin_api_methods %} + @property + def list_operations( + self, + ) -> Callable[[operations_pb2.ListOperationsRequest], operations_pb2.ListOperationsResponse]: + r"""Return a callable for the list_operations method over gRPC. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "list_operations" not in self._stubs: + self._stubs["list_operations"] = self.grpc_channel.unary_unary( + "/google.longrunning.Operations/ListOperations", + request_serializer=operations_pb2.ListOperationsRequest.SerializeToString, + response_deserializer=operations_pb2.ListOperationsResponse.FromString, + ) + return self._stubs["list_operations"] + {% endif %} + + {% endif %} {# LRO #} + {% if api.has_location_mixin %} {% if "ListLocations" in api.mixin_api_methods %} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index 99b35fd5f6..7788d2e0c7 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -29,6 +29,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} {% filter sort_lines %} {% for operations_service in api.get_extended_operations_services(service) %} @@ -181,6 +184,63 @@ class {{ service.name }}Transport(abc.ABC): raise NotImplementedError() {% endfor %} + {% if api.has_operations_mixin %} + {% if "ListOperations" in api.mixin_api_methods %} + @property + def list_operations( + self, + ) -> Callable[ + [operations_pb2.ListOperationsRequest], + Union[operations_pb2.ListOperationsResponse, Awaitable[operations_pb2.ListOperationsResponse]], + ]: + raise NotImplementedError() + {% endif %} + + {% if "GetOperation" in api.mixin_api_methods %} + @property + def get_operation( + self, + ) -> Callable[ + [operations_pb2.GetOperationRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + {% endif %} + + {% if "CancelOperation" in api.mixin_api_methods %} + @property + def cancel_operation( + self, + ) -> Callable[ + [operations_pb2.CancelOperationRequest], + None, + ]: + raise NotImplementedError() + {% endif %} + + {% if "DeleteOperation" in api.mixin_api_methods %} + @property + def delete_operation( + self, + ) -> Callable[ + [operations_pb2.DeleteOperationRequest], + None, + ]: + raise NotImplementedError() + {% endif %} + + {% if "WaitOperation" in api.mixin_api_methods %} + @property + def wait_operation( + self, + ) -> Callable[ + [operations_pb2.WaitOperationRequest], + Union[operations_pb2.Operation, Awaitable[operations_pb2.Operation]], + ]: + raise NotImplementedError() + {% endif %} + {% endif %} + {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} {% if not opts.add_iam_methods and api.has_iam_mixin %} {% if "SetIamPolicy" in api.mixin_api_methods %} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index dfcbf833a2..e2422154fd 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -28,6 +28,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index b29d68495c..eec77586ac 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -28,6 +28,9 @@ from google.iam.v1 import policy_pb2 # type: ignore {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 # type: ignore {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} {% endfilter %} from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO from .grpc import {{ service.name }}GrpcTransport diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 index 134bdfe1bc..4e7f525e55 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/_test_mixins.py.j2 @@ -1,3 +1,651 @@ +{% if api.has_operations_mixin and ('grpc' in opts.transport or 'grpc_asyncio' in opts.transport) %} + +{% if "DeleteOperation" in api.mixin_api_methods %} +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_delete_operation(transport: str = "grpc"): +{% else %} +def test_delete_operation(transport: str = "grpc"): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% endif %} + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.DeleteOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = None + response = client.delete_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + response = await client.delete_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert response is None +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_delete_operation_field_headers_async(): +{% else %} +def test_delete_operation_field_headers(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.DeleteOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + {% if mode == "" %} + call.return_value = None + + client.delete_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + await client.delete_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_delete_operation_from_dict_async(): +{% else %} +def test_delete_operation_from_dict(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.delete_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = None + + response = client.delete_operation( + request={ + "name": "locations", + } + ) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + response = await client.delete_operation( + request={ + "name": "locations", + } + ) + {% endif %} + call.assert_called() +{% endfor %} + +{% endif %} {# DeleteOperation #} + +{% if "CancelOperation" in api.mixin_api_methods %} +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_cancel_operation(transport: str = "grpc"): +{% else %} +def test_cancel_operation(transport: str = "grpc"): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% endif %} + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.CancelOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = None + response = client.cancel_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + response = await client.cancel_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert response is None +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_cancel_operation_field_headers_async(): +{% else %} +def test_cancel_operation_field_headers(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.CancelOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + {% if mode == "" %} + call.return_value = None + + client.cancel_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + await client.cancel_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_cancel_operation_from_dict_async(): +{% else %} +def test_cancel_operation_from_dict(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.cancel_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = None + + response = client.cancel_operation( + request={ + "name": "locations", + } + ) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + None + ) + response = await client.cancel_operation( + request={ + "name": "locations", + } + ) + {% endif %} + call.assert_called() +{% endfor %} +{% endif %} {# CancelOperation #} + +{% if "WaitOperation" in api.mixin_api_methods %} +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_wait_operation(transport: str = "grpc"): +{% else %} +def test_wait_operation(transport: str = "grpc"): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% endif %} + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.WaitOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + response = client.wait_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + response = await client.wait_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.Operation) +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_wait_operation_field_headers_async(): +{% else %} +def test_wait_operation_field_headers(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.WaitOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + + client.wait_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + await client.wait_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_wait_operation_from_dict_async(): +{% else %} +def test_wait_operation_from_dict(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.wait_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + + response = client.wait_operation( + request={ + "name": "locations", + } + ) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + response = await client.wait_operation( + request={ + "name": "locations", + } + ) + {% endif %} + call.assert_called() +{% endfor %} + +{% endif %} {# WaitOperation #} + +{% if "GetOperation" in api.mixin_api_methods %} +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_get_operation(transport: str = "grpc"): +{% else %} +def test_get_operation(transport: str = "grpc"): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% endif %} + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.GetOperationRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + response = client.get_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + response = await client.get_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.Operation) +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_get_operation_field_headers_async(): +{% else %} +def test_get_operation_field_headers(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.GetOperationRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + + client.get_operation(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + await client.get_operation(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_get_operation_from_dict_async(): +{% else %} +def test_get_operation_from_dict(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_operation), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.Operation() + + response = client.get_operation( + request={ + "name": "locations", + } + ) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.Operation() + ) + response = await client.get_operation( + request={ + "name": "locations", + } + ) + {% endif %} + call.assert_called() +{% endfor %} + +{% endif %} {# GetOperation #} + +{% if "ListOperations" in api.mixin_api_methods %} +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_list_operations(transport: str = "grpc"): +{% else %} +def test_list_operations(transport: str = "grpc"): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + {% endif %} + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = operations_pb2.ListOperationsRequest() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.ListOperationsResponse() + response = client.list_operations(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.ListOperationsResponse() + ) + response = await client.list_operations(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.ListOperationsResponse) +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_list_operations_field_headers_async(): +{% else %} +def test_list_operations_field_headers(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = operations_pb2.ListOperationsRequest() + request.name = "locations" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + {% if mode == "" %} + call.return_value = operations_pb2.ListOperationsResponse() + + client.list_operations(request) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.ListOperationsResponse() + ) + await client.list_operations(request) + {% endif %} + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ("x-goog-request-params", "name=locations",) in kw["metadata"] +{% endfor %} + +{% for mode in ["", "async"] %} +{% if mode == "async" %} +@pytest.mark.asyncio +async def test_list_operations_from_dict_async(): +{% else %} +def test_list_operations_from_dict(): +{% endif %} + {% if mode == "" %} + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% else %} + client = {{ service.async_client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + ) + {% endif %} + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.list_operations), "__call__") as call: + # Designate an appropriate return value for the call. + {% if mode == "" %} + call.return_value = operations_pb2.ListOperationsResponse() + + response = client.list_operations( + request={ + "name": "locations", + } + ) + {% else %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + operations_pb2.ListOperationsResponse() + ) + response = await client.list_operations( + request={ + "name": "locations", + } + ) + {% endif %} + call.assert_called() +{% endfor %} + +{% endif %} {# ListOperations #} + +{% endif %} {# LRO #} + {% if api.has_location_mixin and ('grpc' in opts.transport or 'grpc_asyncio' in opts.transport) %} {# ListLocation #} diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 6492d5b753..aeecc8a71c 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -58,6 +58,9 @@ from google.protobuf import json_format {% if api.has_location_mixin %} from google.cloud.location import locations_pb2 {% endif %} +{% if api.has_operations_mixin %} +from google.longrunning import operations_pb2 +{% endif %} from google.api_core import gapic_v1 {% for method in service.methods.values() %} {% for ref_type in method.ref_types @@ -767,15 +770,47 @@ def test_{{ service.name|snake_case }}_base_transport(): '{{ method.transport_safe_name|snake_case }}', {% endfor %} {# TODO: Remove after https://github.com/googleapis/gapic-generator-python/pull/1240 is merged. #} - {% if opts.add_iam_methods or api.has_iam_mixin %} + {% if opts.add_iam_methods %} 'set_iam_policy', 'get_iam_policy', 'test_iam_permissions', {% endif %} + {% if not opts.add_iam_methods and api.has_iam_mixin %} + {% if "SetIamPolicy" in api.mixin_api_methods %} + 'set_iam_policy', + {% endif %} + {% if "GetIamPolicy" in api.mixin_api_methods %} + 'get_iam_policy', + {% endif %} + {% if "TestIamPermissions" in api.mixin_api_methods %} + 'test_iam_permissions', + {% endif %} + {% endif %} {% if api.has_location_mixin %} + {% if "GetLocation" in api.mixin_api_methods %} 'get_location', + {% endif %} + {% if "ListLocations" in api.mixin_api_methods %} 'list_locations', {% endif %} + {% endif %} + {% if api.has_operations_mixin %} + {% if "GetOperation" in api.mixin_api_methods %} + 'get_operation', + {% endif %} + {% if "WaitOperation" in api.mixin_api_methods %} + 'wait_operation', + {% endif %} + {% if "CancelOperation" in api.mixin_api_methods %} + 'cancel_operation', + {% endif %} + {% if "DeleteOperation" in api.mixin_api_methods %} + 'delete_operation', + {% endif %} + {% if "ListOperations" in api.mixin_api_methods %} + 'list_operations', + {% endif %} + {% endif %} ) for method in methods: with pytest.raises(NotImplementedError): diff --git a/tests/unit/schema/test_api.py b/tests/unit/schema/test_api.py index 67b1a304a5..d897255068 100644 --- a/tests/unit/schema/test_api.py +++ b/tests/unit/schema/test_api.py @@ -2275,3 +2275,84 @@ def test_mixin_api_methods_iam_overrides(): }) api_schema = api.API.build(fd, 'google.example.v1', opts=opts) assert api_schema.mixin_api_methods == {} + + +def test_mixin_api_methods_lro(): + fd = ( + make_file_pb2( + name='example.proto', + package='google.example.v1', + messages=(make_message_pb2(name='ExampleRequest', fields=()), + make_message_pb2(name='ExampleResponse', fields=()), + ), + services=(descriptor_pb2.ServiceDescriptorProto( + name='FooService', + method=( + descriptor_pb2.MethodDescriptorProto( + name='FooMethod', + # Input and output types don't matter. + input_type='google.example.v1.ExampleRequest', + output_type='google.example.v1.ExampleResponse', + ), + ), + ),), + ), + ) + r1 = { + 'selector': 'google.longrunning.Operations.CancelOperation', + 'post': '/v1/{name=examples/*}/*', + 'body': '*' + } + r2 = { + 'selector': 'google.longrunning.Operations.DeleteOperation', + 'get': '/v1/{name=examples/*}/*', + 'body': '*' + } + r3 = { + 'selector': 'google.longrunning.Operations.WaitOperation', + 'post': '/v1/{name=examples/*}/*', + 'body': '*' + } + r4 = { + 'selector': 'google.longrunning.Operations.GetOperation', + 'post': '/v1/{name=examples/*}/*', + 'body': '*' + } + opts = Options(service_yaml_config={ + 'apis': [ + { + 'name': 'google.longrunning.Operations' + } + ], + 'http': { + 'rules': [r1, r2, r3, r4] + } + }) + + ms = methods_from_service(operations_pb2, 'Operations') + assert len(ms) == 5 + m1 = ms['CancelOperation'] + m1.options.ClearExtension(annotations_pb2.http) + m1.options.Extensions[annotations_pb2.http].selector = r1['selector'] + m1.options.Extensions[annotations_pb2.http].post = r1['post'] + m1.options.Extensions[annotations_pb2.http].body = r1['body'] + m2 = ms['DeleteOperation'] + m2.options.ClearExtension(annotations_pb2.http) + m2.options.Extensions[annotations_pb2.http].selector = r2['selector'] + m2.options.Extensions[annotations_pb2.http].get = r2['get'] + m2.options.Extensions[annotations_pb2.http].body = r2['body'] + m3 = ms['WaitOperation'] + m3.options.ClearExtension(annotations_pb2.http) + m3.options.Extensions[annotations_pb2.http].selector = r3['selector'] + m3.options.Extensions[annotations_pb2.http].post = r3['post'] + m3.options.Extensions[annotations_pb2.http].body = r3['body'] + m4 = ms['GetOperation'] + m4.options.ClearExtension(annotations_pb2.http) + m4.options.Extensions[annotations_pb2.http].selector = r4['selector'] + m4.options.Extensions[annotations_pb2.http].post = r4['post'] + m4.options.Extensions[annotations_pb2.http].body = r4['body'] + + api_schema = api.API.build(fd, 'google.example.v1', opts=opts) + assert api_schema.mixin_api_methods == { + 'CancelOperation': m1, 'DeleteOperation': m2, 'WaitOperation': m3, + 'GetOperation': m4}