-
Notifications
You must be signed in to change notification settings - Fork 178
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): allow setting runtime parameter values and CSV files in cli analysis #16387
Changes from 4 commits
c0cc3a2
db8b95d
b34517b
480cded
46ab472
701dcd8
a25b754
da3f944
a29d318
bfba6d0
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 |
---|---|---|
|
@@ -23,8 +23,14 @@ | |
) | ||
import logging | ||
import sys | ||
import json | ||
|
||
from opentrons.protocol_engine.types import RunTimeParameter, EngineStatus | ||
from opentrons.protocol_engine.types import ( | ||
RunTimeParameter, | ||
CSVRuntimeParamPaths, | ||
PrimitiveRunTimeParamValuesType, | ||
EngineStatus, | ||
) | ||
from opentrons.protocols.api_support.types import APIVersion | ||
from opentrons.protocol_reader import ( | ||
ProtocolReader, | ||
|
@@ -104,8 +110,22 @@ class _Output: | |
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), | ||
default="WARNING", | ||
) | ||
@click.option( | ||
"--rtp-values", | ||
help="Serialized JSON of runtime parameter variable names to values.", | ||
default="{}", | ||
type=str, | ||
) | ||
@click.option( | ||
"--rtp-files", | ||
help="Serialized JSON of runtime parameter variable names to file paths.", | ||
default="{}", | ||
type=str, | ||
) | ||
def analyze( | ||
files: Sequence[Path], | ||
rtp_values: str, | ||
rtp_files: str, | ||
json_output: Optional[IO[bytes]], | ||
human_json_output: Optional[IO[bytes]], | ||
log_output: str, | ||
|
@@ -125,7 +145,7 @@ def analyze( | |
|
||
try: | ||
with _capture_logs(log_output, log_level): | ||
sys.exit(run(_analyze, files, outputs, check)) | ||
sys.exit(run(_analyze, files, rtp_values, rtp_files, outputs, check)) | ||
except click.ClickException: | ||
raise | ||
except Exception as e: | ||
|
@@ -194,6 +214,31 @@ def _get_input_files(files_and_dirs: Sequence[Path]) -> List[Path]: | |
return results | ||
|
||
|
||
def _get_runtime_parameter_values( | ||
serialized_rtp_values: str, | ||
) -> PrimitiveRunTimeParamValuesType: | ||
rtp_values = {} | ||
try: | ||
for variable_name, value in json.loads(serialized_rtp_values).items(): | ||
assert isinstance( | ||
value, (bool, int, float, str) | ||
), f"Value '{value}' is not of allowed type boolean, integer, float or string" | ||
rtp_values[variable_name] = value | ||
except Exception as error: | ||
raise click.ClickException(f"Could not parse runtime parameter values: {error}") | ||
return rtp_values | ||
|
||
|
||
def _get_runtime_parameter_paths(serialized_rtp_files: str) -> CSVRuntimeParamPaths: | ||
try: | ||
return { | ||
variable_name: Path(path_string) | ||
for variable_name, path_string in json.loads(serialized_rtp_files).items() | ||
} | ||
except Exception as error: | ||
raise click.ClickException(f"Could not parse runtime parameter files: {error}") | ||
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. Ditto. |
||
|
||
|
||
R = TypeVar("R") | ||
|
||
|
||
|
@@ -238,7 +283,11 @@ def _convert_exc() -> Iterator[EnumeratedError]: | |
) | ||
|
||
|
||
async def _do_analyze(protocol_source: ProtocolSource) -> RunResult: | ||
async def _do_analyze( | ||
protocol_source: ProtocolSource, | ||
rtp_values: PrimitiveRunTimeParamValuesType, | ||
rtp_paths: CSVRuntimeParamPaths, | ||
) -> RunResult: | ||
|
||
orchestrator = await create_simulating_orchestrator( | ||
robot_type=protocol_source.robot_type, protocol_config=protocol_source.config | ||
|
@@ -247,8 +296,8 @@ async def _do_analyze(protocol_source: ProtocolSource) -> RunResult: | |
await orchestrator.load( | ||
protocol_source=protocol_source, | ||
parse_mode=ParseMode.NORMAL, | ||
run_time_param_values=None, | ||
run_time_param_paths=None, | ||
run_time_param_values=rtp_values, | ||
run_time_param_paths=rtp_paths, | ||
) | ||
except Exception as error: | ||
err_id = "analysis-setup-error" | ||
|
@@ -285,9 +334,16 @@ async def _do_analyze(protocol_source: ProtocolSource) -> RunResult: | |
|
||
|
||
async def _analyze( | ||
files_and_dirs: Sequence[Path], outputs: Sequence[_Output], check: bool | ||
files_and_dirs: Sequence[Path], | ||
rtp_values: str, | ||
rtp_files: str, | ||
outputs: Sequence[_Output], | ||
check: bool, | ||
) -> int: | ||
input_files = _get_input_files(files_and_dirs) | ||
parsed_rtp_values = _get_runtime_parameter_values(rtp_values) | ||
rtp_paths = _get_runtime_parameter_paths(rtp_files) | ||
|
||
try: | ||
protocol_source = await ProtocolReader().read_saved( | ||
files=input_files, | ||
|
@@ -296,7 +352,7 @@ async def _analyze( | |
except ProtocolFilesInvalidError as error: | ||
raise click.ClickException(str(error)) | ||
|
||
analysis = await _do_analyze(protocol_source) | ||
analysis = await _do_analyze(protocol_source, parsed_rtp_values, rtp_paths) | ||
return_code = _get_return_code(analysis) | ||
|
||
if not outputs: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
"""Parameter context for python protocols.""" | ||
import uuid | ||
from typing import List, Optional, Union, Dict | ||
|
||
from opentrons.protocols.api_support.types import APIVersion | ||
|
@@ -251,8 +252,16 @@ def initialize_csv_files( | |
f" but '{variable_name}' is not a CSV parameter." | ||
) | ||
|
||
# The parent folder in the path will be the file ID, so we can use that to resolve that here | ||
# TODO(jbl 2024-09-30) Refactor this so file ID is passed as its own argument and not assumed from the path | ||
# If this is running on a robot, the parent folder in the path will be the file ID | ||
# If it is running locally, most likely the parent folder will not be a UUID, so instead we will change | ||
# this to be an empty string | ||
Comment on lines
+255
to
+258
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. It seems like we want to go the other way—remove any notion of file ID from this class—doesn't it? Isn't the file ID really a robot-server concept? Like, nothing in a Python protocol has any notion of a file UUID4. Protocol authors deal with parameter names and values. 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'd agree with that, but I think that the caller specifying the ID is a good enough way to separate the concerns |
||
file_id = file_path.parent.name | ||
try: | ||
uuid.UUID(file_id, version=4) | ||
except ValueError: | ||
file_id = "" | ||
|
||
file_name = file_path.name | ||
|
||
with file_path.open("rb") as fh: | ||
|
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.
As a general rule of thumb, let's try to avoid this pattern:
I think it's contributed a lot of the messiness in our error messages. One reason is that in the general case,
str(e)
can be empty, or, if it's not empty, it can be confusingly incomplete. Try it with aKeyError
from accessing a nonexistent dict key, for example.I see three solutions:
JSONDecodeError
. It has a number of specific guaranteed fields that you could build a message out of. Or you could dostr(json_decode_error)
—that's okay because we can test what it does and prove to ourselves that it will always be nice and readable and self-contained, unlike the generalException
case.Exception
, and let some higher-level code take care of doing (1). Click might have some nice way of handling this, I dunno.I would probably pursue (2) or (3) here.
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.
Also, separately: if you still end up raising a
ClickException
, you might want to raise its more specific subclass,BadParameter
.