Skip to content

Commit b609053

Browse files
committed
Adding support for null and geo point values in v1beta3.
1 parent 04e131d commit b609053

File tree

3 files changed

+201
-21
lines changed

3 files changed

+201
-21
lines changed

gcloud/datastore/helpers.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import datetime
2121

2222
from google.protobuf.internal.type_checkers import Int64ValueChecker
23+
from google.protobuf import struct_pb2
24+
from google.type import latlng_pb2
2325
import six
2426

2527
from gcloud._helpers import _datetime_to_pb_timestamp
@@ -327,6 +329,10 @@ def _pb_attr_value(val):
327329
name, value = 'entity', val
328330
elif isinstance(val, list):
329331
name, value = 'array', val
332+
elif isinstance(val, GeoPoint):
333+
name, value = 'geo_point', val.to_protobuf()
334+
elif val is None:
335+
name, value = 'null', struct_pb2.NULL_VALUE
330336
else:
331337
raise ValueError("Unknown protobuf attr type %s" % type(val))
332338

@@ -347,8 +353,9 @@ def _get_value_from_value_pb(value_pb):
347353
:param value_pb: The Value Protobuf.
348354
349355
:returns: The value provided by the Protobuf.
356+
:raises: :class:`ValueError <exceptions.ValueError>` if no value type
357+
has been set.
350358
"""
351-
result = None
352359
value_type = value_pb.WhichOneof('value_type')
353360

354361
if value_type == 'timestamp_value':
@@ -379,6 +386,16 @@ def _get_value_from_value_pb(value_pb):
379386
result = [_get_value_from_value_pb(value)
380387
for value in value_pb.array_value.values]
381388

389+
elif value_type == 'geo_point_value':
390+
result = GeoPoint(value_pb.geo_point_value.latitude,
391+
value_pb.geo_point_value.longitude)
392+
393+
elif value_type == 'null_value':
394+
result = None
395+
396+
else:
397+
raise ValueError('Value protobuf did not have any value set')
398+
382399
return result
383400

384401

@@ -399,10 +416,6 @@ def _set_protobuf_value(value_pb, val):
399416
:class:`gcloud.datastore.entity.Entity`
400417
:param val: The value to be assigned.
401418
"""
402-
if val is None:
403-
value_pb.Clear()
404-
return
405-
406419
attr, val = _pb_attr_value(val)
407420
if attr == 'key_value':
408421
value_pb.key_value.CopyFrom(val)
@@ -416,6 +429,8 @@ def _set_protobuf_value(value_pb, val):
416429
for item in val:
417430
i_pb = l_pb.add()
418431
_set_protobuf_value(i_pb, item)
432+
elif attr == 'geo_point_value':
433+
value_pb.geo_point_value.CopyFrom(val)
419434
else: # scalar, just assign
420435
setattr(value_pb, attr, val)
421436

@@ -445,3 +460,47 @@ def _prepare_key_for_request(key_pb):
445460
new_key_pb.partition_id.ClearField('project_id')
446461
key_pb = new_key_pb
447462
return key_pb
463+
464+
465+
class GeoPoint(object):
466+
"""Simple container for a geo point value.
467+
468+
:type latitude: float
469+
:param latitude: Latitude of a point.
470+
471+
:type longitude: float
472+
:param longitude: Longitude of a point.
473+
"""
474+
475+
def __init__(self, latitude, longitude):
476+
self.latitude = latitude
477+
self.longitude = longitude
478+
479+
def to_protobuf(self):
480+
"""Convert the current object to protobuf.
481+
482+
:rtype: :class:`google.type.latlng_pb2.LatLng`.
483+
:returns: The current point as a protobuf.
484+
"""
485+
return latlng_pb2.LatLng(latitude=self.latitude,
486+
longitude=self.longitude)
487+
488+
def __eq__(self, other):
489+
"""Compare two geo points for equality.
490+
491+
:rtype: boolean
492+
:returns: True if the points compare equal, else False.
493+
"""
494+
if not isinstance(other, GeoPoint):
495+
return False
496+
497+
return (self.latitude == other.latitude and
498+
self.longitude == other.longitude)
499+
500+
def __ne__(self, other):
501+
"""Compare two geo points for inequality.
502+
503+
:rtype: boolean
504+
:returns: False if the points compare equal, else True.
505+
"""
506+
return not self.__eq__(other)

