From 8a8ed129ee7dc3bf09596efe68c493ec7565e75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Thu, 5 Sep 2024 12:15:18 +0200 Subject: [PATCH] improve space filter ergonomics to more easily list global nodes (#1913) --- CHANGELOG.md | 6 +++++ cognite/client/_version.py | 2 +- cognite/client/data_classes/filters.py | 21 ++++++++++----- pyproject.toml | 2 +- .../test_data_modeling/test_instances.py | 12 +++++++++ .../test_data_models/test_filters.py | 26 ++++++++++++------- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e573cbe..72d0f7874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ Changes are grouped as follows - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [7.58.6] - 2024-09-05 +### Fixed +- Data modeling convenience filter `SpaceFilter` now allows listing of global nodes by using `equals` + (when a single space is requested (requirement)). This also affects the `space` parameter to e.g. + `client.data_modeling.instances.list(...)` + ## [7.58.5] - 2024-09-04 ### Added - Data modeling filters now support properties that are lists. diff --git a/cognite/client/_version.py b/cognite/client/_version.py index 7805bd1a2..ccd04ba03 100644 --- a/cognite/client/_version.py +++ b/cognite/client/_version.py @@ -1,4 +1,4 @@ from __future__ import annotations -__version__ = "7.58.5" +__version__ = "7.58.6" __api_subversion__ = "20230101" diff --git a/cognite/client/data_classes/filters.py b/cognite/client/data_classes/filters.py index 9f08598d1..fb385090e 100644 --- a/cognite/client/data_classes/filters.py +++ b/cognite/client/data_classes/filters.py @@ -809,7 +809,7 @@ class Search(FilterWithPropertyAndValue): # ######################################################### # -class SpaceFilter(FilterWithPropertyAndValueList): +class SpaceFilter(FilterWithProperty): """Filters instances based on the space. Args: @@ -827,15 +827,24 @@ class SpaceFilter(FilterWithPropertyAndValueList): >>> flt = SpaceFilter("space3", instance_type="edge") """ - _filter_name = In._filter_name - def __init__(self, space: str | SequenceNotStr[str], instance_type: Literal["node", "edge"] = "node") -> None: - space_list = [space] if isinstance(space, str) else list(space) - super().__init__(property=[instance_type, "space"], values=space_list) + super().__init__(property=[instance_type, "space"]) + space = [space] if isinstance(space, str) else list(space) + single = len(space) == 1 + self._value = space[0] if single else space + self._value_key = "value" if single else "values" + self._filter_name = Equals._filter_name if single else In._filter_name + self._involved_filter: set[type[Filter]] = {Equals if single else In} @classmethod def load(cls, filter_: dict[str, Any]) -> NoReturn: raise NotImplementedError("Custom filter 'SpaceFilter' can not be loaded") + def _filter_body(self, camel_case_property: bool) -> dict[str, Any]: + return { + "property": self._dump_property(camel_case_property), + self._value_key: _dump_filter_value(self._value), + } + def _involved_filter_types(self) -> set[type[Filter]]: - return {In} + return self._involved_filter diff --git a/pyproject.toml b/pyproject.toml index 9ae6335af..5209f3871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cognite-sdk" -version = "7.58.5" +version = "7.58.6" description = "Cognite Python SDK" readme = "README.md" documentation = "https://cognite-sdk-python.readthedocs-hosted.com" diff --git a/tests/tests_integration/test_api/test_data_modeling/test_instances.py b/tests/tests_integration/test_api/test_data_modeling/test_instances.py index 8ee76650a..91bcde8c9 100644 --- a/tests/tests_integration/test_api/test_data_modeling/test_instances.py +++ b/tests/tests_integration/test_api/test_data_modeling/test_instances.py @@ -1083,6 +1083,18 @@ def test_search_person(self, cognite_client: CogniteClient) -> None: assert len(persons) > 0 assert all(isinstance(person, PersonRead) for person in persons) + def test_listing_global_nodes(self, cognite_client: CogniteClient) -> None: + from cognite.client.data_classes.data_modeling.cdm.v1 import CogniteUnit + + # Space must be explicitly specified or nothing will be returned: + no_nodes = cognite_client.data_modeling.instances.list(sources=CogniteUnit.get_source()) + assert len(no_nodes) == 0 + + nodes = cognite_client.data_modeling.instances.list( + space="cdf_cdm_units", sources=CogniteUnit.get_source(), limit=5 + ) + assert len(nodes) == 5 + class TestInstancesSync: def test_sync_movies_released_in_1994(self, cognite_client: CogniteClient, movie_view: View) -> None: 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 d201428c9..6bd50012a 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 @@ -255,25 +255,33 @@ def test_user_given_metadata_keys_are_not_camel_cased(property_cls: type) -> Non class TestSpaceFilter: @pytest.mark.parametrize( - "inst_type, space, expected_spaces", + "inst_type, space, expected", ( - ("node", "myspace", ["myspace"]), - ("edge", ["myspace"], ["myspace"]), - ("node", ["myspace", "another"], ["myspace", "another"]), + ("node", "myspace", {"equals": {"property": ["node", "space"], "value": "myspace"}}), + (None, ["myspace"], {"equals": {"property": ["node", "space"], "value": "myspace"}}), + ("edge", ["myspace"], {"equals": {"property": ["edge", "space"], "value": "myspace"}}), + ("node", ["myspace", "another"], {"in": {"property": ["node", "space"], "values": ["myspace", "another"]}}), + ("node", ("myspace", "another"), {"in": {"property": ["node", "space"], "values": ["myspace", "another"]}}), ), ) def test_space_filter( - self, inst_type: Literal["node", "edge"], space: str | list[str], expected_spaces: list[str] + self, inst_type: Literal["node", "edge"], space: str | list[str], expected: dict[str, Any] ) -> None: - space_filter = f.SpaceFilter(space, inst_type) - expected = {"in": {"property": [inst_type, "space"], "values": expected_spaces}} + space_filter = f.SpaceFilter(space, inst_type) if inst_type else f.SpaceFilter(space) assert expected == space_filter.dump() def test_space_filter_passes_isinstance_checks(self) -> None: space_filter = f.SpaceFilter("myspace", "edge") assert isinstance(space_filter, Filter) - def test_space_filter_passes_verification(self, cognite_client: CogniteClient) -> None: - space_filter = f.SpaceFilter("myspace", "edge") + @pytest.mark.parametrize( + "space_filter", + [ + f.SpaceFilter("s1", "edge"), + f.SpaceFilter(["s1"], "edge"), + f.SpaceFilter(["s1", "s2"], "edge"), + ], + ) + def test_space_filter_passes_verification(self, cognite_client: CogniteClient, space_filter: f.SpaceFilter) -> None: cognite_client.data_modeling.instances._validate_filter(space_filter) assert True