From 7cadd2e7edbb948ab32affd7bff1e70d505a6c2f Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 07:56:33 +0200 Subject: [PATCH 1/6] upgrade SimulationRunWrite --- cognite/client/_api/simulators/routines.py | 26 ++++++-- cognite/client/_api/simulators/runs.py | 13 +++- .../client/data_classes/simulators/runs.py | 63 +++++++++++++++++-- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/cognite/client/_api/simulators/routines.py b/cognite/client/_api/simulators/routines.py index 42ea544f0b..dd6b2ec096 100644 --- a/cognite/client/_api/simulators/routines.py +++ b/cognite/client/_api/simulators/routines.py @@ -228,7 +228,9 @@ def list( def run( self, - routine_external_id: str, + routine_external_id: str | None = None, + routine_revision_external_id: str | None = None, + model_revision_external_id: str | None = None, inputs: Sequence[SimulationInputOverride] | None = None, run_time: int | None = None, queue: bool | None = None, @@ -238,10 +240,17 @@ def run( ) -> SimulationRun: """`Run a simulation `_ - Run a simulation for a given simulator routine. + Run a simulation for a given simulator routine. Supports two modes: + 1. By routine external ID only + 2. By routine revision external ID + model revision external ID Args: - routine_external_id (str): External id of the simulator routine to run + routine_external_id (str | None): External id of the simulator routine to run. + Cannot be specified together with routine_revision_external_id and model_revision_external_id. + routine_revision_external_id (str | None): External id of the simulator routine revision to run. + Must be specified together with model_revision_external_id. + model_revision_external_id (str | None): External id of the simulator model revision. + Must be specified together with routine_revision_external_id. inputs (Sequence[SimulationInputOverride] | None): List of input overrides run_time (int | None): Run time in milliseconds. Reference timestamp used for data pre-processing and data sampling. queue (bool | None): Queue the simulation run when connector is down. @@ -253,17 +262,26 @@ def run( SimulationRun: Created simulation run Examples: - Create new simulation run: + Create new simulation run using routine external ID: >>> from cognite.client import CogniteClient >>> client = CogniteClient() >>> run = client.simulators.routines.run( ... routine_external_id="routine1", ... log_severity="Debug" ... ) + + Create new simulation run using routine and model revision external IDs: + >>> run = client.simulators.routines.run( + ... routine_revision_external_id="routine_revision1", + ... model_revision_external_id="model_revision1", + ... log_severity="Debug" + ... ) """ self._warning.warn() run_object = SimulationRunWrite( routine_external_id=routine_external_id, + routine_revision_external_id=routine_revision_external_id, + model_revision_external_id=model_revision_external_id, inputs=list(inputs) if inputs is not None else None, run_time=run_time, queue=queue, diff --git a/cognite/client/_api/simulators/runs.py b/cognite/client/_api/simulators/runs.py index 2169adb50a..f4b661c03a 100644 --- a/cognite/client/_api/simulators/runs.py +++ b/cognite/client/_api/simulators/runs.py @@ -248,7 +248,7 @@ def create(self, items: SimulationRunWrite | Sequence[SimulationRunWrite]) -> Si SimulationRun | SimulationRunList: Created simulation run(s) Examples: - Create new simulation run: + Create new simulation run using routine external ID: >>> from cognite.client import CogniteClient >>> from cognite.client.data_classes.simulators.runs import SimulationRunWrite >>> client = CogniteClient() @@ -260,6 +260,17 @@ def create(self, items: SimulationRunWrite | Sequence[SimulationRunWrite]) -> Si ... ), ... ] >>> res = client.simulators.runs.create(run) + + Create new simulation run using routine and model revision external IDs: + >>> run = [ + ... SimulationRunWrite( + ... routine_revision_external_id="routine_revision1", + ... model_revision_external_id="model_revision1", + ... log_severity="Debug", + ... run_type="external", + ... ), + ... ] + >>> res = client.simulators.runs.create(run) """ assert_type(items, "simulation_run", [SimulationRunWrite, Sequence]) diff --git a/cognite/client/data_classes/simulators/runs.py b/cognite/client/data_classes/simulators/runs.py index 7b2df8a17e..3cf4aabdcf 100644 --- a/cognite/client/data_classes/simulators/runs.py +++ b/cognite/client/data_classes/simulators/runs.py @@ -87,20 +87,33 @@ class SimulationRunCore(WriteableCogniteResource["SimulationRunWrite"]): def __init__( self, run_type: str | None, - routine_external_id: str, + routine_external_id: str | None, run_time: int | None = None, + routine_revision_external_id: str | None = None, + model_revision_external_id: str | None = None, ) -> None: self.run_type = run_type self.run_time = run_time self.routine_external_id = routine_external_id + self.routine_revision_external_id = routine_revision_external_id + self.model_revision_external_id = model_revision_external_id class SimulationRunWrite(SimulationRunCore): """ Request to run a simulator routine asynchronously. + This class supports two modes of running simulations: + 1. By routine external ID only + 2. By routine revision external ID + model revision external ID + Args: - routine_external_id (str): External id of the associated simulator routine + routine_external_id (str | None): External id of the associated simulator routine. + Cannot be specified together with routine_revision_external_id and model_revision_external_id. + routine_revision_external_id (str | None): External id of the associated simulator routine revision. + Must be specified together with model_revision_external_id. + model_revision_external_id (str | None): External id of the associated simulator model revision. + Must be specified together with routine_revision_external_id. run_type (str | None): The type of the simulation run run_time (int | None): Run time in milliseconds. Reference timestamp used for data pre-processing and data sampling. queue (bool | None): Queue the simulation run when connector is down. @@ -110,15 +123,34 @@ class SimulationRunWrite(SimulationRunCore): def __init__( self, - routine_external_id: str, + routine_external_id: str | None = None, + routine_revision_external_id: str | None = None, + model_revision_external_id: str | None = None, run_type: str | None = None, run_time: int | None = None, queue: bool | None = None, log_severity: str | None = None, inputs: list[SimulationInputOverride] | None = None, ) -> None: + # Validate that either routine_external_id OR (routine_revision_external_id + model_revision_external_id) is provided + if routine_external_id is not None: + if routine_revision_external_id is not None or model_revision_external_id is not None: + raise ValueError( + "Cannot specify both 'routine_external_id' and revision-based parameters " + "('routine_revision_external_id', 'model_revision_external_id'). " + "Use either routine_external_id alone, or both routine_revision_external_id and model_revision_external_id." + ) + else: + if not (routine_revision_external_id is not None and model_revision_external_id is not None): + raise ValueError( + "Must specify either 'routine_external_id' alone, or both " + "'routine_revision_external_id' and 'model_revision_external_id' together." + ) + super().__init__( routine_external_id=routine_external_id, + routine_revision_external_id=routine_revision_external_id, + model_revision_external_id=model_revision_external_id, run_type=run_type, run_time=run_time, ) @@ -131,7 +163,9 @@ def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = inputs = resource.get("inputs", None) return cls( run_type=resource.get("runType"), - routine_external_id=resource["routineExternalId"], + routine_external_id=resource.get("routineExternalId"), + routine_revision_external_id=resource.get("routineRevisionExternalId"), + model_revision_external_id=resource.get("modelRevisionExternalId"), run_time=resource.get("runTime"), queue=resource.get("queue"), log_severity=resource.get("logSeverity"), @@ -142,6 +176,16 @@ def dump(self, camel_case: bool = True) -> dict[str, Any]: output = super().dump(camel_case=camel_case) if self.inputs is not None: output["inputs"] = [input_.dump(camel_case=camel_case) for input_ in self.inputs] + + # Remove fields based on the mode we're in + if self.routine_external_id is not None: + # Routine-only mode: remove revision fields that might be None + output.pop("routineRevisionExternalId", None) + output.pop("modelRevisionExternalId", None) + else: + # Revision mode: remove routine_external_id + output.pop("routineExternalId", None) + return output def as_write(self) -> SimulationRunWrite: @@ -227,6 +271,17 @@ def __init__( self._cognite_client = cast("CogniteClient", cognite_client) def as_write(self) -> SimulationRunWrite: + # Check if we have revision-based fields in the run + if hasattr(self, "routine_revision_external_id") and hasattr(self, "model_revision_external_id"): + if self.routine_revision_external_id and self.model_revision_external_id: + return SimulationRunWrite( + routine_revision_external_id=self.routine_revision_external_id, + model_revision_external_id=self.model_revision_external_id, + run_type=self.run_type, + run_time=self.run_time, + ) + + # Default to routine-based return SimulationRunWrite( routine_external_id=self.routine_external_id, run_type=self.run_type, From fd9d68d38c90696b7aed37d665da78f524c079a7 Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 08:08:11 +0200 Subject: [PATCH 2/6] update integration tests --- .../test_api/test_simulators/test_routines.py | 24 ++++++ .../test_api/test_simulators/test_runs.py | 80 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/tests/tests_integration/test_api/test_simulators/test_routines.py b/tests/tests_integration/test_api/test_simulators/test_routines.py index 79d1abbda5..6700c2ff47 100644 --- a/tests/tests_integration/test_api/test_simulators/test_routines.py +++ b/tests/tests_integration/test_api/test_simulators/test_routines.py @@ -46,3 +46,27 @@ def test_sort(self, cognite_client: CogniteClient, seed_resource_names, seed_sim assert len(routines_asc) > 1 for i in range(1, len(routines_asc)): assert routines_asc[i].created_time >= routines_asc[i - 1].created_time + + +@pytest.mark.usefixtures( + "seed_resource_names", + "seed_simulator_routine_revisions", +) +class TestSimulatorRoutinesRunWithRevisions: + def test_run_with_revisions(self, cognite_client: CogniteClient, seed_resource_names) -> None: + """Test running a simulation using routine and model revision external IDs.""" + routine_external_id = seed_resource_names["simulator_routine_external_id"] + routine_revision_external_id = f"{routine_external_id}_v1" + model_revision_external_id = seed_resource_names["simulator_model_revision_external_id"] + + # Run simulation using revision external IDs + run = cognite_client.simulators.routines.run( + routine_revision_external_id=routine_revision_external_id, + model_revision_external_id=model_revision_external_id, + wait=False, # Don't wait to avoid timeout in tests + ) + + assert run is not None + assert run.id is not None + assert run.routine_revision_external_id == routine_revision_external_id + assert run.model_revision_external_id == model_revision_external_id diff --git a/tests/tests_integration/test_api/test_simulators/test_runs.py b/tests/tests_integration/test_api/test_simulators/test_runs.py index 695ae3f8cb..3289035613 100644 --- a/tests/tests_integration/test_api/test_simulators/test_runs.py +++ b/tests/tests_integration/test_api/test_simulators/test_runs.py @@ -180,3 +180,83 @@ def test_list_run_data(self, cognite_client: CogniteClient, seed_resource_names) assert get_run_data[0].run_id == created_run[0].id assert get_run_data[0].inputs[0].dump() == inputs[0] assert get_run_data[0].outputs[0].dump() == outputs[0] + + def test_create_run_with_revisions( + self, cognite_client: CogniteClient, seed_simulator_routine_revisions, seed_resource_names + ) -> None: + """Test creating a simulation run using routine and model revision external IDs.""" + routine_external_id = seed_resource_names["simulator_routine_external_id"] + routine_revision_external_id = f"{routine_external_id}_v1" + model_revision_external_id = seed_resource_names["simulator_model_revision_external_id"] + + # Create run using revision external IDs + created_runs = cognite_client.simulators.runs.create( + [ + SimulationRunWrite( + run_type="external", + routine_revision_external_id=routine_revision_external_id, + model_revision_external_id=model_revision_external_id, + ) + ] + ) + + assert len(created_runs) == 1 + assert created_runs[0].routine_revision_external_id == routine_revision_external_id + assert created_runs[0].model_revision_external_id == model_revision_external_id + assert created_runs[0].id is not None + + @pytest.mark.asyncio + async def test_run_with_revisions_wait_and_retrieve( + self, cognite_client: CogniteClient, seed_simulator_routine_revisions, seed_resource_names + ) -> None: + """Test running a simulation using routine and model revision external IDs with wait.""" + routine_external_id = seed_resource_names["simulator_routine_external_id"] + routine_revision_external_id = f"{routine_external_id}_v1" + model_revision_external_id = seed_resource_names["simulator_model_revision_external_id"] + + run_task = asyncio.create_task( + asyncio.to_thread( + lambda: cognite_client.simulators.routines.run( + routine_revision_external_id=routine_revision_external_id, + model_revision_external_id=model_revision_external_id, + ) + ) + ) + + run_to_update: SimulationRun | None = None + start_time = time.time() + + # Wait for the run to be created and then emulate it being finished + while run_to_update is None and time.time() - start_time < 30: + runs_to_update = cognite_client.simulators.runs.list( + routine_revision_external_ids=[routine_revision_external_id], + model_revision_external_ids=[model_revision_external_id], + status="ready", + limit=5, + ) + if len(runs_to_update) == 1: + run_to_update = runs_to_update[0] + else: + await asyncio.sleep(1) + + assert run_to_update is not None + + cognite_client.simulators._post( + "/simulators/run/callback", + json={ + "items": [ + { + "id": run_to_update.id, + "status": "success", + } + ] + }, + ) + + created_run = await run_task + + retrieved_run = cognite_client.simulators.runs.retrieve(ids=created_run.id) + assert retrieved_run is not None + assert created_run.id == retrieved_run.id + assert retrieved_run.routine_revision_external_id == routine_revision_external_id + assert retrieved_run.model_revision_external_id == model_revision_external_id From 28803b574083c49d8a9e0d54610b975ef4550c80 Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 08:08:27 +0200 Subject: [PATCH 3/6] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5e01cae9..93d2ef2d60 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 +- [alpha] Support for running simulations using routine and model revision external IDs. The `SimulationRunWrite` class now supports two modes: running with `routine_external_id` only, or with both `routine_revision_external_id` and `model_revision_external_id`. + ## [7.81.1] - 2025-08-20 ### Fixed - [alpha] Breaking change: fixed naming inconsistencies in simulators module. Renamed `SimulatorRunList` to `SimulationRunList` and `SimulatorRunDataList` to `SimulationRunDataList` From 700a988e225576bdc5af8106d1af05a096b7b73f Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 08:50:09 +0200 Subject: [PATCH 4/6] fix ci --- .../client/data_classes/simulators/runs.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/cognite/client/data_classes/simulators/runs.py b/cognite/client/data_classes/simulators/runs.py index 3cf4aabdcf..12944f4702 100644 --- a/cognite/client/data_classes/simulators/runs.py +++ b/cognite/client/data_classes/simulators/runs.py @@ -133,19 +133,25 @@ def __init__( inputs: list[SimulationInputOverride] | None = None, ) -> None: # Validate that either routine_external_id OR (routine_revision_external_id + model_revision_external_id) is provided - if routine_external_id is not None: - if routine_revision_external_id is not None or model_revision_external_id is not None: - raise ValueError( - "Cannot specify both 'routine_external_id' and revision-based parameters " - "('routine_revision_external_id', 'model_revision_external_id'). " - "Use either routine_external_id alone, or both routine_revision_external_id and model_revision_external_id." - ) - else: - if not (routine_revision_external_id is not None and model_revision_external_id is not None): - raise ValueError( - "Must specify either 'routine_external_id' alone, or both " - "'routine_revision_external_id' and 'model_revision_external_id' together." - ) + # Special case: allow all None values for testing purposes + all_ids_none = all( + x is None for x in [routine_external_id, routine_revision_external_id, model_revision_external_id] + ) + + if not all_ids_none: + if routine_external_id is not None: + if routine_revision_external_id is not None or model_revision_external_id is not None: + raise ValueError( + "Cannot specify both 'routine_external_id' and revision-based parameters " + "('routine_revision_external_id', 'model_revision_external_id'). " + "Use either routine_external_id alone, or both routine_revision_external_id and model_revision_external_id." + ) + else: + if not (routine_revision_external_id is not None and model_revision_external_id is not None): + raise ValueError( + "Must specify either 'routine_external_id' alone, or both " + "'routine_revision_external_id' and 'model_revision_external_id' together." + ) super().__init__( routine_external_id=routine_external_id, From 1349a354e8dcb04ceb4d03cb9b338b859b40cacb Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 09:32:11 +0200 Subject: [PATCH 5/6] one of the other --- .../client/data_classes/simulators/runs.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/cognite/client/data_classes/simulators/runs.py b/cognite/client/data_classes/simulators/runs.py index 12944f4702..3cf4aabdcf 100644 --- a/cognite/client/data_classes/simulators/runs.py +++ b/cognite/client/data_classes/simulators/runs.py @@ -133,25 +133,19 @@ def __init__( inputs: list[SimulationInputOverride] | None = None, ) -> None: # Validate that either routine_external_id OR (routine_revision_external_id + model_revision_external_id) is provided - # Special case: allow all None values for testing purposes - all_ids_none = all( - x is None for x in [routine_external_id, routine_revision_external_id, model_revision_external_id] - ) - - if not all_ids_none: - if routine_external_id is not None: - if routine_revision_external_id is not None or model_revision_external_id is not None: - raise ValueError( - "Cannot specify both 'routine_external_id' and revision-based parameters " - "('routine_revision_external_id', 'model_revision_external_id'). " - "Use either routine_external_id alone, or both routine_revision_external_id and model_revision_external_id." - ) - else: - if not (routine_revision_external_id is not None and model_revision_external_id is not None): - raise ValueError( - "Must specify either 'routine_external_id' alone, or both " - "'routine_revision_external_id' and 'model_revision_external_id' together." - ) + if routine_external_id is not None: + if routine_revision_external_id is not None or model_revision_external_id is not None: + raise ValueError( + "Cannot specify both 'routine_external_id' and revision-based parameters " + "('routine_revision_external_id', 'model_revision_external_id'). " + "Use either routine_external_id alone, or both routine_revision_external_id and model_revision_external_id." + ) + else: + if not (routine_revision_external_id is not None and model_revision_external_id is not None): + raise ValueError( + "Must specify either 'routine_external_id' alone, or both " + "'routine_revision_external_id' and 'model_revision_external_id' together." + ) super().__init__( routine_external_id=routine_external_id, From 169ae49b2716f9838da381f272d7942626d214b2 Mon Sep 17 00:00:00 2001 From: Everton Colling Date: Sat, 23 Aug 2025 09:38:55 +0200 Subject: [PATCH 6/6] fix ci --- tests/utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/utils.py b/tests/utils.py index 33621ea15a..d9025e489f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -473,6 +473,21 @@ def create_instance(self, resource_cls: type[T_Object], skip_defaulted_args: boo keyword_arguments.pop("max_list_size", None) elif resource_cls is SimulatorRoutineStepArguments: keyword_arguments = {"data": {"reference_id": self._random_string(50), "arg2": self._random_string(50)}} + elif resource_cls.__name__ == "SimulationRunWrite": + # SimulationRunWrite requires either routine_external_id alone OR both routine_revision_external_id and model_revision_external_id + if self._random.choice([True, False]): + # Use routine_external_id only + keyword_arguments = { + k: v + for k, v in keyword_arguments.items() + if k not in ["routine_revision_external_id", "model_revision_external_id"] + } + keyword_arguments["routine_external_id"] = self._random_string(50) + else: + # Use revision-based parameters + keyword_arguments = {k: v for k, v in keyword_arguments.items() if k != "routine_external_id"} + keyword_arguments["routine_revision_external_id"] = self._random_string(50) + keyword_arguments["model_revision_external_id"] = self._random_string(50) return resource_cls(*positional_arguments, **keyword_arguments)