From 77d227460355596f5c4c4b39d2bb21f51bd80dc0 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 19 Oct 2017 12:57:24 -0700 Subject: [PATCH 1/2] Add google.api_core.gapic_v2.client_info --- .../google/api_core/gapic_v1/client_info.py | 82 +++++++++++++++++++ api_core/google/api_core/gapic_v1/method.py | 73 ++++++----------- api_core/tests/unit/gapic/test_client_info.py | 73 +++++++++++++++++ api_core/tests/unit/gapic/test_method.py | 42 ++++------ 4 files changed, 196 insertions(+), 74 deletions(-) create mode 100644 api_core/google/api_core/gapic_v1/client_info.py create mode 100644 api_core/tests/unit/gapic/test_client_info.py diff --git a/api_core/google/api_core/gapic_v1/client_info.py b/api_core/google/api_core/gapic_v1/client_info.py new file mode 100644 index 000000000000..cfcdca999b99 --- /dev/null +++ b/api_core/google/api_core/gapic_v1/client_info.py @@ -0,0 +1,82 @@ +# 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 (st): The gRPC library version. + api_core_version (str): The google-api-core library version. + gapic_version (Optional[str]): The version of gapic used to generate + the client library. + client_library_version (Optional[str]): The version of the individual + product's 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/api_core/google/api_core/gapic_v1/method.py b/api_core/google/api_core/gapic_v1/method.py index 88e5a57f5728..3a689afbd137 100644 --- a/api_core/google/api_core/gapic_v1/method.py +++ b/api_core/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 @@ def _apply_decorators(func, decorators): 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 @@ 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. - 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 @@ def __call__(self, *args, **kwargs): # 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 @@ def get_topic(name, timeout=None): 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 @@ def get_topic(name, timeout=None): """ 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/api_core/tests/unit/gapic/test_client_info.py b/api_core/tests/unit/gapic/test_client_info.py new file mode 100644 index 000000000000..43b73d5eb7b7 --- /dev/null +++ b/api_core/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/api_core/tests/unit/gapic/test_method.py b/api_core/tests/unit/gapic/test_method.py index 35ac144dd28c..281463eb6c12 100644 --- a/api_core/tests/unit/gapic/test_method.py +++ b/api_core/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 _utcnow_monotonic(): 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') - - -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) + # Check that the default client info was specified in the metadata. 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] + 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_custom_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, metadata={'foo': 'bar'}) + method, client_info=None) 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) == 2 - assert ('foo', 'bar') in metadata + method.assert_called_once_with(1, 2, meep='moop') -def test_wrap_method_with_merged_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={'x-goog-api-client': 'foo/1.2.3'}) + 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) == 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') From 06b2f0dd15c04bf3ae041365539f7548705e199c Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Fri, 20 Oct 2017 11:04:46 -0700 Subject: [PATCH 2/2] Address review comments --- api_core/google/api_core/gapic_v1/client_info.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/api_core/google/api_core/gapic_v1/client_info.py b/api_core/google/api_core/gapic_v1/client_info.py index cfcdca999b99..d81d2836903c 100644 --- a/api_core/google/api_core/gapic_v1/client_info.py +++ b/api_core/google/api_core/gapic_v1/client_info.py @@ -38,12 +38,14 @@ class ClientInfo(object): Args: python_version (str): The Python interpreter version, for example, ``'2.7.13'``. - grpc_version (st): The gRPC library version. + grpc_version (str): The gRPC library version. api_core_version (str): The google-api-core library version. - gapic_version (Optional[str]): The version of gapic used to generate - the client library. - client_library_version (Optional[str]): The version of the individual - product's client library. + 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,