From 6bf0579bfd65d1408a34d3441e9128a20307bb80 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Wed, 3 Apr 2024 15:18:54 -0400 Subject: [PATCH] feat(robot-server): update run creation endpoint to accept runtime parameter values (#14776) Adds an optional argument `runTimeParameterValues` to the request body of the POST /runs endpoint to start a run with new runtime parameter values. --- .../protocols/parameters/validation.py | 19 +++++++++++-------- .../protocols/parameters/test_validation.py | 5 ++++- .../robot_server/runs/engine_store.py | 12 +++++++++--- .../robot_server/runs/router/base_router.py | 4 ++++ .../robot_server/runs/run_data_manager.py | 6 ++++++ robot-server/robot_server/runs/run_models.py | 5 +++++ .../tests/runs/router/test_base_router.py | 9 ++++++++- .../tests/runs/test_run_data_manager.py | 10 +++++++++- 8 files changed, 56 insertions(+), 14 deletions(-) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index cbb2464ebd0..6e5c3b78a9f 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -61,14 +61,17 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers. If something is labelled as an int but is not actually an integer, that will be - caught when it is attempted to be set as the parameter value and will raise the appropriate error there. + as floating points to integers, and bools represented as 1.0/0.0 to True/False. + + If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be + set as the parameter value and will raise the appropriate error there. """ - validated_value: AllowedTypes - if isinstance(value, float) and parameter_type is int and value.is_integer(): - validated_value = int(value) - else: - validated_value = value + validated_value: AllowedTypes = value + if isinstance(value, float): + if parameter_type is bool and (value == 0 or value == 1): + validated_value = bool(value) + elif parameter_type is int and value.is_integer(): + validated_value = int(value) return validated_value @@ -163,7 +166,7 @@ def validate_type(value: ParamType, parameter_type: type) -> None: """Validate parameter value is the correct type.""" if not isinstance(value, parameter_type): raise ParameterValueError( - f"Parameter value has type {type(value)} must match type {parameter_type}." + f"Parameter value {value} has type {type(value)}, must match type {parameter_type}." ) diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 988e203a822..f515da885ed 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -137,13 +137,16 @@ def test_validate_options_raises_name_error() -> None: (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), + (0.0, bool, False), + (1, bool, True), + (3.0, bool, 3.0), (True, bool, True), ], ) def test_ensure_value_type( value: Union[float, bool, str], param_type: type, result: AllowedTypes ) -> None: - """It should ensure the correct type is there, converting floats to ints.""" + """It should ensure that if applicable, the value is coerced into the expected type""" assert result == subject.ensure_value_type(value, param_type) diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 673ff5549f3..8a35c20d92f 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -32,7 +32,10 @@ ) from robot_server.protocols.protocol_store import ProtocolResource -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import ( + DeckConfigurationType, + RunTimeParamValuesType, +) class EngineConflictError(RuntimeError): @@ -154,14 +157,17 @@ async def create( deck_configuration: DeckConfigurationType, notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], + run_time_param_values: Optional[RunTimeParamValuesType] = None, ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. Args: run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. - protocol: The protocol to load the runner with, if any. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + protocol: The protocol to load the runner with, if any. + run_time_param_values: Any runtime parameter values to set. Returns: The initial equipment and status summary of the engine. @@ -217,7 +223,7 @@ async def create( # was uploaded before we added stricter validation, and that # doesn't conform to the new rules. python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, - run_time_param_values=None, + run_time_param_values=run_time_param_values, ) elif isinstance(runner, JsonRunner): assert ( diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index e1e62fdf0d4..728966823fb 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -162,6 +162,9 @@ async def create_run( """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] + rtp_values = ( + request_body.data.runTimeParameterValues if request_body is not None else None + ) protocol_resource = None deck_configuration = await deck_configuration_store.get_deck_configuration() @@ -185,6 +188,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + run_time_param_values=rtp_values, protocol=protocol_resource, notify_publishers=notify_publishers, ) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index f0fc28dca37..5c57a14ecda 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -12,6 +12,7 @@ CurrentCommand, Command, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from robot_server.protocols.protocol_store import ProtocolResource from robot_server.service.task_runner import TaskRunner @@ -142,6 +143,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + run_time_param_values: Optional[RunTimeParamValuesType], notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: @@ -151,7 +153,10 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. notify_publishers: Utilized by the engine to notify publishers of state changes. + run_time_param_values: Any runtime parameter values to set. + protocol: The protocol to load the runner with, if any. Returns: The run resource. @@ -173,6 +178,7 @@ async def create( labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + run_time_param_values=run_time_param_values, notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index e05cd25330c..7da6e0b0a5d 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ Liquid, CommandNote, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel from robot_server.errors.error_responses import ErrorDetails @@ -212,6 +213,10 @@ class RunCreate(BaseModel): default_factory=list, description="Labware offsets to apply as labware are loaded.", ) + runTimeParameterValues: Optional[RunTimeParamValuesType] = Field( + None, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) class RunUpdate(BaseModel): diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 5c772e14be7..5763935cc39 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -92,6 +92,7 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -169,12 +170,17 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) result = await create_run( - request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), + request_body=RequestModel( + data=RunCreate( + protocolId="protocol-id", runTimeParameterValues={"foo": "bar"} + ) + ), protocol_store=mock_protocol_store, run_data_manager=mock_run_data_manager, run_id=run_id, @@ -232,6 +238,7 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index bac302e3065..ba4ceec8799 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -143,6 +143,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -160,6 +161,7 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -187,7 +189,7 @@ async def test_create_with_options( engine_state_summary: StateSummary, run_resource: RunResource, ) -> None: - """It should handle creation with a protocol and labware offsets.""" + """It should handle creation with a protocol, labware offsets and parameters.""" run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) @@ -210,6 +212,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -228,6 +231,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, notify_publishers=mock_notify_publishers, ) @@ -263,6 +267,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -274,6 +279,7 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) @@ -651,6 +657,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -669,6 +676,7 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, notify_publishers=mock_notify_publishers, )