Skip to content

Commit cdfa25b

Browse files
author
Jon Wayne Parrott
authored
Add google.api_core.gapic_v2.client_info (#4225)
* Add google.api_core.gapic_v2.client_info * Address review comments
1 parent c2d2071 commit cdfa25b

File tree

4 files changed

+198
-74
lines changed

4 files changed

+198
-74
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Client information
16+
17+
This module is used by client libraries to send information about the calling
18+
client to services.
19+
"""
20+
21+
import platform
22+
23+
import pkg_resources
24+
25+
_PY_VERSION = platform.python_version()
26+
_GRPC_VERSION = pkg_resources.get_distribution('grpcio').version
27+
_API_CORE_VERSION = pkg_resources.get_distribution('google-api-core').version
28+
METRICS_METADATA_KEY = 'x-goog-api-client'
29+
30+
31+
class ClientInfo(object):
32+
"""Client information used to generate a user-agent for API calls.
33+
34+
This user-agent information is sent along with API calls to allow the
35+
receiving service to do analytics on which versions of Python and Google
36+
libraries are being used.
37+
38+
Args:
39+
python_version (str): The Python interpreter version, for example,
40+
``'2.7.13'``.
41+
grpc_version (str): The gRPC library version.
42+
api_core_version (str): The google-api-core library version.
43+
gapic_version (Optional[str]): The sversion of gapic-generated client
44+
library, if the library was generated by gapic.
45+
client_library_version (Optional[str]): The version of the client
46+
library, generally used if the client library was not generated
47+
by gapic or if additional functionality was built on top of
48+
a gapic client library.
49+
"""
50+
def __init__(
51+
self,
52+
python_version=_PY_VERSION,
53+
grpc_version=_GRPC_VERSION,
54+
api_core_version=_API_CORE_VERSION,
55+
gapic_version=None,
56+
client_library_version=None):
57+
self.python_version = python_version
58+
self.grpc_version = grpc_version
59+
self.api_core_version = api_core_version
60+
self.gapic_version = gapic_version
61+
self.client_library_version = client_library_version
62+
63+
def to_user_agent(self):
64+
"""Returns the user-agent string for this client info."""
65+
# Note: the order here is important as the internal metrics system
66+
# expects these items to be in specific locations.
67+
ua = 'gl-python/{python_version} '
68+
69+
if self.client_library_version is not None:
70+
ua += 'gccl/{client_library_version} '
71+
72+
if self.gapic_version is not None:
73+
ua += 'gapic/{gapic_version} '
74+
75+
ua += 'gax/{api_core_version} grpc/{grpc_version}'
76+
77+
return ua.format(**self.__dict__)
78+
79+
def to_grpc_metadata(self):
80+
"""Returns the gRPC metadata for this client info."""
81+
return (METRICS_METADATA_KEY, self.to_user_agent())
82+
83+
84+
DEFAULT_CLIENT_INFO = ClientInfo()

google/api_core/gapic_v1/method.py

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,15 @@
1919
"""
2020

2121
import functools
22-
import platform
2322

24-
import pkg_resources
2523
import six
2624

2725
from google.api_core import general_helpers
2826
from google.api_core import grpc_helpers
2927
from google.api_core import page_iterator
3028
from google.api_core import timeout
29+
from google.api_core.gapic_v1 import client_info
3130

32-
_PY_VERSION = platform.python_version()
33-
_GRPC_VERSION = pkg_resources.get_distribution('grpcio').version
34-
_API_CORE_VERSION = pkg_resources.get_distribution('google-api-core').version
3531
METRICS_METADATA_KEY = 'x-goog-api-client'
3632
USE_DEFAULT_METADATA = object()
3733
DEFAULT = object()
@@ -57,28 +53,6 @@ def _apply_decorators(func, decorators):
5753
return func
5854

5955

60-
def _prepare_metadata(metadata):
61-
"""Transforms metadata to gRPC format and adds global metrics.
62-
63-
Args:
64-
metadata (Mapping[str, str]): Any current metadata.
65-
66-
Returns:
67-
Sequence[Tuple(str, str)]: The gRPC-friendly metadata keys and values.
68-
"""
69-
client_metadata = 'api-core/{} gl-python/{} grpc/{}'.format(
70-
_API_CORE_VERSION, _PY_VERSION, _GRPC_VERSION)
71-
72-
# Merge this with any existing metric metadata.
73-
if METRICS_METADATA_KEY in metadata:
74-
client_metadata = '{} {}'.format(
75-
client_metadata, metadata[METRICS_METADATA_KEY])
76-
77-
metadata[METRICS_METADATA_KEY] = client_metadata
78-
79-
return list(metadata.items())
80-
81-
8256
def _determine_timeout(default_timeout, specified_timeout, retry):
8357
"""Determines how timeout should be applied to a wrapped method.
8458
@@ -125,16 +99,16 @@ class _GapicCallable(object):
12599
timeout (google.api_core.timeout.Timeout): The default timeout
126100
for the callable. If ``None``, this callable will not specify
127101
a timeout argument to the low-level RPC method by default.
128-
metadata (Optional[Sequence[Tuple[str, str]]]): gRPC call metadata
129-
that's passed to the low-level RPC method. If ``None``, no metadata
130-
will be passed to the low-level RPC method.
102+
user_agent_metadata (Tuple[str, str]): The user agent metadata key and
103+
value to provide to the RPC method. If ``None``, no additional
104+
metadata will be passed to the RPC method.
131105
"""
132106

133-
def __init__(self, target, retry, timeout, metadata):
107+
def __init__(self, target, retry, timeout, user_agent_metadata=None):
134108
self._target = target
135109
self._retry = retry
136110
self._timeout = timeout
137-
self._metadata = metadata
111+
self._user_agent_metadata = user_agent_metadata
138112

139113
def __call__(self, *args, **kwargs):
140114
"""Invoke the low-level RPC with retry, timeout, and metadata."""
@@ -156,17 +130,18 @@ def __call__(self, *args, **kwargs):
156130
# Apply all applicable decorators.
157131
wrapped_func = _apply_decorators(self._target, [retry, timeout_])
158132

159-
# Set the metadata for the call using the metadata calculated by
160-
# _prepare_metadata.
161-
if self._metadata is not None:
162-
kwargs['metadata'] = self._metadata
133+
# Add the user agent metadata to the call.
134+
if self._user_agent_metadata is not None:
135+
metadata = kwargs.get('metadata', [])
136+
metadata.append(self._user_agent_metadata)
137+
kwargs['metadata'] = metadata
163138

164139
return wrapped_func(*args, **kwargs)
165140

166141

167142
def wrap_method(
168143
func, default_retry=None, default_timeout=None,
169-
metadata=USE_DEFAULT_METADATA):
144+
client_info=client_info.DEFAULT_CLIENT_INFO):
170145
"""Wrap an RPC method with common behavior.
171146
172147
This applies common error wrapping, retry, and timeout behavior a function.
@@ -234,11 +209,12 @@ def get_topic(name, timeout=None):
234209
default_timeout (Optional[google.api_core.Timeout]): The default
235210
timeout strategy. Can also be specified as an int or float. If
236211
``None``, the method will not have timeout specified by default.
237-
metadata (Optional(Mapping[str, str])): A dict of metadata keys and
238-
values. This will be augmented with common ``x-google-api-client``
239-
metadata. If ``None``, metadata will not be passed to the function
240-
at all, if :attr:`USE_DEFAULT_METADATA` (the default) then only the
241-
common metadata will be provided.
212+
client_info
213+
(Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
214+
Client information used to create a user-agent string that's
215+
passed as gRPC metadata to the method. If unspecified, then
216+
a sane default will be used. If ``None``, then no user agent
217+
metadata will be provided to the RPC method.
242218
243219
Returns:
244220
Callable: A new callable that takes optional ``retry`` and ``timeout``
@@ -247,14 +223,15 @@ def get_topic(name, timeout=None):
247223
"""
248224
func = grpc_helpers.wrap_errors(func)
249225

