-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api, robot-server): use runtime parameter files set in protocols and runs to set in-protocol values #15855
Changes from 8 commits
953b657
8f97c4c
51a9037
89a69ad
6447fd9
c047fb2
81dc5ae
2588ce2
a2713c4
2b6da77
2bc20d1
6b3dbac
586c419
62f3e62
53a1988
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
"""Parameter context for python protocols.""" | ||
|
||
import tempfile | ||
from typing import List, Optional, Union, Dict | ||
|
||
from opentrons.protocols.api_support.types import APIVersion | ||
|
@@ -19,7 +19,7 @@ | |
from opentrons.protocol_engine.types import ( | ||
RunTimeParameter, | ||
PrimitiveRunTimeParamValuesType, | ||
CSVRunTimeParamFilesType, | ||
CSVRuntimeParamPaths, | ||
FileInfo, | ||
) | ||
|
||
|
@@ -218,15 +218,15 @@ def set_parameters( | |
parameter.value = validated_value | ||
|
||
def initialize_csv_files( | ||
self, run_time_param_file_overrides: CSVRunTimeParamFilesType | ||
self, run_time_param_file_overrides: CSVRuntimeParamPaths | ||
) -> None: | ||
"""Initializes the files for CSV parameters. | ||
|
||
:meta private: | ||
|
||
This is intended for Opentrons internal use only and is not a guaranteed API. | ||
""" | ||
for variable_name, file_id in run_time_param_file_overrides.items(): | ||
for variable_name, file_path in run_time_param_file_overrides.items(): | ||
try: | ||
parameter = self._parameters[variable_name] | ||
except KeyError: | ||
|
@@ -240,11 +240,34 @@ def initialize_csv_files( | |
f"File Id was provided for the parameter '{variable_name}'," | ||
f" but '{variable_name}' is not a CSV parameter." | ||
) | ||
file_id = file_path.parent.name | ||
file_name = file_path.name | ||
with file_path.open() as csv_file: | ||
contents = csv_file.read() | ||
|
||
temporary_file = tempfile.NamedTemporaryFile("r+") | ||
temporary_file.write(contents) | ||
temporary_file.flush() | ||
|
||
parameter_file = open(temporary_file.name, "r") | ||
temporary_file.close() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm reading this correctly:
Why are we doing step 3? Also, I know you're not supporting this now, but heads up for the future: Any kind of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Step 3 is to enforce read-only permissions on the file, since to write to the temporary file we need to make it read-write privileges. Since it's a temporary file there's no inherent danger in giving write privileges but it seemed the more "correct" thing to enforce. As for encoding you can actually change the encoding of an active file even after opening it via |
||
|
||
parameter.file_info = FileInfo(id=file_id, name="") | ||
# TODO (spp, 2024-07-16): set the file name and assign the file as parameter.value. | ||
# Most likely, we will be creating a temporary file copy of the original | ||
# to pass onto the protocol context | ||
parameter.file_info = FileInfo(id=file_id, name=file_name) | ||
parameter.value = parameter_file | ||
|
||
def close_csv_files(self) -> None: | ||
"""Close all file handlers for CSV parameters. | ||
|
||
:meta private: | ||
|
||
This is intended for Opentrons internal use only and is not a guaranteed API. | ||
""" | ||
for parameter in self._parameters.values(): | ||
if ( | ||
isinstance(parameter, csv_parameter_definition.CSVParameterDefinition) | ||
and parameter.value is not None | ||
): | ||
parameter.value.close() | ||
|
||
def export_parameters_for_analysis(self) -> List[RunTimeParameter]: | ||
"""Exports all parameters into a protocol engine models for reporting in analysis. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import logging | ||
from typing import Optional | ||
|
||
from opentrons.protocol_api import ProtocolContext | ||
from opentrons.protocol_api import ProtocolContext, ParameterContext | ||
from opentrons.protocol_api._parameters import Parameters | ||
from opentrons.protocols.execution.execute_python import exec_run | ||
from opentrons.protocols.execution.json_dispatchers import ( | ||
|
@@ -21,25 +21,35 @@ | |
def run_protocol( | ||
protocol: Protocol, | ||
context: ProtocolContext, | ||
parameter_context: Optional[ParameterContext] = None, | ||
run_time_parameters_with_overrides: Optional[Parameters] = None, | ||
) -> None: | ||
"""Run a protocol. | ||
|
||
:param protocol: The :py:class:`.protocols.types.Protocol` to execute | ||
:param context: The protocol context to use. | ||
:param parameter_context: The parameter context to use if running with runtime parameters. | ||
:param run_time_parameters_with_overrides: Run time parameters defined in the protocol, | ||
updated with the run's RTP override values. When we are running either simulate | ||
or execute, this will be None (until RTP is supported in cli commands) | ||
""" | ||
if isinstance(protocol, PythonProtocol): | ||
if protocol.api_level >= APIVersion(2, 0): | ||
exec_run( | ||
proto=protocol, | ||
context=context, | ||
run_time_parameters_with_overrides=run_time_parameters_with_overrides, | ||
) | ||
else: | ||
raise RuntimeError(f"Unsupported python API version: {protocol.api_level}") | ||
try: | ||
if protocol.api_level >= APIVersion(2, 0): | ||
exec_run( | ||
proto=protocol, | ||
context=context, | ||
run_time_parameters_with_overrides=run_time_parameters_with_overrides, | ||
) | ||
else: | ||
raise RuntimeError( | ||
f"Unsupported python API version: {protocol.api_level}" | ||
) | ||
except Exception: | ||
raise | ||
finally: | ||
if parameter_context is not None: | ||
parameter_context.close_csv_files() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm glad we're thinking about closing the files when we're done with them, but is this the correct place to do it? Where are the files opened? For example, it looks like one route for them to be opened is (The guideline that I'm aiming to follow is that every open has exactly one close, and the thing that does the open is the same as the thing that does the close.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this was probably my biggest ❓ with this PR. I didn't immediately see a good place to tightly couple the two as you suggest and so I put it in the place where the majority of protocols would enter in the first place, but I agree it's imperfect. I think this is probably a sign that these should be opened in a different location which would also tie into the next comment you have about file opening. Again, another TODO for next week so science can be unblocked from a functional version of this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. I would look into doing the |
||
else: | ||
if protocol.contents["schemaVersion"] == 3: | ||
ins = execute_json_v3.load_pipettes_from_json(context, protocol.contents) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this reexport for? Are we expecting users of the Python Protocol API to import this in their scripts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was tacked on but via a discussion with Ed we want this easily exposed since this interface is a first class object in our API now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see now, that makes sense. We should aim to define the class inside
opentrons.protocol_api
instead ofopentrons.protocols.parameters.types
, then. Until we do that, mind leaving a note on the class definition so it's obvious that it's user-facing?