Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions api_core/google/api_core/gapic_v1/client_info.py
Original file line number Diff line number Diff line change
@@ -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()
73 changes: 25 additions & 48 deletions api_core/google/api_core/gapic_v1/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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.

Expand Down Expand Up @@ -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."""
Expand All @@ -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.
Expand Down Expand Up @@ -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``
Expand All @@ -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(
Expand Down
73 changes: 73 additions & 0 deletions api_core/tests/unit/gapic/test_client_info.py
Original file line number Diff line number Diff line change
@@ -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())
42 changes: 16 additions & 26 deletions api_core/tests/unit/gapic/test_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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')
Expand Down