Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly handling entity meanings in datastore. #1241

Merged
merged 1 commit into from
Dec 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion gcloud/datastore/_datastore_pb2.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,3 @@
Mutation = _datastore_v1_pb2.Mutation
MutationResult = _datastore_v1_pb2.MutationResult
ReadOptions = _datastore_v1_pb2.ReadOptions
Property = _datastore_v1_pb2.Property # Not present in v1beta3
27 changes: 4 additions & 23 deletions gcloud/datastore/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,26 +229,7 @@ def _assign_entity_to_pb(entity_pb, entity):
:type entity: :class:`gcloud.datastore.entity.Entity`
:param entity: The entity being updated within the batch / transaction.
"""
key_pb = entity.key.to_protobuf()
key_pb = helpers._prepare_key_for_request(key_pb)
entity_pb.key.CopyFrom(key_pb)

for name, value in entity.items():

value_is_list = isinstance(value, list)
if value_is_list and len(value) == 0:
continue

prop = entity_pb.property.add()
# Set the name of the property.
prop.name = name

# Set the appropriate value.
helpers._set_protobuf_value(prop.value, value)

if name in entity.exclude_from_indexes:
if not value_is_list:
prop.value.indexed = False

for sub_value in prop.value.list_value:
sub_value.indexed = False
bare_entity_pb = helpers.entity_to_protobuf(entity)
key_pb = helpers._prepare_key_for_request(bare_entity_pb.key)
bare_entity_pb.key.CopyFrom(key_pb)
entity_pb.CopyFrom(bare_entity_pb)
3 changes: 3 additions & 0 deletions gcloud/datastore/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(self, key=None, exclude_from_indexes=()):
self.key = key
self._exclude_from_indexes = set(_ensure_tuple_or_list(
'exclude_from_indexes', exclude_from_indexes))
# NOTE: This will be populated when parsing a protobuf in
# gcloud.datastore.helpers.entity_from_protobuf.
self._meanings = {}

def __eq__(self, other):
"""Compare two entities for equality.
Expand Down
143 changes: 110 additions & 33 deletions gcloud/datastore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,48 @@ def find_true_dataset_id(dataset_id, connection):
return returned_pb.key.partition_id.dataset_id


def _get_meaning(value_pb, is_list=False):
"""Get the meaning from a protobuf value.

:type value_pb: :class:`gcloud.datastore._entity_pb2.Value`
:param value_pb: The protobuf value to be checked for an
associated meaning.

:type is_list: bool
:param is_list: Boolean indicating if the ``value_pb`` contains
a list value.

:rtype: int
:returns: The meaning for the ``value_pb`` if one is set, else
:data:`None`.
:raises: :class:`ValueError <exceptions.ValueError>` if a list value
has disagreeing meanings (in sub-elements) or has some
elements with meanings and some without.
"""
meaning = None
if is_list:
# An empty list will have no values, hence no shared meaning
# set among them.
if len(value_pb.list_value) == 0:
return None

# We check among all the meanings, some of which may be None,
# the rest which may be enum/int values.
all_meanings = set(_get_meaning(sub_value_pb)
for sub_value_pb in value_pb.list_value)
meaning = all_meanings.pop()
# The value we popped off should have been unique. If not
# then we can't handle a list with values that have more
# than one meaning.
if all_meanings:
raise ValueError('Different meanings set on values '
'within a list_value')
elif value_pb.HasField('meaning'):
meaning = value_pb.meaning

return meaning


def entity_from_protobuf(pb):
"""Factory method for creating an entity based on a protobuf.

Expand All @@ -90,11 +132,19 @@ def entity_from_protobuf(pb):
key = key_from_protobuf(pb.key)

entity_props = {}
entity_meanings = {}
exclude_from_indexes = []

for property_pb in pb.property:
value = _get_value_from_property_pb(property_pb)
entity_props[property_pb.name] = value
value = _get_value_from_value_pb(property_pb.value)
prop_name = property_pb.name
entity_props[prop_name] = value

# Check if the property has an associated meaning.
meaning = _get_meaning(property_pb.value,
is_list=isinstance(value, list))
if meaning is not None:
entity_meanings[prop_name] = (meaning, value)

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

if not indexed_values.pop():
exclude_from_indexes.append(property_pb.name)
exclude_from_indexes.append(prop_name)
else:
if not property_pb.value.indexed:
exclude_from_indexes.append(property_pb.name)
exclude_from_indexes.append(prop_name)

entity = Entity(key=key, exclude_from_indexes=exclude_from_indexes)
entity.update(entity_props)
entity._meanings.update(entity_meanings)

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

return entity


def entity_to_protobuf(entity):
"""Converts an entity into a protobuf.

