Skip to content

Commit 697d50c

Browse files
committed
Adding api.delete() and removing Key.delete().
Also: - Changing api.get() back to accept only `keys` and returns only a list (a lot of headache for not much gain). - Factored out behavior to extract shared dataset_id from a set of keys into _get_dataset_id_from_keys(). - Updated docstrings and other tests that rely on changed / removed methods. See googleapis#518 for some context.
1 parent bfb8c7d commit 697d50c

File tree

11 files changed

+170
-146
lines changed

11 files changed

+170
-146
lines changed

gcloud/datastore/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from gcloud import credentials
5252
from gcloud.datastore import _implicit_environ
5353
from gcloud.datastore.api import allocate_ids
54+
from gcloud.datastore.api import delete
5455
from gcloud.datastore.api import get
5556
from gcloud.datastore.batch import Batch
5657
from gcloud.datastore.connection import Connection

gcloud/datastore/api.py

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
Query objects rather than via protobufs.
1919
"""
2020

21-
import collections
22-
2321
from gcloud.datastore import _implicit_environ
22+
from gcloud.datastore.batch import _BATCHES
23+
from gcloud.datastore.batch import Batch
2424
from gcloud.datastore import helpers
2525

2626

@@ -60,12 +60,33 @@ def _require_connection(connection=None):
6060
return connection
6161

6262

63-
def get(key_or_keys, missing=None, deferred=None, connection=None):
63+
def _get_dataset_id_from_keys(keys):
64+
"""Determines dataset ID from a list of keys.
65+
66+
:type keys: list of :class:`gcloud.datastore.key.Key`
67+
:param keys: The keys from the same dataset.
68+
69+
:rtype: string
70+
:returns: The dataset ID of the keys.
71+
:raises: :class:`ValueError` if the key dataset IDs don't agree.
72+
"""
73+
dataset_id = keys[0].dataset_id
74+
# Rather than creating a list or set of all dataset IDs, we iterate
75+
# and check. We could allow the backend to check this for us if IDs
76+
# with no prefix worked (GoogleCloudPlatform/google-cloud-datastore#59)
77+
# or if we made sure that a prefix s~ or e~ was on each key.
78+
for key in keys[1:]:
79+
if key.dataset_id != dataset_id:
80+
raise ValueError('All keys in get must be from the same dataset.')
81+
82+
return dataset_id
83+
84+
85+
def get(keys, missing=None, deferred=None, connection=None):
6486
"""Retrieves entities, along with their attributes.
6587
66-
:type key_or_keys: list of :class:`gcloud.datastore.key.Key` or single
67-
:class:`gcloud.datastore.key.Key`
68-
:param key_or_keys: The key or keys to be retrieved from the datastore.
88+
:type keys: list of :class:`gcloud.datastore.key.Key`
89+
:param keys: The keys to be retrieved from the datastore.
6990
7091
:type missing: an empty list or None.
7192
:param missing: If a list is passed, the key-only entities returned
@@ -80,27 +101,14 @@ def get(key_or_keys, missing=None, deferred=None, connection=None):
80101
:type connection: :class:`gcloud.datastore.connection.Connection`
81102
:param connection: Optional. The connection used to connect to datastore.
82103
83-
:rtype: list of :class:`gcloud.datastore.entity.Entity`, single
84-
:class:`gcloud.datastore.entity.Entity`, or ``NoneType``
85-
:returns: The requested entities, or single entity.
104+
:rtype: list of :class:`gcloud.datastore.entity.Entity`
105+
:returns: The requested entities.
86106
"""
87-
if isinstance(key_or_keys, collections.Iterable):
88-
keys = key_or_keys
89-
else:
90-
keys = [key_or_keys]
91-
92107
if not keys:
93108
return []
94109

95110
connection = _require_connection(connection)
96-
dataset_id = keys[0].dataset_id
97-
# Rather than creating a list or set of all dataset IDs, we iterate
98-
# and check. We could allow the backend to check this for us if IDs
99-
# with no prefix worked (GoogleCloudPlatform/google-cloud-datastore#59)
100-
# or if we made sure that a prefix s~ or e~ was on each key.
101-
for key in keys[1:]:
102-
if key.dataset_id != dataset_id:
103-
raise ValueError('All keys in get must be from the same dataset.')
111+
dataset_id = _get_dataset_id_from_keys(keys)
104112

105113
entity_pbs = connection.lookup(
106114
dataset_id=dataset_id,
@@ -122,11 +130,33 @@ def get(key_or_keys, missing=None, deferred=None, connection=None):
122130
for entity_pb in entity_pbs:
123131
entities.append(helpers.entity_from_protobuf(entity_pb))
124132

125-
if keys is key_or_keys:
126-
return entities
127-
else:
128-
if entities:
129-
return entities[0]
133+
return entities
134+
135+
136+
def delete(keys, connection=None):
137+
"""Delete the keys in the Cloud Datastore.
138+
139+
:type keys: list of :class:`gcloud.datastore.key.Key`
140+
:param keys: The keys to be deleted from the datastore.
141+
142+
:type connection: :class:`gcloud.datastore.connection.Connection`
143+
:param connection: Optional connection used to connect to datastore.
144+
"""
145+
if not keys:
146+
return
147+
148+
connection = connection or _implicit_environ.CONNECTION
149+
150+
# We allow partial keys to attempt a delete, the backend will fail.
151+
current = _BATCHES.top
152+
in_batch = current is not None
153+
if not in_batch:
154+
dataset_id = _get_dataset_id_from_keys(keys)
155+
current = Batch(dataset_id=dataset_id, connection=connection)
156+
for key in keys:
157+
current.delete(key)
158+
if not in_batch:
159+
current.commit()
130160

131161

132162
def allocate_ids(incomplete_key, num_ids, connection=None):

gcloud/datastore/batch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class Batch(object):
8282
operations and the delete operatiuon into the same mutation, and send
8383
them to the server in a single API request::
8484
85-
>>> from gcloud import datastore
85+
>>> from gcloud.datastore.batch import Batch
8686
>>> batch = Batch()
8787
>>> batch.put(entity1)
8888
>>> batch.put(entity2)
@@ -102,7 +102,7 @@ class Batch(object):
102102
103103
>>> from gcloud import datastore
104104
>>> dataset = datastore.get_dataset('dataset-id')
105-
>>> with Batch as batch:
105+
>>> with Batch() as batch:
106106
... do_some_work(batch)
107107
... raise Exception() # rolls back
108108
"""

