From 976b4a33d5d34a484a3e7579901ad45b33195e9c Mon Sep 17 00:00:00 2001 From: abhilash-kumar-nair Date: Tue, 8 Sep 2026 12:01:31 +0530 Subject: [PATCH 1/2] feat: add update_experiment_definition_metadata to Model entity Binds PUT /api/projects/{id}/models/{path}/experiment-definitions/{id}/metadata, so a definition can be renamed or flagged as the model's default without fetching and re-sending the whole experiment. The server renamed the metadata request field 'isDefault' to 'setAsDefault' on the create and update endpoints, so send that instead. The old name is silently ignored rather than rejected, which made the client store every definition as non-default. This requires a server carrying that change. For the same reason 'is_default' on update_experiment_definition now defaults to None, meaning the stored flag is kept, instead of False, which reset it. Both endpoints replace the whole metadata, so whatever the caller leaves out is filled in from the stored definition. --- modelon/impact/client/entities/model.py | 119 +++++++++++++++++++----- modelon/impact/client/sal/project.py | 19 +++- 2 files changed, 115 insertions(+), 23 deletions(-) diff --git a/modelon/impact/client/entities/model.py b/modelon/impact/client/entities/model.py index f08e24d5..1e6a3bb2 100644 --- a/modelon/impact/client/entities/model.py +++ b/modelon/impact/client/entities/model.py @@ -3,7 +3,7 @@ import logging import os from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union from modelon.impact.client.configuration import Experimental from modelon.impact.client.entities.custom_function import ( @@ -376,12 +376,12 @@ def update_experiment_definition( experiment_definition_id: str, definition: SimpleModelicaExperimentDefinition, name: Optional[str] = None, - is_default: bool = False, + is_default: Optional[bool] = None, ) -> ExperimentDefinitionEntry: """Updates an existing experiment definition for this model. - The stored definition is replaced with the given one, including its - metadata (name and default flag). + The stored experiment is replaced with the given one. Metadata the + caller leaves out is kept as it is. Requires an active modeling session, since a definition inherited from a parent class via 'extends' can only be recognized as read-only by @@ -394,7 +394,8 @@ def update_experiment_definition( name: New name for the experiment definition. Default: None, which keeps the existing name unchanged. is_default: Whether to mark this experiment definition as the - default for the model. Default: False. + default for the model. Default: None, which keeps the existing + default flag unchanged. Example:: @@ -402,8 +403,60 @@ def update_experiment_definition( model = session.get_model("LibA.Model") entry = model.get_experiment_definitions()[0] definition = entry.definition.with_modifiers({'inertia1.J': 2}) - updated = model.update_experiment_definition( - entry.id, definition, is_default=entry.is_default + updated = model.update_experiment_definition(entry.id, definition) + + Raises: + ExperimentDefinitionReadOnlyError: If the experiment definition is + read-only, e.g. because it belongs to a read-only project or is + inherited from a parent class via 'extends'. + + """ + name, is_default = self._experiment_definition_metadata_to_keep( + experiment_definition_id, name, is_default + ) + resp = self._sal.project.experiment_definition_update( + self._project_id, + self._class_name, + experiment_definition_id, + name=name, + experiment=_to_editable_experiment_content(definition), + is_default=is_default, + ) + return self._experiment_definition_entry_from_item(resp["data"]) + + @Experimental + def update_experiment_definition_metadata( + self, + experiment_definition_id: str, + name: Optional[str] = None, + is_default: Optional[bool] = None, + ) -> ExperimentDefinitionEntry: + """Updates the metadata of an existing experiment definition, leaving the stored + experiment itself untouched. + + Use this to rename a definition, or to mark it as the model's default, + without having to fetch and re-send the whole experiment. + + Requires an active modeling session, since a definition inherited from + a parent class via 'extends' can only be recognized as read-only by + resolving the model's extends clauses. + + Args: + experiment_definition_id: The ID of the experiment definition to + update, e.g. from get_experiment_definitions(). + name: New name for the experiment definition. Default: None, which + keeps the existing name unchanged. + is_default: Whether to mark this experiment definition as the + default for the model. Default: None, which keeps the existing + default flag unchanged. + + Example:: + + with workspace.new_modeling_session() as session: + model = session.get_model("LibA.Model") + entry = model.get_experiment_definitions()[0] + renamed = model.update_experiment_definition_metadata( + entry.id, name="My experiment" ) Raises: @@ -411,6 +464,35 @@ def update_experiment_definition( read-only, e.g. because it belongs to a read-only project or is inherited from a parent class via 'extends'. + """ + name, is_default = self._experiment_definition_metadata_to_keep( + experiment_definition_id, name, is_default + ) + resp = self._sal.project.experiment_definition_metadata_update( + self._project_id, + self._class_name, + experiment_definition_id, + name=name, + is_default=is_default, + ) + return self._experiment_definition_entry_from_item(resp["data"]) + + def _experiment_definition_metadata_to_keep( + self, + experiment_definition_id: str, + name: Optional[str], + is_default: Optional[bool], + ) -> Tuple[str, bool]: + """Resolves the metadata to send for an update of an experiment definition of + this model. + + Both endpoints replace the whole metadata, so whatever the caller left + out is filled in from the stored definition to keep it as it is. + + Raises: + ExperimentDefinitionReadOnlyError: If the experiment definition is + read-only. + """ metadata = self._get_experiment_definition_metadata(experiment_definition_id) if metadata is not None and metadata["isReadOnly"]: @@ -419,23 +501,18 @@ def update_experiment_definition( "read-only and cannot be updated. It belongs to a read-only " "project or is inherited from a parent class via 'extends'." ) - if name is None: + if name is None or is_default is None: if metadata is None: raise ValueError( - "Could not resolve the current name of experiment " - f"definition '{experiment_definition_id}'; pass 'name' " - "explicitly." + "Could not resolve the current metadata of experiment " + f"definition '{experiment_definition_id}'; pass 'name' and " + "'is_default' explicitly." ) - name = metadata["name"] - resp = self._sal.project.experiment_definition_update( - self._project_id, - self._class_name, - experiment_definition_id, - name=name, - experiment=_to_editable_experiment_content(definition), - is_default=is_default, - ) - return self._experiment_definition_entry_from_item(resp["data"]) + if name is None: + name = metadata["name"] + if is_default is None: + is_default = metadata["isDefault"] + return name, is_default def _get_experiment_definition_metadata( self, experiment_definition_id: str diff --git a/modelon/impact/client/sal/project.py b/modelon/impact/client/sal/project.py index 297ee99a..8ef9def7 100644 --- a/modelon/impact/client/sal/project.py +++ b/modelon/impact/client/sal/project.py @@ -59,7 +59,7 @@ def experiment_definition_create( ).resolve() body = { "data": { - "metadata": {"name": name, "isDefault": is_default}, + "metadata": {"name": name, "setAsDefault": is_default}, "experiment": experiment, } } @@ -80,12 +80,27 @@ def experiment_definition_update( ).resolve() body = { "data": { - "metadata": {"name": name, "isDefault": is_default}, + "metadata": {"name": name, "setAsDefault": is_default}, "experiment": experiment, } } return self._http_client.put_json(url, body=body) + def experiment_definition_metadata_update( + self, + project_id: str, + model_path: str, + experiment_definition_id: str, + name: str, + is_default: bool = False, + ) -> Dict[str, Any]: + url = ( + self._base_uri / f"api/projects/{project_id}/models/{model_path}/" + f"experiment-definitions/{experiment_definition_id}/metadata" + ).resolve() + body = {"data": {"name": name, "setAsDefault": is_default}} + return self._http_client.put_json(url, body=body) + def project_options_get( self, project_id: str, workspace_id: str, custom_function: str ) -> Dict[str, Any]: From 1641c27ff114bce63c83fcd24900eaecf7736457 Mon Sep 17 00:00:00 2001 From: abhilash-kumar-nair Date: Tue, 8 Sep 2026 12:56:57 +0530 Subject: [PATCH 2/2] fix: move build image base to Debian bookworm Debian 11 bullseye stopped receiving security updates, so the Release file for bullseye-security is no longer regenerated and has now passed its Valid-Until: Suite: oldoldstable-security Date: Mon, 31 Aug 2026 21:13:04 UTC Valid-Until: Mon, 07 Sep 2026 21:13:04 UTC apt-get update fails on that expiry, which breaks the first apt layer of the build image and so every make target that goes through build-docker. This is permanent rather than a stale mirror, so it will not clear on a retry. python:3.9.6 was the last 3.9 image built on bullseye. Move to python:3.9.25-bookworm, keeping Python 3.9 as the version floor declared in pyproject.toml and pinning the exact patch as the previous base did. The newer libenchant in bookworm resolves en_US through aspell rather than hunspell, which is stricter, so 'pylint --enable spelling' started reporting 37 words it had previously let through. It turns out the check was near enough a no-op before: hunspell accepted anything, including the typo it now caught in Experiment.failed ("in experiment thar have failed"). Fix that typo and add the technical words it flags to the spelling wordlist. Verified in the rebuilt image: 'make lint' exits 0 and the full suite is green at 308 passed. --- Dockerfile | 2 +- docs/source/spelling_wordlist.txt | 10 +++++++++- modelon/impact/client/entities/experiment.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 13724131..d68634ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.6 +FROM python:3.9.25-bookworm # Do not run as root RUN adduser dev diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index 34292a3d..1475b51b 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -54,4 +54,12 @@ userspace sys netloc StartTime -StopTime \ No newline at end of file +StopTime +Orchestrator +orchestrator +attrib +dir +parametrization +parametrized +str +workspaces diff --git a/modelon/impact/client/entities/experiment.py b/modelon/impact/client/entities/experiment.py index fc7f710d..c7376ae7 100644 --- a/modelon/impact/client/entities/experiment.py +++ b/modelon/impact/client/entities/experiment.py @@ -163,7 +163,7 @@ def successful(self) -> int: @property def failed(self) -> int: - """Number of cases in experiment thar have failed.""" + """Number of cases in experiment that have failed.""" return self._failed @property