Skip to content

Commit

Permalink
Bringing back the Cluster.from_* constructors in Client.
Browse files Browse the repository at this point in the history
  • Loading branch information
dhermes committed Jul 27, 2015
1 parent b8afe20 commit 6250668
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 11 deletions.
123 changes: 113 additions & 10 deletions gcloud_bigtable/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
import six
import socket

from oauth2client.client import GoogleCredentials
from oauth2client.client import SignedJwtAssertionCredentials
from oauth2client.client import _get_application_default_credential_from_file

try:
from google.appengine.api import app_identity
except ImportError:
Expand Down Expand Up @@ -149,32 +153,38 @@ class Client(object):
:type credentials: :class:`oauth2client.client.OAuth2Credentials` or
:class:`NoneType`
:param credentials: The OAuth2 Credentials to use for this cluster.
:param credentials: (Optional) The OAuth2 Credentials to use for this
cluster. If not provided, defaulst to the Google
Application Default Credentials.
:type project_id: string
:param project_id: The ID of the project which owns the clusters, tables
and data. If not provided, will attempt to
determine from the environment.
:param project_id: (Optional) The ID of the project which owns the
clusters, tables and data. If not provided, will
attempt to determine from the environment.
:type read_only: boolean
:param read_only: Boolean indicating if the data scope should be
for reading only (or for writing as well).
:param read_only: (Optional) Boolean indicating if the data scope should be
for reading only (or for writing as well). Defaults to
``False``.
:type admin: boolean
:param admin: Boolean indicating if the client will be used to interact
with the Cluster Admin or Table Admin APIs. This requires
the ``ADMIN_SCOPE``.
:param admin: (Optional) Boolean indicating if the client will be used to
interact with the Cluster Admin or Table Admin APIs. This
requires the ``ADMIN_SCOPE``. Defaults to ``False``.
:raises: :class:`ValueError` if both ``read_only`` and
``admin`` are ``True``
"""

def __init__(self, credentials, project_id=None,
def __init__(self, credentials=None, project_id=None,
read_only=False, admin=False):
if read_only and admin:
raise ValueError('A read-only client cannot also perform'
'administrative actions.')

if credentials is None:
credentials = GoogleCredentials.get_application_default()

scopes = []
if read_only:
scopes.append(READ_ONLY_SCOPE)
Expand All @@ -187,6 +197,84 @@ def __init__(self, credentials, project_id=None,
self._credentials = credentials.create_scoped(scopes)
self._project_id = _determine_project_id(project_id)

@classmethod
def from_service_account_json(cls, json_credentials_path, project_id=None,
read_only=False, admin=False):
"""Factory to retrieve JSON credentials while creating client object.
:type json_credentials_path: string
:param json_credentials_path: The path to a private key file (this file
was given to you when you created the
service account). This file must contain
a JSON object with a private key and
other credentials information (downloaded
from the Google APIs console).
:type project_id: string
:param project_id: The ID of the project which owns the clusters,
tables and data. Will be passed to :class:`Client`
constructor.
:type read_only: boolean
:param read_only: Boolean indicating if the data scope should be
for reading only (or for writing as well). Will be
passed to :class:`Client` constructor.
:type admin: boolean
:param admin: Boolean indicating if the client will be used to
interact with the Cluster Admin or Table Admin APIs. Will
be passed to :class:`Client` constructor.
:rtype: :class:`Client`
:returns: The client created with the retrieved JSON credentials.
"""
credentials = _get_application_default_credential_from_file(
json_credentials_path)
return cls(credentials=credentials, project_id=project_id,
read_only=read_only, admin=admin)

@classmethod
def from_service_account_p12(cls, client_email, private_key_path,
project_id=None, read_only=False,
admin=False):
"""Factory to retrieve P12 credentials while creating client object.
.. note::
Unless you have an explicit reason to use a PKCS12 key for your
service account, we recommend using a JSON key.
:type client_email: string
:param client_email: The e-mail attached to the service account.
:type private_key_path: string
:param private_key_path: The path to a private key file (this file was
given to you when you created the service
account). This file must be in P12 format.
:type project_id: string
:param project_id: The ID of the project which owns the clusters,
tables and data. Will be passed to :class:`Client`
constructor.
:type read_only: boolean
:param read_only: Boolean indicating if the data scope should be
for reading only (or for writing as well). Will be
passed to :class:`Client` constructor.
:type admin: boolean
:param admin: Boolean indicating if the client will be used to
interact with the Cluster Admin or Table Admin APIs. Will
be passed to :class:`Client` constructor.
:rtype: :class:`Client`
:returns: The client created with the retrieved P12 credentials.
"""
credentials = SignedJwtAssertionCredentials(
service_account_name=client_email,
private_key=_get_contents(private_key_path))
return cls(credentials=credentials, project_id=project_id,
read_only=read_only, admin=admin)

@property
def credentials(self):
"""Getter for client's credentials.
Expand Down Expand Up @@ -292,3 +380,18 @@ def list_clusters(self, timeout_seconds=TIMEOUT_SECONDS):
clusters = [Cluster.from_pb(cluster_pb, self)
for cluster_pb in list_clusters_response.clusters]
return clusters, failed_zones


