diff --git a/CHANGELOG.md b/CHANGELOG.md index cfe62344ea..1ba95de610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ Changes are grouped as follows - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [Unreleased] +### Added +- Support for the `/simulators` and `/simulators/integration` API endpoints. + ## [7.70.7] - 2024-12-20 ### Fixed - Passing a valid but empty string as external_id no longer raises an error for certain SDK methods diff --git a/cognite/client/_api/simulators/__init__.py b/cognite/client/_api/simulators/__init__.py new file mode 100644 index 0000000000..73e83ce68a --- /dev/null +++ b/cognite/client/_api/simulators/__init__.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from cognite.client._api.simulators.integrations import SimulatorIntegrationsAPI +from cognite.client._api_client import APIClient +from cognite.client._constants import DEFAULT_LIMIT_READ +from cognite.client.data_classes.simulators.simulators import Simulator, SimulatorList +from cognite.client.utils._experimental import FeaturePreviewWarning + +if TYPE_CHECKING: + from cognite.client import CogniteClient + from cognite.client.config import ClientConfig + + +class SimulatorsAPI(APIClient): + _RESOURCE_PATH = "/simulators" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self.integrations = SimulatorIntegrationsAPI(config, api_version, cognite_client) + self._warning = FeaturePreviewWarning( + api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" + ) + + def list(self, limit: int = DEFAULT_LIMIT_READ) -> SimulatorList: + """`List simulators `_ + + List simulators + + Args: + limit (int): Maximum number of results to return. Defaults to 25. Set to -1, float(“inf”) or None to return all items. + + Returns: + SimulatorList: List of simulators + + Examples: + + List simulators: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.list() + + """ + self._warning.warn() + return self._list(method="POST", limit=limit, resource_cls=Simulator, list_cls=SimulatorList) diff --git a/cognite/client/_api/simulators/integrations.py b/cognite/client/_api/simulators/integrations.py new file mode 100644 index 0000000000..71b34245e3 --- /dev/null +++ b/cognite/client/_api/simulators/integrations.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from cognite.client._api_client import APIClient +from cognite.client._constants import DEFAULT_LIMIT_READ +from cognite.client.data_classes._base import CogniteFilter +from cognite.client.data_classes.simulators.filters import SimulatorIntegrationFilter +from cognite.client.data_classes.simulators.simulators import ( + SimulatorIntegration, + SimulatorIntegrationList, +) +from cognite.client.utils._experimental import FeaturePreviewWarning +from cognite.client.utils._identifier import IdentifierSequence +from cognite.client.utils.useful_types import SequenceNotStr + +if TYPE_CHECKING: + from cognite.client import CogniteClient + from cognite.client.config import ClientConfig + + +class SimulatorIntegrationsAPI(APIClient): + _RESOURCE_PATH = "/simulators/integrations" + + def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: + super().__init__(config, api_version, cognite_client) + self._DELETE_LIMIT = 1 + self._warning = FeaturePreviewWarning( + api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" + ) + + def list( + self, + limit: int = DEFAULT_LIMIT_READ, + filter: SimulatorIntegrationFilter | None = None, + ) -> SimulatorIntegrationList: + """`Filter simulator integrations `_ + Retrieves a list of simulator integrations that match the given criteria + Args: + limit (int): The maximum number of simulator integrations to return. + filter (SimulatorIntegrationFilter | None): Filter to apply. + Returns: + SimulatorIntegrationList: List of simulator integrations + Examples: + List simulator integrations: + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> res = client.simulators.integrations.list() + + Filter integrations by active status: + >>> from cognite.client.data_classes.simulators.filters import SimulatorIntegrationFilter + >>> res = client.simulators.integrations.list( + ... filter=SimulatorIntegrationFilter(active=True)) + """ + + self._warning.warn() + return self._list( + method="POST", + limit=limit, + resource_cls=SimulatorIntegration, + list_cls=SimulatorIntegrationList, + filter=filter.dump() if isinstance(filter, CogniteFilter) else None, + ) + + def delete( + self, + id: int | Sequence[int] | None = None, + external_ids: str | SequenceNotStr[str] | SequenceNotStr[str] | None = None, + ) -> None: + """`Delete one or more integrations `_ + + Args: + id (int | Sequence[int] | None): Id or list of ids + external_ids (str | SequenceNotStr[str] | SequenceNotStr[str] | None): No description. + + Examples: + + Delete integrations by id or external id: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> client.simulators.integrations.delete(id=[1,2,3], external_id="foo") + """ + self._delete_multiple( + identifiers=IdentifierSequence.load(ids=id, external_ids=external_ids), + wrap_ids=True, + ) diff --git a/cognite/client/_api_client.py b/cognite/client/_api_client.py index eedc93aacc..5e3846f0be 100644 --- a/cognite/client/_api_client.py +++ b/cognite/client/_api_client.py @@ -101,6 +101,7 @@ class APIClient: "geospatial/(compute|crs/byids|featuretypes/(byids|list))", "geospatial/featuretypes/[A-Za-z][A-Za-z0-9_]{0,31}/features/(aggregate|list|byids|search|search-streaming|[A-Za-z][A-Za-z0-9_]{0,255}/rasters/[A-Za-z][A-Za-z0-9_]{0,31})", "transformations/(filter|byids|jobs/byids|schedules/byids|query/run)", + "simulators/list", "extpipes/(list|byids|runs/list)", "workflows/.*", "hostedextractors/.*", diff --git a/cognite/client/_cognite_client.py b/cognite/client/_cognite_client.py index b5c0f01b46..9c9c0a4108 100644 --- a/cognite/client/_cognite_client.py +++ b/cognite/client/_cognite_client.py @@ -23,6 +23,7 @@ from cognite.client._api.raw import RawAPI from cognite.client._api.relationships import RelationshipsAPI from cognite.client._api.sequences import SequencesAPI +from cognite.client._api.simulators import SimulatorsAPI from cognite.client._api.templates import TemplatesAPI from cognite.client._api.three_d import ThreeDAPI from cognite.client._api.time_series import TimeSeriesAPI @@ -83,6 +84,8 @@ def __init__(self, config: ClientConfig | None = None) -> None: self.documents = DocumentsAPI(self._config, self._API_VERSION, self) self.workflows = WorkflowAPI(self._config, self._API_VERSION, self) self.units = UnitAPI(self._config, self._API_VERSION, self) + self.simulators = SimulatorsAPI(self._config, self._API_VERSION, self) + # APIs just using base_url: self._api_client = APIClient(self._config, api_version=None, cognite_client=self) diff --git a/cognite/client/data_classes/simulators/__init__.py b/cognite/client/data_classes/simulators/__init__.py index 9d48db4f9f..6c3fc96dc0 100644 --- a/cognite/client/data_classes/simulators/__init__.py +++ b/cognite/client/data_classes/simulators/__init__.py @@ -1 +1,23 @@ from __future__ import annotations + +from cognite.client.data_classes.simulators.simulators import ( + Simulator, + SimulatorIntegration, + SimulatorIntegrationList, + SimulatorList, + SimulatorStep, + SimulatorStepField, + SimulatorStepOption, + SimulatorUnitEntry, +) + +__all__ = [ + "Simulator", + "SimulatorIntegration", + "SimulatorIntegrationList", + "SimulatorList", + "SimulatorStep", + "SimulatorStepField", + "SimulatorStepOption", + "SimulatorUnitEntry", +] diff --git a/cognite/client/data_classes/simulators/filters.py b/cognite/client/data_classes/simulators/filters.py new file mode 100644 index 0000000000..e912b961a6 --- /dev/null +++ b/cognite/client/data_classes/simulators/filters.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from cognite.client.data_classes._base import CogniteFilter +from cognite.client.utils.useful_types import SequenceNotStr + + +class SimulatorIntegrationFilter(CogniteFilter): + def __init__( + self, + simulator_external_ids: SequenceNotStr[str] | None = None, + active: bool | None = None, + ) -> None: + self.simulator_external_ids = simulator_external_ids + self.active = active diff --git a/cognite/client/data_classes/simulators/simulators.py b/cognite/client/data_classes/simulators/simulators.py new file mode 100644 index 0000000000..15fd0b2631 --- /dev/null +++ b/cognite/client/data_classes/simulators/simulators.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from typing_extensions import Self + +from cognite.client.data_classes._base import ( + CogniteObject, + CogniteResource, + CogniteResourceList, + IdTransformerMixin, +) + +if TYPE_CHECKING: + from cognite.client import CogniteClient + + +class Simulator(CogniteResource): + """The simulator resource contains the definitions necessary for Cognite Data Fusion (CDF) to interact with a given simulator. + + It serves as a central contract that allows APIs, UIs, and integrations (connectors) to utilize the same definitions + when dealing with a specific simulator. Each simulator is uniquely identified and can be associated with various + file extension types, model types, step fields, and unit quantities. Simulators are essential for managing data + flows between CDF and external simulation tools, ensuring consistency and reliability in data handling. + + This is the read/response format of the simulator. + + Args: + external_id (str): External id of the simulator + id (int): No description. + name (str): Name of the simulator + file_extension_types (str | Sequence[str]): File extension types supported by the simulator + model_types (SimulatorModelType | Sequence[SimulatorModelType] | None): Model types supported by the simulator + step_fields (SimulatorStep | Sequence[SimulatorStep] | None): Step types supported by the simulator when creating routines + unit_quantities (SimulatorQuantity | Sequence[SimulatorQuantity] | None): Quantities and their units supported by the simulator + + """ + + def __init__( + self, + external_id: str, + id: int, + name: str, + file_extension_types: str | Sequence[str], + model_types: SimulatorModelType | Sequence[SimulatorModelType] | None = None, + step_fields: SimulatorStep | Sequence[SimulatorStep] | None = None, + unit_quantities: SimulatorQuantity | Sequence[SimulatorQuantity] | None = None, + ) -> None: + self.external_id = external_id + self.name = name + self.file_extension_types = file_extension_types + self.model_types = model_types + self.step_fields = step_fields + self.unit_quantities = unit_quantities + self.id = id + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + id=resource["id"], + external_id=resource["externalId"], + name=resource["name"], + file_extension_types=resource["fileExtensionTypes"], + model_types=SimulatorModelType._load_list(resource["modelTypes"], cognite_client) + if "modelTypes" in resource + else None, + step_fields=SimulatorStep._load_list(resource["stepFields"], cognite_client) + if "stepFields" in resource + else None, + unit_quantities=SimulatorQuantity._load_list(resource["unitQuantities"], cognite_client) + if "unitQuantities" in resource + else None, + ) + + def dump(self, camel_case: bool = True) -> dict[str, Any]: + output = super().dump(camel_case=camel_case) + if isinstance(self.model_types, SimulatorModelType): + output["modelTypes" if camel_case else "model_types"] = self.model_types.dump(camel_case=camel_case) + if isinstance(self.step_fields, SimulatorStep): + output["stepFields" if camel_case else "step_fields"] = self.step_fields.dump(camel_case=camel_case) + if isinstance(self.unit_quantities, SimulatorQuantity): + output["unitQuantities" if camel_case else "unit_quantities"] = self.unit_quantities.dump( + camel_case=camel_case + ) + + return output + + +@dataclass +class SimulatorRoutineStep(CogniteObject): + step_type: str + arguments: dict[str, Any] + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + step_type=resource["stepType"], + arguments=resource["arguments"], + ) + + +@dataclass +class SimulatorUnitEntry(CogniteObject): + label: str + name: str + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + label=resource["label"], + name=resource["name"], + ) + + +@dataclass +class SimulatorStepOption(CogniteObject): + label: str + value: str + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + label=resource["label"], + value=resource["value"], + ) + + +@dataclass +class SimulatorModelType(CogniteObject): + name: str + key: str + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + name=resource["name"], + key=resource["key"], + ) + + @classmethod + def _load_list( + cls, resource: dict[str, Any] | list[dict[str, Any]], cognite_client: CogniteClient | None = None + ) -> SimulatorModelType | list[SimulatorModelType]: + if isinstance(resource, list): + return [cls._load(res, cognite_client) for res in resource] + + return cls._load(resource, cognite_client) + + +@dataclass +class SimulatorQuantity(CogniteObject): + name: str + label: str + units: Sequence[SimulatorUnitEntry] + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + name=resource["name"], + label=resource["label"], + units=[SimulatorUnitEntry._load(unit_, cognite_client) for unit_ in resource["units"]], + ) + + @classmethod + def _load_list( + cls, resource: dict[str, Any] | list[dict[str, Any]], cognite_client: CogniteClient | None = None + ) -> SimulatorQuantity | list[SimulatorQuantity]: + if isinstance(resource, list): + return [cls._load(res, cognite_client) for res in resource] + + return cls._load(resource, cognite_client) + + def dump(self, camel_case: bool = True) -> dict[str, Any]: + output = super().dump(camel_case=camel_case) + output["units"] = [unit_.dump(camel_case=camel_case) for unit_ in self.units] + + return output + + +@dataclass +class SimulatorStepField(CogniteObject): + name: str + label: str + info: str + options: Sequence[SimulatorStepOption] | None = None + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + name=resource["name"], + label=resource["label"], + info=resource["info"], + options=[SimulatorStepOption._load(option_, cognite_client) for option_ in resource["options"]] + if "options" in resource + else None, + ) + + def dump(self, camel_case: bool = True) -> dict[str, Any]: + output = super().dump(camel_case=camel_case) + if self.options is not None: + output["options"] = [option_.dump(camel_case=camel_case) for option_ in self.options] + + return output + + +@dataclass +class SimulatorStep(CogniteObject): + step_type: str + fields: Sequence[SimulatorStepField] + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + step_type=resource["stepType"], + fields=[SimulatorStepField._load(field_, cognite_client) for field_ in resource["fields"]], + ) + + @classmethod + def _load_list( + cls, resource: dict[str, Any] | list[dict[str, Any]], cognite_client: CogniteClient | None = None + ) -> SimulatorStep | list[SimulatorStep]: + if isinstance(resource, list): + return [cls._load(res, cognite_client) for res in resource] + + return cls._load(resource, cognite_client) + + def dump(self, camel_case: bool = True) -> dict[str, Any]: + output = super().dump(camel_case=camel_case) + output["fields"] = [field_.dump(camel_case=camel_case) for field_ in self.fields] + + return output + + +class SimulatorList(CogniteResourceList[Simulator], IdTransformerMixin): + _RESOURCE = Simulator + + +class SimulatorIntegration(CogniteResource): + """ + The simulator integration resource represents a simulator connector in Cognite Data Fusion (CDF). + It provides information about the configured connectors for a given simulator, including their status and additional + details such as dataset, name, license status, connector version, simulator version, and more. This resource is essential + for monitoring and managing the interactions between CDF and external simulators, ensuring proper data flow and integration. + This is the read/response format of the simulator integration. + Args: + id (int): No description. + external_id (str): External id of the simulator integration + simulator_external_id (str): External id of the associated simulator + heartbeat (int): The interval in seconds between the last heartbeat and the current time + data_set_id (int): The id of the dataset associated with the simulator integration + connector_version (str): The version of the connector + log_id (int): No description. + active (bool): No description. + created_time (int): No description. + last_updated_time (int): No description. + license_status (str | None): The status of the license + simulator_version (str | None): The version of the simulator + license_last_checked_time (int | None): The time when the license was last checked + connector_status (str | None): The status of the connector + connector_status_updated_time (int | None): The time when the connector status was last updated + """ + + def __init__( + self, + id: int, + external_id: str, + simulator_external_id: str, + heartbeat: int, + data_set_id: int, + connector_version: str, + log_id: int, + active: bool, + created_time: int, + last_updated_time: int, + license_status: str | None = None, + simulator_version: str | None = None, + license_last_checked_time: int | None = None, + connector_status: str | None = None, + connector_status_updated_time: int | None = None, + ) -> None: + self.id = id + self.log_id = log_id + self.external_id = external_id + self.simulator_external_id = simulator_external_id + self.heartbeat = heartbeat + self.data_set_id = data_set_id + self.connector_version = connector_version + self.license_status = license_status + self.simulator_version = simulator_version + self.license_last_checked_time = license_last_checked_time + self.connector_status = connector_status + self.connector_status_updated_time = connector_status_updated_time + self.active = active + self.created_time = created_time + self.last_updated_time = last_updated_time + + @classmethod + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + return cls( + id=resource["id"], + active=resource["active"], + log_id=resource["logId"], + external_id=resource["externalId"], + simulator_external_id=resource["simulatorExternalId"], + heartbeat=resource["heartbeat"], + data_set_id=resource["dataSetId"], + connector_version=resource["connectorVersion"], + license_status=resource.get("licenseStatus"), + simulator_version=resource.get("simulatorVersion"), + license_last_checked_time=resource.get("licenseLastCheckedTime"), + connector_status=resource.get("connectorStatus"), + connector_status_updated_time=resource.get("connectorStatusUpdatedTime"), + created_time=resource["createdTime"], + last_updated_time=resource["lastUpdatedTime"], + ) + + +class SimulatorIntegrationList(CogniteResourceList[SimulatorIntegration], IdTransformerMixin): + _RESOURCE = SimulatorIntegration diff --git a/cognite/client/testing.py b/cognite/client/testing.py index 2a7be50abd..f014c1eae8 100644 --- a/cognite/client/testing.py +++ b/cognite/client/testing.py @@ -43,6 +43,8 @@ from cognite.client._api.raw import RawAPI, RawDatabasesAPI, RawRowsAPI, RawTablesAPI from cognite.client._api.relationships import RelationshipsAPI from cognite.client._api.sequences import SequencesAPI, SequencesDataAPI +from cognite.client._api.simulators import SimulatorsAPI +from cognite.client._api.simulators.integrations import SimulatorIntegrationsAPI from cognite.client._api.synthetic_time_series import SyntheticDatapointsAPI from cognite.client._api.templates import ( TemplateGroupsAPI, @@ -142,6 +144,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.relationships = MagicMock(spec_set=RelationshipsAPI) + self.simulators = MagicMock(spec=SimulatorsAPI) + self.simulators.integrations = MagicMock(spec_set=SimulatorIntegrationsAPI) + self.sequences = MagicMock(spec=SequencesAPI) self.sequences.data = MagicMock(spec_set=SequencesDataAPI) diff --git a/tests/tests_integration/test_api/test_simulators/conftest.py b/tests/tests_integration/test_api/test_simulators/conftest.py new file mode 100644 index 0000000000..cd93be94ff --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/conftest.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import time + +import pytest + +from cognite.client._cognite_client import CogniteClient +from cognite.client.data_classes.data_sets import DataSetWrite +from tests.tests_integration.test_api.test_simulators.seed.data import resource_names, simulator, simulator_integration + + +@pytest.fixture(scope="session") +def seed_resource_names(cognite_client: CogniteClient) -> dict[str, str]: + seed_data_set_external_id = resource_names["simulator_test_data_set_external_id"] + data_set = cognite_client.data_sets.retrieve(external_id=seed_data_set_external_id) + if not data_set: + data_sets = cognite_client.data_sets.create( + [DataSetWrite(external_id=seed_data_set_external_id, name=seed_data_set_external_id)] + ) + data_set = data_sets[0] + resource_names["simulator_test_data_set_id"] = data_set.id + return resource_names.copy() + + +@pytest.fixture(scope="session") +def seed_simulator(cognite_client: CogniteClient, seed_resource_names) -> None: + simulator_external_id = seed_resource_names["simulator_external_id"] + simulators = cognite_client.simulators.list(limit=None) + if not simulators.get(external_id=simulator_external_id): + cognite_client.simulators._post("/simulators", json={"items": [simulator]}) + + +@pytest.fixture(scope="session") +def seed_simulator_integration(cognite_client: CogniteClient, seed_simulator, seed_resource_names) -> None: + simulator_integrations = cognite_client.simulators.integrations.list(limit=None) + if not simulator_integrations.get(external_id=simulator_integration["externalId"]): + simulator_integration["heartbeat"] = int(time.time() * 1000) + simulator_integration["dataSetId"] = seed_resource_names["simulator_test_data_set_id"] + cognite_client.simulators._post( + "/simulators/integrations", + json={"items": [simulator_integration]}, + ) + else: + integration = simulator_integrations.get(external_id=simulator_integration["externalId"]) + # update hearbeat instead + cognite_client.simulators.integrations._post( + "/simulators/integrations/update", + json={"items": [{"id": integration.id, "update": {"heartbeat": {"set": int(time.time() * 1000)}}}]}, + ) diff --git a/tests/tests_integration/test_api/test_simulators/seed/data.py b/tests/tests_integration/test_api/test_simulators/seed/data.py new file mode 100644 index 0000000000..757d286444 --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/seed/data.py @@ -0,0 +1,216 @@ +# This file contains the data used to seed the test environment for the simulator tests +data_set_external_id = "sdk_tests_dwsim1" + +resource_names = { + "simulator_external_id": "py_sdk_integration_tests", + "simulator_integration_external_id": "py_sdk_integration_tests_connector", + "simulator_model_external_id": "py_sdk_integration_tests_model", + "simulator_model_revision_external_id": "pysdk_model_revision", + "simulator_model_file_external_id": "ShowerMixer_simulator_model_file_3", + "simulator_routine_external_id": "pysdk_routine", + "simulator_routine_revision_external_id": "pysdk_routine_revision", + "simulator_test_data_set_id": None, + "simulator_test_data_set_external_id": data_set_external_id, +} + +simulator = { + "name": resource_names["simulator_external_id"], + "externalId": resource_names["simulator_external_id"], + "fileExtensionTypes": ["dwxmz"], + "modelTypes": [{"name": "Steady State", "key": "SteadyState"}], + "stepFields": [ + { + "stepType": "get/set", + "fields": [ + { + "name": "objectName", + "label": "Simulation Object Name", + "info": "Enter the name of the DWSIM object, i.e. Feed", + }, + { + "name": "objectProperty", + "label": "Simulation Object Property", + "info": "Enter the property of the DWSIM object, i.e. Temperature", + }, + ], + }, + { + "stepType": "command", + "fields": [ + { + "name": "command", + "label": "Command", + "info": "Select a command", + "options": [{"label": "Solve Flowsheet", "value": "Solve"}], + } + ], + }, + ], + "unitQuantities": [ + { + "name": "mass", + "label": "Mass", + "units": [{"label": "kg", "name": "kg"}, {"label": "g", "name": "g"}, {"label": "lb", "name": "lb"}], + }, + { + "name": "time", + "label": "Time", + "units": [{"label": "s", "name": "s"}, {"label": "min.", "name": "min."}, {"label": "h", "name": "h"}], + }, + { + "name": "accel", + "label": "Acceleration", + "units": [ + {"label": "m/s2", "name": "m/s2"}, + {"label": "cm/s2", "name": "cm/s2"}, + {"label": "ft/s2", "name": "ft/s2"}, + ], + }, + { + "name": "force", + "label": "Force", + "units": [ + {"label": "N", "name": "N"}, + {"label": "dyn", "name": "dyn"}, + {"label": "kgf", "name": "kgf"}, + {"label": "lbf", "name": "lbf"}, + ], + }, + { + "name": "volume", + "label": "Volume", + "units": [ + {"label": "m3", "name": "m3"}, + {"label": "cm3", "name": "cm3"}, + {"label": "L", "name": "L"}, + {"label": "ft3", "name": "ft3"}, + {"label": "bbl", "name": "bbl"}, + {"label": "gal[US]", "name": "gal[US]"}, + {"label": "gal[UK]", "name": "gal[UK]"}, + ], + }, + { + "name": "density", + "label": "Density", + "units": [ + {"label": "kg/m3", "name": "kg/m3"}, + {"label": "g/cm3", "name": "g/cm3"}, + {"label": "lbm/ft3", "name": "lbm/ft3"}, + ], + }, + { + "name": "diameter", + "label": "Diameter", + "units": [{"label": "mm", "name": "mm"}, {"label": "in", "name": "in"}], + }, + { + "name": "distance", + "label": "Distance", + "units": [{"label": "m", "name": "m"}, {"label": "ft", "name": "ft"}, {"label": "cm", "name": "cm"}], + }, + { + "name": "heatflow", + "label": "Heat Flow", + "units": [ + {"label": "kW", "name": "kW"}, + {"label": "kcal/h", "name": "kcal/h"}, + {"label": "BTU/h", "name": "BTU/h"}, + {"label": "BTU/s", "name": "BTU/s"}, + {"label": "cal/s", "name": "cal/s"}, + {"label": "HP", "name": "HP"}, + {"label": "kJ/h", "name": "kJ/h"}, + {"label": "kJ/d", "name": "kJ/d"}, + {"label": "MW", "name": "MW"}, + {"label": "W", "name": "W"}, + {"label": "BTU/d", "name": "BTU/d"}, + {"label": "MMBTU/d", "name": "MMBTU/d"}, + {"label": "MMBTU/h", "name": "MMBTU/h"}, + {"label": "kcal/s", "name": "kcal/s"}, + {"label": "kcal/h", "name": "kcal/h"}, + {"label": "kcal/d", "name": "kcal/d"}, + ], + }, + { + "name": "pressure", + "label": "Pressure", + "units": [ + {"label": "Pa", "name": "Pa"}, + {"label": "atm", "name": "atm"}, + {"label": "kgf/cm2", "name": "kgf/cm2"}, + {"label": "kgf/cm2g", "name": "kgf/cm2g"}, + {"label": "lbf/ft2", "name": "lbf/ft2"}, + {"label": "kPa", "name": "kPa"}, + {"label": "kPag", "name": "kPag"}, + {"label": "bar", "name": "bar"}, + {"label": "barg", "name": "barg"}, + {"label": "ftH2O", "name": "ftH2O"}, + {"label": "inH2O", "name": "inH2O"}, + {"label": "inHg", "name": "inHg"}, + {"label": "mbar", "name": "mbar"}, + {"label": "mH2O", "name": "mH2O"}, + {"label": "mmH2O", "name": "mmH2O"}, + {"label": "mmHg", "name": "mmHg"}, + {"label": "MPa", "name": "MPa"}, + {"label": "psi", "name": "psi"}, + {"label": "psig", "name": "psig"}, + ], + }, + { + "name": "velocity", + "label": "Velocity", + "units": [ + {"label": "m/s", "name": "m/s"}, + {"label": "cm/s", "name": "cm/s"}, + {"label": "mm/s", "name": "mm/s"}, + {"label": "km/h", "name": "km/h"}, + {"label": "ft/h", "name": "ft/h"}, + {"label": "ft/min", "name": "ft/min"}, + {"label": "ft/s", "name": "ft/s"}, + {"label": "in/s", "name": "in/s"}, + ], + }, + { + "name": "temperature", + "label": "Temperature", + "units": [ + {"label": "K", "name": "K"}, + {"label": "R", "name": "R"}, + {"label": "C", "name": "C"}, + {"label": "F", "name": "F"}, + ], + }, + { + "name": "volumetricFlow", + "label": "Volumetric Flow", + "units": [ + {"label": "m3/h", "name": "m3/h"}, + {"label": "cm3/s", "name": "cm3/s"}, + {"label": "L/h", "name": "L/h"}, + {"label": "L/min", "name": "L/min"}, + {"label": "L/s", "name": "L/s"}, + {"label": "ft3/h", "name": "ft3/h"}, + {"label": "ft3/min", "name": "ft3/min"}, + {"label": "ft3/s", "name": "ft3/s"}, + {"label": "gal[US]/h", "name": "gal[US]/h"}, + {"label": "gal[US]/min", "name": "gal[US]/min"}, + {"label": "gal[US]/s", "name": "gal[US]/s"}, + {"label": "gal[UK]/h", "name": "gal[UK]/h"}, + {"label": "gal[UK]/min", "name": "gal[UK]/min"}, + {"label": "gal[UK]/s", "name": "gal[UK]/s"}, + ], + }, + ], +} + +simulator_integration = { + "externalId": resource_names["simulator_integration_external_id"], + "simulatorExternalId": resource_names["simulator_external_id"], + "heartbeat": 0, + "dataSetId": resource_names["simulator_test_data_set_id"], + "connectorVersion": "1.0.0", + "simulatorVersion": "1.0.0", + "licenseStatus": "AVAILABLE", + "licenseLastCheckedTime": 0, + "connectorStatus": "IDLE", + "connectorStatusUpdatedTime": 0, +} diff --git a/tests/tests_integration/test_api/test_simulators/test_integrations.py b/tests/tests_integration/test_api/test_simulators/test_integrations.py new file mode 100644 index 0000000000..e365f91a97 --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/test_integrations.py @@ -0,0 +1,54 @@ +import time + +import pytest + +from cognite.client._cognite_client import CogniteClient +from cognite.client.data_classes.simulators.filters import SimulatorIntegrationFilter +from cognite.client.utils._text import random_string +from tests.tests_integration.test_api.test_simulators.seed.data import simulator_integration + + +class TestSimulatorIntegrations: + @pytest.mark.usefixtures("seed_resource_names", "seed_simulator_integration") + def test_list_integrations(self, cognite_client: CogniteClient) -> None: + integrations = cognite_client.simulators.integrations.list(limit=5) + + assert len(integrations) > 0 + + def test_filter_integrations(self, cognite_client: CogniteClient, seed_resource_names) -> None: + all_integrations = cognite_client.simulators.integrations.list() + active_integrations = cognite_client.simulators.integrations.list( + filter=SimulatorIntegrationFilter(active=True) + ) + + filtered_integrations = cognite_client.simulators.integrations.list( + filter=SimulatorIntegrationFilter(simulator_external_ids=[seed_resource_names["simulator_external_id"]]) + ) + + assert len(all_integrations) > 0 + assert filtered_integrations[0].external_id == seed_resource_names["simulator_integration_external_id"] + # assert filtered_integrations[0].data_set_id == seed_resource_names["simulator_test_data_set_id"] + assert filtered_integrations[0].active is True + assert filtered_integrations[0].created_time is not None + assert filtered_integrations[0].last_updated_time is not None + + assert len(active_integrations) > 0 + assert len(filtered_integrations) > 0 + + def test_delete_integrations(self, cognite_client: CogniteClient, seed_resource_names) -> None: + simulator_integration["heartbeat"] = int(time.time() * 1000) + simulator_integration["externalId"] = random_string(50) + simulator_integration["dataSetId"] = seed_resource_names["simulator_test_data_set_id"] + + cognite_client.simulators._post( + "/simulators/integrations", + json={"items": [simulator_integration]}, + ) + + all_integrations = cognite_client.simulators.integrations.list(limit=None) + assert all_integrations.get(external_id=simulator_integration["externalId"]) is not None + + cognite_client.simulators.integrations.delete(external_ids=simulator_integration["externalId"]) + + all_integrations = cognite_client.simulators.integrations.list(limit=None) + assert all_integrations.get(external_id=simulator_integration["externalId"]) is None diff --git a/tests/tests_integration/test_api/test_simulators/test_simulators.py b/tests/tests_integration/test_api/test_simulators/test_simulators.py new file mode 100644 index 0000000000..6030c438eb --- /dev/null +++ b/tests/tests_integration/test_api/test_simulators/test_simulators.py @@ -0,0 +1,13 @@ +import pytest + +from cognite.client._cognite_client import CogniteClient + + +@pytest.mark.usefixtures("seed_simulator") +class TestSimulators: + def test_list_simulators(self, cognite_client: CogniteClient) -> None: + simulators = cognite_client.simulators.list(limit=5) + for simulator in simulators: + assert simulator.id is not None + + assert len(simulators) > 0 diff --git a/tests/tests_unit/test_api_client.py b/tests/tests_unit/test_api_client.py index c27bb4e6ea..0cfbb30075 100644 --- a/tests/tests_unit/test_api_client.py +++ b/tests/tests_unit/test_api_client.py @@ -1366,6 +1366,9 @@ def test_is_retryable_resource_api_endpoints(self, api_client_with_token, method ("POST", "https://api.cognitedata.com/api/v1/projects/bla/context/diagram/detect", True), ("GET", "https://api.cognitedata.com/api/v1/projects/bla/context/diagram/convert/123", True), ("GET", "https://api.cognitedata.com/api/v1/projects/bla/context/diagram/detect/456", True), + # Simulators + ("POST", "https://api.cognitedata.com/api/v1/projects/bla/simulators/list", True), + ("POST", "https://api.cognitedata.com/api/v1/projects/bla/simulators/delete", False), ] ), )