From f5086c7cffa0faa6f31caeac89c213dc391c5953 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Wed, 23 Sep 2015 22:52:04 -0700 Subject: [PATCH] Adding helpers to make gRPC stubs from a client. --- gcloud/bigtable/_helpers.py | 92 ++++++++++++++++++++ gcloud/bigtable/client.py | 33 ++++++- gcloud/bigtable/test__helpers.py | 143 +++++++++++++++++++++++++++++++ gcloud/bigtable/test_client.py | 33 ++++++- 4 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 gcloud/bigtable/_helpers.py create mode 100644 gcloud/bigtable/test__helpers.py diff --git a/gcloud/bigtable/_helpers.py b/gcloud/bigtable/_helpers.py new file mode 100644 index 000000000000..4c1683880841 --- /dev/null +++ b/gcloud/bigtable/_helpers.py @@ -0,0 +1,92 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# 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. + +"""Utility methods for gcloud Bigtable. + +Primarily includes helpers for dealing with low-level +protobuf objects. +""" + + +# See https://gist.github.com/dhermes/bbc5b7be1932bfffae77 +# for appropriate values on other systems. +# NOTE: Even this path is Unix specific. +SSL_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt' + + +class MetadataTransformer(object): + """Callable class to transform metadata for gRPC requests. + + :type client: :class:`.client.Client` + :param client: The client that owns the cluster. Provides authorization and + user agent. + """ + + def __init__(self, client): + self._credentials = client.credentials + self._user_agent = client.user_agent + + def __call__(self, ignored_val): + """Adds authorization header to request metadata.""" + access_token = self._credentials.get_access_token().access_token + return [ + ('Authorization', 'Bearer ' + access_token), + ('User-agent', self._user_agent), + ] + + +def get_certs(): + """Gets the root certificates. + + .. note:: + + This is only called by :func:`make_stub`. For most applications, + a few gRPC stubs (four total, one for each service) will be created + when a :class:`.Client` is created. This function will not likely + be used again while that application is running. + + However, it may be worthwhile to cache the output of this function. + + :rtype: str + :returns: The root certificates for the current machine. + """ + with open(SSL_CERT_FILE, mode='rb') as file_obj: + return file_obj.read() + + +def make_stub(client, stub_factory, host, port): + """Makes a stub for the an API. + + :type client: :class:`.client.Client` + :param client: The client that owns the cluster. Provides authorization and + user agent. + + :type stub_factory: callable + :param stub_factory: A factory which will create a gRPC stub for + a given service. + + :type host: str + :param host: The host for the service. + + :type port: int + :param port: The port for the service. + + :rtype: :class:`grpc.early_adopter.implementations._Stub` + :returns: The stub object used to make gRPC requests to a given API. + """ + custom_metadata_transformer = MetadataTransformer(client) + return stub_factory(host, port, + metadata_transformer=custom_metadata_transformer, + secure=True, + root_certificates=get_certs()) diff --git a/gcloud/bigtable/client.py b/gcloud/bigtable/client.py index 86664d7cc0d0..6d32554aa24e 100644 --- a/gcloud/bigtable/client.py +++ b/gcloud/bigtable/client.py @@ -55,6 +55,12 @@ 'cloud-bigtable.data.readonly') """Scope for reading table data.""" +DEFAULT_TIMEOUT_SECONDS = 10 +"""The default timeout to use for API requests.""" + +DEFAULT_USER_AGENT = 'gcloud-bigtable-python' +"""The default user agent for API requests.""" + class Client(_ClientFactoryMixin, _ClientProjectMixin): """Client for interacting with Google Cloud Bigtable API. @@ -86,12 +92,22 @@ class Client(_ClientFactoryMixin, _ClientProjectMixin): interact with the Cluster Admin or Table Admin APIs. This requires the :const:`ADMIN_SCOPE`. Defaults to :data:`False`. + :type user_agent: str + :param user_agent: (Optional) The user agent to be used with API request. + Defaults to :const:`DEFAULT_USER_AGENT`. + + :type timeout_seconds: int + :param timeout_seconds: Number of seconds for request time-out. If not + passed, defaults to + :const:`DEFAULT_TIMEOUT_SECONDS`. + :raises: :class:`ValueError ` if both ``read_only`` and ``admin`` are :data:`True` """ def __init__(self, project=None, credentials=None, - read_only=False, admin=False): + read_only=False, admin=False, user_agent=DEFAULT_USER_AGENT, + timeout_seconds=DEFAULT_TIMEOUT_SECONDS): _ClientProjectMixin.__init__(self, project=project) if credentials is None: credentials = get_credentials() @@ -109,4 +125,17 @@ def __init__(self, project=None, credentials=None, if admin: scopes.append(ADMIN_SCOPE) - self.credentials = credentials.create_scoped(scopes) + self._admin = bool(admin) + self._credentials = credentials.create_scoped(scopes) + self.user_agent = user_agent + self.timeout_seconds = timeout_seconds + + @property + def credentials(self): + """Getter for client's credentials. + + :rtype: + :class:`OAuth2Credentials ` + :returns: The credentials stored on the client. + """ + return self._credentials diff --git a/gcloud/bigtable/test__helpers.py b/gcloud/bigtable/test__helpers.py new file mode 100644 index 000000000000..9373fef78ae5 --- /dev/null +++ b/gcloud/bigtable/test__helpers.py @@ -0,0 +1,143 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# 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. + + +import unittest2 + + +class TestMetadataTransformer(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.bigtable._helpers import MetadataTransformer + return MetadataTransformer + + def _makeOne(self, *args, **kwargs): + return self._getTargetClass()(*args, **kwargs) + + def test_constructor(self): + from gcloud.bigtable.client import Client + from gcloud.bigtable.client import DATA_SCOPE + + credentials = _Credentials() + project = 'PROJECT' + user_agent = 'USER_AGENT' + client = Client(project=project, credentials=credentials, + user_agent=user_agent) + transformer = self._makeOne(client) + self.assertTrue(transformer._credentials is credentials) + self.assertEqual(transformer._user_agent, user_agent) + self.assertEqual(credentials._scopes, [DATA_SCOPE]) + + def test___call__(self): + from gcloud.bigtable.client import Client + from gcloud.bigtable.client import DATA_SCOPE + from gcloud.bigtable.client import DEFAULT_USER_AGENT + + access_token_expected = 'FOOBARBAZ' + credentials = _Credentials(access_token=access_token_expected) + project = 'PROJECT' + client = Client(project=project, credentials=credentials) + + transformer = self._makeOne(client) + result = transformer(None) + self.assertEqual( + result, + [ + ('Authorization', 'Bearer ' + access_token_expected), + ('User-agent', DEFAULT_USER_AGENT), + ]) + self.assertEqual(credentials._scopes, [DATA_SCOPE]) + self.assertEqual(len(credentials._tokens), 1) + + +class Test_get_certs(unittest2.TestCase): + + def _callFUT(self, *args, **kwargs): + from gcloud.bigtable._helpers import get_certs + return get_certs(*args, **kwargs) + + def test_it(self): + import tempfile + from gcloud._testing import _Monkey + from gcloud.bigtable import _helpers as MUT + + # Just write to a mock file. + filename = tempfile.mktemp() + contents = b'FOOBARBAZ' + with open(filename, 'wb') as file_obj: + file_obj.write(contents) + + with _Monkey(MUT, SSL_CERT_FILE=filename): + result = self._callFUT() + + self.assertEqual(result, contents) + + +class Test_make_stub(unittest2.TestCase): + + def _callFUT(self, *args, **kwargs): + from gcloud.bigtable._helpers import make_stub + return make_stub(*args, **kwargs) + + def test_it(self): + from gcloud._testing import _Monkey + from gcloud.bigtable import _helpers as MUT + + mock_result = object() + stub_inputs = [] + + def mock_stub_factory(host, port, metadata_transformer=None, + secure=None, root_certificates=None): + stub_inputs.append((host, port, metadata_transformer, + secure, root_certificates)) + return mock_result + + transformed = object() + clients = [] + + def mock_transformer(client): + clients.append(client) + return transformed + + host = 'HOST' + port = 1025 + certs = 'FOOBAR' + client = object() + with _Monkey(MUT, get_certs=lambda: certs, + MetadataTransformer=mock_transformer): + result = self._callFUT(client, mock_stub_factory, host, port) + + self.assertTrue(result is mock_result) + self.assertEqual(stub_inputs, [(host, port, transformed, True, certs)]) + self.assertEqual(clients, [client]) + + +class _Credentials(object): + + _scopes = None + + def __init__(self, access_token=None): + self._access_token = access_token + self._tokens = [] + + def get_access_token(self): + from oauth2client.client import AccessTokenInfo + token = AccessTokenInfo(access_token=self._access_token, + expires_in=None) + self._tokens.append(token) + return token + + def create_scoped(self, scope): + self._scopes = scope + return self diff --git a/gcloud/bigtable/test_client.py b/gcloud/bigtable/test_client.py index 8b0b20d07aa6..8755048b06a8 100644 --- a/gcloud/bigtable/test_client.py +++ b/gcloud/bigtable/test_client.py @@ -27,15 +27,25 @@ def _makeOne(self, *args, **kwargs): def _constructor_test_helper(self, expected_scopes, creds, read_only=False, admin=False, + user_agent=None, timeout_seconds=None, expected_creds=None): + from gcloud.bigtable import client as MUT + + user_agent = user_agent or MUT.DEFAULT_USER_AGENT + timeout_seconds = timeout_seconds or MUT.DEFAULT_TIMEOUT_SECONDS PROJECT = 'PROJECT' client = self._makeOne(project=PROJECT, credentials=creds, - read_only=read_only, admin=admin) + read_only=read_only, admin=admin, + user_agent=user_agent, + timeout_seconds=timeout_seconds) expected_creds = expected_creds or creds - self.assertTrue(client.credentials is expected_creds) + self.assertTrue(client._credentials is expected_creds) + self.assertEqual(client._credentials._scopes, expected_scopes) + self.assertEqual(client.project, PROJECT) - self.assertEqual(client.credentials._scopes, expected_scopes) + self.assertEqual(client.timeout_seconds, timeout_seconds) + self.assertEqual(client.user_agent, user_agent) def test_constructor_default_scopes(self): from gcloud.bigtable import client as MUT @@ -44,6 +54,17 @@ def test_constructor_default_scopes(self): creds = _Credentials() self._constructor_test_helper(expected_scopes, creds) + def test_constructor_custom_user_agent_and_timeout(self): + from gcloud.bigtable import client as MUT + + timeout_seconds = 1337 + user_agent = 'custom-application' + expected_scopes = [MUT.DATA_SCOPE] + creds = _Credentials() + self._constructor_test_helper(expected_scopes, creds, + user_agent=user_agent, + timeout_seconds=timeout_seconds) + def test_constructor_with_admin(self): from gcloud.bigtable import client as MUT @@ -78,6 +99,12 @@ def mock_get_credentials(): self._constructor_test_helper(expected_scopes, None, expected_creds=creds) + def test_credentials_getter(self): + credentials = _Credentials() + project = 'PROJECT' + client = self._makeOne(project=project, credentials=credentials) + self.assertTrue(client.credentials is credentials) + class _Credentials(object):