diff --git a/core/google/cloud/operation.py b/core/google/cloud/operation.py index b98222143b4c..388e45dcbb94 100644 --- a/core/google/cloud/operation.py +++ b/core/google/cloud/operation.py @@ -15,6 +15,7 @@ """Wrap long-running operations returned from Google Cloud APIs.""" from google.longrunning import operations_pb2 +from google.protobuf import json_format _GOOGLE_APIS_PREFIX = 'type.googleapis.com' @@ -100,8 +101,13 @@ class Operation(object): :type name: str :param name: The fully-qualified path naming the operation. - :type client: object: must provide ``_operations_stub`` accessor. + :type client: :class:`~google.cloud.client.Client` :param client: The client used to poll for the status of the operation. + If the operation was created via JSON/HTTP, the client + must own a :class:`~google.cloud.connection.Connection` + to send polling requests. If created via protobuf, the + client must have a gRPC stub in the ``_operations_stub`` + attribute. :type caller_metadata: dict :param caller_metadata: caller-assigned metadata about the operation @@ -130,6 +136,8 @@ class Operation(object): converted into the correct types. """ + _from_grpc = True + def __init__(self, name, client, **caller_metadata): self.name = name self.client = client @@ -155,6 +163,30 @@ def from_pb(cls, operation_pb, client, **caller_metadata): """ result = cls(operation_pb.name, client, **caller_metadata) result._update_state(operation_pb) + result._from_grpc = True + return result + + @classmethod + def from_dict(cls, operation, client, **caller_metadata): + """Factory: construct an instance from a dictionary. + + :type operation: dict + :param operation: Operation as a JSON object. + + :type client: :class:`~google.cloud.client.Client` + :param client: The client used to poll for the status of the operation. + + :type caller_metadata: dict + :param caller_metadata: caller-assigned metadata about the operation + + :rtype: :class:`Operation` + :returns: new instance, with attributes based on the protobuf. + """ + operation_pb = json_format.ParseDict( + operation, operations_pb2.Operation()) + result = cls(operation_pb.name, client, **caller_metadata) + result._update_state(operation_pb) + result._from_grpc = False return result @property @@ -169,12 +201,39 @@ def complete(self): def _get_operation_rpc(self): """Polls the status of the current operation. + Uses gRPC request to check. + :rtype: :class:`~google.longrunning.operations_pb2.Operation` :returns: The latest status of the current operation. """ request_pb = operations_pb2.GetOperationRequest(name=self.name) return self.client._operations_stub.GetOperation(request_pb) + def _get_operation_http(self): + """Checks the status of the current operation. + + Uses HTTP request to check. + + :rtype: :class:`~google.longrunning.operations_pb2.Operation` + :returns: The latest status of the current operation. + """ + path = 'operations/%s' % (self.name,) + api_response = self.client.connection.api_request( + method='GET', path=path) + return json_format.ParseDict( + api_response, operations_pb2.Operation()) + + def _get_operation(self): + """Checks the status of the current operation. + + :rtype: :class:`~google.longrunning.operations_pb2.Operation` + :returns: The latest status of the current operation. + """ + if self._from_grpc: + return self._get_operation_rpc() + else: + return self._get_operation_http() + def _update_state(self, operation_pb): """Update the state of the current object based on operation. @@ -205,7 +264,7 @@ def poll(self): if self.complete: raise ValueError('The operation has completed.') - operation_pb = self._get_operation_rpc() + operation_pb = self._get_operation() self._update_state(operation_pb) return self.complete diff --git a/core/unit_tests/test_operation.py b/core/unit_tests/test_operation.py index a30ec97f19ae..7c204278e6b3 100644 --- a/core/unit_tests/test_operation.py +++ b/core/unit_tests/test_operation.py @@ -103,7 +103,7 @@ def test_w_conflict(self): self.assertEqual(type_url_map, {type_url: other}) -class OperationTests(unittest.TestCase): +class TestOperation(unittest.TestCase): OPERATION_NAME = 'operations/projects/foo/instances/bar/operations/123' @@ -125,6 +125,7 @@ def test_ctor_defaults(self): self.assertIsNone(operation.error) self.assertIsNone(operation.metadata) self.assertEqual(operation.caller_metadata, {}) + self.assertTrue(operation._from_grpc) def test_ctor_explicit(self): client = _Client() @@ -138,6 +139,7 @@ def test_ctor_explicit(self): self.assertIsNone(operation.error) self.assertIsNone(operation.metadata) self.assertEqual(operation.caller_metadata, {'foo': 'bar'}) + self.assertTrue(operation._from_grpc) def test_from_pb_wo_metadata_or_kw(self): from google.longrunning import operations_pb2 @@ -202,6 +204,38 @@ def test_from_pb_w_metadata_and_kwargs(self): self.assertEqual(operation.metadata, meta) self.assertEqual(operation.caller_metadata, {'baz': 'qux'}) + def test_from_dict(self): + from google.protobuf.struct_pb2 import Struct + from google.cloud._testing import _Monkey + from google.cloud import operation as MUT + + type_url = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,) + api_response = { + 'name': self.OPERATION_NAME, + 'metadata': { + '@type': type_url, + 'value': {'foo': 'Bar'}, + }, + } + + client = _Client() + klass = self._getTargetClass() + + with _Monkey(MUT, _TYPE_URL_MAP={type_url: Struct}): + operation = klass.from_dict(api_response, client) + + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertIs(operation.client, client) + self.assertIsNone(operation.target) + self.assertIsNone(operation.response) + self.assertIsNone(operation.error) + self.assertIsInstance(operation.metadata, Struct) + self.assertEqual(len(operation.metadata.fields), 1) + self.assertEqual( + operation.metadata.fields['foo'].string_value, 'Bar') + self.assertEqual(operation.caller_metadata, {}) + self.assertFalse(operation._from_grpc) + def test_complete_property(self): client = _Client() operation = self._makeOne(self.OPERATION_NAME, client) @@ -249,6 +283,35 @@ def test_poll_true(self): self.assertIsInstance(request_pb, operations_pb2.GetOperationRequest) self.assertEqual(request_pb.name, self.OPERATION_NAME) + def test_poll_http(self): + from google.protobuf.struct_pb2 import Struct + from google.cloud._testing import _Monkey + from google.cloud import operation as MUT + + type_url = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,) + name = '2302903294023' + api_response = { + 'name': name, + 'done': True, + 'metadata': { + '@type': type_url, + 'value': {'foo': 'Bar'}, + }, + } + connection = _Connection(api_response) + client = _Client(connection) + operation = self._makeOne(name, client) + operation._from_grpc = False + + with _Monkey(MUT, _TYPE_URL_MAP={type_url: Struct}): + self.assertTrue(operation.poll()) + + expected_path = 'operations/%s' % (name,) + self.assertEqual(connection._requested, [{ + 'method': 'GET', + 'path': expected_path, + }]) + def test__update_state_done(self): from google.longrunning import operations_pb2 @@ -339,7 +402,20 @@ def GetOperation(self, request_pb): return self._get_operation_response +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + self._requested.append(kw) + response, self._responses = self._responses[0], self._responses[1:] + return response + + class _Client(object): - def __init__(self): + def __init__(self, connection=None): self._operations_stub = _OperationsStub() + self.connection = connection