250-
if metadata is USE_DEFAULT_METADATA:
251-
metadata = {}
252-
253-
if metadata is not None:
254-
metadata = _prepare_metadata(metadata)
226+
if client_info is not None:
227+
user_agent_metadata = client_info.to_grpc_metadata()
228+
else:
229+
user_agent_metadata = None
255230

256231
return general_helpers.wraps(func)(
257-
_GapicCallable(func, default_retry, default_timeout, metadata))
232+
_GapicCallable(
233+
func, default_retry, default_timeout,
234+
user_agent_metadata=user_agent_metadata))
258235

259236

260237
def wrap_with_paging(

tests/unit/gapic/test_client_info.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from google.api_core.gapic_v1 import client_info
17+
18+
19+
def test_constructor_defaults():
20+
info = client_info.ClientInfo()
21+
22+
assert info.python_version is not None
23+
assert info.grpc_version is not None
24+
assert info.api_core_version is not None
25+
assert info.gapic_version is None
26+
assert info.client_library_version is None
27+
28+
29+
def test_constructor_options():
30+
info = client_info.ClientInfo(
31+
python_version='1',
32+
grpc_version='2',
33+
api_core_version='3',
34+
gapic_version='4',
35+
client_library_version='5')
36+
37+
assert info.python_version == '1'
38+
assert info.grpc_version == '2'
39+
assert info.api_core_version == '3'
40+
assert info.gapic_version == '4'
41+
assert info.client_library_version == '5'
42+
43+
44+
def test_to_user_agent_minimal():
45+
info = client_info.ClientInfo(
46+
python_version='1',
47+
grpc_version='2',
48+
api_core_version='3')
49+
50+
user_agent = info.to_user_agent()
51+
52+
assert user_agent == 'gl-python/1 gax/3 grpc/2'
53+
54+
55+
def test_to_user_agent_full():
56+
info = client_info.ClientInfo(
57+
python_version='1',
58+
grpc_version='2',
59+
api_core_version='3',
60+
gapic_version='4',
61+
client_library_version='5')
62+
63+
user_agent = info.to_user_agent()
64+
65+
assert user_agent == 'gl-python/1 gccl/5 gapic/4 gax/3 grpc/2'
66+
67+
68+
def test_to_grpc_metadata():
69+
info = client_info.ClientInfo()
70+
71+
metadata = info.to_grpc_metadata()
72+
73+
assert metadata == (client_info.METRICS_METADATA_KEY, info.to_user_agent())

tests/unit/gapic/test_method.py

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from google.api_core import exceptions
2020
from google.api_core import retry
2121
from google.api_core import timeout
22+
import google.api_core.gapic_v1.client_info
2223
import google.api_core.gapic_v1.method
2324
import google.api_core.page_iterator
2425

@@ -34,59 +35,48 @@ def _utcnow_monotonic():
3435
def test_wrap_method_basic():
3536
method = mock.Mock(spec=['__call__'], return_value=42)
3637

37-
wrapped_method = google.api_core.gapic_v1.method.wrap_method(
38-
method, metadata=None)
38+
wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
3939

4040
result = wrapped_method(1, 2, meep='moop')
4141

4242
assert result == 42
43-
method.assert_called_once_with(1, 2, meep='moop')
44-
45-
46-
def test_wrap_method_with_default_metadata():
47-
method = mock.Mock(spec=['__call__'])
48-
49-
wrapped_method = google.api_core.gapic_v1.method.wrap_method(method)
50-
51-
wrapped_method(1, 2, meep='moop')
52-
5343
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
5444

45+
# Check that the default client info was specified in the metadata.
5546
metadata = method.call_args[1]['metadata']
5647
assert len(metadata) == 1
57-
assert metadata[0][0] == 'x-goog-api-client'
58-
assert 'api-core' in metadata[0][1]
48+
client_info = google.api_core.gapic_v1.client_info.DEFAULT_CLIENT_INFO
49+
user_agent_metadata = client_info.to_grpc_metadata()
50+
assert user_agent_metadata in metadata
5951

6052

61-
def test_wrap_method_with_custom_metadata():
53+
def test_wrap_method_with_no_client_info():
6254
method = mock.Mock(spec=['__call__'])
6355

6456
wrapped_method = google.api_core.gapic_v1.method.wrap_method(
65-
method, metadata={'foo': 'bar'})
57+
method, client_info=None)
6658

