diff --git a/cognite/pygen/_core/templates/api_class_node.py.jinja b/cognite/pygen/_core/templates/api_class_node.py.jinja index d35508775..1efbbb60e 100644 --- a/cognite/pygen/_core/templates/api_class_node.py.jinja +++ b/cognite/pygen/_core/templates/api_class_node.py.jinja @@ -81,6 +81,12 @@ class {{ api_class.name }}({% if data_class.is_writable %}NodeAPI{% else %}NodeR A query API for {{ data_class.doc_list_name}}. """ + warnings.warn( + "This method is deprecated and will soon be removed. " + "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = {{ data_class.filter_name }}( self._view_id,{% for parm in list_method.parameters %} diff --git a/docs/usage/edge_properties.ipynb b/docs/usage/edge_properties.ipynb index e928f4262..3b58b789b 100644 --- a/docs/usage/edge_properties.ipynb +++ b/docs/usage/edge_properties.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "id": "e32acfc3-632c-479e-95be-331461c4d5f9", "metadata": { "editable": true, @@ -17,43 +17,26 @@ "source": [ "# This is just to enable improting the generated SDK from the examples folder in the pygen repository\n", "import sys\n", + "import warnings\n", "\n", "from tests.constants import REPO_ROOT\n", "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", "sys.path.append(str(REPO_ROOT / \"examples\"))" ] }, { - "attachments": { - "1b7b0795-705e-4cee-88da-f0f1e7d82be6.png": { - "image/png": "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" - } - }, + "attachments": {}, "cell_type": "markdown", "id": "34ed5247-d76c-4b22-a5d9-6ead02628433", "metadata": {}, "source": [ "# Working with Edges\n", "\n", - "Ofte edges in our data model is simply connecting two nodes. However, it is possible to have properties on edges as we have on nodes. This can be useful for filtering on these properties to select which nodes and edges we want.\n", - "\n", - "In this example, we will use the model shown below in the Cognite Data Fusion interface:\n", - "\n", - "![image.png](attachment:1b7b0795-705e-4cee-88da-f0f1e7d82be6.png)\n", - "\n", - "\n", - "We note that we have one two `views`/`types` used for `nodes`, `UnitProcedure`and `EquipmentModule`, and one for edges `StartEndTime`. A `UnitProcedure` is connected through multiple `EquipmentModules` thorugh the `StartEndTime` edge.\n", + "In this tutorial, we use edges with properties. We notice that the connection between `WindTurbine` and `Metmast`goes through an edge with the property `distance`.\n", "\n", - "\n", - "First, we have to generate an SDK for the `EquipmentUnit` model. The call below shows a truncated version of the call used to generate the SDK.\n", - "\n", - "```bash\n", - "pygen generate --client-name EquipmentUnitClient --to-level-package equipment_unit\n", - "```\n", - "\n", - "Note that you can use any of the methods for generating the SDK as described in see the [QuickStart guides](../quickstart/cdf_notebook.html).\n", - "\n", - "Second, we have setup a `config.toml`in the current working directory with credentials to connect to Cognite Data Fusion\n" + "\n" ] }, { @@ -63,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "from equipment_unit import EquipmentUnitClient" + "from wind_turbine import WindTurbineClient" ] }, { @@ -73,7 +56,7 @@ "metadata": {}, "outputs": [], "source": [ - "pygen = EquipmentUnitClient.from_toml(\"config.toml\")" + "pygen = WindTurbineClient.from_toml(\"config.toml\")" ] }, { @@ -89,12 +72,12 @@ "id": "a095350c-834d-4606-8582-b536ca8a9282", "metadata": {}, "source": [ - "The `StartEndTime` edges are available on the `unit_procedure` class, and have a `list` method we can use to list and filter all edges of this type." + "The `Distance` edges are available on the `wind_turbine` class, and have a `list` method we can use to list and filter all edges of this type." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "c5f449e7-7daf-42fb-8417-73f70f212094", "metadata": {}, "outputs": [ @@ -124,102 +107,94 @@ "
\n", - " | space | \n", - "external_id | \n", - "edge_type | \n", - "start_node | \n", - "end_node | \n", - "end_time | \n", - "start_time | \n", - "data_record | \n", - "
---|---|---|---|---|---|---|---|---|
0 | \n", - "IntegrationTestsImmutable | \n", - "unit_procedure:Scott Patel:equipment_module:Mi... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "2023-11-07 18:58:39+00:00 | \n", - "2023-11-04 23:22:42+00:00 | \n", - "{'version': 1, 'last_updated_time': 2023-11-13... | \n", - "
\n", - " | space | \n", - "external_id | \n", - "edge_type | \n", - "start_node | \n", - "end_node | \n", - "end_time | \n", - "start_time | \n", - "data_record | \n", - "
---|---|---|---|---|---|---|---|---|
0 | \n", - "IntegrationTestsImmutable | \n", - "unit_procedure:Kelly Evans:equipment_module:Gr... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "{'space': 'IntegrationTestsImmutable', 'extern... | \n", - "2023-11-13 18:01:26+00:00 | \n", - "2023-11-12 13:56:38+00:00 | \n", - "{'version': 1, 'last_updated_time': 2023-11-13... | \n", - "
\n", - " | value | \n", - "
---|---|
space | \n", - "IntegrationTestsImmutable | \n", - "
external_id | \n", - "unit_procedure:Matthew Gonzalez | \n", - "
data_record | \n", - "{'version': 1, 'last_updated_time': 2023-11-13... | \n", - "
node_type | \n", - "None | \n", - "
name | \n", - "Matthew Gonzalez | \n", - "
type_ | \n", - "red | \n", - "
work_orders | \n", - "None | \n", - "
work_units | \n", - "[{'space': 'IntegrationTestsImmutable', 'exter... | \n", - "
\n", - " | value | \n", - "||
---|---|---|---|
space | \n", - "IntegrationTestsImmutable | \n", - "||
external_id | \n", - "equipment_module:Hannah Mcgee | \n", + "{'space': 'sp_wind', 'external_id': 'utsira_st... | \n", "|
data_record | \n", - "{'version': 1, 'last_updated_time': 2023-11-13... | \n", - "||
node_type | \n", - "None | \n", - "||
description | \n", - "Box likely camera short part different half. L... | \n", - "||
name | \n", - "Hannah Mcgee | \n", - "||
sensor_value | \n", - "Hospital wear trouble part about question. Def... | \n", + "{'version': 1, 'last_updated_time': 2024-11-16... | \n", "|
type_ | \n", - "orange | \n", + "distance | \n", + "1000.0 | \n", "
Call .list_full() to return a list of {nodes[0].title()} and -.list_{last_connection_name}() to return a list of {nodes[-1].title()}.
-""" - - -class NodeQueryCore(QueryCore[T_DomainModelList, T_DomainListEnd]): - _result_cls: ClassVar[type[DomainModel]] - - def list_full(self, limit: int = DEFAULT_QUERY_LIMIT) -> T_DomainModelList: - builder = self._create_query(limit, self._result_list_cls, return_step="first", try_reverse=True) - builder.execute_query(self._client, remove_not_connected=True) - return builder.unpack() - - def _list(self, limit: int = DEFAULT_QUERY_LIMIT) -> T_DomainListEnd: - builder = self._create_query(limit, cast(type[DomainModelList], self._result_list_cls_end), return_step="last") - for step in builder[:-1]: - step.select = None - builder.execute_query(self._client, remove_not_connected=False) - return builder.unpack() - - def _dump_yaml(self) -> str: - return self._create_query(DEFAULT_QUERY_LIMIT, self._result_list_cls)._dump_yaml() - - def _create_query( - self, - limit: int, - result_list_cls: type[DomainModelList], - return_step: Literal["first", "last"] | None = None, - try_reverse: bool = False, - ) -> DataClassQueryBuilder: - builder = DataClassQueryBuilder(result_list_cls, return_step=return_step) - from_: str | None = None - first: bool = True - is_last_reverse_list = False - for item in self._creation_path: - if is_last_reverse_list: - raise ValueError( - "Cannot traverse past reverse direct relation of list. " - "This is a limitation of the modeling implementation in your data model." - "To do this query, you need to reimplement the data model and use an edge to " - "implement this connection instead of a reverse direct relation" - ) - name = builder.create_name(from_) - max_retrieve_limit = limit if first else -1 - step: QueryStep - if isinstance(item, NodeQueryCore) and isinstance(item._expression, dm.query.NodeResultSetExpression): - step = NodeQueryStep( - name=name, - expression=item._expression, - result_cls=item._result_cls, - max_retrieve_limit=max_retrieve_limit, - connection_type=item._connection_type, - ) - step.expression.from_ = from_ - step.expression.filter = item._assemble_filter() - builder.append(step) - elif isinstance(item, NodeQueryCore) and isinstance(item._expression, dm.query.EdgeResultSetExpression): - edge_name = name - step = EdgeQueryStep(name=edge_name, expression=item._expression, max_retrieve_limit=max_retrieve_limit) - step.expression.from_ = from_ - builder.append(step) - - name = builder.create_name(edge_name) - node_step = NodeQueryStep( - name=name, - expression=dm.query.NodeResultSetExpression( - from_=edge_name, - filter=item._assemble_filter(), - ), - result_cls=item._result_cls, - ) - builder.append(node_step) - elif isinstance(item, EdgeQueryCore): - step = EdgeQueryStep( - name=name, - expression=cast(dm.query.EdgeResultSetExpression, item._expression), - result_cls=item._result_cls, - ) - step.expression.from_ = from_ - step.expression.filter = item._assemble_filter() - builder.append(step) - else: - raise TypeError(f"Unsupported query step type: {type(item._expression)}") - - is_last_reverse_list = item._connection_type == "reverse-list" - first = False - from_ = name - return builder - - -class EdgeQueryCore(QueryCore[T_DomainList, T_DomainListEnd]): - _result_cls: ClassVar[type[DomainRelation]] - - -@dataclass(frozen=True) -class ViewPropertyId(CogniteObject): - view: dm.ViewId - property: str - - @classmethod - def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> "ViewPropertyId": - return cls( - view=dm.ViewId.load(resource["view"]), - property=resource["identifier"], - ) - - def dump(self, camel_case: bool = True) -> dict[str, Any]: - return { - "view": self.view.dump(camel_case=camel_case, include_type=False), - "identifier": self.property, - } - - -class QueryReducingBatchSize(UserWarning): - """Raised when a query is too large and the batch size must be reduced.""" - - ... - - -def chunker(sequence: Sequence, chunk_size: int) -> Iterator[Sequence]: - """ - Split a sequence into chunks of size chunk_size. - - Args: - sequence: The sequence to split. - chunk_size: The size of each chunk. - - Returns: - An iterator over the chunks. - - """ - for i in range(0, len(sequence), chunk_size): - yield sequence[i : i + chunk_size] - - -class QueryStep: - def __init__( - self, - name: str, - expression: dm.query.ResultSetExpression, - view_id: dm.ViewId | None = None, - max_retrieve_limit: int = -1, - select: dm.query.Select | None | type[_NotSetSentinel] = _NotSetSentinel, - raw_filter: dm.Filter | None = None, - connection_type: Literal["reverse-list"] | None = None, - view_property: ViewPropertyId | None = None, - selected_properties: list[str] | None = None, - ): - self.name = name - self.expression = expression - self.view_id = view_id - self.max_retrieve_limit = max_retrieve_limit - self.select: dm.query.Select | None - if select is _NotSetSentinel: - try: - self.select = self._default_select() - except NotImplementedError: - raise ValueError(f"You need to provide a select to instantiate a {type(self).__name__}") from None - else: - self.select = select # type: ignore[assignment] - self.raw_filter = raw_filter - self.connection_type = connection_type - self.view_property = view_property - self.selected_properties = selected_properties - self._max_retrieve_batch_limit = ACTUAL_INSTANCE_QUERY_LIMIT - self.cursor: str | None = None - self.total_retrieved: int = 0 - self.last_batch_count: int = 0 - self.results: list[Instance] = [] - - def _default_select(self) -> dm.query.Select: - if self.view_id is None: - return dm.query.Select() - else: - return dm.query.Select([dm.query.SourceSelector(self.view_id, ["*"])]) - - @property - def is_queryable(self) -> bool: - # We cannot query across reverse-list connections - return self.connection_type != "reverse-list" - - @property - def from_(self) -> str | None: - return self.expression.from_ - - @property - def is_single_direct_relation(self) -> bool: - return isinstance(self.expression, dm.query.NodeResultSetExpression) and self.expression.through is not None - - @property - def node_expression(self) -> dm.query.NodeResultSetExpression | None: - if isinstance(self.expression, dm.query.NodeResultSetExpression): - return self.expression - return None - - @property - def edge_expression(self) -> dm.query.EdgeResultSetExpression | None: - if isinstance(self.expression, dm.query.EdgeResultSetExpression): - return self.expression - return None - - @property - def node_results(self) -> Iterable[dm.Node]: - return (item for item in self.results if isinstance(item, dm.Node)) - - @property - def edge_results(self) -> Iterable[dm.Edge]: - return (item for item in self.results if isinstance(item, dm.Edge)) - - def update_expression_limit(self) -> None: - if self.is_unlimited: - self.expression.limit = self._max_retrieve_batch_limit - else: - self.expression.limit = max(min(INSTANCE_QUERY_LIMIT, self.max_retrieve_limit - self.total_retrieved), 0) - - def reduce_max_batch_limit(self) -> bool: - self._max_retrieve_batch_limit = max(1, self._max_retrieve_batch_limit // 2) - return self._max_retrieve_batch_limit > 1 - - @property - def is_unlimited(self) -> bool: - return self.max_retrieve_limit in {None, -1, math.inf} - - @property - def is_finished(self) -> bool: - return ( - (not self.is_unlimited and self.total_retrieved >= self.max_retrieve_limit) - or self.cursor is None - or self.last_batch_count == 0 - # Single direct relations are dependent on the parent node, - # so we assume that the parent node is the limiting factor. - or self.is_single_direct_relation - ) - - def count_total(self, cognite_client: CogniteClient) -> float: - if self.view_id is None: - raise ValueError("Cannot count total if select is not set") - - return cognite_client.data_modeling.instances.aggregate( - self.view_id, Count("externalId"), filter=self.raw_filter - ).value - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(name={self.name!r}, from={self.from_!r}, results={len(self.results)})" - - -class QueryBuilder(list, MutableSequence[QueryStep]): - """This is a helper class to build and execute a query. It is responsible for - doing the paging of the query and keeping track of the results.""" - - def __init__(self, steps: Collection[QueryStep] | None = None): - super().__init__(steps or []) - - def _reset(self): - for expression in self: - expression.total_retrieved = 0 - expression.cursor = None - expression.results = [] - - def _update_expression_limits(self) -> None: - for expression in self: - expression.update_expression_limit() - - def _build(self) -> tuple[dm.query.Query, list[QueryStep], set[str]]: - with_ = {step.name: step.expression for step in self if step.is_queryable} - select = {step.name: step.select for step in self if step.select is not None and step.is_queryable} - cursors = self._cursors - - step_by_name = {step.name: step for step in self} - search: list[QueryStep] = [] - temporary_select: set[str] = set() - for step in self: - if step.is_queryable: - continue - if step.node_expression is not None: - search.append(step) - # Ensure that select is set for the parent - if step.from_ in select or step.from_ is None: - continue - view_id = step_by_name[step.from_].view_id - if view_id is None: - continue - select[step.from_] = dm.query.Select([dm.query.SourceSelector(view_id, ["*"])]) - temporary_select.add(step.from_) - return dm.query.Query(with_=with_, select=select, cursors=cursors), search, temporary_select - - def _dump_yaml(self) -> str: - return self._build()[0].dump_yaml() - - @property - def _cursors(self) -> dict[str, str | None]: - return {expression.name: expression.cursor for expression in self if expression.is_queryable} - - def _update(self, batch: dm.query.QueryResult): - for expression in self: - if expression.name not in batch: - continue - expression.last_batch_count = len(batch[expression.name]) - expression.total_retrieved += expression.last_batch_count - expression.cursor = batch.cursors.get(expression.name) - expression.results.extend(batch[expression.name].data) - - @property - def _is_finished(self) -> bool: - return self[0].is_finished - - def _reduce_max_batch_limit(self) -> bool: - for expression in self: - if not expression.reduce_max_batch_limit(): - return False - return True - - def execute_query(self, client: CogniteClient, remove_not_connected: bool = False) -> dict[str, list[Instance]]: - self._reset() - query, to_search, temp_select = self._build() - - if not self: - raise ValueError("No query steps to execute") - - count: float | None = None - with suppress(ValueError, CogniteAPIError): - count = self[0].count_total(client) - - is_large_query = False - last_progress_print = 0 - nodes_per_second = 0.0 - while True: - self._update_expression_limits() - query.cursors = self._cursors - t0 = time.time() - try: - batch = client.data_modeling.instances.query(query) - except CogniteAPIError as e: - if e.code == 408: - # Too big query, try to reduce the limit - if self._reduce_max_batch_limit(): - continue - new_limit = self[0]._max_retrieve_batch_limit - warnings.warn( - f"Query is too large, reducing batch size to {new_limit:,}, and trying again", - QueryReducingBatchSize, - stacklevel=2, - ) - - raise e - - self._fetch_reverse_direct_relation_of_lists(client, to_search, batch) - - for name in temp_select: - batch.pop(name, None) - - last_execution_time = time.time() - t0 - - self._update(batch) - if self._is_finished: - break - - if count is None: - continue - # Estimate the number of nodes per second using exponential moving average - last_batch_nodes_per_second = len(batch[self[0].name]) / last_execution_time - if nodes_per_second == 0.0: - nodes_per_second = last_batch_nodes_per_second - else: - nodes_per_second = 0.1 * last_batch_nodes_per_second + 0.9 * nodes_per_second - # Estimate the time to completion - remaining_nodes = count - self[0].total_retrieved - remaining_time = remaining_nodes / nodes_per_second - - if is_large_query and (self[0].total_retrieved - last_progress_print) > PRINT_PROGRESS_PER_N_NODES: - estimate = datetime.timedelta(seconds=round(remaining_time, 0)) - print( - f"Progress: {self[0].total_retrieved:,}/{count:,} nodes retrieved. " - f"Estimated time to completion: {estimate}" - ) - last_progress_print = self[0].total_retrieved - - if is_large_query is False and remaining_time > MINIMUM_ESTIMATED_SECONDS_BEFORE_PRINT_PROGRESS: - is_large_query = True - print("Large query detected. Will print progress.") - - if remove_not_connected and len(self) > 1: - _QueryResultCleaner(self).clean() - - return {step.name: step.results for step in self} - - @staticmethod - def _fetch_reverse_direct_relation_of_lists( - client: CogniteClient, to_search: list[QueryStep], batch: dm.query.QueryResult - ) -> None: - """Reverse direct relations for lists are not supported by the query API. - This method fetches them separately.""" - for step in to_search: - if step.from_ is None or step.from_ not in batch: - continue - item_ids = [node.as_id() for node in batch[step.from_].data] - if not item_ids: - continue - - view_id = step.view_id - expression = step.node_expression - if view_id is None or expression is None: - raise ValueError( - "Invalid state of the query. Search should always be a node expression with view properties" - ) - if expression.through is None: - raise ValueError("Missing through set in a reverse-list query") - limit = SEARCH_LIMIT if step.is_unlimited else min(step.max_retrieve_limit, SEARCH_LIMIT) - - step_result = dm.NodeList[dm.Node]([]) - for item_ids_chunk in chunker(item_ids, IN_FILTER_CHUNK_SIZE): - is_items = dm.filters.In(view_id.as_property_ref(expression.through.property), item_ids_chunk) - is_selected = is_items if step.raw_filter is None else dm.filters.And(is_items, step.raw_filter) - - chunk_result = client.data_modeling.instances.search( - view_id, properties=None, filter=is_selected, limit=limit - ) - step_result.extend(chunk_result) - - batch[step.name] = dm.NodeListWithCursor(step_result, None) - return None - - def get_from(self) -> str | None: - if len(self) == 0: - return None - return self[-1].name - - def create_name(self, from_: str | None) -> str: - if from_ is None: - return "0" - return f"{from_}_{len(self)}" - - def append(self, __object: QueryStep, /) -> None: - # Extra validation to ensure all assumptions are met - if len(self) == 0: - if __object.from_ is not None: - raise ValueError("The first step should not have a 'from_' value") - else: - if __object.from_ is None: - raise ValueError("The 'from_' value should be set") - super().append(__object) - - def extend(self, __iterable: Iterable[QueryStep], /) -> None: - for item in __iterable: - self.append(item) - - # The implementations below are to get proper type hints - def __iter__(self) -> Iterator[QueryStep]: - return super().__iter__() - - @overload - def __getitem__(self, item: SupportsIndex) -> QueryStep: ... - - @overload - def __getitem__(self, item: slice) -> "QueryBuilder": ... - - def __getitem__(self, item: SupportsIndex | slice) -> "QueryStep | QueryBuilder": - value = super().__getitem__(item) - if isinstance(item, slice): - return QueryBuilder(value) # type: ignore[arg-type] - return cast(QueryStep, value) - - -class _QueryResultCleaner: - """Remove nodes and edges that are not connected through the entire query""" - - def __init__(self, steps: list[QueryStep]): - self._tree = self._create_tree(steps) - self._root = steps[0] - - @classmethod - def _create_tree(cls, steps: list[QueryStep]) -> dict[str, list[QueryStep]]: - tree: dict[str, list[QueryStep]] = defaultdict(list) - for step in steps: - if step.from_ is None: - continue - tree[step.from_].append(step) - return dict(tree) - - def clean(self) -> None: - self._clean(self._root) - - @staticmethod - def as_node_id(direct_relation: dm.DirectRelationReference | dict[str, str]) -> dm.NodeId: - if isinstance(direct_relation, dict): - return dm.NodeId(direct_relation["space"], direct_relation["externalId"]) - - return dm.NodeId(direct_relation.space, direct_relation.external_id) - - def _clean(self, step: QueryStep) -> tuple[set[dm.NodeId], str | None]: - if step.name not in self._tree: - # Leaf Node - direct_relation: str | None = None - if step.node_expression and (through := step.node_expression.through) is not None: - direct_relation = through.property - if step.node_expression.direction == "inwards": - return { - node_id for item in step.node_results for node_id in self._get_relations(item, direct_relation) - }, None - - return {item.as_id() for item in step.results}, direct_relation # type: ignore[attr-defined] - - expected_ids_by_property: dict[str | None, set[dm.NodeId]] = {} - for child in self._tree[step.name]: - child_ids, property_id = self._clean(child) - if property_id not in expected_ids_by_property: - expected_ids_by_property[property_id] = child_ids - else: - expected_ids_by_property[property_id] |= child_ids - - if step.node_expression is not None: - filtered_results: list[Instance] = [] - for node in step.node_results: - if self._is_connected_node(node, expected_ids_by_property): - filtered_results.append(node) - step.results = filtered_results - direct_relation = None if step.node_expression.through is None else step.node_expression.through.property - return {node.as_id() for node in step.node_results}, direct_relation - - if step.edge_expression: - if len(expected_ids_by_property) > 1 or None not in expected_ids_by_property: - raise RuntimeError(f"Invalid state of {type(self).__name__}") - expected_ids = expected_ids_by_property[None] - if step.edge_expression.direction == "outwards": - step.results = [edge for edge in step.edge_results if self.as_node_id(edge.end_node) in expected_ids] - return {self.as_node_id(edge.start_node) for edge in step.edge_results}, None - else: # inwards - step.results = [edge for edge in step.edge_results if self.as_node_id(edge.start_node) in expected_ids] - return {self.as_node_id(edge.end_node) for edge in step.edge_results}, None - - raise TypeError(f"Unsupported query step type: {type(step)}") - - @classmethod - def _is_connected_node(cls, node: dm.Node, expected_ids_by_property: dict[str | None, set[dm.NodeId]]) -> bool: - if not expected_ids_by_property: - return True - if None in expected_ids_by_property: - if node.as_id() in expected_ids_by_property[None]: - return True - if len(expected_ids_by_property) == 1: - return False - node_properties = next(iter(node.properties.values())) - for property_id, expected_ids in expected_ids_by_property.items(): - if property_id is None: - continue - value = node_properties.get(property_id) - if value is None: - continue - elif isinstance(value, list): - if {cls.as_node_id(item) for item in value if isinstance(item, dict)} & expected_ids: - return True - elif isinstance(value, dict) and cls.as_node_id(value) in expected_ids: - return True - return False - - @classmethod - def _get_relations(cls, node: dm.Node, property_id: str) -> Iterable[dm.NodeId]: - if property_id is None: - return {node.as_id()} - value = next(iter(node.properties.values())).get(property_id) - if isinstance(value, list): - return [cls.as_node_id(item) for item in value if isinstance(item, dict)] - elif isinstance(value, dict): - return [cls.as_node_id(value)] - return [] - - -class NodeQueryStep(QueryStep): - def __init__( - self, - name: str, - expression: dm.query.NodeResultSetExpression, - result_cls: type[DomainModel], - max_retrieve_limit: int = -1, - select: dm.query.Select | None | type[_NotSetSentinel] = _NotSetSentinel, - raw_filter: dm.Filter | None = None, - connection_type: Literal["reverse-list"] | None = None, - ): - self.result_cls = result_cls - super().__init__(name, expression, result_cls._view_id, max_retrieve_limit, select, raw_filter, connection_type) - - def unpack(self) -> dict[dm.NodeId | str, DomainModel]: - return { - ( - instance.as_id() if instance.space != DEFAULT_INSTANCE_SPACE else instance.external_id - ): self.result_cls.from_instance(instance) - for instance in cast(list[dm.Node], self.results) - } - - @property - def node_results(self) -> list[dm.Node]: - return cast(list[dm.Node], self.results) - - @property - def node_expression(self) -> dm.query.NodeResultSetExpression: - return cast(dm.query.NodeResultSetExpression, self.expression) - - -class EdgeQueryStep(QueryStep): - def __init__( - self, - name: str, - expression: dm.query.EdgeResultSetExpression, - result_cls: type[DomainRelation] | None = None, - max_retrieve_limit: int = -1, - select: dm.query.Select | None | type[_NotSetSentinel] = _NotSetSentinel, - raw_filter: dm.Filter | None = None, - ): - self.result_cls = result_cls - view_id = result_cls._view_id if result_cls is not None else None - super().__init__(name, expression, view_id, max_retrieve_limit, select, raw_filter, None) - - def unpack(self) -> dict[dm.NodeId, list[dm.Edge | DomainRelation]]: - output: dict[dm.NodeId, list[dm.Edge | DomainRelation]] = defaultdict(list) - for edge in cast(list[dm.Edge], self.results): - edge_source = edge.start_node if self.expression.direction == "outwards" else edge.end_node - value = self.result_cls.from_instance(edge) if self.result_cls is not None else edge - output[as_node_id(edge_source)].append(value) # type: ignore[arg-type] - return output - - @property - def edge_results(self) -> list[dm.Edge]: - return cast(list[dm.Edge], self.results) - - @property - def edge_expression(self) -> dm.query.EdgeResultSetExpression: - return cast(dm.query.EdgeResultSetExpression, self.expression) - - -class DataClassQueryBuilder(QueryBuilder, Generic[T_DomainModelList]): - """This is a helper class to build and execute a query. It is responsible for - doing the paging of the query and keeping track of the results.""" - - def __init__( - self, - result_cls: type[T_DomainModelList] | None, - steps: Collection[QueryStep] | None = None, - return_step: Literal["first", "last"] | None = None, - ): - super().__init__(steps or []) - self._result_list_cls = result_cls - self._return_step: Literal["first", "last"] | None = return_step - - def unpack(self) -> T_DomainModelList: - if self._result_list_cls is None: - raise ValueError("No result class set, unable to unpack results") - selected = [step for step in self if step.select is not None] - if len(selected) == 0: - return self._result_list_cls([]) - elif len(selected) == 1: - # Validated in the append method - if self._return_step == "first": - selected_step = cast(NodeQueryStep, self[0]) - elif self._return_step == "last": - selected_step = cast(NodeQueryStep, self[-1]) - else: - raise ValueError(f"Invalid return_step: {self._return_step}") - return self._result_list_cls(selected_step.unpack().values()) - # More than one step, we need to unpack the nodes and edges - nodes_by_from: dict[str | None, dict[dm.NodeId | str, DomainModel]] = defaultdict(dict) - edges_by_from: dict[str, dict[dm.NodeId, list[dm.Edge | DomainRelation]]] = defaultdict(dict) - for step in reversed(self): - # Validated in the append method - from_ = cast(str, step.from_) - if isinstance(step, EdgeQueryStep): - edges_by_from[from_].update(step.unpack()) - if step.name in nodes_by_from: - nodes_by_from[from_].update(nodes_by_from[step.name]) - del nodes_by_from[step.name] - elif isinstance(step, NodeQueryStep): - unpacked = step.unpack() - nodes_by_from[from_].update(unpacked) # type: ignore[arg-type] - if step.name in nodes_by_from or step.name in edges_by_from: - step.result_cls._update_connections( - unpacked, # type: ignore[arg-type] - nodes_by_from.get(step.name, {}), # type: ignore[arg-type] - edges_by_from.get(step.name, {}), - ) - if self._return_step == "first": - return self._result_list_cls(nodes_by_from[None].values()) - elif self._return_step == "last" and self[-1].from_ in nodes_by_from: - return self._result_list_cls(nodes_by_from[self[-1].from_].values()) - elif self._return_step == "last": - raise ValueError("Cannot return the last step when the last step is an edge query") - else: - raise ValueError(f"Invalid return_step: {self._return_step}") - - def append(self, __object: QueryStep, /) -> None: - # Extra validation to ensure all assumptions are met - if len(self) == 0: - if __object.from_ is not None: - raise ValueError("The first step should not have a 'from_' value") - if self._result_list_cls is None: - if self._return_step is None: - self._return_step = "first" - else: - if not isinstance(__object, NodeQueryStep): - raise ValueError("The first step should be a NodeQueryStep") - # If the first step is a NodeQueryStep, and matches the instance - # in the result_list_cls we can return the result from the first step - # Alternative is result_cls is not set, then we also assume that the first step - if self._return_step is None: - if __object.result_cls is self._result_list_cls._INSTANCE: - self._return_step = "first" - else: - # If not, we assume that the last step is the one we want to return - self._return_step = "last" - else: - if __object.from_ is None: - raise ValueError("The 'from_' value should be set") - super().append(__object) - - def extend(self, __iterable: Iterable[QueryStep], /) -> None: - for item in __iterable: - self.append(item) - - # The implementations below are to get proper type hints - def __iter__(self) -> Iterator[QueryStep]: - return super().__iter__() - - @overload - def __getitem__(self, item: SupportsIndex) -> QueryStep: ... - - @overload - def __getitem__(self, item: slice) -> DataClassQueryBuilder[T_DomainModelList]: ... - - def __getitem__(self, item: SupportsIndex | slice) -> QueryStep | DataClassQueryBuilder[T_DomainModelList]: - value = super().__getitem__(item) - if isinstance(item, slice): - return DataClassQueryBuilder(self._result_list_cls, value) # type: ignore[arg-type] - return cast(QueryStep, value) - - -T_QueryCore = TypeVar("T_QueryCore") - - -class Filtering(Generic[T_QueryCore], ABC): - def __init__(self, query: T_QueryCore, prop_path: list[str] | tuple[str, ...]): - self._query = query - self._prop_path = prop_path - self._filter: dm.Filter | None = None - - def _raise_if_filter_set(self): - if self._filter is not None: - raise ValueError("Filter has already been set") - - def _as_filter(self) -> dm.Filter | None: - return self._filter - - -class StringFilter(Filtering[T_QueryCore]): - def equals(self, value: str) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Equals(self._prop_path, value) - return self._query - - def prefix(self, prefix: str) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Prefix(self._prop_path, prefix) - return self._query - - def in_(self, values: list[str]) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.In(self._prop_path, values) - return self._query - - -class BooleanFilter(Filtering[T_QueryCore]): - def equals(self, value: bool) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Equals(self._prop_path, value) - return self._query - - -class IntFilter(Filtering[T_QueryCore]): - def range(self, gte: int | None, lte: int | None) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Range(self._prop_path, gte=gte, lte=lte) - return self._query - - -class FloatFilter(Filtering[T_QueryCore]): - def range(self, gte: float | None, lte: float | None) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Range(self._prop_path, gte=gte, lte=lte) - return self._query - - -class TimestampFilter(Filtering[T_QueryCore]): - def range(self, gte: datetime.datetime | None, lte: datetime.datetime | None) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Range( - self._prop_path, - gte=gte.isoformat(timespec="milliseconds") if gte else None, - lte=lte.isoformat(timespec="milliseconds") if lte else None, - ) - return self._query - - -class DateFilter(Filtering[T_QueryCore]): - def range(self, gte: datetime.date | None, lte: datetime.date | None) -> T_QueryCore: - self._raise_if_filter_set() - self._filter = dm.filters.Range( - self._prop_path, - gte=gte.isoformat() if gte else None, - lte=lte.isoformat() if lte else None, - ) - return self._query diff --git a/examples/equipment_unit/data_classes/_equipment_module.py b/examples/equipment_unit/data_classes/_equipment_module.py deleted file mode 100644 index 30b9a04bc..000000000 --- a/examples/equipment_unit/data_classes/_equipment_module.py +++ /dev/null @@ -1,396 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Sequence -from typing import Any, ClassVar, Literal, no_type_check, Optional, Union - -from cognite.client import data_modeling as dm, CogniteClient -from cognite.client.data_classes import ( - TimeSeries as CogniteTimeSeries, - TimeSeriesWrite as CogniteTimeSeriesWrite, -) -from pydantic import Field -from pydantic import field_validator, model_validator - -from equipment_unit.data_classes._core import ( - DEFAULT_INSTANCE_SPACE, - DEFAULT_QUERY_LIMIT, - DataRecord, - DataRecordGraphQL, - DataRecordWrite, - DomainModel, - DomainModelWrite, - DomainModelWriteList, - DomainModelList, - DomainRelation, - DomainRelationWrite, - GraphQLCore, - ResourcesWrite, - FileMetadata, - FileMetadataWrite, - FileMetadataGraphQL, - TimeSeries, - TimeSeriesWrite, - TimeSeriesGraphQL, - T_DomainModelList, - as_direct_relation_reference, - as_instance_dict_id, - as_node_id, - as_pygen_node_id, - are_nodes_equal, - is_tuple_id, - select_best_node, - QueryCore, - NodeQueryCore, - StringFilter, -) - - -__all__ = [ - "EquipmentModule", - "EquipmentModuleWrite", - "EquipmentModuleApply", - "EquipmentModuleList", - "EquipmentModuleWriteList", - "EquipmentModuleApplyList", - "EquipmentModuleFields", - "EquipmentModuleTextFields", - "EquipmentModuleGraphQL", -] - - -EquipmentModuleTextFields = Literal["external_id", "description", "name", "sensor_value", "type_"] -EquipmentModuleFields = Literal["external_id", "description", "name", "sensor_value", "type_"] - -_EQUIPMENTMODULE_PROPERTIES_BY_FIELD = { - "external_id": "externalId", - "description": "description", - "name": "name", - "sensor_value": "sensor_value", - "type_": "type", -} - - -class EquipmentModuleGraphQL(GraphQLCore): - """This represents the reading version of equipment module, used - when data is retrieved from CDF using GraphQL. - - It is used when retrieving data from CDF using GraphQL. - - Args: - space: The space where the node is located. - external_id: The external id of the equipment module. - data_record: The data record of the equipment module node. - description: The description field. - name: The name field. - sensor_value: The sensor value field. - type_: The type field. - """ - - view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "EquipmentModule", "b1cd4bf14a7a33") - description: Optional[str] = None - name: Optional[str] = None - sensor_value: Optional[TimeSeriesGraphQL] = None - type_: Optional[str] = Field(None, alias="type") - - @model_validator(mode="before") - def parse_data_record(cls, values: Any) -> Any: - if not isinstance(values, dict): - return values - if "lastUpdatedTime" in values or "createdTime" in values: - values["dataRecord"] = DataRecordGraphQL( - created_time=values.pop("createdTime", None), - last_updated_time=values.pop("lastUpdatedTime", None), - ) - return values - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_read(self) -> EquipmentModule: - """Convert this GraphQL format of equipment module to the reading format.""" - if self.data_record is None: - raise ValueError("This object cannot be converted to a read format because it lacks a data record.") - return EquipmentModule( - space=self.space, - external_id=self.external_id, - data_record=DataRecord( - version=0, - last_updated_time=self.data_record.last_updated_time, - created_time=self.data_record.created_time, - ), - description=self.description, - name=self.name, - sensor_value=self.sensor_value.as_read() if self.sensor_value else None, - type_=self.type_, - ) - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_write(self) -> EquipmentModuleWrite: - """Convert this GraphQL format of equipment module to the writing format.""" - return EquipmentModuleWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=0), - description=self.description, - name=self.name, - sensor_value=self.sensor_value.as_write() if self.sensor_value else None, - type_=self.type_, - ) - - -class EquipmentModule(DomainModel): - """This represents the reading version of equipment module. - - It is used to when data is retrieved from CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the equipment module. - data_record: The data record of the equipment module node. - description: The description field. - name: The name field. - sensor_value: The sensor value field. - type_: The type field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "EquipmentModule", "b1cd4bf14a7a33") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, None] = None - description: Optional[str] = None - name: Optional[str] = None - sensor_value: Union[TimeSeries, str, None] = None - type_: Optional[str] = Field(None, alias="type") - - def as_write(self) -> EquipmentModuleWrite: - """Convert this read version of equipment module to the writing version.""" - return EquipmentModuleWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=self.data_record.version), - description=self.description, - name=self.name, - sensor_value=( - self.sensor_value.as_write() if isinstance(self.sensor_value, CogniteTimeSeries) else self.sensor_value - ), - type_=self.type_, - ) - - def as_apply(self) -> EquipmentModuleWrite: - """Convert this read version of equipment module to the writing version.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class EquipmentModuleWrite(DomainModelWrite): - """This represents the writing version of equipment module. - - It is used to when data is sent to CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the equipment module. - data_record: The data record of the equipment module node. - description: The description field. - name: The name field. - sensor_value: The sensor value field. - type_: The type field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "EquipmentModule", "b1cd4bf14a7a33") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, dm.NodeId, tuple[str, str], None] = None - description: Optional[str] = None - name: Optional[str] = None - sensor_value: Union[TimeSeriesWrite, str, None] = None - type_: Optional[str] = Field(None, alias="type") - - def _to_instances_write( - self, - cache: set[tuple[str, str]], - write_none: bool = False, - allow_version_increase: bool = False, - ) -> ResourcesWrite: - resources = ResourcesWrite() - if self.as_tuple_id() in cache: - return resources - - properties: dict[str, Any] = {} - - if self.description is not None or write_none: - properties["description"] = self.description - - if self.name is not None or write_none: - properties["name"] = self.name - - if self.sensor_value is not None or write_none: - properties["sensor_value"] = ( - self.sensor_value - if isinstance(self.sensor_value, str) or self.sensor_value is None - else self.sensor_value.external_id - ) - - if self.type_ is not None or write_none: - properties["type"] = self.type_ - - if properties: - this_node = dm.NodeApply( - space=self.space, - external_id=self.external_id, - existing_version=None if allow_version_increase else self.data_record.existing_version, - type=as_direct_relation_reference(self.node_type), - sources=[ - dm.NodeOrEdgeData( - source=self._view_id, - properties=properties, - ) - ], - ) - resources.nodes.append(this_node) - cache.add(self.as_tuple_id()) - - if isinstance(self.sensor_value, CogniteTimeSeriesWrite): - resources.time_series.append(self.sensor_value) - - return resources - - -class EquipmentModuleApply(EquipmentModuleWrite): - def __new__(cls, *args, **kwargs) -> EquipmentModuleApply: - warnings.warn( - "EquipmentModuleApply is deprecated and will be removed in v1.0. Use EquipmentModuleWrite instead." - "The motivation for this change is that Write is a more descriptive name for the writing version of the" - "EquipmentModule.", - UserWarning, - stacklevel=2, - ) - return super().__new__(cls) - - -class EquipmentModuleList(DomainModelList[EquipmentModule]): - """List of equipment modules in the read version.""" - - _INSTANCE = EquipmentModule - - def as_write(self) -> EquipmentModuleWriteList: - """Convert these read versions of equipment module to the writing versions.""" - return EquipmentModuleWriteList([node.as_write() for node in self.data]) - - def as_apply(self) -> EquipmentModuleWriteList: - """Convert these read versions of primitive nullable to the writing versions.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class EquipmentModuleWriteList(DomainModelWriteList[EquipmentModuleWrite]): - """List of equipment modules in the writing version.""" - - _INSTANCE = EquipmentModuleWrite - - -class EquipmentModuleApplyList(EquipmentModuleWriteList): ... - - -def _create_equipment_module_filter( - view_id: dm.ViewId, - description: str | list[str] | None = None, - description_prefix: str | None = None, - name: str | list[str] | None = None, - name_prefix: str | None = None, - type_: str | list[str] | None = None, - type_prefix: str | None = None, - external_id_prefix: str | None = None, - space: str | list[str] | None = None, - filter: dm.Filter | None = None, -) -> dm.Filter | None: - filters: list[dm.Filter] = [] - if isinstance(description, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("description"), value=description)) - if description and isinstance(description, list): - filters.append(dm.filters.In(view_id.as_property_ref("description"), values=description)) - if description_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("description"), value=description_prefix)) - if isinstance(name, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("name"), value=name)) - if name and isinstance(name, list): - filters.append(dm.filters.In(view_id.as_property_ref("name"), values=name)) - if name_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("name"), value=name_prefix)) - if isinstance(type_, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("type"), value=type_)) - if type_ and isinstance(type_, list): - filters.append(dm.filters.In(view_id.as_property_ref("type"), values=type_)) - if type_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("type"), value=type_prefix)) - if external_id_prefix is not None: - filters.append(dm.filters.Prefix(["node", "externalId"], value=external_id_prefix)) - if isinstance(space, str): - filters.append(dm.filters.Equals(["node", "space"], value=space)) - if space and isinstance(space, list): - filters.append(dm.filters.In(["node", "space"], values=space)) - if filter: - filters.append(filter) - return dm.filters.And(*filters) if filters else None - - -class _EquipmentModuleQuery(NodeQueryCore[T_DomainModelList, EquipmentModuleList]): - _view_id = EquipmentModule._view_id - _result_cls = EquipmentModule - _result_list_cls_end = EquipmentModuleList - - def __init__( - self, - created_types: set[type], - creation_path: list[QueryCore], - client: CogniteClient, - result_list_cls: type[T_DomainModelList], - expression: dm.query.ResultSetExpression | None = None, - connection_name: str | None = None, - connection_type: Literal["reverse-list"] | None = None, - reverse_expression: dm.query.ResultSetExpression | None = None, - ): - - super().__init__( - created_types, - creation_path, - client, - result_list_cls, - expression, - dm.filters.HasData(views=[self._view_id]), - connection_name, - connection_type, - reverse_expression, - ) - - self.space = StringFilter(self, ["node", "space"]) - self.external_id = StringFilter(self, ["node", "externalId"]) - self.description = StringFilter(self, self._view_id.as_property_ref("description")) - self.name = StringFilter(self, self._view_id.as_property_ref("name")) - self.type_ = StringFilter(self, self._view_id.as_property_ref("type")) - self._filter_classes.extend( - [ - self.space, - self.external_id, - self.description, - self.name, - self.type_, - ] - ) - - def list_equipment_module(self, limit: int = DEFAULT_QUERY_LIMIT) -> EquipmentModuleList: - return self._list(limit=limit) - - -class EquipmentModuleQuery(_EquipmentModuleQuery[EquipmentModuleList]): - def __init__(self, client: CogniteClient): - super().__init__(set(), [], client, EquipmentModuleList) diff --git a/examples/equipment_unit/data_classes/_start_end_time.py b/examples/equipment_unit/data_classes/_start_end_time.py deleted file mode 100644 index 80e9df41e..000000000 --- a/examples/equipment_unit/data_classes/_start_end_time.py +++ /dev/null @@ -1,428 +0,0 @@ -from __future__ import annotations - -import datetime -import warnings -from typing import Any, ClassVar, Literal, no_type_check, Optional, TYPE_CHECKING, Union - -from cognite.client import data_modeling as dm, CogniteClient - -from equipment_unit.data_classes._core import ( - DEFAULT_INSTANCE_SPACE, - DataRecord, - DataRecordWrite, - DomainModel, - DomainModelCore, - DomainModelWrite, - DomainRelation, - DomainRelationWrite, - DomainRelationList, - DomainRelationWriteList, - GraphQLCore, - ResourcesWrite, - DomainModelList, - T_DomainList, - EdgeQueryCore, - NodeQueryCore, - QueryCore, - StringFilter, - TimestampFilter, -) -from equipment_unit.data_classes._unit_procedure import UnitProcedureWrite -from equipment_unit.data_classes._equipment_module import EquipmentModule, EquipmentModuleGraphQL, EquipmentModuleWrite -from equipment_unit.data_classes._work_order import WorkOrder, WorkOrderGraphQL, WorkOrderWrite - -if TYPE_CHECKING: - from equipment_unit.data_classes._equipment_module import ( - EquipmentModule, - EquipmentModuleGraphQL, - EquipmentModuleWrite, - ) - from equipment_unit.data_classes._work_order import WorkOrder, WorkOrderGraphQL, WorkOrderWrite - - -__all__ = [ - "StartEndTime", - "StartEndTimeWrite", - "StartEndTimeApply", - "StartEndTimeList", - "StartEndTimeWriteList", - "StartEndTimeApplyList", - "StartEndTimeFields", -] - - -StartEndTimeTextFields = Literal["external_id",] -StartEndTimeFields = Literal["external_id", "end_time", "start_time"] -_STARTENDTIME_PROPERTIES_BY_FIELD = { - "external_id": "externalId", - "end_time": "end_time", - "start_time": "start_time", -} - - -class StartEndTimeGraphQL(GraphQLCore): - """This represents the reading version of start end time, used - when data is retrieved from CDF using GraphQL. - - It is used when retrieving data from CDF using GraphQL. - - Args: - space: The space where the node is located. - external_id: The external id of the start end time. - data_record: The data record of the start end time node. - end_node: The end node of this edge. - end_time: The end time field. - start_time: The start time field. - """ - - view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "StartEndTime", "d416e0ed98186b") - end_node: Union[EquipmentModuleGraphQL, WorkOrderGraphQL, None] = None - end_time: Optional[datetime.datetime] = None - start_time: Optional[datetime.datetime] = None - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_read(self) -> StartEndTime: - """Convert this GraphQL format of start end time to the reading format.""" - if self.data_record is None: - raise ValueError("This object cannot be converted to a read format because it lacks a data record.") - return StartEndTime( - space=self.space, - external_id=self.external_id, - data_record=DataRecord( - version=0, - last_updated_time=self.data_record.last_updated_time, - created_time=self.data_record.created_time, - ), - end_node=self.end_node.as_read() if isinstance(self.end_node, GraphQLCore) else self.end_node, - end_time=self.end_time, - start_time=self.start_time, - ) - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_write(self) -> StartEndTimeWrite: - """Convert this GraphQL format of start end time to the writing format.""" - return StartEndTimeWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=0), - end_node=self.end_node.as_write() if isinstance(self.end_node, DomainModel) else self.end_node, - end_time=self.end_time, - start_time=self.start_time, - ) - - -class StartEndTime(DomainRelation): - """This represents the reading version of start end time. - - It is used to when data is retrieved from CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the start end time. - data_record: The data record of the start end time edge. - end_node: The end node of this edge. - end_time: The end time field. - start_time: The start time field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "StartEndTime", "d416e0ed98186b") - space: str = DEFAULT_INSTANCE_SPACE - end_node: Union[EquipmentModule, WorkOrder, str, dm.NodeId] - end_time: Optional[datetime.datetime] = None - start_time: Optional[datetime.datetime] = None - - def as_write(self) -> StartEndTimeWrite: - """Convert this read version of start end time to the writing version.""" - return StartEndTimeWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=self.data_record.version), - end_node=self.end_node.as_write() if isinstance(self.end_node, DomainModel) else self.end_node, - end_time=self.end_time, - start_time=self.start_time, - ) - - def as_apply(self) -> StartEndTimeWrite: - """Convert this read version of start end time to the writing version.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class StartEndTimeWrite(DomainRelationWrite): - """This represents the writing version of start end time. - - It is used to when data is sent to CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the start end time. - data_record: The data record of the start end time edge. - end_node: The end node of this edge. - end_time: The end time field. - start_time: The start time field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "StartEndTime", "d416e0ed98186b") - space: str = DEFAULT_INSTANCE_SPACE - end_node: Union[EquipmentModuleWrite, WorkOrderWrite, str, dm.NodeId] - end_time: Optional[datetime.datetime] = None - start_time: Optional[datetime.datetime] = None - - def _to_instances_write( - self, - cache: set[tuple[str, str]], - start_node: DomainModelWrite, - edge_type: dm.DirectRelationReference, - write_none: bool = False, - allow_version_increase: bool = False, - ) -> ResourcesWrite: - resources = ResourcesWrite() - if self.external_id and (self.space, self.external_id) in cache: - return resources - - _validate_end_node(start_node, self.end_node) - - if isinstance(self.end_node, DomainModelWrite): - end_node = self.end_node.as_direct_reference() - elif isinstance(self.end_node, str): - end_node = dm.DirectRelationReference(self.space, self.end_node) - elif isinstance(self.end_node, dm.NodeId): - end_node = dm.DirectRelationReference(self.end_node.space, self.end_node.external_id) - else: - raise ValueError(f"Invalid type for equipment_module: {type(self.end_node)}") - - external_id = self.external_id or DomainRelationWrite.external_id_factory(start_node, self.end_node, edge_type) - - properties: dict[str, Any] = {} - - if self.end_time is not None or write_none: - properties["end_time"] = self.end_time.isoformat(timespec="milliseconds") if self.end_time else None - - if self.start_time is not None or write_none: - properties["start_time"] = self.start_time.isoformat(timespec="milliseconds") if self.start_time else None - - if properties: - this_edge = dm.EdgeApply( - space=self.space, - external_id=external_id, - type=edge_type, - start_node=start_node.as_direct_reference(), - end_node=end_node, - existing_version=None if allow_version_increase else self.data_record.existing_version, - sources=[ - dm.NodeOrEdgeData( - source=self._view_id, - properties=properties, - ) - ], - ) - resources.edges.append(this_edge) - cache.add((self.space, external_id)) - - if isinstance(self.end_node, DomainModelWrite): - other_resources = self.end_node._to_instances_write(cache) - resources.extend(other_resources) - - return resources - - -class StartEndTimeApply(StartEndTimeWrite): - def __new__(cls, *args, **kwargs) -> StartEndTimeApply: - warnings.warn( - "StartEndTimeApply is deprecated and will be removed in v1.0. Use StartEndTimeWrite instead." - "The motivation for this change is that Write is a more descriptive name for the writing version of the" - "StartEndTime.", - UserWarning, - stacklevel=2, - ) - return super().__new__(cls) - - -class StartEndTimeList(DomainRelationList[StartEndTime]): - """List of start end times in the reading version.""" - - _INSTANCE = StartEndTime - - def as_write(self) -> StartEndTimeWriteList: - """Convert this read version of start end time list to the writing version.""" - return StartEndTimeWriteList([edge.as_write() for edge in self]) - - def as_apply(self) -> StartEndTimeWriteList: - """Convert these read versions of start end time list to the writing versions.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class StartEndTimeWriteList(DomainRelationWriteList[StartEndTimeWrite]): - """List of start end times in the writing version.""" - - _INSTANCE = StartEndTimeWrite - - -class StartEndTimeApplyList(StartEndTimeWriteList): ... - - -def _create_start_end_time_filter( - edge_type: dm.DirectRelationReference, - view_id: dm.ViewId, - start_node: str | list[str] | dm.NodeId | list[dm.NodeId] | None = None, - start_node_space: str = DEFAULT_INSTANCE_SPACE, - end_node: str | list[str] | dm.NodeId | list[dm.NodeId] | None = None, - space_end_node: str = DEFAULT_INSTANCE_SPACE, - min_end_time: datetime.datetime | None = None, - max_end_time: datetime.datetime | None = None, - min_start_time: datetime.datetime | None = None, - max_start_time: datetime.datetime | None = None, - external_id_prefix: str | None = None, - space: str | list[str] | None = None, - filter: dm.Filter | None = None, -) -> dm.Filter: - filters: list[dm.Filter] = [ - dm.filters.Equals( - ["edge", "type"], - {"space": edge_type.space, "externalId": edge_type.external_id}, - ) - ] - if start_node and isinstance(start_node, str): - filters.append( - dm.filters.Equals(["edge", "startNode"], value={"space": start_node_space, "externalId": start_node}) - ) - if start_node and isinstance(start_node, dm.NodeId): - filters.append( - dm.filters.Equals( - ["edge", "startNode"], value=start_node.dump(camel_case=True, include_instance_type=False) - ) - ) - if start_node and isinstance(start_node, list): - filters.append( - dm.filters.In( - ["edge", "startNode"], - values=[ - ( - {"space": start_node_space, "externalId": ext_id} - if isinstance(ext_id, str) - else ext_id.dump(camel_case=True, include_instance_type=False) - ) - for ext_id in start_node - ], - ) - ) - if end_node and isinstance(end_node, str): - filters.append(dm.filters.Equals(["edge", "endNode"], value={"space": space_end_node, "externalId": end_node})) - if end_node and isinstance(end_node, dm.NodeId): - filters.append( - dm.filters.Equals(["edge", "endNode"], value=end_node.dump(camel_case=True, include_instance_type=False)) - ) - if end_node and isinstance(end_node, list): - filters.append( - dm.filters.In( - ["edge", "endNode"], - values=[ - ( - {"space": space_end_node, "externalId": ext_id} - if isinstance(ext_id, str) - else ext_id.dump(camel_case=True, include_instance_type=False) - ) - for ext_id in end_node - ], - ) - ) - if min_end_time is not None or max_end_time is not None: - filters.append( - dm.filters.Range( - view_id.as_property_ref("end_time"), - gte=min_end_time.isoformat(timespec="milliseconds") if min_end_time else None, - lte=max_end_time.isoformat(timespec="milliseconds") if max_end_time else None, - ) - ) - if min_start_time is not None or max_start_time is not None: - filters.append( - dm.filters.Range( - view_id.as_property_ref("start_time"), - gte=min_start_time.isoformat(timespec="milliseconds") if min_start_time else None, - lte=max_start_time.isoformat(timespec="milliseconds") if max_start_time else None, - ) - ) - if external_id_prefix is not None: - filters.append(dm.filters.Prefix(["edge", "externalId"], value=external_id_prefix)) - if isinstance(space, str): - filters.append(dm.filters.Equals(["edge", "space"], value=space)) - if space and isinstance(space, list): - filters.append(dm.filters.In(["edge", "space"], values=space)) - if filter: - filters.append(filter) - return dm.filters.And(*filters) - - -_EXPECTED_START_NODES_BY_END_NODE: dict[type[DomainModelWrite], set[type[DomainModelWrite]]] = { - EquipmentModuleWrite: {UnitProcedureWrite}, - WorkOrderWrite: {UnitProcedureWrite}, -} - - -def _validate_end_node( - start_node: DomainModelWrite, end_node: Union[EquipmentModuleWrite, WorkOrderWrite, str, dm.NodeId] -) -> None: - if isinstance(end_node, (str, dm.NodeId)): - # Nothing to validate - return - if type(end_node) not in _EXPECTED_START_NODES_BY_END_NODE: - raise ValueError( - f"Invalid end node type: {type(end_node)}. Should be one of {[t.__name__ for t in _EXPECTED_START_NODES_BY_END_NODE.keys()]}" - ) - if type(start_node) not in _EXPECTED_START_NODES_BY_END_NODE[type(end_node)]: - raise ValueError( - f"Invalid end node type: {type(end_node)}. Expected one of: {_EXPECTED_START_NODES_BY_END_NODE[type(end_node)]}" - ) - - -class _StartEndTimeQuery(EdgeQueryCore[T_DomainList, StartEndTimeList]): - _view_id = StartEndTime._view_id - _result_cls = StartEndTime - _result_list_cls_end = StartEndTimeList - - def __init__( - self, - created_types: set[type], - creation_path: list[QueryCore], - client: CogniteClient, - result_list_cls: type[T_DomainList], - end_node_cls: type[NodeQueryCore], - expression: dm.query.ResultSetExpression | None = None, - connection_name: str | None = None, - ): - from ._equipment_module import _EquipmentModuleQuery - from ._work_order import _WorkOrderQuery - - super().__init__(created_types, creation_path, client, result_list_cls, expression, None, connection_name) - if end_node_cls not in created_types: - self.end_node = end_node_cls( - created_types=created_types.copy(), - creation_path=self._creation_path, - client=client, - result_list_cls=result_list_cls, # type: ignore[type-var] - expression=dm.query.NodeResultSetExpression(), - ) - - self.space = StringFilter(self, ["node", "space"]) - self.external_id = StringFilter(self, ["node", "externalId"]) - self.end_time = TimestampFilter(self, self._view_id.as_property_ref("end_time")) - self.start_time = TimestampFilter(self, self._view_id.as_property_ref("start_time")) - self._filter_classes.extend( - [ - self.space, - self.external_id, - self.end_time, - self.start_time, - ] - ) diff --git a/examples/equipment_unit/data_classes/_unit_procedure.py b/examples/equipment_unit/data_classes/_unit_procedure.py deleted file mode 100644 index 975a40de0..000000000 --- a/examples/equipment_unit/data_classes/_unit_procedure.py +++ /dev/null @@ -1,518 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, ClassVar, Literal, no_type_check, Optional, Union - -from cognite.client import data_modeling as dm, CogniteClient -from pydantic import Field -from pydantic import field_validator, model_validator - -from equipment_unit.data_classes._core import ( - DEFAULT_INSTANCE_SPACE, - DEFAULT_QUERY_LIMIT, - DataRecord, - DataRecordGraphQL, - DataRecordWrite, - DomainModel, - DomainModelWrite, - DomainModelWriteList, - DomainModelList, - DomainRelation, - DomainRelationWrite, - GraphQLCore, - ResourcesWrite, - T_DomainModelList, - as_direct_relation_reference, - as_instance_dict_id, - as_node_id, - as_pygen_node_id, - are_nodes_equal, - is_tuple_id, - select_best_node, - QueryCore, - NodeQueryCore, - StringFilter, -) - -if TYPE_CHECKING: - from equipment_unit.data_classes._start_end_time import ( - StartEndTime, - StartEndTimeList, - StartEndTimeGraphQL, - StartEndTimeWrite, - StartEndTimeWriteList, - ) - - -__all__ = [ - "UnitProcedure", - "UnitProcedureWrite", - "UnitProcedureApply", - "UnitProcedureList", - "UnitProcedureWriteList", - "UnitProcedureApplyList", - "UnitProcedureFields", - "UnitProcedureTextFields", - "UnitProcedureGraphQL", -] - - -UnitProcedureTextFields = Literal["external_id", "name", "type_"] -UnitProcedureFields = Literal["external_id", "name", "type_"] - -_UNITPROCEDURE_PROPERTIES_BY_FIELD = { - "external_id": "externalId", - "name": "name", - "type_": "type", -} - - -class UnitProcedureGraphQL(GraphQLCore): - """This represents the reading version of unit procedure, used - when data is retrieved from CDF using GraphQL. - - It is used when retrieving data from CDF using GraphQL. - - Args: - space: The space where the node is located. - external_id: The external id of the unit procedure. - data_record: The data record of the unit procedure node. - name: The name field. - type_: The type field. - work_orders: The work order field. - work_units: The work unit field. - """ - - view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "UnitProcedure", "a6e2fea1e1c664") - name: Optional[str] = None - type_: Optional[str] = Field(None, alias="type") - work_orders: Optional[list[StartEndTimeGraphQL]] = Field(default=None, repr=False) - work_units: Optional[list[StartEndTimeGraphQL]] = Field(default=None, repr=False) - - @model_validator(mode="before") - def parse_data_record(cls, values: Any) -> Any: - if not isinstance(values, dict): - return values - if "lastUpdatedTime" in values or "createdTime" in values: - values["dataRecord"] = DataRecordGraphQL( - created_time=values.pop("createdTime", None), - last_updated_time=values.pop("lastUpdatedTime", None), - ) - return values - - @field_validator("work_orders", "work_units", mode="before") - def parse_graphql(cls, value: Any) -> Any: - if not isinstance(value, dict): - return value - if "items" in value: - return value["items"] - return value - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_read(self) -> UnitProcedure: - """Convert this GraphQL format of unit procedure to the reading format.""" - if self.data_record is None: - raise ValueError("This object cannot be converted to a read format because it lacks a data record.") - return UnitProcedure( - space=self.space, - external_id=self.external_id, - data_record=DataRecord( - version=0, - last_updated_time=self.data_record.last_updated_time, - created_time=self.data_record.created_time, - ), - name=self.name, - type_=self.type_, - work_orders=[work_order.as_read() for work_order in self.work_orders or []], - work_units=[work_unit.as_read() for work_unit in self.work_units or []], - ) - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_write(self) -> UnitProcedureWrite: - """Convert this GraphQL format of unit procedure to the writing format.""" - return UnitProcedureWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=0), - name=self.name, - type_=self.type_, - work_orders=[work_order.as_write() for work_order in self.work_orders or []], - work_units=[work_unit.as_write() for work_unit in self.work_units or []], - ) - - -class UnitProcedure(DomainModel): - """This represents the reading version of unit procedure. - - It is used to when data is retrieved from CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the unit procedure. - data_record: The data record of the unit procedure node. - name: The name field. - type_: The type field. - work_orders: The work order field. - work_units: The work unit field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "UnitProcedure", "a6e2fea1e1c664") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, None] = None - name: Optional[str] = None - type_: Optional[str] = Field(None, alias="type") - work_orders: Optional[list[StartEndTime]] = Field(default=None, repr=False) - work_units: Optional[list[StartEndTime]] = Field(default=None, repr=False) - - def as_write(self) -> UnitProcedureWrite: - """Convert this read version of unit procedure to the writing version.""" - return UnitProcedureWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=self.data_record.version), - name=self.name, - type_=self.type_, - work_orders=[work_order.as_write() for work_order in self.work_orders or []], - work_units=[work_unit.as_write() for work_unit in self.work_units or []], - ) - - def as_apply(self) -> UnitProcedureWrite: - """Convert this read version of unit procedure to the writing version.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - @classmethod - def _update_connections( - cls, - instances: dict[dm.NodeId | str, UnitProcedure], # type: ignore[override] - nodes_by_id: dict[dm.NodeId | str, DomainModel], - edges_by_source_node: dict[dm.NodeId, list[dm.Edge | DomainRelation]], - ) -> None: - from ._start_end_time import StartEndTime - - for instance in instances.values(): - if edges := edges_by_source_node.get(instance.as_id()): - work_orders: list[StartEndTime] = [] - work_units: list[StartEndTime] = [] - for edge in edges: - value: DomainModel | DomainRelation | str | dm.NodeId - if isinstance(edge, DomainRelation): - value = edge - else: - other_end: dm.DirectRelationReference = ( - edge.end_node - if edge.start_node.space == instance.space - and edge.start_node.external_id == instance.external_id - else edge.start_node - ) - destination: dm.NodeId | str = ( - as_node_id(other_end) - if other_end.space != DEFAULT_INSTANCE_SPACE - else other_end.external_id - ) - if destination in nodes_by_id: - value = nodes_by_id[destination] - else: - value = destination - edge_type = edge.edge_type if isinstance(edge, DomainRelation) else edge.type - - if edge_type == dm.DirectRelationReference( - "IntegrationTestsImmutable", "UnitProcedure.work_order" - ) and isinstance(value, StartEndTime): - work_orders.append(value) - if end_node := nodes_by_id.get(as_pygen_node_id(value.end_node)): - value.end_node = end_node # type: ignore[assignment] - if edge_type == dm.DirectRelationReference( - "IntegrationTestsImmutable", "UnitProcedure.equipment_module" - ) and isinstance(value, StartEndTime): - work_units.append(value) - if end_node := nodes_by_id.get(as_pygen_node_id(value.end_node)): - value.end_node = end_node # type: ignore[assignment] - - instance.work_orders = work_orders - instance.work_units = work_units - - -class UnitProcedureWrite(DomainModelWrite): - """This represents the writing version of unit procedure. - - It is used to when data is sent to CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the unit procedure. - data_record: The data record of the unit procedure node. - name: The name field. - type_: The type field. - work_orders: The work order field. - work_units: The work unit field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "UnitProcedure", "a6e2fea1e1c664") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, dm.NodeId, tuple[str, str], None] = None - name: Optional[str] = None - type_: Optional[str] = Field(None, alias="type") - work_orders: Optional[list[StartEndTimeWrite]] = Field(default=None, repr=False) - work_units: Optional[list[StartEndTimeWrite]] = Field(default=None, repr=False) - - @field_validator("work_orders", "work_units", mode="before") - def as_node_id(cls, value: Any) -> Any: - if isinstance(value, dm.DirectRelationReference): - return dm.NodeId(value.space, value.external_id) - elif isinstance(value, tuple) and len(value) == 2 and all(isinstance(item, str) for item in value): - return dm.NodeId(value[0], value[1]) - elif isinstance(value, list): - return [cls.as_node_id(item) for item in value] - return value - - def _to_instances_write( - self, - cache: set[tuple[str, str]], - write_none: bool = False, - allow_version_increase: bool = False, - ) -> ResourcesWrite: - resources = ResourcesWrite() - if self.as_tuple_id() in cache: - return resources - - properties: dict[str, Any] = {} - - if self.name is not None or write_none: - properties["name"] = self.name - - if self.type_ is not None or write_none: - properties["type"] = self.type_ - - if properties: - this_node = dm.NodeApply( - space=self.space, - external_id=self.external_id, - existing_version=None if allow_version_increase else self.data_record.existing_version, - type=as_direct_relation_reference(self.node_type), - sources=[ - dm.NodeOrEdgeData( - source=self._view_id, - properties=properties, - ) - ], - ) - resources.nodes.append(this_node) - cache.add(self.as_tuple_id()) - - for work_order in self.work_orders or []: - if isinstance(work_order, DomainRelationWrite): - other_resources = work_order._to_instances_write( - cache, - self, - dm.DirectRelationReference("IntegrationTestsImmutable", "UnitProcedure.work_order"), - ) - resources.extend(other_resources) - - for work_unit in self.work_units or []: - if isinstance(work_unit, DomainRelationWrite): - other_resources = work_unit._to_instances_write( - cache, - self, - dm.DirectRelationReference("IntegrationTestsImmutable", "UnitProcedure.equipment_module"), - ) - resources.extend(other_resources) - - return resources - - -class UnitProcedureApply(UnitProcedureWrite): - def __new__(cls, *args, **kwargs) -> UnitProcedureApply: - warnings.warn( - "UnitProcedureApply is deprecated and will be removed in v1.0. Use UnitProcedureWrite instead." - "The motivation for this change is that Write is a more descriptive name for the writing version of the" - "UnitProcedure.", - UserWarning, - stacklevel=2, - ) - return super().__new__(cls) - - -class UnitProcedureList(DomainModelList[UnitProcedure]): - """List of unit procedures in the read version.""" - - _INSTANCE = UnitProcedure - - def as_write(self) -> UnitProcedureWriteList: - """Convert these read versions of unit procedure to the writing versions.""" - return UnitProcedureWriteList([node.as_write() for node in self.data]) - - def as_apply(self) -> UnitProcedureWriteList: - """Convert these read versions of primitive nullable to the writing versions.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - @property - def work_orders(self) -> StartEndTimeList: - from ._start_end_time import StartEndTime, StartEndTimeList - - return StartEndTimeList( - [item for items in self.data for item in items.work_orders or [] if isinstance(item, StartEndTime)] - ) - - @property - def work_units(self) -> StartEndTimeList: - from ._start_end_time import StartEndTime, StartEndTimeList - - return StartEndTimeList( - [item for items in self.data for item in items.work_units or [] if isinstance(item, StartEndTime)] - ) - - -class UnitProcedureWriteList(DomainModelWriteList[UnitProcedureWrite]): - """List of unit procedures in the writing version.""" - - _INSTANCE = UnitProcedureWrite - - @property - def work_orders(self) -> StartEndTimeWriteList: - from ._start_end_time import StartEndTimeWrite, StartEndTimeWriteList - - return StartEndTimeWriteList( - [item for items in self.data for item in items.work_orders or [] if isinstance(item, StartEndTimeWrite)] - ) - - @property - def work_units(self) -> StartEndTimeWriteList: - from ._start_end_time import StartEndTimeWrite, StartEndTimeWriteList - - return StartEndTimeWriteList( - [item for items in self.data for item in items.work_units or [] if isinstance(item, StartEndTimeWrite)] - ) - - -class UnitProcedureApplyList(UnitProcedureWriteList): ... - - -def _create_unit_procedure_filter( - view_id: dm.ViewId, - name: str | list[str] | None = None, - name_prefix: str | None = None, - type_: str | list[str] | None = None, - type_prefix: str | None = None, - external_id_prefix: str | None = None, - space: str | list[str] | None = None, - filter: dm.Filter | None = None, -) -> dm.Filter | None: - filters: list[dm.Filter] = [] - if isinstance(name, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("name"), value=name)) - if name and isinstance(name, list): - filters.append(dm.filters.In(view_id.as_property_ref("name"), values=name)) - if name_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("name"), value=name_prefix)) - if isinstance(type_, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("type"), value=type_)) - if type_ and isinstance(type_, list): - filters.append(dm.filters.In(view_id.as_property_ref("type"), values=type_)) - if type_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("type"), value=type_prefix)) - if external_id_prefix is not None: - filters.append(dm.filters.Prefix(["node", "externalId"], value=external_id_prefix)) - if isinstance(space, str): - filters.append(dm.filters.Equals(["node", "space"], value=space)) - if space and isinstance(space, list): - filters.append(dm.filters.In(["node", "space"], values=space)) - if filter: - filters.append(filter) - return dm.filters.And(*filters) if filters else None - - -class _UnitProcedureQuery(NodeQueryCore[T_DomainModelList, UnitProcedureList]): - _view_id = UnitProcedure._view_id - _result_cls = UnitProcedure - _result_list_cls_end = UnitProcedureList - - def __init__( - self, - created_types: set[type], - creation_path: list[QueryCore], - client: CogniteClient, - result_list_cls: type[T_DomainModelList], - expression: dm.query.ResultSetExpression | None = None, - connection_name: str | None = None, - connection_type: Literal["reverse-list"] | None = None, - reverse_expression: dm.query.ResultSetExpression | None = None, - ): - from ._equipment_module import _EquipmentModuleQuery - from ._start_end_time import _StartEndTimeQuery - from ._work_order import _WorkOrderQuery - - super().__init__( - created_types, - creation_path, - client, - result_list_cls, - expression, - dm.filters.HasData(views=[self._view_id]), - connection_name, - connection_type, - reverse_expression, - ) - - if _StartEndTimeQuery not in created_types: - self.work_orders = _StartEndTimeQuery( - created_types.copy(), - self._creation_path, - client, - result_list_cls, - _WorkOrderQuery, - dm.query.EdgeResultSetExpression( - direction="outwards", - chain_to="destination", - ), - connection_name="work_orders", - ) - - if _StartEndTimeQuery not in created_types: - self.work_units = _StartEndTimeQuery( - created_types.copy(), - self._creation_path, - client, - result_list_cls, - _EquipmentModuleQuery, - dm.query.EdgeResultSetExpression( - direction="outwards", - chain_to="destination", - ), - connection_name="work_units", - ) - - self.space = StringFilter(self, ["node", "space"]) - self.external_id = StringFilter(self, ["node", "externalId"]) - self.name = StringFilter(self, self._view_id.as_property_ref("name")) - self.type_ = StringFilter(self, self._view_id.as_property_ref("type")) - self._filter_classes.extend( - [ - self.space, - self.external_id, - self.name, - self.type_, - ] - ) - - def list_unit_procedure(self, limit: int = DEFAULT_QUERY_LIMIT) -> UnitProcedureList: - return self._list(limit=limit) - - -class UnitProcedureQuery(_UnitProcedureQuery[UnitProcedureList]): - def __init__(self, client: CogniteClient): - super().__init__(set(), [], client, UnitProcedureList) diff --git a/examples/equipment_unit/data_classes/_work_order.py b/examples/equipment_unit/data_classes/_work_order.py deleted file mode 100644 index 81b49d59c..000000000 --- a/examples/equipment_unit/data_classes/_work_order.py +++ /dev/null @@ -1,364 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Sequence -from typing import Any, ClassVar, Literal, no_type_check, Optional, Union - -from cognite.client import data_modeling as dm, CogniteClient -from pydantic import Field -from pydantic import field_validator, model_validator - -from equipment_unit.data_classes._core import ( - DEFAULT_INSTANCE_SPACE, - DEFAULT_QUERY_LIMIT, - DataRecord, - DataRecordGraphQL, - DataRecordWrite, - DomainModel, - DomainModelWrite, - DomainModelWriteList, - DomainModelList, - DomainRelation, - DomainRelationWrite, - GraphQLCore, - ResourcesWrite, - T_DomainModelList, - as_direct_relation_reference, - as_instance_dict_id, - as_node_id, - as_pygen_node_id, - are_nodes_equal, - is_tuple_id, - select_best_node, - QueryCore, - NodeQueryCore, - StringFilter, -) - - -__all__ = [ - "WorkOrder", - "WorkOrderWrite", - "WorkOrderApply", - "WorkOrderList", - "WorkOrderWriteList", - "WorkOrderApplyList", - "WorkOrderFields", - "WorkOrderTextFields", - "WorkOrderGraphQL", -] - - -WorkOrderTextFields = Literal["external_id", "description", "performed_by", "type_"] -WorkOrderFields = Literal["external_id", "description", "performed_by", "type_"] - -_WORKORDER_PROPERTIES_BY_FIELD = { - "external_id": "externalId", - "description": "description", - "performed_by": "performedBy", - "type_": "type", -} - - -class WorkOrderGraphQL(GraphQLCore): - """This represents the reading version of work order, used - when data is retrieved from CDF using GraphQL. - - It is used when retrieving data from CDF using GraphQL. - - Args: - space: The space where the node is located. - external_id: The external id of the work order. - data_record: The data record of the work order node. - description: The description field. - performed_by: The performed by field. - type_: The type field. - """ - - view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "WorkOrder", "c5543fb2b1bc81") - description: Optional[str] = None - performed_by: Optional[str] = Field(None, alias="performedBy") - type_: Optional[str] = Field(None, alias="type") - - @model_validator(mode="before") - def parse_data_record(cls, values: Any) -> Any: - if not isinstance(values, dict): - return values - if "lastUpdatedTime" in values or "createdTime" in values: - values["dataRecord"] = DataRecordGraphQL( - created_time=values.pop("createdTime", None), - last_updated_time=values.pop("lastUpdatedTime", None), - ) - return values - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_read(self) -> WorkOrder: - """Convert this GraphQL format of work order to the reading format.""" - if self.data_record is None: - raise ValueError("This object cannot be converted to a read format because it lacks a data record.") - return WorkOrder( - space=self.space, - external_id=self.external_id, - data_record=DataRecord( - version=0, - last_updated_time=self.data_record.last_updated_time, - created_time=self.data_record.created_time, - ), - description=self.description, - performed_by=self.performed_by, - type_=self.type_, - ) - - # We do the ignore argument type as we let pydantic handle the type checking - @no_type_check - def as_write(self) -> WorkOrderWrite: - """Convert this GraphQL format of work order to the writing format.""" - return WorkOrderWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=0), - description=self.description, - performed_by=self.performed_by, - type_=self.type_, - ) - - -class WorkOrder(DomainModel): - """This represents the reading version of work order. - - It is used to when data is retrieved from CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the work order. - data_record: The data record of the work order node. - description: The description field. - performed_by: The performed by field. - type_: The type field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "WorkOrder", "c5543fb2b1bc81") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, None] = None - description: Optional[str] = None - performed_by: Optional[str] = Field(None, alias="performedBy") - type_: Optional[str] = Field(None, alias="type") - - def as_write(self) -> WorkOrderWrite: - """Convert this read version of work order to the writing version.""" - return WorkOrderWrite( - space=self.space, - external_id=self.external_id, - data_record=DataRecordWrite(existing_version=self.data_record.version), - description=self.description, - performed_by=self.performed_by, - type_=self.type_, - ) - - def as_apply(self) -> WorkOrderWrite: - """Convert this read version of work order to the writing version.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class WorkOrderWrite(DomainModelWrite): - """This represents the writing version of work order. - - It is used to when data is sent to CDF. - - Args: - space: The space where the node is located. - external_id: The external id of the work order. - data_record: The data record of the work order node. - description: The description field. - performed_by: The performed by field. - type_: The type field. - """ - - _view_id: ClassVar[dm.ViewId] = dm.ViewId("IntegrationTestsImmutable", "WorkOrder", "c5543fb2b1bc81") - - space: str = DEFAULT_INSTANCE_SPACE - node_type: Union[dm.DirectRelationReference, dm.NodeId, tuple[str, str], None] = None - description: Optional[str] = None - performed_by: Optional[str] = Field(None, alias="performedBy") - type_: Optional[str] = Field(None, alias="type") - - def _to_instances_write( - self, - cache: set[tuple[str, str]], - write_none: bool = False, - allow_version_increase: bool = False, - ) -> ResourcesWrite: - resources = ResourcesWrite() - if self.as_tuple_id() in cache: - return resources - - properties: dict[str, Any] = {} - - if self.description is not None or write_none: - properties["description"] = self.description - - if self.performed_by is not None or write_none: - properties["performedBy"] = self.performed_by - - if self.type_ is not None or write_none: - properties["type"] = self.type_ - - if properties: - this_node = dm.NodeApply( - space=self.space, - external_id=self.external_id, - existing_version=None if allow_version_increase else self.data_record.existing_version, - type=as_direct_relation_reference(self.node_type), - sources=[ - dm.NodeOrEdgeData( - source=self._view_id, - properties=properties, - ) - ], - ) - resources.nodes.append(this_node) - cache.add(self.as_tuple_id()) - - return resources - - -class WorkOrderApply(WorkOrderWrite): - def __new__(cls, *args, **kwargs) -> WorkOrderApply: - warnings.warn( - "WorkOrderApply is deprecated and will be removed in v1.0. Use WorkOrderWrite instead." - "The motivation for this change is that Write is a more descriptive name for the writing version of the" - "WorkOrder.", - UserWarning, - stacklevel=2, - ) - return super().__new__(cls) - - -class WorkOrderList(DomainModelList[WorkOrder]): - """List of work orders in the read version.""" - - _INSTANCE = WorkOrder - - def as_write(self) -> WorkOrderWriteList: - """Convert these read versions of work order to the writing versions.""" - return WorkOrderWriteList([node.as_write() for node in self.data]) - - def as_apply(self) -> WorkOrderWriteList: - """Convert these read versions of primitive nullable to the writing versions.""" - warnings.warn( - "as_apply is deprecated and will be removed in v1.0. Use as_write instead.", - UserWarning, - stacklevel=2, - ) - return self.as_write() - - -class WorkOrderWriteList(DomainModelWriteList[WorkOrderWrite]): - """List of work orders in the writing version.""" - - _INSTANCE = WorkOrderWrite - - -class WorkOrderApplyList(WorkOrderWriteList): ... - - -def _create_work_order_filter( - view_id: dm.ViewId, - description: str | list[str] | None = None, - description_prefix: str | None = None, - performed_by: str | list[str] | None = None, - performed_by_prefix: str | None = None, - type_: str | list[str] | None = None, - type_prefix: str | None = None, - external_id_prefix: str | None = None, - space: str | list[str] | None = None, - filter: dm.Filter | None = None, -) -> dm.Filter | None: - filters: list[dm.Filter] = [] - if isinstance(description, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("description"), value=description)) - if description and isinstance(description, list): - filters.append(dm.filters.In(view_id.as_property_ref("description"), values=description)) - if description_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("description"), value=description_prefix)) - if isinstance(performed_by, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("performedBy"), value=performed_by)) - if performed_by and isinstance(performed_by, list): - filters.append(dm.filters.In(view_id.as_property_ref("performedBy"), values=performed_by)) - if performed_by_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("performedBy"), value=performed_by_prefix)) - if isinstance(type_, str): - filters.append(dm.filters.Equals(view_id.as_property_ref("type"), value=type_)) - if type_ and isinstance(type_, list): - filters.append(dm.filters.In(view_id.as_property_ref("type"), values=type_)) - if type_prefix is not None: - filters.append(dm.filters.Prefix(view_id.as_property_ref("type"), value=type_prefix)) - if external_id_prefix is not None: - filters.append(dm.filters.Prefix(["node", "externalId"], value=external_id_prefix)) - if isinstance(space, str): - filters.append(dm.filters.Equals(["node", "space"], value=space)) - if space and isinstance(space, list): - filters.append(dm.filters.In(["node", "space"], values=space)) - if filter: - filters.append(filter) - return dm.filters.And(*filters) if filters else None - - -class _WorkOrderQuery(NodeQueryCore[T_DomainModelList, WorkOrderList]): - _view_id = WorkOrder._view_id - _result_cls = WorkOrder - _result_list_cls_end = WorkOrderList - - def __init__( - self, - created_types: set[type], - creation_path: list[QueryCore], - client: CogniteClient, - result_list_cls: type[T_DomainModelList], - expression: dm.query.ResultSetExpression | None = None, - connection_name: str | None = None, - connection_type: Literal["reverse-list"] | None = None, - reverse_expression: dm.query.ResultSetExpression | None = None, - ): - - super().__init__( - created_types, - creation_path, - client, - result_list_cls, - expression, - dm.filters.HasData(views=[self._view_id]), - connection_name, - connection_type, - reverse_expression, - ) - - self.space = StringFilter(self, ["node", "space"]) - self.external_id = StringFilter(self, ["node", "externalId"]) - self.description = StringFilter(self, self._view_id.as_property_ref("description")) - self.performed_by = StringFilter(self, self._view_id.as_property_ref("performedBy")) - self.type_ = StringFilter(self, self._view_id.as_property_ref("type")) - self._filter_classes.extend( - [ - self.space, - self.external_id, - self.description, - self.performed_by, - self.type_, - ] - ) - - def list_work_order(self, limit: int = DEFAULT_QUERY_LIMIT) -> WorkOrderList: - return self._list(limit=limit) - - -class WorkOrderQuery(_WorkOrderQuery[WorkOrderList]): - def __init__(self, client: CogniteClient): - super().__init__(set(), [], client, WorkOrderList) diff --git a/examples/omni/_api/cdf_external_references.py b/examples/omni/_api/cdf_external_references.py index 74f1cd784..96815b950 100644 --- a/examples/omni/_api/cdf_external_references.py +++ b/examples/omni/_api/cdf_external_references.py @@ -76,6 +76,11 @@ def __call__( A query API for cdf external references. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_cdf_external_reference_filter( self._view_id, diff --git a/examples/omni/_api/cdf_external_references_listed.py b/examples/omni/_api/cdf_external_references_listed.py index 66a6b919a..4409305bc 100644 --- a/examples/omni/_api/cdf_external_references_listed.py +++ b/examples/omni/_api/cdf_external_references_listed.py @@ -79,6 +79,11 @@ def __call__( A query API for cdf external references listeds. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_cdf_external_references_listed_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_a.py b/examples/omni/_api/connection_item_a.py index cd6ebe035..06413eba8 100644 --- a/examples/omni/_api/connection_item_a.py +++ b/examples/omni/_api/connection_item_a.py @@ -96,6 +96,11 @@ def __call__( A query API for connection item as. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_a_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_b.py b/examples/omni/_api/connection_item_b.py index 6d4f71bf1..c927d6ebd 100644 --- a/examples/omni/_api/connection_item_b.py +++ b/examples/omni/_api/connection_item_b.py @@ -79,6 +79,11 @@ def __call__( A query API for connection item bs. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_b_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_c_node.py b/examples/omni/_api/connection_item_c_node.py index 2be295fd0..d3728a57e 100644 --- a/examples/omni/_api/connection_item_c_node.py +++ b/examples/omni/_api/connection_item_c_node.py @@ -77,6 +77,11 @@ def __call__( A query API for connection item c nodes. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_c_node_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_d.py b/examples/omni/_api/connection_item_d.py index cdaa0fe93..b7ccc87d0 100644 --- a/examples/omni/_api/connection_item_d.py +++ b/examples/omni/_api/connection_item_d.py @@ -95,6 +95,11 @@ def __call__( A query API for connection item ds. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_d_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_e.py b/examples/omni/_api/connection_item_e.py index 7ddd30d27..eb34626da 100644 --- a/examples/omni/_api/connection_item_e.py +++ b/examples/omni/_api/connection_item_e.py @@ -93,6 +93,11 @@ def __call__( A query API for connection item es. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_e_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_f.py b/examples/omni/_api/connection_item_f.py index af98622d5..190e563f3 100644 --- a/examples/omni/_api/connection_item_f.py +++ b/examples/omni/_api/connection_item_f.py @@ -94,6 +94,11 @@ def __call__( A query API for connection item fs. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_f_filter( self._view_id, diff --git a/examples/omni/_api/connection_item_g.py b/examples/omni/_api/connection_item_g.py index 11fd8f519..d453e6ac5 100644 --- a/examples/omni/_api/connection_item_g.py +++ b/examples/omni/_api/connection_item_g.py @@ -81,6 +81,11 @@ def __call__( A query API for connection item gs. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_g_filter( self._view_id, diff --git a/examples/omni/_api/dependent_on_non_writable.py b/examples/omni/_api/dependent_on_non_writable.py index 2780d3a23..eb04fb667 100644 --- a/examples/omni/_api/dependent_on_non_writable.py +++ b/examples/omni/_api/dependent_on_non_writable.py @@ -81,6 +81,11 @@ def __call__( A query API for dependent on non writables. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_dependent_on_non_writable_filter( self._view_id, diff --git a/examples/omni/_api/empty.py b/examples/omni/_api/empty.py index 40a20f568..74a91cb06 100644 --- a/examples/omni/_api/empty.py +++ b/examples/omni/_api/empty.py @@ -100,6 +100,11 @@ def __call__( A query API for empties. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_empty_filter( self._view_id, diff --git a/examples/omni/_api/implementation_1.py b/examples/omni/_api/implementation_1.py index a0bf0989b..fd26d000a 100644 --- a/examples/omni/_api/implementation_1.py +++ b/examples/omni/_api/implementation_1.py @@ -85,6 +85,11 @@ def __call__( A query API for implementation 1. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_implementation_1_filter( self._view_id, diff --git a/examples/omni/_api/implementation_1_non_writeable.py b/examples/omni/_api/implementation_1_non_writeable.py index 32e575f68..be8917df4 100644 --- a/examples/omni/_api/implementation_1_non_writeable.py +++ b/examples/omni/_api/implementation_1_non_writeable.py @@ -78,6 +78,11 @@ def __call__( A query API for implementation 1 non writeables. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_implementation_1_non_writeable_filter( self._view_id, diff --git a/examples/omni/_api/implementation_2.py b/examples/omni/_api/implementation_2.py index 73916b376..1e0d56905 100644 --- a/examples/omni/_api/implementation_2.py +++ b/examples/omni/_api/implementation_2.py @@ -77,6 +77,11 @@ def __call__( A query API for implementation 2. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_implementation_2_filter( self._view_id, diff --git a/examples/omni/_api/main_interface.py b/examples/omni/_api/main_interface.py index d4499ea6c..9126218dd 100644 --- a/examples/omni/_api/main_interface.py +++ b/examples/omni/_api/main_interface.py @@ -79,6 +79,11 @@ def __call__( A query API for main interfaces. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_main_interface_filter( self._view_id, diff --git a/examples/omni/_api/primitive_nullable.py b/examples/omni/_api/primitive_nullable.py index 1dd98cc39..3672a0341 100644 --- a/examples/omni/_api/primitive_nullable.py +++ b/examples/omni/_api/primitive_nullable.py @@ -102,6 +102,11 @@ def __call__( A query API for primitive nullables. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_primitive_nullable_filter( self._view_id, diff --git a/examples/omni/_api/primitive_nullable_listed.py b/examples/omni/_api/primitive_nullable_listed.py index 9c90ef8cd..090084a36 100644 --- a/examples/omni/_api/primitive_nullable_listed.py +++ b/examples/omni/_api/primitive_nullable_listed.py @@ -77,6 +77,11 @@ def __call__( A query API for primitive nullable listeds. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_primitive_nullable_listed_filter( self._view_id, diff --git a/examples/omni/_api/primitive_required.py b/examples/omni/_api/primitive_required.py index d531b4e0d..f8e6f7ef6 100644 --- a/examples/omni/_api/primitive_required.py +++ b/examples/omni/_api/primitive_required.py @@ -102,6 +102,11 @@ def __call__( A query API for primitive requireds. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_primitive_required_filter( self._view_id, diff --git a/examples/omni/_api/primitive_required_listed.py b/examples/omni/_api/primitive_required_listed.py index 9943bac0c..e6791773f 100644 --- a/examples/omni/_api/primitive_required_listed.py +++ b/examples/omni/_api/primitive_required_listed.py @@ -77,6 +77,11 @@ def __call__( A query API for primitive required listeds. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_primitive_required_listed_filter( self._view_id, diff --git a/examples/omni/_api/primitive_with_defaults.py b/examples/omni/_api/primitive_with_defaults.py index 18d8c6144..dfc82409c 100644 --- a/examples/omni/_api/primitive_with_defaults.py +++ b/examples/omni/_api/primitive_with_defaults.py @@ -87,6 +87,11 @@ def __call__( A query API for primitive with defaults. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_primitive_with_default_filter( self._view_id, diff --git a/examples/omni/_api/sub_interface.py b/examples/omni/_api/sub_interface.py index abe498e7e..bbae7417c 100644 --- a/examples/omni/_api/sub_interface.py +++ b/examples/omni/_api/sub_interface.py @@ -85,6 +85,11 @@ def __call__( A query API for sub interfaces. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_sub_interface_filter( self._view_id, diff --git a/examples/omni_multi/_api/implementation_1_v_1.py b/examples/omni_multi/_api/implementation_1_v_1.py index b4b3e0a23..3d955f55c 100644 --- a/examples/omni_multi/_api/implementation_1_v_1.py +++ b/examples/omni_multi/_api/implementation_1_v_1.py @@ -82,6 +82,11 @@ def __call__( A query API for implementation 1 v 1. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_implementation_1_v_1_filter( self._view_id, diff --git a/examples/omni_multi/_api/implementation_1_v_2.py b/examples/omni_multi/_api/implementation_1_v_2.py index 3305bb132..f17e61c33 100644 --- a/examples/omni_multi/_api/implementation_1_v_2.py +++ b/examples/omni_multi/_api/implementation_1_v_2.py @@ -82,6 +82,11 @@ def __call__( A query API for implementation 1 v 2. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_implementation_1_v_2_filter( self._view_id, diff --git a/examples/omni_multi/_api/main_interface.py b/examples/omni_multi/_api/main_interface.py index 8fff3de7a..efe16645c 100644 --- a/examples/omni_multi/_api/main_interface.py +++ b/examples/omni_multi/_api/main_interface.py @@ -76,6 +76,11 @@ def __call__( A query API for main interfaces. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_main_interface_filter( self._view_id, diff --git a/examples/omni_multi/_api/sub_interface.py b/examples/omni_multi/_api/sub_interface.py index 4a4c74cb9..0a553277d 100644 --- a/examples/omni_multi/_api/sub_interface.py +++ b/examples/omni_multi/_api/sub_interface.py @@ -80,6 +80,11 @@ def __call__( A query API for sub interfaces. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_sub_interface_filter( self._view_id, diff --git a/examples/omni_sub/_api/connection_item_a.py b/examples/omni_sub/_api/connection_item_a.py index 053bc7f36..b3b8a8e2d 100644 --- a/examples/omni_sub/_api/connection_item_a.py +++ b/examples/omni_sub/_api/connection_item_a.py @@ -93,6 +93,11 @@ def __call__( A query API for connection item as. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_a_filter( self._view_id, diff --git a/examples/omni_sub/_api/connection_item_b.py b/examples/omni_sub/_api/connection_item_b.py index 9610f3dce..fe513cc0a 100644 --- a/examples/omni_sub/_api/connection_item_b.py +++ b/examples/omni_sub/_api/connection_item_b.py @@ -78,6 +78,11 @@ def __call__( A query API for connection item bs. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_b_filter( self._view_id, diff --git a/examples/omni_sub/_api/connection_item_c_node.py b/examples/omni_sub/_api/connection_item_c_node.py index bc76f857b..851c29e12 100644 --- a/examples/omni_sub/_api/connection_item_c_node.py +++ b/examples/omni_sub/_api/connection_item_c_node.py @@ -76,6 +76,11 @@ def __call__( A query API for connection item c nodes. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_connection_item_c_node_filter( self._view_id, diff --git a/examples/wind_turbine/_api/blade.py b/examples/wind_turbine/_api/blade.py index d8da3457b..2749a6cff 100644 --- a/examples/wind_turbine/_api/blade.py +++ b/examples/wind_turbine/_api/blade.py @@ -76,6 +76,11 @@ def __call__( A query API for blades. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_blade_filter( self._view_id, diff --git a/examples/wind_turbine/_api/data_sheet.py b/examples/wind_turbine/_api/data_sheet.py index 312c163f0..313e759ac 100644 --- a/examples/wind_turbine/_api/data_sheet.py +++ b/examples/wind_turbine/_api/data_sheet.py @@ -92,6 +92,11 @@ def __call__( A query API for data sheets. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_data_sheet_filter( self._view_id, diff --git a/examples/wind_turbine/_api/gearbox.py b/examples/wind_turbine/_api/gearbox.py index 5c4bd386e..33e24a3ce 100644 --- a/examples/wind_turbine/_api/gearbox.py +++ b/examples/wind_turbine/_api/gearbox.py @@ -97,6 +97,11 @@ def __call__( A query API for gearboxes. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_gearbox_filter( self._view_id, diff --git a/examples/wind_turbine/_api/generating_unit.py b/examples/wind_turbine/_api/generating_unit.py index 63cb4bd80..30de97aed 100644 --- a/examples/wind_turbine/_api/generating_unit.py +++ b/examples/wind_turbine/_api/generating_unit.py @@ -87,6 +87,11 @@ def __call__( A query API for generating units. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_generating_unit_filter( self._view_id, diff --git a/examples/wind_turbine/_api/generator.py b/examples/wind_turbine/_api/generator.py index a9af83a42..933b96cce 100644 --- a/examples/wind_turbine/_api/generator.py +++ b/examples/wind_turbine/_api/generator.py @@ -88,6 +88,11 @@ def __call__( A query API for generators. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_generator_filter( self._view_id, diff --git a/examples/wind_turbine/_api/high_speed_shaft.py b/examples/wind_turbine/_api/high_speed_shaft.py index 0514da60e..0203f8860 100644 --- a/examples/wind_turbine/_api/high_speed_shaft.py +++ b/examples/wind_turbine/_api/high_speed_shaft.py @@ -97,6 +97,11 @@ def __call__( A query API for high speed shafts. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_high_speed_shaft_filter( self._view_id, diff --git a/examples/wind_turbine/_api/main_shaft.py b/examples/wind_turbine/_api/main_shaft.py index 28e7400f5..a80b52a79 100644 --- a/examples/wind_turbine/_api/main_shaft.py +++ b/examples/wind_turbine/_api/main_shaft.py @@ -115,6 +115,11 @@ def __call__( A query API for main shafts. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_main_shaft_filter( self._view_id, diff --git a/examples/wind_turbine/_api/metmast.py b/examples/wind_turbine/_api/metmast.py index 6399683d7..5d8a4f276 100644 --- a/examples/wind_turbine/_api/metmast.py +++ b/examples/wind_turbine/_api/metmast.py @@ -87,6 +87,11 @@ def __call__( A query API for metmasts. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_metmast_filter( self._view_id, diff --git a/examples/wind_turbine/_api/nacelle.py b/examples/wind_turbine/_api/nacelle.py index 98847c22a..9e612817b 100644 --- a/examples/wind_turbine/_api/nacelle.py +++ b/examples/wind_turbine/_api/nacelle.py @@ -165,6 +165,11 @@ def __call__( A query API for nacelles. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_nacelle_filter( self._view_id, diff --git a/examples/wind_turbine/_api/power_inverter.py b/examples/wind_turbine/_api/power_inverter.py index 304f0d164..519660ada 100644 --- a/examples/wind_turbine/_api/power_inverter.py +++ b/examples/wind_turbine/_api/power_inverter.py @@ -97,6 +97,11 @@ def __call__( A query API for power inverters. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_power_inverter_filter( self._view_id, diff --git a/examples/wind_turbine/_api/rotor.py b/examples/wind_turbine/_api/rotor.py index f022f3ba5..2750adf74 100644 --- a/examples/wind_turbine/_api/rotor.py +++ b/examples/wind_turbine/_api/rotor.py @@ -88,6 +88,11 @@ def __call__( A query API for rotors. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_rotor_filter( self._view_id, diff --git a/examples/wind_turbine/_api/sensor_position.py b/examples/wind_turbine/_api/sensor_position.py index deed0d5a4..553455d08 100644 --- a/examples/wind_turbine/_api/sensor_position.py +++ b/examples/wind_turbine/_api/sensor_position.py @@ -156,6 +156,11 @@ def __call__( A query API for sensor positions. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_sensor_position_filter( self._view_id, diff --git a/examples/wind_turbine/_api/sensor_time_series.py b/examples/wind_turbine/_api/sensor_time_series.py index 94dd5f381..686a2554d 100644 --- a/examples/wind_turbine/_api/sensor_time_series.py +++ b/examples/wind_turbine/_api/sensor_time_series.py @@ -93,6 +93,11 @@ def __call__( A query API for sensor time series. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_sensor_time_series_filter( self._view_id, diff --git a/examples/wind_turbine/_api/solar_panel.py b/examples/wind_turbine/_api/solar_panel.py index feccc928b..e4d042934 100644 --- a/examples/wind_turbine/_api/solar_panel.py +++ b/examples/wind_turbine/_api/solar_panel.py @@ -100,6 +100,11 @@ def __call__( A query API for solar panels. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_solar_panel_filter( self._view_id, diff --git a/examples/wind_turbine/_api/wind_turbine.py b/examples/wind_turbine/_api/wind_turbine.py index f97b17364..5860be3b8 100644 --- a/examples/wind_turbine/_api/wind_turbine.py +++ b/examples/wind_turbine/_api/wind_turbine.py @@ -133,6 +133,11 @@ def __call__( A query API for wind turbines. """ + warnings.warn( + "This method is deprecated and will soon be removed. " "Use the .select() method instead.", + UserWarning, + stacklevel=2, + ) has_data = dm.filters.HasData(views=[self._view_id]) filter_ = _create_wind_turbine_filter( self._view_id, diff --git a/tests/constants.py b/tests/constants.py index 03a020fe5..9ee80c804 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -130,14 +130,6 @@ def load_read_nodes(self, data_model_id: dm.DataModelId) -> dm.NodeList: instance_space=None, ) -EQUIPMENT_UNIT_SDK = ExampleSDK( - data_model_ids=[DataModelId("IntegrationTestsImmutable", "EquipmentUnit", "2")], - _top_level_package="equipment_unit", - client_name="EquipmentUnitClient", - generate_sdk=True, - instance_space="IntegrationTestsImmutable", -) - CORE_SDK = ExampleSDK( data_model_ids=[DataModelId("cdf_cdm", "CogniteCore", "v1")], _top_level_package="cognite_core", @@ -163,27 +155,6 @@ def load_read_nodes(self, data_model_id: dm.DataModelId) -> dm.NodeList: ) -class EquipmentSDKFiles: - client_dir = EQUIPMENT_UNIT_SDK.client_dir - client = client_dir / "_api_client.py" - data_classes = client_dir / "data_classes" - core_data = data_classes / "_core.py" - start_end_time_data = data_classes / "_start_end_time.py" - unit_procedure_data = data_classes / "_unit_procedure.py" - equipment_module_data = data_classes / "_equipment_module.py" - - api = client_dir / "_api" - equipment_api = api / "equipment_module.py" - equipment_module_sensor_value_api = api / "equipment_module_sensor_value.py" - - unit_procedure_api = api / "unit_procedure.py" - unit_procedure_query = api / "unit_procedure_query.py" - unit_procedure_work_units = api / "unit_procedure_work_units.py" - core_api = api / "_core.py" - - data_init = data_classes / "__init__.py" - - class OmniFiles: client_dir = OMNI_SDK.client_dir client = client_dir / "_api_client.py" diff --git a/tests/data/models/EquipmentUnit/model.yaml b/tests/data/models/EquipmentUnit/model.yaml deleted file mode 100644 index 20345003d..000000000 --- a/tests/data/models/EquipmentUnit/model.yaml +++ /dev/null @@ -1,262 +0,0 @@ -space: IntegrationTestsImmutable -externalId: EquipmentUnit -name: EquipmentUnit -description: '' -version: '2' -views: -- space: IntegrationTestsImmutable - externalId: EquipmentModule - name: EquipmentModule - implements: [] - version: b1cd4bf14a7a33 - writable: true - usedFor: node - isGlobal: false - properties: - description: - container: - space: IntegrationTestsImmutable - externalId: EquipmentModule - containerPropertyIdentifier: description - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: description - description: null - name: - container: - space: IntegrationTestsImmutable - externalId: EquipmentModule - containerPropertyIdentifier: name - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: name - description: null - sensor_value: - container: - space: IntegrationTestsImmutable - externalId: EquipmentModule - containerPropertyIdentifier: sensor_value - type: - list: false - type: timeseries - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: sensor_value - description: null - type: - container: - space: IntegrationTestsImmutable - externalId: EquipmentModule - containerPropertyIdentifier: type - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: type - description: null - lastUpdatedTime: 1703424931271 - createdTime: 1703424931271 -- space: IntegrationTestsImmutable - externalId: StartEndTime - name: StartEndTime - implements: [] - version: d416e0ed98186b - writable: true - usedFor: edge - isGlobal: false - properties: - end_time: - container: - space: IntegrationTestsImmutable - externalId: StartEndTime - containerPropertyIdentifier: end_time - type: - list: false - type: timestamp - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: end_time - description: null - start_time: - container: - space: IntegrationTestsImmutable - externalId: StartEndTime - containerPropertyIdentifier: start_time - type: - list: false - type: timestamp - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: start_time - description: null - lastUpdatedTime: 1703424931271 - createdTime: 1703424931271 -- space: IntegrationTestsImmutable - externalId: UnitProcedure - name: UnitProcedure - implements: [] - version: a6e2fea1e1c664 - writable: true - usedFor: node - isGlobal: false - properties: - name: - container: - space: IntegrationTestsImmutable - externalId: UnitProcedure - containerPropertyIdentifier: name - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: name - description: null - type: - container: - space: IntegrationTestsImmutable - externalId: UnitProcedure - containerPropertyIdentifier: type - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: type - description: null - work_orders: - type: - space: IntegrationTestsImmutable - externalId: UnitProcedure.work_order - source: - space: IntegrationTestsImmutable - externalId: WorkOrder - version: c5543fb2b1bc81 - type: view - name: work_orders - description: null - edgeSource: - space: IntegrationTestsImmutable - externalId: StartEndTime - version: d416e0ed98186b - type: view - direction: outwards - connectionType: multi_edge_connection - work_units: - type: - space: IntegrationTestsImmutable - externalId: UnitProcedure.equipment_module - source: - space: IntegrationTestsImmutable - externalId: EquipmentModule - version: b1cd4bf14a7a33 - type: view - name: work_units - description: null - edgeSource: - space: IntegrationTestsImmutable - externalId: StartEndTime - version: d416e0ed98186b - type: view - direction: outwards - connectionType: multi_edge_connection - lastUpdatedTime: 1703424931271 - createdTime: 1703424931271 -- space: IntegrationTestsImmutable - externalId: WorkOrder - name: WorkOrder - implements: [] - version: c5543fb2b1bc81 - writable: true - usedFor: node - isGlobal: false - properties: - description: - container: - space: IntegrationTestsImmutable - externalId: WorkOrder - containerPropertyIdentifier: description - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: description - description: null - performedBy: - container: - space: IntegrationTestsImmutable - externalId: WorkOrder - containerPropertyIdentifier: performedBy - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: performedBy - description: null - type: - container: - space: IntegrationTestsImmutable - externalId: WorkOrder - containerPropertyIdentifier: type - type: - list: false - collation: ucs_basic - type: text - nullable: true - immutable: false - autoIncrement: false - source: null - defaultValue: null - name: type - description: null - lastUpdatedTime: 1703424931271 - createdTime: 1703424931271 -isGlobal: false -lastUpdatedTime: 1703485511412 -createdTime: 1703424932659 diff --git a/tests/test_integration/test_list_edges.py b/tests/test_integration/test_list_edges.py new file mode 100644 index 000000000..553f32eb6 --- /dev/null +++ b/tests/test_integration/test_list_edges.py @@ -0,0 +1,35 @@ +from wind_turbine import WindTurbineClient +from wind_turbine import data_classes as data_cls + + +def test_edges_to_pandas(turbine_client: WindTurbineClient) -> None: + df = turbine_client.wind_turbine.metmast_edge.list(max_distance=1000, limit=3).to_pandas() + assert len(df) <= 3 + assert not df.empty + assert "distance" in df.columns + assert sum(df["distance"] <= 1000) == len(df) + + +def test_upsert_with_edge(turbine_client: WindTurbineClient) -> None: + new_turbine = data_cls.WindTurbineWrite( + external_id="doctriono_b", + name="A new Wind Turbine", + capacity=8.0, + metmast=[ + data_cls.DistanceWrite( + distance=500.0, + end_node=data_cls.MetmastWrite( + external_id="doctrino_weather", + position=42.0, + ), + ) + ], + ) + + try: + created = turbine_client.upsert(new_turbine) + + assert len(created.nodes) == 2 + assert len(created.edges) == 1 + finally: + turbine_client.delete(new_turbine) diff --git a/tests/test_integration/test_sdks/__init__.py b/tests/test_integration/test_sdks/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_integration/test_sdks/test_equipment_sdk/__init__.py b/tests/test_integration/test_sdks/test_equipment_sdk/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_integration/test_sdks/test_equipment_sdk/conftest.py b/tests/test_integration/test_sdks/test_equipment_sdk/conftest.py deleted file mode 100644 index f4c097741..000000000 --- a/tests/test_integration/test_sdks/test_equipment_sdk/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest -from equipment_unit import EquipmentUnitClient - - -@pytest.fixture() -def workorder(client_config) -> EquipmentUnitClient: - return EquipmentUnitClient.azure_project(**client_config) diff --git a/tests/test_integration/test_sdks/test_equipment_sdk/test_unit_procedure.py b/tests/test_integration/test_sdks/test_equipment_sdk/test_unit_procedure.py deleted file mode 100644 index cafbeb676..000000000 --- a/tests/test_integration/test_sdks/test_equipment_sdk/test_unit_procedure.py +++ /dev/null @@ -1,104 +0,0 @@ -import pytest -from cognite.client import CogniteClient -from cognite.client.data_classes import TimeSeriesWrite -from equipment_unit.data_classes import ( - EquipmentModule, - EquipmentModuleWrite, - StartEndTime, - StartEndTimeList, - StartEndTimeWrite, - UnitProcedureList, - UnitProcedureWrite, -) - -from examples.equipment_unit import EquipmentUnitClient - - -@pytest.fixture -def start_end_time_edges(workorder: EquipmentUnitClient) -> StartEndTimeList: - edges = workorder.unit_procedure.work_units_edge.list(limit=-1) - assert len(edges) > 2, "There should be at least three edge in the list" - assert isinstance(edges, StartEndTimeList) - return edges - - -@pytest.fixture -def unit_procedure_list(workorder: EquipmentUnitClient) -> UnitProcedureList: - nodes = workorder.unit_procedure.list(limit=5) - assert len(nodes) > 2, "There should be at least three node in the list" - assert isinstance(nodes, UnitProcedureList) - return nodes - - -def test_edges_to_pandas(start_end_time_edges: StartEndTimeList) -> None: - df = start_end_time_edges.to_pandas() - assert len(df) == len(start_end_time_edges) - - -def test_unit_procedure_list_to_pandas(unit_procedure_list: UnitProcedureList) -> None: - df = unit_procedure_list.to_pandas() - assert len(df) == len(unit_procedure_list) - - -def test_single_unit_procedure_to_pandas(unit_procedure_list: UnitProcedureList) -> None: - procedure = unit_procedure_list[0] - - series = procedure.to_pandas() - assert len(series) == 8, "We only have the four properties + the external id, space, node type, and data_record" - - -def test_filter_start_end_time_edges(start_end_time_edges: StartEndTimeList, workorder: EquipmentUnitClient) -> None: - sorted_by_start_time = sorted(start_end_time_edges, key=lambda x: x.start_time) - - filtered = workorder.unit_procedure.work_units_edge.list( - min_start_time=sorted_by_start_time[1].start_time, limit=-1 - ) - - assert len(filtered) == len(sorted_by_start_time) - 1 - - -def test_filter_unit_procedure_through_edge(workorder: EquipmentUnitClient) -> None: - unit_procedures = workorder.unit_procedure(type_="red", limit=3).work_units(limit=5).query() - - assert 1 <= len(unit_procedures) <= 3 - assert all(procedure.type_ == "red" for procedure in unit_procedures) - for unit_procedure in unit_procedures: - for work_unit in unit_procedure.work_units: - assert isinstance(work_unit, StartEndTime) - assert isinstance(work_unit.end_node, EquipmentModule) - - -def test_apply_unit_procedure_with_edge(workorder: EquipmentUnitClient, cognite_client: CogniteClient) -> None: - new_procedure = UnitProcedureWrite( - external_id="procedure:new_procedure", - name="New procedure", - type_="New type", - work_units=[ - StartEndTimeWrite( - start_time="2021-01-01T00:00:00Z", - end_time="2021-01-01T00:00:00Z", - end_node=EquipmentModuleWrite( - external_id="module:new_module", - name="New module", - equipment_module_type="New type", - sensor_value=TimeSeriesWrite( - external_id="timeseries:123", - is_step=True, - description="This is a test timeseries, it should not persist", - ), - description="New description", - ), - ), - ], - ) - - instances = new_procedure.to_instances_write() - try: - created = workorder.unit_procedure.apply(new_procedure) - - assert len(created.nodes) == 2 - assert len(created.edges) == 1 - assert len(created.time_series) == 1 - finally: - cognite_client.data_modeling.instances.delete(instances.nodes.as_ids(), instances.edges.as_ids()) - cognite_client.time_series.delete(external_id=instances.time_series.as_external_ids(), ignore_unknown_ids=True) diff --git a/tests/test_unit/test_generator/conftest.py b/tests/test_unit/test_generator/conftest.py index 37ca88e7f..e567a84ed 100644 --- a/tests/test_unit/test_generator/conftest.py +++ b/tests/test_unit/test_generator/conftest.py @@ -1,32 +1,9 @@ import pytest from cognite.client import data_modeling as dm -from tests.constants import ( - EQUIPMENT_UNIT_SDK, - PUMP_SDK, -) +from tests.constants import PUMP_SDK @pytest.fixture(scope="session") def pump_model() -> dm.DataModel[dm.View]: return PUMP_SDK.load_data_model() - - -@pytest.fixture(scope="session") -def equipment_unit_model() -> dm.DataModel[dm.View]: - return EQUIPMENT_UNIT_SDK.load_data_model() - - -@pytest.fixture(scope="session") -def unit_procedure_view(equipment_unit_model: dm.DataModel) -> dm.View: - return next(v for v in equipment_unit_model.views if v.name == "UnitProcedure") - - -@pytest.fixture(scope="session") -def equipment_module_view(equipment_unit_model: dm.DataModel) -> dm.View: - return next(v for v in equipment_unit_model.views if v.name == "EquipmentModule") - - -@pytest.fixture(scope="session") -def start_end_time_view(equipment_unit_model: dm.DataModel) -> dm.View: - return next(v for v in equipment_unit_model.views if v.name == "StartEndTime") diff --git a/tests/test_unit/test_generator/test_sdk_generator_equipment_unit_sdk.py b/tests/test_unit/test_generator/test_sdk_generator_equipment_unit_sdk.py deleted file mode 100644 index a01df7015..000000000 --- a/tests/test_unit/test_generator/test_sdk_generator_equipment_unit_sdk.py +++ /dev/null @@ -1,191 +0,0 @@ -from __future__ import annotations - -import difflib - -import pytest -from cognite.client import data_modeling as dm - -from cognite.pygen._core.generators import APIGenerator, MultiAPIGenerator, SDKGenerator -from cognite.pygen._generator import CodeFormatter -from cognite.pygen.config import PygenConfig -from tests.constants import EQUIPMENT_UNIT_SDK, EquipmentSDKFiles - - -@pytest.fixture -def sdk_generator(equipment_unit_model: dm.DataModel[dm.View], pygen_config: PygenConfig) -> SDKGenerator: - return SDKGenerator( - EQUIPMENT_UNIT_SDK.top_level_package, - EQUIPMENT_UNIT_SDK.client_name, - equipment_unit_model, - config=pygen_config, - default_instance_space=EQUIPMENT_UNIT_SDK.instance_space, - ) - - -@pytest.fixture -def multi_api_generator(sdk_generator: SDKGenerator) -> MultiAPIGenerator: - return sdk_generator._multi_api_generator - - -@pytest.fixture -def unit_procedure_api_generator(multi_api_generator: MultiAPIGenerator, unit_procedure_view: dm.View) -> APIGenerator: - api_generator = multi_api_generator[unit_procedure_view.as_id()] - assert api_generator is not None, "Could not find API generator for unit procedure view" - return api_generator - - -@pytest.fixture -def equipment_module_api_generator( - multi_api_generator: MultiAPIGenerator, equipment_module_view: dm.View -) -> APIGenerator: - api_generator = multi_api_generator[equipment_module_view.as_id()] - assert api_generator is not None, "Could not find API generator for equipment module view" - return api_generator - - -@pytest.fixture -def start_end_time_api_generator(multi_api_generator: MultiAPIGenerator, start_end_time_view: dm.View) -> APIGenerator: - api_generator = multi_api_generator.api_by_type_by_view_id["edge"][start_end_time_view.as_id()] - assert api_generator is not None, "Could not find API generator for start end time view" - return api_generator - - -def test_generate_data_class_file_unit_procedure( - unit_procedure_api_generator: APIGenerator, pygen_config: PygenConfig, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.unit_procedure_data.read_text() - - # Act - actual = unit_procedure_api_generator.generate_data_class_file() - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_generate_data_class_start_end_time( - start_end_time_api_generator: APIGenerator, pygen_config: PygenConfig, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.start_end_time_data.read_text() - - # Act - actual = start_end_time_api_generator.generate_data_class_file() - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_generate_data_class_equipment_module( - equipment_module_api_generator: APIGenerator, pygen_config: PygenConfig, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.equipment_module_data.read_text() - - # Act - actual = equipment_module_api_generator.generate_data_class_file() - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_create_view_api_classes_equipment_module( - equipment_module_api_generator: APIGenerator, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.equipment_api.read_text() - - # Act - actual = equipment_module_api_generator.generate_api_file(EQUIPMENT_UNIT_SDK.client_name) - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_generate_equipment_module_sensor_value_api( - equipment_module_api_generator: APIGenerator, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.equipment_module_sensor_value_api.read_text() - - # Act - _, actual = next(equipment_module_api_generator.generate_timeseries_api_files(EQUIPMENT_UNIT_SDK.client_name)) - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_create_view_api_classes_unit_procedure( - unit_procedure_api_generator: APIGenerator, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.unit_procedure_api.read_text() - - # Act - actual = unit_procedure_api_generator.generate_api_file(EQUIPMENT_UNIT_SDK.client_name) - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_create_view_api_classes_unit_procedure_query( - unit_procedure_api_generator: APIGenerator, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.unit_procedure_query.read_text() - - # Act - actual = unit_procedure_api_generator.generate_api_query_file(EQUIPMENT_UNIT_SDK.client_name) - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_create_view_api_classes_unit_procedure_work_units( - unit_procedure_api_generator: APIGenerator, code_formatter: CodeFormatter -): - # Arrange - expected = EquipmentSDKFiles.unit_procedure_work_units.read_text() - - # Act - - actual_by_file_name = { - filename: content - for filename, content in unit_procedure_api_generator.generate_edge_api_files(EQUIPMENT_UNIT_SDK.client_name) - } - actual = actual_by_file_name[EquipmentSDKFiles.unit_procedure_work_units.stem] - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_generate_data_class_init_file(multi_api_generator: MultiAPIGenerator, code_formatter: CodeFormatter): - # Arrange - expected = EquipmentSDKFiles.data_init.read_text() - - # Act - actual = multi_api_generator.generate_data_classes_init_file() - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected - - -def test_create_api_client(sdk_generator: SDKGenerator, code_formatter: CodeFormatter): - # Arrange - expected = EquipmentSDKFiles.client.read_text() - - # Act - actual = sdk_generator._generate_api_client_file() - actual = code_formatter.format_code(actual) - - # Assert - assert actual == expected, "\n".join(difflib.unified_diff(expected.splitlines(), actual.splitlines()))