From 56cefc063f708239606639fc42366ffec744ae58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 13:50:01 +0200 Subject: [PATCH 1/9] add build filter helper function in DM TS API --- cognite/client/_api/data_modeling/time_series.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cognite/client/_api/data_modeling/time_series.py b/cognite/client/_api/data_modeling/time_series.py index 47da352cc6..ebc93d3658 100644 --- a/cognite/client/_api/data_modeling/time_series.py +++ b/cognite/client/_api/data_modeling/time_series.py @@ -9,7 +9,7 @@ from cognite.client.data_classes.data_modeling.ids import NodeId, ViewId from cognite.client.data_classes.data_modeling.instances import InstanceSort, Node, NodeList from cognite.client.data_classes.data_modeling.views import View -from cognite.client.data_classes.filters import Filter +from cognite.client.data_classes.filters import Equals, Filter from cognite.client.utils._data_modeling import resolve_source, strip_canonical_source from cognite.client.utils.useful_types import SequenceNotStr @@ -21,6 +21,20 @@ COGNITE_TIME_SERIES_VIEW_ID = CogniteTimeSeries.get_source() +def _build_filter(filter: Filter | dict[str, Any] | None, is_state: bool | None = None) -> Filter | None: + if isinstance(filter, dict): + filter = Filter.load(filter) + + if is_state is None: + return filter + + is_state_flt: Filter = Equals(COGNITE_TIME_SERIES_VIEW_ID.as_property_ref("type"), value="state") + if not is_state: + is_state_flt = ~is_state_flt + + return is_state_flt if filter is None else is_state_flt & filter + + class DataModelingTimeSeriesAPI(APIClient): def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: AsyncCogniteClient) -> None: # TODO: Add DataModelingDatapointsAPI From 25321cc95608bfb468105809837c95924ade21dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 13:50:32 +0200 Subject: [PATCH 2/9] add 'is_state' param to DM TS API /list --- cognite/client/_api/data_modeling/time_series.py | 7 +++++++ cognite/client/_sync_api/data_modeling/time_series.py | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cognite/client/_api/data_modeling/time_series.py b/cognite/client/_api/data_modeling/time_series.py index ebc93d3658..a93bb77803 100644 --- a/cognite/client/_api/data_modeling/time_series.py +++ b/cognite/client/_api/data_modeling/time_series.py @@ -121,6 +121,7 @@ async def list( space: str | SequenceNotStr[str] | None = None, sort: Sequence[InstanceSort | dict] | InstanceSort | dict | None = None, filter: Filter | dict[str, Any] | None = None, + is_state: bool | None = None, limit: int | None = DEFAULT_LIMIT_READ, ) -> NodeList[Node]: """`List time series nodes `_. @@ -132,6 +133,7 @@ async def list( space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces). sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results. filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`. + is_state (bool | None): If True, only return state time series. If False, only return non-state (numeric and string) time series. Default: None (all types). Combined with ``filter`` (if given) using AND. limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items. Returns: @@ -149,6 +151,10 @@ async def list( >>> res = client.data_modeling.time_series.list(space="my-space", limit=None) + List only state time series: + + >>> res = client.data_modeling.time_series.list(is_state=True) + Fetch properties from a custom view (note, only time series will be returned), and apply a custom filter on the name: @@ -161,6 +167,7 @@ async def list( ... limit=None, ... ) """ + filter = _build_filter(filter, is_state=is_state) sources, strip = resolve_source(source, COGNITE_TIME_SERIES_VIEW_ID) results = await self._instances_api.list( instance_type="node", diff --git a/cognite/client/_sync_api/data_modeling/time_series.py b/cognite/client/_sync_api/data_modeling/time_series.py index b9ae21683e..5ae247429a 100644 --- a/cognite/client/_sync_api/data_modeling/time_series.py +++ b/cognite/client/_sync_api/data_modeling/time_series.py @@ -98,6 +98,7 @@ def list( space: str | SequenceNotStr[str] | None = None, sort: Sequence[InstanceSort | dict] | InstanceSort | dict | None = None, filter: Filter | dict[str, Any] | None = None, + is_state: bool | None = None, limit: int | None = DEFAULT_LIMIT_READ, ) -> NodeList[Node]: """ @@ -110,6 +111,7 @@ def list( space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces). sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results. filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`. + is_state (bool | None): If True, only return state time series. If False, only return non-state (numeric and string) time series. Default: None (all types). Combined with ``filter`` (if given) using AND. limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items. Returns: @@ -127,6 +129,10 @@ def list( >>> res = client.data_modeling.time_series.list(space="my-space", limit=None) + List only state time series: + + >>> res = client.data_modeling.time_series.list(is_state=True) + Fetch properties from a custom view (note, only time series will be returned), and apply a custom filter on the name: @@ -141,6 +147,6 @@ def list( """ return run_sync( self.__async_client.data_modeling.time_series.list( - source=source, space=space, sort=sort, filter=filter, limit=limit + source=source, space=space, sort=sort, filter=filter, is_state=is_state, limit=limit ) ) From ad71d92811d0a1f7b8d52952ecb0d5f03836b526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 13:52:15 +0200 Subject: [PATCH 3/9] add note to legacy TS API about "no state ts here" --- cognite/client/_api/time_series.py | 10 +++++++++- cognite/client/_sync_api/time_series.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cognite/client/_api/time_series.py b/cognite/client/_api/time_series.py index ac38001c6f..4b9bfe88bc 100644 --- a/cognite/client/_api/time_series.py +++ b/cognite/client/_api/time_series.py @@ -151,6 +151,10 @@ async def __call__( Yields: TimeSeries | TimeSeriesList: yields TimeSeries one by one if chunk_size is not specified, else TimeSeriesList objects. + + Note: + State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. """ # noqa: DOC404 asset_subtree_ids_processed = process_asset_subtree_ids(asset_subtree_ids, asset_subtree_external_ids) data_set_ids_processed = process_data_set_ids(data_set_ids, data_set_external_ids) @@ -784,12 +788,16 @@ async def list( Returns: TimeSeriesList: The requested time series. - .. note:: + Note: When using `partitions`, there are few considerations to keep in mind: * `limit` has to be set to `None` (or `-1`). * API may reject requests if you specify more than 10 partitions. When Cognite enforces this behavior, the requests result in a 400 Bad Request status. * Partitions are done independently of sorting: there's no guarantee of the sort order between elements from different partitions. For this reason providing a `sort` parameter when using `partitions` is not allowed. + Note: + State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + Examples: List time series: diff --git a/cognite/client/_sync_api/time_series.py b/cognite/client/_sync_api/time_series.py index e62cd85b87..0e64e52c5e 100644 --- a/cognite/client/_sync_api/time_series.py +++ b/cognite/client/_sync_api/time_series.py @@ -137,6 +137,10 @@ def __call__( Yields: TimeSeries | TimeSeriesList: yields TimeSeries one by one if chunk_size is not specified, else TimeSeriesList objects. + + Note: + State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. """ # noqa: DOC404 yield from SyncIterator( self.__async_client.time_series( @@ -723,12 +727,16 @@ def list( Returns: TimeSeriesList: The requested time series. - .. note:: + Note: When using `partitions`, there are few considerations to keep in mind: * `limit` has to be set to `None` (or `-1`). * API may reject requests if you specify more than 10 partitions. When Cognite enforces this behavior, the requests result in a 400 Bad Request status. * Partitions are done independently of sorting: there's no guarantee of the sort order between elements from different partitions. For this reason providing a `sort` parameter when using `partitions` is not allowed. + Note: + State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + Examples: List time series: From 476c17ccb46ad65dff5f227fa95032ec0ff857c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 14:10:49 +0200 Subject: [PATCH 4/9] add test for data_modeling.time_series.list with is_state T/F --- .../test_api/test_datapoints.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/tests_integration/test_api/test_datapoints.py b/tests/tests_integration/test_api/test_datapoints.py index 0233d4539a..ae495730d0 100644 --- a/tests/tests_integration/test_api/test_datapoints.py +++ b/tests/tests_integration/test_api/test_datapoints.py @@ -45,6 +45,7 @@ TimeSeries, TimeSeriesList, TimeSeriesWrite, + filters, ) from cognite.client.data_classes.data_modeling import NodeApply, NodeOrEdgeData, Space from cognite.client.data_classes.data_modeling.cdm.v1 import ( @@ -494,6 +495,12 @@ def space_for_time_series(cognite_client: CogniteClient) -> Iterator[Space]: yield cognite_client.data_modeling.spaces.apply(space) +def _dms_ts_listing_name(postfix: str) -> str: + # Shared by a numeric and a state time series, so that we can list both from the classic time series API by name + # (time series created in DM have no classic external ID): + return f"dms-ts-type-listing-long-name-unlikely-to-collide-{postfix}" + + @pytest.fixture(scope="session") def ts_create_in_dms( cognite_client: CogniteClient, space_for_time_series: Space, os_and_py_version: str @@ -504,6 +511,7 @@ def ts_create_in_dms( external_id=f"dms-time-series-{os_and_py_version}", is_step=True, time_series_type="numeric", + name=_dms_ts_listing_name(os_and_py_version), ) (dms_ts_node,) = cognite_client.data_modeling.instances.apply(dms_ts).nodes return dms_ts_node @@ -567,6 +575,7 @@ def _create_state_time_series( async_client: AsyncCogniteClient, space_for_time_series: Space, state_set: NodeApplyResult, + name: str | None = None, ) -> NodeApplyResult: state_ts = CogniteTimeSeriesApply( space=space_for_time_series.space, @@ -574,6 +583,7 @@ def _create_state_time_series( is_step=False, time_series_type="state", state_set=(state_set.space, state_set.external_id), + name=name, ) with pytest.MonkeyPatch.context() as mp: mp.setattr(async_client.data_modeling.instances, "_api_subversion", "beta") @@ -615,6 +625,7 @@ def state_ts( async_client=async_client, space_for_time_series=space_for_time_series, state_set=state_set, + name=_dms_ts_listing_name(os_and_py_version), ) @@ -635,6 +646,35 @@ def state_ts_b( ) +class TestListStateTimeSeries: + def test_state_time_series_only_listed_from_data_modeling( + self, + cognite_client: CogniteClient, + os_and_py_version: str, + ts_create_in_dms: NodeApplyResult, + state_ts: NodeApplyResult, + ) -> None: + # The classic time series API never returns state time series (unless 'includeAllTypes=true' is passed, which + # the SDK doesn't support - and will never tbh as it mixes legacy and DM-only features), -even- when filtering on type: + numeric_id, state_id = ts_create_in_dms.as_id(), state_ts.as_id() + classic = cognite_client.time_series.list(name=_dms_ts_listing_name(os_and_py_version), limit=None) + classic_instance_ids = [ts.instance_id for ts in classic] + + assert numeric_id in classic_instance_ids # positive control: the name filter works for DM time series + assert state_id not in classic_instance_ids + + # ...but they can of course be listed from the data modeling time series API: + ours = filters.InstanceReferences([numeric_id, state_id]) + res = cognite_client.data_modeling.time_series.list(is_state=True, filter=ours, limit=None) + assert res.as_ids() == [state_id] + + res = cognite_client.data_modeling.time_series.list(is_state=False, filter=ours, limit=None) + assert res.as_ids() == [numeric_id] + + res = cognite_client.data_modeling.time_series.list(filter=ours, limit=None) + assert set(res.as_ids()) == {numeric_id, state_id} + + @pytest.mark.allow_no_semaphore( "StateDatapointsPoster._insert_datapoints holds the semaphore via outer " "'async with' and calls the http client directly with semaphore=None to avoid double-acquiring." From 8940df9bc4701e09cb25d1c3c53c60d24d8d02bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 14:16:01 +0200 Subject: [PATCH 5/9] add unit tests for _build_filter and the is_state filter in data_modeling.time_series.list --- .../test_data_modeling/test_time_series.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/tests_unit/test_api/test_data_modeling/test_time_series.py diff --git a/tests/tests_unit/test_api/test_data_modeling/test_time_series.py b/tests/tests_unit/test_api/test_data_modeling/test_time_series.py new file mode 100644 index 0000000000..c7d383a09f --- /dev/null +++ b/tests/tests_unit/test_api/test_data_modeling/test_time_series.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from cognite.client import CogniteClient +from cognite.client._api.data_modeling.time_series import _build_filter +from cognite.client._cognite_client import AsyncCogniteClient +from cognite.client.data_classes.data_modeling import NodeList +from cognite.client.data_classes.filters import Filter + +TYPE_IS_STATE = {"equals": {"property": ["cdf_cdm", "CogniteTimeSeries/v1", "type"], "value": "state"}} +SPACE_FILTER = {"equals": {"property": ["node", "space"], "value": "sp"}} + + +def as_sent(flt: Filter | None) -> dict[str, Any] | None: + # Properties are loaded as tuples of strings, but end up as lists after json has serialized. + # Thus we have this small helper to convert it to the expected format. + if flt is None: + return None + else: + return json.loads(json.dumps(flt.dump(camel_case_property=False))) + + +class TestBuildFilter: + @pytest.mark.parametrize("filter_as_dict", [True, False]) + @pytest.mark.parametrize( + "filter, is_state, expected", + [ + (None, None, None), + (None, True, TYPE_IS_STATE), + (None, False, {"not": TYPE_IS_STATE}), + (SPACE_FILTER, None, SPACE_FILTER), + (SPACE_FILTER, True, {"and": [TYPE_IS_STATE, SPACE_FILTER]}), + (SPACE_FILTER, False, {"and": [{"not": TYPE_IS_STATE}, SPACE_FILTER]}), + ], + ) + def test_build_filter( + self, + filter: dict[str, Any] | None, + is_state: bool | None, + expected: dict[str, Any] | None, + filter_as_dict: bool, + ) -> None: + given: Filter | dict | None = filter + if not (filter is None or filter_as_dict): + given = Filter.load(filter) + + assert expected == as_sent(_build_filter(given, is_state=is_state)) + + +class TestDMTimeSeriesListIsState: + @pytest.fixture + def list_mock(self, async_client: AsyncCogniteClient, monkeypatch: pytest.MonkeyPatch) -> AsyncMock: + mock = AsyncMock(return_value=NodeList([])) + monkeypatch.setattr(async_client.data_modeling.instances, "list", mock) + return mock + + @staticmethod + def sent_filter(list_mock: AsyncMock) -> dict[str, Any] | None: + flt = list_mock.call_args.kwargs["filter"] + if flt is None or isinstance(flt, dict): + return flt + return json.loads(json.dumps(flt.dump(camel_case_property=False))) + + @pytest.mark.parametrize( + "kwargs, expected", + [ + ({}, None), + ({"is_state": True}, TYPE_IS_STATE), + ({"is_state": False}, {"not": TYPE_IS_STATE}), + ({"filter": SPACE_FILTER}, SPACE_FILTER), + ({"is_state": True, "filter": SPACE_FILTER}, {"and": [TYPE_IS_STATE, SPACE_FILTER]}), + ({"is_state": False, "filter": SPACE_FILTER}, {"and": [{"not": TYPE_IS_STATE}, SPACE_FILTER]}), + ], + ) + def test_is_state_is_combined_with_filter( + self, cognite_client: CogniteClient, list_mock: AsyncMock, kwargs: dict[str, Any], expected: dict | None + ) -> None: + cognite_client.data_modeling.time_series.list(**kwargs) + assert self.sent_filter(list_mock) == expected From cf036c92ab0eee6383e6a16ec548fb1aa02c7887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 17:14:20 +0200 Subject: [PATCH 6/9] add TimeSeriesType literal alias --- cognite/client/data_classes/time_series.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cognite/client/data_classes/time_series.py b/cognite/client/data_classes/time_series.py index fcab391f3d..7b919fc625 100644 --- a/cognite/client/data_classes/time_series.py +++ b/cognite/client/data_classes/time_series.py @@ -38,6 +38,8 @@ if TYPE_CHECKING: from cognite.client.data_classes import Asset, Datapoint +TimeSeriesType: TypeAlias = Literal["numeric", "string", "state"] + class TimeSeries(WriteableCogniteResourceWithClientRef["TimeSeriesWrite"]): """This represents a sequence of data points. The TimeSeries object is the metadata about From 1cd5e835da4a93d45c4de76e3718a3aa754306c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 17:14:25 +0200 Subject: [PATCH 7/9] replace is_state with time_series_type A single type (or a sequence of one) filters with Equals, several types with In, and an empty sequence raises. --- .../client/_api/data_modeling/time_series.py | 30 +++++++++++-------- cognite/client/_api/time_series.py | 4 +-- .../_sync_api/data_modeling/time_series.py | 12 ++++---- cognite/client/_sync_api/time_series.py | 4 +-- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/cognite/client/_api/data_modeling/time_series.py b/cognite/client/_api/data_modeling/time_series.py index a93bb77803..33346dfe1d 100644 --- a/cognite/client/_api/data_modeling/time_series.py +++ b/cognite/client/_api/data_modeling/time_series.py @@ -9,7 +9,8 @@ from cognite.client.data_classes.data_modeling.ids import NodeId, ViewId from cognite.client.data_classes.data_modeling.instances import InstanceSort, Node, NodeList from cognite.client.data_classes.data_modeling.views import View -from cognite.client.data_classes.filters import Equals, Filter +from cognite.client.data_classes.filters import Equals, Filter, In +from cognite.client.data_classes.time_series import TimeSeriesType from cognite.client.utils._data_modeling import resolve_source, strip_canonical_source from cognite.client.utils.useful_types import SequenceNotStr @@ -21,18 +22,22 @@ COGNITE_TIME_SERIES_VIEW_ID = CogniteTimeSeries.get_source() -def _build_filter(filter: Filter | dict[str, Any] | None, is_state: bool | None = None) -> Filter | None: +def _build_filter( + filter: Filter | dict[str, Any] | None, time_series_type: TimeSeriesType | Sequence[TimeSeriesType] | None = None +) -> Filter | None: if isinstance(filter, dict): filter = Filter.load(filter) - if is_state is None: + if time_series_type is None: return filter - is_state_flt: Filter = Equals(COGNITE_TIME_SERIES_VIEW_ID.as_property_ref("type"), value="state") - if not is_state: - is_state_flt = ~is_state_flt + type_prop = COGNITE_TIME_SERIES_VIEW_ID.as_property_ref("type") + types = [time_series_type] if isinstance(time_series_type, str) else list(time_series_type) + if not types: + raise ValueError("'time_series_type' must not be empty, pass None to list all kinds of time series") - return is_state_flt if filter is None else is_state_flt & filter + type_flt = Equals(type_prop, value=types[0]) if len(types) == 1 else In(type_prop, values=types) + return type_flt if filter is None else type_flt & filter class DataModelingTimeSeriesAPI(APIClient): @@ -121,7 +126,7 @@ async def list( space: str | SequenceNotStr[str] | None = None, sort: Sequence[InstanceSort | dict] | InstanceSort | dict | None = None, filter: Filter | dict[str, Any] | None = None, - is_state: bool | None = None, + time_series_type: TimeSeriesType | Sequence[TimeSeriesType] | None = None, limit: int | None = DEFAULT_LIMIT_READ, ) -> NodeList[Node]: """`List time series nodes `_. @@ -133,7 +138,7 @@ async def list( space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces). sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results. filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`. - is_state (bool | None): If True, only return state time series. If False, only return non-state (numeric and string) time series. Default: None (all types). Combined with ``filter`` (if given) using AND. + time_series_type (TimeSeriesType | Sequence[TimeSeriesType] | None): Only return time series of this type (or types). The types are ``"numeric"``, ``"string"`` and ``"state"``. Default: None (all). limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items. Returns: @@ -151,9 +156,10 @@ async def list( >>> res = client.data_modeling.time_series.list(space="my-space", limit=None) - List only state time series: + List only state time series, or e.g. only numeric and string time series: - >>> res = client.data_modeling.time_series.list(is_state=True) + >>> res = client.data_modeling.time_series.list(time_series_type="state") + >>> res = client.data_modeling.time_series.list(time_series_type=["numeric", "string"]) Fetch properties from a custom view (note, only time series will be returned), and apply a custom filter on the name: @@ -167,7 +173,7 @@ async def list( ... limit=None, ... ) """ - filter = _build_filter(filter, is_state=is_state) + filter = _build_filter(filter, time_series_type=time_series_type) sources, strip = resolve_source(source, COGNITE_TIME_SERIES_VIEW_ID) results = await self._instances_api.list( instance_type="node", diff --git a/cognite/client/_api/time_series.py b/cognite/client/_api/time_series.py index 4b9bfe88bc..9c7c8c0eb5 100644 --- a/cognite/client/_api/time_series.py +++ b/cognite/client/_api/time_series.py @@ -154,7 +154,7 @@ async def __call__( Note: State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). - You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``time_series_type="state"``. """ # noqa: DOC404 asset_subtree_ids_processed = process_asset_subtree_ids(asset_subtree_ids, asset_subtree_external_ids) data_set_ids_processed = process_data_set_ids(data_set_ids, data_set_external_ids) @@ -796,7 +796,7 @@ async def list( Note: State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). - You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``time_series_type="state"``. Examples: diff --git a/cognite/client/_sync_api/data_modeling/time_series.py b/cognite/client/_sync_api/data_modeling/time_series.py index 5ae247429a..db4986c304 100644 --- a/cognite/client/_sync_api/data_modeling/time_series.py +++ b/cognite/client/_sync_api/data_modeling/time_series.py @@ -17,6 +17,7 @@ from cognite.client.data_classes.data_modeling.instances import InstanceSort, Node, NodeList from cognite.client.data_classes.data_modeling.views import View from cognite.client.data_classes.filters import Filter +from cognite.client.data_classes.time_series import TimeSeriesType from cognite.client.utils._async_helpers import run_sync from cognite.client.utils.useful_types import SequenceNotStr @@ -98,7 +99,7 @@ def list( space: str | SequenceNotStr[str] | None = None, sort: Sequence[InstanceSort | dict] | InstanceSort | dict | None = None, filter: Filter | dict[str, Any] | None = None, - is_state: bool | None = None, + time_series_type: TimeSeriesType | Sequence[TimeSeriesType] | None = None, limit: int | None = DEFAULT_LIMIT_READ, ) -> NodeList[Node]: """ @@ -111,7 +112,7 @@ def list( space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces). sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results. filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`. - is_state (bool | None): If True, only return state time series. If False, only return non-state (numeric and string) time series. Default: None (all types). Combined with ``filter`` (if given) using AND. + time_series_type (TimeSeriesType | Sequence[TimeSeriesType] | None): Only return time series of this type, or any of these types, e.g. ``"state"`` or ``["numeric", "string"]``. The types are ``"numeric"``, ``"string"`` and ``"state"``. Default: None (all types). Combined with ``filter`` (if given) using AND. limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items. Returns: @@ -129,9 +130,10 @@ def list( >>> res = client.data_modeling.time_series.list(space="my-space", limit=None) - List only state time series: + List only state time series, or e.g. only numeric and string time series: - >>> res = client.data_modeling.time_series.list(is_state=True) + >>> res = client.data_modeling.time_series.list(time_series_type="state") + >>> res = client.data_modeling.time_series.list(time_series_type=["numeric", "string"]) Fetch properties from a custom view (note, only time series will be returned), and apply a custom filter on the name: @@ -147,6 +149,6 @@ def list( """ return run_sync( self.__async_client.data_modeling.time_series.list( - source=source, space=space, sort=sort, filter=filter, is_state=is_state, limit=limit + source=source, space=space, sort=sort, filter=filter, time_series_type=time_series_type, limit=limit ) ) diff --git a/cognite/client/_sync_api/time_series.py b/cognite/client/_sync_api/time_series.py index 0e64e52c5e..9f4c5bffb9 100644 --- a/cognite/client/_sync_api/time_series.py +++ b/cognite/client/_sync_api/time_series.py @@ -140,7 +140,7 @@ def __call__( Note: State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). - You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``time_series_type="state"``. """ # noqa: DOC404 yield from SyncIterator( self.__async_client.time_series( @@ -735,7 +735,7 @@ def list( Note: State time series are never returned by this method as they are a Data Modeling-only feature (the API leaves them out by default). - You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``is_state=True``. + You can list these like any other Data Modeling instance, or through the dedicated helper :meth:`client.data_modeling.time_series.list ` with ``time_series_type="state"``. Examples: From 8062bdcb4eeeaa865f4a7b185a1c33eb596afb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 17:14:25 +0200 Subject: [PATCH 8/9] update tests for time_series_type in data_modeling.time_series.list --- .../test_api/test_datapoints.py | 11 +++++- .../test_data_modeling/test_time_series.py | 39 ++++++++++++------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/tests_integration/test_api/test_datapoints.py b/tests/tests_integration/test_api/test_datapoints.py index ae495730d0..e0e19ddcbe 100644 --- a/tests/tests_integration/test_api/test_datapoints.py +++ b/tests/tests_integration/test_api/test_datapoints.py @@ -665,12 +665,19 @@ def test_state_time_series_only_listed_from_data_modeling( # ...but they can of course be listed from the data modeling time series API: ours = filters.InstanceReferences([numeric_id, state_id]) - res = cognite_client.data_modeling.time_series.list(is_state=True, filter=ours, limit=None) + res = cognite_client.data_modeling.time_series.list(time_series_type="state", filter=ours, limit=None) assert res.as_ids() == [state_id] - res = cognite_client.data_modeling.time_series.list(is_state=False, filter=ours, limit=None) + res = cognite_client.data_modeling.time_series.list( + time_series_type=["numeric", "string"], filter=ours, limit=None + ) assert res.as_ids() == [numeric_id] + res = cognite_client.data_modeling.time_series.list( + time_series_type=["numeric", "state"], filter=ours, limit=None + ) + assert set(res.as_ids()) == {numeric_id, state_id} + res = cognite_client.data_modeling.time_series.list(filter=ours, limit=None) assert set(res.as_ids()) == {numeric_id, state_id} diff --git a/tests/tests_unit/test_api/test_data_modeling/test_time_series.py b/tests/tests_unit/test_api/test_data_modeling/test_time_series.py index c7d383a09f..4c9d0341ab 100644 --- a/tests/tests_unit/test_api/test_data_modeling/test_time_series.py +++ b/tests/tests_unit/test_api/test_data_modeling/test_time_series.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from collections.abc import Sequence from typing import Any from unittest.mock import AsyncMock @@ -11,8 +12,11 @@ from cognite.client._cognite_client import AsyncCogniteClient from cognite.client.data_classes.data_modeling import NodeList from cognite.client.data_classes.filters import Filter +from cognite.client.data_classes.time_series import TimeSeriesType -TYPE_IS_STATE = {"equals": {"property": ["cdf_cdm", "CogniteTimeSeries/v1", "type"], "value": "state"}} +TYPE_PROPERTY = ["cdf_cdm", "CogniteTimeSeries/v1", "type"] +TYPE_IS_STATE = {"equals": {"property": TYPE_PROPERTY, "value": "state"}} +TYPE_IN_NUMERIC_STRING = {"in": {"property": TYPE_PROPERTY, "values": ["numeric", "string"]}} SPACE_FILTER = {"equals": {"property": ["node", "space"], "value": "sp"}} @@ -28,20 +32,23 @@ def as_sent(flt: Filter | None) -> dict[str, Any] | None: class TestBuildFilter: @pytest.mark.parametrize("filter_as_dict", [True, False]) @pytest.mark.parametrize( - "filter, is_state, expected", + "filter, time_series_type, expected", [ (None, None, None), - (None, True, TYPE_IS_STATE), - (None, False, {"not": TYPE_IS_STATE}), + (None, "state", TYPE_IS_STATE), # single -> equals + (None, ["state"], TYPE_IS_STATE), # ...also in a sequence + (None, ("state",), TYPE_IS_STATE), + (None, ["numeric", "string"], TYPE_IN_NUMERIC_STRING), # multiple -> in + (None, ("numeric", "string"), TYPE_IN_NUMERIC_STRING), # any sequence works (SPACE_FILTER, None, SPACE_FILTER), - (SPACE_FILTER, True, {"and": [TYPE_IS_STATE, SPACE_FILTER]}), - (SPACE_FILTER, False, {"and": [{"not": TYPE_IS_STATE}, SPACE_FILTER]}), + (SPACE_FILTER, "state", {"and": [TYPE_IS_STATE, SPACE_FILTER]}), + (SPACE_FILTER, ["numeric", "string"], {"and": [TYPE_IN_NUMERIC_STRING, SPACE_FILTER]}), ], ) def test_build_filter( self, filter: dict[str, Any] | None, - is_state: bool | None, + time_series_type: TimeSeriesType | Sequence[TimeSeriesType] | None, expected: dict[str, Any] | None, filter_as_dict: bool, ) -> None: @@ -49,10 +56,15 @@ def test_build_filter( if not (filter is None or filter_as_dict): given = Filter.load(filter) - assert expected == as_sent(_build_filter(given, is_state=is_state)) + assert expected == as_sent(_build_filter(given, time_series_type=time_series_type)) + @pytest.mark.parametrize("time_series_type", [[], ()]) + def test_build_filter_raises_on_empty_time_series_types(self, time_series_type: Sequence[TimeSeriesType]) -> None: + with pytest.raises(ValueError, match="'time_series_type' must not be empty, pass None"): + _build_filter(None, time_series_type=time_series_type) -class TestDMTimeSeriesListIsState: + +class TestDMTimeSeriesListTimeSeriesTypes: @pytest.fixture def list_mock(self, async_client: AsyncCogniteClient, monkeypatch: pytest.MonkeyPatch) -> AsyncMock: mock = AsyncMock(return_value=NodeList([])) @@ -70,14 +82,13 @@ def sent_filter(list_mock: AsyncMock) -> dict[str, Any] | None: "kwargs, expected", [ ({}, None), - ({"is_state": True}, TYPE_IS_STATE), - ({"is_state": False}, {"not": TYPE_IS_STATE}), + ({"time_series_type": "state"}, TYPE_IS_STATE), + ({"time_series_type": ["numeric", "string"]}, TYPE_IN_NUMERIC_STRING), ({"filter": SPACE_FILTER}, SPACE_FILTER), - ({"is_state": True, "filter": SPACE_FILTER}, {"and": [TYPE_IS_STATE, SPACE_FILTER]}), - ({"is_state": False, "filter": SPACE_FILTER}, {"and": [{"not": TYPE_IS_STATE}, SPACE_FILTER]}), + ({"time_series_type": "state", "filter": SPACE_FILTER}, {"and": [TYPE_IS_STATE, SPACE_FILTER]}), ], ) - def test_is_state_is_combined_with_filter( + def test_time_series_types_is_combined_with_filter( self, cognite_client: CogniteClient, list_mock: AsyncMock, kwargs: dict[str, Any], expected: dict | None ) -> None: cognite_client.data_modeling.time_series.list(**kwargs) From 8ef17bcf99fba1eec85d8f2eebededf39338ba4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20V=2E=20Treider?= Date: Fri, 2 Oct 2026 17:28:23 +0200 Subject: [PATCH 9/9] rerun sync codegen after rebase --- cognite/client/_sync_api/data_modeling/time_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognite/client/_sync_api/data_modeling/time_series.py b/cognite/client/_sync_api/data_modeling/time_series.py index db4986c304..5e94b7f714 100644 --- a/cognite/client/_sync_api/data_modeling/time_series.py +++ b/cognite/client/_sync_api/data_modeling/time_series.py @@ -112,7 +112,7 @@ def list( space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces). sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results. filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`. - time_series_type (TimeSeriesType | Sequence[TimeSeriesType] | None): Only return time series of this type, or any of these types, e.g. ``"state"`` or ``["numeric", "string"]``. The types are ``"numeric"``, ``"string"`` and ``"state"``. Default: None (all types). Combined with ``filter`` (if given) using AND. + time_series_type (TimeSeriesType | Sequence[TimeSeriesType] | None): Only return time series of this type (or types). The types are ``"numeric"``, ``"string"`` and ``"state"``. Default: None (all). limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items. Returns: