diff --git a/gcloud/bigtable/client.py b/gcloud/bigtable/client.py index 3eb8a90d5409..f5a27c277e81 100644 --- a/gcloud/bigtable/client.py +++ b/gcloud/bigtable/client.py @@ -19,17 +19,20 @@ In the hierarchy of API concepts * a :class:`Client` owns a :class:`.Cluster` -* a :class:`.Cluster` owns a :class:`Table ` -* a :class:`Table ` owns a +* a :class:`.Cluster` owns a :class:`Table ` +* a :class:`Table ` owns a :class:`ColumnFamily <.column_family.ColumnFamily>` -* a :class:`Table ` owns a :class:`Row <.row.Row>` +* a :class:`Table ` owns a :class:`Row <.row.Row>` (and all the cells in the row) """ import copy +from gcloud.bigtable._generated import bigtable_cluster_data_pb2 as data_pb2 from gcloud.bigtable._generated import bigtable_cluster_service_pb2 +from gcloud.bigtable._generated import ( + bigtable_cluster_service_messages_pb2 as messages_pb2) from gcloud.bigtable._generated import bigtable_service_pb2 from gcloud.bigtable._generated import bigtable_table_service_pb2 from gcloud.bigtable._generated import operations_pb2 @@ -192,7 +195,7 @@ def project_name(self): The project name is of the form - ``"projects/{project_id}"`` + ``"projects/{project}"`` :rtype: str :returns: The project name to be used with the Cloud Bigtable Admin @@ -375,3 +378,25 @@ def cluster(self, zone, cluster_id, display_name=None, serve_nodes=3): """ return Cluster(zone, cluster_id, self, display_name=display_name, serve_nodes=serve_nodes) + + def list_zones(self): + """Lists zones associated with project. + + :rtype: list + :returns: The names (as :class:`str`) of the zones + :raises: :class:`ValueError ` if one of the + zones is not in ``OK`` state. + """ + request_pb = messages_pb2.ListZonesRequest(name=self.project_name) + response = self._cluster_stub.ListZones.async(request_pb, + self.timeout_seconds) + # We expect a `.messages_pb2.ListZonesResponse` + list_zones_response = response.result() + + result = [] + for zone in list_zones_response.zones: + if zone.status != data_pb2.Zone.OK: + raise ValueError('Zone %s not in OK state' % ( + zone.display_name,)) + result.append(zone.display_name) + return result diff --git a/gcloud/bigtable/column_family.py b/gcloud/bigtable/column_family.py index b00e27a55dc9..2b29b8f01666 100644 --- a/gcloud/bigtable/column_family.py +++ b/gcloud/bigtable/column_family.py @@ -22,7 +22,7 @@ class ColumnFamily(object): :param column_family_id: The ID of the column family. Must be of the form ``[_a-zA-Z0-9][-_.a-zA-Z0-9]*``. - :type table: :class:`Table ` + :type table: :class:`Table ` :param table: The table that owns the column family. """ diff --git a/gcloud/bigtable/row.py b/gcloud/bigtable/row.py index 82c5c2268afd..b5eaac0f0239 100644 --- a/gcloud/bigtable/row.py +++ b/gcloud/bigtable/row.py @@ -24,7 +24,7 @@ class Row(object): :type row_key: bytes :param row_key: The key for the current row. - :type table: :class:`Table ` + :type table: :class:`Table ` :param table: The table that owns the row. """ diff --git a/gcloud/bigtable/test_client.py b/gcloud/bigtable/test_client.py index 7e4dd9f92bd8..5684975f4baa 100644 --- a/gcloud/bigtable/test_client.py +++ b/gcloud/bigtable/test_client.py @@ -494,6 +494,59 @@ def test_cluster_factory(self): self.assertEqual(cluster.serve_nodes, serve_nodes) self.assertTrue(cluster._client is client) + def _list_zones_helper(self, zone_status): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + from gcloud.bigtable._generated import ( + bigtable_cluster_service_messages_pb2 as messages_pb2) + + credentials = _Credentials() + project = 'PROJECT' + timeout_seconds = 281330 + client = self._makeOne(project=project, credentials=credentials, + admin=True, timeout_seconds=timeout_seconds) + + # Create request_pb + request_pb = messages_pb2.ListZonesRequest( + name='projects/' + project, + ) + + # Create response_pb + zone1 = 'foo' + zone2 = 'bar' + response_pb = messages_pb2.ListZonesResponse( + zones=[ + data_pb2.Zone(display_name=zone1, status=zone_status), + data_pb2.Zone(display_name=zone2, status=zone_status), + ], + ) + + # Patch the stub used by the API method. + client._cluster_stub_internal = stub = _FakeStub(response_pb) + + # Create expected_result. + expected_result = [zone1, zone2] + + # Perform the method and check the result. + result = client.list_zones() + self.assertEqual(result, expected_result) + self.assertEqual(stub.method_calls, [( + 'ListZones', + (request_pb, timeout_seconds), + {}, + )]) + + def test_list_zones(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + self._list_zones_helper(data_pb2.Zone.OK) + + def test_list_zones_failure(self): + from gcloud.bigtable._generated import ( + bigtable_cluster_data_pb2 as data_pb2) + with self.assertRaises(ValueError): + self._list_zones_helper(data_pb2.Zone.EMERGENCY_MAINENANCE) + class _Credentials(object): @@ -511,8 +564,11 @@ def __eq__(self, other): class _FakeStub(object): + """Acts as a gPRC stub.""" - def __init__(self): + def __init__(self, *results): + self.results = results + self.method_calls = [] self._entered = 0 self._exited = [] @@ -523,3 +579,38 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self._exited.append((exc_type, exc_val, exc_tb)) return True + + def __getattr__(self, name): + # We need not worry about attributes set in constructor + # since __getattribute__ will handle them. + return _MethodMock(name, self) + + +class _MethodMock(object): + """Mock for :class:`grpc.framework.alpha._reexport._UnaryUnarySyncAsync`. + + May need to be callable and needs to (in our use) have an + ``async`` method. + """ + + def __init__(self, name, factory): + self._name = name + self._factory = factory + + def async(self, *args, **kwargs): + """Async method meant to mock a gRPC stub request.""" + self._factory.method_calls.append((self._name, args, kwargs)) + curr_result, self._factory.results = (self._factory.results[0], + self._factory.results[1:]) + return _AsyncResult(curr_result) + + +class _AsyncResult(object): + """Result returned from a ``_MethodMock.async`` call.""" + + def __init__(self, result): + self._result = result + + def result(self): + """Result method on an asyc object.""" + return self._result