Add google.api_core.gapic_v2.client_info (#4225)
* Add google.api_core.gapic_v2.client_info
* Address review comments
diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py
new file mode 100644
index 0000000..d81d283
--- /dev/null
+++ b/google/api_core/gapic_v1/client_info.py
@@ -0,0 +1,84 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+"""Client information
+
+This module is used by client libraries to send information about the calling
+client to services.
+"""
+
+import platform
+
+import pkg_resources
+
+_PY_VERSION = platform.python_version()
+_GRPC_VERSION = pkg_resources.get_distribution('grpcio').version
+_API_CORE_VERSION = pkg_resources.get_distribution('google-api-core').version
+METRICS_METADATA_KEY = 'x-goog-api-client'
+
+
+class ClientInfo(object):
+ """Client information used to generate a user-agent for API calls.
+
+ This user-agent information is sent along with API calls to allow the
+ receiving service to do analytics on which versions of Python and Google
+ libraries are being used.
+
+ Args:
+ python_version (str): The Python interpreter version, for example,
+ ``'2.7.13'``.
+ grpc_version (str): The gRPC library version.
+ api_core_version (str): The google-api-core library version.
+ gapic_version (Optional[str]): The sversion of gapic-generated client
+ library, if the library was generated by gapic.
+ client_library_version (Optional[str]): The version of the client
+ library, generally used if the client library was not generated
+ by gapic or if additional functionality was built on top of
+ a gapic client library.
+ """
+ def __init__(
+ self,
+ python_version=_PY_VERSION,
+ grpc_version=_GRPC_VERSION,
+ api_core_version=_API_CORE_VERSION,
+ gapic_version=None,
+ client_library_version=None):
+ self.python_version = python_version
+ self.grpc_version = grpc_version
+ self.api_core_version = api_core_version
+ self.gapic_version = gapic_version
+ self.client_library_version = client_library_version
+
+ def to_user_agent(self):
+ """Returns the user-agent string for this client info."""
+ # Note: the order here is important as the internal metrics system
+ # expects these items to be in specific locations.
+ ua = 'gl-python/{python_version} '
+
+ if self.client_library_version is not None:
+ ua += 'gccl/{client_library_version} '
+
+ if self.gapic_version is not None:
+ ua += 'gapic/{gapic_version} '
+
+ ua += 'gax/{api_core_version} grpc/{grpc_version}'
+
+ return ua.format(**self.__dict__)
+
+ def to_grpc_metadata(self):
+ """Returns the gRPC metadata for this client info."""
+ return (METRICS_METADATA_KEY, self.to_user_agent())
+
+
+DEFAULT_CLIENT_INFO = ClientInfo()
diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py
index 88e5a57..3a689af 100644
--- a/google/api_core/gapic_v1/method.py
+++ b/google/api_core/gapic_v1/method.py
@@ -19,19 +19,15 @@
"""
import functools
-import platform
-import pkg_resources
import six
from google.api_core import general_helpers
from google.api_core import grpc_helpers
from google.api_core import page_iterator
from google.api_core import timeout
+from google.api_core.gapic_v1 import client_info
-_PY_VERSION = platform.python_version()
-_GRPC_VERSION = pkg_resources.get_distribution('grpcio').version
-_API_CORE_VERSION = pkg_resources.get_distribution('google-api-core').version
METRICS_METADATA_KEY = 'x-goog-api-client'
USE_DEFAULT_METADATA = object()
DEFAULT = object()
@@ -57,28 +53,6 @@
return func
-def _prepare_metadata(metadata):
- """Transforms metadata to gRPC format and adds global metrics.
-
- Args:
- metadata (Mapping[str, str]): Any current metadata.
-
- Returns:
- Sequence[Tuple(str, str)]: The gRPC-friendly metadata keys and values.
- """
- client_metadata = 'api-core/{} gl-python/{} grpc/{}'.format(
- _API_CORE_VERSION, _PY_VERSION, _GRPC_VERSION)
-
- # Merge this with any existing metric metadata.
- if METRICS_METADATA_KEY in metadata:
- client_metadata = '{} {}'.format(
- client_metadata, metadata[METRICS_METADATA_KEY])
-
- metadata[METRICS_METADATA_KEY] = client_metadata
-
- return list(metadata.items())
-
-
def _determine_timeout(default_timeout, specified_timeout, retry):
"""Determines how timeout should be applied to a wrapped method.
@@ -125,16 +99,16 @@
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.
- metadata (Optional[Sequence[Tuple[str, str]]]): gRPC call metadata
- that's passed to the low-level RPC method. If ``None``, no metadata
- will be passed to the low-level RPC method.
+ 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.
"""
- def __init__(self, target, retry, timeout, metadata):
+ def __init__(self, target, retry, timeout, user_agent_metadata=None):
self._target = target
self._retry = retry
self._timeout = timeout
- self._metadata = metadata
+ self._user_agent_metadata = user_agent_metadata
def __call__(self, *args, **kwargs):
"""Invoke the low-level RPC with retry, timeout, and metadata."""
@@ -156,17 +130,18 @@
# Apply all applicable decorators.
wrapped_func = _apply_decorators(self._target, [retry, timeout_])
- # Set the metadata for the call using the metadata calculated by
- # _prepare_metadata.
- if self._metadata is not None:
- kwargs['metadata'] = self._metadata
+ # 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)
+ kwargs['metadata'] = metadata
return wrapped_func(*args, **kwargs)
def wrap_method(
func, default_retry=None, default_timeout=None,
- metadata=USE_DEFAULT_METADATA):
+ client_info=client_info.DEFAULT_CLIENT_INFO):
"""Wrap an RPC method with common behavior.
This applies common error wrapping, retry, and timeout behavior a function.
@@ -234,11 +209,12 @@
default_timeout (Optional[google.api_core.Timeout]): The default
timeout strategy. Can also be specified as an int or float. If
``None``, the method will not have timeout specified by default.
- metadata (Optional(Mapping[str, str])): A dict of metadata keys and
- values. This will be augmented with common ``x-google-api-client``
- metadata. If ``None``, metadata will not be passed to the function
- at all, if :attr:`USE_DEFAULT_METADATA` (the default) then only the
- common metadata will be provided.
+ client_info
+ (Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
+ Client information used to create a user-agent string that's
+ passed as gRPC metadata to the method. If unspecified, then
+ a sane default will be used. If ``None``, then no user agent
+ metadata will be provided to the RPC method.
Returns:
Callable: A new callable that takes optional ``retry`` and ``timeout``
@@ -247,14 +223,15 @@
"""
func = grpc_helpers.wrap_errors(func)
- if metadata is USE_DEFAULT_METADATA:
- metadata = {}
-
- if metadata is not None:
- metadata = _prepare_metadata(metadata)
+ if client_info is not None:
+ user_agent_metadata = client_info.to_grpc_metadata()
+ else:
+ user_agent_metadata = None
return general_helpers.wraps(func)(
- _GapicCallable(func, default_retry, default_timeout, metadata))
+ _GapicCallable(
+ func, default_retry, default_timeout,
+ user_agent_metadata=user_agent_metadata))
def wrap_with_paging(
diff --git a/tests/unit/gapic/test_client_info.py b/tests/unit/gapic/test_client_info.py
new file mode 100644
index 0000000..43b73d5
--- /dev/null
+++ b/tests/unit/gapic/test_client_info.py
@@ -0,0 +1,73 @@
+# Copyright 2017 Google Inc.
+#
+# 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.
+
+
+from google.api_core.gapic_v1 import client_info
+
+
+def test_constructor_defaults():
+ info = client_info.ClientInfo()
+
+ assert info.python_version is not None
+ assert info.grpc_version is not None
+ assert info.api_core_version is not None
+ assert info.gapic_version is None
+ assert info.client_library_version is None
+
+
+def test_constructor_options():
+ info = client_info.ClientInfo(
+ python_version='1',
+ grpc_version='2',
+ api_core_version='3',
+ gapic_version='4',
+ client_library_version='5')
+
+ assert info.python_version == '1'
+ assert info.grpc_version == '2'
+ assert info.api_core_version == '3'
+ assert info.gapic_version == '4'
+ assert info.client_library_version == '5'
+
+
+def test_to_user_agent_minimal():
+ info = client_info.ClientInfo(
+ python_version='1',
+ grpc_version='2',
+ api_core_version='3')
+
+ user_agent = info.to_user_agent()
+
+ assert user_agent == 'gl-python/1 gax/3 grpc/2'
+
+
+def test_to_user_agent_full():
+ info = client_info.ClientInfo(
+ python_version='1',
+ grpc_version='2',
+ api_core_version='3',
+ gapic_version='4',
+ client_library_version='5')
+
+ user_agent = info.to_user_agent()
+
+ assert user_agent == 'gl-python/1 gccl/5 gapic/4 gax/3 grpc/2'
+
+
+def test_to_grpc_metadata():
+ info = client_info.ClientInfo()
+
+ metadata = info.to_grpc_metadata()
+
+ assert metadata == (client_info.METRICS_METADATA_KEY, info.to_user_agent())
diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py
index 35ac144..281463e 100644
--- a/tests/unit/gapic/test_method.py
+++ b/tests/unit/gapic/test_method.py
@@ -19,6 +19,7 @@
from google.api_core import exceptions
from google.api_core import retry
from google.api_core import timeout
+import google.api_core.gapic_v1.client_info
import google.api_core.gapic_v1.method
import google.api_core.page_iterator
@@ -34,59 +35,48 @@
def test_wrap_method_basic():
method = mock.Mock(spec=['__call__'], return_value=42)
- wrapped_method = google.api_core.gapic_v1.method.wrap_method(
- method, metadata=None)
+ wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
result = wrapped_method(1, 2, meep='moop')
assert result == 42
+ method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
+
+ # Check that the default client info was specified in the metadata.
+ metadata = method.call_args[1]['metadata']
+ assert len(metadata) == 1
+ client_info = google.api_core.gapic_v1.client_info.DEFAULT_CLIENT_INFO
+ user_agent_metadata = client_info.to_grpc_metadata()
+ assert user_agent_metadata in metadata
+
+
+def test_wrap_method_with_no_client_info():
+ method = mock.Mock(spec=['__call__'])
+
+ wrapped_method = google.api_core.gapic_v1.method.wrap_method(
+ method, client_info=None)
+
+ wrapped_method(1, 2, meep='moop')
+
method.assert_called_once_with(1, 2, meep='moop')
-def test_wrap_method_with_default_metadata():
- method = mock.Mock(spec=['__call__'])
-
- wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
-
- wrapped_method(1, 2, meep='moop')
-
- method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
-
- metadata = method.call_args[1]['metadata']
- assert len(metadata) == 1
- assert metadata[0][0] == 'x-goog-api-client'
- assert 'api-core' in metadata[0][1]
-
-
-def test_wrap_method_with_custom_metadata():
+def test_wrap_method_with_custom_client_info():
+ client_info = google.api_core.gapic_v1.client_info.ClientInfo(
+ python_version=1, grpc_version=2, api_core_version=3, gapic_version=4,
+ client_library_version=5)
method = mock.Mock(spec=['__call__'])
wrapped_method = google.api_core.gapic_v1.method.wrap_method(
- method, metadata={'foo': 'bar'})
+ method, client_info=client_info)
wrapped_method(1, 2, meep='moop')
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
+ # Check that the custom client info was specified in the metadata.
metadata = method.call_args[1]['metadata']
- assert len(metadata) == 2
- assert ('foo', 'bar') in metadata
-
-
-def test_wrap_method_with_merged_metadata():
- method = mock.Mock(spec=['__call__'])
-
- wrapped_method = google.api_core.gapic_v1.method.wrap_method(
- method, metadata={'x-goog-api-client': 'foo/1.2.3'})
-
- wrapped_method(1, 2, meep='moop')
-
- method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
-
- metadata = method.call_args[1]['metadata']
- assert len(metadata) == 1
- assert metadata[0][0] == 'x-goog-api-client'
- assert metadata[0][1].endswith(' foo/1.2.3')
+ assert client_info.to_grpc_metadata() in metadata
@mock.patch('time.sleep')