Skip to content

Commit aa7e011

Browse files
committed
Properly handling entity meanings in datastore.
To do this, added entity_to_protobuf method that could be used recursively. Also solves #1206 since recursively serializing nested entities to protobuf was being done incorrectly. Fixes #1065. Fixes #1206.
1 parent 21101e0 commit aa7e011

File tree

5 files changed

+358
-73
lines changed

5 files changed

+358
-73
lines changed

gcloud/datastore/batch.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,6 @@ def _assign_entity_to_mutation(mutation_pb, entity, auto_id_entities):
251251
during commit.
252252
"""
253253
auto_id = entity.key.is_partial
254-
255-
key_pb = entity.key.to_protobuf()
256-
key_pb = helpers._prepare_key_for_request(key_pb)
257-
258254
if auto_id:
259255
insert = mutation_pb.insert_auto_id.add()
260256
auto_id_entities.append(entity)
@@ -264,24 +260,7 @@ def _assign_entity_to_mutation(mutation_pb, entity, auto_id_entities):
264260
# based on prior existence / removal of the entity.
265261
insert = mutation_pb.upsert.add()
266262

267-
insert.key.CopyFrom(key_pb)
268-
269-
for name, value in entity.items():
270-
271-
value_is_list = isinstance(value, list)
272-
if value_is_list and len(value) == 0:
273-
continue
274-
275-
prop = insert.property.add()
276-
# Set the name of the property.
277-
prop.name = name
278-
279-
# Set the appropriate value.
280-
helpers._set_protobuf_value(prop.value, value)
281-
282-
if name in entity.exclude_from_indexes:
283-
if not value_is_list:
284-
prop.value.indexed = False
285-
286-
for sub_value in prop.value.list_value:
287-
sub_value.indexed = False
263+
entity_pb = helpers.entity_to_protobuf(entity)
264+
key_pb = helpers._prepare_key_for_request(entity_pb.key)
265+
entity_pb.key.CopyFrom(key_pb)
266+
insert.CopyFrom(entity_pb)

gcloud/datastore/entity.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def __init__(self, key=None, exclude_from_indexes=()):
8181
self.key = key
8282
self._exclude_from_indexes = set(_ensure_tuple_or_list(
8383
'exclude_from_indexes', exclude_from_indexes))
84+
# NOTE: This will be populated when parsing a protobuf in
85+
# gcloud.datastore.helpers.entity_from_protobuf.
86+
self._meanings = {}
8487

8588
def __eq__(self, other):
8689
"""Compare two entities for equality.

gcloud/datastore/helpers.py

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,47 @@ def find_true_dataset_id(dataset_id, connection):
7373
return returned_pb.key.partition_id.dataset_id
7474

7575

