Skip to content

Commit 55d11b9

Browse files
WeiranFangtheacodes
authored andcommitted
Add support for gRPC connection management (available when using optional grpc_gcp dependency) (googleapis#5553)
1 parent b8fd3c3 commit 55d11b9

File tree

9 files changed

+347
-32
lines changed

9 files changed

+347
-32
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ htmlcov
4545
# JetBrains
4646
.idea
4747

48+
# VS Code
49+
.vscode
50+
4851
# Built documentation
4952
docs/_build
5053
docs/_build_doc2dash

api_core/google/api_core/grpc_helpers.py

+33-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
import google.auth.transport.grpc
2727
import google.auth.transport.requests
2828

29+
try:
30+
import grpc_gcp
31+
HAS_GRPC_GCP = True
32+
except ImportError:
33+
HAS_GRPC_GCP = False
2934

3035
# The list of gRPC Callable interfaces that return iterators.
3136
_STREAM_WRAP_CLASSES = (
@@ -149,7 +154,11 @@ def wrap_errors(callable_):
149154
return _wrap_unary_errors(callable_)
150155

151156

152-
def create_channel(target, credentials=None, scopes=None, **kwargs):
157+
def create_channel(target,
158+
credentials=None,
159+
scopes=None,
160+
ssl_credentials=None,
161+
**kwargs):
153162
"""Create a secure channel with credentials.
154163
155164
Args:
@@ -160,8 +169,10 @@ def create_channel(target, credentials=None, scopes=None, **kwargs):
160169
scopes (Sequence[str]): A optional list of scopes needed for this
161170
service. These are only used when credentials are not specified and
162171
are passed to :func:`google.auth.default`.
172+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
173+
credentials. This can be used to specify different certificates.
163174
kwargs: Additional key-word args passed to
164-
:func:`google.auth.transport.grpc.secure_authorized_channel`.
175+
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
165176
166177
Returns:
167178
grpc.Channel: The created channel.
@@ -174,8 +185,26 @@ def create_channel(target, credentials=None, scopes=None, **kwargs):
174185

175186
request = google.auth.transport.requests.Request()
176187

177-
return google.auth.transport.grpc.secure_authorized_channel(
178-
credentials, request, target, **kwargs)
188+
# Create the metadata plugin for inserting the authorization header.
189+
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
190+
credentials, request)
191+
192+
# Create a set of grpc.CallCredentials using the metadata plugin.
193+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
194+
195+
if ssl_credentials is None:
196+
ssl_credentials = grpc.ssl_channel_credentials()
197+
198+
# Combine the ssl credentials and the authorization credentials.
199+
composite_credentials = grpc.composite_channel_credentials(
200+
ssl_credentials, google_auth_credentials)
201+
202+
if HAS_GRPC_GCP:
203+
# If grpc_gcp module is available use grpc_gcp.secure_channel,
204+
# otherwise, use grpc.secure_channel to create grpc channel.
205+
return grpc_gcp.secure_channel(target, composite_credentials, **kwargs)
206+
else:
207+
return grpc.secure_channel(target, composite_credentials, **kwargs)
179208

180209

181210
_MethodCall = collections.namedtuple(

api_core/nox.py

+17
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ def unit(session, py):
6565
default(session)
6666

6767

68+
@nox.session
69+
@nox.parametrize('py', ['2.7', '3.5', '3.6', '3.7'])
70+
def unit_grpc_gcp(session, py):
71+
"""Run the unit test suite with grpcio-gcp installed."""
72+
73+
# Run unit tests against all supported versions of Python.
74+
session.interpreter = 'python{}'.format(py)
75+
76+
# Set the virtualenv dirname.
77+
session.virtualenv_dirname = 'unit-grpc-gcp-' + py
78+
79+
# Install grpcio-gcp
80+
session.install('grpcio-gcp')
81+
82+
default(session)
83+
84+
6885
@nox.session
6986
def lint(session):
7087
"""Run linters.

api_core/tests/unit/test_grpc_helpers.py

+115-18
Original file line numberDiff line numberDiff line change
@@ -176,60 +176,157 @@ def test_wrap_errors_streaming(wrap_stream_errors):
176176
wrap_stream_errors.assert_called_once_with(callable_)
177177

178178

179+
@mock.patch('grpc.composite_channel_credentials')
179180
@mock.patch(
180181
'google.auth.default',
181182
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
182-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
183-
def test_create_channel_implicit(secure_authorized_channel, default):
183+
@mock.patch('grpc.secure_channel')
184+
def test_create_channel_implicit(
185+
grpc_secure_channel, default, composite_creds_call):
184186
target = 'example.com:443'
187+
composite_creds = composite_creds_call.return_value
185188

186189
channel = grpc_helpers.create_channel(target)
187190

188-
assert channel is secure_authorized_channel.return_value
191+
assert channel is grpc_secure_channel.return_value
189192
default.assert_called_once_with(scopes=None)
190-
secure_authorized_channel.assert_called_once_with(
191-
mock.sentinel.credentials, mock.ANY, target)
193+
if (grpc_helpers.HAS_GRPC_GCP):
194+
grpc_secure_channel.assert_called_once_with(
195+
target, composite_creds, None)
196+
else:
197+
grpc_secure_channel.assert_called_once_with(
198+
target, composite_creds)
192199

193200

201+
@mock.patch('grpc.composite_channel_credentials')
194202
@mock.patch(
195203
'google.auth.default',
196204
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
197-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
205+
@mock.patch('grpc.secure_channel')
206+
def test_create_channel_implicit_with_ssl_creds(
207+
grpc_secure_channel, default, composite_creds_call):
208+
target = 'example.com:443'
209+
210+
ssl_creds = grpc.ssl_channel_credentials()
211+
212+
grpc_helpers.create_channel(target, ssl_credentials=ssl_creds)
213+
214+
default.assert_called_once_with(scopes=None)
215+
composite_creds_call.assert_called_once_with(ssl_creds, mock.ANY)
216+
composite_creds = composite_creds_call.return_value
217+
if (grpc_helpers.HAS_GRPC_GCP):
218+
grpc_secure_channel.assert_called_once_with(
219+
target, composite_creds, None)
220+
else:
221+
grpc_secure_channel.assert_called_once_with(
222+
target, composite_creds)
223+
224+
225+
@mock.patch('grpc.composite_channel_credentials')
226+
@mock.patch(
227+
'google.auth.default',
228+
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
229+
@mock.patch('grpc.secure_channel')
198230
def test_create_channel_implicit_with_scopes(
199-
secure_authorized_channel, default):
231+
grpc_secure_channel, default, composite_creds_call):
200232
target = 'example.com:443'
233+
composite_creds = composite_creds_call.return_value
201234

202235
channel = grpc_helpers.create_channel(target, scopes=['one', 'two'])
203236

204-
assert channel is secure_authorized_channel.return_value
237+
assert channel is grpc_secure_channel.return_value
205238
default.assert_called_once_with(scopes=['one', 'two'])
206-
207-
208-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
209-
def test_create_channel_explicit(secure_authorized_channel):
239+
if (grpc_helpers.HAS_GRPC_GCP):
240+
grpc_secure_channel.assert_called_once_with(
241+
target, composite_creds, None)
242+
else:
243+
grpc_secure_channel.assert_called_once_with(
244+
target, composite_creds)
245+
246+
247+
@mock.patch('grpc.composite_channel_credentials')
248+
@mock.patch('google.auth.credentials.with_scopes_if_required')
249+
@mock.patch('grpc.secure_channel')
250+
def test_create_channel_explicit(
251+
grpc_secure_channel, auth_creds, composite_creds_call):
210252
target = 'example.com:443'
253+
composite_creds = composite_creds_call.return_value
211254

212255
channel = grpc_helpers.create_channel(
213256
target, credentials=mock.sentinel.credentials)
214257

215-
assert channel is secure_authorized_channel.return_value
216-
secure_authorized_channel.assert_called_once_with(
217-
mock.sentinel.credentials, mock.ANY, target)
258+
auth_creds.assert_called_once_with(mock.sentinel.credentials, None)
259+
assert channel is grpc_secure_channel.return_value
260+
if (grpc_helpers.HAS_GRPC_GCP):
261+
grpc_secure_channel.assert_called_once_with(
262+
target, composite_creds, None)
263+
else:
264+
grpc_secure_channel.assert_called_once_with(
265+
target, composite_creds)
218266

219267

220-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
221-
def test_create_channel_explicit_scoped(unused_secure_authorized_channel):
268+
@mock.patch('grpc.composite_channel_credentials')
269+
@mock.patch('grpc.secure_channel')
270+
def test_create_channel_explicit_scoped(
271+
grpc_secure_channel, composite_creds_call):
272+
target = 'example.com:443'
222273
scopes = ['1', '2']
274+
composite_creds = composite_creds_call.return_value
275+
276+
credentials = mock.create_autospec(
277+
google.auth.credentials.Scoped, instance=True)
278+
credentials.requires_scopes = True
279+
280+
channel = grpc_helpers.create_channel(
281+
target,
282+
credentials=credentials,
283+
scopes=scopes)
284+
285+
credentials.with_scopes.assert_called_once_with(scopes)
286+
assert channel is grpc_secure_channel.return_value
287+
if (grpc_helpers.HAS_GRPC_GCP):
288+
grpc_secure_channel.assert_called_once_with(
289+
target, composite_creds, None)
290+
else:
291+
grpc_secure_channel.assert_called_once_with(
292+
target, composite_creds)
293+
294+
295+
@pytest.mark.skipif(not grpc_helpers.HAS_GRPC_GCP,
296+
reason='grpc_gcp module not available')
297+
@mock.patch('grpc_gcp.secure_channel')
298+
def test_create_channel_with_grpc_gcp(grpc_gcp_secure_channel):
299+
target = 'example.com:443'
300+
scopes = ['test_scope']
223301

224302
credentials = mock.create_autospec(
225303
google.auth.credentials.Scoped, instance=True)
226304
credentials.requires_scopes = True
227305

228306
grpc_helpers.create_channel(
229-
mock.sentinel.target,
307+
target,
230308
credentials=credentials,
231309
scopes=scopes)
310+
grpc_gcp_secure_channel.assert_called()
311+
credentials.with_scopes.assert_called_once_with(scopes)
232312

313+
314+
@pytest.mark.skipif(grpc_helpers.HAS_GRPC_GCP,
315+
reason='grpc_gcp module not available')
316+
@mock.patch('grpc.secure_channel')
317+
def test_create_channel_without_grpc_gcp(grpc_secure_channel):
318+
target = 'example.com:443'
319+
scopes = ['test_scope']
320+
321+
credentials = mock.create_autospec(
322+
google.auth.credentials.Scoped, instance=True)
323+
credentials.requires_scopes = True
324+
325+
grpc_helpers.create_channel(
326+
target,
327+
credentials=credentials,
328+
scopes=scopes)
329+
grpc_secure_channel.assert_called()
233330
credentials.with_scopes.assert_called_once_with(scopes)
234331

235332

spanner/MANIFEST.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include README.rst LICENSE
2-
recursive-include google *.json *.proto
2+
recursive-include google *.json *.proto *.config
33
recursive-include tests *
44
global-exclude *.pyc __pycache__
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
channel_pool: {
2+
max_size: 10
3+
max_concurrent_streams_low_watermark: 100
4+
}
5+
method: {
6+
name: "/google.spanner.v1.Spanner/CreateSession"
7+
affinity: {
8+
command: BIND
9+
affinity_key: "name"
10+
}
11+
}
12+
method: {
13+
name: "/google.spanner.v1.Spanner/GetSession"
14+
affinity: {
15+
command: BOUND
16+
affinity_key: "name"
17+
}
18+
}
19+
method: {
20+
name: "/google.spanner.v1.Spanner/DeleteSession"
21+
affinity: {
22+
command: UNBIND
23+
affinity_key: "name"
24+
}
25+
}
26+
method: {
27+
name: "/google.spanner.v1.Spanner/ExecuteSql"
28+
affinity: {
29+
command: BOUND
30+
affinity_key: "session"
31+
}
32+
}
33+
method: {
34+
name: "/google.spanner.v1.Spanner/ExecuteStreamingSql"
35+
affinity: {
36+
command: BOUND
37+
affinity_key: "session"
38+
}
39+
}
40+
method: {
41+
name: "/google.spanner.v1.Spanner/Read"
42+
affinity: {
43+
command: BOUND
44+
affinity_key: "session"
45+
}
46+
}
47+
method: {
48+
name: "/google.spanner.v1.Spanner/StreamingRead"
49+
affinity: {
50+
command: BOUND
51+
affinity_key: "session"
52+
}
53+
}
54+
method: {
55+
name: "/google.spanner.v1.Spanner/BeginTransaction"
56+
affinity: {
57+
command: BOUND
58+
affinity_key: "session"
59+
}
60+
}
61+
method: {
62+
name: "/google.spanner.v1.Spanner/Commit"
63+
affinity: {
64+
command: BOUND
65+
affinity_key: "session"
66+
}
67+
}
68+
method: {
69+
name: "/google.spanner.v1.Spanner/Rollback"
70+
affinity: {
71+
command: BOUND
72+
affinity_key: "session"
73+
}
74+
}
75+
method: {
76+
name: "/google.spanner.v1.Spanner/PartitionQuery"
77+
affinity: {
78+
command: BOUND
79+
affinity_key: "session"
80+
}
81+
}
82+
method: {
83+
name: "/google.spanner.v1.Spanner/PartitionRead"
84+
affinity: {
85+
command: BOUND
86+
affinity_key: "session"
87+
}
88+
}

0 commit comments

Comments
 (0)