Skip to content

Commit 0860e59

Browse files
committed
Ideas commit for better (implicit) auth story in datastore.
Also - Integrates this idea into the datastore regression tests. - Creates a (temporarily ignored) cyclic import - Has an uncovered statement that confuses nosetests - Implements a friendlier (more gcloud-node like) version of allocated_ids - DOES NOT update CONTRIBUTING to explain change to regression tests or ditch unneeded helper / environ code for regressions - Creates a datastore cleanup script for when tests get broken
1 parent 2b73c7b commit 0860e59

File tree

14 files changed

+410
-34
lines changed

14 files changed

+410
-34
lines changed

CONTRIBUTING.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ Running Regression Tests
168168
`docs <https://cloud.google.com/storage/docs/authentication#generating-a-private-key>`__
169169
for explanation on how to get a private key.
170170

171+
DJH: THIS PART NEEDS TO BE UPDATED AFTER DISCUSSION OF IMPLICIT ENVIRON
172+
USE IN PRODUCTION CODE.
173+
171174
- Examples of these can be found in ``regression/local_test_setup.sample``. We
172175
recommend copying this to ``regression/local_test_setup``, editing the values
173176
and sourcing them into your environment::

gcloud/datastore/__init__.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,38 @@
3232
which represents a lookup or search over the rows in the datastore.
3333
"""
3434

35+
import os
36+
3537
__version__ = '0.1.2'
3638

3739
SCOPE = ('https://www.googleapis.com/auth/datastore ',
3840
'https://www.googleapis.com/auth/userinfo.email')
3941
"""The scope required for authenticating as a Cloud Datastore consumer."""
42+
DATASET = None
43+
"""Module global which allows users to only optionally use a dataset."""
44+
45+
46+
def get_local_dataset_settings():
47+
"""Determines auth settings from local enviroment.
48+
49+
Currently only supports enviroment variables but will implicitly
50+
support App Engine, Compute Engine and other environments in
51+
the future.
52+
53+
Local environment variables used are:
54+
- GCLOUD_DATASET_ID
55+
- GCLOUD_CLIENT_EMAIL
56+
- GCLOUD_KEY_FILE
57+
"""
58+
local_dataset_settings = (
59+
os.getenv('GCLOUD_DATASET_ID'),
60+
os.getenv('GCLOUD_CLIENT_EMAIL'),
61+
os.getenv('GCLOUD_KEY_FILE'),
62+
)
63+
if None in local_dataset_settings:
64+
return None
65+
else:
66+
return local_dataset_settings
4067

4168

4269
def get_connection(client_email, private_key_path):
@@ -102,3 +129,73 @@ def get_dataset(dataset_id, client_email, private_key_path):
102129
"""
103130
connection = get_connection(client_email, private_key_path)
104131
return connection.dataset(dataset_id)
132+
133+
134+
def _require_dataset():
135+
"""Convenience method to ensure DATASET is set.
136+
137+
:raises: :class:`EnvironmentError` if DATASET is not set.
138+
"""
139+
if DATASET is None:
140+
raise EnvironmentError('Dataset could not be implied.')
141+
142+
143+
def get_entity(key):
144+
"""Retrieves entity from implicit dataset, along with its attributes.
145+
146+
:type key: :class:`gcloud.datastore.key.Key`
147+
:param key: The name of the item to retrieve.
148+
149+
:rtype: :class:`gcloud.datastore.entity.Entity` or ``None``
150+
:return: The requested entity, or ``None`` if there was no match found.
151+
"""
152+
_require_dataset()
153+
return DATASET.get_entity(key)
154+
155+
156+
def get_entities(keys):
157+
"""Retrieves entities from implied dataset, along with their attributes.
158+
159+
:type keys: list of :class:`gcloud.datastore.key.Key`
160+
:param keys: The name of the item to retrieve.
161+
162+
:rtype: list of :class:`gcloud.datastore.entity.Entity`
163+
:return: The requested entities.
164+
"""
165+
_require_dataset()
166+
return DATASET.get_entities(keys)
167+
168+
169+
def allocate_ids(incomplete_key, num_ids):
170+
"""Allocates a list of IDs from a partial key.
171+
172+
:type incomplete_key: A :class:`gcloud.datastore.key.Key`
173+
:param incomplete_key: The partial key to use as base for allocated IDs.
174+
175+
:type num_ids: A :class:`int`.
176+
:param num_ids: The number of IDs to allocate.
177+
178+
:rtype: list of :class:`gcloud.datastore.key.Key`
179+
:return: The (complete) keys allocated with `incomplete_key` as root.
180+
"""
181+
_require_dataset()
182+
183+
if not incomplete_key.is_partial():
184+
raise ValueError(('Key is not partial.', incomplete_key))
185+
186+
incomplete_key_pb = incomplete_key.to_protobuf()
187+
incomplete_key_pbs = [incomplete_key_pb] * num_ids
188+
189+
allocated_key_pbs = DATASET.connection().allocate_ids(
190+
DATASET.id(), incomplete_key_pbs)
191+
allocated_ids = [allocated_key_pb.path_element[-1].id
192+
for allocated_key_pb in allocated_key_pbs]
193+
return [incomplete_key.id(allocated_id)
194+
for allocated_id in allocated_ids]
195+
196+
197+
# Set DATASET if it can be implied from the environment.
198+
LOCAL_DATASET_SETTINGS = get_local_dataset_settings()
199+
if LOCAL_DATASET_SETTINGS is not None:
200+
DATASET = get_dataset(*LOCAL_DATASET_SETTINGS)
201+
del LOCAL_DATASET_SETTINGS

