Skip to content

Commit 8a8a106

Browse files
feat(Data Modeling): support usedFor=record on containers (#2621)
1 parent 3326f55 commit 8a8a106

5 files changed

Lines changed: 129 additions & 14 deletions

File tree

cognite/client/_api/data_modeling/containers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Container,
1111
ContainerApply,
1212
ContainerList,
13+
ContainerUsedFor,
1314
_ContainerFilter,
1415
)
1516
from cognite.client.data_classes.data_modeling.ids import (
@@ -48,6 +49,7 @@ def __call__(
4849
chunk_size: None = None,
4950
space: str | None = None,
5051
include_global: bool = False,
52+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
5153
limit: int | None = None,
5254
) -> AsyncIterator[Container]: ...
5355

@@ -57,6 +59,7 @@ def __call__(
5759
chunk_size: int,
5860
space: str | None = None,
5961
include_global: bool = False,
62+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
6063
limit: int | None = None,
6164
) -> AsyncIterator[ContainerList]: ...
6265

@@ -65,6 +68,7 @@ async def __call__(
6568
chunk_size: int | None = None,
6669
space: str | None = None,
6770
include_global: bool = False,
71+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
6872
limit: int | None = None,
6973
) -> AsyncIterator[Container] | AsyncIterator[ContainerList]:
7074
"""Iterate over containers
@@ -75,12 +79,13 @@ async def __call__(
7579
chunk_size (int | None): Number of containers to return in each chunk. Defaults to yielding one container a time.
7680
space (str | None): The space to query.
7781
include_global (bool): Whether the global containers should be returned.
82+
used_for (ContainerUsedFor | Sequence[ContainerUsedFor] | None): Only include containers marked for these purposes. If omitted, containers of every kind (nodes, edges, and records) are returned.
7883
limit (int | None): Maximum number of containers to return. Defaults to returning all items.
7984
8085
Yields:
8186
Container | ContainerList: yields Container one by one if chunk_size is not specified, else ContainerList objects.
8287
""" # noqa: DOC404
83-
flt = _ContainerFilter(space, include_global)
88+
flt = _ContainerFilter(space, include_global, used_for)
8489
async for item in self._list_generator(
8590
list_cls=ContainerList,
8691
resource_cls=Container,
@@ -226,13 +231,15 @@ async def list(
226231
space: str | None = None,
227232
limit: int | None = DATA_MODELING_DEFAULT_LIMIT_READ,
228233
include_global: bool = False,
234+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
229235
) -> ContainerList:
230236
"""`List containers <https://api-docs.cognite.com/20230101/tag/Containers/operation/listContainers>`_.
231237
232238
Args:
233239
space (str | None): The space to query
234240
limit (int | None): Maximum number of containers to return. Defaults to 10. Set to -1, float("inf") or None to return all items.
235241
include_global (bool): Whether the global containers should be returned.
242+
used_for (ContainerUsedFor | Sequence[ContainerUsedFor] | None): Only include containers marked for these purposes. If omitted, containers of every kind (nodes, edges, and records) are returned.
236243
237244
Returns:
238245
ContainerList: List of requested containers
@@ -246,6 +253,16 @@ async def list(
246253
>>> # async_client = AsyncCogniteClient() # another option
247254
>>> container_list = client.data_modeling.containers.list(limit=5)
248255
256+
Filter containers by `used_for`. Note that "all" refers to containers that stores *both*
257+
nodes and edges:
258+
259+
>>> # High-volume data containers (records)
260+
>>> record_containers = client.data_modeling.containers.list(used_for="record")
261+
>>> # Containers that store ONLY nodes or ONLY edges (excludes "all"):
262+
>>> containers = client.data_modeling.containers.list(used_for=["node", "edge"])
263+
>>> # All containers that can store nodes:
264+
>>> containers = client.data_modeling.containers.list(used_for=["node", "all"])
265+
249266
Iterate over containers, one-by-one:
250267
251268
>>> for container in client.data_modeling.containers():
@@ -256,7 +273,7 @@ async def list(
256273
>>> for container_list in client.data_modeling.containers(chunk_size=10):
257274
... container_list # do something with the containers
258275
"""
259-
flt = _ContainerFilter(space, include_global)
276+
flt = _ContainerFilter(space, include_global, used_for)
260277
return await self._list(
261278
list_cls=ContainerList,
262279
resource_cls=Container,

cognite/client/_sync_api/data_modeling/containers.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
===============================================================================
3-
465575f7283dac5a1831298bc22b3bda
3+
fa523a777c9b728e0712f75d938fbb0c
44
This file is auto-generated from the Async API modules, - do not edit manually!
55
===============================================================================
66
"""
@@ -17,6 +17,7 @@
1717
Container,
1818
ContainerApply,
1919
ContainerList,
20+
ContainerUsedFor,
2021
)
2122
from cognite.client.data_classes.data_modeling.ids import (
2223
ConstraintIdentifier,
@@ -38,19 +39,30 @@ def __init__(self, async_client: AsyncCogniteClient) -> None:
3839

3940
@overload
4041
def __call__(
41-
self, chunk_size: None = None, space: str | None = None, include_global: bool = False, limit: int | None = None
42+
self,
43+
chunk_size: None = None,
44+
space: str | None = None,
45+
include_global: bool = False,
46+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
47+
limit: int | None = None,
4248
) -> Iterator[Container]: ...
4349

4450
@overload
4551
def __call__(
46-
self, chunk_size: int, space: str | None = None, include_global: bool = False, limit: int | None = None
52+
self,
53+
chunk_size: int,
54+
space: str | None = None,
55+
include_global: bool = False,
56+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
57+
limit: int | None = None,
4758
) -> Iterator[ContainerList]: ...
4859

4960
def __call__(
5061
self,
5162
chunk_size: int | None = None,
5263
space: str | None = None,
5364
include_global: bool = False,
65+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
5466
limit: int | None = None,
5567
) -> Iterator[Container] | Iterator[ContainerList]:
5668
"""
@@ -62,14 +74,15 @@ def __call__(
6274
chunk_size (int | None): Number of containers to return in each chunk. Defaults to yielding one container a time.
6375
space (str | None): The space to query.
6476
include_global (bool): Whether the global containers should be returned.
77+
used_for (ContainerUsedFor | Sequence[ContainerUsedFor] | None): Only include containers marked for these purposes. If omitted, containers of every kind (nodes, edges, and records) are returned.
6578
limit (int | None): Maximum number of containers to return. Defaults to returning all items.
6679
6780
Yields:
6881
Container | ContainerList: yields Container one by one if chunk_size is not specified, else ContainerList objects.
6982
""" # noqa: DOC404
7083
yield from SyncIterator(
7184
self.__async_client.data_modeling.containers(
72-
chunk_size=chunk_size, space=space, include_global=include_global, limit=limit
85+
chunk_size=chunk_size, space=space, include_global=include_global, used_for=used_for, limit=limit
7386
)
7487
) # type: ignore [misc]
7588

@@ -171,6 +184,7 @@ def list(
171184
space: str | None = None,
172185
limit: int | None = DATA_MODELING_DEFAULT_LIMIT_READ,
173186
include_global: bool = False,
187+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
174188
) -> ContainerList:
175189
"""
176190
`List containers <https://api-docs.cognite.com/20230101/tag/Containers/operation/listContainers>`_.
@@ -179,6 +193,7 @@ def list(
179193
space (str | None): The space to query
180194
limit (int | None): Maximum number of containers to return. Defaults to 10. Set to -1, float("inf") or None to return all items.
181195
include_global (bool): Whether the global containers should be returned.
196+
used_for (ContainerUsedFor | Sequence[ContainerUsedFor] | None): Only include containers marked for these purposes. If omitted, containers of every kind (nodes, edges, and records) are returned.
182197
183198
Returns:
184199
ContainerList: List of requested containers
@@ -192,6 +207,16 @@ def list(
192207
>>> # async_client = AsyncCogniteClient() # another option
193208
>>> container_list = client.data_modeling.containers.list(limit=5)
194209
210+
Filter containers by `used_for`. Note that "all" refers to containers that stores *both*
211+
nodes and edges:
212+
213+
>>> # High-volume data containers (records)
214+
>>> record_containers = client.data_modeling.containers.list(used_for="record")
215+
>>> # Containers that store ONLY nodes or ONLY edges (excludes "all"):
216+
>>> containers = client.data_modeling.containers.list(used_for=["node", "edge"])
217+
>>> # All containers that can store nodes:
218+
>>> containers = client.data_modeling.containers.list(used_for=["node", "all"])
219+
195220
Iterate over containers, one-by-one:
196221
197222
>>> for container in client.data_modeling.containers():
@@ -203,7 +228,9 @@ def list(
203228
... container_list # do something with the containers
204229
"""
205230
return run_sync(
206-
self.__async_client.data_modeling.containers.list(space=space, limit=limit, include_global=include_global)
231+
self.__async_client.data_modeling.containers.list(
232+
space=space, limit=limit, include_global=include_global, used_for=used_for
233+
)
207234
)
208235

209236
@overload

cognite/client/data_classes/data_modeling/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ContainerList,
1515
ContainerProperty,
1616
ContainerPropertyApply,
17+
ContainerUsedFor,
1718
Index,
1819
IndexApply,
1920
InvertedIndex,
@@ -168,6 +169,7 @@
168169
"ContainerList",
169170
"ContainerProperty",
170171
"ContainerPropertyApply",
172+
"ContainerUsedFor",
171173
"DataModel",
172174
"DataModelApply",
173175
"DataModelApplyList",

cognite/client/data_classes/data_modeling/containers.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC, abstractmethod
44
from builtins import type as type_
5-
from collections.abc import Mapping
5+
from collections.abc import Mapping, Sequence
66
from dataclasses import dataclass, field
77
from typing import Any, Literal, TypeVar, cast
88

@@ -30,6 +30,9 @@
3030
_T_Constraint = TypeVar("_T_Constraint", bound="ConstraintCore")
3131
_T_Index = TypeVar("_T_Index", bound="IndexCore")
3232

33+
ContainerUsedFor = Literal["node", "edge", "record", "all"]
34+
_ALL_CONTAINER_USED_FOR: tuple[ContainerUsedFor, ...] = ("node", "edge", "record", "all")
35+
3336

3437
@dataclass
3538
class ContainerCore(DataModelingSchemaResource["ContainerApply"], ABC):
@@ -107,13 +110,13 @@ class ContainerApply(ContainerCore):
107110
name (str | None): Human readable name for the container.
108111
constraints (Mapping[str, ConstraintApply]): Set of constraints to apply to the container
109112
indexes (Mapping[str, IndexApply]): Set of indexes to apply to the container.
110-
used_for (Literal['node', 'edge', 'all'] | None): Should this operation apply to nodes, edges or both.
113+
used_for (ContainerUsedFor | None): Whether the container is for nodes, edges, records, or both nodes and edges (``all``).
111114
"""
112115

113116
properties: Mapping[str, ContainerPropertyApply]
114117
constraints: Mapping[str, ConstraintApply] = field(default_factory=dict)
115118
indexes: Mapping[str, IndexApply] = field(default_factory=dict)
116-
used_for: Literal["node", "edge", "all"] | None = None
119+
used_for: ContainerUsedFor | None = None
117120

118121
def __post_init__(self) -> None:
119122
validate_data_modeling_identifier(self.space, self.external_id)
@@ -149,14 +152,14 @@ class Container(ContainerCore):
149152
is_global (bool): Whether this is a global container, i.e., one of the out-of-the-box models.
150153
last_updated_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.
151154
created_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.
152-
used_for (Literal['node', 'edge', 'all']): Should this operation apply to nodes, edges or both.
155+
used_for (ContainerUsedFor): Whether the container is for nodes, edges, records, or both nodes and edges (``all``).
153156
"""
154157

155158
properties: Mapping[str, ContainerProperty]
156159
is_global: bool
157160
last_updated_time: int
158161
created_time: int
159-
used_for: Literal["node", "edge", "all"]
162+
used_for: ContainerUsedFor
160163
constraints: Mapping[str, Constraint] = field(default_factory=dict)
161164
indexes: Mapping[str, Index] = field(default_factory=dict)
162165

@@ -226,9 +229,23 @@ def as_write(self) -> ContainerApplyList:
226229

227230

228231
class _ContainerFilter(CogniteFilter):
229-
def __init__(self, space: str | None = None, include_global: bool = False) -> None:
232+
def __init__(
233+
self,
234+
space: str | None = None,
235+
include_global: bool = False,
236+
used_for: ContainerUsedFor | Sequence[ContainerUsedFor] | None = None,
237+
) -> None:
230238
self.space = space
231239
self.include_global = include_global
240+
self.used_for: Sequence[ContainerUsedFor]
241+
if used_for is None:
242+
self.used_for = list(_ALL_CONTAINER_USED_FOR)
243+
elif isinstance(used_for, str):
244+
self.used_for = [used_for]
245+
elif isinstance(used_for, Sequence):
246+
self.used_for = cast("Sequence[ContainerUsedFor]", used_for)
247+
else:
248+
raise TypeError(f"Invalid value for 'used_for': {used_for!r}")
232249

233250

234251
@dataclass(frozen=True)

tests/tests_unit/test_api/test_data_modeling/test_containers.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import re
44
from typing import TYPE_CHECKING, Any, cast
5+
from urllib.parse import parse_qs, urlparse
56

67
import pytest
78
from pytest_httpx import HTTPXMock
89

910
from cognite.client import CogniteClient
1011
from cognite.client.data_classes.data_modeling import ContainerApply, ContainerId, ContainerPropertyApply, Text
11-
from cognite.client.data_classes.data_modeling.containers import BTreeIndexApply, RequiresConstraintApply
12+
from cognite.client.data_classes.data_modeling.containers import (
13+
BTreeIndexApply,
14+
RequiresConstraintApply,
15+
_ContainerFilter,
16+
)
1217
from tests.utils import get_url
1318

1419
if TYPE_CHECKING:
@@ -116,3 +121,50 @@ def test_apply_retrieve_and_delete_index(
116121

117122
deleted_indexes = cognite_client.data_modeling.containers.delete_indexes([(new_container.as_id(), "index1")])
118123
assert deleted_indexes == [(new_container.as_id(), "index1")]
124+
125+
def test_list_request_includes_used_for(
126+
self, httpx_mock: Any, cognite_client: CogniteClient, async_client: AsyncCogniteClient
127+
) -> None:
128+
base = get_url(async_client.data_modeling.containers) + "/models/containers"
129+
httpx_mock.add_response(url=re.compile("^" + re.escape(base)), json={"items": []})
130+
131+
cognite_client.data_modeling.containers.list(used_for=["node", "record"], limit=5)
132+
133+
req = httpx_mock.get_requests()[0]
134+
raw_query = urlparse(str(req.url)).query
135+
# Spec defines `usedFor` as a query array with no explicit style/explode, so OpenAPI's
136+
# default (style=form, explode=true) applies: each value gets its own `usedFor=` pair.
137+
assert "usedFor=node" in raw_query and "usedFor=record" in raw_query
138+
assert "usedFor=node%2Crecord" not in raw_query and "usedFor=node,record" not in raw_query
139+
qs = parse_qs(raw_query)
140+
assert qs.get("usedFor") == ["node", "record"]
141+
142+
def test_list_request_used_for_single_value(
143+
self, httpx_mock: Any, cognite_client: CogniteClient, async_client: AsyncCogniteClient
144+
) -> None:
145+
base = get_url(async_client.data_modeling.containers) + "/models/containers"
146+
httpx_mock.add_response(url=re.compile("^" + re.escape(base)), json={"items": []})
147+
148+
cognite_client.data_modeling.containers.list(used_for="record")
149+
150+
req = httpx_mock.get_requests()[0]
151+
qs = parse_qs(urlparse(str(req.url)).query)
152+
assert qs.get("usedFor") == ["record"]
153+
154+
def test_list_request_default_returns_all_kinds(
155+
self, httpx_mock: Any, cognite_client: CogniteClient, async_client: AsyncCogniteClient
156+
) -> None:
157+
# When the caller does not specify `used_for`, the SDK should request all container kinds,
158+
# including records, rather than rely on the server's default of `all` which excludes records.
159+
base = get_url(async_client.data_modeling.containers) + "/models/containers"
160+
httpx_mock.add_response(url=re.compile("^" + re.escape(base)), json={"items": []})
161+
162+
cognite_client.data_modeling.containers.list()
163+
164+
req = httpx_mock.get_requests()[0]
165+
qs = parse_qs(urlparse(str(req.url)).query)
166+
assert qs.get("usedFor") == ["node", "edge", "record", "all"]
167+
168+
def test_container_filter_rejects_invalid_used_for(self) -> None:
169+
with pytest.raises(TypeError, match="Invalid value for 'used_for'"):
170+
_ContainerFilter(used_for=123) # type: ignore[arg-type]

0 commit comments

Comments
 (0)