From 431d61fed2ccd4bb34a28f921badc4d2de6a53ac Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 22 Aug 2016 17:56:58 -0400 Subject: [PATCH 1/9] Factor out shared 'Operation' class and helpers. Intended to replace the versions currently in '.bigtable.instance' and 'bigtable.cluster'. Differences in approach from those: - No parsing / reconstruction of operation's 'name': the new class just holds the name as retrieved. - Add a 'from_pb' classmethod factory, which attempts to parse out the 'metadata' Any, IFF the 'type_url' on the Any is set, and there is a class registered for it. - Drops the complicated '__eq__'/'__ne__' as YAGNI: if needed, reimplement using just the 'name' attr. Use new 'gcloud.operations.Operation' for long-running ops. --- docs/index.rst | 1 + docs/operation-api.rst | 7 + gcloud/bigtable/cluster.py | 131 +---------- gcloud/bigtable/instance.py | 138 +---------- gcloud/bigtable/test_cluster.py | 308 ++++-------------------- gcloud/bigtable/test_instance.py | 388 ++++--------------------------- gcloud/operation.py | 136 +++++++++++ gcloud/test_operation.py | 238 +++++++++++++++++++ 8 files changed, 489 insertions(+), 858 deletions(-) create mode 100644 docs/operation-api.rst create mode 100644 gcloud/operation.py create mode 100644 gcloud/test_operation.py diff --git a/docs/index.rst b/docs/index.rst index ec1ea380c7e8..75a8ddea61f6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,6 +74,7 @@ bigtable-row bigtable-row-filters bigtable-row-data + operation-api .. toctree:: :maxdepth: 0 diff --git a/docs/operation-api.rst b/docs/operation-api.rst new file mode 100644 index 000000000000..5a09a8042c77 --- /dev/null +++ b/docs/operation-api.rst @@ -0,0 +1,7 @@ +Long-Running Operations +~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: gcloud.operation + :members: + :show-inheritance: + diff --git a/gcloud/bigtable/cluster.py b/gcloud/bigtable/cluster.py index db43be2e34c1..f3052f0bcb14 100644 --- a/gcloud/bigtable/cluster.py +++ b/gcloud/bigtable/cluster.py @@ -17,24 +17,16 @@ import re -from google.longrunning import operations_pb2 - from gcloud.bigtable._generated import ( instance_pb2 as data_v2_pb2) from gcloud.bigtable._generated import ( bigtable_instance_admin_pb2 as messages_v2_pb2) +from gcloud.operation import Operation _CLUSTER_NAME_RE = re.compile(r'^projects/(?P[^/]+)/' r'instances/(?P[^/]+)/clusters/' r'(?P[a-z][-a-z0-9]*)$') -_OPERATION_NAME_RE = re.compile(r'^operations/' - r'projects/([^/]+)/' - r'instances/([^/]+)/' - r'clusters/([a-z][-a-z0-9]*)/' - r'operations/(?P\d+)$') -_TYPE_URL_MAP = { -} DEFAULT_SERVE_NODES = 3 """Default number of nodes to use when creating a cluster.""" @@ -58,109 +50,6 @@ def _prepare_create_request(cluster): ) -def _parse_pb_any_to_native(any_val, expected_type=None): - """Convert a serialized "google.protobuf.Any" value to actual type. - - :type any_val: :class:`google.protobuf.any_pb2.Any` - :param any_val: A serialized protobuf value container. - - :type expected_type: str - :param expected_type: (Optional) The type URL we expect ``any_val`` - to have. - - :rtype: object - :returns: The de-serialized object. - :raises: :class:`ValueError ` if the - ``expected_type`` does not match the ``type_url`` on the input. - """ - if expected_type is not None and expected_type != any_val.type_url: - raise ValueError('Expected type: %s, Received: %s' % ( - expected_type, any_val.type_url)) - container_class = _TYPE_URL_MAP[any_val.type_url] - return container_class.FromString(any_val.value) - - -def _process_operation(operation_pb): - """Processes a create protobuf response. - - :type operation_pb: :class:`google.longrunning.operations_pb2.Operation` - :param operation_pb: The long-running operation response from a - Create/Update/Undelete cluster request. - - :rtype: tuple - :returns: integer ID of the operation (``operation_id``). - :raises: :class:`ValueError ` if the operation name - doesn't match the :data:`_OPERATION_NAME_RE` regex. - """ - match = _OPERATION_NAME_RE.match(operation_pb.name) - if match is None: - raise ValueError('Operation name was not in the expected ' - 'format after a cluster modification.', - operation_pb.name) - operation_id = int(match.group('operation_id')) - - return operation_id - - -class Operation(object): - """Representation of a Google API Long-Running Operation. - - In particular, these will be the result of operations on - clusters using the Cloud Bigtable API. - - :type op_type: str - :param op_type: The type of operation being performed. Expect - ``create``, ``update`` or ``undelete``. - - :type op_id: int - :param op_id: The ID of the operation. - - :type cluster: :class:`Cluster` - :param cluster: The cluster that created the operation. - """ - - def __init__(self, op_type, op_id, cluster=None): - self.op_type = op_type - self.op_id = op_id - self._cluster = cluster - self._complete = False - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - return (other.op_type == self.op_type and - other.op_id == self.op_id and - other._cluster == self._cluster and - other._complete == self._complete) - - def __ne__(self, other): - return not self.__eq__(other) - - def finished(self): - """Check if the operation has finished. - - :rtype: bool - :returns: A boolean indicating if the current operation has completed. - :raises: :class:`ValueError ` if the operation - has already completed. - """ - if self._complete: - raise ValueError('The operation has completed.') - - operation_name = ('operations/' + self._cluster.name + - '/operations/%d' % (self.op_id,)) - request_pb = operations_pb2.GetOperationRequest(name=operation_name) - # We expect a `google.longrunning.operations_pb2.Operation`. - client = self._cluster._instance._client - operation_pb = client._operations_stub.GetOperation(request_pb) - - if operation_pb.done: - self._complete = True - return True - else: - return False - - class Cluster(object): """Representation of a Google Cloud Bigtable Cluster. @@ -317,11 +206,12 @@ def create(self): """ request_pb = _prepare_create_request(self) # We expect a `google.longrunning.operations_pb2.Operation`. - operation_pb = self._instance._client._instance_stub.CreateCluster( - request_pb) + client = self._instance._client + operation_pb = client._instance_stub.CreateCluster(request_pb) - op_id = _process_operation(operation_pb) - return Operation('create', op_id, cluster=self) + operation = Operation.from_pb(operation_pb, client) + operation.target = self + return operation def update(self): """Update this cluster. @@ -346,11 +236,12 @@ def update(self): serve_nodes=self.serve_nodes, ) # Ignore expected `._generated.instance_pb2.Cluster`. - operation_pb = self._instance._client._instance_stub.UpdateCluster( - request_pb) + client = self._instance._client + operation_pb = client._instance_stub.UpdateCluster(request_pb) - op_id = _process_operation(operation_pb) - return Operation('update', op_id, cluster=self) + operation = Operation.from_pb(operation_pb, client) + operation.target = self + return operation def delete(self): """Delete this cluster. diff --git a/gcloud/bigtable/instance.py b/gcloud/bigtable/instance.py index a70b276812fd..072f08ba2336 100644 --- a/gcloud/bigtable/instance.py +++ b/gcloud/bigtable/instance.py @@ -17,9 +17,7 @@ import re -from google.longrunning import operations_pb2 - -from gcloud._helpers import _pb_timestamp_to_datetime +from gcloud.operation import Operation from gcloud.bigtable._generated import ( instance_pb2 as data_v2_pb2) from gcloud.bigtable._generated import ( @@ -34,16 +32,6 @@ _EXISTING_INSTANCE_LOCATION_ID = 'see-existing-cluster' _INSTANCE_NAME_RE = re.compile(r'^projects/(?P[^/]+)/' r'instances/(?P[a-z][-a-z0-9]*)$') -_OPERATION_NAME_RE = re.compile(r'^operations/projects/([^/]+)/' - r'instances/([a-z][-a-z0-9]*)/' - r'locations/(?P[a-z][-a-z0-9]*)/' - r'operations/(?P\d+)$') -_TYPE_URL_BASE = 'type.googleapis.com/google.bigtable.' -_ADMIN_TYPE_URL_BASE = _TYPE_URL_BASE + 'admin.v2.' -_INSTANCE_CREATE_METADATA = _ADMIN_TYPE_URL_BASE + 'CreateInstanceMetadata' -_TYPE_URL_MAP = { - _INSTANCE_CREATE_METADATA: messages_v2_pb2.CreateInstanceMetadata, -} def _prepare_create_request(instance): @@ -71,125 +59,6 @@ def _prepare_create_request(instance): return message -def _parse_pb_any_to_native(any_val, expected_type=None): - """Convert a serialized "google.protobuf.Any" value to actual type. - - :type any_val: :class:`google.protobuf.any_pb2.Any` - :param any_val: A serialized protobuf value container. - - :type expected_type: str - :param expected_type: (Optional) The type URL we expect ``any_val`` - to have. - - :rtype: object - :returns: The de-serialized object. - :raises: :class:`ValueError ` if the - ``expected_type`` does not match the ``type_url`` on the input. - """ - if expected_type is not None and expected_type != any_val.type_url: - raise ValueError('Expected type: %s, Received: %s' % ( - expected_type, any_val.type_url)) - container_class = _TYPE_URL_MAP[any_val.type_url] - return container_class.FromString(any_val.value) - - -def _process_operation(operation_pb): - """Processes a create protobuf response. - - :type operation_pb: :class:`google.longrunning.operations_pb2.Operation` - :param operation_pb: The long-running operation response from a - Create/Update/Undelete instance request. - - :rtype: (int, str, datetime) - :returns: (operation_id, location_id, operation_begin). - :raises: :class:`ValueError ` if the operation name - doesn't match the :data:`_OPERATION_NAME_RE` regex. - """ - match = _OPERATION_NAME_RE.match(operation_pb.name) - if match is None: - raise ValueError('Operation name was not in the expected ' - 'format after instance creation.', - operation_pb.name) - location_id = match.group('location_id') - operation_id = int(match.group('operation_id')) - - request_metadata = _parse_pb_any_to_native(operation_pb.metadata) - operation_begin = _pb_timestamp_to_datetime( - request_metadata.request_time) - - return operation_id, location_id, operation_begin - - -class Operation(object): - """Representation of a Google API Long-Running Operation. - - In particular, these will be the result of operations on - instances using the Cloud Bigtable API. - - :type op_type: str - :param op_type: The type of operation being performed. Expect - ``create``, ``update`` or ``undelete``. - - :type op_id: int - :param op_id: The ID of the operation. - - :type begin: :class:`datetime.datetime` - :param begin: The time when the operation was started. - - :type location_id: str - :param location_id: ID of the location in which the operation is running - - :type instance: :class:`Instance` - :param instance: The instance that created the operation. - """ - - def __init__(self, op_type, op_id, begin, location_id, instance=None): - self.op_type = op_type - self.op_id = op_id - self.begin = begin - self.location_id = location_id - self._instance = instance - self._complete = False - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - return (other.op_type == self.op_type and - other.op_id == self.op_id and - other.begin == self.begin and - other.location_id == self.location_id and - other._instance == self._instance and - other._complete == self._complete) - - def __ne__(self, other): - return not self.__eq__(other) - - def finished(self): - """Check if the operation has finished. - - :rtype: bool - :returns: A boolean indicating if the current operation has completed. - :raises: :class:`ValueError ` if the operation - has already completed. - """ - if self._complete: - raise ValueError('The operation has completed.') - - operation_name = ( - 'operations/%s/locations/%s/operations/%d' % - (self._instance.name, self.location_id, self.op_id)) - request_pb = operations_pb2.GetOperationRequest(name=operation_name) - # We expect a `google.longrunning.operations_pb2.Operation`. - operation_pb = self._instance._client._operations_stub.GetOperation( - request_pb) - - if operation_pb.done: - self._complete = True - return True - else: - return False - - class Instance(object): """Representation of a Google Cloud Bigtable Instance. @@ -359,8 +228,9 @@ def create(self): # We expect a `google.longrunning.operations_pb2.Operation`. operation_pb = self._client._instance_stub.CreateInstance(request_pb) - op_id, loc_id, op_begin = _process_operation(operation_pb) - return Operation('create', op_id, op_begin, loc_id, instance=self) + operation = Operation.from_pb(operation_pb, self._client) + operation.target = self + return operation def update(self): """Update this instance. diff --git a/gcloud/bigtable/test_cluster.py b/gcloud/bigtable/test_cluster.py index 0569f1ea046a..c847ec0a28e4 100644 --- a/gcloud/bigtable/test_cluster.py +++ b/gcloud/bigtable/test_cluster.py @@ -16,119 +16,6 @@ import unittest -class TestOperation(unittest.TestCase): - - def _getTargetClass(self): - from gcloud.bigtable.cluster import Operation - return Operation - - def _makeOne(self, *args, **kwargs): - return self._getTargetClass()(*args, **kwargs) - - def _constructor_test_helper(self, cluster=None): - op_type = 'fake-op' - op_id = 8915 - operation = self._makeOne(op_type, op_id, cluster=cluster) - - self.assertEqual(operation.op_type, op_type) - self.assertEqual(operation.op_id, op_id) - self.assertEqual(operation._cluster, cluster) - self.assertFalse(operation._complete) - - def test_constructor_defaults(self): - self._constructor_test_helper() - - def test_constructor_explicit_cluster(self): - cluster = object() - self._constructor_test_helper(cluster=cluster) - - def test___eq__(self): - op_type = 'fake-op' - op_id = 8915 - cluster = object() - operation1 = self._makeOne(op_type, op_id, cluster=cluster) - operation2 = self._makeOne(op_type, op_id, cluster=cluster) - self.assertEqual(operation1, operation2) - - def test___eq__type_differ(self): - operation1 = self._makeOne('foo', 123, None) - operation2 = object() - self.assertNotEqual(operation1, operation2) - - def test___ne__same_value(self): - op_type = 'fake-op' - op_id = 8915 - cluster = object() - operation1 = self._makeOne(op_type, op_id, cluster=cluster) - operation2 = self._makeOne(op_type, op_id, cluster=cluster) - comparison_val = (operation1 != operation2) - self.assertFalse(comparison_val) - - def test___ne__(self): - operation1 = self._makeOne('foo', 123, None) - operation2 = self._makeOne('bar', 456, None) - self.assertNotEqual(operation1, operation2) - - def test_finished_without_operation(self): - operation = self._makeOne(None, None, None) - operation._complete = True - with self.assertRaises(ValueError): - operation.finished() - - def _finished_helper(self, done): - from google.longrunning import operations_pb2 - from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable.cluster import Cluster - - PROJECT = 'PROJECT' - INSTANCE_ID = 'instance-id' - CLUSTER_ID = 'cluster-id' - OP_TYPE = 'fake-op' - OP_ID = 789 - - client = _Client(PROJECT) - instance = _Instance(INSTANCE_ID, client) - cluster = Cluster(CLUSTER_ID, instance) - operation = self._makeOne(OP_TYPE, OP_ID, cluster=cluster) - - # Create request_pb - op_name = ('operations/projects/' + PROJECT + - '/instances/' + INSTANCE_ID + - '/clusters/' + CLUSTER_ID + - '/operations/%d' % (OP_ID,)) - request_pb = operations_pb2.GetOperationRequest(name=op_name) - - # Create response_pb - response_pb = operations_pb2.Operation(done=done) - - # Patch the stub used by the API method. - client._operations_stub = stub = _FakeStub(response_pb) - - # Create expected_result. - expected_result = done - - # Perform the method and check the result. - result = operation.finished() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'GetOperation', - (request_pb,), - {}, - )]) - - if done: - self.assertTrue(operation._complete) - else: - self.assertFalse(operation._complete) - - def test_finished(self): - self._finished_helper(done=True) - - def test_finished_not_done(self): - self._finished_helper(done=False) - - class TestCluster(unittest.TestCase): PROJECT = 'project' @@ -342,17 +229,16 @@ def test_reload(self): def test_create(self): from google.longrunning import operations_pb2 - from gcloud._testing import _Monkey + from gcloud.operation import Operation + from gcloud.bigtable._generated import ( + bigtable_instance_admin_pb2 as messages_v2_pb2) from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable import cluster as MUT + SERVE_NODES = 4 client = _Client(self.PROJECT) instance = _Instance(self.INSTANCE_ID, client) - cluster = self._makeOne(self.CLUSTER_ID, instance) - - # Create request_pb. Just a mock since we monkey patch - # _prepare_create_request - request_pb = object() + cluster = self._makeOne( + self.CLUSTER_ID, instance, serve_nodes=SERVE_NODES) # Create response_pb OP_ID = 5678 @@ -364,41 +250,31 @@ def test_create(self): # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) - # Create expected_result. - expected_result = MUT.Operation('create', OP_ID, cluster=cluster) - - # Create the mocks. - prep_create_called = [] - - def mock_prep_create_req(cluster): - prep_create_called.append(cluster) - return request_pb - - process_operation_called = [] - - def mock_process_operation(operation_pb): - process_operation_called.append(operation_pb) - return OP_ID - # Perform the method and check the result. - with _Monkey(MUT, _prepare_create_request=mock_prep_create_req, - _process_operation=mock_process_operation): - result = cluster.create() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'CreateCluster', - (request_pb,), - {}, - )]) - self.assertEqual(prep_create_called, [cluster]) - self.assertEqual(process_operation_called, [response_pb]) + result = cluster.create() + + self.assertTrue(isinstance(result, Operation)) + self.assertEqual(result.name, OP_NAME) + self.assertTrue(result.target is cluster) + self.assertTrue(result.client is client) + + self.assertEqual(len(stub.method_calls), 1) + api_name, args, kwargs = stub.method_calls[0] + self.assertEqual(api_name, 'CreateCluster') + request_pb, = args + self.assertTrue( + isinstance(request_pb, messages_v2_pb2.CreateClusterRequest)) + self.assertEqual(request_pb.parent, instance.name) + self.assertEqual(request_pb.cluster_id, self.CLUSTER_ID) + self.assertEqual(request_pb.cluster.serve_nodes, SERVE_NODES) + self.assertEqual(kwargs, {}) def test_update(self): from google.longrunning import operations_pb2 - from gcloud._testing import _Monkey + from gcloud.operation import Operation + from gcloud.bigtable._generated import ( + instance_pb2 as data_v2_pb2) from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable import cluster as MUT SERVE_NODES = 81 @@ -414,33 +290,31 @@ def test_update(self): ) # Create response_pb - response_pb = operations_pb2.Operation() + OP_ID = 5678 + OP_NAME = ( + 'operations/projects/%s/instances/%s/clusters/%s/operations/%d' % + (self.PROJECT, self.INSTANCE_ID, self.CLUSTER_ID, OP_ID)) + response_pb = operations_pb2.Operation(name=OP_NAME) # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) - # Create expected_result. - OP_ID = 5678 - expected_result = MUT.Operation('update', OP_ID, cluster=cluster) + result = cluster.update() - # Create mocks - process_operation_called = [] + self.assertTrue(isinstance(result, Operation)) + self.assertEqual(result.name, OP_NAME) + self.assertTrue(result.target is cluster) + self.assertTrue(result.client is client) - def mock_process_operation(operation_pb): - process_operation_called.append(operation_pb) - return OP_ID - - # Perform the method and check the result. - with _Monkey(MUT, _process_operation=mock_process_operation): - result = cluster.update() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'UpdateCluster', - (request_pb,), - {}, - )]) - self.assertEqual(process_operation_called, [response_pb]) + self.assertEqual(len(stub.method_calls), 1) + api_name, args, kwargs = stub.method_calls[0] + self.assertEqual(api_name, 'UpdateCluster') + request_pb, = args + self.assertTrue( + isinstance(request_pb, data_v2_pb2.Cluster)) + self.assertEqual(request_pb.name, self.CLUSTER_NAME) + self.assertEqual(request_pb.serve_nodes, SERVE_NODES) + self.assertEqual(kwargs, {}) def test_delete(self): from google.protobuf import empty_pb2 @@ -499,98 +373,6 @@ def test_it(self): self.assertEqual(request_pb.cluster.serve_nodes, SERVE_NODES) -class Test__parse_pb_any_to_native(unittest.TestCase): - - def _callFUT(self, any_val, expected_type=None): - from gcloud.bigtable.cluster import _parse_pb_any_to_native - return _parse_pb_any_to_native(any_val, expected_type=expected_type) - - def test_with_known_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable import cluster as MUT - - cell = _CellPB( - timestamp_micros=0, - value=b'foobar', - ) - - type_url = 'type.googleapis.com/' + cell.DESCRIPTOR.full_name - fake_type_url_map = {type_url: cell.__class__} - - any_val = any_pb2.Any( - type_url=type_url, - value=cell.SerializeToString(), - ) - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - result = self._callFUT(any_val) - - self.assertEqual(result, cell) - - def test_unknown_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable import cluster as MUT - - fake_type_url_map = {} - any_val = any_pb2.Any() - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - with self.assertRaises(KeyError): - self._callFUT(any_val) - - def test_disagreeing_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable import cluster as MUT - - type_url1 = 'foo' - type_url2 = 'bar' - fake_type_url_map = {type_url1: None} - any_val = any_pb2.Any(type_url=type_url2) - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - with self.assertRaises(ValueError): - self._callFUT(any_val, expected_type=type_url1) - - -class Test__process_operation(unittest.TestCase): - - def _callFUT(self, operation_pb): - from gcloud.bigtable.cluster import _process_operation - return _process_operation(operation_pb) - - def test_it(self): - from google.longrunning import operations_pb2 - - PROJECT = 'project' - INSTANCE_ID = 'instance-id' - CLUSTER_ID = 'cluster-id' - EXPECTED_OPERATION_ID = 234 - OPERATION_NAME = ( - 'operations/projects/%s/instances/%s/clusters/%s/operations/%d' % - (PROJECT, INSTANCE_ID, CLUSTER_ID, EXPECTED_OPERATION_ID)) - - operation_pb = operations_pb2.Operation(name=OPERATION_NAME) - - # Exectute method with mocks in place. - operation_id = self._callFUT(operation_pb) - - # Check outputs. - self.assertEqual(operation_id, EXPECTED_OPERATION_ID) - - def test_op_name_parsing_failure(self): - from google.longrunning import operations_pb2 - - operation_pb = operations_pb2.Operation(name='invalid') - with self.assertRaises(ValueError): - self._callFUT(operation_pb) - - -def _CellPB(*args, **kw): - from gcloud.bigtable._generated import ( - data_pb2 as data_v2_pb2) - return data_v2_pb2.Cell(*args, **kw) - - def _ClusterPB(*args, **kw): from gcloud.bigtable._generated import ( instance_pb2 as instance_v2_pb2) diff --git a/gcloud/bigtable/test_instance.py b/gcloud/bigtable/test_instance.py index fd44dfcb4335..2e61879ffdd2 100644 --- a/gcloud/bigtable/test_instance.py +++ b/gcloud/bigtable/test_instance.py @@ -13,132 +13,9 @@ # limitations under the License. -import datetime import unittest -class TestOperation(unittest.TestCase): - - OP_TYPE = 'fake-op' - OP_ID = 8915 - BEGIN = datetime.datetime(2015, 10, 22, 1, 1) - LOCATION_ID = 'loc-id' - - def _getTargetClass(self): - from gcloud.bigtable.instance import Operation - return Operation - - def _makeOne(self, *args, **kwargs): - return self._getTargetClass()(*args, **kwargs) - - def _constructor_test_helper(self, instance=None): - operation = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - - self.assertEqual(operation.op_type, self.OP_TYPE) - self.assertEqual(operation.op_id, self.OP_ID) - self.assertEqual(operation.begin, self.BEGIN) - self.assertEqual(operation.location_id, self.LOCATION_ID) - self.assertEqual(operation._instance, instance) - self.assertFalse(operation._complete) - - def test_constructor_defaults(self): - self._constructor_test_helper() - - def test_constructor_explicit_instance(self): - instance = object() - self._constructor_test_helper(instance=instance) - - def test___eq__(self): - instance = object() - operation1 = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - operation2 = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - self.assertEqual(operation1, operation2) - - def test___eq__type_differ(self): - operation1 = self._makeOne('foo', 123, None, self.LOCATION_ID) - operation2 = object() - self.assertNotEqual(operation1, operation2) - - def test___ne__same_value(self): - instance = object() - operation1 = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - operation2 = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - comparison_val = (operation1 != operation2) - self.assertFalse(comparison_val) - - def test___ne__(self): - operation1 = self._makeOne('foo', 123, None, self.LOCATION_ID) - operation2 = self._makeOne('bar', 456, None, self.LOCATION_ID) - self.assertNotEqual(operation1, operation2) - - def test_finished_without_operation(self): - operation = self._makeOne(None, None, None, None) - operation._complete = True - with self.assertRaises(ValueError): - operation.finished() - - def _finished_helper(self, done): - from google.longrunning import operations_pb2 - from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable.instance import Instance - - PROJECT = 'PROJECT' - INSTANCE_ID = 'instance-id' - - client = _Client(PROJECT) - instance = Instance(INSTANCE_ID, client, self.LOCATION_ID) - operation = self._makeOne( - self.OP_TYPE, self.OP_ID, self.BEGIN, self.LOCATION_ID, - instance=instance) - - # Create request_pb - op_name = ('operations/projects/' + PROJECT + - '/instances/' + INSTANCE_ID + - '/locations/' + self.LOCATION_ID + - '/operations/%d' % (self.OP_ID,)) - request_pb = operations_pb2.GetOperationRequest(name=op_name) - - # Create response_pb - response_pb = operations_pb2.Operation(done=done) - - # Patch the stub used by the API method. - client._operations_stub = stub = _FakeStub(response_pb) - - # Create expected_result. - expected_result = done - - # Perform the method and check the result. - result = operation.finished() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'GetOperation', - (request_pb,), - {}, - )]) - - if done: - self.assertTrue(operation._complete) - else: - self.assertFalse(operation._complete) - - def test_finished(self): - self._finished_helper(done=True) - - def test_finished_not_done(self): - self._finished_helper(done=False) - - class TestInstance(unittest.TestCase): PROJECT = 'project' @@ -350,61 +227,49 @@ def test_reload(self): def test_create(self): from google.longrunning import operations_pb2 - from gcloud._testing import _Monkey + from gcloud.bigtable._generated import ( + bigtable_instance_admin_pb2 as messages_v2_pb2) from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable import instance as MUT + from gcloud.operation import Operation + from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES client = _Client(self.PROJECT) - instance = self._makeOne(self.INSTANCE_ID, client, self.LOCATION_ID) - - # Create request_pb. Just a mock since we monkey patch - # _prepare_create_request - request_pb = object() + instance = self._makeOne(self.INSTANCE_ID, client, self.LOCATION_ID, + display_name=self.DISPLAY_NAME) # Create response_pb - OP_BEGIN = object() response_pb = operations_pb2.Operation(name=self.OP_NAME) # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) - # Create expected_result. - expected_result = MUT.Operation('create', self.OP_ID, OP_BEGIN, - self.LOCATION_ID, instance=instance) - - # Create the mocks. - prep_create_called = [] - - def mock_prep_create_req(instance): - prep_create_called.append(instance) - return request_pb - - process_operation_called = [] - - def mock_process_operation(operation_pb): - process_operation_called.append(operation_pb) - return self.OP_ID, self.LOCATION_ID, OP_BEGIN - # Perform the method and check the result. - with _Monkey(MUT, - _prepare_create_request=mock_prep_create_req, - _process_operation=mock_process_operation): - result = instance.create() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'CreateInstance', - (request_pb,), - {}, - )]) - self.assertEqual(prep_create_called, [instance]) - self.assertEqual(process_operation_called, [response_pb]) + result = instance.create() + + self.assertTrue(isinstance(result, Operation)) + self.assertEqual(result.name, self.OP_NAME) + self.assertTrue(result.target is instance) + self.assertTrue(result.client is client) + + self.assertEqual(len(stub.method_calls), 1) + api_name, args, kwargs = stub.method_calls[0] + self.assertEqual(api_name, 'CreateInstance') + request_pb, = args + self.assertTrue( + isinstance(request_pb, messages_v2_pb2.CreateInstanceRequest)) + self.assertEqual(request_pb.parent, 'projects/%s' % (self.PROJECT,)) + self.assertEqual(request_pb.instance_id, self.INSTANCE_ID) + self.assertEqual(request_pb.instance.display_name, self.DISPLAY_NAME) + cluster = request_pb.clusters[self.INSTANCE_ID] + self.assertEqual(cluster.serve_nodes, DEFAULT_SERVE_NODES) + self.assertEqual(kwargs, {}) def test_create_w_explicit_serve_nodes(self): from google.longrunning import operations_pb2 - from gcloud._testing import _Monkey + from gcloud.bigtable._generated import ( + bigtable_instance_admin_pb2 as messages_v2_pb2) from gcloud.bigtable._testing import _FakeStub - from gcloud.bigtable import instance as MUT + from gcloud.operation import Operation SERVE_NODES = 5 @@ -412,48 +277,32 @@ def test_create_w_explicit_serve_nodes(self): instance = self._makeOne(self.INSTANCE_ID, client, self.LOCATION_ID, serve_nodes=SERVE_NODES) - # Create request_pb. Just a mock since we monkey patch - # _prepare_create_request - request_pb = object() - # Create response_pb - OP_BEGIN = object() response_pb = operations_pb2.Operation(name=self.OP_NAME) # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) - # Create expected_result. - expected_result = MUT.Operation('create', self.OP_ID, OP_BEGIN, - self.LOCATION_ID, instance=instance) - - # Create the mocks. - prep_create_called = [] - - def mock_prep_create_req(instance): - prep_create_called.append(instance) - return request_pb - - process_operation_called = [] - - def mock_process_operation(operation_pb): - process_operation_called.append(operation_pb) - return self.OP_ID, self.LOCATION_ID, OP_BEGIN - # Perform the method and check the result. - with _Monkey(MUT, - _prepare_create_request=mock_prep_create_req, - _process_operation=mock_process_operation): - result = instance.create() - - self.assertEqual(result, expected_result) - self.assertEqual(stub.method_calls, [( - 'CreateInstance', - (request_pb,), - {}, - )]) - self.assertEqual(prep_create_called, [instance]) - self.assertEqual(process_operation_called, [response_pb]) + result = instance.create() + + self.assertTrue(isinstance(result, Operation)) + self.assertEqual(result.name, self.OP_NAME) + self.assertTrue(result.target is instance) + self.assertTrue(result.client is client) + + self.assertEqual(len(stub.method_calls), 1) + api_name, args, kwargs = stub.method_calls[0] + self.assertEqual(api_name, 'CreateInstance') + request_pb, = args + self.assertTrue( + isinstance(request_pb, messages_v2_pb2.CreateInstanceRequest)) + self.assertEqual(request_pb.parent, 'projects/%s' % (self.PROJECT,)) + self.assertEqual(request_pb.instance_id, self.INSTANCE_ID) + self.assertEqual(request_pb.instance.display_name, self.INSTANCE_ID) + cluster = request_pb.clusters[self.INSTANCE_ID] + self.assertEqual(cluster.serve_nodes, SERVE_NODES) + self.assertEqual(kwargs, {}) def test_update(self): from gcloud.bigtable._generated import ( @@ -704,149 +553,6 @@ def test_w_explicit_serve_nodes(self): self.assertEqual(cluster.serve_nodes, SERVE_NODES) -class Test__parse_pb_any_to_native(unittest.TestCase): - - def _callFUT(self, any_val, expected_type=None): - from gcloud.bigtable.instance import _parse_pb_any_to_native - return _parse_pb_any_to_native(any_val, expected_type=expected_type) - - def test_with_known_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable._generated import ( - data_pb2 as data_v2_pb2) - from gcloud.bigtable import instance as MUT - - TYPE_URL = 'type.googleapis.com/' + data_v2_pb2._CELL.full_name - fake_type_url_map = {TYPE_URL: data_v2_pb2.Cell} - - cell = data_v2_pb2.Cell( - timestamp_micros=0, - value=b'foobar', - ) - any_val = any_pb2.Any( - type_url=TYPE_URL, - value=cell.SerializeToString(), - ) - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - result = self._callFUT(any_val) - - self.assertEqual(result, cell) - - def test_with_create_instance_metadata(self): - from google.protobuf import any_pb2 - from google.protobuf.timestamp_pb2 import Timestamp - from gcloud.bigtable._generated import ( - instance_pb2 as data_v2_pb2) - from gcloud.bigtable._generated import ( - bigtable_instance_admin_pb2 as messages_v2_pb) - - TYPE_URL = ('type.googleapis.com/' + - messages_v2_pb._CREATEINSTANCEMETADATA.full_name) - metadata = messages_v2_pb.CreateInstanceMetadata( - request_time=Timestamp(seconds=1, nanos=1234), - finish_time=Timestamp(seconds=10, nanos=891011), - original_request=messages_v2_pb.CreateInstanceRequest( - parent='foo', - instance_id='bar', - instance=data_v2_pb2.Instance( - display_name='quux', - ), - ), - ) - - any_val = any_pb2.Any( - type_url=TYPE_URL, - value=metadata.SerializeToString(), - ) - result = self._callFUT(any_val) - self.assertEqual(result, metadata) - - def test_unknown_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable import instance as MUT - - fake_type_url_map = {} - any_val = any_pb2.Any() - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - with self.assertRaises(KeyError): - self._callFUT(any_val) - - def test_disagreeing_type_url(self): - from google.protobuf import any_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable import instance as MUT - - TYPE_URL1 = 'foo' - TYPE_URL2 = 'bar' - fake_type_url_map = {TYPE_URL1: None} - any_val = any_pb2.Any(type_url=TYPE_URL2) - with _Monkey(MUT, _TYPE_URL_MAP=fake_type_url_map): - with self.assertRaises(ValueError): - self._callFUT(any_val, expected_type=TYPE_URL1) - - -class Test__process_operation(unittest.TestCase): - - def _callFUT(self, operation_pb): - from gcloud.bigtable.instance import _process_operation - return _process_operation(operation_pb) - - def test_it(self): - from google.longrunning import operations_pb2 - from gcloud._testing import _Monkey - from gcloud.bigtable._generated import ( - bigtable_instance_admin_pb2 as messages_v2_pb) - from gcloud.bigtable import instance as MUT - - PROJECT = 'PROJECT' - INSTANCE_ID = 'instance-id' - LOCATION_ID = 'location' - OP_ID = 234 - OPERATION_NAME = ( - 'operations/projects/%s/instances/%s/locations/%s/operations/%d' % - (PROJECT, INSTANCE_ID, LOCATION_ID, OP_ID)) - - current_op = operations_pb2.Operation(name=OPERATION_NAME) - - # Create mocks. - request_metadata = messages_v2_pb.CreateInstanceMetadata() - parse_pb_any_called = [] - - def mock_parse_pb_any_to_native(any_val, expected_type=None): - parse_pb_any_called.append((any_val, expected_type)) - return request_metadata - - expected_operation_begin = object() - ts_to_dt_called = [] - - def mock_pb_timestamp_to_datetime(timestamp): - ts_to_dt_called.append(timestamp) - return expected_operation_begin - - # Exectute method with mocks in place. - with _Monkey(MUT, _parse_pb_any_to_native=mock_parse_pb_any_to_native, - _pb_timestamp_to_datetime=mock_pb_timestamp_to_datetime): - op_id, loc_id, op_begin = self._callFUT(current_op) - - # Check outputs. - self.assertEqual(op_id, OP_ID) - self.assertTrue(op_begin is expected_operation_begin) - self.assertEqual(loc_id, LOCATION_ID) - - # Check mocks were used correctly. - self.assertEqual(parse_pb_any_called, [(current_op.metadata, None)]) - self.assertEqual(ts_to_dt_called, [request_metadata.request_time]) - - def test_op_name_parsing_failure(self): - from google.longrunning import operations_pb2 - - operation_pb = operations_pb2.Operation(name='invalid') - with self.assertRaises(ValueError): - self._callFUT(operation_pb) - - class _Client(object): def __init__(self, project): diff --git a/gcloud/operation.py b/gcloud/operation.py new file mode 100644 index 000000000000..ff58e984fa55 --- /dev/null +++ b/gcloud/operation.py @@ -0,0 +1,136 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# 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. + +"""Wrap long-running operations returned from Google Cloud APIs.""" + +from google.longrunning import operations_pb2 + + +_GOOGLE_APIS_PREFIX = 'types.googleapis.com' + +_TYPE_URL_MAP = { +} + + +def _compute_type_url(klass, prefix=_GOOGLE_APIS_PREFIX): + """Compute a type URL for a klass. + + :type klass: type + :param klass: class to be used as a factory for the given type + + :type prefix: str + :param prefix: URL prefix for the type + + :rtype: str + :returns: the URL, prefixed as appropriate + """ + descriptor = getattr(klass, 'DESCRIPTOR', None) + + if descriptor is not None: + name = descriptor.full_name + else: + name = '%s.%s' % (klass.__module__, klass.__name__) + + return '%s/%s' % (prefix, name) + + +def _register_type_url(type_url, klass): + """Register a klass as the factory for a given type URL. + + :type type_url: str + :param type_url: URL naming the type + + :type klass: type + :param klass: class to be used as a factory for the given type + + :raises: ValueError if a registration already exists for the URL. + """ + if type_url in _TYPE_URL_MAP: + if _TYPE_URL_MAP[type_url] is not klass: + raise ValueError("Conflict: %s" % (_TYPE_URL_MAP[type_url],)) + + _TYPE_URL_MAP[type_url] = klass + + +class Operation(object): + """Representation of a Google API Long-Running Operation. + + :type name: str + :param name: The fully-qualified path naming the operation. + + :type client: object: must provide ``_operations_stub`` accessor. + :param client: The client used to poll for the status of the operation. + + :type metadata: dict + :param metadata: Metadata about the operation + """ + + target = None + """Instance assocated with the operations: callers may set.""" + + def __init__(self, name, client, metadata=None): + self.name = name + self.client = client + self.metadata = metadata or {} + self._complete = False + + @classmethod + def from_pb(cls, op_pb, client): + """Factory: construct an instance from a protobuf. + + :type op_pb: :class:`google.longrunning.operations_pb2.Operation` + :param op_pb: Protobuf to be parsed. + + :type client: object: must provide ``_operations_stub`` accessor. + :param client: The client used to poll for the status of the operation. + + :rtype: :class:`Operation` + :returns: new instance, with attributes based on the protobuf. + """ + metadata = None + if op_pb.metadata.type_url: + type_url = op_pb.metadata.type_url + md_klass = _TYPE_URL_MAP.get(type_url) + if md_klass: + metadata = md_klass.FromString(op_pb.metadata.value) + return cls(op_pb.name, client, metadata) + + @property + def complete(self): + """Has the operation already completed? + + :rtype: bool + :returns: True if already completed, else false. + """ + return self._complete + + def finished(self): + """Check if the operation has finished. + + :rtype: bool + :returns: A boolean indicating if the current operation has completed. + :raises: :class:`ValueError ` if the operation + has already completed. + """ + if self.complete: + raise ValueError('The operation has completed.') + + request_pb = operations_pb2.GetOperationRequest(name=self.name) + # We expect a `google.longrunning.operations_pb2.Operation`. + operation_pb = self.client._operations_stub.GetOperation(request_pb) + + if operation_pb.done: + self._complete = True + + return self.complete diff --git a/gcloud/test_operation.py b/gcloud/test_operation.py new file mode 100644 index 000000000000..bad04f35afc0 --- /dev/null +++ b/gcloud/test_operation.py @@ -0,0 +1,238 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# 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 unittest + + +class Test__compute_type_url(unittest.TestCase): + + def _callFUT(self, klass, prefix=None): + from gcloud.operation import _compute_type_url + if prefix is None: + return _compute_type_url(klass) + return _compute_type_url(klass, prefix) + + def test_wo_prefix_w_descriptor(self): + from google.protobuf.struct_pb2 import Struct + from gcloud.operation import _GOOGLE_APIS_PREFIX + + type_url = self._callFUT(Struct) + + self.assertEqual( + type_url, + '%s/%s' % (_GOOGLE_APIS_PREFIX, Struct.DESCRIPTOR.full_name)) + + def test_w_prefix_wo_descriptor(self): + klass = self.__class__ + PREFIX = 'test.gcloud-python.com' + + type_url = self._callFUT(klass, PREFIX) + + self.assertEqual( + type_url, + '%s/%s.%s' % (PREFIX, klass.__module__, klass.__name__)) + + +class Test__register_type_url(unittest.TestCase): + + def _callFUT(self, type_url, klass): + from gcloud.operation import _register_type_url + _register_type_url(type_url, klass) + + def test_simple(self): + from gcloud import operation as MUT + from gcloud._testing import _Monkey + TYPE_URI = 'testing.gcloud-python.com/testing' + klass = object() + type_url_map = {} + + with _Monkey(MUT, _TYPE_URL_MAP=type_url_map): + self._callFUT(TYPE_URI, klass) + + self.assertEqual(type_url_map, {TYPE_URI: klass}) + + def test_w_same_class(self): + from gcloud import operation as MUT + from gcloud._testing import _Monkey + TYPE_URI = 'testing.gcloud-python.com/testing' + klass = object() + type_url_map = {TYPE_URI: klass} + + with _Monkey(MUT, _TYPE_URL_MAP=type_url_map): + self._callFUT(TYPE_URI, klass) + + self.assertEqual(type_url_map, {TYPE_URI: klass}) + + def test_w_conflict(self): + from gcloud import operation as MUT + from gcloud._testing import _Monkey + TYPE_URI = 'testing.gcloud-python.com/testing' + klass, other = object(), object() + type_url_map = {TYPE_URI: other} + + with _Monkey(MUT, _TYPE_URL_MAP=type_url_map): + with self.assertRaises(ValueError): + self._callFUT(TYPE_URI, klass) + + self.assertEqual(type_url_map, {TYPE_URI: other}) + + +class OperationTests(unittest.TestCase): + + OPERATION_NAME = 'operations/projects/foo/instances/bar/operations/123' + + def _getTargetClass(self): + from gcloud.operation import Operation + return Operation + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor_wo_metadata(self): + client = _Client() + operation = self._makeOne( + self.OPERATION_NAME, client) + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertTrue(operation.client is client) + self.assertTrue(operation.target is None) + self.assertEqual(operation.metadata, {}) + + def test_ctor_w_metadata(self): + client = _Client() + metadata = {'foo': 'bar'} + operation = self._makeOne( + self.OPERATION_NAME, client, metadata) + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertTrue(operation.client is client) + self.assertTrue(operation.target is None) + self.assertEqual(operation.metadata, metadata) + + def test_from_pb_wo_metadata(self): + from google.longrunning import operations_pb2 + client = _Client() + operation_pb = operations_pb2.Operation(name=self.OPERATION_NAME) + klass = self._getTargetClass() + + operation = klass.from_pb(operation_pb, client) + + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertTrue(operation.client is client) + self.assertEqual(operation.metadata, {}) + + def test_from_pb_w_unknown_metadata(self): + from google.longrunning import operations_pb2 + from google.protobuf.any_pb2 import Any + from google.protobuf.struct_pb2 import Struct, Value + TYPE_URI = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,) + + client = _Client() + meta = Struct(fields={'foo': Value(string_value=u'Bar')}) + metadata_pb = Any(type_url=TYPE_URI, value=meta.SerializeToString()) + operation_pb = operations_pb2.Operation( + name=self.OPERATION_NAME, metadata=metadata_pb) + klass = self._getTargetClass() + + operation = klass.from_pb(operation_pb, client) + + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertTrue(operation.client is client) + self.assertEqual(operation.metadata, {}) + + def test_from_pb_w_metadata(self): + from google.longrunning import operations_pb2 + from google.protobuf.any_pb2 import Any + from google.protobuf.struct_pb2 import Struct, Value + from gcloud import operation as MUT + from gcloud._testing import _Monkey + TYPE_URI = 'type.googleapis.com/%s' % (Struct.DESCRIPTOR.full_name,) + type_url_map = {TYPE_URI: Struct} + + client = _Client() + meta = Struct(fields={'foo': Value(string_value=u'Bar')}) + metadata_pb = Any(type_url=TYPE_URI, value=meta.SerializeToString()) + operation_pb = operations_pb2.Operation( + name=self.OPERATION_NAME, metadata=metadata_pb) + klass = self._getTargetClass() + + with _Monkey(MUT, _TYPE_URL_MAP=type_url_map): + operation = klass.from_pb(operation_pb, client) + + self.assertEqual(operation.name, self.OPERATION_NAME) + self.assertTrue(operation.client is client) + self.assertTrue(isinstance(operation.metadata, Struct)) + self.assertEqual(list(operation.metadata.fields), ['foo']) + self.assertEqual(operation.metadata.fields['foo'].string_value, 'Bar') + + def test_complete_property(self): + client = _Client() + operation = self._makeOne(self.OPERATION_NAME, client) + self.assertFalse(operation.complete) + operation._complete = True + self.assertTrue(operation.complete) + with self.assertRaises(AttributeError): + operation.complete = False + + def test_finished_already_complete(self): + client = _Client() + operation = self._makeOne(self.OPERATION_NAME, client) + operation._complete = True + + with self.assertRaises(ValueError): + operation.finished() + + def test_finished_false(self): + from google.longrunning.operations_pb2 import GetOperationRequest + response_pb = _GetOperationResponse(False) + client = _Client() + stub = client._operations_stub + stub._get_operation_response = response_pb + operation = self._makeOne(self.OPERATION_NAME, client) + + self.assertFalse(operation.finished()) + + request_pb = stub._get_operation_requested + self.assertTrue(isinstance(request_pb, GetOperationRequest)) + self.assertEqual(request_pb.name, self.OPERATION_NAME) + + def test_finished_true(self): + from google.longrunning.operations_pb2 import GetOperationRequest + response_pb = _GetOperationResponse(True) + client = _Client() + stub = client._operations_stub + stub._get_operation_response = response_pb + operation = self._makeOne(self.OPERATION_NAME, client) + + self.assertTrue(operation.finished()) + + request_pb = stub._get_operation_requested + self.assertTrue(isinstance(request_pb, GetOperationRequest)) + self.assertEqual(request_pb.name, self.OPERATION_NAME) + + +class _GetOperationResponse(object): + def __init__(self, done): + self.done = done + + +class _OperationsStub(object): + + def GetOperation(self, request_pb): + self._get_operation_requested = request_pb + return self._get_operation_response + + +class _Client(object): + + def __init__(self): + self._operations_stub = _OperationsStub() From 12b42a6864e311d039c03ebee23e7ffd80b78df9 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 22 Aug 2016 22:26:05 -0400 Subject: [PATCH 2/9] Add metadata type url registrations. Lost in merge conflict resolution. --- gcloud/bigtable/cluster.py | 8 ++++++++ gcloud/bigtable/instance.py | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gcloud/bigtable/cluster.py b/gcloud/bigtable/cluster.py index f3052f0bcb14..22d445430fed 100644 --- a/gcloud/bigtable/cluster.py +++ b/gcloud/bigtable/cluster.py @@ -22,6 +22,8 @@ from gcloud.bigtable._generated import ( bigtable_instance_admin_pb2 as messages_v2_pb2) from gcloud.operation import Operation +from gcloud.operation import _compute_type_url +from gcloud.operation import _register_type_url _CLUSTER_NAME_RE = re.compile(r'^projects/(?P[^/]+)/' @@ -32,6 +34,12 @@ """Default number of nodes to use when creating a cluster.""" +_UPDATE_CLUSTER_METADATA_URL = _compute_type_url( + messages_v2_pb2.UpdateClusterMetadata) +_register_type_url( + _UPDATE_CLUSTER_METADATA_URL, messages_v2_pb2.UpdateClusterMetadata) + + def _prepare_create_request(cluster): """Creates a protobuf request for a CreateCluster request. diff --git a/gcloud/bigtable/instance.py b/gcloud/bigtable/instance.py index 072f08ba2336..853ed7bfb036 100644 --- a/gcloud/bigtable/instance.py +++ b/gcloud/bigtable/instance.py @@ -17,7 +17,6 @@ import re -from gcloud.operation import Operation from gcloud.bigtable._generated import ( instance_pb2 as data_v2_pb2) from gcloud.bigtable._generated import ( @@ -27,6 +26,9 @@ from gcloud.bigtable.cluster import Cluster from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES from gcloud.bigtable.table import Table +from gcloud.operation import Operation +from gcloud.operation import _compute_type_url +from gcloud.operation import _register_type_url _EXISTING_INSTANCE_LOCATION_ID = 'see-existing-cluster' @@ -34,6 +36,12 @@ r'instances/(?P[a-z][-a-z0-9]*)$') +_CREATE_INSTANCE_METADATA_URL = _compute_type_url( + messages_v2_pb2.CreateInstanceMetadata) +_register_type_url( + _CREATE_INSTANCE_METADATA_URL, messages_v2_pb2.CreateInstanceMetadata) + + def _prepare_create_request(instance): """Creates a protobuf request for a CreateInstance request. From 23660b3a80291e4e0103885f58dc0a4225462374 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 22 Aug 2016 22:29:13 -0400 Subject: [PATCH 3/9] Move 'operation-api' document to 'core' section. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 75a8ddea61f6..17d7d5e73b5d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ gcloud-api gcloud-config gcloud-auth + operation-api .. toctree:: :maxdepth: 0 @@ -74,7 +75,6 @@ bigtable-row bigtable-row-filters bigtable-row-data - operation-api .. toctree:: :maxdepth: 0 From 2a501cd3bcf5b240f690d88006188eebc6a9e9f2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 22 Aug 2016 22:31:36 -0400 Subject: [PATCH 4/9] Fix copyright years. --- gcloud/operation.py | 2 +- gcloud/test_operation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gcloud/operation.py b/gcloud/operation.py index ff58e984fa55..f73a8b3a5196 100644 --- a/gcloud/operation.py +++ b/gcloud/operation.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/gcloud/test_operation.py b/gcloud/test_operation.py index bad04f35afc0..88e76d2cc7f1 100644 --- a/gcloud/test_operation.py +++ b/gcloud/test_operation.py @@ -1,4 +1,4 @@ -# Copyright 2015 Google Inc. All rights reserved. +# Copyright 2016 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From dfa9ac1ca6c8f219fe4d3f31b6a42a9c06e9cf15 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 22 Aug 2016 22:37:13 -0400 Subject: [PATCH 5/9] Rename 'Operation.finished' -> 'poll'. Clarify distinction between that method, which makes an API call to test for completion, and the 'complete' property, which stores the last result of that call. --- gcloud/operation.py | 2 +- gcloud/test_operation.py | 12 ++++++------ system_tests/bigtable.py | 18 +++++++++--------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gcloud/operation.py b/gcloud/operation.py index f73a8b3a5196..3e923c8798bf 100644 --- a/gcloud/operation.py +++ b/gcloud/operation.py @@ -115,7 +115,7 @@ def complete(self): """ return self._complete - def finished(self): + def poll(self): """Check if the operation has finished. :rtype: bool diff --git a/gcloud/test_operation.py b/gcloud/test_operation.py index 88e76d2cc7f1..8aa6047bcf7a 100644 --- a/gcloud/test_operation.py +++ b/gcloud/test_operation.py @@ -183,15 +183,15 @@ def test_complete_property(self): with self.assertRaises(AttributeError): operation.complete = False - def test_finished_already_complete(self): + def test_poll_already_complete(self): client = _Client() operation = self._makeOne(self.OPERATION_NAME, client) operation._complete = True with self.assertRaises(ValueError): - operation.finished() + operation.poll() - def test_finished_false(self): + def test_poll_false(self): from google.longrunning.operations_pb2 import GetOperationRequest response_pb = _GetOperationResponse(False) client = _Client() @@ -199,13 +199,13 @@ def test_finished_false(self): stub._get_operation_response = response_pb operation = self._makeOne(self.OPERATION_NAME, client) - self.assertFalse(operation.finished()) + self.assertFalse(operation.poll()) request_pb = stub._get_operation_requested self.assertTrue(isinstance(request_pb, GetOperationRequest)) self.assertEqual(request_pb.name, self.OPERATION_NAME) - def test_finished_true(self): + def test_poll_true(self): from google.longrunning.operations_pb2 import GetOperationRequest response_pb = _GetOperationResponse(True) client = _Client() @@ -213,7 +213,7 @@ def test_finished_true(self): stub._get_operation_response = response_pb operation = self._makeOne(self.OPERATION_NAME, client) - self.assertTrue(operation.finished()) + self.assertTrue(operation.poll()) request_pb = stub._get_operation_requested self.assertTrue(isinstance(request_pb, GetOperationRequest)) diff --git a/system_tests/bigtable.py b/system_tests/bigtable.py index c7113d4a883f..768893910a29 100644 --- a/system_tests/bigtable.py +++ b/system_tests/bigtable.py @@ -63,25 +63,25 @@ class Config(object): INSTANCE = None -def _operation_wait(operation, max_attempts=5): +def _wait_until_complete(operation, max_attempts=5): """Wait until an operation has completed. :type operation: :class:`gcloud.bigtable.instance.Operation` - :param operation: Operation that has not finished. + :param operation: Operation that has not complete. :type max_attempts: int :param max_attempts: (Optional) The maximum number of times to check if - the operation has finished. Defaults to 5. + the operation has complete. Defaults to 5. :rtype: bool - :returns: Boolean indicating if the operation finished. + :returns: Boolean indicating if the operation is complete. """ - def _operation_finished(result): + def _operation_complete(result): return result - retry = RetryResult(_operation_finished, max_tries=max_attempts) - return retry(operation.finished)() + retry = RetryResult(_operation_complete, max_tries=max_attempts) + return retry(operation.poll)() def _retry_on_unavailable(exc): @@ -105,7 +105,7 @@ def setUpModule(): # After listing, create the test instance. created_op = Config.INSTANCE.create() - if not _operation_wait(created_op): + if not _wait_until_complete(created_op): raise RuntimeError('Instance creation exceed 5 seconds.') @@ -150,7 +150,7 @@ def test_create_instance(self): self.instances_to_delete.append(instance) # We want to make sure the operation completes. - self.assertTrue(_operation_wait(operation)) + self.assertTrue(_wait_until_complete(operation)) # Create a new instance instance and make sure it is the same. instance_alt = Config.CLIENT.instance(ALT_INSTANCE_ID, LOCATION_ID) From cf3f8603f99c84c4b976db5d53333423cf8e9062 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Aug 2016 11:32:10 -0400 Subject: [PATCH 6/9] Include 'operation' as top-level module for json-docs. --- scripts/generate_json_docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/generate_json_docs.py b/scripts/generate_json_docs.py index 130c7eccee7a..e142a88817f7 100644 --- a/scripts/generate_json_docs.py +++ b/scripts/generate_json_docs.py @@ -625,6 +625,7 @@ def main(): 'iterator': [], 'logging': [], 'monitoring': [], + 'operation': [], 'pubsub': [], 'resource_manager': [], 'storage': [], From f2fb74e50bea0dd3ddffa39f72e6e07fdb5dc124 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Aug 2016 12:22:01 -0400 Subject: [PATCH 7/9] '_compute_type_url': drop support for non-protobuf classes. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/2165#discussion_r75898357 --- gcloud/operation.py | 8 +------- gcloud/test_operation.py | 10 +++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/gcloud/operation.py b/gcloud/operation.py index 3e923c8798bf..8fd95df90743 100644 --- a/gcloud/operation.py +++ b/gcloud/operation.py @@ -35,13 +35,7 @@ def _compute_type_url(klass, prefix=_GOOGLE_APIS_PREFIX): :rtype: str :returns: the URL, prefixed as appropriate """ - descriptor = getattr(klass, 'DESCRIPTOR', None) - - if descriptor is not None: - name = descriptor.full_name - else: - name = '%s.%s' % (klass.__module__, klass.__name__) - + name = klass.DESCRIPTOR.full_name return '%s/%s' % (prefix, name) diff --git a/gcloud/test_operation.py b/gcloud/test_operation.py index 8aa6047bcf7a..90714ffc2157 100644 --- a/gcloud/test_operation.py +++ b/gcloud/test_operation.py @@ -23,7 +23,7 @@ def _callFUT(self, klass, prefix=None): return _compute_type_url(klass) return _compute_type_url(klass, prefix) - def test_wo_prefix_w_descriptor(self): + def test_wo_prefix(self): from google.protobuf.struct_pb2 import Struct from gcloud.operation import _GOOGLE_APIS_PREFIX @@ -33,15 +33,15 @@ def test_wo_prefix_w_descriptor(self): type_url, '%s/%s' % (_GOOGLE_APIS_PREFIX, Struct.DESCRIPTOR.full_name)) - def test_w_prefix_wo_descriptor(self): - klass = self.__class__ + def test_w_prefix(self): + from google.protobuf.struct_pb2 import Struct PREFIX = 'test.gcloud-python.com' - type_url = self._callFUT(klass, PREFIX) + type_url = self._callFUT(Struct, PREFIX) self.assertEqual( type_url, - '%s/%s.%s' % (PREFIX, klass.__module__, klass.__name__)) + '%s/%s' % (PREFIX, Struct.DESCRIPTOR.full_name)) class Test__register_type_url(unittest.TestCase): From 9d73629fcecca60804c7dab00b6bafede7001d35 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Aug 2016 16:21:31 -0400 Subject: [PATCH 8/9] Separate back-end-provided 'pb_metadata' from caller-provided 'metadata'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/2165#discussion_r75940962 --- gcloud/operation.py | 23 +++++++++++++++-------- gcloud/test_operation.py | 28 +++++++++++++++++----------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/gcloud/operation.py b/gcloud/operation.py index 8fd95df90743..5fbdc9faca3f 100644 --- a/gcloud/operation.py +++ b/gcloud/operation.py @@ -66,21 +66,25 @@ class Operation(object): :type client: object: must provide ``_operations_stub`` accessor. :param client: The client used to poll for the status of the operation. - :type metadata: dict - :param metadata: Metadata about the operation + :type pb_metadata: object + :param pb_metadata: Instance of protobuf metadata class + + :type kw: dict + :param kw: caller-assigned metadata about the operation """ target = None """Instance assocated with the operations: callers may set.""" - def __init__(self, name, client, metadata=None): + def __init__(self, name, client, pb_metadata=None, **kw): self.name = name self.client = client - self.metadata = metadata or {} + self.pb_metadata = pb_metadata + self.metadata = kw.copy() self._complete = False @classmethod - def from_pb(cls, op_pb, client): + def from_pb(cls, op_pb, client, **kw): """Factory: construct an instance from a protobuf. :type op_pb: :class:`google.longrunning.operations_pb2.Operation` @@ -89,16 +93,19 @@ def from_pb(cls, op_pb, client): :type client: object: must provide ``_operations_stub`` accessor. :param client: The client used to poll for the status of the operation. + :type kw: dict + :param kw: caller-assigned metadata about the operation + :rtype: :class:`Operation` :returns: new instance, with attributes based on the protobuf. """ - metadata = None + pb_metadata = None if op_pb.metadata.type_url: type_url = op_pb.metadata.type_url md_klass = _TYPE_URL_MAP.get(type_url) if md_klass: - metadata = md_klass.FromString(op_pb.metadata.value) - return cls(op_pb.name, client, metadata) + pb_metadata = md_klass.FromString(op_pb.metadata.value) + return cls(op_pb.name, client, pb_metadata, **kw) @property def complete(self): diff --git a/gcloud/test_operation.py b/gcloud/test_operation.py index 90714ffc2157..be7aa89a3854 100644 --- a/gcloud/test_operation.py +++ b/gcloud/test_operation.py @@ -99,26 +99,28 @@ def _getTargetClass(self): def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) - def test_ctor_wo_metadata(self): + def test_ctor_defaults(self): client = _Client() operation = self._makeOne( self.OPERATION_NAME, client) self.assertEqual(operation.name, self.OPERATION_NAME) self.assertTrue(operation.client is client) self.assertTrue(operation.target is None) + self.assertTrue(operation.pb_metadata is None) self.assertEqual(operation.metadata, {}) - def test_ctor_w_metadata(self): + def test_ctor_explicit(self): client = _Client() - metadata = {'foo': 'bar'} + pb_metadata = object() operation = self._makeOne( - self.OPERATION_NAME, client, metadata) + self.OPERATION_NAME, client, pb_metadata, foo='bar') self.assertEqual(operation.name, self.OPERATION_NAME) self.assertTrue(operation.client is client) self.assertTrue(operation.target is None) - self.assertEqual(operation.metadata, metadata) + self.assertTrue(operation.pb_metadata is pb_metadata) + self.assertEqual(operation.metadata, {'foo': 'bar'}) - def test_from_pb_wo_metadata(self): + def test_from_pb_wo_metadata_or_kw(self): from google.longrunning import operations_pb2 client = _Client() operation_pb = operations_pb2.Operation(name=self.OPERATION_NAME) @@ -128,6 +130,7 @@ def test_from_pb_wo_metadata(self): self.assertEqual(operation.name, self.OPERATION_NAME) self.assertTrue(operation.client is client) + self.assertTrue(operation.pb_metadata is None) self.assertEqual(operation.metadata, {}) def test_from_pb_w_unknown_metadata(self): @@ -147,9 +150,10 @@ def test_from_pb_w_unknown_metadata(self): self.assertEqual(operation.name, self.OPERATION_NAME) self.assertTrue(operation.client is client) + self.assertTrue(operation.pb_metadata is None) self.assertEqual(operation.metadata, {}) - def test_from_pb_w_metadata(self): + def test_from_pb_w_metadata_and_kwargs(self): from google.longrunning import operations_pb2 from google.protobuf.any_pb2 import Any from google.protobuf.struct_pb2 import Struct, Value @@ -166,13 +170,15 @@ def test_from_pb_w_metadata(self): klass = self._getTargetClass() with _Monkey(MUT, _TYPE_URL_MAP=type_url_map): - operation = klass.from_pb(operation_pb, client) + operation = klass.from_pb(operation_pb, client, baz='qux') self.assertEqual(operation.name, self.OPERATION_NAME) self.assertTrue(operation.client is client) - self.assertTrue(isinstance(operation.metadata, Struct)) - self.assertEqual(list(operation.metadata.fields), ['foo']) - self.assertEqual(operation.metadata.fields['foo'].string_value, 'Bar') + pb_metadata = operation.pb_metadata + self.assertTrue(isinstance(pb_metadata, Struct)) + self.assertEqual(list(pb_metadata.fields), ['foo']) + self.assertEqual(pb_metadata.fields['foo'].string_value, 'Bar') + self.assertEqual(operation.metadata, {'baz': 'qux'}) def test_complete_property(self): client = _Client() From 01ffb452d5e8937eba6d7ef8871916d6eb4ef393 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 23 Aug 2016 17:02:23 -0400 Subject: [PATCH 9/9] Capture call-site metadata. Also, test that operation_pb.metadta is correctly reflected. --- gcloud/bigtable/cluster.py | 4 +++- gcloud/bigtable/instance.py | 1 + gcloud/bigtable/test_cluster.py | 24 +++++++++++++++++++++++- gcloud/bigtable/test_instance.py | 19 ++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/gcloud/bigtable/cluster.py b/gcloud/bigtable/cluster.py index 22d445430fed..3fd38fa4d28d 100644 --- a/gcloud/bigtable/cluster.py +++ b/gcloud/bigtable/cluster.py @@ -219,6 +219,7 @@ def create(self): operation = Operation.from_pb(operation_pb, client) operation.target = self + operation.metadata['request_type'] = 'CreateCluster' return operation def update(self): @@ -243,12 +244,13 @@ def update(self): name=self.name, serve_nodes=self.serve_nodes, ) - # Ignore expected `._generated.instance_pb2.Cluster`. + # We expect a `google.longrunning.operations_pb2.Operation`. client = self._instance._client operation_pb = client._instance_stub.UpdateCluster(request_pb) operation = Operation.from_pb(operation_pb, client) operation.target = self + operation.metadata['request_type'] = 'UpdateCluster' return operation def delete(self): diff --git a/gcloud/bigtable/instance.py b/gcloud/bigtable/instance.py index 853ed7bfb036..19a3131085f4 100644 --- a/gcloud/bigtable/instance.py +++ b/gcloud/bigtable/instance.py @@ -238,6 +238,7 @@ def create(self): operation = Operation.from_pb(operation_pb, self._client) operation.target = self + operation.metadata['request_type'] = 'CreateInstance' return operation def update(self): diff --git a/gcloud/bigtable/test_cluster.py b/gcloud/bigtable/test_cluster.py index c847ec0a28e4..9ad256159fd0 100644 --- a/gcloud/bigtable/test_cluster.py +++ b/gcloud/bigtable/test_cluster.py @@ -257,6 +257,8 @@ def test_create(self): self.assertEqual(result.name, OP_NAME) self.assertTrue(result.target is cluster) self.assertTrue(result.client is client) + self.assertTrue(result.pb_metadata is None) + self.assertEqual(result.metadata, {'request_type': 'CreateCluster'}) self.assertEqual(len(stub.method_calls), 1) api_name, args, kwargs = stub.method_calls[0] @@ -270,11 +272,20 @@ def test_create(self): self.assertEqual(kwargs, {}) def test_update(self): + import datetime from google.longrunning import operations_pb2 from gcloud.operation import Operation + from google.protobuf.any_pb2 import Any + from gcloud._helpers import _datetime_to_pb_timestamp from gcloud.bigtable._generated import ( instance_pb2 as data_v2_pb2) + from gcloud.bigtable._generated import ( + bigtable_instance_admin_pb2 as messages_v2_pb2) from gcloud.bigtable._testing import _FakeStub + from gcloud.bigtable.cluster import _UPDATE_CLUSTER_METADATA_URL + + NOW = datetime.datetime.utcnow() + NOW_PB = _datetime_to_pb_timestamp(NOW) SERVE_NODES = 81 @@ -294,7 +305,14 @@ def test_update(self): OP_NAME = ( 'operations/projects/%s/instances/%s/clusters/%s/operations/%d' % (self.PROJECT, self.INSTANCE_ID, self.CLUSTER_ID, OP_ID)) - response_pb = operations_pb2.Operation(name=OP_NAME) + metadata = messages_v2_pb2.UpdateClusterMetadata(request_time=NOW_PB) + response_pb = operations_pb2.Operation( + name=OP_NAME, + metadata=Any( + type_url=_UPDATE_CLUSTER_METADATA_URL, + value=metadata.SerializeToString() + ) + ) # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) @@ -305,6 +323,10 @@ def test_update(self): self.assertEqual(result.name, OP_NAME) self.assertTrue(result.target is cluster) self.assertTrue(result.client is client) + self.assertIsInstance(result.pb_metadata, + messages_v2_pb2.UpdateClusterMetadata) + self.assertEqual(result.pb_metadata.request_time, NOW_PB) + self.assertEqual(result.metadata, {'request_type': 'UpdateCluster'}) self.assertEqual(len(stub.method_calls), 1) api_name, args, kwargs = stub.method_calls[0] diff --git a/gcloud/bigtable/test_instance.py b/gcloud/bigtable/test_instance.py index 2e61879ffdd2..0f9fce2ad8c5 100644 --- a/gcloud/bigtable/test_instance.py +++ b/gcloud/bigtable/test_instance.py @@ -226,19 +226,32 @@ def test_reload(self): self.assertEqual(instance.display_name, DISPLAY_NAME) def test_create(self): + import datetime from google.longrunning import operations_pb2 + from google.protobuf.any_pb2 import Any from gcloud.bigtable._generated import ( bigtable_instance_admin_pb2 as messages_v2_pb2) + from gcloud._helpers import _datetime_to_pb_timestamp from gcloud.bigtable._testing import _FakeStub from gcloud.operation import Operation from gcloud.bigtable.cluster import DEFAULT_SERVE_NODES + from gcloud.bigtable.instance import _CREATE_INSTANCE_METADATA_URL + NOW = datetime.datetime.utcnow() + NOW_PB = _datetime_to_pb_timestamp(NOW) client = _Client(self.PROJECT) instance = self._makeOne(self.INSTANCE_ID, client, self.LOCATION_ID, display_name=self.DISPLAY_NAME) # Create response_pb - response_pb = operations_pb2.Operation(name=self.OP_NAME) + metadata = messages_v2_pb2.CreateInstanceMetadata(request_time=NOW_PB) + response_pb = operations_pb2.Operation( + name=self.OP_NAME, + metadata=Any( + type_url=_CREATE_INSTANCE_METADATA_URL, + value=metadata.SerializeToString(), + ) + ) # Patch the stub used by the API method. client._instance_stub = stub = _FakeStub(response_pb) @@ -250,6 +263,10 @@ def test_create(self): self.assertEqual(result.name, self.OP_NAME) self.assertTrue(result.target is instance) self.assertTrue(result.client is client) + self.assertIsInstance(result.pb_metadata, + messages_v2_pb2.CreateInstanceMetadata) + self.assertEqual(result.pb_metadata.request_time, NOW_PB) + self.assertEqual(result.metadata, {'request_type': 'CreateInstance'}) self.assertEqual(len(stub.method_calls), 1) api_name, args, kwargs = stub.method_calls[0]