gcloud/datastore/test_helpers.py

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,25 @@ def test_array(self):
497497
self.assertEqual(name, 'array_value')
498498
self.assertTrue(value is values)
499499

500+
def test_geo_point(self):
501+
from google.type import latlng_pb2
502+
from gcloud.datastore.helpers import GeoPoint
503+
504+
lat = 42.42
505+
lng = 99.0007
506+
geo_pt = GeoPoint(latitude=lat, longitude=lng)
507+
geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng)
508+
name, value = self._callFUT(geo_pt)
509+
self.assertEqual(name, 'geo_point_value')
510+
self.assertEqual(value, geo_pt_pb)
511+
512+
def test_null(self):
513+
from google.protobuf import struct_pb2
514+
515+
name, value = self._callFUT(None)
516+
self.assertEqual(name, 'null_value')
517+
self.assertEqual(value, struct_pb2.NULL_VALUE)
518+
500519
def test_object(self):
501520
self.assertRaises(ValueError, self._callFUT, object())
502521

@@ -586,11 +605,34 @@ def test_array(self):
586605
items = self._callFUT(pb)
587606
self.assertEqual(items, ['Foo', 'Bar'])
588607

608+
def test_geo_point(self):
609+
from google.type import latlng_pb2
610+
from gcloud.datastore._generated import entity_pb2
611+
from gcloud.datastore.helpers import GeoPoint
612+
613+
lat = -3.14
614+
lng = 13.37
615+
geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng)
616+
pb = entity_pb2.Value(geo_point_value=geo_pt_pb)
617+
result = self._callFUT(pb)
618+
self.assertIsInstance(result, GeoPoint)
619+
self.assertEqual(result.latitude, lat)
620+
self.assertEqual(result.longitude, lng)
621+
622+
def test_null(self):
623+
from google.protobuf import struct_pb2
624+
from gcloud.datastore._generated import entity_pb2
625+
626+
pb = entity_pb2.Value(null_value=struct_pb2.NULL_VALUE)
627+
result = self._callFUT(pb)
628+
self.assertIsNone(result)
629+
589630
def test_unknown(self):
590631
from gcloud.datastore._generated import entity_pb2
591632

592633
pb = entity_pb2.Value()
593-
self.assertEqual(self._callFUT(pb), None)
634+
with self.assertRaises(ValueError):
635+
self._callFUT(pb)
594636

595637

596638
class Test_set_protobuf_value(unittest2.TestCase):
@@ -627,23 +669,9 @@ def test_key(self):
627669
self.assertEqual(value, key.to_protobuf())
628670

629671
def test_none(self):
630-
from gcloud.datastore.entity import Entity
631-
632-
entity = Entity()
633672
pb = self._makePB()
634-
635-
self._callFUT(pb, False)
636-
self._callFUT(pb, 3.1415926)
637-
self._callFUT(pb, 42)
638-
self._callFUT(pb, (1 << 63) - 1)
639-
self._callFUT(pb, 'str')
640-
self._callFUT(pb, b'str')
641-
self._callFUT(pb, u'str')
642-
self._callFUT(pb, entity)
643-
self._callFUT(pb, [u'a', 0, 3.14])
644-
645673
self._callFUT(pb, None)
646-
self.assertEqual(len(pb.ListFields()), 0)
674+
self.assertEqual(pb.WhichOneof('value_type'), 'null_value')
647675

648676
def test_bool(self):
649677
pb = self._makePB()
@@ -733,6 +761,18 @@ def test_array(self):
733761
self.assertEqual(marshalled[1].integer_value, values[1])
734762
self.assertEqual(marshalled[2].double_value, values[2])
735763

764+
def test_geo_point(self):
765+
from google.type import latlng_pb2
766+
from gcloud.datastore.helpers import GeoPoint
767+
768+
pb = self._makePB()
769+
lat = 9.11
770+
lng = 3.337
771+
geo_pt = GeoPoint(latitude=lat, longitude=lng)
772+
geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng)
773+
self._callFUT(pb, geo_pt)
774+
self.assertEqual(pb.geo_point_value, geo_pt_pb)
775+
736776

737777
class Test__prepare_key_for_request(unittest2.TestCase):
738778

