diff --git a/docs/api/modules.rst b/docs/api/modules.rst index 940aa38420..49aa4a3600 100644 --- a/docs/api/modules.rst +++ b/docs/api/modules.rst @@ -13,6 +13,7 @@ API Documentation lifecycle partition predicate + projection proxy/modules security serialization diff --git a/docs/api/projection.rst b/docs/api/projection.rst new file mode 100644 index 0000000000..625ae8656e --- /dev/null +++ b/docs/api/projection.rst @@ -0,0 +1,4 @@ +Projection +========== + +.. automodule:: hazelcast.projection \ No newline at end of file diff --git a/docs/features.rst b/docs/features.rst index f9b87ac30d..25ea0d21e0 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -30,6 +30,7 @@ features: - Built-in Predicates - Listener with Predicate - Fast Aggregations +- Projections - Near Cache Support - Programmatic Configuration - SSL Support (requires Enterprise server) diff --git a/docs/using_python_client_with_hazelcast_imdg.rst b/docs/using_python_client_with_hazelcast_imdg.rst index 047d3be1af..3f70d609ac 100644 --- a/docs/using_python_client_with_hazelcast_imdg.rst +++ b/docs/using_python_client_with_hazelcast_imdg.rst @@ -2166,31 +2166,30 @@ See the following example. import hazelcast from hazelcast.core import HazelcastJsonValue - from hazelcast.predicate import greater_or_equal + from hazelcast.predicate import greater from hazelcast.projection import single_attribute, multi_attribute client = hazelcast.HazelcastClient() employees = client.get_map("employees").blocking() - employees.put(1, HazelcastJsonValue('{"Age": 23, "Height": 180, "Weight": 60}')) - employees.put(2, HazelcastJsonValue('{"Age": 21, "Height": 170, "Weight": 70}')) + employees.put(1, HazelcastJsonValue({"age": 25, "height": 180, "weight": 60})) + employees.put(2, HazelcastJsonValue({"age": 21, "height": 170, "weight": 70})) + employees.put(3, HazelcastJsonValue({"age": 40, "height": 175, "weight": 75})) - employee_ages = employees.project(single_attribute("Age")) - # Prints: - # The ages of employees are [21, 23] - print("The ages of employees are %s" % employee_ages) + ages = employees.project(single_attribute("age")) - # Run Single Attribute With Predicate - employee_ages = employees.project(single_attribute("Age"), greater_or_equal("Age", 23)) - # Prints: - # The employee age is 23 - print("The employee age is: %s" % employee_ages[0]) + # Prints: "Ages of the employees are [21, 25, 40]" + print("Ages of the employees are %s" % ages) - # Run Multi Attribute Projection - employee_multi_attribute = employees.project(multi_attribute("Age", "Height")) - # Prints: - # Employee 1 age and height: [21, 170] Employee 2 age and height: [23, 180] - print("Employee 1 age and height: %s Employee 2 age and height: %s" % (employee_multi_attribute[0], employee_multi_attribute[1])) + filtered_ages = employees.project(single_attribute("age"), greater("age", 23)) + + # Prints: "Ages of the filtered employees are [25, 40]" + print("Ages of the filtered employees are %s" % filtered_ages) + + attributes = employees.project(multi_attribute("age", "height")) + + # Prints: "Ages and heights of the employees are [[21, 170], [25, 180], [40, 175]]" + print("Ages and heights of the employees are %s" % attributes) Performance diff --git a/examples/projections/projections_example.py b/examples/projections/projections_example.py new file mode 100644 index 0000000000..d2254425b9 --- /dev/null +++ b/examples/projections/projections_example.py @@ -0,0 +1,29 @@ +import hazelcast + +from hazelcast.core import HazelcastJsonValue +from hazelcast.predicate import less_or_equal +from hazelcast.projection import single_attribute, multi_attribute + +client = hazelcast.HazelcastClient() + +people = client.get_map("people").blocking() + +people.put_all( + { + 1: HazelcastJsonValue({"name": "Philip", "age": 46}), + 2: HazelcastJsonValue({"name": "Elizabeth", "age": 44}), + 3: HazelcastJsonValue({"name": "Henry", "age": 13}), + 4: HazelcastJsonValue({"name": "Paige", "age": 15}), + } +) + +names = people.project(single_attribute("name")) +print("Names of the people are %s." % names) + +children_names = people.project(single_attribute("name"), less_or_equal("age", 18)) +print("Names of the children are %s." % children_names) + +names_and_ages = people.project(multi_attribute("name", "age")) +print("Names and ages of the people are %s." % names_and_ages) + +client.shutdown() diff --git a/hazelcast/core.py b/hazelcast/core.py index 385f12f7cb..7d4c063b27 100644 --- a/hazelcast/core.py +++ b/hazelcast/core.py @@ -388,8 +388,10 @@ def __init__(self, key=None, value=None): @property def key(self): + """Key of the entry.""" return self._key @property def value(self): + """Value of the entry.""" return self._value diff --git a/hazelcast/projection.py b/hazelcast/projection.py index 8355e9e469..7e20a38b4b 100644 --- a/hazelcast/projection.py +++ b/hazelcast/projection.py @@ -28,6 +28,7 @@ def get_class_id(self): def _validate_attribute_path(attribute_path): + # type: (str) -> None if not attribute_path: raise ValueError("attribute_path must not be None or empty") @@ -37,6 +38,7 @@ def _validate_attribute_path(attribute_path): class _SingleAttributeProjection(_AbstractProjection): def __init__(self, attribute_path): + # type: (str) -> None _validate_attribute_path(attribute_path) self._attribute_path = attribute_path @@ -48,7 +50,8 @@ def get_class_id(self): class _MultiAttributeProjection(_AbstractProjection): - def __init__(self, *attribute_paths): + def __init__(self, attribute_paths): + # type: (list[str]) -> None if not attribute_paths: raise ValueError("Specify at least one attribute path") @@ -73,6 +76,7 @@ def get_class_id(self): def single_attribute(attribute_path): + # type: (str) -> Projection """Creates a projection that extracts the value of the given attribute path. @@ -81,12 +85,13 @@ def single_attribute(attribute_path): Returns: Projection[any]: A projection that extracts the value of the given - attribute path. + attribute path. """ return _SingleAttributeProjection(attribute_path) def multi_attribute(*attribute_paths): + # type: (str) -> Projection """Creates a projection that extracts the values of one or more attribute paths. @@ -95,16 +100,17 @@ def multi_attribute(*attribute_paths): Returns: Projection[list]: A projection that extracts the values of the given - attribute paths. + attribute paths. """ - return _MultiAttributeProjection(*attribute_paths) + return _MultiAttributeProjection(list(attribute_paths)) def identity(): + # type: () -> Projection """Creates a projection that does no transformation. Returns: Projection[hazelcast.core.MapEntry]: A projection that does no - transformation. + transformation. """ return _IdentityProjection() diff --git a/hazelcast/proxy/map.py b/hazelcast/proxy/map.py index 2c16329504..b18d18ca56 100644 --- a/hazelcast/proxy/map.py +++ b/hazelcast/proxy/map.py @@ -328,6 +328,7 @@ def aggregate(self, aggregator, predicate=None): """ check_not_none(aggregator, "aggregator can't be none") aggregator_data = self._to_data(aggregator) + if predicate: if isinstance(predicate, PagingPredicate): raise AssertionError("Paging predicate is not supported.") @@ -347,44 +348,6 @@ def handler(message): request = map_aggregate_codec.encode_request(self.name, aggregator_data) return self._invoke(request, handler) - def project(self, projection, predicate=None): - """Applies the projection logic on map entries and filter the result with the - predicate, if given. - - Args: - projection (hazelcast.projection.Projection): Projection to project the - entries with. - predicate (hazelcast.predicate.Predicate): Predicate to filter the entries - with. - - Returns: - hazelcast.future.Future: The result of the projection. - """ - check_not_none(projection, "Projection can't be none") - projection_data = self._to_data(projection) - if predicate: - if isinstance(predicate, PagingPredicate): - raise AssertionError("Paging predicate is not supported.") - - def handler(message): - return ImmutableLazyDataList( - map_project_with_predicate_codec.decode_response(message), self._to_object - ) - - predicate_data = self._to_data(predicate) - request = map_project_with_predicate_codec.encode_request( - self.name, projection_data, predicate_data - ) - return self._invoke(request, handler) - - def handler(message): - return ImmutableLazyDataList( - map_project_codec.decode_response(message), self._to_object - ) - - request = map_project_codec.encode_request(self.name, projection_data) - return self._invoke(request, handler) - def clear(self): """Clears the map. @@ -876,6 +839,45 @@ def lock(self, key, lease_time=None): self._invocation_service.invoke(invocation) return invocation.future + def project(self, projection, predicate=None): + """Applies the projection logic on map entries and filter the result with the + predicate, if given. + + Args: + projection (hazelcast.projection.Projection): Projection to project the + entries with. + predicate (hazelcast.predicate.Predicate): Predicate to filter the entries + with. + + Returns: + hazelcast.future.Future: The result of the projection. + """ + check_not_none(projection, "Projection can't be none") + projection_data = self._to_data(projection) + + if predicate: + if isinstance(predicate, PagingPredicate): + raise AssertionError("Paging predicate is not supported.") + + def handler(message): + return ImmutableLazyDataList( + map_project_with_predicate_codec.decode_response(message), self._to_object + ) + + predicate_data = self._to_data(predicate) + request = map_project_with_predicate_codec.encode_request( + self.name, projection_data, predicate_data + ) + return self._invoke(request, handler) + + def handler(message): + return ImmutableLazyDataList( + map_project_codec.decode_response(message), self._to_object + ) + + request = map_project_codec.encode_request(self.name, projection_data) + return self._invoke(request, handler) + def put(self, key, value, ttl=None, max_idle=None): """Associates the specified value with the specified key in this map. diff --git a/tests/integration/backward_compatible/proxy/map_test.py b/tests/integration/backward_compatible/proxy/map_test.py index e478b0594d..ec93eb4860 100644 --- a/tests/integration/backward_compatible/proxy/map_test.py +++ b/tests/integration/backward_compatible/proxy/map_test.py @@ -41,7 +41,7 @@ from hazelcast.core import HazelcastJsonValue from hazelcast.config import IndexType, IntType from hazelcast.errors import HazelcastError -from hazelcast.predicate import greater_or_equal, less_or_equal, sql, between +from hazelcast.predicate import greater_or_equal, less_or_equal, sql, paging, true from hazelcast.proxy.map import EntryEventType from hazelcast.serialization.api import IdentifiedDataSerializable from hazelcast.six.moves import range @@ -796,6 +796,14 @@ def setUp(self): def tearDown(self): self.map.destroy() + def test_aggregate_with_none_aggregator(self): + with self.assertRaises(AssertionError): + self.map.aggregate(None) + + def test_aggregate_with_paging_predicate(self): + with self.assertRaises(AssertionError): + self.map.aggregate(int_avg("foo"), paging(true(), 10)) + def test_int_average(self): average = self.map.aggregate(int_avg()) self.assertEqual(24.5, average) @@ -1034,23 +1042,32 @@ def setUp(self): def tearDown(self): self.map.destroy() + def test_project_with_none_projection(self): + with self.assertRaises(AssertionError): + self.map.project(None) + + def test_project_with_paging_predicate(self): + with self.assertRaises(AssertionError): + self.map.project(single_attribute("foo"), paging(true(), 10)) + def test_single_attribute(self): - attribute = self.map.project(single_attribute("attr1")) - six.assertCountEqual(self, [4, 1], attribute) + attributes = self.map.project(single_attribute("attr1")) + six.assertCountEqual(self, [1, 4], attributes) def test_single_attribute_with_predicate(self): - attribute = self.map.project(single_attribute("attr1"), greater_or_equal("attr1", 4)) - self.assertEqual([4], attribute) + attributes = self.map.project(single_attribute("attr1"), greater_or_equal("attr1", 4)) + six.assertCountEqual(self, [4], attributes) def test_multi_attribute(self): attributes = self.map.project(multi_attribute("attr1", "attr2")) - six.assertCountEqual(self, [[4, 5], [1, 2]], attributes) + six.assertCountEqual(self, [[1, 2], [4, 5]], attributes) def test_multi_attribute_with_predicate(self): attributes = self.map.project( - multi_attribute("attr1", "attr2"), greater_or_equal("attr2", 3) + multi_attribute("attr1", "attr2"), + greater_or_equal("attr2", 3), ) - self.assertEqual([[4, 5]], attributes) + six.assertCountEqual(self, [[4, 5]], attributes) def test_identity(self): attributes = self.map.project(identity()) @@ -1065,6 +1082,8 @@ def test_identity(self): def test_identity_with_predicate(self): attributes = self.map.project(identity(), greater_or_equal("attr2", 3)) - self.assertEqual( - HazelcastJsonValue('{"attr1": 4, "attr2": 5, "attr3": 6}'), attributes[0].value + six.assertCountEqual( + self, + [HazelcastJsonValue('{"attr1": 4, "attr2": 5, "attr3": 6}')], + [attribute.value for attribute in attributes], ) diff --git a/tests/unit/projection_test.py b/tests/unit/projection_test.py new file mode 100644 index 0000000000..646908cc56 --- /dev/null +++ b/tests/unit/projection_test.py @@ -0,0 +1,25 @@ +import unittest + +from hazelcast.projection import single_attribute, multi_attribute + + +class ProjectionsInvalidInputTest(unittest.TestCase): + def test_single_attribute_with_any_operator(self): + with self.assertRaises(ValueError): + single_attribute("foo[any]") + + def test_single_attribute_with_empty_path(self): + with self.assertRaises(ValueError): + single_attribute("") + + def test_multi_attribute_with_no_paths(self): + with self.assertRaises(ValueError): + multi_attribute() + + def test_multi_attribute_with_any_operator(self): + with self.assertRaises(ValueError): + multi_attribute("valid", "invalid[any]") + + def test_multi_attribute_with_empty_path(self): + with self.assertRaises(ValueError): + multi_attribute("valid", "")