gcloud/datastore/connection.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,7 @@ def delete_entities(self, dataset_id, key_pbs, mutation=None):
506506
This method deals only with
507507
:class:`gcloud.datastore.datastore_v1_pb2.Key` protobufs and not
508508
with any of the other abstractions. For example, it's used
509-
under the hood in the
510-
:meth:`gcloud.datastore.entity.Entity.delete` method.
509+
under the hood in :func:`gcloud.datastore.api.delete`.
511510
512511
:type dataset_id: string
513512
:param dataset_id: The ID of the dataset from which to delete the keys.

gcloud/datastore/demo/demo.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
print(datastore.get([toy.key]))
3535

3636
# And we should be able to delete it...
37-
toy.key.delete()
37+
datastore.delete([toy.key])
3838

3939
# Since we deleted it, if we do another lookup it shouldn't be there again:
4040
print(datastore.get([toy.key]))
@@ -48,10 +48,10 @@
4848
(4567, 'Printer', 11),
4949
(5678, 'Printer', 12),
5050
(6789, 'Computer', 13)]
51-
samples = []
51+
sample_keys = []
5252
for id, name, age in SAMPLE_DATA:
5353
key = datastore.Key('Thing', id)
54-
samples.append(key)
54+
sample_keys.append(key)
5555
entity = datastore.Entity(key)
5656
entity['name'] = name
5757
entity['age'] = age
@@ -72,7 +72,7 @@
7272
print(list(query.fetch()))
7373

7474
# Now delete them.
75-
print([key.delete() for key in samples])
75+
datastore.delete(sample_keys)
7676

7777
# You can also work inside a transaction.
7878
# (Check the official docs for explanations of what's happening here.)
@@ -92,7 +92,7 @@
9292
print('Committing the transaction...')
9393

