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

Adding support for null and geo point values in v1beta3. #1464

Merged
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
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