diff --git a/cognite/client/_api/simulators/simulation_runs.py b/cognite/client/_api/simulators/simulation_runs.py index d787d33fc1..9031e78874 100644 --- a/cognite/client/_api/simulators/simulation_runs.py +++ b/cognite/client/_api/simulators/simulation_runs.py @@ -5,8 +5,9 @@ from cognite.client._api_client import APIClient from cognite.client._constants import DEFAULT_LIMIT_READ from cognite.client.data_classes.simulators.filters import SimulationRunsFilter -from cognite.client.data_classes.simulators.simulators import SimulationRun, SimulationRunsList +from cognite.client.data_classes.simulators.simulators import SimulationRun, SimulationRunCall, SimulationRunsList from cognite.client.utils._experimental import FeaturePreviewWarning +from cognite.client.utils._identifier import IdentifierSequence if TYPE_CHECKING: from cognite.client import ClientConfig, CogniteClient @@ -14,6 +15,7 @@ class SimulatorRunsAPI(APIClient): _RESOURCE_PATH = "/simulators/runs" + _RESOURCE_PATH_RUN = "/simulators/run" def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: super().__init__(config, api_version, cognite_client) @@ -57,3 +59,45 @@ def list( if isinstance(filter, dict) else None, ) + + def retrieve(self, id: int | None = None) -> SimulationRun | None: + """` Retrieve a simulation run + + Args: + id (int | None): ID + + Returns: + SimulationRun | None: The simulation run + + Examples: + + """ + identifier = IdentifierSequence.load(ids=id).as_singleton() + return self._retrieve_multiple(list_cls=SimulationRunsList, resource_cls=SimulationRun, identifiers=identifier) + + def run( + self, + run_call: SimulationRunCall | None = None, + wait: bool = False, + ) -> SimulationRun: + """`Run a simulation. + + Args: + run_call (SimulationRunCall | None): No description. + wait (bool): No description. + + Returns: + SimulationRun: A simulation run object. + + """ + url = self._RESOURCE_PATH_RUN + try: + res = self._post(url, json={"items": [run_call]}) + response = res.json() + run_response = response["items"][0] + except (KeyError, IndexError, ValueError) as e: + raise RuntimeError("Failed to parse simulation run response") from e + run = SimulationRun._load(run_response, cognite_client=self._cognite_client) + if wait: + run.wait() + return run diff --git a/cognite/client/_api/simulators/simulator_models.py b/cognite/client/_api/simulators/simulator_models.py index b02c5449a1..b84ed1a3fb 100644 --- a/cognite/client/_api/simulators/simulator_models.py +++ b/cognite/client/_api/simulators/simulator_models.py @@ -1,19 +1,25 @@ from __future__ import annotations from collections.abc import Sequence -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, overload from cognite.client._api_client import APIClient from cognite.client._constants import DEFAULT_LIMIT_READ from cognite.client.data_classes.simulators.filters import SimulatorModelRevisionsFilter, SimulatorModelsFilter from cognite.client.data_classes.simulators.simulators import ( SimulatorModel, + SimulatorModelCore, SimulatorModelList, SimulatorModelRevision, + SimulatorModelRevisionCore, SimulatorModelRevisionList, + SimulatorModelRevisionWrite, + SimulatorModelUpdate, + SimulatorModelWrite, ) from cognite.client.utils._experimental import FeaturePreviewWarning from cognite.client.utils._identifier import IdentifierSequence +from cognite.client.utils._validation import assert_type from cognite.client.utils.useful_types import SequenceNotStr if TYPE_CHECKING: @@ -136,6 +142,9 @@ class SimulatorModelsAPI(APIClient): def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None: super().__init__(config, api_version, cognite_client) + self._CREATE_LIMIT = 1 + self._DELETE_LIMIT = 1 + self._UPDATE_LIMIT = 1 self.revisions = SimulatorModelRevisionsAPI(config, api_version, cognite_client) self._warning = FeaturePreviewWarning( api_maturity="General Availability", sdk_maturity="alpha", feature_name="Simulators" @@ -168,7 +177,7 @@ def list( return self._list( method="POST", limit=limit, - url_path="/simulators/models/list", + url_path=self._RESOURCE_PATH + "/list", resource_cls=SimulatorModel, list_cls=SimulatorModelList, filter=filter.dump() @@ -209,5 +218,140 @@ def retrieve(self, id: int | None = None, external_id: str | None = None) -> Sim list_cls=SimulatorModelList, resource_cls=SimulatorModel, identifiers=identifiers, - resource_path="/simulators/models", + resource_path=self._RESOURCE_PATH, + ) + + @overload + def create(self, models: Sequence[SimulatorModel]) -> SimulatorModelList: ... + + @overload + def create(self, models: SimulatorModel | SimulatorModelWrite) -> SimulatorModelList: ... + + def create( + self, models: SimulatorModel | SimulatorModelWrite | Sequence[SimulatorModel] | Sequence[SimulatorModelWrite] + ) -> SimulatorModel | SimulatorModelList: + """`Create simulator models `_ + + You can create an arbitrary number of simulator models, and the SDK will split the request into multiple requests. + + Args: + models (SimulatorModel | SimulatorModelWrite | Sequence[SimulatorModel] | Sequence[SimulatorModelWrite]): No description. + + Returns: + SimulatorModel | SimulatorModelList: Created simulator model(s) + + Examples: + + Create new simulator models: + + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes import SimulatorModelWrite + >>> client = CogniteClient() + >>> models = [SimulatorModelWrite(name="model1"), SimulatorModelWrite(name="model2")] + >>> res = client.simulators.models.create(models) + + """ + assert_type(models, "simulator_model", [SimulatorModelCore, Sequence]) + + return self._create_multiple( + list_cls=SimulatorModelList, + resource_cls=SimulatorModel, + items=models, + input_resource_cls=SimulatorModelWrite, + resource_path=self._RESOURCE_PATH, + ) + + def delete( + self, + ids: int | Sequence[int] | None = None, + external_ids: str | SequenceNotStr[str] | None = None, + ) -> None: + """`Delete simulator models `_ + + Args: + ids (int | Sequence[int] | None): No description. + external_ids (str | SequenceNotStr[str] | None): External id/ids of the models to delete. + Examples: + + Delete models by id or external id: + + >>> from cognite.client import CogniteClient + >>> client = CogniteClient() + >>> client.simulators.delete(ids=[1,2,3], external_ids="3") + """ + self._delete_multiple( + identifiers=IdentifierSequence.load(ids=ids, external_ids=external_ids), + wrap_ids=True, + resource_path=self._RESOURCE_PATH, + ) + + @overload + def create_revisions( + self, revisions: Sequence[SimulatorModelRevision] + ) -> SimulatorModelRevision | SimulatorModelRevisionList: ... + + @overload + def create_revisions( + self, revisions: SimulatorModelRevision | SimulatorModelRevisionWrite + ) -> SimulatorModelRevision | SimulatorModelRevisionList: ... + + def create_revisions( + self, + revisions: SimulatorModelRevision + | SimulatorModelRevisionWrite + | Sequence[SimulatorModelRevision] + | Sequence[SimulatorModelRevisionWrite], + ) -> SimulatorModelRevision | SimulatorModelRevisionList: + """`Create one or more simulator model revisions. `_ + + You can create an arbitrary number of simulator model revisions, and the SDK will split the request into multiple requests. + + Args: + revisions (SimulatorModelRevision | SimulatorModelRevisionWrite | Sequence[SimulatorModelRevision] | Sequence[SimulatorModelRevisionWrite]): Simulator model or list of Simulator models to create. + + Returns: + SimulatorModelRevision | SimulatorModelRevisionList: Created simulator model(s) + + Examples: + + Create new simulator models: + + >>> from cognite.client import CogniteClient + >>> from cognite.client.data_classes import SimulatorModelRevision + >>> client = CogniteClient() + >>> models = [SimulatorModelRevision(external_id="model1"), SimulatorModelRevision(external_id="model2")] + >>> res = client.simulators.models.create_revision(models) + + """ + assert_type(revisions, "simulator_model_revision", [SimulatorModelRevisionCore, SimulatorModelRevisionWrite]) + + return self._create_multiple( + list_cls=SimulatorModelRevisionList, + resource_cls=SimulatorModelRevision, + items=revisions, + input_resource_cls=SimulatorModelRevisionWrite, + resource_path=self._RESOURCE_PATH + "/revisions", + ) + + @overload + def update( + self, + item: Sequence[SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate], + ) -> SimulatorModel: ... + + @overload + def update( + self, + item: SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate, + ) -> SimulatorModel: ... + + def update( + self, + item: SimulatorModel + | SimulatorModelWrite + | SimulatorModelUpdate + | Sequence[SimulatorModel | SimulatorModelWrite | SimulatorModelUpdate], + ) -> SimulatorModel | SimulatorModelList: + return self._update_multiple( + list_cls=SimulatorModelList, resource_cls=SimulatorModel, update_cls=SimulatorModelUpdate, items=item ) diff --git a/cognite/client/data_classes/simulators/simulators.py b/cognite/client/data_classes/simulators/simulators.py index 28996ad83b..45bc8e12ca 100644 --- a/cognite/client/data_classes/simulators/simulators.py +++ b/cognite/client/data_classes/simulators/simulators.py @@ -1,17 +1,22 @@ from __future__ import annotations +import time from abc import ABC from collections.abc import Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from typing_extensions import Self from cognite.client.data_classes._base import ( CogniteObject, + CognitePrimitiveUpdate, + CogniteResource, CogniteResourceList, + CogniteUpdate, ExternalIDTransformerMixin, IdTransformerMixin, + PropertySpec, WriteableCogniteResource, WriteableCogniteResourceList, ) @@ -411,7 +416,9 @@ class SimulatorCore(WriteableCogniteResource["SimulatorWrite"], ABC): 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. #### + flows between CDF and external simulation tools, ensuring consistency and reliability in data handling. + + #### Limitations: - A project can have a maximum of 100 simulators This is the read/response format of the simulator. @@ -740,7 +747,9 @@ def __init__( self.status_message = status_message @classmethod - def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + def _load( + cls: type[T_SimulatorModelRevision], resource: dict[str, Any], cognite_client: CogniteClient | None = None + ) -> T_SimulatorModelRevision: instance = super()._load(resource, cognite_client) return instance @@ -779,6 +788,9 @@ def _load( ) +T_SimulatorModelRevision = TypeVar("T_SimulatorModelRevision", bound=SimulatorModelRevisionCore) + + class SimulatorModelRevision(SimulatorModelRevisionCore): """ @@ -787,7 +799,7 @@ class SimulatorModelRevision(SimulatorModelRevisionCore): Args: external_id (str | None): External id of the simulator model revision - simulator_external_id (str | None): No description. + simulator_external_id (str | None): External id of the simulator. model_external_id (str | None): External id of the associated simulator model data_set_id (int | None): The id of the dataset associated with the simulator model revision file_id (int | None): The id of the file associated with the simulator model revision @@ -842,7 +854,7 @@ def __init__( self.last_updated_time: int = last_updated_time # type: ignore @classmethod - def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> SimulatorModelRevision: instance = super()._load(resource, cognite_client) return instance @@ -1032,7 +1044,9 @@ def __init__( self.description = description @classmethod - def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: + def _load( + cls: type[T_SimulatorModel], resource: dict[str, Any], cognite_client: CogniteClient | None = None + ) -> T_SimulatorModel: instance = super()._load(resource, cognite_client) return instance @@ -1040,6 +1054,9 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: return super().dump(camel_case=camel_case) +T_SimulatorModel = TypeVar("T_SimulatorModel", bound=SimulatorModelCore) + + class SimulatorModelWrite(SimulatorModelCore): def __init__( self, @@ -1074,6 +1091,9 @@ def as_write(self) -> SimulatorModelWrite: """Returns self.""" return self + def dump(self, camel_case: bool = True) -> dict[str, Any]: + return super().dump(camel_case) + class SimulatorModel(SimulatorModelCore): """ @@ -1152,7 +1172,28 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: return super().dump(camel_case) -class SimulationRunCore(WriteableCogniteResource["SimulationRunWrite"], ABC): +class SimulatorModelUpdate(CogniteUpdate): + class _PrimitiveModelUpdate(CognitePrimitiveUpdate): + def set(self, value: Any) -> None: + self._set(value) + + @property + def name(self) -> _PrimitiveModelUpdate: + return SimulatorModelUpdate._PrimitiveModelUpdate(self, "name") + + @property + def description(self) -> _PrimitiveModelUpdate: + return SimulatorModelUpdate._PrimitiveModelUpdate(self, "description") + + @classmethod + def _get_update_properties(cls, item: CogniteResource | None = None) -> list[PropertySpec]: + return [ + PropertySpec("name"), + PropertySpec("description"), + ] + + +class SimulationRunCore(CogniteResource, ABC): """""" """_summary_ @@ -1199,66 +1240,10 @@ def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = return instance def dump(self, camel_case: bool = True) -> dict[str, Any]: - return super().dump(camel_case) + return super().dump(camel_case=camel_case) -class SimulationRunWrite(SimulationRunCore): - def __init__( - self, - simulator_external_id: str | None = None, - simulator_integration_external_id: str | None = None, - model_external_id: str | None = None, - model_revision_external_id: str | None = None, - routine_external_id: str | None = None, - routine_revision_external_id: str | None = None, - run_time: int | None = None, - simulation_time: int | None = None, - status: str | None = None, - status_message: str | None = None, - data_set_id: int | None = None, - run_type: str | None = None, - user_id: str | None = None, - log_id: int | None = None, - ) -> None: - super().__init__( - simulator_external_id=simulator_external_id, - simulator_integration_external_id=simulator_integration_external_id, - model_external_id=model_external_id, - model_revision_external_id=model_revision_external_id, - routine_external_id=routine_external_id, - routine_revision_external_id=routine_revision_external_id, - run_time=run_time, - simulation_time=simulation_time, - status=status, - status_message=status_message, - data_set_id=data_set_id, - run_type=run_type, - user_id=user_id, - log_id=log_id, - ) - - @classmethod - def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self: - return cls( - simulator_external_id=resource["simulatorExternalId"], - simulator_integration_external_id=resource["simulatorIntegrationExternalId"], - model_external_id=resource["modelExternalId"], - model_revision_external_id=resource["modelRevisionExternalId"], - routine_external_id=resource["routineExternalId"], - routine_revision_external_id=resource["routineRevisionExternalId"], - run_time=resource["runTime"], - simulation_time=resource["simulationTime"], - status=resource["status"], - status_message=resource.get("statusMessage"), - data_set_id=resource["dataSetId"], - run_type=resource["runType"], - user_id=resource["userId"], - log_id=resource["logId"], - ) - - def as_write(self) -> SimulationRunWrite: - """Returns self.""" - return self +T_SimulationRun = TypeVar("T_SimulationRun", bound=SimulationRunCore) class SimulationRun(SimulationRunCore): @@ -1272,13 +1257,10 @@ class SimulationRun(SimulationRunCore): Simulation runs provide a historical record of the simulations performed, allowing users to analyze and compare different runs, track changes over time, and make informed decisions based on the simulation results. - Limitations: - * A retention policy is in place for simulation runs, allowing up to 100000 entries. - * Once this limit is reached, the oldest runs will be deleted to accommodate new runs. - This is the read/response format of a simulation run. Args: + cognite_client (CogniteClient | None): An instance of the Cognite client. simulator_external_id (str | None): External id of the associated simulator simulator_integration_external_id (str | None): External id of the associated simulator integration model_external_id (str | None): External id of the associated simulator model @@ -1301,6 +1283,7 @@ class SimulationRun(SimulationRunCore): def __init__( self, + cognite_client: CogniteClient | None = None, simulator_external_id: str | None = None, simulator_integration_external_id: str | None = None, model_external_id: str | None = None, @@ -1340,9 +1323,25 @@ def __init__( # correct here (i.e. int and not Optional[int]), we force the type to be int rather than # Optional[int]. # TODO: In the next major version we can make these properties required in the constructor - self.id: int = id # type: ignore - self.created_time: int = created_time # type: ignore - self.last_updated_time: int = last_updated_time # type: ignore + self.id = id # type: ignore + self.created_time = created_time # type: ignore + self.last_updated_time = last_updated_time # type: ignore + self.simulator_external_id = simulator_external_id + self.simulator_integration_external_id = simulator_integration_external_id + self.model_external_id = model_external_id + self.model_revision_external_id = model_revision_external_id + self.routine_external_id = routine_external_id + self.routine_revision_external_id = routine_revision_external_id + self.run_time = run_time + self.simulation_time = simulation_time + self.status = status + self.status_message = status_message + self.data_set_id = data_set_id + self.run_type = run_type + self.user_id = user_id + self.log_id = log_id + if cognite_client is not None: + self._cognite_client = cognite_client @classmethod def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> SimulationRun: @@ -1351,27 +1350,28 @@ def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> S def dump(self, camel_case: bool = True) -> dict[str, Any]: return super().dump(camel_case=camel_case) - def as_write(self): - return SimulationRunWrite( - simulator_external_id=self.simulator_external_id, - simulator_integration_external_id=self.simulator_integration_external_id, - model_external_id=self.model_external_id, - model_revision_external_id=self.model_revision_external_id, - routine_external_id=self.routine_external_id, - routine_revision_external_id=self.routine_revision_external_id, - run_time=self.run_time, - simulation_time=self.simulation_time, - status=self.status, - status_message=self.status_message, - data_set_id=self.data_set_id, - run_type=self.run_type, - user_id=self.user_id, - log_id=self.log_id, - ) - def __hash__(self) -> int: return hash(self.id) + def update(self) -> None: + latest = self._cognite_client.simulators.runs.retrieve(id=self.id) + if latest is None: + raise RuntimeError("Unable to update the simulation run object (it was not found)") + self.status = latest.status + + def wait(self) -> None: + while self.status is not None and self.status.lower() == "ready": + self.update() + time.sleep(1.0) + + # def get_logs(self) -> SimulationRunLog: + # """`Retrieve logs for this simulation call. + + # Returns: + # Log for the simulation run. + # """ + # return self._cognite_client.simulation.runs.get_logs(call_id=call_id, function_id=function_id) + class SimulatorRoutineCore(WriteableCogniteResource["SimulatorRoutineWrite"], ABC): """ @@ -1385,11 +1385,6 @@ class SimulatorRoutineCore(WriteableCogniteResource["SimulatorRoutineWrite"], AB Each model can have multiple routines, each performing different objectives such as calculating optimal operation setpoints, forecasting production, benchmarking asset performance, and more. - Limitations: - - Each simulator model can have a maximum of 10 simulator routines - - Each simulator routine can have a maximum of 10 revisions - This is the read/response format of a simulator routine. Args: @@ -1600,12 +1595,25 @@ def as_write(self) -> SimulatorModelRevisionWriteList: ) -class SimulationRunWriteList(CogniteResourceList[SimulationRunWrite], ExternalIDTransformerMixin): - _RESOURCE = SimulationRunWrite +class SimulationRunsList(CogniteResourceList[SimulationRun], IdTransformerMixin): + _RESOURCE = SimulationRun + +class SimulationRunCall(CogniteResource): + """A representation of a Simulation Run call. -class SimulationRunsList(WriteableCogniteResourceList[SimulationRunWrite, SimulationRun], IdTransformerMixin): - _RESOURCE = SimulationRun + Args: + routine_external_id (str | None): No description. + routine_revision_external_id (str | None): No description. + """ + + def __init__( + self, + routine_external_id: str | None = None, + routine_revision_external_id: str | None = None, + ) -> None: + self.routine_external_id = routine_external_id + self.routine_revision_external_id = routine_revision_external_id - def as_write(self) -> SimulationRunWriteList: - return SimulationRunWriteList([a.as_write() for a in self.data], cognite_client=self._get_cognite_client()) + def dump(self, camel_case: bool = True) -> dict[str, Any]: + return super().dump(camel_case=camel_case) diff --git a/tests/tests_integration/test_api/test_simulators/seed/data.py b/tests/tests_integration/test_api/test_simulators/seed/data.py index b3db2c021a..8b0aa56ece 100644 --- a/tests/tests_integration/test_api/test_simulators/seed/data.py +++ b/tests/tests_integration/test_api/test_simulators/seed/data.py @@ -1,6 +1,19 @@ +import time + +resource_names = { + "simulator_external_id": "integration_tests_workflow", + "simulator_integration_external_id": "integration_tests_workflow_connector", + "simulator_model_external_id": "integration_tests_workflow_model_01", + "simulator_model_revision_external_id": "integration_tests_workflow_model_revision_1", + "simulator_model_file_external_id": "ShowerMixer_simulator_model_file_3", + "simulator_routine_external_id": "integration_tests_workflow_routine", + "simulator_routine_revision_external_id": "integration_tests_workflow_routine_revision", + "simulator_test_data_set_id": 97552494921583, +} + simulator = { - "name": "DWSIM", - "externalId": "integration_tests_workflow", + "name": resource_names["simulator_external_id"], + "externalId": resource_names["simulator_external_id"], "fileExtensionTypes": ["dwxmz"], "modelTypes": [{"name": "Steady State", "key": "SteadyState"}], "stepFields": [ @@ -164,14 +177,34 @@ {"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": "integration_tests_workflow_connector", - "simulatorExternalId": "integration_tests_workflow", - "heartbeat": 1706396950969, + "externalId": resource_names["simulator_integration_external_id"], + "simulatorExternalId": resource_names["simulator_external_id"], + "heartbeat": int(time.time() * 1000), "dataSetId": 97552494921583, "connectorVersion": "1.0.0", "simulatorVersion": "1.0.0", @@ -183,33 +216,32 @@ simulator_model = { - "externalId": "integration_tests_workflow_model", - "simulatorExternalId": "integration_tests_workflow", + "externalId": resource_names["simulator_model_external_id"], + "simulatorExternalId": resource_names["simulator_external_id"], "name": "Test Simulator Model", "description": "Test Simulator Model Desc", "dataSetId": 97552494921583, - "labels": [{"externalId": "simconfig-labels-PROSPER"}], "type": "SteadyState", } simulator_model_revision = { - "externalId": "integration_tests_workflow_model_revision", - "modelExternalId": "integration_tests_workflow_model", + "externalId": resource_names["simulator_model_revision_external_id"], + "modelExternalId": resource_names["simulator_model_external_id"], "description": "test sim model revision description", "fileId": 00000000000000, } simulator_routine = { - "externalId": "integration_tests_workflow_routine", - "modelExternalId": "integration_tests_workflow_model", - "simulatorIntegrationExternalId": "integration_tests_workflow_connector", + "externalId": resource_names["simulator_routine_external_id"], + "modelExternalId": resource_names["simulator_model_external_id"], + "simulatorIntegrationExternalId": resource_names["simulator_integration_external_id"], "name": "Routine test", "description": "test", } simulator_routine_revision = { - "externalId": "integration_tests_workflow_routine_revision", - "routineExternalId": "integration_tests_workflow_routine", + "externalId": resource_names["simulator_routine_revision_external_id"], + "routineExternalId": resource_names["simulator_routine_external_id"], "configuration": { "schedule": {"enabled": True, "cronExpression": "*/10 * * * *"}, "dataSampling": {"enabled": True, "validationWindow": None, "samplingWindow": 15, "granularity": 1}, diff --git a/tests/tests_integration/test_api/test_simulators/test_simulators.py b/tests/tests_integration/test_api/test_simulators/test_simulators.py index 50bd289dd2..e6f5a12e2f 100644 --- a/tests/tests_integration/test_api/test_simulators/test_simulators.py +++ b/tests/tests_integration/test_api/test_simulators/test_simulators.py @@ -1,8 +1,23 @@ +import datetime +import time + import pytest from cognite.client import CogniteClient -from cognite.client.data_classes.simulators.filters import SimulatorIntegrationFilter +from cognite.client.data_classes.files import FileMetadata +from cognite.client.data_classes.simulators.filters import ( + SimulationRunsFilter, + SimulatorIntegrationFilter, + SimulatorModelRevisionsFilter, + SimulatorModelsFilter, +) +from cognite.client.data_classes.simulators.simulators import ( + SimulationRunCall, + SimulatorModel, + SimulatorModelRevision, +) from tests.tests_integration.test_api.test_simulators.seed.data import ( + resource_names, simulator, simulator_integration, simulator_model, @@ -12,52 +27,97 @@ ) +@pytest.fixture(scope="class") +def seed_resource_names() -> dict[str, str]: + return resource_names + + @pytest.fixture -def add_simulator_resoures(cognite_client: CogniteClient) -> None: - simulator_external_id = "integration_tests_workflow" - simulator_model_file_external_id = "ShowerMixer_simulator_model_file" - - file = cognite_client.files.upload( - path="tests/tests_integration/test_api/test_simulators/seed/data/ShowerMixer.dwxmz", - external_id=simulator_model_file_external_id, - name="ShowerMixer.dwxmz", - data_set_id=97552494921583, +def seed_file(cognite_client: CogniteClient, seed_resource_names) -> FileMetadata | None: + # check if file already exists + file = cognite_client.files.retrieve(external_id=seed_resource_names["simulator_model_file_external_id"]) + if (file is None) or (file is False): + file = cognite_client.files.upload( + path="tests/tests_integration/test_api/test_simulators/seed/ShowerMixer.dwxmz", + external_id=seed_resource_names["simulator_model_file_external_id"], + name="ShowerMixer.dwxmz", + data_set_id=97552494921583, + ) + yield file + + +@pytest.fixture +def seed_simulator(cognite_client: CogniteClient, seed_resource_names) -> None: + simulator_external_id = seed_resource_names["simulator_external_id"] + simulators = cognite_client.simulators.list() + for sim in simulators: + if sim.external_id == simulator_external_id: + return + + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators", + json={"items": [simulator]}, ) - resources = [ - {"url": f"/api/v1/projects/{cognite_client.config.project}/simulators", "seed": simulator}, - { - "url": f"/api/v1/projects/{cognite_client.config.project}/simulators/integrations", - "seed": simulator_integration, - }, - {"url": f"/api/v1/projects/{cognite_client.config.project}/simulators/models", "seed": simulator_model}, - { - "url": f"/api/v1/projects/{cognite_client.config.project}/simulators/models/revisions", - "seed": {**simulator_model_revision, "fileId": file.id}, - }, - {"url": f"/api/v1/projects/{cognite_client.config.project}/simulators/routines", "seed": simulator_routine}, - { - "url": f"/api/v1/projects/{cognite_client.config.project}/simulators/routines/revisions", - "seed": simulator_routine_revision, - }, - ] - - for resource in resources: + +@pytest.fixture +def seed_simulator_integration(cognite_client: CogniteClient, seed_simulator) -> None: + def create_integration(): cognite_client.post( - resource["url"], - json={"items": [resource["seed"]]}, - headers={"cdf-version": "alpha"}, + f"/api/v1/projects/{cognite_client.config.project}/simulators/integrations", + json={"items": [simulator_integration]}, ) - yield None + try: + create_integration() + except Exception: + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/integrations/delete", + json={"items": [{"externalId": simulator_integration["externalId"]}]}, + ) + create_integration() + pass + +@pytest.fixture +def seed_simulator_models(cognite_client: CogniteClient, seed_simulator_integration) -> None: cognite_client.post( - f"/api/v1/projects/{cognite_client.config.project}/simulators/delete", - json={"items": [{"externalId": simulator_external_id}]}, - headers={"cdf-version": "alpha"}, + f"/api/v1/projects/{cognite_client.config.project}/simulators/models", + json={"items": [simulator_model]}, # Post actual simulator models here + ) + + +@pytest.fixture +def seed_simulator_model_revisions(cognite_client: CogniteClient, seed_simulator_models, seed_file) -> None: + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/models/revisions", + json={"items": [{**simulator_model_revision, "fileId": seed_file.id}]}, # Post actual simulator models here + ) + + +@pytest.fixture +def seed_simulator_routines(cognite_client: CogniteClient, seed_simulator_model_revisions) -> None: + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/routines", + json={"items": [simulator_routine]}, + ) + + +@pytest.fixture +def seed_simulator_routine_revisions(cognite_client: CogniteClient, seed_simulator_routines) -> None: + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/routines/revisions", + json={"items": [simulator_routine_revision]}, ) - cognite_client.files.delete(external_id=simulator_model_file_external_id) + +@pytest.fixture(scope="class") +def delete_simulator(cognite_client: CogniteClient, seed_resource_names) -> None: + yield + cognite_client.post( + f"/api/v1/projects/{cognite_client.config.project}/simulators/delete", + json={"items": [{"externalId": seed_resource_names["simulator_external_id"]}]}, + ) class TestSimulators: @@ -91,24 +151,94 @@ def test_filter_integrations(self, cognite_client: CogniteClient) -> None: assert len(all_integrations) != len(dwsim_integrations) +@pytest.mark.usefixtures("seed_resource_names", "seed_simulator", "delete_simulator") class TestSimulatorModels: - def test_list_models(self, cognite_client: CogniteClient) -> None: - models = cognite_client.simulators.models.list(limit=5) + @pytest.mark.usefixtures("seed_simulator_models", "seed_simulator_model_revisions") + def test_list_models(self, cognite_client: CogniteClient, seed_resource_names) -> None: + models = cognite_client.simulators.models.list( + limit=5, filter=SimulatorModelsFilter(simulator_external_ids=[seed_resource_names["simulator_external_id"]]) + ) assert len(models) > 0 - def test_retrieve_model(self, cognite_client: CogniteClient) -> None: + def test_retrieve_model(self, cognite_client: CogniteClient, seed_resource_names) -> None: model = cognite_client.simulators.models.retrieve(external_id="TEST_WORKFLOWS_SIMINT_INTEGRATION_MODEL") assert model is not None assert model.external_id == "TEST_WORKFLOWS_SIMINT_INTEGRATION_MODEL" - def test_list_model_revisions(self, cognite_client: CogniteClient) -> None: - revisions = cognite_client.simulators.models.revisions.list(limit=5) + def test_list_model_revisions(self, cognite_client: CogniteClient, seed_resource_names) -> None: + revisions = cognite_client.simulators.models.list( + limit=5, + filter=SimulatorModelRevisionsFilter( + model_external_ids=[seed_resource_names["simulator_model_external_id"]] + ), + ) assert len(revisions) > 0 - def test_retrieve_model_revision(self, cognite_client: CogniteClient) -> None: - model = cognite_client.simulators.models.revisions.retrieve(external_id="Shower_mixer-1") + def test_retrieve_model_revision(self, cognite_client: CogniteClient, seed_resource_names) -> None: + # TODO : this test is incorrect, it should retrieve model revisions instead of model + model = cognite_client.simulators.models.retrieve( + external_id=seed_resource_names["simulator_model_external_id"] + ) assert model is not None - assert model.external_id == "Shower_mixer-1" + assert model.external_id == seed_resource_names["simulator_model_external_id"] + + @pytest.mark.usefixtures("seed_file", "seed_resource_names") + def test_create_model(self, cognite_client: CogniteClient, seed_file: FileMetadata, seed_resource_names) -> None: + model_external_id_1 = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + model_external_id_2 = datetime.datetime.now().strftime("%Y%m%d%H%M%S2") + models_to_create = [ + SimulatorModel( + name="sdk-test-model1", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id_1, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ), + SimulatorModel( + name="sdk-test-model2", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id_2, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ), + ] + + models_created = cognite_client.simulators.models.create(models_to_create) + + assert models_created is not None + assert len(models_created) == 2 + model_revision_external_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "revision" + model_revision_to_create = SimulatorModelRevision( + external_id=model_revision_external_id, + model_external_id=model_external_id_1, + file_id=seed_file.id, + description="Test revision", + ) + + model_revision_created = cognite_client.simulators.models.create_revisions(model_revision_to_create) + assert model_revision_created is not None + assert model_revision_created.external_id == model_revision_external_id + cognite_client.simulators.models.delete(external_ids=[model_external_id_1, model_external_id_2]) + + def test_update_model(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_external_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + models_to_create = SimulatorModel( + name="sdk-test-model1", + simulator_external_id=seed_resource_names["simulator_external_id"], + external_id=model_external_id, + data_set_id=seed_resource_names["simulator_test_data_set_id"], + type="SteadyState", + ) + + models_created = cognite_client.simulators.models.create(models_to_create) + assert models_created is not None + assert models_created.external_id == model_external_id # Validate external ID + models_created.description = "updated description" # Update the description + models_created.name = "updated name" # Update the name + model_updated = cognite_client.simulators.models.update(models_created) + assert model_updated is not None + assert model_updated.description == "updated description" + assert model_updated.name == "updated name" class TestSimulatorRoutines: @@ -129,6 +259,38 @@ def test_retrieve_routine_revision(self, cognite_client: CogniteClient) -> None: class TestSimulationRuns: - def test_list_runs(self, cognite_client: CogniteClient) -> None: - routines = cognite_client.simulators.runs.list(limit=5) + @pytest.mark.usefixtures("seed_simulator_routine_revisions", "delete_simulator", "seed_resource_names") + def test_create_runs(self, cognite_client: CogniteClient, seed_resource_names) -> None: + integrations = cognite_client.simulators.integrations.list() + assert len(integrations) > 0 + # Check if the seeded simulator integration is alive + integration_to_check = seed_resource_names["simulator_integration_external_id"] + integration = next((x for x in integrations if x.external_id == integration_to_check), None) + assert integration is not None + assert integration.heartbeat >= time.time() - 60 + + run_to_create = SimulationRunCall( + routine_external_id=seed_resource_names["simulator_routine_external_id"], + ) + + run = cognite_client.simulators.runs.run(run_to_create) + assert run is not None + assert run.status == "ready" + + @pytest.mark.usefixtures("seed_resource_names") + def test_list_runs(self, cognite_client: CogniteClient, seed_resource_names) -> None: + model_external_id = seed_resource_names["simulator_model_external_id"] + routines = cognite_client.simulators.runs.list( + limit=5, filter=SimulationRunsFilter(model_external_ids=[model_external_id]) + ) assert len(routines) > 0 + + @pytest.mark.usefixtures("seed_resource_names") + async def test_run_async(self, cognite_client: CogniteClient, seed_resource_names) -> None: + run_to_create = SimulationRunCall( + routine_external_id=seed_resource_names["simulator_routine_external_id"], + ) + + run = cognite_client.simulators.runs.run(run_to_create) + + assert run is not None