9494
# Now that the transaction is commited, let's delete the entities.
95-
print(key.delete(), key2.delete())
95+
datastore.delete([key, key2])
9696

9797
# To rollback a transaction, just call .rollback()
9898
with datastore.Transaction() as t:
@@ -116,4 +116,4 @@
116116
print(thing.key) # This will be complete
117117

118118
# Now let's delete the entity.
119-
thing.key.delete()
119+
datastore.delete([thing.key])

gcloud/datastore/key.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -217,19 +217,6 @@ def to_protobuf(self):
217217

218218
return key
219219

220-
def delete(self, connection=None):
221-
"""Delete the key in the Cloud Datastore.
222-
223-
:type connection: :class:`gcloud.datastore.connection.Connection`
224-
:param connection: Optional connection used to connect to datastore.
225-
"""
226-
# We allow partial keys to attempt a delete, the backend will fail.
227-
connection = connection or _implicit_environ.CONNECTION
228-
connection.delete_entities(
229-
dataset_id=self.dataset_id,
230-
key_pbs=[self.to_protobuf()],
231-
)
232-
233220
@property
234221
def is_partial(self):
235222
"""Boolean indicating if the key has an ID (or name).

gcloud/datastore/test_api.py

Lines changed: 95 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,15 @@ def test_get_no_keys(self):
101101
results = self._callFUT([])
102102
self.assertEqual(results, [])
103103

104-
def _miss_helper(self, expected_results, use_list=True):
104+
def test_get_miss(self):
105105
from gcloud.datastore.key import Key
106106
from gcloud.datastore.test_connection import _Connection
107107

108108
DATASET_ID = 'DATASET'
109109
connection = _Connection()
110110
key = Key('Kind', 1234, dataset_id=DATASET_ID)
111-
if use_list:
112-
key = [key]
113-
results = self._callFUT(key, connection=connection)
114-
self.assertEqual(results, expected_results)
115-
116-
def test_get_miss(self):
117-
self._miss_helper([], use_list=True)
118-
119-
def test_get_miss_single_key(self):
120-
self._miss_helper(None, use_list=False)
111+
results = self._callFUT([key], connection=connection)
112+
self.assertEqual(results, [])
121113

122114
def test_get_miss_w_missing(self):
123115
from gcloud.datastore import datastore_v1_pb2 as datastore_pb
@@ -248,33 +240,6 @@ def test_get_hit_multiple_keys_different_dataset(self):
248240
with self.assertRaises(ValueError):
249241
self._callFUT([key1, key2], connection=object())
250242

251-
def test_get_hit_single_key(self):
252-
from gcloud.datastore.key import Key
253-
from gcloud.datastore.test_connection import _Connection
254-
255-
DATASET_ID = 'DATASET'
256-
KIND = 'Kind'
257-
ID = 1234
258-
PATH = [{'kind': KIND, 'id': ID}]
259-
260-
# Make a found entity pb to be returned from mock backend.
261-
entity_pb = self._make_entity_pb(DATASET_ID, KIND, ID,
262-
'foo', 'Foo')
263-
264-
# Make a connection to return the entity pb.
265-
connection = _Connection(entity_pb)
266-
267-
key = Key(KIND, ID, dataset_id=DATASET_ID)
268-
result = self._callFUT(key, connection=connection)
269-
new_key = result.key
270-
271-
# Check the returned value is as expected.
272-
self.assertFalse(new_key is key)
273-
self.assertEqual(new_key.dataset_id, DATASET_ID)
274-
self.assertEqual(new_key.path, PATH)
275-
self.assertEqual(list(result), ['foo'])
276-
self.assertEqual(result['foo'], 'Foo')
277-
278243
def test_get_implicit(self):
279244
from gcloud.datastore import _implicit_environ
280245
from gcloud.datastore.key import Key
@@ -313,6 +278,98 @@ def test_get_implicit(self):
313278
self.assertEqual(result['foo'], 'Foo')
314279

315280

281+
class Test_delete_function(unittest2.TestCase):
282+
283+
def _callFUT(self, keys, connection=None):
284+
from gcloud.datastore.api import delete
285+
return delete(keys, connection=connection)
286+
287+
def test_delete_no_batch(self):
288+
from gcloud.datastore.test_batch import _Connection
289+
from gcloud.datastore.test_batch import _Key
290+
291+
# Build basic mocks needed to delete.
292+
_DATASET = 'DATASET'
293+
connection = _Connection()
294+
key = _Key(_DATASET)
295+
296+
result = self._callFUT([key], connection=connection)
297+
self.assertEqual(result, None)
298+
299+
def test_delete_existing_batch(self):
300+
from gcloud._testing import _Monkey
301+
from gcloud.datastore import api
302+
from gcloud.datastore.batch import _Batches
303+
from gcloud.datastore.batch import Batch
304+
from gcloud.datastore.test_batch import _Connection
305+
from gcloud.datastore.test_batch import _Key
306+
307+
# Build basic mocks needed to delete.
308+
_DATASET = 'DATASET'
309+
connection = _Connection()
310+
key = _Key(_DATASET)
311+
312+
# Set up mock Batch on stack so we can check it is used.
313+
_BATCHES = _Batches()
314+
CURR_BATCH = Batch(dataset_id=_DATASET, connection=connection)
315+
_BATCHES.push(CURR_BATCH)
316+
317+
with _Monkey(api, _BATCHES=_BATCHES):
318+
result = self._callFUT([key], connection=connection)
319+
320+
self.assertEqual(result, None)
321+
self.assertEqual(
322+
connection._deleted,
323+
[(_DATASET, [key._key], CURR_BATCH.mutation)])
324+
325+
def test_delete_implicit_connection(self):
326+
from gcloud._testing import _Monkey
327+
from gcloud.datastore import _implicit_environ
328+
from gcloud.datastore import api
329+
from gcloud.datastore.batch import _Batches
330+
from gcloud.datastore.batch import Batch
331+
from gcloud.datastore.test_batch import _Connection
332+
from gcloud.datastore.test_batch import _Key
333+
334+
# Build basic mocks needed to delete.
335+
_DATASET = 'DATASET'
336+
connection = _Connection()
337+
key = _Key(_DATASET)
338+
339+
# Set up mock Batch on stack so we can check it is used.
340+
_BATCHES = _Batches()
341+
342+
with _Monkey(_implicit_environ, CONNECTION=connection):
343+
CURR_BATCH = Batch(dataset_id=_DATASET)
344+
_BATCHES.push(CURR_BATCH)
345+
with _Monkey(api, _BATCHES=_BATCHES):
346+
result = self._callFUT([key])
347+
348+
self.assertEqual(result, None)
349+
self.assertEqual(
350+
connection._deleted,
351+
[(_DATASET, [key._key], CURR_BATCH.mutation)])
352+
353+
def test_delete_no_keys(self):
354+
from gcloud.datastore import _implicit_environ
355+
356+
self.assertEqual(_implicit_environ.CONNECTION, None)
357+
result = self._callFUT([])
358+
self.assertEqual(result, None)
359+
360+
def test_delete_no_connection(self):
361+
from gcloud.datastore import _implicit_environ
362+
from gcloud.datastore.test_batch import _Key
363+
364+
# Build basic mocks needed to delete.
365+
_DATASET = 'DATASET'
366+
key = _Key(_DATASET)
367+
368+
self.assertEqual(_implicit_environ.CONNECTION, None)
369+
with self.assertRaises(ValueError):
370+
self._callFUT([key])
371+
372+
316373
class Test_allocate_ids_function(unittest2.TestCase):
317374

318375
def _callFUT(self, incomplete_key, num_ids, connection=None):

gcloud/datastore/test_connection.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,11 +1229,6 @@ def allocate_ids(self, dataset_id, key_pbs):
12291229
num_pbs = len(key_pbs)
12301230
return [_KeyProto(i) for i in range(num_pbs)]
12311231

1232-
def delete_entities(self, dataset_id, key_pbs):
1233-
self._called_dataset_id = dataset_id
1234-
self._called_key_pbs = key_pbs
1235-
return True
1236-
12371232

12381233
class _PathElementProto(object):
12391234

0 commit comments

Comments
 (0)