Repository: googleapis/google-cloud-python

Issue:
api_core: Add metadata option to wrap_method
To update GAPIC to use routing headers, `wrap_method` needs to accept more metadata than just the user agent. Example:
```python
metadata = [
    (b'x-google-request-params',
     b'name={}&book.read={}'.format(name, book.read))]

self._create_book(request, retry=retry, timeout=timeout, metadata=metadata)
```


Discussion:
@eoogbe not to be obnoxious, but I have the exact same question about this change as I had with the (same? similar?) feature proposed in https://github.com/googleapis/gax-python/pull/203

Why does this need to occur at *wrap* time as opposed to *invocation* time? Doing this at wrap time adds complexity to this layer but adding metadata at invocation time is a feature that already exists. It seems the rationale here is "routing headers" - won't those depend on the RPC request message which isn't known until invocation time?
@jonparrott I got a little confused at the api change. Can I specify metadata once the method is wrapped method?
Yep! The wrapper passes through all keyword args to the wrapped method so
you can just pass it in like you would when invoking any old grpc method. I
can throw up a code snippet tomorrow if needed.

On Wed, Nov 1, 2017, 9:39 PM Evawere Ogbe <notifications@github.com> wrote:

> @jonparrott <https://github.com/jonparrott> I got a little confused at
> the api change. Can I specify metadata once the method is wrapped method?
>
> —
> You are receiving this because you were mentioned.
>
>
> Reply to this email directly, view it on GitHub
> <https://github.com/GoogleCloudPlatform/google-cloud-python/pull/4322#issuecomment-341315861>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAPUc1wJ2FRwzsknpg4k1diuM4nuDxaOks5syUdqgaJpZM4QO7XJ>
> .
>


Resolving patch:
diff --git a/api_core/google/api_core/gapic_v1/method.py b/api_core/google/api_core/gapic_v1/method.py
--- a/api_core/google/api_core/gapic_v1/method.py
+++ b/api_core/google/api_core/gapic_v1/method.py
@@ -94,16 +94,17 @@ class _GapicCallable(object):
         timeout (google.api_core.timeout.Timeout): The default timeout
             for the callable. If ``None``, this callable will not specify
             a timeout argument to the low-level RPC method by default.
-        user_agent_metadata (Tuple[str, str]): The user agent metadata key and
-            value to provide to the RPC method. If ``None``, no additional
-            metadata will be passed to the RPC method.
+        metadata (Sequence[Tuple[str, str]]): Additional metadata that is
+            provided to the RPC method on every invocation. This is merged with
+            any metadata specified during invocation. If ``None``, no
+            additional metadata will be passed to the RPC method.
     """
 
-    def __init__(self, target, retry, timeout, user_agent_metadata=None):
+    def __init__(self, target, retry, timeout, metadata=None):
         self._target = target
         self._retry = retry
         self._timeout = timeout
-        self._user_agent_metadata = user_agent_metadata
+        self._metadata = metadata
 
     def __call__(self, *args, **kwargs):
         """Invoke the low-level RPC with retry, timeout, and metadata."""
@@ -126,9 +127,9 @@ def __call__(self, *args, **kwargs):
         wrapped_func = _apply_decorators(self._target, [retry, timeout_])
 
         # Add the user agent metadata to the call.
-        if self._user_agent_metadata is not None:
-            metadata = kwargs.get('metadata', [])
-            metadata.append(self._user_agent_metadata)
+        if self._metadata is not None:
+            metadata = list(kwargs.get('metadata', []))
+            metadata.extend(self._metadata)
             kwargs['metadata'] = metadata
 
         return wrapped_func(*args, **kwargs)
@@ -219,11 +220,11 @@ def get_topic(name, timeout=None):
     func = grpc_helpers.wrap_errors(func)
 
     if client_info is not None:
-        user_agent_metadata = client_info.to_grpc_metadata()
+        user_agent_metadata = [client_info.to_grpc_metadata()]
     else:
         user_agent_metadata = None
 
     return general_helpers.wraps(func)(
         _GapicCallable(
             func, default_retry, default_timeout,
-            user_agent_metadata=user_agent_metadata))
+            metadata=user_agent_metadata))


Test patch:
diff --git a/api_core/tests/unit/gapic/test_method.py b/api_core/tests/unit/gapic/test_method.py
--- a/api_core/tests/unit/gapic/test_method.py
+++ b/api_core/tests/unit/gapic/test_method.py
@@ -79,6 +79,21 @@ def test_wrap_method_with_custom_client_info():
     assert client_info.to_grpc_metadata() in metadata
 
 
+def test_invoke_wrapped_method_with_metadata():
+    method = mock.Mock(spec=['__call__'])
+
+    wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
+
+    wrapped_method(mock.sentinel.request, metadata=[('a', 'b')])
+
+    method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY)
+    metadata = method.call_args[1]['metadata']
+    # Metadata should have two items: the client info metadata and our custom
+    # metadata.
+    assert len(metadata) == 2
+    assert ('a', 'b') in metadata
+
+
 @mock.patch('time.sleep')
 def test_wrap_method_with_default_retry_and_timeout(unusued_sleep):
     method = mock.Mock(