diff --git a/firestore/docs/constants.rst b/firestore/docs/constants.rst deleted file mode 100644 index df5b1901a7ee..000000000000 --- a/firestore/docs/constants.rst +++ /dev/null @@ -1,6 +0,0 @@ -Constants -~~~~~~~~~ - -.. automodule:: google.cloud.firestore_v1beta1.constants - :members: - :show-inheritance: diff --git a/firestore/docs/index.rst b/firestore/docs/index.rst index 9091d3157921..68f1519a5566 100644 --- a/firestore/docs/index.rst +++ b/firestore/docs/index.rst @@ -13,7 +13,7 @@ API Reference query batch transaction - constants + transforms types diff --git a/firestore/docs/transforms.rst b/firestore/docs/transforms.rst new file mode 100644 index 000000000000..ab683e626270 --- /dev/null +++ b/firestore/docs/transforms.rst @@ -0,0 +1,6 @@ +Transforms +~~~~~~~~~~ + +.. automodule:: google.cloud.firestore_v1beta1.transforms + :members: + :show-inheritance: diff --git a/firestore/google/cloud/firestore_v1beta1/__init__.py b/firestore/google/cloud/firestore_v1beta1/__init__.py index 35b1654620ff..dda63c728177 100644 --- a/firestore/google/cloud/firestore_v1beta1/__init__.py +++ b/firestore/google/cloud/firestore_v1beta1/__init__.py @@ -26,8 +26,10 @@ from google.cloud.firestore_v1beta1.batch import WriteBatch from google.cloud.firestore_v1beta1.client import Client from google.cloud.firestore_v1beta1.collection import CollectionReference -from google.cloud.firestore_v1beta1.constants import DELETE_FIELD -from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP +from google.cloud.firestore_v1beta1.transforms import ArrayRemove +from google.cloud.firestore_v1beta1.transforms import ArrayUnion +from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD +from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP from google.cloud.firestore_v1beta1.document import DocumentReference from google.cloud.firestore_v1beta1.document import DocumentSnapshot from google.cloud.firestore_v1beta1.gapic import enums @@ -39,6 +41,8 @@ __all__ = [ '__version__', + 'ArrayRemove', + 'ArrayUnion', 'Client', 'CollectionReference', 'DELETE_FIELD', diff --git a/firestore/google/cloud/firestore_v1beta1/_helpers.py b/firestore/google/cloud/firestore_v1beta1/_helpers.py index 56c8f9de4008..fe8a1f5aed9c 100644 --- a/firestore/google/cloud/firestore_v1beta1/_helpers.py +++ b/firestore/google/cloud/firestore_v1beta1/_helpers.py @@ -30,7 +30,7 @@ from google.cloud import exceptions from google.cloud._helpers import _datetime_to_pb_timestamp from google.cloud._helpers import _pb_timestamp_to_datetime -from google.cloud.firestore_v1beta1 import constants +from google.cloud.firestore_v1beta1 import transforms from google.cloud.firestore_v1beta1 import types from google.cloud.firestore_v1beta1.gapic import enums from google.cloud.firestore_v1beta1.proto import common_pb2 @@ -654,7 +654,7 @@ def get_doc_id(document_pb, expected_prefix): return document_id -_EmptyDict = constants.Sentinel("Marker for an empty dict value") +_EmptyDict = transforms.Sentinel("Marker for an empty dict value") def extract_fields(document_data, prefix_path, expand_dots=False): @@ -713,6 +713,8 @@ def __init__(self, document_data): self.field_paths = [] self.deleted_fields = [] self.server_timestamps = [] + self.array_removes = {} + self.array_unions = {} self.set_fields = {} self.empty_document = False @@ -724,12 +726,18 @@ def __init__(self, document_data): if field_path == prefix_path and value is _EmptyDict: self.empty_document = True - elif value is constants.DELETE_FIELD: + elif value is transforms.DELETE_FIELD: self.deleted_fields.append(field_path) - elif value is constants.SERVER_TIMESTAMP: + elif value is transforms.SERVER_TIMESTAMP: self.server_timestamps.append(field_path) + elif isinstance(value, transforms.ArrayRemove): + self.array_removes[field_path] = value.values + + elif isinstance(value, transforms.ArrayUnion): + self.array_unions[field_path] = value.values + else: self.field_paths.append(field_path) set_field_value(self.set_fields, field_path, value) @@ -739,11 +747,18 @@ def _get_document_iterator(self, prefix_path): @property def has_transforms(self): - return bool(self.server_timestamps) + return bool( + self.server_timestamps + or self.array_removes + or self.array_unions + ) @property def transform_paths(self): - return sorted(self.server_timestamps) + return sorted( + self.server_timestamps + + list(self.array_removes) + + list(self.array_unions)) def _get_update_mask(self, allow_empty_mask=False): return None @@ -768,16 +783,34 @@ def get_update_pb( return update_pb def get_transform_pb(self, document_path, exists=None): + + def make_array_value(values): + value_list = [encode_value(element) for element in values] + return document_pb2.ArrayValue(values=value_list) + + path_field_transforms = [ + (path, write_pb2.DocumentTransform.FieldTransform( + field_path=path.to_api_repr(), + set_to_server_value=REQUEST_TIME_ENUM, + )) for path in self.server_timestamps + ] + [ + (path, write_pb2.DocumentTransform.FieldTransform( + field_path=path.to_api_repr(), + remove_all_from_array=make_array_value(values), + )) for path, values in self.array_removes.items() + ] + [ + (path, write_pb2.DocumentTransform.FieldTransform( + field_path=path.to_api_repr(), + append_missing_elements=make_array_value(values), + )) for path, values in self.array_unions.items() + ] + field_transforms = [ + transform for path, transform in sorted(path_field_transforms) + ] transform_pb = write_pb2.Write( transform=write_pb2.DocumentTransform( document=document_path, - field_transforms=[ - write_pb2.DocumentTransform.FieldTransform( - field_path=path.to_api_repr(), - set_to_server_value=REQUEST_TIME_ENUM, - ) - for path in self.server_timestamps - ], + field_transforms=field_transforms, ), ) if exists is not None: @@ -953,12 +986,21 @@ def _apply_merge_paths(self, merge): ] merged_transform_paths.update(tranform_merge_paths) - # TODO: other transforms self.server_timestamps = [ path for path in self.server_timestamps if path in merged_transform_paths ] + self.array_removes = { + path: values for path, values in self.array_removes.items() + if path in merged_transform_paths + } + + self.array_unions = { + path: values for path, values in self.array_unions.items() + if path in merged_transform_paths + } + def apply_merge(self, merge): if merge is True: # merge all fields self._apply_merge_all() diff --git a/firestore/google/cloud/firestore_v1beta1/constants.py b/firestore/google/cloud/firestore_v1beta1/constants.py deleted file mode 100644 index 4ce1efb743e7..000000000000 --- a/firestore/google/cloud/firestore_v1beta1/constants.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2017 Google LLC All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Helpful constants to use for Google Cloud Firestore.""" - - -class Sentinel(object): - """Sentinel objects used to signal special handling.""" - __slots__ = ('description',) - - def __init__(self, description): - self.description = description - - def __repr__(self): - return "Sentinel: {}".format(self.description) - - -DELETE_FIELD = Sentinel("Value used to delete a field in a document.") - - -SERVER_TIMESTAMP = Sentinel( - "Value used to set a document field to the server timestamp.") diff --git a/firestore/google/cloud/firestore_v1beta1/document.py b/firestore/google/cloud/firestore_v1beta1/document.py index b4d6c2fa1312..097664badf4b 100644 --- a/firestore/google/cloud/firestore_v1beta1/document.py +++ b/firestore/google/cloud/firestore_v1beta1/document.py @@ -306,7 +306,7 @@ def update(self, field_updates, option=None): ``field_updates``. To delete / remove a field from an existing document, use the - :attr:`~.firestore_v1beta1.constants.DELETE_FIELD` sentinel. So + :attr:`~.firestore_v1beta1.transforms.DELETE_FIELD` sentinel. So with the example above, sending .. code-block:: python @@ -330,7 +330,7 @@ def update(self, field_updates, option=None): To set a field to the current time on the server when the update is received, use the - :attr:`~.firestore_v1beta1.constants.SERVER_TIMESTAMP` sentinel. + :attr:`~.firestore_v1beta1.transforms.SERVER_TIMESTAMP` sentinel. Sending .. code-block:: python diff --git a/firestore/google/cloud/firestore_v1beta1/transforms.py b/firestore/google/cloud/firestore_v1beta1/transforms.py new file mode 100644 index 000000000000..b3b73da20a16 --- /dev/null +++ b/firestore/google/cloud/firestore_v1beta1/transforms.py @@ -0,0 +1,82 @@ +# Copyright 2017 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpful constants to use for Google Cloud Firestore.""" + + +class Sentinel(object): + """Sentinel objects used to signal special handling.""" + __slots__ = ('description',) + + def __init__(self, description): + self.description = description + + def __repr__(self): + return "Sentinel: {}".format(self.description) + + +DELETE_FIELD = Sentinel("Value used to delete a field in a document.") + + +SERVER_TIMESTAMP = Sentinel( + "Value used to set a document field to the server timestamp.") + + +class _ValueList(object): + """Read-only list of values. + + Args: + values (List | Tuple): values held in the helper. + """ + slots = ('_values',) + + def __init__(self, values): + if not isinstance(values, (list, tuple)): + raise ValueError("'values' must be a list or tuple.") + + if len(values) == 0: + raise ValueError("'values' must be non-empty.") + + self._values = list(values) + + @property + def values(self): + """Values to append. + + Returns (List): + values to be appended by the transform. + """ + return self._values + + +class ArrayUnion(_ValueList): + """Field transform: appends missing values to an array field. + + See: + https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.DocumentTransform.FieldTransform.FIELDS.google.firestore.v1beta1.ArrayValue.google.firestore.v1beta1.DocumentTransform.FieldTransform.append_missing_elements + + Args: + values (List | Tuple): values to append. + """ + + +class ArrayRemove(_ValueList): + """Field transform: remove values from an array field. + + See: + https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.DocumentTransform.FieldTransform.FIELDS.google.firestore.v1beta1.ArrayValue.google.firestore.v1beta1.DocumentTransform.FieldTransform.remove_all_from_array + + Args: + values (List | Tuple): values to remove. + """ diff --git a/firestore/tests/unit/test__helpers.py b/firestore/tests/unit/test__helpers.py index 5e6f33b56a3a..cc62780728a6 100644 --- a/firestore/tests/unit/test__helpers.py +++ b/firestore/tests/unit/test__helpers.py @@ -1241,13 +1241,15 @@ def test_ctor_w_empty_document(self): self.assertEqual(inst.field_paths, []) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, {}) self.assertTrue(inst.empty_document) self.assertFalse(inst.has_transforms) self.assertEqual(inst.transform_paths, []) def test_ctor_w_delete_field_shallow(self): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD document_data = { 'a': DELETE_FIELD, @@ -1259,13 +1261,15 @@ def test_ctor_w_delete_field_shallow(self): self.assertEqual(inst.field_paths, []) self.assertEqual(inst.deleted_fields, [_make_field_path('a')]) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, {}) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) self.assertEqual(inst.transform_paths, []) def test_ctor_w_delete_field_nested(self): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD document_data = { 'a': { @@ -1282,13 +1286,15 @@ def test_ctor_w_delete_field_nested(self): self.assertEqual( inst.deleted_fields, [_make_field_path('a', 'b', 'c')]) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, {}) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) self.assertEqual(inst.transform_paths, []) def test_ctor_w_server_timestamp_shallow(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_data = { 'a': SERVER_TIMESTAMP, @@ -1300,13 +1306,15 @@ def test_ctor_w_server_timestamp_shallow(self): self.assertEqual(inst.field_paths, []) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, [_make_field_path('a')]) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, {}) self.assertFalse(inst.empty_document) self.assertTrue(inst.has_transforms) self.assertEqual(inst.transform_paths, [_make_field_path('a')]) def test_ctor_w_server_timestamp_nested(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_data = { 'a': { @@ -1323,6 +1331,114 @@ def test_ctor_w_server_timestamp_nested(self): self.assertEqual(inst.deleted_fields, []) self.assertEqual( inst.server_timestamps, [_make_field_path('a', 'b', 'c')]) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) + self.assertEqual(inst.set_fields, {}) + self.assertFalse(inst.empty_document) + self.assertTrue(inst.has_transforms) + self.assertEqual( + inst.transform_paths, [_make_field_path('a', 'b', 'c')]) + + def test_ctor_w_array_remove_shallow(self): + from google.cloud.firestore_v1beta1.transforms import ArrayRemove + + values = [1, 3, 5] + document_data = { + 'a': ArrayRemove(values), + } + + inst = self._make_one(document_data) + + expected_array_removes = { + _make_field_path('a'): values, + } + self.assertEqual(inst.document_data, document_data) + self.assertEqual(inst.field_paths, []) + self.assertEqual(inst.deleted_fields, []) + self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, expected_array_removes) + self.assertEqual(inst.array_unions, {}) + self.assertEqual(inst.set_fields, {}) + self.assertFalse(inst.empty_document) + self.assertTrue(inst.has_transforms) + self.assertEqual(inst.transform_paths, [_make_field_path('a')]) + + def test_ctor_w_array_remove_nested(self): + from google.cloud.firestore_v1beta1.transforms import ArrayRemove + + values = [2, 4, 8] + document_data = { + 'a': { + 'b': { + 'c': ArrayRemove(values), + } + } + } + + inst = self._make_one(document_data) + + expected_array_removes = { + _make_field_path('a', 'b', 'c'): values, + } + self.assertEqual(inst.document_data, document_data) + self.assertEqual(inst.field_paths, []) + self.assertEqual(inst.deleted_fields, []) + self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, expected_array_removes) + self.assertEqual(inst.array_unions, {}) + self.assertEqual(inst.set_fields, {}) + self.assertFalse(inst.empty_document) + self.assertTrue(inst.has_transforms) + self.assertEqual( + inst.transform_paths, [_make_field_path('a', 'b', 'c')]) + + def test_ctor_w_array_union_shallow(self): + from google.cloud.firestore_v1beta1.transforms import ArrayUnion + + values = [1, 3, 5] + document_data = { + 'a': ArrayUnion(values), + } + + inst = self._make_one(document_data) + + expected_array_unions = { + _make_field_path('a'): values, + } + self.assertEqual(inst.document_data, document_data) + self.assertEqual(inst.field_paths, []) + self.assertEqual(inst.deleted_fields, []) + self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, expected_array_unions) + self.assertEqual(inst.set_fields, {}) + self.assertFalse(inst.empty_document) + self.assertTrue(inst.has_transforms) + self.assertEqual(inst.transform_paths, [_make_field_path('a')]) + + def test_ctor_w_array_union_nested(self): + from google.cloud.firestore_v1beta1.transforms import ArrayUnion + + values = [2, 4, 8] + document_data = { + 'a': { + 'b': { + 'c': ArrayUnion(values), + } + } + } + + inst = self._make_one(document_data) + + expected_array_unions = { + _make_field_path('a', 'b', 'c'): values, + } + self.assertEqual(inst.document_data, document_data) + self.assertEqual(inst.field_paths, []) + self.assertEqual(inst.deleted_fields, []) + self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, expected_array_unions) self.assertEqual(inst.set_fields, {}) self.assertFalse(inst.empty_document) self.assertTrue(inst.has_transforms) @@ -1343,6 +1459,8 @@ def test_ctor_w_empty_dict_shallow(self): self.assertEqual(inst.field_paths, expected_field_paths) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, document_data) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) @@ -1367,6 +1485,8 @@ def test_ctor_w_empty_dict_nested(self): self.assertEqual(inst.field_paths, expected_field_paths) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, document_data) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) @@ -1390,6 +1510,8 @@ def test_ctor_w_normal_value_shallow(self): self.assertEqual(inst.field_paths, expected_field_paths) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, document_data) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) @@ -1418,6 +1540,8 @@ def test_ctor_w_normal_value_nested(self): self.assertEqual(inst.field_paths, expected_field_paths) self.assertEqual(inst.deleted_fields, []) self.assertEqual(inst.server_timestamps, []) + self.assertEqual(inst.array_removes, {}) + self.assertEqual(inst.array_unions, {}) self.assertEqual(inst.set_fields, document_data) self.assertFalse(inst.empty_document) self.assertFalse(inst.has_transforms) @@ -1456,9 +1580,9 @@ def test_get_update_pb_wo_exists_precondition(self): self.assertEqual(update_pb.update.fields, encode_dict(document_data)) self.assertFalse(update_pb.HasField('current_document')) - def test_get_transform_pb_w_exists_precondition(self): + def test_get_transform_pb_w_server_timestamp_w_exists_precondition(self): from google.cloud.firestore_v1beta1.proto import write_pb2 - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP from google.cloud.firestore_v1beta1._helpers import REQUEST_TIME_ENUM document_data = { @@ -1481,9 +1605,9 @@ def test_get_transform_pb_w_exists_precondition(self): self.assertTrue(transform_pb.HasField('current_document')) self.assertFalse(transform_pb.current_document.exists) - def test_get_transform_pb_wo_exists_precondition(self): + def test_get_transform_pb_w_server_timestamp_wo_exists_precondition(self): from google.cloud.firestore_v1beta1.proto import write_pb2 - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP from google.cloud.firestore_v1beta1._helpers import REQUEST_TIME_ENUM document_data = { @@ -1509,6 +1633,73 @@ def test_get_transform_pb_wo_exists_precondition(self): self.assertEqual(transform.set_to_server_value, REQUEST_TIME_ENUM) self.assertFalse(transform_pb.HasField('current_document')) + @staticmethod + def _array_value_to_list(array_value): + from google.cloud.firestore_v1beta1._helpers import decode_value + + return [ + decode_value(element, client=None) + for element in array_value.values + ] + + def test_get_transform_pb_w_array_remove(self): + from google.cloud.firestore_v1beta1.proto import write_pb2 + from google.cloud.firestore_v1beta1.transforms import ArrayRemove + + values = [2, 4, 8] + document_data = { + 'a': { + 'b': { + 'c': ArrayRemove(values), + }, + }, + } + inst = self._make_one(document_data) + document_path = ( + 'projects/project-id/databases/(default)/' + 'documents/document-id') + + transform_pb = inst.get_transform_pb(document_path) + + self.assertIsInstance(transform_pb, write_pb2.Write) + self.assertEqual(transform_pb.transform.document, document_path) + transforms = transform_pb.transform.field_transforms + self.assertEqual(len(transforms), 1) + transform = transforms[0] + self.assertEqual(transform.field_path, 'a.b.c') + removed = self._array_value_to_list(transform.remove_all_from_array) + self.assertEqual(removed, values) + self.assertFalse(transform_pb.HasField('current_document')) + + def test_get_transform_pb_w_array_union(self): + from google.cloud.firestore_v1beta1.proto import write_pb2 + from google.cloud.firestore_v1beta1.transforms import ArrayUnion + + values = [1, 3, 5] + document_data = { + 'a': { + 'b': { + 'c': ArrayUnion(values), + }, + }, + } + inst = self._make_one(document_data) + document_path = ( + 'projects/project-id/databases/(default)/' + 'documents/document-id') + + transform_pb = inst.get_transform_pb(document_path) + + self.assertIsInstance(transform_pb, write_pb2.Write) + self.assertEqual(transform_pb.transform.document, document_path) + transforms = transform_pb.transform.field_transforms + self.assertEqual(len(transforms), 1) + transform = transforms[0] + self.assertEqual(transform.field_path, 'a.b.c') + added = self._array_value_to_list(transform.append_missing_elements) + self.assertEqual(added, values) + self.assertFalse(transform_pb.HasField('current_document')) + class Test_pbs_for_create(unittest.TestCase): @@ -1553,7 +1744,7 @@ def _make_write_w_transform(document_path, fields): ) def _helper(self, do_transform=False, empty_val=False): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') document_data = { @@ -1646,7 +1837,7 @@ def test_w_empty_document(self): self.assertEqual(write_pbs, expected_pbs) def test_w_only_server_timestamp(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') document_data = {'butter': SERVER_TIMESTAMP} @@ -1659,7 +1850,7 @@ def test_w_only_server_timestamp(self): self.assertEqual(write_pbs, expected_pbs) def _helper(self, do_transform=False, empty_val=False): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') document_data = { @@ -1734,7 +1925,7 @@ def test_apply_merge_all_w_empty_document(self): self.assertFalse(inst.has_updates) def test_apply_merge_all_w_delete(self): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD document_data = { 'write_me': 'value', @@ -1754,7 +1945,7 @@ def test_apply_merge_all_w_delete(self): self.assertTrue(inst.has_updates) def test_apply_merge_all_w_server_timestamp(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_data = { 'write_me': 'value', @@ -1787,7 +1978,7 @@ def test_apply_merge_list_fields_w_empty_document(self): inst.apply_merge(['nonesuch', 'or.this']) def test_apply_merge_list_fields_w_unmerged_delete(self): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD document_data = { 'write_me': 'value', @@ -1801,7 +1992,7 @@ def test_apply_merge_list_fields_w_unmerged_delete(self): inst.apply_merge(['write_me', 'delete_me']) def test_apply_merge_list_fields_w_delete(self): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD document_data = { 'write_me': 'value', @@ -1864,7 +2055,7 @@ def test_apply_merge_list_fields_w_non_merge_field(self): self.assertTrue(inst.has_updates) def test_apply_merge_list_fields_w_server_timestamp(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_data = { 'write_me': 'value', @@ -1895,6 +2086,72 @@ def test_apply_merge_list_fields_w_server_timestamp(self): self.assertEqual(inst.server_timestamps, expected_server_timestamps) self.assertTrue(inst.has_updates) + def test_apply_merge_list_fields_w_array_remove(self): + from google.cloud.firestore_v1beta1.transforms import ArrayRemove + + values = [2, 4, 8] + document_data = { + 'write_me': 'value', + 'remove_me': ArrayRemove(values), + 'ignored_remove_me': ArrayRemove((1, 3, 5)), + } + inst = self._make_one(document_data) + + inst.apply_merge( + [_make_field_path('write_me'), _make_field_path('remove_me')]) + + expected_data_merge = [ + _make_field_path('write_me'), + ] + expected_transform_merge = [ + _make_field_path('remove_me'), + ] + expected_merge = [ + _make_field_path('remove_me'), + _make_field_path('write_me'), + ] + self.assertEqual(inst.data_merge, expected_data_merge) + self.assertEqual(inst.transform_merge, expected_transform_merge) + self.assertEqual(inst.merge, expected_merge) + expected_array_removes = { + _make_field_path('remove_me'): values, + } + self.assertEqual(inst.array_removes, expected_array_removes) + self.assertTrue(inst.has_updates) + + def test_apply_merge_list_fields_w_array_union(self): + from google.cloud.firestore_v1beta1.transforms import ArrayUnion + + values = [1, 3, 5] + document_data = { + 'write_me': 'value', + 'union_me': ArrayUnion(values), + 'ignored_union_me': ArrayUnion((2, 4, 8)), + } + inst = self._make_one(document_data) + + inst.apply_merge( + [_make_field_path('write_me'), _make_field_path('union_me')]) + + expected_data_merge = [ + _make_field_path('write_me'), + ] + expected_transform_merge = [ + _make_field_path('union_me'), + ] + expected_merge = [ + _make_field_path('union_me'), + _make_field_path('write_me'), + ] + self.assertEqual(inst.data_merge, expected_data_merge) + self.assertEqual(inst.transform_merge, expected_transform_merge) + self.assertEqual(inst.merge, expected_merge) + expected_array_unions = { + _make_field_path('union_me'): values, + } + self.assertEqual(inst.array_unions, expected_array_unions) + self.assertTrue(inst.has_updates) + class Test_pbs_for_set_with_merge(unittest.TestCase): @@ -1977,7 +2234,7 @@ def test_with_merge_field_wo_transform(self): self.assertEqual(write_pbs, expected_pbs) def test_with_merge_true_w_transform(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') update_data = { @@ -2001,7 +2258,7 @@ def test_with_merge_true_w_transform(self): self.assertEqual(write_pbs, expected_pbs) def test_with_merge_field_w_transform(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') update_data = { @@ -2026,7 +2283,7 @@ def test_with_merge_field_w_transform(self): self.assertEqual(write_pbs, expected_pbs) def test_with_merge_field_w_transform_masking_simple(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') update_data = { @@ -2049,7 +2306,7 @@ def test_with_merge_field_w_transform_masking_simple(self): self.assertEqual(write_pbs, expected_pbs) def test_with_merge_field_w_transform_parent(self): - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP document_path = _make_ref_string(u'little', u'town', u'of', u'ham') update_data = { @@ -2190,7 +2447,7 @@ def _call_fut(document_path, field_updates, option): def _helper(self, option=None, do_transform=False, **write_kwargs): from google.cloud.firestore_v1beta1 import _helpers - from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP + from google.cloud.firestore_v1beta1.transforms import SERVER_TIMESTAMP from google.cloud.firestore_v1beta1.gapic import enums from google.cloud.firestore_v1beta1.proto import common_pb2 from google.cloud.firestore_v1beta1.proto import document_pb2 diff --git a/firestore/tests/unit/test_cross_language.py b/firestore/tests/unit/test_cross_language.py index 9362d874861b..5190eadc6c4f 100644 --- a/firestore/tests/unit/test_cross_language.py +++ b/firestore/tests/unit/test_cross_language.py @@ -26,46 +26,6 @@ from google.cloud.firestore_v1beta1.proto import test_pb2 from google.cloud.firestore_v1beta1.proto import write_pb2 -_UNIMPLEMENTED_FEATURES = [ - # tests having to do with the ArrayUnion, ArrayRemove, and Delete - # transforms - 'create-all-transforms.textproto', - 'create-arrayremove-multi.textproto', - 'create-arrayremove-nested.textproto', - 'create-arrayremove-noarray-nested.textproto', - 'create-arrayremove-noarray.textproto', - 'create-arrayremove.textproto', - 'create-arrayunion-multi.textproto', - 'create-arrayunion-nested.textproto', - 'create-arrayunion-noarray-nested.textproto', - 'create-arrayunion-noarray.textproto', - 'create-arrayunion.textproto', - 'set-all-transforms.textproto', - 'set-arrayremove-multi.textproto', - 'set-arrayremove-nested.textproto', - 'set-arrayremove-noarray-nested.textproto', - 'set-arrayremove-noarray.textproto', - 'set-arrayremove.textproto', - 'set-arrayunion-multi.textproto', - 'set-arrayunion-nested.textproto', - 'set-arrayunion-noarray-nested.textproto', - 'set-arrayunion-noarray.textproto', - 'set-arrayunion.textproto', - 'update-all-transforms.textproto', - 'update-arrayremove-alone.textproto', - 'update-arrayremove-multi.textproto', - 'update-arrayremove-nested.textproto', - 'update-arrayremove-noarray-nested.textproto', - 'update-arrayremove-noarray.textproto', - 'update-arrayremove.textproto', - 'update-arrayunion-alone.textproto', - 'update-arrayunion-multi.textproto', - 'update-arrayunion-nested.textproto', - 'update-arrayunion-noarray-nested.textproto', - 'update-arrayunion-noarray.textproto', - 'update-arrayunion.textproto', - ] - def _load_testproto(filename): with open(filename, 'r') as tp_file: @@ -79,44 +39,37 @@ def _load_testproto(filename): return test_proto -_UNIMPLEMENTED_FEATURE_TESTPROTOS = [ - _load_testproto(filename) for filename in sorted( - glob.glob('tests/unit/testdata/*.textproto')) - if os.path.split(filename)[-1] in _UNIMPLEMENTED_FEATURES -] - -IMPLEMENTED_FEATURE_TESTPROTOS = [ +ALL_TESTPROTOS = [ _load_testproto(filename) for filename in sorted( glob.glob('tests/unit/testdata/*.textproto')) - if not os.path.split(filename)[-1] in _UNIMPLEMENTED_FEATURES ] _CREATE_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'create'] _GET_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'get'] _SET_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'set'] _UPDATE_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'update'] _UPDATE_PATHS_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'update_paths'] _DELETE_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'delete'] _LISTEN_TESTPROTOS = [ - test_proto for test_proto in IMPLEMENTED_FEATURE_TESTPROTOS + test_proto for test_proto in ALL_TESTPROTOS if test_proto.WhichOneof('test') == 'listen'] @@ -239,22 +192,23 @@ def test_listen_paths_testprotos(test_proto): # pragma: NO COVER pass -@pytest.mark.skip(reason="Feature not yet implemented in Python.") -@pytest.mark.parametrize('test_proto', _UNIMPLEMENTED_FEATURE_TESTPROTOS) -def test_unimplemented_features_testprotos(test_proto): # pragma: NO COVER - pass - - def convert_data(v): # Replace the strings 'ServerTimestamp' and 'Delete' with the corresponding # sentinels. - from google.cloud.firestore_v1beta1 import SERVER_TIMESTAMP, DELETE_FIELD + from google.cloud.firestore_v1beta1 import ArrayRemove + from google.cloud.firestore_v1beta1 import ArrayUnion + from google.cloud.firestore_v1beta1 import DELETE_FIELD + from google.cloud.firestore_v1beta1 import SERVER_TIMESTAMP if v == 'ServerTimestamp': return SERVER_TIMESTAMP elif v == 'Delete': return DELETE_FIELD elif isinstance(v, list): + if v[0] == 'ArrayRemove': + return ArrayRemove([convert_data(e) for e in v[1:]]) + if v[0] == 'ArrayUnion': + return ArrayUnion([convert_data(e) for e in v[1:]]) return [convert_data(e) for e in v] elif isinstance(v, dict): return {k: convert_data(v2) for k, v2 in v.items()} diff --git a/firestore/tests/unit/test_document.py b/firestore/tests/unit/test_document.py index 75531d92edbe..0145372a75e0 100644 --- a/firestore/tests/unit/test_document.py +++ b/firestore/tests/unit/test_document.py @@ -340,7 +340,7 @@ def _write_pb_for_update(document_path, update_values, field_paths): ) def _update_helper(self, **option_kwargs): - from google.cloud.firestore_v1beta1.constants import DELETE_FIELD + from google.cloud.firestore_v1beta1.transforms import DELETE_FIELD # Create a minimal fake GAPIC with a dummy response. firestore_api = mock.Mock(spec=['commit']) diff --git a/firestore/tests/unit/test_transforms.py b/firestore/tests/unit/test_transforms.py new file mode 100644 index 000000000000..8833848833ae --- /dev/null +++ b/firestore/tests/unit/test_transforms.py @@ -0,0 +1,54 @@ +# Copyright 2017 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + + +class Test_ValueList(unittest.TestCase): + + @staticmethod + def _get_target_class(): + from google.cloud.firestore_v1beta1.transforms import _ValueList + + return _ValueList + + def _make_one(self, values): + return self._get_target_class()(values) + + def test_ctor_w_non_list_non_tuple(self): + invalid_values = ( + None, + u'phred', + b'DEADBEEF', + 123, + {}, + object(), + ) + for invalid_value in invalid_values: + with self.assertRaises(ValueError): + self._make_one(invalid_value) + + def test_ctor_w_empty(self): + with self.assertRaises(ValueError): + self._make_one([]) + + def test_ctor_w_non_empty_list(self): + values = ['phred', 'bharney'] + union = self._make_one(values) + self.assertEqual(union.values, values) + + def test_ctor_w_non_empty_tuple(self): + values = ('phred', 'bharney') + union = self._make_one(values) + self.assertEqual(union.values, list(values))