Skip to content

Commit 6b1b431

Browse files
committed
Expose 'missing'/'deferred' in 'Connection.lookup'/'Dataset.get_entities'.
If 'deferred' list is not passed, the connection retries any keys in a deferred response. Closes #306.
1 parent ac999e8 commit 6b1b431

File tree

4 files changed

+283
-34
lines changed

4 files changed

+283
-34
lines changed

gcloud/datastore/connection.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def dataset(self, *args, **kwargs):
167167
kwargs['connection'] = self
168168
return Dataset(*args, **kwargs)
169169

170-
def lookup(self, dataset_id, key_pbs):
170+
def lookup(self, dataset_id, key_pbs, missing=None, deferred=None):
171171
"""Lookup keys from a dataset in the Cloud Datastore.
172172
173173
Maps the ``DatastoreService.Lookup`` protobuf RPC.
@@ -201,6 +201,16 @@ def lookup(self, dataset_id, key_pbs):
201201
(or a single Key)
202202
:param key_pbs: The key (or keys) to retrieve from the datastore.
203203
204+
:type missing: list or None.
205+
:param missing: If a list is passed, the key-only entities returned
206+
by the backend as "missing" will be copied into it.
207+
Use only as a keyword param.
208+
209+
:type deferred: list or None.
210+
:param deferred: If a list is passed, the keys returned
211+
by the backend as "deferred" will be copied into it.
212+
Use only as a keyword param.
213+
204214
:rtype: list of :class:`gcloud.datastore.datastore_v1_pb2.Entity`
205215
(or a single Entity)
206216
:returns: The entities corresponding to the keys provided.
@@ -219,10 +229,32 @@ def lookup(self, dataset_id, key_pbs):
219229
for key_pb in key_pbs:
220230
lookup_request.key.add().CopyFrom(key_pb)
221231

222-
lookup_response = self._rpc(dataset_id, 'lookup', lookup_request,
223-
datastore_pb.LookupResponse)
232+
results = []
233+
while True: # loop against possible deferred.
234+
lookup_response = self._rpc(dataset_id, 'lookup', lookup_request,
235+
datastore_pb.LookupResponse)
236+
237+
results.extend(
238+
[result.entity for result in lookup_response.found])
239+
240+
if missing is not None:
241+
missing.extend(
242+
[result.entity for result in lookup_response.missing])
243+
244+
if deferred is not None:
245+
deferred.extend([key for key in lookup_response.deferred])
246+
break
247+
248+
if lookup_response.deferred: # retry
249+
for old_key in list(lookup_request.key):
250+
lookup_request.key.remove(old_key)
251+
for def_key in lookup_response.deferred:
252+
lookup_request.key.add().CopyFrom(def_key)
253+
else:
254+
break
224255

225-
results = [result.entity for result in lookup_response.found]
256+
# Hmm, should we sleep here? Asked in:
257+
# https://github.com/GoogleCloudPlatform/gcloud-python/issues/306#issuecomment-67377587
226258

227259
if single_key:
228260
if results:

gcloud/datastore/dataset.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,41 @@ def get_entity(self, key_or_path):
142142
if entities:
143143
return entities[0]
144144

145-
def get_entities(self, keys):
145+
def get_entities(self, keys, missing=None, deferred=None):
146146
"""Retrieves entities from the dataset, along with their attributes.
147147
148148
:type key: list of :class:`gcloud.datastore.key.Key`
149149
:param item_name: The name of the item to retrieve.
150150
151+
:type missing: list or None.
152+
:param missing: If a list is passed, the key-only entities returned
153+
by the backend as "missing" will be copied into it.
154+
Use only as a keyword param.
155+
156+
:type deferred: list or None.
157+
:param deferred: If a list is passed, the keys returned
158+
by the backend as "deferred" will be copied into it.
159+
Use only as a keyword param.
160+
151161
:rtype: list of :class:`gcloud.datastore.entity.Entity`
152162
:return: The requested entities.
153163
"""
154164
entity_pbs = self.connection().lookup(
155165
dataset_id=self.id(),
156-
key_pbs=[k.to_protobuf() for k in keys]
166+
key_pbs=[k.to_protobuf() for k in keys],
167+
missing=missing, deferred=deferred,
157168
)
158169

170+
if missing is not None:
171+
missing[:] = [
172+
helpers.entity_from_protobuf(missed_pb, dataset=self)
173+
for missed_pb in missing]
174+
175+
if deferred is not None:
176+
deferred[:] = [
177+
helpers.key_from_protobuf(deferred_pb)
178+
for deferred_pb in deferred]
179+
159180
entities = []
160181
for entity_pb in entity_pbs:
161182
entities.append(helpers.entity_from_protobuf(

gcloud/datastore/test_connection.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,145 @@ def test_lookup_multiple_keys_empty_response(self):
306306
self.assertEqual(keys[0], key_pb1)
307307
self.assertEqual(keys[1], key_pb2)
308308

309+
def test_lookup_multiple_keys_w_missing(self):
310+
from gcloud.datastore.connection import datastore_pb
311+
from gcloud.datastore.key import Key
312+
313+
DATASET_ID = 'DATASET'
314+
key_pb1 = Key(path=[{'kind': 'Kind', 'id': 1234}]).to_protobuf()
315+
key_pb2 = Key(path=[{'kind': 'Kind', 'id': 2345}]).to_protobuf()
316+
rsp_pb = datastore_pb.LookupResponse()
317+
er_1 = rsp_pb.missing.add()
318+
er_1.entity.key.CopyFrom(key_pb1)
319+
er_2 = rsp_pb.missing.add()
320+
er_2.entity.key.CopyFrom(key_pb2)
321+
conn = self._makeOne()
322+
URI = '/'.join([
323+
conn.API_BASE_URL,
324+
'datastore',
325+
conn.API_VERSION,
326+
'datasets',
327+
DATASET_ID,
328+
'lookup',
329+
])
330+
http = conn._http = Http({'status': '200'}, rsp_pb.SerializeToString())
331+
missing = []
332+
result = conn.lookup(DATASET_ID, [key_pb1, key_pb2], missing=missing)
333+
self.assertEqual(result, [])
334+
self.assertEqual([missed.key for missed in missing],
335+
[key_pb1, key_pb2])
336+
cw = http._called_with
337+
self.assertEqual(cw['uri'], URI)
338+
self.assertEqual(cw['method'], 'POST')
339+
self.assertEqual(cw['headers']['Content-Type'],
340+
'application/x-protobuf')
341+
self.assertEqual(cw['headers']['User-Agent'], conn.USER_AGENT)
342+
rq_class = datastore_pb.LookupRequest
343+
request = rq_class()
344+
request.ParseFromString(cw['body'])
345+
keys = list(request.key)
346+
self.assertEqual(len(keys), 2)
347+
self.assertEqual(keys[0], key_pb1)
348+
self.assertEqual(keys[1], key_pb2)
349+
350+
def test_lookup_multiple_keys_w_deferred(self):
351+
from gcloud.datastore.connection import datastore_pb
352+
from gcloud.datastore.key import Key
353+
354+
DATASET_ID = 'DATASET'
355+
key_pb1 = Key(path=[{'kind': 'Kind', 'id': 1234}]).to_protobuf()
356+
key_pb2 = Key(path=[{'kind': 'Kind', 'id': 2345}]).to_protobuf()
357+
rsp_pb = datastore_pb.LookupResponse()
358+
rsp_pb.deferred.add().CopyFrom(key_pb1)
359+
rsp_pb.deferred.add().CopyFrom(key_pb2)
360+
conn = self._makeOne()
361+
URI = '/'.join([
362+
conn.API_BASE_URL,
363+
'datastore',
364+
conn.API_VERSION,
365+
'datasets',
366+
DATASET_ID,
367+
'lookup',
368+
])
369+
http = conn._http = Http({'status': '200'}, rsp_pb.SerializeToString())
370+
deferred = []
371+
result = conn.lookup(DATASET_ID, [key_pb1, key_pb2], deferred=deferred)
372+
self.assertEqual(result, [])
373+
self.assertEqual([def_key for def_key in deferred], [key_pb1, key_pb2])
374+
cw = http._called_with
375+
self.assertEqual(cw['uri'], URI)
376+
self.assertEqual(cw['method'], 'POST')
377+
self.assertEqual(cw['headers']['Content-Type'],
378+
'application/x-protobuf')
379+
self.assertEqual(cw['headers']['User-Agent'], conn.USER_AGENT)
380+
rq_class = datastore_pb.LookupRequest
381+
request = rq_class()
382+
request.ParseFromString(cw['body'])
383+
keys = list(request.key)
384+
self.assertEqual(len(keys), 2)
385+
self.assertEqual(keys[0], key_pb1)
386+
self.assertEqual(keys[1], key_pb2)
387+
388+
def test_lookup_multiple_keys_w_deferred_from_backend_but_not_passed(self):
389+
from gcloud.datastore.connection import datastore_pb
390+
from gcloud.datastore.key import Key
391+
392+
DATASET_ID = 'DATASET'
393+
key_pb1 = Key(path=[{'kind': 'Kind', 'id': 1234}]).to_protobuf()
394+
key_pb2 = Key(path=[{'kind': 'Kind', 'id': 2345}]).to_protobuf()
395+
rsp_pb1 = datastore_pb.LookupResponse()
396+
entity1 = datastore_pb.Entity()
397+
entity1.key.CopyFrom(key_pb1)
398+
rsp_pb1.found.add(entity=entity1)
399+
rsp_pb1.deferred.add().CopyFrom(key_pb2)
400+
rsp_pb2 = datastore_pb.LookupResponse()
401+
entity2 = datastore_pb.Entity()
402+
entity2.key.CopyFrom(key_pb2)
403+
rsp_pb2.found.add(entity=entity2)
404+
conn = self._makeOne()
405+
URI = '/'.join([
406+
conn.API_BASE_URL,
407+
'datastore',
408+
conn.API_VERSION,
409+
'datasets',
410+
DATASET_ID,
411+
'lookup',
412+
])
413+
http = conn._http = HttpMultiple(
414+
({'status': '200'}, rsp_pb1.SerializeToString()),
415+
({'status': '200'}, rsp_pb2.SerializeToString()),
416+
)
417+
found = conn.lookup(DATASET_ID, [key_pb1, key_pb2])
418+
self.assertEqual(len(found), 2)
419+
self.assertEqual(found[0].key.path_element[0].kind, 'Kind')
420+
self.assertEqual(found[0].key.path_element[0].id, 1234)
421+
self.assertEqual(found[1].key.path_element[0].kind, 'Kind')
422+
self.assertEqual(found[1].key.path_element[0].id, 2345)
423+
cw = http._called_with
424+
rq_class = datastore_pb.LookupRequest
425+
request = rq_class()
426+
self.assertEqual(len(cw), 2)
427+
self.assertEqual(cw[0]['uri'], URI)
428+
self.assertEqual(cw[0]['method'], 'POST')
429+
self.assertEqual(cw[0]['headers']['Content-Type'],
430+
'application/x-protobuf')
431+
self.assertEqual(cw[0]['headers']['User-Agent'], conn.USER_AGENT)
432+
request.ParseFromString(cw[0]['body'])
433+
keys = list(request.key)
434+
self.assertEqual(len(keys), 2)
435+
self.assertEqual(keys[0], key_pb1)
436+
self.assertEqual(keys[1], key_pb2)
437+
438+
self.assertEqual(cw[1]['uri'], URI)
439+
self.assertEqual(cw[1]['method'], 'POST')
440+
self.assertEqual(cw[1]['headers']['Content-Type'],
441+
'application/x-protobuf')
442+
self.assertEqual(cw[1]['headers']['User-Agent'], conn.USER_AGENT)
443+
request.ParseFromString(cw[1]['body'])
444+
keys = list(request.key)
445+
self.assertEqual(len(keys), 1)
446+
self.assertEqual(keys[0], key_pb2)
447+
309448
def test_run_query_wo_namespace_empty_result(self):
310449
from gcloud.datastore.connection import datastore_pb
311450
from gcloud.datastore.query import Query
@@ -901,3 +1040,15 @@ def __init__(self, headers, content):
9011040
def request(self, **kw):
9021041
self._called_with = kw
9031042
return self._headers, self._content
1043+
1044+
1045+
class HttpMultiple(object):
1046+
1047+
def __init__(self, *responses):
1048+
self._called_with = []
1049+
self._responses = list(responses)
1050+
1051+
def request(self, **kw):
1052+
self._called_with.append(kw)
1053+
result, self._responses = self._responses[0], self._responses[1:]
1054+
return result

0 commit comments

Comments
 (0)