-
Notifications
You must be signed in to change notification settings - Fork 37
feat(records): filter endpoint #2699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b5cec33
63b348c
cbda60e
25548a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,11 +10,24 @@ | |
| CogniteResource, | ||
| CogniteResourceList, | ||
| WriteableCogniteResource, | ||
| WriteableCogniteResourceList, | ||
| ) | ||
| from cognite.client.data_classes.data_modeling.ids import ContainerId | ||
| from cognite.client.data_classes.data_modeling.instances import TypeInformation | ||
| from cognite.client.utils._identifier import IdentifierSequenceCore, RecordId | ||
|
|
||
| __all__ = ["RecordContainerId", "RecordId", "RecordIdSequence", "RecordSource", "RecordWrite", "RecordWriteList"] | ||
| __all__ = [ | ||
| "Record", | ||
| "RecordContainerId", | ||
| "RecordId", | ||
| "RecordIdSequence", | ||
| "RecordList", | ||
| "RecordSource", | ||
| "RecordSourceSelector", | ||
| "RecordWrite", | ||
| "RecordWriteList", | ||
| "TimeRange", | ||
| ] | ||
|
|
||
|
|
||
| class RecordIdSequence(IdentifierSequenceCore[RecordId]): | ||
|
|
@@ -106,3 +119,154 @@ class RecordWriteList(CogniteResourceList[RecordWrite]): | |
|
|
||
| def as_ids(self) -> list[RecordId]: | ||
| return [v.as_id() for v in self] | ||
|
|
||
|
|
||
| class Record(WriteableCogniteResource["RecordWrite"]): | ||
| """A record returned from the stream records API. | ||
|
|
||
| This is the read version of :class:`RecordWrite`. | ||
|
|
||
| Args: | ||
| space (str): Space the record belongs to. | ||
| external_id (str): External ID of the record. | ||
| created_time (int): Creation time in milliseconds since epoch. | ||
| last_updated_time (int): Last updated time in milliseconds since epoch. | ||
| properties (dict[str, dict[str, dict[str, Any]]] | None): Property values keyed by | ||
| ``{space: {container_external_id: {property_id: value}}}``. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| space: str, | ||
| external_id: str, | ||
| created_time: int, | ||
| last_updated_time: int, | ||
| properties: dict[str, dict[str, dict[str, Any]]] | None = None, | ||
| ) -> None: | ||
| self.space = space | ||
| self.external_id = external_id | ||
| self.created_time = created_time | ||
| self.last_updated_time = last_updated_time | ||
| self.properties = properties | ||
|
|
||
| @classmethod | ||
| def _load(cls, resource: dict[str, Any]) -> Self: | ||
| return cls( | ||
| space=resource["space"], | ||
| external_id=resource["externalId"], | ||
| created_time=resource["createdTime"], | ||
| last_updated_time=resource["lastUpdatedTime"], | ||
| properties=resource.get("properties"), | ||
| ) | ||
|
|
||
| def dump(self, camel_case: bool = True) -> dict[str, Any]: | ||
| output: dict[str, Any] = { | ||
| "space": self.space, | ||
| "externalId" if camel_case else "external_id": self.external_id, | ||
| "createdTime" if camel_case else "created_time": self.created_time, | ||
| "lastUpdatedTime" if camel_case else "last_updated_time": self.last_updated_time, | ||
| } | ||
| if self.properties is not None: | ||
| output["properties"] = self.properties | ||
| return output | ||
|
|
||
| def as_id(self) -> RecordId: | ||
| return RecordId(space=self.space, external_id=self.external_id) | ||
|
|
||
| def as_write(self) -> RecordWrite: | ||
| """Reconstruct the :class:`RecordWrite` by grouping read properties back into sources.""" | ||
| sources = [ | ||
| RecordSource( | ||
| source=RecordContainerId(space=space, external_id=container), | ||
| properties=dict(props), | ||
| ) | ||
| for space, containers in (self.properties or {}).items() | ||
| for container, props in containers.items() | ||
| ] | ||
| return RecordWrite(space=self.space, external_id=self.external_id, sources=sources) | ||
|
|
||
|
|
||
| class RecordList(WriteableCogniteResourceList[RecordWrite, Record]): | ||
| """A list of :class:`Record` objects. | ||
|
|
||
| Args: | ||
| resources (Sequence[Record]): The records. | ||
| typing (TypeInformation | None): Property type information, present when the request | ||
| was made with ``include_typing=True``. | ||
| """ | ||
|
|
||
| _RESOURCE = Record | ||
|
|
||
| def __init__(self, resources: Sequence[Record], typing: TypeInformation | None = None) -> None: | ||
| super().__init__(resources) | ||
| self.typing = typing | ||
|
|
||
| def as_ids(self) -> list[RecordId]: | ||
| return [record.as_id() for record in self] | ||
|
|
||
| def as_write(self) -> RecordWriteList: | ||
| return RecordWriteList([record.as_write() for record in self]) | ||
|
|
||
| @classmethod | ||
| def _load_raw_api_response(cls, responses: list[dict[str, Any]]) -> Self: | ||
| typing = next((TypeInformation._load(resp["typing"]) for resp in responses if "typing" in resp), None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the API response contains a typing = next((TypeInformation._load(resp[\"typing\"]) for resp in responses if resp.get(\"typing\") is not None), None) |
||
| resources = [cls._RESOURCE._load(item) for response in responses for item in response.get("items", [])] | ||
| return cls(resources, typing) | ||
|
|
||
|
|
||
| class TimeRange(CogniteResource): | ||
| """A time range filter on ``lastUpdatedTime``. | ||
|
|
||
| Bounds are either milliseconds since the Unix epoch (int) or an ISO-8601 string. At least a | ||
| lower bound (``gte`` or ``gt``) is required for immutable streams; specifying two lower or two | ||
| upper bounds is not allowed. | ||
|
|
||
| Args: | ||
| gte (int | str | None): Greater than or equal to. | ||
| gt (int | str | None): Greater than. | ||
| lte (int | str | None): Less than or equal to. | ||
| lt (int | str | None): Less than. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| gte: int | str | None = None, | ||
| gt: int | str | None = None, | ||
| lte: int | str | None = None, | ||
| lt: int | str | None = None, | ||
| ) -> None: | ||
| self.gte = gte | ||
| self.gt = gt | ||
| self.lte = lte | ||
| self.lt = lt | ||
|
|
||
| @classmethod | ||
| def _load(cls, resource: dict[str, Any]) -> Self: | ||
| return cls(gte=resource.get("gte"), gt=resource.get("gt"), lte=resource.get("lte"), lt=resource.get("lt")) | ||
|
|
||
| def dump(self, camel_case: bool = True) -> dict[str, Any]: | ||
| return { | ||
| key: value | ||
| for key, value in {"gte": self.gte, "gt": self.gt, "lte": self.lte, "lt": self.lt}.items() | ||
| if value is not None | ||
| } | ||
|
|
||
|
|
||
| class RecordSourceSelector(CogniteResource): | ||
| """Selects which container properties to return for a record. | ||
|
|
||
| Args: | ||
| source (RecordContainerId): The container to select properties from. | ||
| properties (list[str]): Property identifiers to return; use ``["*"]`` to return all. | ||
| """ | ||
|
|
||
| def __init__(self, source: RecordContainerId, properties: list[str]) -> None: | ||
| self.source = source | ||
| self.properties = properties | ||
|
|
||
| @classmethod | ||
| def _load(cls, resource: dict[str, Any]) -> Self: | ||
| return cls(source=RecordContainerId.load(resource["source"]), properties=resource["properties"]) | ||
|
|
||
| def dump(self, camel_case: bool = True) -> dict[str, Any]: | ||
| return {"source": self.source.dump(camel_case=camel_case), "properties": self.properties} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a user passes a dictionary or a list of dictionaries for the
sortparameter (which is a common pattern in the SDK),isinstance(sort, InstanceSort)will evaluate toFalse, andlist(sort)on a dictionary will return its keys. This leads to anAttributeErrorwhen trying to call.dump()on a string key. Update the logic to support bothInstanceSortand dictionary representations gracefully.