Skip to content

Commit d8e8d83

Browse files
authored
feat(Data Modeling, Time Series): Extend Data Modeling with Time Series /retrieve + /list (DM-3842) (#2672)
1 parent 197bd46 commit d8e8d83

15 files changed

Lines changed: 510 additions & 52 deletions

File tree

cognite/client/_api/data_modeling/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from cognite.client._api.data_modeling.spaces import SpacesAPI
1313
from cognite.client._api.data_modeling.statistics import StatisticsAPI
1414
from cognite.client._api.data_modeling.streams import StreamsAPI
15+
from cognite.client._api.data_modeling.time_series import DataModelingTimeSeriesAPI
1516
from cognite.client._api.data_modeling.views import ViewsAPI
1617
from cognite.client._api_client import APIClient
1718

@@ -26,6 +27,7 @@ def __init__(self, config: ClientConfig, api_version: str | None, cognite_client
2627
self.containers = ContainersAPI(config, api_version, cognite_client)
2728
self.data_models = DataModelsAPI(config, api_version, cognite_client)
2829
self.files = DataModelingFilesAPI(config, api_version, cognite_client)
30+
self.time_series = DataModelingTimeSeriesAPI(config, api_version, cognite_client)
2931
self.spaces = SpacesAPI(config, api_version, cognite_client)
3032
self.views = ViewsAPI(config, api_version, cognite_client)
3133
self.instances = InstancesAPI(config, api_version, cognite_client)

cognite/client/_api/data_modeling/files.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from cognite.client.data_classes.files import FileMetadata
1717
from cognite.client.data_classes.filters import Filter
1818
from cognite.client.exceptions import CogniteFileUploadError, CogniteNotFoundError
19+
from cognite.client.utils._data_modeling import resolve_source, strip_canonical_source
1920
from cognite.client.utils._retry import Backoff
2021
from cognite.client.utils.useful_types import SequenceNotStr
2122

@@ -30,26 +31,6 @@
3031
COGNITE_FILE_VIEW_ID = CogniteFile.get_source()
3132

3233

33-
def _resolve_source(source: View | ViewId | tuple[str, str, str]) -> tuple[list[ViewId], bool]:
34-
match source:
35-
case ViewId():
36-
source_as_id = source
37-
case View():
38-
source_as_id = source.as_id()
39-
case [str(), str(), str()]:
40-
source_as_id = ViewId(*source)
41-
case _:
42-
raise TypeError(f"Expected View, ViewId, or a (space, external_id, version) tuple, got {type(source)}")
43-
44-
if source_as_id == COGNITE_FILE_VIEW_ID:
45-
return [source_as_id], False
46-
47-
# User has passed a custom source, we include CogniteFile source to guarantee only file nodes
48-
# are returned. We will later strip them (hence the 'True' flag) to avoid returning nodes with
49-
# properties from multiple sources as they are very annoying to work with in the SDK.
50-
return [source_as_id, COGNITE_FILE_VIEW_ID], True
51-
52-
5334
class DataModelingFilesAPI(APIClient):
5435
def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: AsyncCogniteClient) -> None:
5536
super().__init__(config, api_version, cognite_client)
@@ -422,11 +403,10 @@ async def retrieve(
422403
... source=ViewId("my-space", "MyFileExtension", "v1"),
423404
... )
424405
"""
425-
sources, strip = _resolve_source(source)
406+
sources, strip = resolve_source(source, COGNITE_FILE_VIEW_ID)
426407
result = await self._instances_api.retrieve_nodes(nodes=node_ids, sources=sources)
427-
if strip and result:
428-
for node in [result] if isinstance(result, Node) else result:
429-
node.drop_source(COGNITE_FILE_VIEW_ID)
408+
if strip:
409+
strip_canonical_source(result, COGNITE_FILE_VIEW_ID)
430410
return result
431411

432412
async def list(
@@ -476,7 +456,7 @@ async def list(
476456
... limit=None,
477457
... )
478458
"""
479-
sources, strip = _resolve_source(source)
459+
sources, strip = resolve_source(source, COGNITE_FILE_VIEW_ID)
480460
results = await self._instances_api.list(
481461
instance_type="node",
482462
sources=sources,
@@ -486,6 +466,5 @@ async def list(
486466
limit=limit,
487467
)
488468
if strip:
489-
for node in results:
490-
node.drop_source(COGNITE_FILE_VIEW_ID)
469+
strip_canonical_source(results, COGNITE_FILE_VIEW_ID)
491470
return results
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Sequence
4+
from typing import TYPE_CHECKING, Any, NoReturn, overload
5+
6+
from cognite.client._api_client import APIClient
7+
from cognite.client._constants import DEFAULT_LIMIT_READ
8+
from cognite.client.data_classes.data_modeling.cdm.v1 import CogniteTimeSeries
9+
from cognite.client.data_classes.data_modeling.ids import NodeId, ViewId
10+
from cognite.client.data_classes.data_modeling.instances import InstanceSort, Node, NodeList
11+
from cognite.client.data_classes.data_modeling.views import View
12+
from cognite.client.data_classes.filters import Filter
13+
from cognite.client.utils._data_modeling import resolve_source, strip_canonical_source
14+
from cognite.client.utils.useful_types import SequenceNotStr
15+
16+
if TYPE_CHECKING:
17+
from cognite.client import AsyncCogniteClient
18+
from cognite.client._api.data_modeling.instances import InstancesAPI
19+
from cognite.client.config import ClientConfig
20+
21+
COGNITE_TIME_SERIES_VIEW_ID = CogniteTimeSeries.get_source()
22+
23+
24+
class DataModelingTimeSeriesAPI(APIClient):
25+
def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: AsyncCogniteClient) -> None:
26+
# TODO: Add DataModelingDatapointsAPI
27+
super().__init__(config, api_version, cognite_client)
28+
29+
@property
30+
def _instances_api(self) -> InstancesAPI:
31+
return self._cognite_client.data_modeling.instances
32+
33+
async def __call__(self) -> NoReturn:
34+
raise NotImplementedError("This method is not implemented yet!")
35+
36+
@overload
37+
async def retrieve(
38+
self,
39+
node_ids: NodeId | tuple[str, str],
40+
*,
41+
source: View | ViewId | tuple[str, str, str] = COGNITE_TIME_SERIES_VIEW_ID,
42+
) -> Node | None: ...
43+
44+
@overload
45+
async def retrieve(
46+
self,
47+
node_ids: Sequence[NodeId] | Sequence[tuple[str, str]],
48+
*,
49+
source: View | ViewId | tuple[str, str, str] = COGNITE_TIME_SERIES_VIEW_ID,
50+
) -> NodeList[Node]: ...
51+
52+
async def retrieve(
53+
self,
54+
node_ids: NodeId | tuple[str, str] | Sequence[NodeId] | Sequence[tuple[str, str]],
55+
*,
56+
source: View | ViewId | tuple[str, str, str] = COGNITE_TIME_SERIES_VIEW_ID,
57+
) -> Node | NodeList[Node] | None:
58+
"""`Retrieve one or more time series by instance ID <https://api-docs.cognite.com/20230101/tag/Instances/operation/byExternalIdsInstances>`_.
59+
60+
Only nodes that are time series (i.e. have data in the CogniteTimeSeries view) will be returned.
61+
If a single instance ID is requested and it is not found, ``None`` is returned.
62+
63+
Args:
64+
node_ids (NodeId | tuple[str, str] | Sequence[NodeId] | Sequence[tuple[str, str]]): Single instance ID or a list of instance IDs.
65+
source (View | ViewId | tuple[str, str, str]): The view to fetch properties from. Defaults to CogniteTimeSeries.
66+
67+
Returns:
68+
Node | NodeList[Node] | None: A single ``Node`` (or ``None`` if not found) when given a single identifier, or a ``NodeList`` when given a sequence.
69+
70+
Examples:
71+
72+
Retrieve a single time series by instance ID:
73+
74+
>>> from cognite.client import CogniteClient
75+
>>> from cognite.client.data_classes.data_modeling import NodeId
76+
>>> client = CogniteClient()
77+
>>> res = client.data_modeling.time_series.retrieve(NodeId("my-space", "my-ts"))
78+
79+
Using a tuple shorthand:
80+
81+
>>> res = client.data_modeling.time_series.retrieve(("my-space", "my-ts"))
82+
83+
Retrieve multiple time series nodes:
84+
85+
>>> res = client.data_modeling.time_series.retrieve(
86+
... [("my-space", "ts-1"), ("my-space", "ts-2")]
87+
... )
88+
89+
Fetch properties from a custom view (note, only time series will be returned):
90+
91+
>>> from cognite.client.data_classes.data_modeling import ViewId
92+
>>> res = client.data_modeling.time_series.retrieve(
93+
... NodeId("my-space", "my-ts"),
94+
... source=ViewId("my-space", "MyTimeSeriesExtension", "v1"),
95+
... )
96+
"""
97+
sources, strip = resolve_source(source, COGNITE_TIME_SERIES_VIEW_ID)
98+
result = await self._instances_api.retrieve_nodes(nodes=node_ids, sources=sources)
99+
if strip:
100+
strip_canonical_source(result, COGNITE_TIME_SERIES_VIEW_ID)
101+
return result
102+
103+
async def list(
104+
self,
105+
*,
106+
source: View | ViewId | tuple[str, str, str] = COGNITE_TIME_SERIES_VIEW_ID,
107+
space: str | SequenceNotStr[str] | None = None,
108+
sort: Sequence[InstanceSort | dict] | InstanceSort | dict | None = None,
109+
filter: Filter | dict[str, Any] | None = None,
110+
limit: int | None = DEFAULT_LIMIT_READ,
111+
) -> NodeList[Node]:
112+
"""`List time series nodes <https://api-docs.cognite.com/20230101/tag/Instances/operation/advancedListInstance>`_.
113+
114+
Only time series nodes will be returned, regardless of the source passed.
115+
116+
Args:
117+
source (View | ViewId | tuple[str, str, str]): The view to fetch properties from. Defaults to CogniteTimeSeries.
118+
space (str | SequenceNotStr[str] | None): Restrict results to this space (or list of spaces).
119+
sort (Sequence[InstanceSort | dict] | InstanceSort | dict | None): Sort order for the results.
120+
filter (Filter | dict[str, Any] | None): Advanced filter to apply. See :class:`~cognite.client.data_classes.filters`.
121+
limit (int | None): Maximum number of results to return. Defaults to 25. Set to -1, float("inf") or None to return all items.
122+
123+
Returns:
124+
NodeList[Node]: The matching time series.
125+
126+
Examples:
127+
128+
List a few time series:
129+
130+
>>> from cognite.client import CogniteClient
131+
>>> client = CogniteClient()
132+
>>> res = client.data_modeling.time_series.list(limit=5)
133+
134+
List all time series in a specific space:
135+
136+
>>> res = client.data_modeling.time_series.list(space="my-space", limit=None)
137+
138+
Fetch properties from a custom view (note, only time series will be returned), and
139+
apply a custom filter on the name:
140+
141+
>>> from cognite.client.data_classes.data_modeling import ViewId
142+
>>> from cognite.client.data_classes import filters
143+
>>> view_id = ViewId("my-space", "MyTimeSeriesExtension", "v1")
144+
>>> res = client.data_modeling.time_series.list(
145+
... source=view_id,
146+
... filter=filters.Prefix(view_id.as_property_ref("name"), "sensor"),
147+
... limit=None,
148+
... )
149+
"""
150+
sources, strip = resolve_source(source, COGNITE_TIME_SERIES_VIEW_ID)
151+
results = await self._instances_api.list(
152+
instance_type="node",
153+
sources=sources,
154+
space=space,
155+
sort=sort,
156+
filter=filter,
157+
limit=limit,
158+
)
159+
if strip:
160+
strip_canonical_source(results, COGNITE_TIME_SERIES_VIEW_ID)
161+
return results

cognite/client/_cognite_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from cognite.client._api.data_modeling.spaces import SpacesAPI
5656
from cognite.client._api.data_modeling.statistics import StatisticsAPI
5757
from cognite.client._api.data_modeling.streams import StreamsAPI
58+
from cognite.client._api.data_modeling.time_series import DataModelingTimeSeriesAPI
5859
from cognite.client._api.data_modeling.views import ViewsAPI
5960
from cognite.client._api.datapoints import DatapointsAPI
6061
from cognite.client._api.datapoints_subscriptions import DatapointsSubscriptionAPI
@@ -443,6 +444,7 @@ def _make_accessors_for_building_docs() -> None:
443444
AsyncCogniteClient.data_modeling.statistics.spaces = SpaceStatisticsAPI # type: ignore
444445
AsyncCogniteClient.data_modeling.streams = StreamsAPI # type: ignore
445446
AsyncCogniteClient.data_modeling.records = RecordsAPI # type: ignore
447+
AsyncCogniteClient.data_modeling.time_series = DataModelingTimeSeriesAPI # type: ignore
446448
AsyncCogniteClient.documents = DocumentsAPI # type: ignore
447449
AsyncCogniteClient.documents.previews = DocumentPreviewAPI # type: ignore
448450
AsyncCogniteClient.workflows = WorkflowAPI # type: ignore

cognite/client/_sync_api/data_modeling/__init__.py

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cognite/client/_sync_api/data_modeling/files.py

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)