Repository: googleapis/google-cloud-python

Issue:
Bigtable happybase create_table should throw AlreadyExists when table already exists.
HappyBase `happybase.Connection.create_table` throws `happybase.hbase.ttypes.AlreadyExists` when a table already exists.
https://github.com/wbolster/happybase/blob/3565b2b43b377b3e539315570d3e51d053d7a8a1/happybase/Hbase.thrift#L274

In the gcloud-python implementation, this throws an internal gRPC error.

```
Create table Hello-Bigtable
Traceback (most recent call last):
  File "hello.py", line 119, in <module>
    main()
  File "hello.py", line 95, in main
    table = create_table(connection, TABLE_NAME, COLUMN_FAMILY_NAME)
  File "hello.py", line 76, in create_table
    column_family_name: dict()  # Use default options.
  File "/usr/local/google/home/swast/venv/bigtable_helloworld/local/lib/python2.7/site-packages/gcloud/bigtable/happybase/connection.py", line 341, in create_table
    low_level_table.create()
  File "/usr/local/google/home/swast/venv/bigtable_helloworld/local/lib/python2.7/site-packages/gcloud/bigtable/table.py", line 177, in create
    client._table_stub.CreateTable(request_pb, client.timeout_seconds)
  File "/usr/local/google/home/swast/venv/bigtable_helloworld/local/lib/python2.7/site-packages/grpc/framework/crust/implementations.py", line 75, in __call__
    protocol_options, metadata, request)
  File "/usr/local/google/home/swast/venv/bigtable_helloworld/local/lib/python2.7/site-packages/grpc/framework/crust/_calls.py", line 109, in blocking_unary_unary
    return next(rendezvous)
  File "/usr/local/google/home/swast/venv/bigtable_helloworld/local/lib/python2.7/site-packages/grpc/framework/crust/_control.py", line 415, in next
    raise self._termination.abortion_error
grpc.framework.interfaces.face.face.NetworkError: NetworkError(code=StatusCode.ALREADY_EXISTS, details="Error while creating table Hello-Bigtable in cluster projects/swast-bigtable-examples/zones/us-central1-c/clusters/my-cluster : Transaction Failed : Cannot re-create existing table: Hello-Bigtable")
```

I can catch this with the following, but it seems weird to differ from vanilla HappyBase in this way.

```
from gcloud import bigtable
from gcloud.bigtable import happybase
from grpc.framework.interfaces.face import face as grpcface


TABLE_NAME = 'Hello-Bigtable'
COLUMN_FAMILY_NAME = 'cf1'


def create_table(connection, table_name, column_family_name):
    try:
        connection.create_table(
            table_name,
            {
                column_family_name: dict()  # Use default options.
            })
    except grpcface.NetworkError as exp:
        if exp.code != interfaces.StatusCode.ALREADY_EXISTS:
            raise
        print("Table already exists.")
    return connection.table(table_name)


client = bigtable.Client(project=args.project, admin=True)
with client:
    cluster = client.cluster('us-central1-c', 'my-cluster')
    cluster.reload()
    connection = happybase.Connection(cluster=cluster)
    print connection.tables()

    print('Create table {0}'.format(TABLE_NAME))
    table = create_table(connection, TABLE_NAME, COLUMN_FAMILY_NAME)
```



Discussion:
Example of catching AlreadyExists in the wild. https://github.com/openstack/aodh/blob/dd72badd5b460d2d0c969414bb1c8c2de20be727/aodh/storage/hbase/utils.py#L221

Sorry for the delay, 15 days eek! Digging in now

BTW I really appreciate having someone familiar with vanilla HappyBase!


Resolving patch:
diff --git a/gcloud/bigtable/happybase/connection.py b/gcloud/bigtable/happybase/connection.py
--- a/gcloud/bigtable/happybase/connection.py
+++ b/gcloud/bigtable/happybase/connection.py
@@ -20,6 +20,14 @@
 
 import six
 
+from grpc.beta import interfaces
+from grpc.framework.interfaces.face import face
+
+try:
+    from happybase.hbase.ttypes import AlreadyExists
+except ImportError:
+    from gcloud.exceptions import Conflict as AlreadyExists
+
 from gcloud.bigtable.client import Client
 from gcloud.bigtable.column_family import GCRuleIntersection
 from gcloud.bigtable.column_family import MaxAgeGCRule
