Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cognite/client/_api/data_modeling/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ async def ingest(
... ),
... stream_id="my-stream",
... )

Ingest a record through a view:

>>> from cognite.client.data_classes.data_modeling.records import RecordViewId
>>> source = RecordSource(
... RecordViewId("my-space", "my-view", "v1"), {"temperature": 22.5}
... )
>>> record = RecordWrite("my-space", "rec-2", sources=[source])
>>> client.data_modeling.records.ingest(record, stream_id="my-stream")
"""
self._warning.warn()
item_list: list[RecordWrite] = [items] if isinstance(items, RecordWrite) else list(items)
Expand Down Expand Up @@ -228,6 +237,9 @@ async def aggregate(
) -> RecordsAggregation:
"""`Aggregate records from a stream <https://api-docs.cognite.com/20230101/tag/Records/operation/aggregateRecords>`_.

Aggregate properties may reference multiple containers or a single view, but cannot
mix views and containers. This restriction does not apply to filters or target units.

Args:
aggregates (Mapping[str, Aggregate | dict[str, Any]]): Aggregate request tree keyed
by client-defined aggregate IDs.
Expand Down Expand Up @@ -378,7 +390,7 @@ async def filter(
last_updated_time (TimeRange | None): Filter by last-updated time. **Required for
immutable streams** (must include a lower bound).
filter (Filter | None): Filter expression (see :mod:`cognite.client.data_classes.filters`).
sources (Sequence[RecordSourceSelector] | None): Which container properties to return.
sources (Sequence[RecordSourceSelector] | None): Which container or view properties to return.
sort (Sequence[InstanceSort] | InstanceSort | None): Sort specification(s); up to 5.
limit (int): Maximum number of records to return (1-1000). This endpoint returns a single
page and does not paginate, so a larger limit is an error rather than a silent cap.
Expand Down Expand Up @@ -485,7 +497,7 @@ async def sync(
cursor (str | None): Resume from a cursor from a previously yielded chunk. Mutually
exclusive with ``initialize_cursor``.
filter (Filter | None): Filter expression (see :mod:`cognite.client.data_classes.filters`).
sources (Sequence[RecordSourceSelector] | None): Which container properties to return.
sources (Sequence[RecordSourceSelector] | None): Which container or view properties to return.
target_units (RecordTargetUnits | Sequence[RecordTargetUnit] | None): Properties to convert
to another unit.
chunk_size (int): Number of records per yielded chunk, between 1 and 1000. Defaults to 1000.
Expand Down
18 changes: 15 additions & 3 deletions cognite/client/_sync_api/data_modeling/records.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cognite/client/data_classes/data_modeling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
EdgeId,
NodeId,
PropertyId,
PropertyPath,
VersionedDataModelingId,
ViewId,
ViewIdentifier,
Expand Down Expand Up @@ -229,6 +230,7 @@
"NodeResultSetExpressionSync",
"PropertyId",
"PropertyOptions",
"PropertyPath",
"PropertyType",
"Query",
"QueryResult",
Expand Down
56 changes: 31 additions & 25 deletions cognite/client/data_classes/data_modeling/_validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from cognite.client.utils.useful_types import SequenceNotStr, is_sequence_not_str
from cognite.client.data_classes.data_modeling.ids import ContainerId, PropertyId, PropertyPath, ViewId
from cognite.client.utils.useful_types import is_sequence_not_str

RESERVED_EXTERNAL_IDS = frozenset(
{
Expand Down Expand Up @@ -46,9 +47,10 @@


PROPERTY_PATH_HINT = (
"A property is addressed by its path: [space, container_external_id, property_id], e.g. "
'["my_space", "my_container", "temperature"]. Endpoints that allow top level properties take '
'them as a single segment, e.g. ["lastUpdatedTime"].'
"A property is addressed by its path: [space, container_external_id, property_id] or "
'[space, "view_external_id/version", property_id], e.g. ["my_space", "my_container", "temperature"] '
'or ["my_space", "my_view/v1", "temperature"], or by calling view_or_container.as_property_ref("temperature"). '
'Endpoints that allow top level properties take them as a single segment, e.g. ["lastUpdatedTime"].'
)


Expand All @@ -59,34 +61,38 @@ def validate_data_modeling_identifier(space: str | None, external_id: str | None
raise ValueError(f"The external ID: {external_id!r} is reserved. Please use another ID.")


def validate_property_path(
prop: SequenceNotStr[str], argument: str = "property", hint: str = PROPERTY_PATH_HINT
) -> list[str]:
def validate_property_path(prop: PropertyPath, argument: str = "property", hint: str = PROPERTY_PATH_HINT) -> list[str]:
"""Validate a property path and return it as a list of segments.

A bare string is a sequence of characters, so passing one where a sequence of strings is
expected silently produces one segment per character instead of failing; reject it here.
Paths are short, at most three segments, so every segment is type checked.

Args:
prop (SequenceNotStr[str]): The user-provided property path.
prop (PropertyPath): The user-provided property path, (source, property) tuple, or PropertyId.
argument (str): Name of the argument, used in the error message.
hint (str): Actionable follow-up appended to the error message. Defaults to describing a
fully qualified property path, which is what most arguments taking one expect.
hint (str): Guidance appended to validation errors.

Returns:
list[str]: The validated path as a list.
"""
if not is_sequence_not_str(prop):
got = f"the string {prop!r}" if isinstance(prop, str) else type(prop).__name__
raise TypeError(f"{argument!r} must be a sequence of strings, not {got}. {hint}")
path = list(prop)
if not path:
raise ValueError(f"{argument!r} must not be empty. {hint}")
for segment in path:
if not isinstance(segment, str):
match prop:
case PropertyId():
return list(prop.as_property_ref())
case tuple([ContainerId() | ViewId() as source, str(prop_name)]):
return list(source.as_property_ref(prop_name))
case tuple([ContainerId() | ViewId(), invalid_prop]):
raise TypeError(
f"{argument!r} must be a sequence of strings, but {segment!r} is of type "
f"{type(segment).__name__}. {hint}"
f"{argument!r} given as a (source, property) tuple must have a string property, "
f"but {invalid_prop!r} is of type {type(invalid_prop).__name__}. {hint}"
)
return path
case _ if not is_sequence_not_str(prop):
got = f"the string {prop!r}" if isinstance(prop, str) else type(prop).__name__
raise TypeError(f"{argument!r} must be a sequence of strings, not {got}. {hint}")
case _:
path = list(prop)
if not path:
raise ValueError(f"{argument!r} must not be empty. {hint}")
for segment in path:
if not isinstance(segment, str):
raise TypeError(
f"{argument!r} must be a sequence of strings, but {segment!r} is of type "
f"{type(segment).__name__}. {hint}"
)
return path
22 changes: 11 additions & 11 deletions cognite/client/data_classes/data_modeling/aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@

from cognite.client.data_classes._base import CogniteResource
from cognite.client.data_classes.data_modeling._validation import validate_property_path
from cognite.client.data_classes.data_modeling.ids import PropertyPath
from cognite.client.data_classes.filters import Filter
from cognite.client.utils._text import convert_all_keys_to_snake_case, to_snake_case
from cognite.client.utils.useful_types import SequenceNotStr


def _dump_aggregate_value(value: Any) -> Any:
Expand Down Expand Up @@ -107,11 +107,11 @@ def _load(cls, resource: dict[str, Any]) -> Aggregate:


class Average(Aggregate):
"""Average aggregate over a container property."""
"""Average aggregate over a container or view property."""

_aggregate_name = "avg"

def __init__(self, property: SequenceNotStr[str]) -> None:
def __init__(self, property: PropertyPath) -> None:
self.property = validate_property_path(property)

def _dump_body(self) -> dict[str, Any]:
Expand All @@ -123,7 +123,7 @@ class Count(Aggregate):

_aggregate_name = "count"

def __init__(self, property: SequenceNotStr[str] | None = None) -> None:
def __init__(self, property: PropertyPath | None = None) -> None:
self.property = validate_property_path(property) if property is not None else None

def _dump_body(self) -> dict[str, Any]:
Expand All @@ -135,7 +135,7 @@ class Min(Aggregate):

_aggregate_name = "min"

def __init__(self, property: SequenceNotStr[str]) -> None:
def __init__(self, property: PropertyPath) -> None:
self.property = validate_property_path(property)

def _dump_body(self) -> dict[str, Any]:
Expand All @@ -147,19 +147,19 @@ class Max(Aggregate):

_aggregate_name = "max"

def __init__(self, property: SequenceNotStr[str]) -> None:
def __init__(self, property: PropertyPath) -> None:
self.property = validate_property_path(property)

def _dump_body(self) -> dict[str, Any]:
return {"property": self.property}


class Sum(Aggregate):
"""Sum aggregate over a container property."""
"""Sum aggregate over a container or view property."""

_aggregate_name = "sum"

def __init__(self, property: SequenceNotStr[str]) -> None:
def __init__(self, property: PropertyPath) -> None:
self.property = validate_property_path(property)

def _dump_body(self) -> dict[str, Any]:
Expand All @@ -173,7 +173,7 @@ class UniqueValues(Aggregate):

def __init__(
self,
property: SequenceNotStr[str],
property: PropertyPath,
aggregates: Mapping[str, Aggregate | dict[str, Any]] | None = None,
size: int | None = None,
):
Expand All @@ -197,7 +197,7 @@ class NumberHistogram(Aggregate):

def __init__(
self,
property: SequenceNotStr[str],
property: PropertyPath,
interval: float,
aggregates: Mapping[str, Aggregate | dict[str, Any]] | None = None,
hard_bounds: Mapping[str, float] | None = None,
Expand All @@ -223,7 +223,7 @@ class TimeHistogram(Aggregate):

def __init__(
self,
property: SequenceNotStr[str],
property: PropertyPath,
*,
calendar_interval: str | None = None,
fixed_interval: str | None = None,
Expand Down
5 changes: 5 additions & 0 deletions cognite/client/data_classes/data_modeling/ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ class PropertyId(CogniteResource):
source: ViewId | ContainerId
property: str

def as_property_ref(self) -> tuple[str, str, str]:
"""Return the fully qualified property path as a tuple of three strings."""
return self.source.as_property_ref(self.property)

@classmethod
def _load(cls, resource: dict[str, Any]) -> Self:
return cls(
Expand Down Expand Up @@ -195,6 +199,7 @@ def version(self) -> str | None: ...
ContainerIdentifier = ContainerId | tuple[str, str]
ConstraintIdentifier = tuple[ContainerId, str]
IndexIdentifier = tuple[ContainerId, str]
PropertyPath = SequenceNotStr[str] | tuple[ContainerId | ViewId, str] | PropertyId
ViewIdentifier = ViewId | tuple[str, str] | tuple[str, str, str]
DataModelIdentifier = DataModelId | tuple[str, str] | tuple[str, str, str]
NodeIdentifier = NodeId | tuple[str, str, str]
Expand Down
Loading
Loading