Skip to content

Commit

Permalink
Merge pull request #1464 from dhermes/v1beta3-impl-geo-and-null
Browse files Browse the repository at this point in the history
Adding support for null and geo point values in v1beta3.
  • Loading branch information
dhermes committed Feb 13, 2016
2 parents 302c1a0 + e3bbe8a commit 710fe77
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 21 deletions.
69 changes: 64 additions & 5 deletions gcloud/datastore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand All @@ -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 <exceptions.ValueError>` if no value type
has been set.
"""
result = None
value_type = value_pb.WhichOneof('value_type')

if value_type == 'timestamp_value':
Expand Down Expand Up @@ -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


Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
126 changes: 110 additions & 16 deletions gcloud/datastore/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions system_tests/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down

0 comments on commit 710fe77

Please sign in to comment.