Skip to content

Commit afd10ec

Browse files
authored
Fix datapoints retrieve_latest with missing instance_id + ignore=True (#2120)
1 parent 4a3c0d1 commit afd10ec

File tree

10 files changed

+73
-28
lines changed

10 files changed

+73
-28
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ Changes are grouped as follows
2222
- Support for the `/simulators/models` and `/simulators/models/revisions` API endpoints.
2323
- Support for the `/simulators` and `/simulators/integration` API endpoints.
2424

25+
## [7.73.4] - 2025-02-24
26+
### Fixed
27+
- An issue with `DatapointsAPI.retrieve_latest` and usage of `instance_id` when using `ignore_unknown_ids=True`
28+
and *any type* of identifier was not found (no longer raises `TypeError`).
29+
2530
## [7.73.3] - 2025-02-07
2631
### Added
2732
- Listable property types for containers in Data Modeling now accept `max_list_size`.

cognite/client/_api/datapoints.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,14 +2107,18 @@ def _post_fix_status_codes_and_stringified_floats(self, result: list[dict[str, A
21072107
ids_exists = (
21082108
{("id", r["id"]) for r in result}
21092109
.union({("xid", r.get("externalId")) for r in result})
2110-
.union({("inst_id", r.get("instanceId")) for r in result})
2110+
.union({("inst_id", NodeId.load_if(r.get("instanceId"))) for r in result})
21112111
.difference({("xid", None), ("inst_id", None)})
21122112
) # fmt: skip
21132113
self._all_identifiers = [
21142114
query
21152115
for query in self._all_identifiers
21162116
if ids_exists.intersection(
2117-
(("id", query.get("id")), ("xid", query.get("externalId")), ("inst_id", query.get("instanceId")))
2117+
(
2118+
("id", query.get("id")),
2119+
("xid", query.get("externalId")),
2120+
("inst_id", NodeId.load_if(query.get("instanceId"))),
2121+
)
21182122
)
21192123
]
21202124
for query, res in zip(self._all_identifiers, result):

cognite/client/_api/simulators/integrations.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,22 @@ def __call__(
7676

7777
def list(
7878
self,
79-
limit: int = DEFAULT_LIMIT_READ,
79+
limit: int | None = DEFAULT_LIMIT_READ,
8080
filter: SimulatorIntegrationFilter | None = None,
8181
) -> SimulatorIntegrationList:
8282
"""`Filter simulator integrations <https://developer.cognite.com/api#tag/Simulator-Integrations/operation/filter_simulator_integrations_simulators_integrations_list_post>`_
8383
Retrieves a list of simulator integrations that match the given criteria
84+
8485
Args:
85-
limit (int): The maximum number of simulator integrations to return.
86+
limit (int | None): The maximum number of simulator integrations to return, pass None to return all.
8687
filter (SimulatorIntegrationFilter | None): Filter to apply.
88+
8789
Returns:
8890
SimulatorIntegrationList: List of simulator integrations
91+
8992
Examples:
90-
List simulator integrations:
93+
94+
List a few simulator integrations:
9195
>>> from cognite.client import CogniteClient
9296
>>> client = CogniteClient()
9397
>>> res = client.simulators.integrations.list()
@@ -97,7 +101,6 @@ def list(
97101
>>> res = client.simulators.integrations.list(
98102
... filter=SimulatorIntegrationFilter(active=True))
99103
"""
100-
101104
self._warning.warn()
102105
return self._list(
103106
method="POST",

cognite/client/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
22

3-
__version__ = "7.73.3"
3+
__version__ = "7.73.4"
44

55
__api_subversion__ = "20230101"

cognite/client/data_classes/simulators/filters.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
from collections.abc import Sequence
44

55
from cognite.client.data_classes._base import CogniteFilter
6-
from cognite.client.utils.useful_types import SequenceNotStr
76

87

98
class SimulatorIntegrationFilter(CogniteFilter):
109
def __init__(
1110
self,
12-
simulator_external_ids: SequenceNotStr[str] | None = None,
11+
simulator_external_ids: str | Sequence[str] | None = None,
1312
active: bool | None = None,
1413
) -> None:
15-
self.simulator_external_ids = simulator_external_ids
14+
self.simulator_external_ids = (
15+
[simulator_external_ids] if isinstance(simulator_external_ids, str) else simulator_external_ids
16+
)
1617
self.active = active
1718

1819

1920
class SimulatorModelsFilter(CogniteFilter):
2021
def __init__(
2122
self,
22-
simulator_external_ids: Sequence[str] | None = None,
23+
simulator_external_ids: str | Sequence[str] | None = None,
2324
) -> None:
2425
self.simulator_external_ids = (
2526
[simulator_external_ids] if isinstance(simulator_external_ids, str) else simulator_external_ids
@@ -29,7 +30,7 @@ def __init__(
2930
class SimulatorModelRevisionsFilter(CogniteFilter):
3031
def __init__(
3132
self,
32-
model_external_ids: Sequence[str] | None = None,
33+
model_external_ids: str | Sequence[str] | None = None,
3334
all_versions: bool | None = None,
3435
) -> None:
3536
self.model_external_ids = [model_external_ids] if isinstance(model_external_ids, str) else model_external_ids

cognite/client/utils/_identifier.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ class InstanceId:
3232
def dump(self, camel_case: bool = True, include_instance_type: bool = True) -> dict[str, str]:
3333
return {"space": self.space, "externalId" if camel_case else "external_id": self.external_id}
3434

35+
@overload
36+
@classmethod
37+
def load_if(cls, data: None) -> None: ...
38+
39+
@overload
40+
@classmethod
41+
def load_if(cls, data: dict[str, str] | tuple[str, str] | Self) -> Self: ...
42+
43+
@classmethod
44+
def load_if(cls, data: dict[str, str] | tuple[str, str] | Self | None) -> Self | None:
45+
# Note: For experimentation - I'd like to add this as a universal method to all classes to avoid
46+
# the endless spam of 'MyClass.load(foo["bar"]) if "bar" in foo else None' in the codebase!
47+
if data is None:
48+
return None
49+
return cls.load(data)
50+
3551
@classmethod
3652
def load(cls, data: dict[str, str] | tuple[str, str] | Self) -> Self:
3753
if isinstance(data, cls):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "cognite-sdk"
33

4-
version = "7.73.3"
4+
version = "7.73.4"
55

66
description = "Cognite Python SDK"
77
readme = "README.md"

tests/tests_integration/test_api/test_datapoints.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2826,6 +2826,26 @@ def test_instance_id_usage(self, cognite_client, instance_ts_id, instance_ts_lat
28262826
assert isinstance(dp.value, float)
28272827
assert int(dp.timestamp / 1000) == int(dp.value)
28282828

2829+
def test_instance_id_and_missing(self, cognite_client, instance_ts_id):
2830+
# Before 7.73.4 (and after support for instance_id was added ofc), when a not-found time series was requested
2831+
# by instance_id and ignore_unknown_ids=True was passed, a TypeError was raised instead of a CogniteNotFoundError
2832+
# as we tried to put a dict into a set (not hashable)
2833+
missing = NodeId("I-do-not", "exist")
2834+
res = cognite_client.time_series.data.retrieve_latest(instance_id=missing, ignore_unknown_ids=True)
2835+
assert res is None
2836+
# This could also happen - but on a separate codeline - when the missing was not an instance_id, as we loaded the
2837+
# dict in before a comparison of the identifiers:
2838+
res = cognite_client.time_series.data.retrieve_latest(id=1, instance_id=instance_ts_id, ignore_unknown_ids=True)
2839+
assert type(res) is DatapointsList
2840+
assert len(res) == 1
2841+
assert res[0].instance_id == instance_ts_id
2842+
2843+
# ...and just to ensure this still works as expected:
2844+
with pytest.raises(CogniteNotFoundError, match=r"^Not found: \[{'"):
2845+
cognite_client.time_series.data.retrieve_latest(id=1, instance_id=instance_ts_id, ignore_unknown_ids=False)
2846+
with pytest.raises(CogniteNotFoundError, match=r"^Not found: \[{'"):
2847+
cognite_client.time_series.data.retrieve_latest(instance_id=missing, ignore_unknown_ids=False)
2848+
28292849

28302850
class TestInsertDatapointsAPI:
28312851
@pytest.mark.usefixtures("post_spy")

tests/tests_integration/test_api/test_simulators/test_integrations.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,41 @@
88
from tests.tests_integration.test_api.test_simulators.seed.data import simulator_integration
99

1010

11+
@pytest.mark.usefixtures("seed_resource_names", "seed_simulator_integration")
1112
class TestSimulatorIntegrations:
12-
@pytest.mark.usefixtures("seed_resource_names", "seed_simulator_integration")
1313
def test_list_integrations(self, cognite_client: CogniteClient) -> None:
1414
integrations = cognite_client.simulators.integrations.list(limit=5)
1515

1616
assert len(integrations) > 0
1717

18-
def test_filter_integrations(self, cognite_client: CogniteClient, seed_resource_names) -> None:
18+
def test_filter_integrations_asdfdsa(self, cognite_client: CogniteClient, seed_resource_names) -> None:
1919
for integration in cognite_client.simulators.integrations(filter=SimulatorIntegrationFilter(active=True)):
2020
assert integration.active is True
2121

2222
all_integrations = cognite_client.simulators.integrations.list()
2323
active_integrations = cognite_client.simulators.integrations.list(
2424
filter=SimulatorIntegrationFilter(active=True)
2525
)
26-
2726
filtered_integrations = cognite_client.simulators.integrations.list(
2827
filter=SimulatorIntegrationFilter(simulator_external_ids=[seed_resource_names["simulator_external_id"]])
2928
)
30-
3129
assert len(all_integrations) > 0
32-
assert filtered_integrations[0].external_id == seed_resource_names["simulator_integration_external_id"]
33-
# assert filtered_integrations[0].data_set_id == seed_resource_names["simulator_test_data_set_id"]
34-
assert filtered_integrations[0].active is True
35-
assert filtered_integrations[0].created_time is not None
36-
assert filtered_integrations[0].last_updated_time is not None
37-
3830
assert len(active_integrations) > 0
3931
assert len(filtered_integrations) > 0
4032

33+
item = filtered_integrations.get(external_id=seed_resource_names["simulator_integration_external_id"])
34+
assert item is not None
35+
# assert item.data_set_id == seed_resource_names["simulator_test_data_set_id"]
36+
assert item.active is True
37+
assert item.created_time is not None
38+
assert item.last_updated_time is not None
39+
4140
def test_delete_integrations(self, cognite_client: CogniteClient, seed_resource_names) -> None:
4241
simulator_integration["heartbeat"] = int(time.time() * 1000)
4342
simulator_integration["externalId"] = random_string(50)
4443
simulator_integration["dataSetId"] = seed_resource_names["simulator_test_data_set_id"]
4544

46-
cognite_client.simulators._post(
47-
"/simulators/integrations",
48-
json={"items": [simulator_integration]},
49-
)
45+
cognite_client.simulators._post("/simulators/integrations", json={"items": [simulator_integration]})
5046

5147
all_integrations = cognite_client.simulators.integrations.list(limit=None)
5248
assert all_integrations.get(external_id=simulator_integration["externalId"]) is not None

tests/tests_unit/test_api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def test_headers_correct(self, mock_all_requests_ok, api_client_with_token):
163163
api_client_with_token._post(URL_PATH, {"any": "OK"}, headers={"additional": "stuff"})
164164
headers = mock_all_requests_ok.calls[0].request.headers
165165

166-
assert "gzip, deflate" == headers["accept-encoding"]
166+
assert "gzip, deflate, zstd" == headers["accept-encoding"]
167167
assert "gzip" == headers["content-encoding"]
168168
assert f"CognitePythonSDK:{utils._auxiliary.get_current_sdk_version()}" == headers["x-cdp-sdk"]
169169
assert "Bearer abc" == headers["Authorization"]

0 commit comments

Comments
 (0)