diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index d5b126542d4..ad861051b6c 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -909,3 +909,5 @@ class EnumParameter(RTPBase): RunTimeParameter = Union[IntParameter, FloatParameter, EnumParameter] + +RunTimeParameterValues = Dict[str, Union[float, bool, str]] # update value types as more RTP types are added \ No newline at end of file diff --git a/api/src/opentrons/protocol_reader/protocol_reader.py b/api/src/opentrons/protocol_reader/protocol_reader.py index 309a25cd8b3..9d4a47f4a5c 100644 --- a/api/src/opentrons/protocol_reader/protocol_reader.py +++ b/api/src/opentrons/protocol_reader/protocol_reader.py @@ -1,6 +1,6 @@ """Read relevant protocol information from a set of files.""" from pathlib import Path -from typing import Optional, Sequence +from typing import Optional, Sequence, Dict, Union from opentrons.protocols.parse import PythonParseMode @@ -24,6 +24,7 @@ JsonProtocolConfig, PythonProtocolConfig, ) +from ..protocol_engine.types import RunTimeParameterValues class ProtocolReader: @@ -53,7 +54,10 @@ def __init__( self._file_hasher = file_hasher or FileHasher() async def save( - self, files: Sequence[BufferedFile], directory: Path, content_hash: str + self, + files: Sequence[BufferedFile], + directory: Path, content_hash: str, + run_time_param_values: RunTimeParameterValues, ) -> ProtocolSource: """Compute a `ProtocolSource` from buffered files and save them as files. @@ -65,6 +69,7 @@ async def save( files: List buffered files. Do not attempt to reuse any objects in this list once they've been passed to the ProtocolReader. directory: Name of the directory to create and place files in. + run_time_param_values: Client-supplied values for Run Time Parameters. Returns: A ProtocolSource describing the validated protocol. @@ -98,6 +103,7 @@ async def save( config=self._map_config(role_analysis), robot_type=role_analysis.main_file.robot_type, metadata=role_analysis.main_file.metadata, + run_time_param_values=run_time_param_values, ) async def read_saved( @@ -164,6 +170,9 @@ async def read_saved( config=self._map_config(role_analysis), robot_type=role_analysis.main_file.robot_type, metadata=role_analysis.main_file.metadata, + # We are not passing any RTP values, just reading the existing protocol. + # RTPs passed when the protocol was uploaded/ analyzed will be in the analysis. + run_time_param_values=None, ) @staticmethod diff --git a/api/src/opentrons/protocol_reader/protocol_source.py b/api/src/opentrons/protocol_reader/protocol_source.py index ab1aa21e375..7fbfc60a241 100644 --- a/api/src/opentrons/protocol_reader/protocol_source.py +++ b/api/src/opentrons/protocol_reader/protocol_source.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Union from typing_extensions import Literal +from opentrons.protocol_engine.types import RunTimeParameterValues from opentrons.protocols.api_support.types import APIVersion from opentrons_shared_data.robot.dev_types import RobotType @@ -122,3 +123,4 @@ class ProtocolSource: metadata: Metadata robot_type: RobotType config: ProtocolConfig + run_time_param_values: Optional[RunTimeParameterValues] diff --git a/api/src/opentrons/protocol_runner/legacy_wrappers.py b/api/src/opentrons/protocol_runner/legacy_wrappers.py index 6a816f5e9a1..a469809e411 100644 --- a/api/src/opentrons/protocol_runner/legacy_wrappers.py +++ b/api/src/opentrons/protocol_runner/legacy_wrappers.py @@ -94,6 +94,7 @@ def read( extra_data={ data_path.name: data_path.read_bytes() for data_path in data_file_paths }, + run_time_param_values=protocol_source.run_time_param_values, python_parse_mode=python_parse_mode, ) diff --git a/api/src/opentrons/protocols/parse.py b/api/src/opentrons/protocols/parse.py index 712b4fe4416..6d92d31032c 100644 --- a/api/src/opentrons/protocols/parse.py +++ b/api/src/opentrons/protocols/parse.py @@ -212,6 +212,7 @@ def _parse_json( def _parse_python( protocol_contents: Union[str, bytes], python_parse_mode: PythonParseMode, + run_time_param_values: Optional[Dict[str, Union[float, bool, str]]], filename: Optional[str] = None, bundled_labware: Optional[Dict[str, "LabwareDefinition"]] = None, bundled_data: Optional[Dict[str, bytes]] = None, @@ -273,6 +274,7 @@ def _parse_python( bundled_data=bundled_data, bundled_python=bundled_python, extra_labware=extra_labware, + run_time_param_values=run_time_param_values, ) return result @@ -303,6 +305,7 @@ def _parse_bundle( def parse( protocol_file: Union[str, bytes], + run_time_param_values: Optional[Dict[str, Union[float, bool, str]]], filename: Optional[str] = None, extra_labware: Optional[Dict[str, "LabwareDefinition"]] = None, extra_data: Optional[Dict[str, bytes]] = None, @@ -314,6 +317,8 @@ def parse( :param protocol_file: The protocol file, or for single-file protocols, a string of the protocol contents. + :param run_time_param_values: Values of run-time parameters sent by the client, + to be used during analysis/ run. :param filename: The name of the protocol. Optional, but helps with deducing the kind of protocol (e.g. if it ends with '.json' we can treat it like json) diff --git a/api/src/opentrons/protocols/types.py b/api/src/opentrons/protocols/types.py index 273a3e877d4..075e1e3ac49 100644 --- a/api/src/opentrons/protocols/types.py +++ b/api/src/opentrons/protocols/types.py @@ -74,6 +74,8 @@ class PythonProtocol(_ProtocolCommon): bundled_python: Optional[Dict[str, str]] # this should only be included when the protocol is not a zip extra_labware: Optional[Dict[str, "LabwareDefinition"]] + # TODO: Use a common type definition between engine's RTP type & this one + run_time_param_values: Optional[Dict[str, Union[float, bool, str]]] Protocol = Union[JsonProtocol, PythonProtocol] diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index e71be06864f..8c2bf36cd45 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -1,4 +1,5 @@ """Router for /protocols endpoints.""" +import json import logging from textwrap import dedent from datetime import datetime @@ -165,6 +166,10 @@ async def create_protocol( " protocol resources on the robot." ), ), + runTimeParameterValues: Optional[str] = Form( + default=None, + description="Key value pairs of run-time parameters defined in a protocol." + ), protocol_directory: Path = Depends(get_protocol_directory), protocol_store: ProtocolStore = Depends(get_protocol_store), analysis_store: AnalysisStore = Depends(get_analysis_store), @@ -184,6 +189,7 @@ async def create_protocol( Arguments: files: List of uploaded files, from form-data. key: Optional key for client-side tracking + runTimeParameterValues: Key value pairs of run-time parameters defined in a protocol. protocol_directory: Location to store uploaded files. protocol_store: In-memory database of protocol resources. analysis_store: In-memory database of protocol analyses. @@ -205,10 +211,12 @@ async def create_protocol( assert file.filename is not None buffered_files = await file_reader_writer.read(files=files) # type: ignore[arg-type] + parsed_rtp = json.loads(runTimeParameterValues) if runTimeParameterValues else None content_hash = await file_hasher.hash(buffered_files) cached_protocol_id = protocol_store.get_id_by_hash(content_hash) - if cached_protocol_id is not None: + if not parsed_rtp and cached_protocol_id is not None: + # Neither a new protocol nor any run-time parameters passed with an existing protocol. resource = protocol_store.get(protocol_id=cached_protocol_id) analyses = analysis_store.get_summaries_by_protocol( protocol_id=cached_protocol_id @@ -228,7 +236,8 @@ async def create_protocol( ) log.info( - f'Protocol with id "{cached_protocol_id}" with same contents already exists. returning existing protocol data in response payload' + f'Protocol with id "{cached_protocol_id}" with same contents already exists.' + f' Returning existing protocol data in response payload.' ) return await PydanticResponse.create( @@ -243,6 +252,7 @@ async def create_protocol( files=buffered_files, directory=protocol_directory / protocol_id, content_hash=content_hash, + run_time_param_values=parsed_rtp, ) except ProtocolFilesInvalidError as e: raise ProtocolFilesInvalid(detail=str(e)).as_error(