diff --git a/CHANGELOG.md b/CHANGELOG.md index 16734939f0..6a366eb147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Changes are grouped as follows - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [7.63.9] - 2024-10-21 +### Added +- Filters can now be combined using `And` and `Or` by using the operators `&` and `|`. +- Filters can now be negated by using the `~` operator (instead of using the `Not` filter) + ## [7.63.8] - 2024-10-21 ### Fixed - Data Workflows: workflow external ID and version are now URL encoded to allow characters like `/` when calling `workflows.executions.run` diff --git a/cognite/client/_version.py b/cognite/client/_version.py index 621e769a87..014260ccfd 100644 --- a/cognite/client/_version.py +++ b/cognite/client/_version.py @@ -1,4 +1,4 @@ from __future__ import annotations -__version__ = "7.63.8" +__version__ = "7.63.9" __api_subversion__ = "20230101" diff --git a/cognite/client/data_classes/filters.py b/cognite/client/data_classes/filters.py index 5a45aac45e..e551cd4d4d 100644 --- a/cognite/client/data_classes/filters.py +++ b/cognite/client/data_classes/filters.py @@ -202,6 +202,21 @@ def _involved_filter_types(self) -> set[type[Filter]]: output.update(filter_._involved_filter_types()) return output + def _list_filters_without_nesting(self, other: Filter, operator: type[CompoundFilter]) -> list[Filter]: + filters: list[Filter] = [] + filters.extend(self._filters) if isinstance(self, operator) else filters.append(self) + filters.extend(other._filters) if isinstance(other, operator) else filters.append(other) + return filters + + def __and__(self, other: Filter) -> And: + return And(*self._list_filters_without_nesting(other, And)) + + def __or__(self, other: Filter) -> Or: + return Or(*self._list_filters_without_nesting(other, Or)) + + def __invert__(self) -> Not: + return Not(self) + class UnknownFilter(Filter): def __init__(self, filter_name: str, filter_body: dict[str, Any]) -> None: @@ -322,6 +337,10 @@ class And(CompoundFilter): >>> flt = And( ... Equals(my_view.as_property_ref("some_property"), 42), ... In(my_view.as_property_ref("another_property"), ["a", "b", "c"])) + + Using the "&" operator: + + >>> flt = Equals("age", 42) & Equals("name", "Alice") """ _filter_name = "and" @@ -349,6 +368,10 @@ class Or(CompoundFilter): >>> flt = Or( ... Equals(my_view.as_property_ref("some_property"), 42), ... In(my_view.as_property_ref("another_property"), ["a", "b", "c"])) + + Using the "|" operator: + + >>> flt = Equals("name", "Bob") | Equals("name", "Alice") """ _filter_name = "or" @@ -374,6 +397,10 @@ class Not(CompoundFilter): >>> is_42 = Equals(my_view.as_property_ref("some_property"), 42) >>> flt = Not(is_42) + + Using the "~" operator: + + >>> flt = ~Equals("name", "Bob") """ _filter_name = "not" diff --git a/pyproject.toml b/pyproject.toml index 216d74d20b..d827b24f2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cognite-sdk" -version = "7.63.8" +version = "7.63.9" description = "Cognite Python SDK" readme = "README.md" documentation = "https://cognite-sdk-python.readthedocs-hosted.com" diff --git a/tests/tests_unit/test_data_classes/test_data_models/test_filters.py b/tests/tests_unit/test_data_classes/test_data_models/test_filters.py index 91dfd6ee36..e5d51b7f96 100644 --- a/tests/tests_unit/test_data_classes/test_data_models/test_filters.py +++ b/tests/tests_unit/test_data_classes/test_data_models/test_filters.py @@ -250,6 +250,31 @@ def dump_filter_test_data() -> Iterator[ParameterSet]: ) yield pytest.param(prop_list1, expected, id="Prefix filter with list property of objects") yield pytest.param(prop_list2, expected, id="Prefix filter with list property of dicts") + overloaded_filter = f.Equals(property="name", value="bob") & f.HasData( + containers=[("space", "container")] + ) | ~f.Range(property="size", gt=0) + expected = { + "or": [ + { + "and": [ + {"equals": {"property": ["name"], "value": "bob"}}, + {"hasData": [{"type": "container", "space": "space", "externalId": "container"}]}, + ] + }, + {"not": {"range": {"property": ["size"], "gt": 0}}}, + ] + } + yield pytest.param(overloaded_filter, expected, id="Compound filter with overloaded operators") + nested_overloaded_filter = (f.Equals("a", "b") & f.Equals("c", "d")) & (f.Equals("e", "f") & f.Equals("g", "h")) + expected = { + "and": [ + {"equals": {"property": ["a"], "value": "b"}}, + {"equals": {"property": ["c"], "value": "d"}}, + {"equals": {"property": ["e"], "value": "f"}}, + {"equals": {"property": ["g"], "value": "h"}}, + ] + } + yield pytest.param(nested_overloaded_filter, expected, id="Compound filter with nested overloaded and") @pytest.mark.parametrize("user_filter, expected", list(dump_filter_test_data()))