gcloud/datastore/dataset.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def get_entity(self, key):
106106
"""Retrieves entity from the dataset, along with its attributes.
107107
108108
:type key: :class:`gcloud.datastore.key.Key`
109-
:param item_name: The name of the item to retrieve.
109+
:param key: The name of the item to retrieve.
110110
111111
:rtype: :class:`gcloud.datastore.entity.Entity` or ``None``
112112
:return: The requested entity, or ``None`` if there was no match found.
@@ -118,8 +118,8 @@ def get_entity(self, key):
118118
def get_entities(self, keys):
119119
"""Retrieves entities from the dataset, along with their attributes.
120120
121-
:type key: list of :class:`gcloud.datastore.key.Key`
122-
:param item_name: The name of the item to retrieve.
121+
:type keys: list of :class:`gcloud.datastore.key.Key`
122+
:param keys: The name of the item to retrieve.
123123
124124
:rtype: list of :class:`gcloud.datastore.entity.Entity`
125125
:return: The requested entities.

gcloud/datastore/entity.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
delete or persist the data stored on the entity.
1616
"""
1717

18+
import gcloud.datastore
1819
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
1920
from gcloud.datastore.key import Key
2021

@@ -73,6 +74,8 @@ class Entity(dict):
7374
def __init__(self, dataset=None, kind=None):
7475
super(Entity, self).__init__()
7576
self._dataset = dataset
77+
if self._dataset is None:
78+
self._dataset = gcloud.datastore.DATASET
7679
if kind:
7780
self._key = Key().kind(kind)
7881
else:

gcloud/datastore/query.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import base64
44

5+
import gcloud.datastore
56
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
67
from gcloud.datastore import helpers
78
from gcloud.datastore.key import Key
@@ -56,6 +57,8 @@ class Query(object):
5657

5758
def __init__(self, kind=None, dataset=None, namespace=None):
5859
self._dataset = dataset
60+
if self._dataset is None:
61+
self._dataset = gcloud.datastore.DATASET
5962
self._namespace = namespace
6063
self._pb = datastore_pb.Query()
6164
self._cursor = None

gcloud/datastore/test___init__.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,50 @@ def test_it(self):
3333
self.assertEqual(client._called_with, expected_called_with)
3434

3535

36+
class Test_get_local_dataset_settings(unittest2.TestCase):
37+
38+
def _callFUT(self):
39+
from gcloud.datastore import get_local_dataset_settings
40+
return get_local_dataset_settings()
41+
42+
def _test_with_environ(self, environ, expected_result):
43+
import os
44+
from gcloud._testing import _Monkey
45+
46+
def custom_getenv(key):
47+
return environ.get(key)
48+
49+
with _Monkey(os, getenv=custom_getenv):
50+
result = self._callFUT()
51+
52+
self.assertEqual(result, expected_result)
53+
54+
def test_all_set(self):
55+
# Fake auth variables.
56+
DATASET = 'dataset'
57+
CLIENT_EMAIL = '[email protected]'
58+
TEMP_PATH = 'fakepath'
59+
60+
# Make a custom getenv function to Monkey.
61+
VALUES = {
62+
'GCLOUD_DATASET_ID': DATASET,
63+
'GCLOUD_CLIENT_EMAIL': CLIENT_EMAIL,
64+
'GCLOUD_KEY_FILE': TEMP_PATH,
65+
}
66+
expected_result = (DATASET, CLIENT_EMAIL, TEMP_PATH)
67+
self._test_with_environ(VALUES, expected_result)
68+
69+
def test_partial_set(self):
70+
# Fake auth variables.
71+
DATASET = 'dataset'
72+
73+
# Make a custom getenv function to Monkey.
74+
VALUES = {
75+
'GCLOUD_DATASET_ID': DATASET,
76+
}
77+
self._test_with_environ(VALUES, None)
78+
79+
3680
class Test_get_dataset(unittest2.TestCase):
3781

3882
def _callFUT(self, dataset_id, client_email, private_key_path):
@@ -66,3 +110,160 @@ def test_it(self):
66110
'scope': SCOPE,
67111
}
68112
self.assertEqual(client._called_with, expected_called_with)
113+
114+
115+
class Test_implicit_behavior(unittest2.TestCase):
116+
117+
def test__require_dataset(self):
118+
import gcloud.datastore
119+
original_dataset = gcloud.datastore.DATASET
120+
121+
try:
122+
gcloud.datastore.DATASET = None
123+
self.assertRaises(EnvironmentError,
124+
gcloud.datastore._require_dataset)
125+
gcloud.datastore.DATASET = object()
126+
self.assertEqual(gcloud.datastore._require_dataset(), None)
127+
finally:
128+
gcloud.datastore.DATASET = original_dataset
129+
130+
def test_get_entity(self):
131+
import gcloud.datastore
132+
from gcloud.datastore.test_entity import _Dataset
133+
from gcloud._testing import _Monkey
134+
135+
CUSTOM_DATASET = _Dataset()
136+
DUMMY_KEY = object()
137+
DUMMY_VAL = object()
138+
CUSTOM_DATASET[DUMMY_KEY] = DUMMY_VAL
139+
with _Monkey(gcloud.datastore, DATASET=CUSTOM_DATASET):
140+
result = gcloud.datastore.get_entity(DUMMY_KEY)
141+
self.assertTrue(result is DUMMY_VAL)
142+
143+
def test_get_entities(self):
144+
import gcloud.datastore
145+
from gcloud.datastore.test_entity import _Dataset
146+
from gcloud._testing import _Monkey
147+
148+
class _ExtendedDataset(_Dataset):
149+
def get_entities(self, keys):
150+
return [self.get(key) for key in keys]
151+
152+
CUSTOM_DATASET = _ExtendedDataset()
153+
DUMMY_KEYS = [object(), object()]
154+
DUMMY_VALS = [object(), object()]
155+
for key, val in zip(DUMMY_KEYS, DUMMY_VALS):
156+
CUSTOM_DATASET[key] = val
157+
158+
with _Monkey(gcloud.datastore, DATASET=CUSTOM_DATASET):
159+
result = gcloud.datastore.get_entities(DUMMY_KEYS)
160+
self.assertTrue(result == DUMMY_VALS)
161+
162+
def test_allocate_ids(self):
163+
import gcloud.datastore
164+
from gcloud.datastore.test_entity import _Connection
165+
from gcloud.datastore.test_entity import _DATASET_ID
166+
from gcloud.datastore.test_entity import _Dataset
167+
from gcloud.datastore.test_entity import _Key
168+
from gcloud._testing import _Monkey
169+
170+
class _PathElementProto(object):
171+
COUNTER = 0
172+
173+
def __init__(self):
174+
_PathElementProto.COUNTER += 1
175+
self.id = _PathElementProto.COUNTER
176+
177+
class _KeyProto(object):
178+
179+
def __init__(self):
180+
self.path_element = [_PathElementProto()]
181+
182+
class _ExtendedKey(_Key):
183+
def id(self, id_to_set):
184+
self._called_id = id_to_set
185+
return id_to_set
186+
187+
INCOMPLETE_KEY = _ExtendedKey()
188+
INCOMPLETE_KEY._key = _KeyProto()
189+
INCOMPLETE_KEY._partial = True
190+
NUM_IDS = 2
191+
192+
class _ExtendedConnection(_Connection):
193+
def allocate_ids(self, dataset_id, key_pbs):
194+
self._called_dataset_id = dataset_id
195+
self._called_key_pbs = key_pbs
196+
return key_pbs
197+
198+
CUSTOM_CONNECTION = _ExtendedConnection()
199+
CUSTOM_DATASET = _Dataset(connection=CUSTOM_CONNECTION)
200+
with _Monkey(gcloud.datastore, DATASET=CUSTOM_DATASET):
201+
result = gcloud.datastore.allocate_ids(INCOMPLETE_KEY, NUM_IDS)
202+
203+
self.assertEqual(_PathElementProto.COUNTER, 1)
204+
self.assertEqual(result, [1, 1])
205+
self.assertEqual(CUSTOM_CONNECTION._called_dataset_id, _DATASET_ID)
206+
self.assertEqual(len(CUSTOM_CONNECTION._called_key_pbs), 2)
207+
key_paths = [key_pb.path_element[-1].id
208+
for key_pb in CUSTOM_CONNECTION._called_key_pbs]
209+
self.assertEqual(key_paths, [1, 1])
210+
211+
def test_allocate_ids_with_complete(self):
212+
import gcloud.datastore
213+
from gcloud.datastore.test_entity import _Connection
214+
from gcloud.datastore.test_entity import _Dataset
215+
from gcloud.datastore.test_entity import _Key
216+
from gcloud._testing import _Monkey
217+
218+
COMPLETE_KEY = _Key()
219+
NUM_IDS = 2
220+
CUSTOM_CONNECTION = _Connection()
221+
CUSTOM_DATASET = _Dataset(connection=CUSTOM_CONNECTION)
222+
with _Monkey(gcloud.datastore, DATASET=CUSTOM_DATASET):
223+
self.assertRaises(ValueError, gcloud.datastore.allocate_ids,
224+
COMPLETE_KEY, NUM_IDS)
225+
226+
def test_set_DATASET(self):
227+
import os
228+
import tempfile
229+
from gcloud import credentials
230+
from gcloud.test_credentials import _Client
231+
from gcloud._testing import _Monkey
232+
233+
# Make custom client for doing auth.
234+
client = _Client()
235+
236+
# Fake auth variables.
237+
CLIENT_EMAIL = '[email protected]'
238+
PRIVATE_KEY = 'SEEkR1t'
239+
DATASET = 'dataset'
240+
241+
# Write the fake key to a temp file.
242+
TEMP_PATH = tempfile.mktemp()
243+
with open(TEMP_PATH, 'w') as file_obj:
244+
file_obj.write(PRIVATE_KEY)
245+
file_obj.flush()
246+
247+
# Make a custom getenv function to Monkey.
248+
VALUES = {
249+
'GCLOUD_DATASET_ID': DATASET,
250+
'GCLOUD_CLIENT_EMAIL': CLIENT_EMAIL,
251+
'GCLOUD_KEY_FILE': TEMP_PATH,
252+
}
253+
254+
def custom_getenv(key):
255+
return VALUES.get(key)
256+
257+
# Perform the import again with our test patches.
258+
with _Monkey(credentials, client=client):
259+
with _Monkey(os, getenv=custom_getenv):
260+
import gcloud.datastore
261+
reload(gcloud.datastore)
262+
263+
# Check that the DATASET was correctly implied from the environ.
264+
implicit_dataset = gcloud.datastore.DATASET
265+
self.assertEqual(implicit_dataset.id(), DATASET)
266+
# Check that the credentials on the implicit DATASET was set on the
267+
# fake client.
268+
credentials = implicit_dataset.connection().credentials
269+
self.assertTrue(credentials is client._signed)

gcloud/datastore/test_entity.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
class TestEntity(unittest2.TestCase):
1010

1111
def _getTargetClass(self):
12+
import gcloud.datastore
13+
gcloud.datastore.DATASET = None
14+
1215
from gcloud.datastore.entity import Entity
1316

1417
return Entity

0 commit comments

Comments
 (0)