@@ -894,6 +934,60 @@ def test_array_value_partially_unset(self):
894934
self._callFUT(value_pb, is_list=True)
895935

896936

937+
class TestGeoPoint(unittest2.TestCase):
938+
939+
def _getTargetClass(self):
940+
from gcloud.datastore.helpers import GeoPoint
941+
return GeoPoint
942+
943+
def _makeOne(self, *args, **kwargs):
944+
return self._getTargetClass()(*args, **kwargs)
945+
946+
def test_constructor(self):
947+
lat = 81.2
948+
lng = 359.9999
949+
geo_pt = self._makeOne(lat, lng)
950+
self.assertEqual(geo_pt.latitude, lat)
951+
self.assertEqual(geo_pt.longitude, lng)
952+
953+
def test_to_protobuf(self):
954+
from google.type import latlng_pb2
955+
956+
lat = 0.0001
957+
lng = 20.03
958+
geo_pt = self._makeOne(lat, lng)
959+
result = geo_pt.to_protobuf()
960+
geo_pt_pb = latlng_pb2.LatLng(latitude=lat, longitude=lng)
961+
self.assertEqual(result, geo_pt_pb)
962+
963+
def test___eq__(self):
964+
lat = 0.0001
965+
lng = 20.03
966+
geo_pt1 = self._makeOne(lat, lng)
967+
geo_pt2 = self._makeOne(lat, lng)
968+
self.assertEqual(geo_pt1, geo_pt2)
969+
970+
def test___eq__type_differ(self):
971+
lat = 0.0001
972+
lng = 20.03
973+
geo_pt1 = self._makeOne(lat, lng)
974+
geo_pt2 = object()
975+
self.assertNotEqual(geo_pt1, geo_pt2)
976+
977+
def test___ne__same_value(self):
978+
lat = 0.0001
979+
lng = 20.03
980+
geo_pt1 = self._makeOne(lat, lng)
981+
geo_pt2 = self._makeOne(lat, lng)
982+
comparison_val = (geo_pt1 != geo_pt2)
983+
self.assertFalse(comparison_val)
984+
985+
def test___ne__(self):
986+
geo_pt1 = self._makeOne(0.0, 1.0)
987+
geo_pt2 = self._makeOne(2.0, 3.0)
988+
self.assertNotEqual(geo_pt1, geo_pt2)
989+
990+
897991
class _Connection(object):
898992

899993
_called_project = _called_key_pbs = _lookup_result = None

system_tests/datastore.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from gcloud._helpers import UTC
2323
from gcloud import datastore
2424
from gcloud.datastore import client as client_mod
25+
from gcloud.datastore.helpers import GeoPoint
2526
from gcloud.environment_vars import GCD_DATASET
2627
from gcloud.environment_vars import TESTS_DATASET
2728
from gcloud.exceptions import Conflict
@@ -178,6 +179,32 @@ def test_empty_kind(self):
178179
posts = list(query.fetch(limit=2))
179180
self.assertEqual(posts, [])
180181

182+
def test_all_value_types(self):
183+
key = Config.CLIENT.key('TestPanObject', 1234)
184+
entity = datastore.Entity(key=key)
185+
entity['timestamp'] = datetime.datetime(2014, 9, 9, tzinfo=UTC)
186+
key_stored = Config.CLIENT.key('SavedKey', 'right-here')
187+
entity['key'] = key_stored
188+
entity['truthy'] = True
189+
entity['float'] = 2.718281828
190+
entity['int'] = 3735928559
191+
entity['words'] = u'foo'
192+
entity['blob'] = b'seekretz'
193+
entity_stored = datastore.Entity(key=key_stored)
194+
entity_stored['hi'] = 'bye'
195+
entity['nested'] = entity_stored
196+
entity['items'] = [1, 2, 3]
197+
entity['geo'] = GeoPoint(1.0, 2.0)
198+
entity['nothing_here'] = None
199+
200+
# Store the entity.
201+
self.case_entities_to_delete.append(entity)
202+
Config.CLIENT.put(entity)
203+
204+
# Check the original and retrieved are the the same.
205+
retrieved_entity = Config.CLIENT.get(entity.key)
206+
self.assertEqual(retrieved_entity, entity)
207+
181208

182209
class TestDatastoreSaveKeys(TestDatastore):
183210

0 commit comments

Comments
 (0)