diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e38f6e5d..b5a23b26e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ Changes are grouped as follows - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [7.0.3] - 2023-11-15 +### Fixed +- Bug when `cognite.client.data_classes.filter` used with any `data_modeling` endpoint raised a `CogniteAPIError` for + snake_cased properties. This is now fixed. +- When calling `client.relationships.retrieve`, `.retrieve_multiple`, or `.list` with `fetch_resources=True`, the + `target` and `source` resources were not instantiated with a `cognite_client`. This is now fixed. + ## [7.0.2] - 2023-11-15 ### Fixed - Missing Scope `DataSet` for `TemplateGroupAcl` and `TemplateInstancesAcl`. diff --git a/cognite/client/_api/assets.py b/cognite/client/_api/assets.py index 3724943dac..63526108be 100644 --- a/cognite/client/_api/assets.py +++ b/cognite/client/_api/assets.py @@ -874,7 +874,7 @@ def filter( resource_cls=Asset, method="POST", limit=limit, - advanced_filter=filter.dump(camel_case=True) if isinstance(filter, Filter) else filter, + advanced_filter=filter.dump(camel_case_property=True) if isinstance(filter, Filter) else filter, sort=prepare_filter_sort(sort, AssetSort), other_params={"aggregatedProperties": aggregated_properties_camel} if aggregated_properties_camel else {}, ) diff --git a/cognite/client/_api/data_modeling/instances.py b/cognite/client/_api/data_modeling/instances.py index 8da03502d5..3a808e81f2 100644 --- a/cognite/client/_api/data_modeling/instances.py +++ b/cognite/client/_api/data_modeling/instances.py @@ -254,7 +254,7 @@ def __call__( method="POST", chunk_size=chunk_size, limit=limit, - filter=filter.dump() if isinstance(filter, Filter) else filter, + filter=filter.dump(camel_case_property=False) if isinstance(filter, Filter) else filter, other_params=other_params, ), ) @@ -735,7 +735,7 @@ def search( if properties: body["properties"] = properties if filter: - body["filter"] = filter.dump() if isinstance(filter, Filter) else filter + body["filter"] = filter.dump(camel_case_property=False) if isinstance(filter, Filter) else filter res = self._post(url_path=self._RESOURCE_PATH + "/search", json=body) return list_cls.load(res.json()["items"], cognite_client=None) @@ -835,7 +835,7 @@ def aggregate( if group_by: body["groupBy"] = [group_by] if isinstance(group_by, str) else group_by if filter: - body["filter"] = filter.dump() if isinstance(filter, Filter) else filter + body["filter"] = filter.dump(camel_case_property=False) if isinstance(filter, Filter) else filter if query: body["query"] = query if properties: @@ -933,7 +933,7 @@ def histogram( body["aggregates"] = [histogram.dump(camel_case=True) for histogram in histogram_seq] if filter: - body["filter"] = filter.dump() if isinstance(filter, Filter) else filter + body["filter"] = filter.dump(camel_case_property=False) if isinstance(filter, Filter) else filter if query: body["query"] = query if properties: @@ -1125,7 +1125,7 @@ def list( resource_cls=resource_cls, method="POST", limit=limit, - filter=filter.dump() if isinstance(filter, Filter) else filter, + filter=filter.dump(camel_case_property=False) if isinstance(filter, Filter) else filter, other_params=other_params, ), ) diff --git a/cognite/client/_api/data_modeling/spaces.py b/cognite/client/_api/data_modeling/spaces.py index 1bc1e28d63..97fbd46096 100644 --- a/cognite/client/_api/data_modeling/spaces.py +++ b/cognite/client/_api/data_modeling/spaces.py @@ -7,6 +7,7 @@ from cognite.client.data_classes.data_modeling.ids import _load_space_identifier from cognite.client.data_classes.data_modeling.spaces import Space, SpaceApply, SpaceList from cognite.client.utils._concurrency import ConcurrencySettings +from cognite.client.utils.useful_types import SequenceNotStr class SpacesAPI(APIClient): @@ -63,18 +64,18 @@ def __iter__(self) -> Iterator[Space]: return self() @overload - def retrieve(self, spaces: str) -> Space | None: # type: ignore[overload-overlap] + def retrieve(self, spaces: str) -> Space | None: ... @overload - def retrieve(self, spaces: Sequence[str]) -> SpaceList: + def retrieve(self, spaces: SequenceNotStr[str]) -> SpaceList: ... - def retrieve(self, spaces: str | Sequence[str]) -> Space | SpaceList | None: + def retrieve(self, spaces: str | SequenceNotStr[str]) -> Space | SpaceList | None: """`Retrieve one or more spaces. `_ Args: - spaces (str | Sequence[str]): Space ID + spaces (str | SequenceNotStr[str]): Space ID Returns: Space | SpaceList | None: Requested space or None if it does not exist. @@ -100,11 +101,11 @@ def retrieve(self, spaces: str | Sequence[str]) -> Space | SpaceList | None: executor=ConcurrencySettings.get_data_modeling_executor(), ) - def delete(self, spaces: str | Sequence[str]) -> list[str]: + def delete(self, spaces: str | SequenceNotStr[str]) -> list[str]: """`Delete one or more spaces `_ Args: - spaces (str | Sequence[str]): ID or ID list ids of spaces. + spaces (str | SequenceNotStr[str]): ID or ID list ids of spaces. Returns: list[str]: The space(s) which has been deleted. Examples: diff --git a/cognite/client/_api/events.py b/cognite/client/_api/events.py index 1168c7486c..0e11625bbd 100644 --- a/cognite/client/_api/events.py +++ b/cognite/client/_api/events.py @@ -674,7 +674,7 @@ def filter( resource_cls=Event, method="POST", limit=limit, - advanced_filter=filter.dump(camel_case=True) if isinstance(filter, Filter) else filter, + advanced_filter=filter.dump(camel_case_property=True) if isinstance(filter, Filter) else filter, sort=prepare_filter_sort(sort, EventSort), ) diff --git a/cognite/client/_api/sequences.py b/cognite/client/_api/sequences.py index e1896bc715..a9d8319667 100644 --- a/cognite/client/_api/sequences.py +++ b/cognite/client/_api/sequences.py @@ -763,7 +763,7 @@ def filter( resource_cls=Sequence, method="POST", limit=limit, - advanced_filter=filter.dump(camel_case=True) if isinstance(filter, Filter) else filter, + advanced_filter=filter.dump(camel_case_property=True) if isinstance(filter, Filter) else filter, sort=prepare_filter_sort(sort, SequenceSort), api_subversion="beta", ) diff --git a/cognite/client/_api/time_series.py b/cognite/client/_api/time_series.py index 42634cd921..093239d11a 100644 --- a/cognite/client/_api/time_series.py +++ b/cognite/client/_api/time_series.py @@ -755,7 +755,7 @@ def filter( resource_cls=TimeSeries, method="POST", limit=limit, - advanced_filter=filter.dump(camel_case=True) if isinstance(filter, Filter) else filter, + advanced_filter=filter.dump(camel_case_property=True) if isinstance(filter, Filter) else filter, sort=prepare_filter_sort(sort, TimeSeriesSort), ) diff --git a/cognite/client/_api_client.py b/cognite/client/_api_client.py index 4db462b65d..d4485a40d8 100644 --- a/cognite/client/_api_client.py +++ b/cognite/client/_api_client.py @@ -452,7 +452,7 @@ def _list_generator( body["filter"] = filter if advanced_filter: body["advancedFilter"] = ( - advanced_filter.dump(camel_case=True) + advanced_filter.dump(camel_case_property=True) if isinstance(advanced_filter, Filter) else advanced_filter ) @@ -603,7 +603,7 @@ def get_partition(partition: int) -> list[dict[str, Any]]: } if advanced_filter: body["advancedFilter"] = ( - advanced_filter.dump(camel_case=True) + advanced_filter.dump(camel_case_property=True) if isinstance(advanced_filter, Filter) else advanced_filter ) diff --git a/cognite/client/_version.py b/cognite/client/_version.py index fe884f078e..2fbab5dee3 100644 --- a/cognite/client/_version.py +++ b/cognite/client/_version.py @@ -1,4 +1,4 @@ from __future__ import annotations -__version__ = "7.0.2" +__version__ = "7.0.3" __api_subversion__ = "V20220125" diff --git a/cognite/client/data_classes/data_modeling/ids.py b/cognite/client/data_classes/data_modeling/ids.py index 3614db1518..385b5d46b0 100644 --- a/cognite/client/data_classes/data_modeling/ids.py +++ b/cognite/client/data_classes/data_modeling/ids.py @@ -7,6 +7,7 @@ from cognite.client.utils._auxiliary import rename_and_exclude_keys from cognite.client.utils._identifier import DataModelingIdentifier, DataModelingIdentifierSequence from cognite.client.utils._text import convert_all_keys_recursive, convert_all_keys_to_snake_case +from cognite.client.utils.useful_types import SequenceNotStr @dataclass(frozen=True) @@ -169,7 +170,7 @@ def version(self) -> str | None: Id = Union[Tuple[str, str], Tuple[str, str, str], IdLike, VersionedIdLike] -def _load_space_identifier(ids: str | Sequence[str]) -> DataModelingIdentifierSequence: +def _load_space_identifier(ids: str | SequenceNotStr[str]) -> DataModelingIdentifierSequence: is_sequence = isinstance(ids, Sequence) and not isinstance(ids, str) spaces = [ids] if isinstance(ids, str) else ids return DataModelingIdentifierSequence( @@ -193,7 +194,7 @@ def create_args(id_: Id) -> tuple[str, str, str | None, Literal["node", "edge"] return id_[0], id_[1], None, id_type # type: ignore[return-value] raise ValueError("Instance given as a tuple must have two elements (space, externalId)") if isinstance(id_, tuple): - return id_[0], id_[1], id_[2] if len(id_) == 3 else None, None + return id_[0], id_[1], (id_[2] if len(id_) == 3 else None), None instance_type = None if is_instance: instance_type = "node" if isinstance(id_, NodeId) else "edge" diff --git a/cognite/client/data_classes/filters.py b/cognite/client/data_classes/filters.py index b623a6fadb..bc2ccc8561 100644 --- a/cognite/client/data_classes/filters.py +++ b/cognite/client/data_classes/filters.py @@ -68,8 +68,20 @@ def _dump_property(property_: PropertyReference, camel_case: bool) -> list[str] class Filter(ABC): _filter_name: str - def dump(self, camel_case: bool = True) -> dict[str, Any]: - return {self._filter_name: self._filter_body(camel_case)} + def dump(self, camel_case_property: bool = False) -> dict[str, Any]: + """ + Dump the filter to a dictionary. + + Args: + camel_case_property (bool): Whether to camel case the property names. Defaults to False. Typically, + when the filter is used in data modeling, the property names should not be changed, + while when used with Assets, Events, Sequences, or Files, the property names should be camel cased. + + Returns: + dict[str, Any]: The filter as a dictionary. + + """ + return {self._filter_name: self._filter_body(camel_case_property=camel_case_property)} @classmethod def load(cls, filter_: dict[str, Any]) -> Filter: diff --git a/cognite/client/data_classes/relationships.py b/cognite/client/data_classes/relationships.py index eb600f9659..1e8a089559 100644 --- a/cognite/client/data_classes/relationships.py +++ b/cognite/client/data_classes/relationships.py @@ -95,9 +95,9 @@ def _validate_resource_type(self, resource_type: str | None) -> None: def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> Relationship: instance = super()._load(resource, cognite_client) if instance.source is not None: - instance.source = instance._convert_resource(instance.source, instance.source_type) # type: ignore + instance.source = instance._convert_resource(instance.source, instance.source_type, cognite_client) # type: ignore if instance.target is not None: - instance.target = instance._convert_resource(instance.target, instance.target_type) # type: ignore + instance.target = instance._convert_resource(instance.target, instance.target_type, cognite_client) # type: ignore instance.labels = Label._load_list(instance.labels) return instance diff --git a/pyproject.toml b/pyproject.toml index 8e08d88c39..4b5c07238b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "cognite-sdk" -version = "7.0.2" +version = "7.0.3" description = "Cognite Python SDK" readme = "README.md" documentation = "https://cognite-sdk-python.readthedocs-hosted.com" diff --git a/tests/tests_integration/test_api/test_relationships.py b/tests/tests_integration/test_api/test_relationships.py index 2f197d3471..5318cd377b 100644 --- a/tests/tests_integration/test_api/test_relationships.py +++ b/tests/tests_integration/test_api/test_relationships.py @@ -200,6 +200,16 @@ def test_fetch_resources_retrieve(self, cognite_client, relationship_with_resour assert res[0].source == asset assert res[0].target == time_series + def test_retrieve_relationship_with_resource_client_set( + self, cognite_client: CogniteClient, relationship_with_resources + ) -> None: + relationship, ext_id, asset, time_series = relationship_with_resources + + res = cognite_client.relationships.retrieve(ext_id, fetch_resources=True) + + assert res.source._get_cognite_client() is not None + assert res.target._get_cognite_client() is not None + def test_retrieve_unknown_raises_error(self, cognite_client: CogniteClient): with pytest.raises(CogniteNotFoundError) as e: cognite_client.relationships.retrieve_multiple(external_ids=["this does not exist"]) 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 5752bad24a..56a02d51d1 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 @@ -5,6 +5,7 @@ import cognite.client.data_classes.filters as f from cognite.client.data_classes._base import EnumProperty +from cognite.client.data_classes.data_modeling import ViewId from cognite.client.data_classes.filters import Filter from tests.utils import all_subclasses @@ -99,7 +100,7 @@ def load_and_dump_equals_data() -> Iterator[ParameterSet]: @pytest.mark.parametrize("raw_data", list(load_and_dump_equals_data())) def test_load_and_dump_equals(raw_data: dict) -> None: parsed = Filter.load(raw_data) - dumped = parsed.dump(camel_case=False) + dumped = parsed.dump(camel_case_property=False) assert dumped == raw_data @@ -152,10 +153,28 @@ def dump_filter_test_data() -> Iterator[ParameterSet]: } yield pytest.param(complex_filter, expected, id="And nested and Or with has data and overlaps") + snake_cased_property = f.And( + f.Range( + property=ViewId("space", "viewExternalId", "v1").as_property_ref("start_time"), + lte="2023-09-16T15:50:05.439", + ) + ) + expected = { + "and": [ + { + "range": { + "property": ("space", "viewExternalId/v1", "start_time"), + "lte": "2023-09-16T15:50:05.439", + } + } + ] + } + yield pytest.param(snake_cased_property, expected, id="And range filter with snake cased property") + @pytest.mark.parametrize("user_filter, expected", list(dump_filter_test_data())) def test_dump_filter(user_filter: Filter, expected: dict) -> None: - actual = user_filter.dump(camel_case=False) + actual = user_filter.dump() assert actual == expected @@ -169,7 +188,7 @@ def test_unknown_filter_type() -> None: def test_user_given_metadata_keys_are_not_camel_cased(property_cls: type) -> None: # Bug prior to 6.32.4 would dump user given keys in camelCase flt = f.Equals(property_cls.metadata_key("key_foo_Bar_baz"), "value_foo Bar_baz") # type: ignore [attr-defined] - dumped = flt.dump(camel_case=True)["equals"] + dumped = flt.dump(camel_case_property=True)["equals"] # property may contain more (static) values, so we just verify the end: assert dumped["property"][-2:] == ["metadata", "key_foo_Bar_baz"]