Repository: googleapis/gax-python

Issue:
Make it clear that the call may fail with the INVALID_ARGUMENT status code
### What:
Wrap the gRPC INVALID_ARGUMENT error with a pythonic VALUE_ERROR. We can do this with a special attribute that we consistently use for the wrapped gRPC error.

### Why:
Be more Pythonic.

See https://github.com/googleapis/googleapis/issues/15

Discussion:
geigerj: Note: this is a breaking change, but since we haven't released `0.14.0` yet, I'm not incrementing the version.
codecov-io: ## [Current coverage](https://codecov.io/gh/googleapis/gax-python/pull/132?src=pr) is 98.05% (diff: 100%)
> Merging [#132](https://codecov.io/gh/googleapis/gax-python/pull/132?src=pr) into [master](https://codecov.io/gh/googleapis/gax-python/branch/master?src=pr) will increase coverage by **0.02%**

```diff
@@             master       #132   diff @@
==========================================
  Files             8          8          
  Lines           608        616     +8   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            596        604     +8   
  Misses           12         12          
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [7b3172b...db0bf04](https://codecov.io/gh/googleapis/gax-python/compare/7b3172b9e8ebd1e513955bf60b42aa5f63d37d4a...db0bf049ff5ba03dc15aa40dc4da7ab02e4efed3?src=pr)
bjwatson: LGTM

Patch:
diff --git a/google/gax/api_callable.py b/google/gax/api_callable.py
index 622c08e..00c7763 100644
--- a/google/gax/api_callable.py
+++ b/google/gax/api_callable.py
@@ -36,8 +36,8 @@ import time
 from future import utils
 
 from . import (BackoffSettings, BundleOptions, bundling, _CallSettings, config,
-               PageIterator, ResourceIterator, RetryOptions)
-from .errors import GaxError, RetryError
+               errors, PageIterator, ResourceIterator, RetryOptions)
+from .errors import RetryError
 
 _MILLIS_PER_SECOND = 1000
 
@@ -414,12 +414,12 @@ def construct_settings(
     return defaults
 
 
-def _catch_errors(a_func, errors):
+def _catch_errors(a_func, to_catch):
     """Updates a_func to wrap exceptions with GaxError
 
     Args:
         a_func (callable): A callable.
-        retry (list[Exception]): Configures the exceptions to wrap.
+        to_catch (list[Exception]): Configures the exceptions to wrap.
 
     Returns:
         A function that will wrap certain exceptions with GaxError
@@ -429,8 +429,9 @@ def _catch_errors(a_func, errors):
         try:
             return a_func(*args, **kwargs)
         # pylint: disable=catching-non-exception
-        except tuple(errors) as exception:
-            utils.raise_with_traceback(GaxError('RPC failed', cause=exception))
+        except tuple(to_catch) as exception:
+            utils.raise_with_traceback(
+                errors.create_error('RPC failed', cause=exception))
 
     return inner
 
diff --git a/google/gax/config.py b/google/gax/config.py
index 2d94ebe..3a1d34d 100644
--- a/google/gax/config.py
+++ b/google/gax/config.py
@@ -49,6 +49,10 @@ the client constants configuration for retrying into the correct gRPC objects.
 """
 
 
+NAME_STATUS_CODES = grpc.NAME_STATUS_CODES
+"""Inverse map for STATUS_CODE_NAMES"""
+
+
 create_stub = grpc.create_stub  # pylint: disable=invalid-name,
 """The function to use to create stubs."""
 
diff --git a/google/gax/errors.py b/google/gax/errors.py
index c118b1e..79dc12f 100644
--- a/google/gax/errors.py
+++ b/google/gax/errors.py
@@ -29,6 +29,9 @@
 
 """Provides GAX exceptions."""
 
+from __future__ import absolute_import
+from . import config
+
 
 class GaxError(Exception):
     """Common base class for exceptions raised by GAX.
@@ -51,6 +54,25 @@ class GaxError(Exception):
         return 'GaxError({}, caused by {})'.format(msg, self.cause)
 
 
+def create_error(msg, cause=None):
+    """Creates an error.
+
+    Uses a Python built-in exception if one is available, and a
+    GaxError otherwise.
+
+    Attributes:
+      msg (string): describes the error that occurred.
+      cause (Exception, optional): the exception raised by a lower
+        layer of the RPC stack (for example, gRPC) that caused this
+        exception, or None if this exception originated in GAX.
+    """
+    if config.NAME_STATUS_CODES.get(
+            config.exc_to_code(cause)) == 'INVALID_ARGUMENT':
+        return ValueError('{}: {}'.format(msg, cause))
+    else:
+        return GaxError(msg, cause)
+
+
 class RetryError(GaxError):
     """Indicates an error during automatic GAX retrying."""
     pass
diff --git a/google/gax/grpc.py b/google/gax/grpc.py
index 9ec50fb..bb38550 100644
--- a/google/gax/grpc.py
+++ b/google/gax/grpc.py
@@ -58,6 +58,10 @@ STATUS_CODE_NAMES = {
 """Maps strings used in client config to gRPC status codes."""
 
 
+NAME_STATUS_CODES = dict([(v, k) for (k, v) in STATUS_CODE_NAMES.items()])
+"""Inverse map for STATUS_CODE_NAMES"""
+
+
 def exc_to_code(exc):
     """Retrieves the status code from an exception"""
     if not isinstance(exc, RpcError):


Test patch:
diff --git a/test/test_api_callable.py b/test/test_api_callable.py
index f45c5f4..f30ab66 100644
--- a/test/test_api_callable.py
+++ b/test/test_api_callable.py
@@ -38,6 +38,7 @@ from google.gax import (
     api_callable, bundling, BackoffSettings, BundleDescriptor, BundleOptions,
     _CallSettings, CallOptions, INITIAL_PAGE, PageDescriptor, RetryOptions)
 from google.gax.errors import GaxError, RetryError
+import grpc
 
 
 _SERVICE_NAME = 'test.interface.v1.api'
@@ -484,3 +485,15 @@ class TestCreateApiCallable(unittest2.TestCase):
         other_error_callable = api_callable.create_api_call(
             other_error_func, _CallSettings())
         self.assertRaises(AnotherException, other_error_callable, None)
+
+    def test_wrap_value_error(self):
+
+        invalid_attribute_exc = grpc.RpcError()
+        invalid_attribute_exc.code = lambda: grpc.StatusCode.INVALID_ARGUMENT
+
+        def value_error_func(*dummy_args, **dummy_kwargs):
+            raise invalid_attribute_exc
+
+        value_error_callable = api_callable.create_api_call(
+            value_error_func, _CallSettings())
+        self.assertRaises(ValueError, value_error_callable, None)