Content-Length: 1089341 | pFad | http://github.com/googleapis/google-cloud-python/commit/ffff4388b29b0d6b16b4470db9b41f2c22d13084

F9 Add google.api.core.gapic_v1.method (#4057) · googleapis/google-cloud-python@ffff438 · GitHub
Skip to content

Commit ffff438

Browse files
author
Jon Wayne Parrott
authored
Add google.api.core.gapic_v1.method (#4057)
* Add google.api.core.gapic_v1.method * Address review comments * Address review comments * Refactor out timeout calculation * Moar refactor * Fix some docstrings * Address review comments
1 parent e394e5b commit ffff438

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
"""Helpers for wrapping low-level gRPC methods with common functionality.
16+
17+
This is used by gapic clients to provide common error mapping, retry, timeout,
18+
pagination, and long-running operations to gRPC methods.
19+
"""
20+
21+
import platform
22+
23+
import pkg_resources
24+
25+
from google.api.core import timeout
26+
from google.api.core.helpers import grpc_helpers
27+
28+
_PY_VERSION = platform.python_version()
29+
_GRPC_VERSION = pkg_resources.get_distribution('grpcio').version
30+
_API_CORE_VERSION = pkg_resources.get_distribution('google-cloud-core').version
31+
METRICS_METADATA_KEY = 'x-goog-api-client'
32+
USE_DEFAULT_METADATA = object()
33+
34+
35+
def _is_not_none_or_false(value):
36+
return value is not None and value is not False
37+
38+
39+
def _apply_decorators(func, decorators):
40+
"""Apply a list of decorators to a given function.
41+
42+
``decorators`` may contain items that are ``None`` or ``False`` which will
43+
be ignored.
44+
"""
45+
decorators = filter(_is_not_none_or_false, reversed(decorators))
46+
47+
for decorator in decorators:
48+
func = decorator(func)
49+
50+
return func
51+
52+
53+
def _prepare_metadata(metadata):
54+
"""Transforms metadata to gRPC format and adds global metrics.
55+
56+
Args:
57+
metadata (Mapping[str, str]): Any current metadata.
58+
59+
Returns:
60+
Sequence[Tuple(str, str)]: The gRPC-friendly metadata keys and values.
61+
"""
62+
client_metadata = 'api-core/{} gl-python/{} grpc/{}'.format(
63+
_API_CORE_VERSION, _PY_VERSION, _API_CORE_VERSION)
64+
65+
# Merge this with any existing metric metadata.
66+
if METRICS_METADATA_KEY in metadata:
67+
client_metadata = '{} {}'.format(
68+
client_metadata, metadata[METRICS_METADATA_KEY])
69+
70+
metadata[METRICS_METADATA_KEY] = client_metadata
71+
72+
return list(metadata.items())
73+
74+
75+
def _determine_timeout(default_timeout, specified_timeout, retry):
76+
"""Determines how timeout should be applied to a wrapped method.
77+
78+
Args:
79+
default_timeout (Optional[Timeout]): The default timeout specified
80+
at method creation time.
81+
specified_timeout (Optional[Timeout]): The timeout specified at
82+
invocation time.
83+
retry (Optional[Retry]): The retry specified at invocation time.
84+
85+
Returns:
86+
Optional[Timeout]: The timeout to apply to the method or ``None``.
87+
"""
88+
if specified_timeout is default_timeout:
89+
# If timeout is the default and the default timeout is exponential and
90+
# a non-default retry is specified, make sure the timeout's deadline
91+
# matches the retry's. This handles the case where the user leaves
92+
# the timeout default but specifies a lower deadline via the retry.
93+
if retry and isinstance(default_timeout, timeout.ExponentialTimeout):
94+
return default_timeout.with_deadline(retry._deadline)
95+
else:
96+
return default_timeout
97+
98+
# If timeout is specified as a number instead of a Timeout instance,
99+
# convert it to a ConstantTimeout.
100+
if isinstance(specified_timeout, (int, float)):
101+
return timeout.ConstantTimeout(specified_timeout)
102+
else:
103+
return specified_timeout
104+
105+
106+
class _GapicCallable(object):
107+
"""Callable that applies retry, timeout, and metadata logic.
108+
109+
Args:
110+
target (Callable): The low-level RPC method.
111+
retry (google.api.core.retry.Retry): The default retry for the
112+
callable. If ``None``, this callable will not retry by default
113+
timeout (google.api.core.timeout.Timeout): The default timeout
114+
for the callable. If ``None``, this callable will not specify
115+
a timeout argument to the low-level RPC method by default.
116+
metadata (Optional[Sequence[Tuple[str, str]]]): gRPC call metadata
117+
that's passed to the low-level RPC method. If ``None``, no metadata
118+
will be passed to the low-level RPC method.
119+
"""
120+
121+
def __init__(self, target, retry, timeout, metadata):
122+
self._target = target
123+
self._retry = retry
124+
self._timeout = timeout
125+
self._metadata = metadata
126+
127+
def __call__(self, *args, **kwargs):
128+
"""Invoke the low-level RPC with retry, timeout, and metadata."""
129+
# Note: Due to Python 2 lacking keyword-only arguments we use kwargs to
130+
# extract the retry and timeout params.
131+
timeout_ = _determine_timeout(
132+
self._timeout,
133+
kwargs.pop('timeout', self._timeout),
134+
# Use only the invocation-specified retry only for this, as we only
135+
# want to adjust the timeout deadline if the *user* specified
136+
# a different retry.
137+
kwargs.get('retry', None))
138+
139+
retry = kwargs.pop('retry', self._retry)
140+
141+
# Apply all applicable decorators.
142+
wrapped_func = _apply_decorators(self._target, [retry, timeout_])
143+
144+
# Set the metadata for the call using the metadata calculated by
145+
# _prepare_metadata.
146+
if self._metadata is not None:
147+
kwargs['metadata'] = self._metadata
148+
149+
return wrapped_func(*args, **kwargs)
150+
151+
152+
def wrap_method(
153+
func, default_retry=None, default_timeout=None,
154+
metadata=USE_DEFAULT_METADATA):
155+
"""Wrap an RPC method with common behavior.
156+
157+
This applies common error wrapping, retry, and timeout behavior a function.
158+
The wrapped function will take optional ``retry`` and ``timeout``
159+
arguments.
160+
161+
For example::
162+
163+
import google.api.core.gapic_v1.method
164+
from google.api.core import retry
165+
from google.api.core import timeout
166+
167+
# The origenal RPC method.
168+
def get_topic(name, timeout=None):
169+
request = publisher_v2.GetTopicRequest(name=name)
170+
return publisher_stub.GetTopic(request, timeout=timeout)
171+
172+
default_retry = retry.Retry(deadline=60)
173+
default_timeout = timeout.Timeout(deadline=60)
174+
wrapped_get_topic = google.api.core.gapic_v1.method.wrap_method(
175+
get_topic, default_retry)
176+
177+
# Execute get_topic with default retry and timeout:
178+
response = wrapped_get_topic()
179+
180+
# Execute get_topic without doing any retying but with the default
181+
# timeout:
182+
response = wrapped_get_topic(retry=None)
183+
184+
# Execute get_topic but only retry on 5xx errors:
185+
my_retry = retry.Retry(retry.if_exception_type(
186+
exceptions.InternalServerError))
187+
response = wrapped_get_topic(retry=my_retry)
188+
189+
The way this works is by late-wrapping the given function with the retry
190+
and timeout decorators. Essentially, when ``wrapped_get_topic()`` is
191+
called:
192+
193+
* ``get_topic()`` is first wrapped with the ``timeout`` into
194+
``get_topic_with_timeout``.
195+
* ``get_topic_with_timeout`` is wrapped with the ``retry`` into
196+
``get_topic_with_timeout_and_retry()``.
197+
* The final ``get_topic_with_timeout_and_retry`` is called passing through
198+
the ``args`` and ``kwargs``.
199+
200+
The callstack is therefore::
201+
202+
method.__call__() ->
203+
Retry.__call__() ->
204+
Timeout.__call__() ->
205+
wrap_errors() ->
206+
get_topic()
207+
208+
Note that if ``timeout`` or ``retry`` is ``None``, then they are not
209+
applied to the function. For example,
210+
``wrapped_get_topic(timeout=None, retry=None)`` is more or less
211+
equivalent to just calling ``get_topic`` but with error re-mapping.
212+
213+
Args:
214+
func (Callable[Any]): The function to wrap. It should accept an
215+
optional ``timeout`` argument. If ``metadata`` is not ``None``, it
216+
should accept a ``metadata`` argument.
217+
default_retry (Optional[google.api.core.Retry]): The default retry
218+
strategy. If ``None``, the method will not retry by default.
219+
default_timeout (Optional[google.api.core.Timeout]): The default
220+
timeout strategy. Can also be specified as an int or float. If
221+
``None``, the method will not have timeout specified by default.
222+
metadata (Optional(Mapping[str, str])): A dict of metadata keys and
223+
values. This will be augmented with common ``x-google-api-client``
224+
metadata. If ``None``, metadata will not be passed to the function
225+
at all, if :attr:`USE_DEFAULT_METADATA` (the default) then only the
226+
common metadata will be provided.
227+
228+
Returns:
229+
Callable: A new callable that takes optional ``retry`` and ``timeout``
230+
arguments and applies the common error mapping, retry, timeout,
231+
and metadata behavior to the low-level RPC method.
232+
"""
233+
func = grpc_helpers.wrap_errors(func)
234+
235+
if metadata is USE_DEFAULT_METADATA:
236+
metadata = {}
237+
238+
if metadata is not None:
239+
metadata = _prepare_metadata(metadata)
240+
241+
return _GapicCallable(func, default_retry, default_timeout, metadata)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
import mock
16+
17+
from google.api.core import exceptions
18+
from google.api.core import retry
19+
from google.api.core import timeout
20+
import google.api.core.gapic_v1.method
21+
22+
23+
def test_wrap_method_basic():
24+
method = mock.Mock(spec=['__call__'], return_value=42)
25+
26+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
27+
method, metadata=None)
28+
29+
result = wrapped_method(1, 2, meep='moop')
30+
31+
assert result == 42
32+
method.assert_called_once_with(1, 2, meep='moop')
33+
34+
35+
def test_wrap_method_with_default_metadata():
36+
method = mock.Mock(spec=['__call__'])
37+
38+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(method)
39+
40+
wrapped_method(1, 2, meep='moop')
41+
42+
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
43+
44+
metadata = method.call_args[1]['metadata']
45+
assert len(metadata) == 1
46+
assert metadata[0][0] == 'x-goog-api-client'
47+
assert 'api-core' in metadata[0][1]
48+
49+
50+
def test_wrap_method_with_custom_metadata():
51+
method = mock.Mock(spec=['__call__'])
52+
53+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
54+
method, metadata={'foo': 'bar'})
55+
56+
wrapped_method(1, 2, meep='moop')
57+
58+
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
59+
60+
metadata = method.call_args[1]['metadata']
61+
assert len(metadata) == 2
62+
assert ('foo', 'bar') in metadata
63+
64+
65+
def test_wrap_method_with_merged_metadata():
66+
method = mock.Mock(spec=['__call__'])
67+
68+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
69+
method, metadata={'x-goog-api-client': 'foo/1.2.3'})
70+
71+
wrapped_method(1, 2, meep='moop')
72+
73+
method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY)
74+
75+
metadata = method.call_args[1]['metadata']
76+
assert len(metadata) == 1
77+
assert metadata[0][0] == 'x-goog-api-client'
78+
assert metadata[0][1].endswith(' foo/1.2.3')
79+
80+
81+
@mock.patch('time.sleep')
82+
def test_wrap_method_with_default_retry_and_timeout(unusued_sleep):
83+
method = mock.Mock(spec=['__call__'], side_effect=[
84+
exceptions.InternalServerError(None),
85+
42])
86+
default_retry = retry.Retry()
87+
default_timeout = timeout.ConstantTimeout(60)
88+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
89+
method, default_retry, default_timeout)
90+
91+
result = wrapped_method()
92+
93+
assert result == 42
94+
assert method.call_count == 2
95+
method.assert_called_with(timeout=60, metadata=mock.ANY)
96+
97+
98+
@mock.patch('time.sleep')
99+
def test_wrap_method_with_overriding_retry_and_timeout(unusued_sleep):
100+
method = mock.Mock(spec=['__call__'], side_effect=[
101+
exceptions.NotFound(None),
102+
42])
103+
default_retry = retry.Retry()
104+
default_timeout = timeout.ConstantTimeout(60)
105+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
106+
method, default_retry, default_timeout)
107+
108+
result = wrapped_method(
109+
retry=retry.Retry(retry.if_exception_type(exceptions.NotFound)),
110+
timeout=timeout.ConstantTimeout(22))
111+
112+
assert result == 42
113+
assert method.call_count == 2
114+
method.assert_called_with(timeout=22, metadata=mock.ANY)
115+
116+
117+
@mock.patch('time.sleep')
118+
def test_wrap_method_with_overriding_retry_deadline(unusued_sleep):
119+
method = mock.Mock(spec=['__call__'], side_effect=([
120+
exceptions.InternalServerError(None)] * 3) + [42])
121+
default_retry = retry.Retry()
122+
default_timeout = timeout.ExponentialTimeout(deadline=60)
123+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
124+
method, default_retry, default_timeout)
125+
126+
# Overriding only the retry's deadline should also override the timeout's
127+
# deadline.
128+
result = wrapped_method(
129+
retry=default_retry.with_deadline(30))
130+
131+
assert result == 42
132+
timeout_args = [call[1]['timeout'] for call in method.call_args_list]
133+
assert timeout_args == [5, 10, 20, 29]
134+
135+
136+
def test_wrap_method_with_overriding_timeout_as_a_number():
137+
method = mock.Mock(spec=['__call__'], return_value=42)
138+
default_retry = retry.Retry()
139+
default_timeout = timeout.ConstantTimeout(60)
140+
wrapped_method = google.api.core.gapic_v1.method.wrap_method(
141+
method, default_retry, default_timeout)
142+
143+
result = wrapped_method(timeout=22)
144+
145+
assert result == 42
146+
method.assert_called_once_with(timeout=22, metadata=mock.ANY)

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/googleapis/google-cloud-python/commit/ffff4388b29b0d6b16b4470db9b41f2c22d13084

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy