aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBu Sun Kim <8822365+busunkim96@users.noreply.github.com>2020-06-18 16:27:38 -0700
committerGitHub <noreply@github.com>2020-06-18 16:27:38 -0700
commitded92d0acdcde4295d0e5df05fda0d83783a3991 (patch)
treedb0a37c2ee454c931bba9a0423f8f354c943fdb7
parent7531a5e4897d7ac492a45ef6bfed8f083c71f6ac (diff)
downloadpython-api-core-ded92d0acdcde4295d0e5df05fda0d83783a3991.tar.gz
feat: allow credentials files to be passed for channel creation (#50)
Co-authored-by: Dov Shlachter <dovs@google.com>
-rw-r--r--.gitignore5
-rw-r--r--google/api_core/exceptions.py6
-rw-r--r--google/api_core/grpc_helpers.py37
-rw-r--r--google/api_core/grpc_helpers_async.py14
-rw-r--r--setup.py2
-rw-r--r--tests/asyncio/test_grpc_helpers_async.py56
-rw-r--r--tests/unit/test_grpc_helpers.py61
7 files changed, 169 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
index b87e1ed..8c18b5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,4 +57,7 @@ system_tests/local_test_setup
# Make sure a generated file isn't accidentally committed.
pylintrc
-pylintrc.test \ No newline at end of file
+pylintrc.test
+
+# pytype
+pytype_output
diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py
index d1459ab..b9c46ca 100644
--- a/google/api_core/exceptions.py
+++ b/google/api_core/exceptions.py
@@ -41,6 +41,12 @@ class GoogleAPIError(Exception):
pass
+class DuplicateCredentialArgs(GoogleAPIError):
+ """Raised when multiple credentials are passed."""
+
+ pass
+
+
@six.python_2_unicode_compatible
class RetryError(GoogleAPIError):
"""Raised when a function has exhausted all of its available retries.
diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py
index b617ddf..2203968 100644
--- a/google/api_core/grpc_helpers.py
+++ b/google/api_core/grpc_helpers.py
@@ -176,13 +176,16 @@ def wrap_errors(callable_):
return _wrap_unary_errors(callable_)
-def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials=None):
+def _create_composite_credentials(credentials=None, credentials_file=None, scopes=None, ssl_credentials=None):
"""Create the composite credentials for secure channels.
Args:
credentials (google.auth.credentials.Credentials): The credentials. If
not specified, then this function will attempt to ascertain the
credentials from the environment using :func:`google.auth.default`.
+ credentials_file (str): A file with credentials that can be loaded with
+ :func:`google.auth.load_credentials_from_file`. This argument is
+ mutually exclusive with credentials.
scopes (Sequence[str]): A optional list of scopes needed for this
service. These are only used when credentials are not specified and
are passed to :func:`google.auth.default`.
@@ -191,14 +194,22 @@ def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials
Returns:
grpc.ChannelCredentials: The composed channel credentials object.
+
+ Raises:
+ google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""
- if credentials is None:
- credentials, _ = google.auth.default(scopes=scopes)
- else:
- credentials = google.auth.credentials.with_scopes_if_required(
- credentials, scopes
+ if credentials and credentials_file:
+ raise exceptions.DuplicateCredentialArgs(
+ "'credentials' and 'credentials_file' are mutually exclusive."
)
+ if credentials_file:
+ credentials, _ = google.auth.load_credentials_from_file(credentials_file, scopes=scopes)
+ elif credentials:
+ credentials = google.auth.credentials.with_scopes_if_required(credentials, scopes)
+ else:
+ credentials, _ = google.auth.default(scopes=scopes)
+
request = google.auth.transport.requests.Request()
# Create the metadata plugin for inserting the authorization header.
@@ -218,7 +229,7 @@ def _create_composite_credentials(credentials=None, scopes=None, ssl_credentials
)
-def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
+def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
"""Create a secure channel with credentials.
Args:
@@ -231,14 +242,24 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
are passed to :func:`google.auth.default`.
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
credentials. This can be used to specify different certificates.
+ credentials_file (str): A file with credentials that can be loaded with
+ :func:`google.auth.load_credentials_from_file`. This argument is
+ mutually exclusive with credentials.
kwargs: Additional key-word args passed to
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
Returns:
grpc.Channel: The created channel.
+
+ Raises:
+ google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""
+
composite_credentials = _create_composite_credentials(
- credentials, scopes, ssl_credentials
+ credentials=credentials,
+ credentials_file=credentials_file,
+ scopes=scopes,
+ ssl_credentials=ssl_credentials
)
if HAS_GRPC_GCP:
diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py
index 9ded803..1dfe8b9 100644
--- a/google/api_core/grpc_helpers_async.py
+++ b/google/api_core/grpc_helpers_async.py
@@ -206,7 +206,7 @@ def wrap_errors(callable_):
return _wrap_stream_errors(callable_)
-def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, **kwargs):
+def create_channel(target, credentials=None, scopes=None, ssl_credentials=None, credentials_file=None, **kwargs):
"""Create an AsyncIO secure channel with credentials.
Args:
@@ -219,13 +219,23 @@ def create_channel(target, credentials=None, scopes=None, ssl_credentials=None,
are passed to :func:`google.auth.default`.
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
credentials. This can be used to specify different certificates.
+ credentials_file (str): A file with credentials that can be loaded with
+ :func:`google.auth.load_credentials_from_file`. This argument is
+ mutually exclusive with credentials.
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.
Returns:
aio.Channel: The created channel.
+
+ Raises:
+ google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
"""
+
composite_credentials = grpc_helpers._create_composite_credentials(
- credentials, scopes, ssl_credentials
+ credentials=credentials,
+ credentials_file=credentials_file,
+ scopes=scopes,
+ ssl_credentials=ssl_credentials
)
return aio.secure_channel(target, composite_credentials, **kwargs)
diff --git a/setup.py b/setup.py
index 9fb7977..7f65fd0 100644
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,7 @@ release_status = "Development Status :: 5 - Production/Stable"
dependencies = [
"googleapis-common-protos >= 1.6.0, < 2.0dev",
"protobuf >= 3.12.0",
- "google-auth >= 1.14.0, < 2.0dev",
+ "google-auth >= 1.18.0, < 2.0dev",
"requests >= 2.18.0, < 3.0.0dev",
"setuptools >= 34.0.0",
"six >= 1.10.0",
diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py
index 0053952..d56c4c6 100644
--- a/tests/asyncio/test_grpc_helpers_async.py
+++ b/tests/asyncio/test_grpc_helpers_async.py
@@ -317,6 +317,19 @@ def test_create_channel_implicit_with_scopes(
grpc_secure_channel.assert_called_once_with(target, composite_creds)
+def test_create_channel_explicit_with_duplicate_credentials():
+ target = "example:443"
+
+ with pytest.raises(exceptions.DuplicateCredentialArgs) as excinfo:
+ grpc_helpers_async.create_channel(
+ target,
+ credentials_file="credentials.json",
+ credentials=mock.sentinel.credentials
+ )
+
+ assert "mutually exclusive" in str(excinfo.value)
+
+
@mock.patch("grpc.composite_channel_credentials")
@mock.patch("google.auth.credentials.with_scopes_if_required")
@mock.patch("grpc.experimental.aio.secure_channel")
@@ -350,6 +363,49 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
grpc_secure_channel.assert_called_once_with(target, composite_creds)
+@mock.patch("grpc.composite_channel_credentials")
+@mock.patch("grpc.experimental.aio.secure_channel")
+@mock.patch(
+ "google.auth.load_credentials_from_file",
+ return_value=(mock.sentinel.credentials, mock.sentinel.project)
+)
+def test_create_channnel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
+ target = "example.com:443"
+
+ credentials_file = "/path/to/credentials/file.json"
+ composite_creds = composite_creds_call.return_value
+
+ channel = grpc_helpers_async.create_channel(
+ target, credentials_file=credentials_file
+ )
+
+ google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)
+ assert channel is grpc_secure_channel.return_value
+ grpc_secure_channel.assert_called_once_with(target, composite_creds)
+
+
+@mock.patch("grpc.composite_channel_credentials")
+@mock.patch("grpc.experimental.aio.secure_channel")
+@mock.patch(
+ "google.auth.load_credentials_from_file",
+ return_value=(mock.sentinel.credentials, mock.sentinel.project)
+)
+def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
+ target = "example.com:443"
+ scopes = ["1", "2"]
+
+ credentials_file = "/path/to/credentials/file.json"
+ composite_creds = composite_creds_call.return_value
+
+ channel = grpc_helpers_async.create_channel(
+ target, credentials_file=credentials_file, scopes=scopes
+ )
+
+ google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
+ assert channel is grpc_secure_channel.return_value
+ grpc_secure_channel.assert_called_once_with(target, composite_creds)
+
+
@pytest.mark.skipif(grpc_helpers_async.HAS_GRPC_GCP, reason="grpc_gcp module not available")
@mock.patch("grpc.experimental.aio.secure_channel")
def test_create_channel_without_grpc_gcp(grpc_secure_channel):
diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py
index ef84514..e2f3666 100644
--- a/tests/unit/test_grpc_helpers.py
+++ b/tests/unit/test_grpc_helpers.py
@@ -285,6 +285,17 @@ def test_create_channel_implicit_with_scopes(
grpc_secure_channel.assert_called_once_with(target, composite_creds)
+def test_create_channel_explicit_with_duplicate_credentials():
+ target = "example.com:443"
+
+ with pytest.raises(exceptions.DuplicateCredentialArgs):
+ grpc_helpers.create_channel(
+ target,
+ credentials_file="credentials.json",
+ credentials=mock.sentinel.credentials
+ )
+
+
@mock.patch("grpc.composite_channel_credentials")
@mock.patch("google.auth.credentials.with_scopes_if_required")
@mock.patch("grpc.secure_channel")
@@ -324,6 +335,56 @@ def test_create_channel_explicit_scoped(grpc_secure_channel, composite_creds_cal
grpc_secure_channel.assert_called_once_with(target, composite_creds)
+@mock.patch("grpc.composite_channel_credentials")
+@mock.patch("grpc.secure_channel")
+@mock.patch(
+ "google.auth.load_credentials_from_file",
+ return_value=(mock.sentinel.credentials, mock.sentinel.project)
+)
+def test_create_channel_with_credentials_file(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
+ target = "example.com:443"
+
+ credentials_file = "/path/to/credentials/file.json"
+ composite_creds = composite_creds_call.return_value
+
+ channel = grpc_helpers.create_channel(
+ target, credentials_file=credentials_file
+ )
+
+ google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=None)
+
+ assert channel is grpc_secure_channel.return_value
+ if grpc_helpers.HAS_GRPC_GCP:
+ grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
+ else:
+ grpc_secure_channel.assert_called_once_with(target, composite_creds)
+
+
+@mock.patch("grpc.composite_channel_credentials")
+@mock.patch("grpc.secure_channel")
+@mock.patch(
+ "google.auth.load_credentials_from_file",
+ return_value=(mock.sentinel.credentials, mock.sentinel.project)
+)
+def test_create_channel_with_credentials_file_and_scopes(load_credentials_from_file, grpc_secure_channel, composite_creds_call):
+ target = "example.com:443"
+ scopes = ["1", "2"]
+
+ credentials_file = "/path/to/credentials/file.json"
+ composite_creds = composite_creds_call.return_value
+
+ channel = grpc_helpers.create_channel(
+ target, credentials_file=credentials_file, scopes=scopes
+ )
+
+ google.auth.load_credentials_from_file.assert_called_once_with(credentials_file, scopes=scopes)
+ assert channel is grpc_secure_channel.return_value
+ if grpc_helpers.HAS_GRPC_GCP:
+ grpc_secure_channel.assert_called_once_with(target, composite_creds, None)
+ else:
+ grpc_secure_channel.assert_called_once_with(target, composite_creds)
+
+
@pytest.mark.skipif(
not grpc_helpers.HAS_GRPC_GCP, reason="grpc_gcp module not available"
)