diff --git a/core/google/api/core/operation.py b/core/google/api/core/operation.py index 1cc44f0b3d7b..9022e31fe4eb 100644 --- a/core/google/api/core/operation.py +++ b/core/google/api/core/operation.py @@ -187,7 +187,7 @@ def _cancel_http(api_request, operation_name): def from_http_json(operation, api_request, result_type, **kwargs): - """Create an operation future from using a HTTP/JSON client. + """Create an operation future using a HTTP/JSON client. This interacts with the long-running operations `service`_ (specific to a given API) vis `HTTP/JSON`_. @@ -243,7 +243,7 @@ def _cancel_grpc(operations_stub, operation_name): def from_grpc(operation, operations_stub, result_type, **kwargs): - """Create an operation future from using a gRPC client. + """Create an operation future using a gRPC client. This interacts with the long-running operations `service`_ (specific to a given API) via gRPC. @@ -267,3 +267,30 @@ def from_grpc(operation, operations_stub, result_type, **kwargs): cancel = functools.partial( _cancel_grpc, operations_stub, operation.name) return Operation(operation, refresh, cancel, result_type, **kwargs) + + +def from_gapic(operation, operations_client, result_type, **kwargs): + """Create an operation future from a gapic client. + + This interacts with the long-running operations `service`_ (specific + to a given API) via a gapic client. + + .. _service: https://github.com/googleapis/googleapis/blob/\ + 050400df0fdb16f63b63e9dee53819044bffc857/\ + google/longrunning/operations.proto#L38 + + Args: + operation (google.longrunning.operations_pb2.Operation): The operation. + operations_client (google.api.core.operations_v1.OperationsClient): + The operations client. + result_type (type): The protobuf result type. + kwargs: Keyword args passed into the :class:`Operation` constructor. + + Returns: + Operation: The operation future to track the given operation. + """ + refresh = functools.partial( + operations_client.get_operation, operation.name) + cancel = functools.partial( + operations_client.cancel_operation, operation.name) + return Operation(operation, refresh, cancel, result_type, **kwargs) diff --git a/core/google/api/core/operations_v1/__init__.py b/core/google/api/core/operations_v1/__init__.py new file mode 100644 index 000000000000..fdfec8f979e3 --- /dev/null +++ b/core/google/api/core/operations_v1/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Package for interacting with the google.longrunning.operations meta-API.""" + +from google.api.core.operations_v1.operations_client import OperationsClient + +__all__ = [ + 'OperationsClient' +] diff --git a/core/google/api/core/operations_v1/operations_client.py b/core/google/api/core/operations_v1/operations_client.py new file mode 100644 index 000000000000..7c6ae9174554 --- /dev/null +++ b/core/google/api/core/operations_v1/operations_client.py @@ -0,0 +1,271 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A client for the google.longrunning.operations meta-API. + +This is a client that deals with long-running operations that follow the +pattern outlined by the `Google API Style Guide`_. + +When an API method normally takes long time to complete, it can be designed to +return ``Operation`` to the client, and the client can use this interface to +receive the real response asynchronously by polling the operation resource to +receive the response. + +It is not a separate service, but rather an interface implemented by a larger +service. The protocol-level definition is available at +`google/longrunning/operations.proto`_. Typically, this will be constructed +automatically by another client class to deal with operations. + +.. _Google API Style Guide: + https://cloud.google.com/apis/design/design_pattern + s#long_running_operations +.. _google/longrunning/operations.proto: + https://github.com/googleapis/googleapis/blob/master/google/longrunning + /operations.proto +""" + +from google.api.core import gapic_v1 +from google.api.core.operations_v1 import operations_client_config +from google.longrunning import operations_pb2 + + +class OperationsClient(object): + """Client for interacting with long-running operations within a service. + + Args: + channel (grpc.Channel): The gRPC channel associated with the service + that implements the ``google.longrunning.operations`` interface. + client_config (dict): + A dictionary of call options for each method. If not specified + the default configuration is used. + """ + + def __init__(self, channel, client_config=operations_client_config.config): + # Create the gRPC client stub. + self.operations_stub = operations_pb2.OperationsStub(channel) + + # Create all wrapped methods using the interface configuration. + # The interface config contains all of the default settings for retry + # and timeout for each RPC method. + interfaces = client_config['interfaces'] + interface_config = interfaces['google.longrunning.Operations'] + method_configs = gapic_v1.config.parse_method_configs(interface_config) + + self._get_operation = gapic_v1.method.wrap_method( + self.operations_stub.GetOperation, + default_retry=method_configs['GetOperation'].retry, + default_timeout=method_configs['GetOperation'].timeout) + + self._list_operations = gapic_v1.method.wrap_method( + self.operations_stub.ListOperations, + default_retry=method_configs['ListOperations'].retry, + default_timeout=method_configs['ListOperations'].timeout) + + self._list_operations = gapic_v1.method.wrap_with_paging( + self._list_operations, + 'operations', + 'page_token', + 'next_page_token') + + self._cancel_operation = gapic_v1.method.wrap_method( + self.operations_stub.CancelOperation, + default_retry=method_configs['CancelOperation'].retry, + default_timeout=method_configs['CancelOperation'].timeout) + + self._delete_operation = gapic_v1.method.wrap_method( + self.operations_stub.DeleteOperation, + default_retry=method_configs['DeleteOperation'].retry, + default_timeout=method_configs['DeleteOperation'].timeout) + + # Service calls + def get_operation( + self, name, + retry=gapic_v1.method.DEFAULT, timeout=gapic_v1.method.DEFAULT): + """Gets the latest state of a long-running operation. + + Clients can use this method to poll the operation result at intervals + as recommended by the API service. + + Example: + >>> from google.api.core import operations_v1 + >>> api = operations_v1.OperationsClient() + >>> name = '' + >>> response = api.get_operation(name) + + Args: + name (str): The name of the operation resource. + retry (google.api.core.retry.Retry): The retry strategy to use + when invoking the RPC. If unspecified, the default retry from + the client configuration will be used. If ``None``, then this + method will not retry the RPC at all. + timeout (float): The amount of time in seconds to wait for the RPC + to complete. Note that if ``retry`` is used, this timeout + applies to each individual attempt and the overall time it + takes for this method to complete may be longer. If + unspecified, the the default timeout in the client + configuration is used. If ``None``, then the RPC method will + not time out. + + Returns: + google.longrunning.operations_pb2.Operation: The state of the + operation. + + Raises: + google.api.core.exceptions.GoogleAPICallError: If an error occurred + while invoking the RPC, the appropriate ``GoogleAPICallError`` + subclass will be raised. + """ + request = operations_pb2.GetOperationRequest(name=name) + return self._get_operation(request, retry=retry, timeout=timeout) + + def list_operations( + self, name, filter_, + retry=gapic_v1.method.DEFAULT, timeout=gapic_v1.method.DEFAULT): + """ + Lists operations that match the specified filter in the request. + + Example: + >>> from google.api.core import operations_v1 + >>> api = operations_v1.OperationsClient() + >>> name = '' + >>> + >>> # Iterate over all results + >>> for operation in api.list_operations(name): + >>> # process operation + >>> pass + >>> + >>> # Or iterate over results one page at a time + >>> iter = api.list_operations(name) + >>> for page in iter.pages: + >>> for operation in page: + >>> # process operation + >>> pass + + Args: + name (str): The name of the operation collection. + filter_ (str): The standard list filter. + retry (google.api.core.retry.Retry): The retry strategy to use + when invoking the RPC. If unspecified, the default retry from + the client configuration will be used. If ``None``, then this + method will not retry the RPC at all. + timeout (float): The amount of time in seconds to wait for the RPC + to complete. Note that if ``retry`` is used, this timeout + applies to each individual attempt and the overall time it + takes for this method to complete may be longer. If + unspecified, the the default timeout in the client + configuration is used. If ``None``, then the RPC method will + not time out. + + Returns: + google.api.core.page_iterator.Iterator: An iterator that yields + :class:`google.longrunning.operations_pb2.Operation` instances. + + Raises: + google.api.core.exceptions.MethodNotImplemented: If the server + does not support this method. Services are not required to + implement this method. + google.api.core.exceptions.GoogleAPICallError: If an error occurred + while invoking the RPC, the appropriate ``GoogleAPICallError`` + subclass will be raised. + """ + # Create the request object. + request = operations_pb2.ListOperationsRequest( + name=name, filter=filter_) + return self._list_operations(request, retry=retry, timeout=timeout) + + def cancel_operation( + self, name, + retry=gapic_v1.method.DEFAULT, timeout=gapic_v1.method.DEFAULT): + """Starts asynchronous cancellation on a long-running operation. + + The server makes a best effort to cancel the operation, but success is + not guaranteed. Clients can use :meth:`get_operation` or service- + specific methods to check whether the cancellation succeeded or whether + the operation completed despite cancellation. On successful + cancellation, the operation is not deleted; instead, it becomes an + operation with an ``Operation.error`` value with a + ``google.rpc.Status.code`` of ``1``, corresponding to + ``Code.CANCELLED``. + + Example: + >>> from google.api.core import operations_v1 + >>> api = operations_v1.OperationsClient() + >>> name = '' + >>> api.cancel_operation(name) + + Args: + name (str): The name of the operation resource to be cancelled. + retry (google.api.core.retry.Retry): The retry strategy to use + when invoking the RPC. If unspecified, the default retry from + the client configuration will be used. If ``None``, then this + method will not retry the RPC at all. + timeout (float): The amount of time in seconds to wait for the RPC + to complete. Note that if ``retry`` is used, this timeout + applies to each individual attempt and the overall time it + takes for this method to complete may be longer. If + unspecified, the the default timeout in the client + configuration is used. If ``None``, then the RPC method will + not time out. + + Raises: + google.api.core.exceptions.MethodNotImplemented: If the server + does not support this method. Services are not required to + implement this method. + google.api.core.exceptions.GoogleAPICallError: If an error occurred + while invoking the RPC, the appropriate ``GoogleAPICallError`` + subclass will be raised. + """ + # Create the request object. + request = operations_pb2.CancelOperationRequest(name=name) + self._cancel_operation(request, retry=retry, timeout=timeout) + + def delete_operation( + self, name, + retry=gapic_v1.method.DEFAULT, timeout=gapic_v1.method.DEFAULT): + """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. + + Example: + >>> from google.api.core import operations_v1 + >>> api = operations_v1.OperationsClient() + >>> name = '' + >>> api.delete_operation(name) + + Args: + name (str): The name of the operation resource to be deleted. + retry (google.api.core.retry.Retry): The retry strategy to use + when invoking the RPC. If unspecified, the default retry from + the client configuration will be used. If ``None``, then this + method will not retry the RPC at all. + timeout (float): The amount of time in seconds to wait for the RPC + to complete. Note that if ``retry`` is used, this timeout + applies to each individual attempt and the overall time it + takes for this method to complete may be longer. If + unspecified, the the default timeout in the client + configuration is used. If ``None``, then the RPC method will + not time out. + + Raises: + google.api.core.exceptions.MethodNotImplemented: If the server + does not support this method. Services are not required to + implement this method. + google.api.core.exceptions.GoogleAPICallError: If an error occurred + while invoking the RPC, the appropriate ``GoogleAPICallError`` + subclass will be raised. + """ + # Create the request object. + request = operations_pb2.DeleteOperationRequest(name=name) + self._delete_operation(request, retry=retry, timeout=timeout) diff --git a/core/google/api/core/operations_v1/operations_client_config.py b/core/google/api/core/operations_v1/operations_client_config.py new file mode 100644 index 000000000000..bd79fd5fa280 --- /dev/null +++ b/core/google/api/core/operations_v1/operations_client_config.py @@ -0,0 +1,62 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""gapic configuration for the googe.longrunning.operations client.""" + +config = { + "interfaces": { + "google.longrunning.Operations": { + "retry_codes": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 20000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 600000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "GetOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ListOperations": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CancelOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteOperation": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/core/tests/unit/api_core/operations_v1/__init__.py b/core/tests/unit/api_core/operations_v1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/core/tests/unit/api_core/operations_v1/test_operations_client.py b/core/tests/unit/api_core/operations_v1/test_operations_client.py new file mode 100644 index 000000000000..d354f6b69e23 --- /dev/null +++ b/core/tests/unit/api_core/operations_v1/test_operations_client.py @@ -0,0 +1,101 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from google.api.core import operations_v1 +from google.api.core import page_iterator +from google.longrunning import operations_pb2 + + +def make_operations_stub(channel): + return mock.Mock( + spec=[ + 'GetOperation', 'DeleteOperation', 'ListOperations', + 'CancelOperation']) + + +operations_stub_patch = mock.patch( + 'google.longrunning.operations_pb2.OperationsStub', + autospec=True, + side_effect=make_operations_stub) + + +@operations_stub_patch +def test_constructor(operations_stub): + stub = make_operations_stub(None) + operations_stub.side_effect = None + operations_stub.return_value = stub + + client = operations_v1.OperationsClient(mock.sentinel.channel) + + assert client.operations_stub == stub + operations_stub.assert_called_once_with(mock.sentinel.channel) + + +@operations_stub_patch +def test_get_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + client.operations_stub.GetOperation.return_value = mock.sentinel.operation + + response = client.get_operation('name') + + request = client.operations_stub.GetOperation.call_args[0][0] + assert isinstance(request, operations_pb2.GetOperationRequest) + assert request.name == 'name' + + assert response == mock.sentinel.operation + + +@operations_stub_patch +def test_list_operations(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + operations = [ + operations_pb2.Operation(name='1'), + operations_pb2.Operation(name='2')] + list_response = operations_pb2.ListOperationsResponse( + operations=operations) + client.operations_stub.ListOperations.return_value = list_response + + response = client.list_operations('name', 'filter') + + assert isinstance(response, page_iterator.Iterator) + assert list(response) == operations + + request = client.operations_stub.ListOperations.call_args[0][0] + assert isinstance(request, operations_pb2.ListOperationsRequest) + assert request.name == 'name' + assert request.filter == 'filter' + + +@operations_stub_patch +def test_delete_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + + client.delete_operation('name') + + request = client.operations_stub.DeleteOperation.call_args[0][0] + assert isinstance(request, operations_pb2.DeleteOperationRequest) + assert request.name == 'name' + + +@operations_stub_patch +def test_cancel_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + + client.cancel_operation('name') + + request = client.operations_stub.CancelOperation.call_args[0][0] + assert isinstance(request, operations_pb2.CancelOperationRequest) + assert request.name == 'name' diff --git a/core/tests/unit/api_core/test_operation.py b/core/tests/unit/api_core/test_operation.py index 2332c50fdf4b..b6807bffd7ac 100644 --- a/core/tests/unit/api_core/test_operation.py +++ b/core/tests/unit/api_core/test_operation.py @@ -16,6 +16,7 @@ import mock from google.api.core import operation +from google.api.core import operations_v1 from google.longrunning import operations_pb2 from google.protobuf import struct_pb2 from google.rpc import code_pb2 @@ -205,3 +206,18 @@ def test_from_grpc(): assert future._metadata_type == struct_pb2.Struct assert future.operation.name == TEST_OPERATION_NAME assert future.done + + +def test_from_gapic(): + operation_proto = make_operation_proto(done=True) + operations_client = mock.create_autospec( + operations_v1.OperationsClient, instance=True) + + future = operation.from_gapic( + operation_proto, operations_client, struct_pb2.Struct, + metadata_type=struct_pb2.Struct) + + assert future._result_type == struct_pb2.Struct + assert future._metadata_type == struct_pb2.Struct + assert future.operation.name == TEST_OPERATION_NAME + assert future.done