6759
wrapped_method(1, 2, meep='moop')
6860

69-
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
70-
71-
metadata = method.call_args[1]['metadata']
72-
assert len(metadata) == 2
73-
assert ('foo', 'bar') in metadata
61+
method.assert_called_once_with(1, 2, meep='moop')
7462

7563

76-
def test_wrap_method_with_merged_metadata():
64+
def test_wrap_method_with_custom_client_info():
65+
client_info = google.api_core.gapic_v1.client_info.ClientInfo(
66+
python_version=1, grpc_version=2, api_core_version=3, gapic_version=4,
67+
client_library_version=5)
7768
method = mock.Mock(spec=['__call__'])
7869

7970
wrapped_method = google.api_core.gapic_v1.method.wrap_method(
80-
method, metadata={'x-goog-api-client': 'foo/1.2.3'})
71+
method, client_info=client_info)
8172

8273
wrapped_method(1, 2, meep='moop')
8374

8475
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
8576

77+
# Check that the custom client info was specified in the metadata.
8678
metadata = method.call_args[1]['metadata']
87-
assert len(metadata) == 1
88-
assert metadata[0][0] == 'x-goog-api-client'
89-
assert metadata[0][1].endswith(' foo/1.2.3')
79+
assert client_info.to_grpc_metadata() in metadata
9080

9181

9282
@mock.patch('time.sleep')

0 commit comments

Comments
 (0)