:type entity: :class:`gcloud.datastore.entity.Entity`
:param entity: The entity to be turned into a protobuf.

:rtype: :class:`gcloud.datastore._entity_pb2.Entity`
:returns: The protobuf representing the entity.
"""
entity_pb = _entity_pb2.Entity()
if entity.key is not None:
key_pb = entity.key.to_protobuf()
entity_pb.key.CopyFrom(key_pb)

for name, value in entity.items():
value_is_list = isinstance(value, list)
if value_is_list and len(value) == 0:
continue

prop = entity_pb.property.add()
# Set the name of the property.
prop.name = name

# Set the appropriate value.
_set_protobuf_value(prop.value, value)

# Add index information to protobuf.
if name in entity.exclude_from_indexes:
if not value_is_list:
prop.value.indexed = False

for sub_value in prop.value.list_value:
sub_value.indexed = False

# Add meaning information to protobuf.
if name in entity._meanings:
meaning, orig_value = entity._meanings[name]
# Only add the meaning back to the protobuf if the value is
# unchanged from when it was originally read from the API.
if orig_value is value:

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

# For lists, we set meaning on each sub-element.
if value_is_list:
for sub_value_pb in prop.value.list_value:
sub_value_pb.meaning = meaning
else:
prop.value.meaning = meaning

return entity_pb


def key_from_protobuf(pb):
"""Factory method for creating a key based on a protobuf.

Expand Down Expand Up @@ -248,29 +349,12 @@ def _get_value_from_value_pb(value_pb):
result = entity_from_protobuf(value_pb.entity_value)

elif value_pb.list_value:
result = [_get_value_from_value_pb(x) for x in value_pb.list_value]
result = [_get_value_from_value_pb(value)
for value in value_pb.list_value]

return result


def _get_value_from_property_pb(property_pb):
"""Given a protobuf for a Property, get the correct value.

The Cloud Datastore Protobuf API returns a Property Protobuf which
has one value set and the rest blank. This function retrieves the
the one value provided.

Some work is done to coerce the return value into a more useful type
(particularly in the case of a timestamp value, or a key value).

:type property_pb: :class:`gcloud.datastore._datastore_pb2.Property`
:param property_pb: The Property Protobuf.

:returns: The value provided by the Protobuf.
"""
return _get_value_from_value_pb(property_pb.value)


def _set_protobuf_value(value_pb, val):
"""Assign 'val' to the correct subfield of 'value_pb'.

Expand All @@ -285,7 +369,7 @@ def _set_protobuf_value(value_pb, val):

:type val: :class:`datetime.datetime`, boolean, float, integer, string,
:class:`gcloud.datastore.key.Key`,
:class:`gcloud.datastore.entity.Entity`,
:class:`gcloud.datastore.entity.Entity`
:param val: The value to be assigned.
"""
if val is None:
Expand All @@ -296,15 +380,8 @@ def _set_protobuf_value(value_pb, val):
if attr == 'key_value':
value_pb.key_value.CopyFrom(val)
elif attr == 'entity_value':
e_pb = value_pb.entity_value
e_pb.Clear()
key = val.key
if key is not None:
e_pb.key.CopyFrom(key.to_protobuf())
for item_key, value in val.items():
p_pb = e_pb.property.add()
p_pb.name = item_key
_set_protobuf_value(p_pb.value, value)
entity_pb = entity_to_protobuf(val)
value_pb.entity_value.CopyFrom(entity_pb)
elif attr == 'list_value':
l_pb = value_pb.list_value
for item in val:
Expand Down
1 change: 1 addition & 0 deletions gcloud/datastore/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ def commit(self, dataset_id, mutation, transaction_id):
class _Entity(dict):
key = None
exclude_from_indexes = ()
_meanings = {}


class _Key(object):
Expand Down
Loading