@@ -338,7 +346,13 @@ def create_table(self, name, families):
         # Create table instance and then make API calls.
         name = self._table_name(name)
         low_level_table = _LowLevelTable(name, self._cluster)
-        low_level_table.create()
+        try:
+            low_level_table.create()
+        except face.NetworkError as network_err:
+            if network_err.code == interfaces.StatusCode.ALREADY_EXISTS:
+                raise AlreadyExists(name)
+            else:
+                raise
 
         for column_family_name, gc_rule in gc_rule_dict.items():
             column_family = low_level_table.column_family(


Test patch:
diff --git a/_testing/grpc/beta/interfaces.py b/_testing/grpc/beta/interfaces.py
new file mode 100644
--- /dev/null
+++ b/_testing/grpc/beta/interfaces.py
@@ -0,0 +1,13 @@
+# 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.
+# 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.
diff --git a/_testing/grpc/framework/interfaces/face/face.py b/_testing/grpc/framework/interfaces/face/face.py
new file mode 100644
--- /dev/null
+++ b/_testing/grpc/framework/interfaces/face/face.py
@@ -0,0 +1,13 @@
+# 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.
+# 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.
diff --git a/gcloud/bigtable/happybase/test_connection.py b/gcloud/bigtable/happybase/test_connection.py
--- a/gcloud/bigtable/happybase/test_connection.py
+++ b/gcloud/bigtable/happybase/test_connection.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 
 
+import sys
+
 import unittest2
 
 
@@ -395,6 +397,55 @@ def test_create_table_bad_value(self):
         with self.assertRaises(ValueError):
             connection.create_table(name, families)
 
+    def _create_table_error_helper(self, err_val, err_type):
+        from gcloud._testing import _Monkey
+        from gcloud.bigtable.happybase import connection as MUT
+
+        cluster = _Cluster()  # Avoid implicit environ check.
+        connection = self._makeOne(autoconnect=False, cluster=cluster)
+
+        tables_created = []
+
+        def make_table(*args, **kwargs):
+            kwargs['create_error'] = err_val
+            result = _MockLowLevelTable(*args, **kwargs)
+            tables_created.append(result)
+            return result
+
+        name = 'table-name'
+        families = {'foo': {}}
+        with _Monkey(MUT, _LowLevelTable=make_table):
+            with self.assertRaises(err_type):
+                connection.create_table(name, families)
+
+        self.assertEqual(len(tables_created), 1)
+        self.assertEqual(tables_created[0].create_calls, 1)
+
+    @unittest2.skipUnless(sys.version_info[:2] == (2, 7),
+                          'gRPC only in Python 2.7')
+    def test_create_table_already_exists(self):
+        from grpc.beta import interfaces
+        from grpc.framework.interfaces.face import face
+        from gcloud.bigtable.happybase.connection import AlreadyExists
+
+        err_val = face.NetworkError(None, None,
+                                    interfaces.StatusCode.ALREADY_EXISTS, None)
+        self._create_table_error_helper(err_val, AlreadyExists)
+
+    @unittest2.skipUnless(sys.version_info[:2] == (2, 7),
+                          'gRPC only in Python 2.7')
+    def test_create_table_connection_error(self):
+        from grpc.beta import interfaces
+        from grpc.framework.interfaces.face import face
+        err_val = face.NetworkError(None, None,
+                                    interfaces.StatusCode.INTERNAL, None)
+        self._create_table_error_helper(err_val, face.NetworkError)
+
+    @unittest2.skipUnless(sys.version_info[:2] == (2, 7),
+                          'gRPC only in Python 2.7')
+    def test_create_table_other_error(self):
+        self._create_table_error_helper(RuntimeError, RuntimeError)
+
     def _delete_table_helper(self, disable=False):
         from gcloud._testing import _Monkey
         from gcloud.bigtable.happybase import connection as MUT
@@ -612,6 +663,7 @@ class _MockLowLevelTable(object):
     def __init__(self, *args, **kwargs):
         self.args = args
         self.kwargs = kwargs
+        self.create_error = kwargs.get('create_error')
         self.delete_calls = 0
         self.create_calls = 0
         self.col_fam_created = []
@@ -621,6 +673,8 @@ def delete(self):
 
     def create(self):
         self.create_calls += 1
+        if self.create_error:
+            raise self.create_error
 
     def column_family(self, column_family_id, gc_rule=None):
         result = _MockLowLevelColumnFamily(column_family_id, gc_rule=gc_rule)