def _get_contents(filename):
"""Get the contents of a file.
This is just implemented so we can stub out while testing.
:type filename: string or bytes
:param filename: The name of a file to open.
:rtype: bytes
:returns: The bytes loaded from the file.
"""
with open(filename, 'rb') as file_obj:
return file_obj.read()
101 changes: 101 additions & 0 deletions gcloud_bigtable/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,27 @@ def _constructor_test_helper(self, expected_scopes, project_id=None,
mock_determine_project_id.check_called(self, [(project_id,)])

def test_constructor_default(self):
from gcloud_bigtable._testing import _MockWithAttachedMethods
from gcloud_bigtable._testing import _Monkey
from gcloud_bigtable import client as MUT

scoped_creds = object()
credentials = _MockWithAttachedMethods(scoped_creds)
mock_creds_class = _MockWithAttachedMethods(credentials)

with _Monkey(MUT, GoogleCredentials=mock_creds_class):
client = self._makeOne(project_id=PROJECT_ID)

self.assertEqual(client.project_id, PROJECT_ID)
self.assertTrue(client._credentials is scoped_creds)
self.assertEqual(mock_creds_class._called,
[('get_application_default', (), {})])
expected_scopes = [MUT.DATA_SCOPE]
self.assertEqual(credentials._called, [
('create_scoped', (expected_scopes,), {}),
])

def test_constructor_explicit_credentials(self):
from gcloud_bigtable import client as MUT
expected_scopes = [MUT.DATA_SCOPE]
self._constructor_test_helper(expected_scopes)
Expand All @@ -296,6 +317,69 @@ def test_constructor_both_admin_and_read_only(self):
with self.assertRaises(ValueError):
self._makeOne(None, admin=True, read_only=True)

def test_from_service_account_json(self):
from gcloud_bigtable._testing import _MockCalled
from gcloud_bigtable._testing import _MockWithAttachedMethods
from gcloud_bigtable._testing import _Monkey
from gcloud_bigtable import client as MUT

klass = self._getTargetClass()
scoped_creds = object()
credentials = _MockWithAttachedMethods(scoped_creds)
get_adc = _MockCalled(credentials)
json_credentials_path = 'JSON_CREDENTIALS_PATH'

with _Monkey(MUT,
_get_application_default_credential_from_file=get_adc):
client = klass.from_service_account_json(
json_credentials_path, project_id=PROJECT_ID)

self.assertEqual(client.project_id, PROJECT_ID)
self.assertTrue(client._credentials is scoped_creds)

expected_scopes = [MUT.DATA_SCOPE]
self.assertEqual(credentials._called, [
('create_scoped', (expected_scopes,), {}),
])
# _get_application_default_credential_from_file only has pos. args.
get_adc.check_called(self, [(json_credentials_path,)])

def test_from_service_account_p12(self):
from gcloud_bigtable._testing import _MockCalled
from gcloud_bigtable._testing import _MockWithAttachedMethods
from gcloud_bigtable._testing import _Monkey
from gcloud_bigtable import client as MUT

klass = self._getTargetClass()
scoped_creds = object()
credentials = _MockWithAttachedMethods(scoped_creds)
signed_creds = _MockCalled(credentials)

private_key = 'PRIVATE_KEY'
mock_get_contents = _MockCalled(private_key)
client_email = 'CLIENT_EMAIL'
private_key_path = 'PRIVATE_KEY_PATH'

with _Monkey(MUT, SignedJwtAssertionCredentials=signed_creds,
_get_contents=mock_get_contents):
client = klass.from_service_account_p12(
client_email, private_key_path, project_id=PROJECT_ID)

self.assertEqual(client.project_id, PROJECT_ID)
self.assertTrue(client._credentials is scoped_creds)
expected_scopes = [MUT.DATA_SCOPE]
self.assertEqual(credentials._called, [
('create_scoped', (expected_scopes,), {}),
])
# SignedJwtAssertionCredentials() called with only kwargs
signed_creds_kw = {
'private_key': private_key,
'service_account_name': client_email,
}
signed_creds.check_called(self, [()], [signed_creds_kw])
# Load private key (via _get_contents) from the key path.
mock_get_contents.check_called(self, [(private_key_path,)])

def test_credentials_getter(self):
from gcloud_bigtable._testing import _MockWithAttachedMethods

Expand Down Expand Up @@ -419,3 +503,20 @@ def result_method(client):
self._grpc_client_test_helper('ListClusters', result_method,
request_pb, response_pb, expected_result,
PROJECT_ID)


class Test__get_contents(unittest2.TestCase):

def _callFUT(self, filename):
from gcloud_bigtable.client import _get_contents
return _get_contents(filename)

def test_it(self):
import tempfile

filename = tempfile.mktemp()
contents = b'foobar'
with open(filename, 'wb') as file_obj:
file_obj.write(contents)

self.assertEqual(self._callFUT(filename), contents)
1 change: 0 additions & 1 deletion system_tests/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from __future__ import print_function

import os
import time
import unittest2

from oauth2client.client import GoogleCredentials
Expand Down

0 comments on commit 6250668

Please sign in to comment.