76+
def _get_meaning(value_pb, is_list=False):
77+
"""Get the meaning from a protobuf value.
78+
79+
:type value_pb: :class:`gcloud.datastore._datastore_v1_pb2.Value`
80+
:param value_pb: The protobuf value to be checked for an
81+
associated meaning.
82+
83+
:type is_list: bool
84+
:param is_list: Boolean indicating if the ``value_pb`` contains
85+
a list value.
86+
87+
:rtype: int
88+
:returns: The meaning for the ``value_pb`` if one is set, else
89+
:data:`None`.
90+
:raises: :class:`ValueError <exceptions.ValueError>` if a list value
91+
has disagreeing meanings (in sub-elements) or has some
92+
elements with meanings and some without.
93+
"""
94+
meaning = None
95+
if is_list:
96+
all_meaning = True
97+
for sub_value_pb in value_pb.list_value:
98+
if sub_value_pb.HasField('meaning'):
99+
if meaning is None:
100+
meaning = sub_value_pb.meaning
101+
elif meaning != sub_value_pb.meaning:
102+
raise ValueError('Different meanings set on values '
103+
'within a list_value')
104+
else:
105+
all_meaning = False
106+
107+
if meaning is not None:
108+
if not all_meaning:
109+
raise ValueError('A list_value contained some values with '
110+
'and some without a meaning')
111+
elif value_pb.HasField('meaning'):
112+
meaning = value_pb.meaning
113+
114+
return meaning
115+
116+
76117
def entity_from_protobuf(pb):
77118
"""Factory method for creating an entity based on a protobuf.
78119
@@ -90,11 +131,19 @@ def entity_from_protobuf(pb):
90131
key = key_from_protobuf(pb.key)
91132

92133
entity_props = {}
134+
entity_meanings = {}
93135
exclude_from_indexes = []
94136

95137
for property_pb in pb.property:
96-
value = _get_value_from_property_pb(property_pb)
97-
entity_props[property_pb.name] = value
138+
value = _get_value_from_value_pb(property_pb.value)
139+
prop_name = property_pb.name
140+
entity_props[prop_name] = value
141+
142+
# Check if the property has an associated meaning.
143+
meaning = _get_meaning(property_pb.value,
144+
is_list=isinstance(value, list))
145+
if meaning is not None:
146+
entity_meanings[prop_name] = (meaning, value)
98147

99148
# Check if property_pb.value was indexed. Lists need to be
100149
# special-cased and we require all `indexed` values in a list agree.
@@ -106,16 +155,67 @@ def entity_from_protobuf(pb):
106155
'be indexed or all excluded from indexes.')
107156

108157
if not indexed_values.pop():
109-
exclude_from_indexes.append(property_pb.name)
158+
exclude_from_indexes.append(prop_name)
110159
else:
111160
if not property_pb.value.indexed:
112-
exclude_from_indexes.append(property_pb.name)
161+
exclude_from_indexes.append(prop_name)
113162

114163
entity = Entity(key=key, exclude_from_indexes=exclude_from_indexes)
115164
entity.update(entity_props)
165+
entity._meanings.update(entity_meanings)
116166
return entity
117167

118168

169+
def entity_to_protobuf(entity):
170+
"""Converts an entity into a protobuf.
171+
172+
:type entity: :class:`gcloud.datastore.entity.Entity`
173+
:param entity: The entity to be turned into a protobuf.
174+
175+
:rype: :class:`gcloud.datastore._datastore_v1_pb2.Entity`
176+
:returns: The Protobuf representing the entity.
177+
"""
178+
entity_pb = datastore_pb.Entity()
179+
if entity.key is not None:
180+
key_pb = entity.key.to_protobuf()
181+
entity_pb.key.CopyFrom(key_pb)
182+
183+
for name, value in entity.items():
184+
value_is_list = isinstance(value, list)
185+
if value_is_list and len(value) == 0:
186+
continue
187+
188+
prop = entity_pb.property.add()
189+
# Set the name of the property.
190+
prop.name = name
191+
192+
# Set the appropriate value.
193+
_set_protobuf_value(prop.value, value)
194+
195+
# Add index information to protobuf.
196+
if name in entity.exclude_from_indexes:
197+
if not value_is_list:
198+
prop.value.indexed = False
199+
200+
for sub_value in prop.value.list_value:
201+
sub_value.indexed = False
202+
203+
# Add meaning information to protobuf.
204+
if name in entity._meanings:
205+
meaning, orig_value = entity._meanings[name]
206+
# Only add the meaning back to the protobuf if the value is
207+
# unchanged from when it was originally read from the API.
208+
if orig_value is value:
209+
# For lists, we set meaning on each sub-element.
210+
if value_is_list:
211+
for sub_value_pb in prop.value.list_value:
212+
sub_value_pb.meaning = meaning
213+
else:
214+
prop.value.meaning = meaning
215+
216+
return entity_pb
217+
218+
119219
def key_from_protobuf(pb):
120220
"""Factory method for creating a key based on a protobuf.
121221
@@ -248,29 +348,12 @@ def _get_value_from_value_pb(value_pb):
248348
result = entity_from_protobuf(value_pb.entity_value)
249349

250350
elif value_pb.list_value:
251-
result = [_get_value_from_value_pb(x) for x in value_pb.list_value]
351+
result = [_get_value_from_value_pb(value)
352+
for value in value_pb.list_value]
252353

253354
return result
254355

255356

256-
def _get_value_from_property_pb(property_pb):
257-
"""Given a protobuf for a Property, get the correct value.
258-
259-
The Cloud Datastore Protobuf API returns a Property Protobuf which
260-
has one value set and the rest blank. This function retrieves the
261-
the one value provided.
262-
263-
Some work is done to coerce the return value into a more useful type
264-
(particularly in the case of a timestamp value, or a key value).
265-
266-
:type property_pb: :class:`gcloud.datastore._datastore_v1_pb2.Property`
267-
:param property_pb: The Property Protobuf.
268-
269-
:returns: The value provided by the Protobuf.
270-
"""
271-
return _get_value_from_value_pb(property_pb.value)
272-
273-
274357
def _set_protobuf_value(value_pb, val):
275358
"""Assign 'val' to the correct subfield of 'value_pb'.
276359
@@ -285,7 +368,7 @@ def _set_protobuf_value(value_pb, val):
285368
286369
:type val: :class:`datetime.datetime`, boolean, float, integer, string,
287370
:class:`gcloud.datastore.key.Key`,
288-
:class:`gcloud.datastore.entity.Entity`,
371+
:class:`gcloud.datastore.entity.Entity`
289372
:param val: The value to be assigned.
290373
"""
291374
if val is None:
@@ -296,15 +379,8 @@ def _set_protobuf_value(value_pb, val):
296379
if attr == 'key_value':
297380
value_pb.key_value.CopyFrom(val)
298381
elif attr == 'entity_value':
299-
e_pb = value_pb.entity_value
300-
e_pb.Clear()
301-
key = val.key
302-
if key is not None:
303-
e_pb.key.CopyFrom(key.to_protobuf())
304-
for item_key, value in val.items():
305-
p_pb = e_pb.property.add()
306-
p_pb.name = item_key
307-
_set_protobuf_value(p_pb.value, value)
382+
entity_pb = entity_to_protobuf(val)
383+
value_pb.entity_value.CopyFrom(entity_pb)
308384
elif attr == 'list_value':
309385
l_pb = value_pb.list_value
310386
for item in val:

gcloud/datastore/test_batch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ def commit(self, dataset_id, mutation, transaction_id):
408408
class _Entity(dict):
409409
key = None
410410
exclude_from_indexes = ()
411+
_meanings = {}
411412

412413

413414
class _Key(object):

0 commit comments

Comments
 (0)