diff --git a/gcloud/datastore/helpers.py b/gcloud/datastore/helpers.py index 3ea826c03fff..a62b6b90cbf0 100644 --- a/gcloud/datastore/helpers.py +++ b/gcloud/datastore/helpers.py @@ -20,6 +20,8 @@ import datetime from google.protobuf.internal.type_checkers import Int64ValueChecker +from google.protobuf import struct_pb2 +from google.type import latlng_pb2 import six from gcloud._helpers import _datetime_to_pb_timestamp @@ -327,6 +329,10 @@ def _pb_attr_value(val): name, value = 'entity', val elif isinstance(val, list): name, value = 'array', val + elif isinstance(val, GeoPoint): + name, value = 'geo_point', val.to_protobuf() + elif val is None: + name, value = 'null', struct_pb2.NULL_VALUE else: raise ValueError("Unknown protobuf attr type %s" % type(val)) @@ -347,8 +353,9 @@ def _get_value_from_value_pb(value_pb): :param value_pb: The Value Protobuf. :returns: The value provided by the Protobuf. + :raises: :class:`ValueError ` if no value type + has been set. """ - result = None value_type = value_pb.WhichOneof('value_type') if value_type == 'timestamp_value': @@ -379,6 +386,16 @@ def _get_value_from_value_pb(value_pb): result = [_get_value_from_value_pb(value) for value in value_pb.array_value.values] + elif value_type == 'geo_point_value': + result = GeoPoint(value_pb.geo_point_value.latitude, + value_pb.geo_point_value.longitude) + + elif value_type == 'null_value': + result = None + + else: + raise ValueError('Value protobuf did not have any value set') + return result @@ -399,10 +416,6 @@ def _set_protobuf_value(value_pb, val): :class:`gcloud.datastore.entity.Entity` :param val: The value to be assigned. """ - if val is None: - value_pb.Clear() - return - attr, val = _pb_attr_value(val) if attr == 'key_value': value_pb.key_value.CopyFrom(val) @@ -416,6 +429,8 @@ def _set_protobuf_value(value_pb, val): for item in val: i_pb = l_pb.add() _set_protobuf_value(i_pb, item) + elif attr == 'geo_point_value': + value_pb.geo_point_value.CopyFrom(val) else: # scalar, just assign setattr(value_pb, attr, val) @@ -445,3 +460,47 @@ def _prepare_key_for_request(key_pb): new_key_pb.partition_id.ClearField('project_id') key_pb = new_key_pb return key_pb + + +class GeoPoint(object): + """Simple container for a geo point value. + + :type latitude: float + :param latitude: Latitude of a point. + + :type longitude: float + :param longitude: Longitude of a point. + """ + + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def to_protobuf(self): + """Convert the current object to protobuf. + + :rtype: :class:`google.type.latlng_pb2.LatLng`. + :returns: The current point as a protobuf. + """ + return latlng_pb2.LatLng(latitude=self.latitude, + longitude=self.longitude) + + def __eq__(self, other): + """Compare two geo points for equality. + + :rtype: boolean + :returns: True if the points compare equal, else False. + """ + if not isinstance(other, GeoPoint): + return False + + return (self.latitude == other.latitude and + self.longitude == other.longitude) + + def __ne__(self, other): + """Compare two geo points for inequality. + + :rtype: boolean + :returns: False if the points compare equal, else True. + """ + return not self.__eq__(other) diff --git a/gcloud/datastore/test_helpers.py b/gcloud/datastore/test_helpers.py index d59f605d278f..c51d3641f26a 100644 --- a/gcloud/datastore/test_helpers.py +++ b/gcloud/datastore/test_helpers.py @@ -497,6 +497,25 @@ def test_array(self): self.assertEqual(name, 'array_value') self.assertTrue(value is values) + def test_geo_point(self): + from google.type import latlng_pb2 + from gcloud.datastore.helpers import GeoPoint + + lat = 42.42 + lng = 99.0007 + geo_pt = GeoPoint(latitude=lat, longitude=lng) + geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng) + name, value = self._callFUT(geo_pt) + self.assertEqual(name, 'geo_point_value') + self.assertEqual(value, geo_pt_pb) + + def test_null(self): + from google.protobuf import struct_pb2 + + name, value = self._callFUT(None) + self.assertEqual(name, 'null_value') + self.assertEqual(value, struct_pb2.NULL_VALUE) + def test_object(self): self.assertRaises(ValueError, self._callFUT, object()) @@ -586,11 +605,34 @@ def test_array(self): items = self._callFUT(pb) self.assertEqual(items, ['Foo', 'Bar']) + def test_geo_point(self): + from google.type import latlng_pb2 + from gcloud.datastore._generated import entity_pb2 + from gcloud.datastore.helpers import GeoPoint + + lat = -3.14 + lng = 13.37 + geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng) + pb = entity_pb2.Value(geo_point_value=geo_pt_pb) + result = self._callFUT(pb) + self.assertIsInstance(result, GeoPoint) + self.assertEqual(result.latitude, lat) + self.assertEqual(result.longitude, lng) + + def test_null(self): + from google.protobuf import struct_pb2 + from gcloud.datastore._generated import entity_pb2 + + pb = entity_pb2.Value(null_value=struct_pb2.NULL_VALUE) + result = self._callFUT(pb) + self.assertIsNone(result) + def test_unknown(self): from gcloud.datastore._generated import entity_pb2 pb = entity_pb2.Value() - self.assertEqual(self._callFUT(pb), None) + with self.assertRaises(ValueError): + self._callFUT(pb) class Test_set_protobuf_value(unittest2.TestCase): @@ -627,23 +669,9 @@ def test_key(self): self.assertEqual(value, key.to_protobuf()) def test_none(self): - from gcloud.datastore.entity import Entity - - entity = Entity() pb = self._makePB() - - self._callFUT(pb, False) - self._callFUT(pb, 3.1415926) - self._callFUT(pb, 42) - self._callFUT(pb, (1 << 63) - 1) - self._callFUT(pb, 'str') - self._callFUT(pb, b'str') - self._callFUT(pb, u'str') - self._callFUT(pb, entity) - self._callFUT(pb, [u'a', 0, 3.14]) - self._callFUT(pb, None) - self.assertEqual(len(pb.ListFields()), 0) + self.assertEqual(pb.WhichOneof('value_type'), 'null_value') def test_bool(self): pb = self._makePB() @@ -733,6 +761,18 @@ def test_array(self): self.assertEqual(marshalled[1].integer_value, values[1]) self.assertEqual(marshalled[2].double_value, values[2]) + def test_geo_point(self): + from google.type import latlng_pb2 + from gcloud.datastore.helpers import GeoPoint + + pb = self._makePB() + lat = 9.11 + lng = 3.337 + geo_pt = GeoPoint(latitude=lat, longitude=lng) + geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng) + self._callFUT(pb, geo_pt) + self.assertEqual(pb.geo_point_value, geo_pt_pb) + class Test__prepare_key_for_request(unittest2.TestCase): @@ -894,6 +934,60 @@ def test_array_value_partially_unset(self): self._callFUT(value_pb, is_list=True) +class TestGeoPoint(unittest2.TestCase): + + def _getTargetClass(self): + from gcloud.datastore.helpers import GeoPoint + return GeoPoint + + def _makeOne(self, *args, **kwargs): + return self._getTargetClass()(*args, **kwargs) + + def test_constructor(self): + lat = 81.2 + lng = 359.9999 + geo_pt = self._makeOne(lat, lng) + self.assertEqual(geo_pt.latitude, lat) + self.assertEqual(geo_pt.longitude, lng) + + def test_to_protobuf(self): + from google.type import latlng_pb2 + + lat = 0.0001 + lng = 20.03 + geo_pt = self._makeOne(lat, lng) + result = geo_pt.to_protobuf() + geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng) + self.assertEqual(result, geo_pt_pb) + + def test___eq__(self): + lat = 0.0001 + lng = 20.03 + geo_pt1 = self._makeOne(lat, lng) + geo_pt2 = self._makeOne(lat, lng) + self.assertEqual(geo_pt1, geo_pt2) + + def test___eq__type_differ(self): + lat = 0.0001 + lng = 20.03 + geo_pt1 = self._makeOne(lat, lng) + geo_pt2 = object() + self.assertNotEqual(geo_pt1, geo_pt2) + + def test___ne__same_value(self): + lat = 0.0001 + lng = 20.03 + geo_pt1 = self._makeOne(lat, lng) + geo_pt2 = self._makeOne(lat, lng) + comparison_val = (geo_pt1 != geo_pt2) + self.assertFalse(comparison_val) + + def test___ne__(self): + geo_pt1 = self._makeOne(0.0, 1.0) + geo_pt2 = self._makeOne(2.0, 3.0) + self.assertNotEqual(geo_pt1, geo_pt2) + + class _Connection(object): _called_project = _called_key_pbs = _lookup_result = None diff --git a/system_tests/datastore.py b/system_tests/datastore.py index da183dbea785..34b30dc0f5da 100644 --- a/system_tests/datastore.py +++ b/system_tests/datastore.py @@ -22,6 +22,7 @@ from gcloud._helpers import UTC from gcloud import datastore from gcloud.datastore import client as client_mod +from gcloud.datastore.helpers import GeoPoint from gcloud.environment_vars import GCD_DATASET from gcloud.environment_vars import TESTS_DATASET from gcloud.exceptions import Conflict @@ -178,6 +179,32 @@ def test_empty_kind(self): posts = list(query.fetch(limit=2)) self.assertEqual(posts, []) + def test_all_value_types(self): + key = Config.CLIENT.key('TestPanObject', 1234) + entity = datastore.Entity(key=key) + entity['timestamp'] = datetime.datetime(2014, 9, 9, tzinfo=UTC) + key_stored = Config.CLIENT.key('SavedKey', 'right-here') + entity['key'] = key_stored + entity['truthy'] = True + entity['float'] = 2.718281828 + entity['int'] = 3735928559 + entity['words'] = u'foo' + entity['blob'] = b'seekretz' + entity_stored = datastore.Entity(key=key_stored) + entity_stored['hi'] = 'bye' + entity['nested'] = entity_stored + entity['items'] = [1, 2, 3] + entity['geo'] = GeoPoint(1.0, 2.0) + entity['nothing_here'] = None + + # Store the entity. + self.case_entities_to_delete.append(entity) + Config.CLIENT.put(entity) + + # Check the original and retrieved are the the same. + retrieved_entity = Config.CLIENT.get(entity.key) + self.assertEqual(retrieved_entity, entity) + class TestDatastoreSaveKeys(TestDatastore):