diff --git a/.eslintrc.js b/.eslintrc.js index 60859a5c409..1226f14f9d3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,13 +49,13 @@ module.exports = { importNames: [ 'useAllRunsQuery', 'useRunQuery', - 'useLastRunCommandKey', + 'useAllCommandsQuery', 'useCurrentMaintenanceRun', 'useDeckConfigurationQuery', 'useAllCommandsAsPreSerializedList', ], message: - 'The HTTP hook is deprecated. Utilize the equivalent notification wrapper (useNotifyX) instead.', + 'HTTP hook deprecated. Use the equivalent notification wrapper (useNotifyXYZ).', }, ], }, diff --git a/api/docs/v2/tutorial.rst b/api/docs/v2/tutorial.rst index 473ad6e40c0..246ef3d279b 100644 --- a/api/docs/v2/tutorial.rst +++ b/api/docs/v2/tutorial.rst @@ -262,7 +262,7 @@ In each row, you first need to add solution. This will be similar to what you di .. code-block:: python - left_pipette.transfer(100, reservoir["A2"], row[0], mix_after(3, 50)) + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) As before, the first argument specifies to transfer 100 µL. The second argument is the source, column 2 of the reservoir. The third argument is the destination, the element at index 0 of the current ``row``. Since Python lists are zero-indexed, but columns on labware start numbering at 1, this will be well A1 on the first time through the loop, B1 the second time, and so on. The fourth argument specifies to mix 3 times with 50 µL of fluid each time. @@ -275,7 +275,7 @@ Finally, it’s time to dilute the solution down the row. One approach would be .. code-block:: python - left_pipette.transfer(100, row[:11], row[1:], mix_after(3, 50)) + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) There’s some Python shorthand here, so let’s unpack it. You can get a range of indices from a list using the colon ``:`` operator, and omitting it at either end means “from the beginning” or “until the end” of the list. So the source is ``row[:11]``, from the beginning of the row until its 11th item. And the destination is ``row[1:]``, from index 1 (column 2!) until the end. Since both of these lists have 11 items, ``transfer()`` will *step through them in parallel*, and they’re constructed so when the source is 0, the destination is 1; when the source is 1, the destination is 2; and so on. This condenses all of the subsequent transfers down the row into a single line of code. diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 9a22a3e2e13..4dd1e814b8e 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -191,6 +191,7 @@ PipetteOverpressureError, FirmwareUpdateRequiredError, FailedGripperPickupError, + LiquidNotFoundError, ) from .subsystem_manager import SubsystemManager @@ -1399,6 +1400,18 @@ async def liquid_probe( for node, point in positions.items(): self._position.update({node: point.motor_position}) self._encoder_position.update({node: point.encoder_position}) + if ( + head_node not in positions + or positions[head_node].move_ack + == MoveCompleteAck.complete_without_condition + ): + raise LiquidNotFoundError( + "Liquid not found during probe.", + { + str(node_to_axis(node)): str(point.motor_position) + for node, point in positions.items() + }, + ) return self._position[axis_to_node(Axis.by_mount(mount))] async def capacitive_probe( diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 5fedd5050ce..c50dc08aebb 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2100,7 +2100,7 @@ async def verify_tip_presence( real_mount = OT3Mount.from_mount(mount) status = await self.get_tip_presence_status(real_mount, follow_singular_sensor) if status != expected: - raise FailedTipStateCheck(expected, status.value) + raise FailedTipStateCheck(expected, status) async def _force_pick_up_tip( self, mount: OT3Mount, pipette_spec: TipActionSpec @@ -2556,9 +2556,7 @@ async def liquid_probe( reading from the pressure sensor. If the move is completed without the specified threshold being triggered, a - LiquidNotFound error will be thrown. - If the threshold is triggered before the minimum z distance has been traveled, - a EarlyLiquidSenseTrigger error will be thrown. + LiquidNotFoundError error will be thrown. Otherwise, the function will stop moving once the threshold is triggered, and return the position of the diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 1ea79652f34..3c14cf9e361 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -694,25 +694,13 @@ def __init__( ) -class LiquidNotFound(RuntimeError): - """Error raised if liquid sensing move completes without detecting liquid.""" - - def __init__( - self, position: Dict[Axis, float], max_z_pos: Dict[Axis, float] - ) -> None: - """Initialize LiquidNotFound error.""" - super().__init__( - f"Liquid threshold not found, current_position = {position}" - f"position at max travel allowed = {max_z_pos}" - ) - - class FailedTipStateCheck(RuntimeError): """Error raised if the tip ejector state does not match the expected value.""" - def __init__(self, tip_state_type: TipStateType, actual_state: int) -> None: + def __init__( + self, expected_state: TipStateType, actual_state: TipStateType + ) -> None: """Initialize FailedTipStateCheck error.""" super().__init__( - f"Failed to correctly determine tip state for tip {str(tip_state_type)} " - f"received {bool(actual_state)} but expected {bool(tip_state_type.value)}" + f"Expected tip state {expected_state}, but received {actual_state}." ) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 4dcb81dcc33..6442ffd1f6d 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -11,7 +11,8 @@ BaseLiquidHandlingResult, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from opentrons.hardware_control import HardwareControlAPI @@ -40,7 +41,9 @@ class AspirateResult(BaseLiquidHandlingResult, DestinationPositionResult): pass -class AspirateImplementation(AbstractCommandImpl[AspirateParams, AspirateResult]): +class AspirateImplementation( + AbstractCommandImpl[AspirateParams, SuccessData[AspirateResult, None]] +): """Aspirate command implementation.""" def __init__( @@ -58,7 +61,9 @@ def __init__( self._movement = movement self._command_note_adder = command_note_adder - async def execute(self, params: AspirateParams) -> AspirateResult: + async def execute( + self, params: AspirateParams + ) -> SuccessData[AspirateResult, None]: """Move to and aspirate from the requested well. Raises: @@ -107,12 +112,16 @@ async def execute(self, params: AspirateParams) -> AspirateResult: command_note_adder=self._command_note_adder, ) - return AspirateResult( - volume=volume, position=DeckPoint(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=AspirateResult( + volume=volume, + position=DeckPoint(x=position.x, y=position.y, z=position.z), + ), + private=None, ) -class Aspirate(BaseCommand[AspirateParams, AspirateResult]): +class Aspirate(BaseCommand[AspirateParams, AspirateResult, ErrorOccurrence]): """Aspirate command model.""" commandType: AspirateCommandType = "aspirate" diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index f59bccdd9f7..a70d0cf7f39 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -12,7 +12,8 @@ FlowRateMixin, BaseLiquidHandlingResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from ..errors.exceptions import PipetteNotReadyToAspirateError if TYPE_CHECKING: @@ -36,7 +37,7 @@ class AspirateInPlaceResult(BaseLiquidHandlingResult): class AspirateInPlaceImplementation( - AbstractCommandImpl[AspirateInPlaceParams, AspirateInPlaceResult] + AbstractCommandImpl[AspirateInPlaceParams, SuccessData[AspirateInPlaceResult, None]] ): """AspirateInPlace command implementation.""" @@ -53,7 +54,9 @@ def __init__( self._hardware_api = hardware_api self._command_note_adder = command_note_adder - async def execute(self, params: AspirateInPlaceParams) -> AspirateInPlaceResult: + async def execute( + self, params: AspirateInPlaceParams + ) -> SuccessData[AspirateInPlaceResult, None]: """Aspirate without moving the pipette. Raises: @@ -77,10 +80,12 @@ async def execute(self, params: AspirateInPlaceParams) -> AspirateInPlaceResult: command_note_adder=self._command_note_adder, ) - return AspirateInPlaceResult(volume=volume) + return SuccessData(public=AspirateInPlaceResult(volume=volume), private=None) -class AspirateInPlace(BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult]): +class AspirateInPlace( + BaseCommand[AspirateInPlaceParams, AspirateInPlaceResult, ErrorOccurrence] +): """AspirateInPlace command model.""" commandType: AspirateInPlaceCommandType = "aspirateInPlace" diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index 47338ebc83f..f17b4b44ebc 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -10,7 +10,8 @@ WellLocationMixin, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from opentrons.hardware_control import HardwareControlAPI @@ -34,7 +35,9 @@ class BlowOutResult(DestinationPositionResult): pass -class BlowOutImplementation(AbstractCommandImpl[BlowOutParams, BlowOutResult]): +class BlowOutImplementation( + AbstractCommandImpl[BlowOutParams, SuccessData[BlowOutResult, None]] +): """BlowOut command implementation.""" def __init__( @@ -50,7 +53,7 @@ def __init__( self._state_view = state_view self._hardware_api = hardware_api - async def execute(self, params: BlowOutParams) -> BlowOutResult: + async def execute(self, params: BlowOutParams) -> SuccessData[BlowOutResult, None]: """Move to and blow-out the requested well.""" x, y, z = await self._movement.move_to_well( pipette_id=params.pipetteId, @@ -63,10 +66,12 @@ async def execute(self, params: BlowOutParams) -> BlowOutResult: pipette_id=params.pipetteId, flow_rate=params.flowRate ) - return BlowOutResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=BlowOutResult(position=DeckPoint(x=x, y=y, z=z)), private=None + ) -class BlowOut(BaseCommand[BlowOutParams, BlowOutResult]): +class BlowOut(BaseCommand[BlowOutParams, BlowOutResult, ErrorOccurrence]): """Blow-out command model.""" commandType: BlowOutCommandType = "blowout" diff --git a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py index a46aa89110e..d1527457c9c 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py @@ -9,7 +9,8 @@ PipetteIdMixin, FlowRateMixin, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from opentrons.hardware_control import HardwareControlAPI @@ -35,7 +36,7 @@ class BlowOutInPlaceResult(BaseModel): class BlowOutInPlaceImplementation( - AbstractCommandImpl[BlowOutInPlaceParams, BlowOutInPlaceResult] + AbstractCommandImpl[BlowOutInPlaceParams, SuccessData[BlowOutInPlaceResult, None]] ): """BlowOutInPlace command implementation.""" @@ -50,16 +51,20 @@ def __init__( self._state_view = state_view self._hardware_api = hardware_api - async def execute(self, params: BlowOutInPlaceParams) -> BlowOutInPlaceResult: + async def execute( + self, params: BlowOutInPlaceParams + ) -> SuccessData[BlowOutInPlaceResult, None]: """Blow-out without moving the pipette.""" await self._pipetting.blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate ) - return BlowOutInPlaceResult() + return SuccessData(public=BlowOutInPlaceResult(), private=None) -class BlowOutInPlace(BaseCommand[BlowOutInPlaceParams, BlowOutInPlaceResult]): +class BlowOutInPlace( + BaseCommand[BlowOutInPlaceParams, BlowOutInPlaceResult, ErrorOccurrence] +): """BlowOutInPlace command model.""" commandType: BlowOutInPlaceCommandType = "blowOutInPlace" diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py index c57dac9eb42..b400e2dd33a 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_gripper.py @@ -13,11 +13,8 @@ from opentrons.hardware_control.instruments.ot3.instrument_calibration import ( GripperCalibrationOffset, ) -from opentrons.protocol_engine.commands.command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, -) +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.types import Vec3f from opentrons.protocol_engine.resources import ensure_ot3_hardware @@ -74,7 +71,9 @@ class CalibrateGripperResult(BaseModel): class CalibrateGripperImplementation( - AbstractCommandImpl[CalibrateGripperParams, CalibrateGripperResult] + AbstractCommandImpl[ + CalibrateGripperParams, SuccessData[CalibrateGripperResult, None] + ] ): """The implementation of a `calibrateGripper` command.""" @@ -86,7 +85,9 @@ def __init__( ) -> None: self._hardware_api = hardware_api - async def execute(self, params: CalibrateGripperParams) -> CalibrateGripperResult: + async def execute( + self, params: CalibrateGripperParams + ) -> SuccessData[CalibrateGripperResult, None]: """Execute a `calibrateGripper` command. 1. Move from the current location to the calibration area on the deck. @@ -118,11 +119,14 @@ async def execute(self, params: CalibrateGripperParams) -> CalibrateGripperResul ) calibration_data = result - return CalibrateGripperResult.construct( - jawOffset=Vec3f.construct( - x=probe_offset.x, y=probe_offset.y, z=probe_offset.z + return SuccessData( + public=CalibrateGripperResult.construct( + jawOffset=Vec3f.construct( + x=probe_offset.x, y=probe_offset.y, z=probe_offset.z + ), + savedCalibration=calibration_data, ), - savedCalibration=calibration_data, + private=None, ) @staticmethod @@ -135,7 +139,9 @@ def _convert_to_hw_api_probe( return HWAPIGripperProbe.REAR -class CalibrateGripper(BaseCommand[CalibrateGripperParams, CalibrateGripperResult]): +class CalibrateGripper( + BaseCommand[CalibrateGripperParams, CalibrateGripperResult, ErrorOccurrence] +): """A `calibrateGripper` command.""" commandType: CalibrateGripperCommandType = "calibration/calibrateGripper" diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py index a3e8da549a7..08f5f45330f 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_module.py @@ -7,11 +7,8 @@ from opentrons.types import MountType from opentrons.protocol_engine.resources.ot3_validation import ensure_ot3_hardware -from opentrons.protocol_engine.commands.command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, -) +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence # Work around type-only circular dependencies. if TYPE_CHECKING: @@ -52,7 +49,7 @@ class CalibrateModuleResult(BaseModel): class CalibrateModuleImplementation( - AbstractCommandImpl[CalibrateModuleParams, CalibrateModuleResult] + AbstractCommandImpl[CalibrateModuleParams, SuccessData[CalibrateModuleResult, None]] ): """CalibrateModule command implementation.""" @@ -65,7 +62,9 @@ def __init__( self._state_view = state_view self._hardware_api = hardware_api - async def execute(self, params: CalibrateModuleParams) -> CalibrateModuleResult: + async def execute( + self, params: CalibrateModuleParams + ) -> SuccessData[CalibrateModuleResult, None]: """Execute calibrate-module command.""" ot3_api = ensure_ot3_hardware( self._hardware_api, @@ -85,15 +84,20 @@ async def execute(self, params: CalibrateModuleParams) -> CalibrateModuleResult: ot3_api, ot3_mount, slot.slotName.id, module_serial, nominal_position ) - return CalibrateModuleResult( - moduleOffset=ModuleOffsetVector( - x=module_offset.x, y=module_offset.y, z=module_offset.z + return SuccessData( + public=CalibrateModuleResult( + moduleOffset=ModuleOffsetVector( + x=module_offset.x, y=module_offset.y, z=module_offset.z + ), + location=slot, ), - location=slot, + private=None, ) -class CalibrateModule(BaseCommand[CalibrateModuleParams, CalibrateModuleResult]): +class CalibrateModule( + BaseCommand[CalibrateModuleParams, CalibrateModuleResult, ErrorOccurrence] +): """Calibrate-module command model.""" commandType: CalibrateModuleCommandType = "calibration/calibrateModule" diff --git a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py index e77f2be790d..4369f88a9c5 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/calibrate_pipette.py @@ -3,11 +3,8 @@ from typing_extensions import Literal from pydantic import BaseModel, Field -from ..command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, -) +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence from ...types import InstrumentOffsetVector from opentrons.protocol_engine.resources.ot3_validation import ensure_ot3_hardware @@ -37,7 +34,9 @@ class CalibratePipetteResult(BaseModel): class CalibratePipetteImplementation( - AbstractCommandImpl[CalibratePipetteParams, CalibratePipetteResult] + AbstractCommandImpl[ + CalibratePipetteParams, SuccessData[CalibratePipetteResult, None] + ] ): """CalibratePipette command implementation.""" @@ -48,7 +47,9 @@ def __init__( ) -> None: self._hardware_api = hardware_api - async def execute(self, params: CalibratePipetteParams) -> CalibratePipetteResult: + async def execute( + self, params: CalibratePipetteParams + ) -> SuccessData[CalibratePipetteResult, None]: """Execute calibrate-pipette command.""" # TODO (tz, 20-9-22): Add a better solution to determine if a command can be executed on an OT-3/OT-2 ot3_api = ensure_ot3_hardware( @@ -65,14 +66,19 @@ async def execute(self, params: CalibratePipetteParams) -> CalibratePipetteResul await ot3_api.save_instrument_offset(mount=ot3_mount, delta=pipette_offset) - return CalibratePipetteResult.construct( - pipetteOffset=InstrumentOffsetVector.construct( - x=pipette_offset.x, y=pipette_offset.y, z=pipette_offset.z - ) + return SuccessData( + public=CalibratePipetteResult.construct( + pipetteOffset=InstrumentOffsetVector.construct( + x=pipette_offset.x, y=pipette_offset.y, z=pipette_offset.z + ) + ), + private=None, ) -class CalibratePipette(BaseCommand[CalibratePipetteParams, CalibratePipetteResult]): +class CalibratePipette( + BaseCommand[CalibratePipetteParams, CalibratePipetteResult, ErrorOccurrence] +): """Calibrate-pipette command model.""" commandType: CalibratePipetteCommandType = "calibration/calibratePipette" diff --git a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py index 8ce067963ab..81d9e30d1cc 100644 --- a/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +++ b/api/src/opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py @@ -9,11 +9,8 @@ from opentrons.types import MountType, Point, Mount from opentrons.hardware_control.types import Axis, CriticalPoint -from opentrons.protocol_engine.commands.command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, -) +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.resources.ot3_validation import ensure_ot3_hardware if TYPE_CHECKING: @@ -59,7 +56,8 @@ class MoveToMaintenancePositionResult(BaseModel): class MoveToMaintenancePositionImplementation( AbstractCommandImpl[ - MoveToMaintenancePositionParams, MoveToMaintenancePositionResult + MoveToMaintenancePositionParams, + SuccessData[MoveToMaintenancePositionResult, None], ] ): """Calibration set up position command implementation.""" @@ -75,7 +73,7 @@ def __init__( async def execute( self, params: MoveToMaintenancePositionParams - ) -> MoveToMaintenancePositionResult: + ) -> SuccessData[MoveToMaintenancePositionResult, None]: """Move the requested mount to a maintenance deck slot.""" ot3_api = ensure_ot3_hardware( self._hardware_api, @@ -115,11 +113,15 @@ async def execute( ) await ot3_api.disengage_axes([Axis.Z_L, Axis.Z_R]) - return MoveToMaintenancePositionResult() + return SuccessData(public=MoveToMaintenancePositionResult(), private=None) class MoveToMaintenancePosition( - BaseCommand[MoveToMaintenancePositionParams, MoveToMaintenancePositionResult] + BaseCommand[ + MoveToMaintenancePositionParams, + MoveToMaintenancePositionResult, + ErrorOccurrence, + ] ): """Calibration set up position command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index fcdd7387355..2ece79c0213 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import ( @@ -11,7 +12,6 @@ Generic, Optional, TypeVar, - Tuple, List, Type, Union, @@ -35,6 +35,8 @@ _ParamsT_contra = TypeVar("_ParamsT_contra", bound=BaseModel, contravariant=True) _ResultT = TypeVar("_ResultT", bound=BaseModel) _ResultT_co = TypeVar("_ResultT_co", bound=BaseModel, covariant=True) +_ErrorT = TypeVar("_ErrorT", bound=ErrorOccurrence) +_ErrorT_co = TypeVar("_ErrorT_co", bound=ErrorOccurrence, covariant=True) _PrivateResultT_co = TypeVar("_PrivateResultT_co", covariant=True) @@ -60,7 +62,11 @@ class CommandIntent(str, Enum): FIXIT = "fixit" -class BaseCommandCreate(GenericModel, Generic[_ParamsT]): +class BaseCommandCreate( + GenericModel, + # These type parameters need to be invariant because our fields are mutable. + Generic[_ParamsT], +): """Base class for command creation requests. You shouldn't use this class directly; instead, use or define @@ -99,7 +105,37 @@ class BaseCommandCreate(GenericModel, Generic[_ParamsT]): ) -class BaseCommand(GenericModel, Generic[_ParamsT, _ResultT]): +@dataclass(frozen=True) +class SuccessData(Generic[_ResultT_co, _PrivateResultT_co]): + """Data from the successful completion of a command.""" + + public: _ResultT_co + """Public result data. Exposed over HTTP and stored in databases.""" + + private: _PrivateResultT_co + """Additional result data, only given to `opentrons.protocol_engine` internals.""" + + +@dataclass(frozen=True) +class DefinedErrorData(Generic[_ErrorT_co, _PrivateResultT_co]): + """Data from a command that failed with a defined error. + + This should only be used for "defined" errors, not any error. + See `AbstractCommandImpl.execute()`. + """ + + public: _ErrorT_co + """Public error data. Exposed over HTTP and stored in databases.""" + + private: _PrivateResultT_co + """Additional error data, only given to `opentrons.protocol_engine` internals.""" + + +class BaseCommand( + GenericModel, + # These type parameters need to be invariant because our fields are mutable. + Generic[_ParamsT, _ResultT, _ErrorT], +): """Base command model. You shouldn't use this class directly; instead, use or define @@ -134,7 +170,12 @@ class BaseCommand(GenericModel, Generic[_ParamsT, _ResultT]): None, description="Command execution result data, if succeeded", ) - error: Optional[ErrorOccurrence] = Field( + error: Union[ + _ErrorT, + # ErrorOccurrence here is for undefined errors not captured by _ErrorT. + ErrorOccurrence, + None, + ] = Field( None, description="Reference to error occurrence, if execution failed", ) @@ -169,69 +210,46 @@ class BaseCommand(GenericModel, Generic[_ParamsT, _ResultT]): ), ) - _ImplementationCls: Union[ - Type[AbstractCommandImpl[_ParamsT, _ResultT]], - Type[AbstractCommandWithPrivateResultImpl[_ParamsT, _ResultT, object]], + _ImplementationCls: Type[ + AbstractCommandImpl[ + _ParamsT, + Union[ + SuccessData[ + # Our _ImplementationCls must return public result data that can fit + # in our `result` field: + _ResultT, + # But we don't care (here) what kind of private result data it returns: + object, + ], + DefinedErrorData[ + # Likewise, for our `error` field: + _ErrorT, + object, + ], + ], + ] ] -class AbstractCommandImpl( - ABC, - Generic[_ParamsT_contra, _ResultT_co], -): - """Abstract command creation and execution implementation. - - A given command request should map to a specific command implementation, - which defines how to: - - - Create a command resource from the request model - - Execute the command, mapping data from execution into the result model - - This class should be used as the base class for new commands by default. You should only - use AbstractCommandWithPrivateResultImpl if you actually need private results to send to - the rest of the engine wihtout being published outside of it. - """ - - def __init__( - self, - state_view: StateView, - hardware_api: HardwareControlAPI, - equipment: execution.EquipmentHandler, - movement: execution.MovementHandler, - gantry_mover: execution.GantryMover, - labware_movement: execution.LabwareMovementHandler, - pipetting: execution.PipettingHandler, - tip_handler: execution.TipHandler, - run_control: execution.RunControlHandler, - rail_lights: execution.RailLightsHandler, - status_bar: execution.StatusBarHandler, - command_note_adder: CommandNoteAdder, - ) -> None: - """Initialize the command implementation with execution handlers.""" - pass - - @abstractmethod - async def execute(self, params: _ParamsT_contra) -> _ResultT_co: - """Execute the command, mapping data from execution into a response model.""" - ... +_ExecuteReturnT_co = TypeVar( + "_ExecuteReturnT_co", + bound=Union[ + SuccessData[BaseModel, object], + DefinedErrorData[ErrorOccurrence, object], + ], + covariant=True, +) -class AbstractCommandWithPrivateResultImpl( +class AbstractCommandImpl( ABC, - Generic[_ParamsT_contra, _ResultT_co, _PrivateResultT_co], + Generic[_ParamsT_contra, _ExecuteReturnT_co], ): - """Abstract command creation and execution implementation if the command has private results. + """Abstract command creation and execution implementation. A given command request should map to a specific command implementation, - which defines how to: - - - Create a command resource from the request model - - Execute the command, mapping data from execution into the result model - - This class should be used instead of AbstractCommandImpl as a base class if your command needs - to send data to result handlers that should not be published outside of the engine. - - Note that this class needs an extra type-parameter for the private result. + which defines how to execute the command and map data from execution into the + result model. """ def __init__( @@ -253,8 +271,16 @@ def __init__( pass @abstractmethod - async def execute( - self, params: _ParamsT_contra - ) -> Tuple[_ResultT_co, _PrivateResultT_co]: - """Execute the command, mapping data from execution into a response model.""" + async def execute(self, params: _ParamsT_contra) -> _ExecuteReturnT_co: + """Execute the command, mapping data from execution into a response model. + + This should either: + + - Return a `SuccessData`, if the command completed normally. + - Return a `DefinedErrorData`, if the command failed with a "defined error." + Defined errors are errors that are documented as part of the robot's public + API. + - Raise an exception, if the command failed with any other error + (in other words, an undefined error). + """ ... diff --git a/api/src/opentrons/protocol_engine/commands/comment.py b/api/src/opentrons/protocol_engine/commands/comment.py index 933e3bdbd53..d411b6b4047 100644 --- a/api/src/opentrons/protocol_engine/commands/comment.py +++ b/api/src/opentrons/protocol_engine/commands/comment.py @@ -4,7 +4,8 @@ from typing import Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence CommentCommandType = Literal["comment"] @@ -22,18 +23,20 @@ class CommentResult(BaseModel): """Result data from the execution of a Comment command.""" -class CommentImplementation(AbstractCommandImpl[CommentParams, CommentResult]): +class CommentImplementation( + AbstractCommandImpl[CommentParams, SuccessData[CommentResult, None]] +): """Comment command implementation.""" def __init__(self, **kwargs: object) -> None: pass - async def execute(self, params: CommentParams) -> CommentResult: + async def execute(self, params: CommentParams) -> SuccessData[CommentResult, None]: """No operation taken other than capturing message in command.""" - return CommentResult() + return SuccessData(public=CommentResult(), private=None) -class Comment(BaseCommand[CommentParams, CommentResult]): +class Comment(BaseCommand[CommentParams, CommentResult, ErrorOccurrence]): """Comment command model.""" commandType: CommentCommandType = "comment" diff --git a/api/src/opentrons/protocol_engine/commands/configure_for_volume.py b/api/src/opentrons/protocol_engine/commands/configure_for_volume.py index f1f59c35bcb..9a84e16dc45 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_for_volume.py +++ b/api/src/opentrons/protocol_engine/commands/configure_for_volume.py @@ -1,15 +1,12 @@ """Configure for volume command request, result, and implementation models.""" from __future__ import annotations from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type, Tuple +from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal from .pipetting_common import PipetteIdMixin -from .command import ( - AbstractCommandWithPrivateResultImpl, - BaseCommand, - BaseCommandCreate, -) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from .configuring_common import PipetteConfigUpdateResultMixin if TYPE_CHECKING: @@ -43,10 +40,9 @@ class ConfigureForVolumeResult(BaseModel): class ConfigureForVolumeImplementation( - AbstractCommandWithPrivateResultImpl[ + AbstractCommandImpl[ ConfigureForVolumeParams, - ConfigureForVolumeResult, - ConfigureForVolumePrivateResult, + SuccessData[ConfigureForVolumeResult, ConfigureForVolumePrivateResult], ] ): """Configure for volume command implementation.""" @@ -56,22 +52,25 @@ def __init__(self, equipment: EquipmentHandler, **kwargs: object) -> None: async def execute( self, params: ConfigureForVolumeParams - ) -> Tuple[ConfigureForVolumeResult, ConfigureForVolumePrivateResult]: + ) -> SuccessData[ConfigureForVolumeResult, ConfigureForVolumePrivateResult]: """Check that requested pipette can be configured for the given volume.""" pipette_result = await self._equipment.configure_for_volume( pipette_id=params.pipetteId, volume=params.volume, ) - return ConfigureForVolumeResult(), ConfigureForVolumePrivateResult( - pipette_id=pipette_result.pipette_id, - serial_number=pipette_result.serial_number, - config=pipette_result.static_config, + return SuccessData( + public=ConfigureForVolumeResult(), + private=ConfigureForVolumePrivateResult( + pipette_id=pipette_result.pipette_id, + serial_number=pipette_result.serial_number, + config=pipette_result.static_config, + ), ) class ConfigureForVolume( - BaseCommand[ConfigureForVolumeParams, ConfigureForVolumeResult] + BaseCommand[ConfigureForVolumeParams, ConfigureForVolumeResult, ErrorOccurrence] ): """Configure for volume command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py index 49b90ec7432..ace59d49fde 100644 --- a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py +++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py @@ -1,17 +1,14 @@ """Configure nozzle layout command request, result, and implementation models.""" from __future__ import annotations from pydantic import BaseModel -from typing import TYPE_CHECKING, Optional, Type, Tuple, Union +from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal from .pipetting_common import ( PipetteIdMixin, ) -from .command import ( - AbstractCommandWithPrivateResultImpl, - BaseCommand, - BaseCommandCreate, -) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from .configuring_common import ( PipetteNozzleLayoutResultMixin, ) @@ -55,10 +52,9 @@ class ConfigureNozzleLayoutResult(BaseModel): class ConfigureNozzleLayoutImplementation( - AbstractCommandWithPrivateResultImpl[ + AbstractCommandImpl[ ConfigureNozzleLayoutParams, - ConfigureNozzleLayoutResult, - ConfigureNozzleLayoutPrivateResult, + SuccessData[ConfigureNozzleLayoutResult, ConfigureNozzleLayoutPrivateResult], ] ): """Configure nozzle layout command implementation.""" @@ -71,7 +67,7 @@ def __init__( async def execute( self, params: ConfigureNozzleLayoutParams - ) -> Tuple[ConfigureNozzleLayoutResult, ConfigureNozzleLayoutPrivateResult]: + ) -> SuccessData[ConfigureNozzleLayoutResult, ConfigureNozzleLayoutPrivateResult]: """Check that requested pipette can support the requested nozzle layout.""" primary_nozzle = params.configurationParams.dict().get("primaryNozzle") front_right_nozzle = params.configurationParams.dict().get("frontRightNozzle") @@ -87,14 +83,19 @@ async def execute( **nozzle_params, ) - return ConfigureNozzleLayoutResult(), ConfigureNozzleLayoutPrivateResult( - pipette_id=params.pipetteId, - nozzle_map=nozzle_map, + return SuccessData( + public=ConfigureNozzleLayoutResult(), + private=ConfigureNozzleLayoutPrivateResult( + pipette_id=params.pipetteId, + nozzle_map=nozzle_map, + ), ) class ConfigureNozzleLayout( - BaseCommand[ConfigureNozzleLayoutParams, ConfigureNozzleLayoutResult] + BaseCommand[ + ConfigureNozzleLayoutParams, ConfigureNozzleLayoutResult, ErrorOccurrence + ] ): """Configure nozzle layout command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/custom.py b/api/src/opentrons/protocol_engine/commands/custom.py index e2598d4de15..2ceebda764c 100644 --- a/api/src/opentrons/protocol_engine/commands/custom.py +++ b/api/src/opentrons/protocol_engine/commands/custom.py @@ -14,7 +14,8 @@ from typing import Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence CustomCommandType = Literal["custom"] @@ -38,18 +39,20 @@ class Config: extra = Extra.allow -class CustomImplementation(AbstractCommandImpl[CustomParams, CustomResult]): +class CustomImplementation( + AbstractCommandImpl[CustomParams, SuccessData[CustomResult, None]] +): """Custom command implementation.""" # TODO(mm, 2022-11-09): figure out how a plugin can specify a custom command # implementation. For now, always no-op, so we can use custom commands as containers # for legacy RPC (pre-ProtocolEngine) payloads. - async def execute(self, params: CustomParams) -> CustomResult: + async def execute(self, params: CustomParams) -> SuccessData[CustomResult, None]: """A custom command does nothing when executed directly.""" - return CustomResult.construct() + return SuccessData(public=CustomResult.construct(), private=None) -class Custom(BaseCommand[CustomParams, CustomResult]): +class Custom(BaseCommand[CustomParams, CustomResult, ErrorOccurrence]): """Custom command model.""" commandType: CustomCommandType = "custom" diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index aa5017ed670..7ba9fe2ae52 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -14,7 +14,8 @@ BaseLiquidHandlingResult, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler @@ -40,7 +41,9 @@ class DispenseResult(BaseLiquidHandlingResult, DestinationPositionResult): pass -class DispenseImplementation(AbstractCommandImpl[DispenseParams, DispenseResult]): +class DispenseImplementation( + AbstractCommandImpl[DispenseParams, SuccessData[DispenseResult, None]] +): """Dispense command implementation.""" def __init__( @@ -49,7 +52,9 @@ def __init__( self._movement = movement self._pipetting = pipetting - async def execute(self, params: DispenseParams) -> DispenseResult: + async def execute( + self, params: DispenseParams + ) -> SuccessData[DispenseResult, None]: """Move to and dispense to the requested well.""" position = await self._movement.move_to_well( pipette_id=params.pipetteId, @@ -64,13 +69,16 @@ async def execute(self, params: DispenseParams) -> DispenseResult: push_out=params.pushOut, ) - return DispenseResult( - volume=volume, - position=DeckPoint(x=position.x, y=position.y, z=position.z), + return SuccessData( + public=DispenseResult( + volume=volume, + position=DeckPoint(x=position.x, y=position.y, z=position.z), + ), + private=None, ) -class Dispense(BaseCommand[DispenseParams, DispenseResult]): +class Dispense(BaseCommand[DispenseParams, DispenseResult, ErrorOccurrence]): """Dispense command model.""" commandType: DispenseCommandType = "dispense" diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index 9f0aee8df03..160345de469 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -11,7 +11,8 @@ FlowRateMixin, BaseLiquidHandlingResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import PipettingHandler @@ -36,14 +37,16 @@ class DispenseInPlaceResult(BaseLiquidHandlingResult): class DispenseInPlaceImplementation( - AbstractCommandImpl[DispenseInPlaceParams, DispenseInPlaceResult] + AbstractCommandImpl[DispenseInPlaceParams, SuccessData[DispenseInPlaceResult, None]] ): """DispenseInPlace command implementation.""" def __init__(self, pipetting: PipettingHandler, **kwargs: object) -> None: self._pipetting = pipetting - async def execute(self, params: DispenseInPlaceParams) -> DispenseInPlaceResult: + async def execute( + self, params: DispenseInPlaceParams + ) -> SuccessData[DispenseInPlaceResult, None]: """Dispense without moving the pipette.""" volume = await self._pipetting.dispense_in_place( pipette_id=params.pipetteId, @@ -51,10 +54,12 @@ async def execute(self, params: DispenseInPlaceParams) -> DispenseInPlaceResult: flow_rate=params.flowRate, push_out=params.pushOut, ) - return DispenseInPlaceResult(volume=volume) + return SuccessData(public=DispenseInPlaceResult(volume=volume), private=None) -class DispenseInPlace(BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult]): +class DispenseInPlace( + BaseCommand[DispenseInPlaceParams, DispenseInPlaceResult, ErrorOccurrence] +): """DispenseInPlace command model.""" commandType: DispenseInPlaceCommandType = "dispenseInPlace" diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 923c384e630..ddb3c56cf7e 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -7,7 +7,8 @@ from ..types import DropTipWellLocation, DeckPoint from .pipetting_common import PipetteIdMixin, DestinationPositionResult -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..state import StateView @@ -52,7 +53,9 @@ class DropTipResult(DestinationPositionResult): pass -class DropTipImplementation(AbstractCommandImpl[DropTipParams, DropTipResult]): +class DropTipImplementation( + AbstractCommandImpl[DropTipParams, SuccessData[DropTipResult, None]] +): """Drop tip command implementation.""" def __init__( @@ -66,7 +69,7 @@ def __init__( self._tip_handler = tip_handler self._movement_handler = movement - async def execute(self, params: DropTipParams) -> DropTipResult: + async def execute(self, params: DropTipParams) -> SuccessData[DropTipResult, None]: """Move to and drop a tip using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -101,12 +104,15 @@ async def execute(self, params: DropTipParams) -> DropTipResult: await self._tip_handler.drop_tip(pipette_id=pipette_id, home_after=home_after) - return DropTipResult( - position=DeckPoint(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DropTipResult( + position=DeckPoint(x=position.x, y=position.y, z=position.z) + ), + private=None, ) -class DropTip(BaseCommand[DropTipParams, DropTipResult]): +class DropTip(BaseCommand[DropTipParams, DropTipResult, ErrorOccurrence]): """Drop tip command model.""" commandType: DropTipCommandType = "dropTip" diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py index ae287f028dd..cf27732a6a5 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py @@ -5,7 +5,8 @@ from typing_extensions import Literal from .pipetting_common import PipetteIdMixin -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import TipHandler @@ -34,7 +35,7 @@ class DropTipInPlaceResult(BaseModel): class DropTipInPlaceImplementation( - AbstractCommandImpl[DropTipInPlaceParams, DropTipInPlaceResult] + AbstractCommandImpl[DropTipInPlaceParams, SuccessData[DropTipInPlaceResult, None]] ): """Drop tip in place command implementation.""" @@ -45,16 +46,20 @@ def __init__( ) -> None: self._tip_handler = tip_handler - async def execute(self, params: DropTipInPlaceParams) -> DropTipInPlaceResult: + async def execute( + self, params: DropTipInPlaceParams + ) -> SuccessData[DropTipInPlaceResult, None]: """Drop a tip using the requested pipette.""" await self._tip_handler.drop_tip( pipette_id=params.pipetteId, home_after=params.homeAfter ) - return DropTipInPlaceResult() + return SuccessData(public=DropTipInPlaceResult(), private=None) -class DropTipInPlace(BaseCommand[DropTipInPlaceParams, DropTipInPlaceResult]): +class DropTipInPlace( + BaseCommand[DropTipInPlaceParams, DropTipInPlaceResult, ErrorOccurrence] +): """Drop tip in place command model.""" commandType: DropTipInPlaceCommandType = "dropTipInPlace" diff --git a/api/src/opentrons/protocol_engine/commands/get_tip_presence.py b/api/src/opentrons/protocol_engine/commands/get_tip_presence.py index 0a878418a6b..6c4eea93a84 100644 --- a/api/src/opentrons/protocol_engine/commands/get_tip_presence.py +++ b/api/src/opentrons/protocol_engine/commands/get_tip_presence.py @@ -6,7 +6,8 @@ from typing_extensions import Literal from .pipetting_common import PipetteIdMixin -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from ..types import TipPresenceStatus @@ -37,7 +38,7 @@ class GetTipPresenceResult(BaseModel): class GetTipPresenceImplementation( - AbstractCommandImpl[GetTipPresenceParams, GetTipPresenceResult] + AbstractCommandImpl[GetTipPresenceParams, SuccessData[GetTipPresenceResult, None]] ): """GetTipPresence command implementation.""" @@ -48,7 +49,9 @@ def __init__( ) -> None: self._tip_handler = tip_handler - async def execute(self, params: GetTipPresenceParams) -> GetTipPresenceResult: + async def execute( + self, params: GetTipPresenceParams + ) -> SuccessData[GetTipPresenceResult, None]: """Verify if tip presence is as expected for the requested pipette.""" pipette_id = params.pipetteId @@ -56,10 +59,12 @@ async def execute(self, params: GetTipPresenceParams) -> GetTipPresenceResult: pipette_id=pipette_id, ) - return GetTipPresenceResult(status=result) + return SuccessData(public=GetTipPresenceResult(status=result), private=None) -class GetTipPresence(BaseCommand[GetTipPresenceParams, GetTipPresenceResult]): +class GetTipPresence( + BaseCommand[GetTipPresenceParams, GetTipPresenceResult, ErrorOccurrence] +): """GetTipPresence command model.""" commandType: GetTipPresenceCommandType = "getTipPresence" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py index 796047a5c40..b86bbc0e2ab 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -26,7 +27,9 @@ class CloseLabwareLatchResult(BaseModel): class CloseLabwareLatchImpl( - AbstractCommandImpl[CloseLabwareLatchParams, CloseLabwareLatchResult] + AbstractCommandImpl[ + CloseLabwareLatchParams, SuccessData[CloseLabwareLatchResult, None] + ] ): """Execution implementation of a Heater-Shaker's close labware latch command.""" @@ -39,7 +42,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: CloseLabwareLatchParams) -> CloseLabwareLatchResult: + async def execute( + self, params: CloseLabwareLatchParams + ) -> SuccessData[CloseLabwareLatchResult, None]: """Close a Heater-Shaker's labware latch.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( @@ -54,10 +59,12 @@ async def execute(self, params: CloseLabwareLatchParams) -> CloseLabwareLatchRes if hs_hardware_module is not None: await hs_hardware_module.close_labware_latch() - return CloseLabwareLatchResult() + return SuccessData(public=CloseLabwareLatchResult(), private=None) -class CloseLabwareLatch(BaseCommand[CloseLabwareLatchParams, CloseLabwareLatchResult]): +class CloseLabwareLatch( + BaseCommand[CloseLabwareLatchParams, CloseLabwareLatchResult, ErrorOccurrence] +): """A command to close a Heater-Shaker's latch.""" commandType: CloseLabwareLatchCommandType = "heaterShaker/closeLabwareLatch" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py index f3c7f102c0b..3392ddc5a9d 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -26,7 +27,9 @@ class DeactivateHeaterResult(BaseModel): class DeactivateHeaterImpl( - AbstractCommandImpl[DeactivateHeaterParams, DeactivateHeaterResult] + AbstractCommandImpl[ + DeactivateHeaterParams, SuccessData[DeactivateHeaterResult, None] + ] ): """Execution implementation of a Heater-Shaker's deactivate heater command.""" @@ -39,7 +42,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: DeactivateHeaterParams) -> DeactivateHeaterResult: + async def execute( + self, params: DeactivateHeaterParams + ) -> SuccessData[DeactivateHeaterResult, None]: """Unset a Heater-Shaker's target temperature.""" hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( module_id=params.moduleId @@ -53,10 +58,12 @@ async def execute(self, params: DeactivateHeaterParams) -> DeactivateHeaterResul if hs_hardware_module is not None: await hs_hardware_module.deactivate_heater() - return DeactivateHeaterResult() + return SuccessData(public=DeactivateHeaterResult(), private=None) -class DeactivateHeater(BaseCommand[DeactivateHeaterParams, DeactivateHeaterResult]): +class DeactivateHeater( + BaseCommand[DeactivateHeaterParams, DeactivateHeaterResult, ErrorOccurrence] +): """A command to unset a Heater-Shaker's target temperature.""" commandType: DeactivateHeaterCommandType = "heaterShaker/deactivateHeater" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py index 15e0761d61e..8c77c064282 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -25,7 +26,9 @@ class DeactivateShakerResult(BaseModel): class DeactivateShakerImpl( - AbstractCommandImpl[DeactivateShakerParams, DeactivateShakerResult] + AbstractCommandImpl[ + DeactivateShakerParams, SuccessData[DeactivateShakerResult, None] + ] ): """Execution implementation of a Heater-Shaker's deactivate shaker command.""" @@ -38,7 +41,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: DeactivateShakerParams) -> DeactivateShakerResult: + async def execute( + self, params: DeactivateShakerParams + ) -> SuccessData[DeactivateShakerResult, None]: """Deactivate shaker for a Heater-Shaker.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( @@ -55,10 +60,12 @@ async def execute(self, params: DeactivateShakerParams) -> DeactivateShakerResul if hs_hardware_module is not None: await hs_hardware_module.deactivate_shaker() - return DeactivateShakerResult() + return SuccessData(public=DeactivateShakerResult(), private=None) -class DeactivateShaker(BaseCommand[DeactivateShakerParams, DeactivateShakerResult]): +class DeactivateShaker( + BaseCommand[DeactivateShakerParams, DeactivateShakerResult, ErrorOccurrence] +): """A command to deactivate shaker for a Heater-Shaker.""" commandType: DeactivateShakerCommandType = "heaterShaker/deactivateShaker" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py index 76a3ee9a09d..a823f59149a 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py @@ -4,7 +4,8 @@ from typing_extensions import Literal, Type from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -32,7 +33,9 @@ class OpenLabwareLatchResult(BaseModel): class OpenLabwareLatchImpl( - AbstractCommandImpl[OpenLabwareLatchParams, OpenLabwareLatchResult] + AbstractCommandImpl[ + OpenLabwareLatchParams, SuccessData[OpenLabwareLatchResult, None] + ] ): """Execution implementation of a Heater-Shaker's open latch labware command.""" @@ -47,7 +50,9 @@ def __init__( self._equipment = equipment self._movement = movement - async def execute(self, params: OpenLabwareLatchParams) -> OpenLabwareLatchResult: + async def execute( + self, params: OpenLabwareLatchParams + ) -> SuccessData[OpenLabwareLatchResult, None]: """Open a Heater-Shaker's labware latch.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( @@ -76,10 +81,15 @@ async def execute(self, params: OpenLabwareLatchParams) -> OpenLabwareLatchResul if hs_hardware_module is not None: await hs_hardware_module.open_labware_latch() - return OpenLabwareLatchResult(pipetteRetracted=pipette_should_retract) + return SuccessData( + public=OpenLabwareLatchResult(pipetteRetracted=pipette_should_retract), + private=None, + ) -class OpenLabwareLatch(BaseCommand[OpenLabwareLatchParams, OpenLabwareLatchResult]): +class OpenLabwareLatch( + BaseCommand[OpenLabwareLatchParams, OpenLabwareLatchResult, ErrorOccurrence] +): """A command to open a Heater-Shaker's labware latch.""" commandType: OpenLabwareLatchCommandType = "heaterShaker/openLabwareLatch" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py index 52041519231..ca89166adae 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py @@ -4,7 +4,8 @@ from typing_extensions import Literal, Type from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -33,7 +34,9 @@ class SetAndWaitForShakeSpeedResult(BaseModel): class SetAndWaitForShakeSpeedImpl( - AbstractCommandImpl[SetAndWaitForShakeSpeedParams, SetAndWaitForShakeSpeedResult] + AbstractCommandImpl[ + SetAndWaitForShakeSpeedParams, SuccessData[SetAndWaitForShakeSpeedResult, None] + ] ): """Execution implementation of Heater-Shaker's set and wait shake speed command.""" @@ -51,7 +54,7 @@ def __init__( async def execute( self, params: SetAndWaitForShakeSpeedParams, - ) -> SetAndWaitForShakeSpeedResult: + ) -> SuccessData[SetAndWaitForShakeSpeedResult, None]: """Set and wait for a Heater-Shaker's target shake speed.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( @@ -83,11 +86,18 @@ async def execute( if hs_hardware_module is not None: await hs_hardware_module.set_speed(rpm=validated_speed) - return SetAndWaitForShakeSpeedResult(pipetteRetracted=pipette_should_retract) + return SuccessData( + public=SetAndWaitForShakeSpeedResult( + pipetteRetracted=pipette_should_retract + ), + private=None, + ) class SetAndWaitForShakeSpeed( - BaseCommand[SetAndWaitForShakeSpeedParams, SetAndWaitForShakeSpeedResult] + BaseCommand[ + SetAndWaitForShakeSpeedParams, SetAndWaitForShakeSpeedResult, ErrorOccurrence + ] ): """A command to set and wait for a Heater-Shaker's shake speed.""" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py index accc28e6cee..9e7cfba0f33 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -27,7 +28,9 @@ class SetTargetTemperatureResult(BaseModel): class SetTargetTemperatureImpl( - AbstractCommandImpl[SetTargetTemperatureParams, SetTargetTemperatureResult] + AbstractCommandImpl[ + SetTargetTemperatureParams, SuccessData[SetTargetTemperatureResult, None] + ] ): """Execution implementation of a Heater-Shaker's set temperature command.""" @@ -43,7 +46,7 @@ def __init__( async def execute( self, params: SetTargetTemperatureParams, - ) -> SetTargetTemperatureResult: + ) -> SuccessData[SetTargetTemperatureResult, None]: """Set a Heater-Shaker's target temperature.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( @@ -61,11 +64,11 @@ async def execute( if hs_hardware_module is not None: await hs_hardware_module.start_set_temperature(validated_temp) - return SetTargetTemperatureResult() + return SuccessData(public=SetTargetTemperatureResult(), private=None) class SetTargetTemperature( - BaseCommand[SetTargetTemperatureParams, SetTargetTemperatureResult] + BaseCommand[SetTargetTemperatureParams, SetTargetTemperatureResult, ErrorOccurrence] ): """A command to set a Heater-Shaker's target temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py index 82892e5b6b8..981053cc459 100644 --- a/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -35,7 +36,9 @@ class WaitForTemperatureResult(BaseModel): class WaitForTemperatureImpl( - AbstractCommandImpl[WaitForTemperatureParams, WaitForTemperatureResult] + AbstractCommandImpl[ + WaitForTemperatureParams, SuccessData[WaitForTemperatureResult, None] + ] ): """Execution implementation of a Heater-Shaker's wait for temperature command.""" @@ -50,7 +53,7 @@ def __init__( async def execute( self, params: WaitForTemperatureParams - ) -> WaitForTemperatureResult: + ) -> SuccessData[WaitForTemperatureResult, None]: """Wait for a Heater-Shaker's target temperature to be reached.""" hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate( module_id=params.moduleId @@ -69,11 +72,11 @@ async def execute( if hs_hardware_module is not None: await hs_hardware_module.await_temperature(awaiting_temperature=target_temp) - return WaitForTemperatureResult() + return SuccessData(public=WaitForTemperatureResult(), private=None) class WaitForTemperature( - BaseCommand[WaitForTemperatureParams, WaitForTemperatureResult] + BaseCommand[WaitForTemperatureParams, WaitForTemperatureResult, ErrorOccurrence] ): """A command to wait for a Heater-Shaker's target temperature to be reached.""" diff --git a/api/src/opentrons/protocol_engine/commands/home.py b/api/src/opentrons/protocol_engine/commands/home.py index 1e2cb7d96c9..9455470602a 100644 --- a/api/src/opentrons/protocol_engine/commands/home.py +++ b/api/src/opentrons/protocol_engine/commands/home.py @@ -6,7 +6,8 @@ from opentrons.types import MountType from ..types import MotorAxis -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -40,13 +41,15 @@ class HomeResult(BaseModel): """Result data from the execution of a Home command.""" -class HomeImplementation(AbstractCommandImpl[HomeParams, HomeResult]): +class HomeImplementation( + AbstractCommandImpl[HomeParams, SuccessData[HomeResult, None]] +): """Home command implementation.""" def __init__(self, movement: MovementHandler, **kwargs: object) -> None: self._movement = movement - async def execute(self, params: HomeParams) -> HomeResult: + async def execute(self, params: HomeParams) -> SuccessData[HomeResult, None]: """Home some or all motors to establish positional accuracy.""" if ( params.skipIfMountPositionOk is None @@ -55,10 +58,10 @@ async def execute(self, params: HomeParams) -> HomeResult: ) ): await self._movement.home(axes=params.axes) - return HomeResult() + return SuccessData(public=HomeResult(), private=None) -class Home(BaseCommand[HomeParams, HomeResult]): +class Home(BaseCommand[HomeParams, HomeResult, ErrorOccurrence]): """Command to send some (or all) motors to their home positions. Homing a motor re-establishes positional accuracy the first time a motor diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 64ed68b47ba..6e37607984c 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -15,7 +15,8 @@ AddressableAreaLocation, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..state import StateView @@ -86,7 +87,7 @@ class LoadLabwareResult(BaseModel): class LoadLabwareImplementation( - AbstractCommandImpl[LoadLabwareParams, LoadLabwareResult] + AbstractCommandImpl[LoadLabwareParams, SuccessData[LoadLabwareResult, None]] ): """Load labware command implementation.""" @@ -96,7 +97,9 @@ def __init__( self._equipment = equipment self._state_view = state_view - async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult: + async def execute( + self, params: LoadLabwareParams + ) -> SuccessData[LoadLabwareResult, None]: """Load definition and calibration data necessary for a labware.""" # TODO (tz, 8-15-2023): extend column validation to column 1 when working # on https://opentrons.atlassian.net/browse/RSS-258 and completing @@ -144,14 +147,17 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult: bottom_labware_id=verified_location.labwareId, ) - return LoadLabwareResult( - labwareId=loaded_labware.labware_id, - definition=loaded_labware.definition, - offsetId=loaded_labware.offsetId, + return SuccessData( + public=LoadLabwareResult( + labwareId=loaded_labware.labware_id, + definition=loaded_labware.definition, + offsetId=loaded_labware.offsetId, + ), + private=None, ) -class LoadLabware(BaseCommand[LoadLabwareParams, LoadLabwareResult]): +class LoadLabware(BaseCommand[LoadLabwareParams, LoadLabwareResult, ErrorOccurrence]): """Load labware command resource model.""" commandType: LoadLabwareCommandType = "loadLabware" diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid.py b/api/src/opentrons/protocol_engine/commands/load_liquid.py index b9be0fa3501..02585640b0e 100644 --- a/api/src/opentrons/protocol_engine/commands/load_liquid.py +++ b/api/src/opentrons/protocol_engine/commands/load_liquid.py @@ -4,7 +4,8 @@ from typing import Optional, Type, Dict, TYPE_CHECKING from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..state import StateView @@ -35,13 +36,17 @@ class LoadLiquidResult(BaseModel): pass -class LoadLiquidImplementation(AbstractCommandImpl[LoadLiquidParams, LoadLiquidResult]): +class LoadLiquidImplementation( + AbstractCommandImpl[LoadLiquidParams, SuccessData[LoadLiquidResult, None]] +): """Load liquid command implementation.""" def __init__(self, state_view: StateView, **kwargs: object) -> None: self._state_view = state_view - async def execute(self, params: LoadLiquidParams) -> LoadLiquidResult: + async def execute( + self, params: LoadLiquidParams + ) -> SuccessData[LoadLiquidResult, None]: """Load data necessary for a liquid.""" self._state_view.liquid.validate_liquid_id(params.liquidId) @@ -49,10 +54,10 @@ async def execute(self, params: LoadLiquidParams) -> LoadLiquidResult: labware_id=params.labwareId, wells=params.volumeByWell ) - return LoadLiquidResult() + return SuccessData(public=LoadLiquidResult(), private=None) -class LoadLiquid(BaseCommand[LoadLiquidParams, LoadLiquidResult]): +class LoadLiquid(BaseCommand[LoadLiquidParams, LoadLiquidResult, ErrorOccurrence]): """Load liquid command resource model.""" commandType: LoadLiquidCommandType = "loadLiquid" diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index 5c1d474be4d..e7f847ab92e 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -4,7 +4,8 @@ from typing_extensions import Literal from pydantic import BaseModel, Field -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from ..types import ( DeckSlotLocation, ModuleType, @@ -101,7 +102,9 @@ class LoadModuleResult(BaseModel): ) -class LoadModuleImplementation(AbstractCommandImpl[LoadModuleParams, LoadModuleResult]): +class LoadModuleImplementation( + AbstractCommandImpl[LoadModuleParams, SuccessData[LoadModuleResult, None]] +): """The implementation of the load module command.""" def __init__( @@ -110,7 +113,9 @@ def __init__( self._equipment = equipment self._state_view = state_view - async def execute(self, params: LoadModuleParams) -> LoadModuleResult: + async def execute( + self, params: LoadModuleParams + ) -> SuccessData[LoadModuleResult, None]: """Check that the requested module is attached and assign its identifier.""" module_type = params.model.as_type() self._ensure_module_location(params.location.slotName, module_type) @@ -146,11 +151,14 @@ async def execute(self, params: LoadModuleParams) -> LoadModuleResult: module_id=params.moduleId, ) - return LoadModuleResult( - moduleId=loaded_module.module_id, - serialNumber=loaded_module.serial_number, - model=loaded_module.definition.model, - definition=loaded_module.definition, + return SuccessData( + public=LoadModuleResult( + moduleId=loaded_module.module_id, + serialNumber=loaded_module.serial_number, + model=loaded_module.definition.model, + definition=loaded_module.definition, + ), + private=None, ) def _ensure_module_location( @@ -178,7 +186,7 @@ def _ensure_module_location( ) -class LoadModule(BaseCommand[LoadModuleParams, LoadModuleResult]): +class LoadModule(BaseCommand[LoadModuleParams, LoadModuleResult, ErrorOccurrence]): """The model for a load module command.""" commandType: LoadModuleCommandType = "loadModule" diff --git a/api/src/opentrons/protocol_engine/commands/load_pipette.py b/api/src/opentrons/protocol_engine/commands/load_pipette.py index 5b6be4dea76..ea7ac60bad3 100644 --- a/api/src/opentrons/protocol_engine/commands/load_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/load_pipette.py @@ -8,17 +8,14 @@ from opentrons_shared_data.robot import user_facing_robot_type from opentrons_shared_data.robot.dev_types import RobotTypeEnum from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type, Tuple +from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType -from .command import ( - AbstractCommandWithPrivateResultImpl, - BaseCommand, - BaseCommandCreate, -) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from .configuring_common import PipetteConfigUpdateResultMixin from ..errors import InvalidSpecificationForRobotTypeError, InvalidLoadPipetteSpecsError @@ -64,8 +61,8 @@ class LoadPipetteResult(BaseModel): class LoadPipetteImplementation( - AbstractCommandWithPrivateResultImpl[ - LoadPipetteParams, LoadPipetteResult, LoadPipettePrivateResult + AbstractCommandImpl[ + LoadPipetteParams, SuccessData[LoadPipetteResult, LoadPipettePrivateResult] ] ): """Load pipette command implementation.""" @@ -78,7 +75,7 @@ def __init__( async def execute( self, params: LoadPipetteParams - ) -> Tuple[LoadPipetteResult, LoadPipettePrivateResult]: + ) -> SuccessData[LoadPipetteResult, LoadPipettePrivateResult]: """Check that requested pipette is attached and assign its identifier.""" pipette_generation = convert_to_pipette_name_type( params.pipetteName.value @@ -114,16 +111,17 @@ async def execute( pipette_id=params.pipetteId, ) - return LoadPipetteResult( - pipetteId=loaded_pipette.pipette_id - ), LoadPipettePrivateResult( - pipette_id=loaded_pipette.pipette_id, - serial_number=loaded_pipette.serial_number, - config=loaded_pipette.static_config, + return SuccessData( + public=LoadPipetteResult(pipetteId=loaded_pipette.pipette_id), + private=LoadPipettePrivateResult( + pipette_id=loaded_pipette.pipette_id, + serial_number=loaded_pipette.serial_number, + config=loaded_pipette.static_config, + ), ) -class LoadPipette(BaseCommand[LoadPipetteParams, LoadPipetteResult]): +class LoadPipette(BaseCommand[LoadPipetteParams, LoadPipetteResult, ErrorOccurrence]): """Load pipette command model.""" commandType: LoadPipetteCommandType = "loadPipette" diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py index b1773e98b8f..47a087059d5 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/disengage.py @@ -8,7 +8,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.execution import EquipmentHandler @@ -36,7 +37,9 @@ class DisengageResult(BaseModel): pass -class DisengageImplementation(AbstractCommandImpl[DisengageParams, DisengageResult]): +class DisengageImplementation( + AbstractCommandImpl[DisengageParams, SuccessData[DisengageResult, None]] +): """The implementation of a Magnetic Module disengage command.""" def __init__( @@ -48,7 +51,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: DisengageParams) -> DisengageResult: + async def execute( + self, params: DisengageParams + ) -> SuccessData[DisengageResult, None]: """Execute a Magnetic Module disengage command. Raises: @@ -70,10 +75,10 @@ async def execute(self, params: DisengageParams) -> DisengageResult: if hardware_module is not None: # Not virtualizing modules. await hardware_module.deactivate() - return DisengageResult() + return SuccessData(public=DisengageResult(), private=None) -class Disengage(BaseCommand[DisengageParams, DisengageResult]): +class Disengage(BaseCommand[DisengageParams, DisengageResult, ErrorOccurrence]): """A command to disengage a Magnetic Module's magnets.""" commandType: DisengageCommandType = "magneticModule/disengage" diff --git a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py index f21c23dafef..fcedd750bc3 100644 --- a/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py +++ b/api/src/opentrons/protocol_engine/commands/magnetic_module/engage.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.execution import EquipmentHandler @@ -52,7 +53,9 @@ class EngageResult(BaseModel): pass -class EngageImplementation(AbstractCommandImpl[EngageParams, EngageResult]): +class EngageImplementation( + AbstractCommandImpl[EngageParams, SuccessData[EngageResult, None]] +): """The implementation of a Magnetic Module engage command.""" def __init__( @@ -64,7 +67,7 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: EngageParams) -> EngageResult: + async def execute(self, params: EngageParams) -> SuccessData[EngageResult, None]: """Execute a Magnetic Module engage command. Raises: @@ -92,10 +95,10 @@ async def execute(self, params: EngageParams) -> EngageResult: if hardware_module is not None: # Not virtualizing modules. await hardware_module.engage(height=hardware_height) - return EngageResult() + return SuccessData(public=EngageResult(), private=None) -class Engage(BaseCommand[EngageParams, EngageResult]): +class Engage(BaseCommand[EngageParams, EngageResult, ErrorOccurrence]): """A command to engage a Magnetic Module's magnets.""" commandType: EngageCommandType = "magneticModule/engage" diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 653c390c64b..42728c05272 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -17,7 +17,8 @@ ) from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType from ..resources import labware_validation, fixture_validation -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from opentrons_shared_data.gripper.constants import GRIPPER_PADDLE_WIDTH if TYPE_CHECKING: @@ -74,7 +75,7 @@ class MoveLabwareResult(BaseModel): class MoveLabwareImplementation( - AbstractCommandImpl[MoveLabwareParams, MoveLabwareResult] + AbstractCommandImpl[MoveLabwareParams, SuccessData[MoveLabwareResult, None]] ): """The execution implementation for ``moveLabware`` commands.""" @@ -93,7 +94,7 @@ def __init__( async def execute( # noqa: C901 self, params: MoveLabwareParams - ) -> MoveLabwareResult: + ) -> SuccessData[MoveLabwareResult, None]: """Move a loaded labware to a new location.""" # Allow propagation of LabwareNotLoadedError. current_labware = self._state_view.labware.get(labware_id=params.labwareId) @@ -212,10 +213,12 @@ async def execute( # noqa: C901 # Pause to allow for manual labware movement await self._run_control.wait_for_resume() - return MoveLabwareResult(offsetId=new_offset_id) + return SuccessData( + public=MoveLabwareResult(offsetId=new_offset_id), private=None + ) -class MoveLabware(BaseCommand[MoveLabwareParams, MoveLabwareResult]): +class MoveLabware(BaseCommand[MoveLabwareParams, MoveLabwareResult, ErrorOccurrence]): """A ``moveLabware`` command.""" commandType: MoveLabwareCommandType = "moveLabware" diff --git a/api/src/opentrons/protocol_engine/commands/move_relative.py b/api/src/opentrons/protocol_engine/commands/move_relative.py index 8324e95719b..38ac0806217 100644 --- a/api/src/opentrons/protocol_engine/commands/move_relative.py +++ b/api/src/opentrons/protocol_engine/commands/move_relative.py @@ -5,7 +5,8 @@ from typing_extensions import Literal from ..types import MovementAxis, DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from .pipetting_common import DestinationPositionResult if TYPE_CHECKING: @@ -36,14 +37,16 @@ class MoveRelativeResult(DestinationPositionResult): class MoveRelativeImplementation( - AbstractCommandImpl[MoveRelativeParams, MoveRelativeResult] + AbstractCommandImpl[MoveRelativeParams, SuccessData[MoveRelativeResult, None]] ): """Move relative command implementation.""" def __init__(self, movement: MovementHandler, **kwargs: object) -> None: self._movement = movement - async def execute(self, params: MoveRelativeParams) -> MoveRelativeResult: + async def execute( + self, params: MoveRelativeParams + ) -> SuccessData[MoveRelativeResult, None]: """Move (jog) a given pipette a relative distance.""" x, y, z = await self._movement.move_relative( pipette_id=params.pipetteId, @@ -51,10 +54,14 @@ async def execute(self, params: MoveRelativeParams) -> MoveRelativeResult: distance=params.distance, ) - return MoveRelativeResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=MoveRelativeResult(position=DeckPoint(x=x, y=y, z=z)), private=None + ) -class MoveRelative(BaseCommand[MoveRelativeParams, MoveRelativeResult]): +class MoveRelative( + BaseCommand[MoveRelativeParams, MoveRelativeResult, ErrorOccurrence] +): """Command to move (jog) a given pipette a relative distance.""" commandType: MoveRelativeCommandType = "moveRelative" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index 7dfc0b53895..5d959538ca2 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -12,7 +12,8 @@ MovementMixin, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -71,7 +72,9 @@ class MoveToAddressableAreaResult(DestinationPositionResult): class MoveToAddressableAreaImplementation( - AbstractCommandImpl[MoveToAddressableAreaParams, MoveToAddressableAreaResult] + AbstractCommandImpl[ + MoveToAddressableAreaParams, SuccessData[MoveToAddressableAreaResult, None] + ] ): """Move to addressable area command implementation.""" @@ -83,7 +86,7 @@ def __init__( async def execute( self, params: MoveToAddressableAreaParams - ) -> MoveToAddressableAreaResult: + ) -> SuccessData[MoveToAddressableAreaResult, None]: """Move the requested pipette to the requested addressable area.""" self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName @@ -104,11 +107,16 @@ async def execute( stay_at_highest_possible_z=params.stayAtHighestPossibleZ, ) - return MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)), + private=None, + ) class MoveToAddressableArea( - BaseCommand[MoveToAddressableAreaParams, MoveToAddressableAreaResult] + BaseCommand[ + MoveToAddressableAreaParams, MoveToAddressableAreaResult, ErrorOccurrence + ] ): """Move to addressable area command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py index dc79714c829..d38d7ceb758 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -12,7 +12,8 @@ MovementMixin, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -83,7 +84,8 @@ class MoveToAddressableAreaForDropTipResult(DestinationPositionResult): class MoveToAddressableAreaForDropTipImplementation( AbstractCommandImpl[ - MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult + MoveToAddressableAreaForDropTipParams, + SuccessData[MoveToAddressableAreaForDropTipResult, None], ] ): """Move to addressable area for drop tip command implementation.""" @@ -96,7 +98,7 @@ def __init__( async def execute( self, params: MoveToAddressableAreaForDropTipParams - ) -> MoveToAddressableAreaForDropTipResult: + ) -> SuccessData[MoveToAddressableAreaForDropTipResult, None]: """Move the requested pipette to the requested addressable area in preperation of a drop tip.""" self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName @@ -125,12 +127,19 @@ async def execute( ignore_tip_configuration=params.ignoreTipConfiguration, ) - return MoveToAddressableAreaForDropTipResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=MoveToAddressableAreaForDropTipResult( + position=DeckPoint(x=x, y=y, z=z) + ), + private=None, + ) class MoveToAddressableAreaForDropTip( BaseCommand[ - MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult + MoveToAddressableAreaForDropTipParams, + MoveToAddressableAreaForDropTipResult, + ErrorOccurrence, ] ): """Move to addressable area for drop tip command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py index f6d44f953c3..71e45b05e60 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py @@ -7,7 +7,8 @@ from ..types import DeckPoint from .pipetting_common import PipetteIdMixin, MovementMixin, DestinationPositionResult -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -32,7 +33,9 @@ class MoveToCoordinatesResult(DestinationPositionResult): class MoveToCoordinatesImplementation( - AbstractCommandImpl[MoveToCoordinatesParams, MoveToCoordinatesResult] + AbstractCommandImpl[ + MoveToCoordinatesParams, SuccessData[MoveToCoordinatesResult, None] + ] ): """Move to coordinates command implementation.""" @@ -43,7 +46,9 @@ def __init__( ) -> None: self._movement = movement - async def execute(self, params: MoveToCoordinatesParams) -> MoveToCoordinatesResult: + async def execute( + self, params: MoveToCoordinatesParams + ) -> SuccessData[MoveToCoordinatesResult, None]: """Move the requested pipette to the requested coordinates.""" x, y, z = await self._movement.move_to_coordinates( pipette_id=params.pipetteId, @@ -53,10 +58,15 @@ async def execute(self, params: MoveToCoordinatesParams) -> MoveToCoordinatesRes speed=params.speed, ) - return MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)), + private=None, + ) -class MoveToCoordinates(BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult]): +class MoveToCoordinates( + BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, ErrorOccurrence] +): """Move to well command model.""" commandType: MoveToCoordinatesCommandType = "moveToCoordinates" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 31336645f03..2ed10757b69 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -10,7 +10,8 @@ MovementMixin, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -30,13 +31,17 @@ class MoveToWellResult(DestinationPositionResult): pass -class MoveToWellImplementation(AbstractCommandImpl[MoveToWellParams, MoveToWellResult]): +class MoveToWellImplementation( + AbstractCommandImpl[MoveToWellParams, SuccessData[MoveToWellResult, None]] +): """Move to well command implementation.""" def __init__(self, movement: MovementHandler, **kwargs: object) -> None: self._movement = movement - async def execute(self, params: MoveToWellParams) -> MoveToWellResult: + async def execute( + self, params: MoveToWellParams + ) -> SuccessData[MoveToWellResult, None]: """Move the requested pipette to the requested well.""" x, y, z = await self._movement.move_to_well( pipette_id=params.pipetteId, @@ -48,10 +53,12 @@ async def execute(self, params: MoveToWellParams) -> MoveToWellResult: speed=params.speed, ) - return MoveToWellResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=MoveToWellResult(position=DeckPoint(x=x, y=y, z=z)), private=None + ) -class MoveToWell(BaseCommand[MoveToWellParams, MoveToWellResult]): +class MoveToWell(BaseCommand[MoveToWellParams, MoveToWellResult, ErrorOccurrence]): """Move to well command model.""" commandType: MoveToWellCommandType = "moveToWell" diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 8c2902a5f4b..30716e9dc40 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -10,7 +10,8 @@ WellLocationMixin, DestinationPositionResult, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..state import StateView @@ -49,7 +50,9 @@ class PickUpTipResult(DestinationPositionResult): ) -class PickUpTipImplementation(AbstractCommandImpl[PickUpTipParams, PickUpTipResult]): +class PickUpTipImplementation( + AbstractCommandImpl[PickUpTipParams, SuccessData[PickUpTipResult, None]] +): """Pick up tip command implementation.""" def __init__( @@ -63,7 +66,9 @@ def __init__( self._tip_handler = tip_handler self._movement = movement - async def execute(self, params: PickUpTipParams) -> PickUpTipResult: + async def execute( + self, params: PickUpTipParams + ) -> SuccessData[PickUpTipResult, None]: """Move to and pick up a tip using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -83,15 +88,18 @@ async def execute(self, params: PickUpTipParams) -> PickUpTipResult: well_name=well_name, ) - return PickUpTipResult( - tipVolume=tip_geometry.volume, - tipLength=tip_geometry.length, - tipDiameter=tip_geometry.diameter, - position=DeckPoint(x=position.x, y=position.y, z=position.z), + return SuccessData( + public=PickUpTipResult( + tipVolume=tip_geometry.volume, + tipLength=tip_geometry.length, + tipDiameter=tip_geometry.diameter, + position=DeckPoint(x=position.x, y=position.y, z=position.z), + ), + private=None, ) -class PickUpTip(BaseCommand[PickUpTipParams, PickUpTipResult]): +class PickUpTip(BaseCommand[PickUpTipParams, PickUpTipResult, ErrorOccurrence]): """Pick up tip command model.""" commandType: PickUpTipCommandType = "pickUpTip" diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index 57fa679bb09..d427b38dc1e 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -8,11 +8,8 @@ from .pipetting_common import ( PipetteIdMixin, ) -from .command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, -) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution.pipetting import PipettingHandler @@ -34,8 +31,7 @@ class PrepareToAspirateResult(BaseModel): class PrepareToAspirateImplementation( AbstractCommandImpl[ - PrepareToAspirateParams, - PrepareToAspirateResult, + PrepareToAspirateParams, SuccessData[PrepareToAspirateResult, None] ] ): """Prepare for aspirate command implementation.""" @@ -43,16 +39,20 @@ class PrepareToAspirateImplementation( def __init__(self, pipetting: PipettingHandler, **kwargs: object) -> None: self._pipetting_handler = pipetting - async def execute(self, params: PrepareToAspirateParams) -> PrepareToAspirateResult: + async def execute( + self, params: PrepareToAspirateParams + ) -> SuccessData[PrepareToAspirateResult, None]: """Prepare the pipette to aspirate.""" await self._pipetting_handler.prepare_for_aspirate( pipette_id=params.pipetteId, ) - return PrepareToAspirateResult() + return SuccessData(public=PrepareToAspirateResult(), private=None) -class PrepareToAspirate(BaseCommand[PrepareToAspirateParams, PrepareToAspirateResult]): +class PrepareToAspirate( + BaseCommand[PrepareToAspirateParams, PrepareToAspirateResult, ErrorOccurrence] +): """Prepare for aspirate command model.""" commandType: PrepareToAspirateCommandType = "prepareToAspirate" diff --git a/api/src/opentrons/protocol_engine/commands/reload_labware.py b/api/src/opentrons/protocol_engine/commands/reload_labware.py index 247f717feb9..884b8324d21 100644 --- a/api/src/opentrons/protocol_engine/commands/reload_labware.py +++ b/api/src/opentrons/protocol_engine/commands/reload_labware.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..state import StateView @@ -45,7 +46,7 @@ class ReloadLabwareResult(BaseModel): class ReloadLabwareImplementation( - AbstractCommandImpl[ReloadLabwareParams, ReloadLabwareResult] + AbstractCommandImpl[ReloadLabwareParams, SuccessData[ReloadLabwareResult, None]] ): """Reload labware command implementation.""" @@ -55,19 +56,26 @@ def __init__( self._equipment = equipment self._state_view = state_view - async def execute(self, params: ReloadLabwareParams) -> ReloadLabwareResult: + async def execute( + self, params: ReloadLabwareParams + ) -> SuccessData[ReloadLabwareResult, None]: """Reload the definition and calibration data for a specific labware.""" reloaded_labware = await self._equipment.reload_labware( labware_id=params.labwareId, ) - return ReloadLabwareResult( - labwareId=params.labwareId, - offsetId=reloaded_labware.offsetId, + return SuccessData( + public=ReloadLabwareResult( + labwareId=params.labwareId, + offsetId=reloaded_labware.offsetId, + ), + private=None, ) -class ReloadLabware(BaseCommand[ReloadLabwareParams, ReloadLabwareResult]): +class ReloadLabware( + BaseCommand[ReloadLabwareParams, ReloadLabwareResult, ErrorOccurrence] +): """Reload labware command resource model.""" commandType: ReloadLabwareCommandType = "reloadLabware" diff --git a/api/src/opentrons/protocol_engine/commands/retract_axis.py b/api/src/opentrons/protocol_engine/commands/retract_axis.py index ba23e6612a3..d989f1fd793 100644 --- a/api/src/opentrons/protocol_engine/commands/retract_axis.py +++ b/api/src/opentrons/protocol_engine/commands/retract_axis.py @@ -5,7 +5,8 @@ from typing_extensions import Literal from ..types import MotorAxis -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler @@ -37,20 +38,22 @@ class RetractAxisResult(BaseModel): class RetractAxisImplementation( - AbstractCommandImpl[RetractAxisParams, RetractAxisResult] + AbstractCommandImpl[RetractAxisParams, SuccessData[RetractAxisResult, None]] ): """Retract Axis command implementation.""" def __init__(self, movement: MovementHandler, **kwargs: object) -> None: self._movement = movement - async def execute(self, params: RetractAxisParams) -> RetractAxisResult: + async def execute( + self, params: RetractAxisParams + ) -> SuccessData[RetractAxisResult, None]: """Retract the specified axis.""" await self._movement.retract_axis(axis=params.axis) - return RetractAxisResult() + return SuccessData(public=RetractAxisResult(), private=None) -class RetractAxis(BaseCommand[RetractAxisParams, RetractAxisResult]): +class RetractAxis(BaseCommand[RetractAxisParams, RetractAxisResult, ErrorOccurrence]): """Command to retract the specified axis to its home position.""" commandType: RetractAxisCommandType = "retractAxis" diff --git a/api/src/opentrons/protocol_engine/commands/save_position.py b/api/src/opentrons/protocol_engine/commands/save_position.py index a45937a73e8..41b2cb74641 100644 --- a/api/src/opentrons/protocol_engine/commands/save_position.py +++ b/api/src/opentrons/protocol_engine/commands/save_position.py @@ -7,7 +7,8 @@ from ..types import DeckPoint from ..resources import ModelUtils -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import GantryMover @@ -45,7 +46,7 @@ class SavePositionResult(BaseModel): class SavePositionImplementation( - AbstractCommandImpl[SavePositionParams, SavePositionResult] + AbstractCommandImpl[SavePositionParams, SuccessData[SavePositionResult, None]] ): """Save position command implementation.""" @@ -58,7 +59,9 @@ def __init__( self._gantry_mover = gantry_mover self._model_utils = model_utils or ModelUtils() - async def execute(self, params: SavePositionParams) -> SavePositionResult: + async def execute( + self, params: SavePositionParams + ) -> SuccessData[SavePositionResult, None]: """Check the requested pipette's current position.""" position_id = self._model_utils.ensure_id(params.positionId) fail_on_not_homed = ( @@ -68,13 +71,18 @@ async def execute(self, params: SavePositionParams) -> SavePositionResult: pipette_id=params.pipetteId, fail_on_not_homed=fail_on_not_homed ) - return SavePositionResult( - positionId=position_id, - position=DeckPoint(x=x, y=y, z=z), + return SuccessData( + public=SavePositionResult( + positionId=position_id, + position=DeckPoint(x=x, y=y, z=z), + ), + private=None, ) -class SavePosition(BaseCommand[SavePositionParams, SavePositionResult]): +class SavePosition( + BaseCommand[SavePositionParams, SavePositionResult, ErrorOccurrence] +): """Save Position command model.""" commandType: SavePositionCommandType = "savePosition" diff --git a/api/src/opentrons/protocol_engine/commands/set_rail_lights.py b/api/src/opentrons/protocol_engine/commands/set_rail_lights.py index db5b8cee81f..6235e0d9bb6 100644 --- a/api/src/opentrons/protocol_engine/commands/set_rail_lights.py +++ b/api/src/opentrons/protocol_engine/commands/set_rail_lights.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import RailLightsHandler @@ -28,20 +29,24 @@ class SetRailLightsResult(BaseModel): class SetRailLightsImplementation( - AbstractCommandImpl[SetRailLightsParams, SetRailLightsResult] + AbstractCommandImpl[SetRailLightsParams, SuccessData[SetRailLightsResult, None]] ): """setRailLights command implementation.""" def __init__(self, rail_lights: RailLightsHandler, **kwargs: object) -> None: self._rail_lights = rail_lights - async def execute(self, params: SetRailLightsParams) -> SetRailLightsResult: + async def execute( + self, params: SetRailLightsParams + ) -> SuccessData[SetRailLightsResult, None]: """Dispatch a set lights command setting the state of the rail lights.""" await self._rail_lights.set_rail_lights(params.on) - return SetRailLightsResult() + return SuccessData(public=SetRailLightsResult(), private=None) -class SetRailLights(BaseCommand[SetRailLightsParams, SetRailLightsResult]): +class SetRailLights( + BaseCommand[SetRailLightsParams, SetRailLightsResult, ErrorOccurrence] +): """setRailLights command model.""" commandType: SetRailLightsCommandType = "setRailLights" diff --git a/api/src/opentrons/protocol_engine/commands/set_status_bar.py b/api/src/opentrons/protocol_engine/commands/set_status_bar.py index b493d59908a..cb83aa56ce2 100644 --- a/api/src/opentrons/protocol_engine/commands/set_status_bar.py +++ b/api/src/opentrons/protocol_engine/commands/set_status_bar.py @@ -6,7 +6,8 @@ import enum from opentrons.hardware_control.types import StatusBarState -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import StatusBarHandler @@ -48,22 +49,26 @@ class SetStatusBarResult(BaseModel): class SetStatusBarImplementation( - AbstractCommandImpl[SetStatusBarParams, SetStatusBarResult] + AbstractCommandImpl[SetStatusBarParams, SuccessData[SetStatusBarResult, None]] ): """setStatusBar command implementation.""" def __init__(self, status_bar: StatusBarHandler, **kwargs: object) -> None: self._status_bar = status_bar - async def execute(self, params: SetStatusBarParams) -> SetStatusBarResult: + async def execute( + self, params: SetStatusBarParams + ) -> SuccessData[SetStatusBarResult, None]: """Execute the setStatusBar command.""" if not self._status_bar.status_bar_should_not_be_changed(): state = _animation_to_status_bar_state(params.animation) await self._status_bar.set_status_bar(state) - return SetStatusBarResult() + return SuccessData(public=SetStatusBarResult(), private=None) -class SetStatusBar(BaseCommand[SetStatusBarParams, SetStatusBarResult]): +class SetStatusBar( + BaseCommand[SetStatusBarParams, SetStatusBarResult, ErrorOccurrence] +): """setStatusBar command model.""" commandType: SetStatusBarCommandType = "setStatusBar" diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py index ae03d057c23..979195933b2 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/deactivate.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -25,7 +26,9 @@ class DeactivateTemperatureResult(BaseModel): class DeactivateTemperatureImpl( - AbstractCommandImpl[DeactivateTemperatureParams, DeactivateTemperatureResult] + AbstractCommandImpl[ + DeactivateTemperatureParams, SuccessData[DeactivateTemperatureResult, None] + ] ): """Execution implementation of a Temperature Module's deactivate command.""" @@ -40,7 +43,7 @@ def __init__( async def execute( self, params: DeactivateTemperatureParams - ) -> DeactivateTemperatureResult: + ) -> SuccessData[DeactivateTemperatureResult, None]: """Deactivate a Temperature Module.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. module_substate = self._state_view.modules.get_temperature_module_substate( @@ -54,11 +57,13 @@ async def execute( if temp_hardware_module is not None: await temp_hardware_module.deactivate() - return DeactivateTemperatureResult() + return SuccessData(public=DeactivateTemperatureResult(), private=None) class DeactivateTemperature( - BaseCommand[DeactivateTemperatureParams, DeactivateTemperatureResult] + BaseCommand[ + DeactivateTemperatureParams, DeactivateTemperatureResult, ErrorOccurrence + ] ): """A command to deactivate a Temperature Module.""" diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py index 97b2d9a3ecc..4302773722b 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -32,7 +33,9 @@ class SetTargetTemperatureResult(BaseModel): class SetTargetTemperatureImpl( - AbstractCommandImpl[SetTargetTemperatureParams, SetTargetTemperatureResult] + AbstractCommandImpl[ + SetTargetTemperatureParams, SuccessData[SetTargetTemperatureResult, None] + ] ): """Execution implementation of a Temperature Module's set temperature command.""" @@ -47,7 +50,7 @@ def __init__( async def execute( self, params: SetTargetTemperatureParams - ) -> SetTargetTemperatureResult: + ) -> SuccessData[SetTargetTemperatureResult, None]: """Set a Temperature Module's target temperature.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. module_substate = self._state_view.modules.get_temperature_module_substate( @@ -64,11 +67,14 @@ async def execute( if temp_hardware_module is not None: await temp_hardware_module.start_set_temperature(celsius=validated_temp) - return SetTargetTemperatureResult(targetTemperature=validated_temp) + return SuccessData( + public=SetTargetTemperatureResult(targetTemperature=validated_temp), + private=None, + ) class SetTargetTemperature( - BaseCommand[SetTargetTemperatureParams, SetTargetTemperatureResult] + BaseCommand[SetTargetTemperatureParams, SetTargetTemperatureResult, ErrorOccurrence] ): """A command to set a Temperature Module's target temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py index edb4ea1e0c1..9abd6d13179 100644 --- a/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -34,7 +35,9 @@ class WaitForTemperatureResult(BaseModel): class WaitForTemperatureImpl( - AbstractCommandImpl[WaitForTemperatureParams, WaitForTemperatureResult] + AbstractCommandImpl[ + WaitForTemperatureParams, SuccessData[WaitForTemperatureResult, None] + ] ): """Execution implementation of Temperature Module's wait for temperature command.""" @@ -49,7 +52,7 @@ def __init__( async def execute( self, params: WaitForTemperatureParams - ) -> WaitForTemperatureResult: + ) -> SuccessData[WaitForTemperatureResult, None]: """Wait for a Temperature Module's target temperature.""" # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError. module_substate = self._state_view.modules.get_temperature_module_substate( @@ -71,11 +74,11 @@ async def execute( await temp_hardware_module.await_temperature( awaiting_temperature=target_temp ) - return WaitForTemperatureResult() + return SuccessData(public=WaitForTemperatureResult(), private=None) class WaitForTemperature( - BaseCommand[WaitForTemperatureParams, WaitForTemperatureResult] + BaseCommand[WaitForTemperatureParams, WaitForTemperatureResult, ErrorOccurrence] ): """A command to wait for a Temperature Module's target temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py index b67f1cb4aea..de7768c4c7a 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/close_lid.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.types import MotorAxis if TYPE_CHECKING: @@ -26,7 +27,9 @@ class CloseLidResult(BaseModel): """Result data from closing a Thermocycler's lid.""" -class CloseLidImpl(AbstractCommandImpl[CloseLidParams, CloseLidResult]): +class CloseLidImpl( + AbstractCommandImpl[CloseLidParams, SuccessData[CloseLidResult, None]] +): """Execution implementation of a Thermocycler's close lid command.""" def __init__( @@ -40,7 +43,9 @@ def __init__( self._equipment = equipment self._movement = movement - async def execute(self, params: CloseLidParams) -> CloseLidResult: + async def execute( + self, params: CloseLidParams + ) -> SuccessData[CloseLidResult, None]: """Close a Thermocycler's lid.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -60,10 +65,10 @@ async def execute(self, params: CloseLidParams) -> CloseLidResult: if thermocycler_hardware is not None: await thermocycler_hardware.close() - return CloseLidResult() + return SuccessData(public=CloseLidResult(), private=None) -class CloseLid(BaseCommand[CloseLidParams, CloseLidResult]): +class CloseLid(BaseCommand[CloseLidParams, CloseLidResult, ErrorOccurrence]): """A command to close a Thermocycler's lid.""" commandType: CloseLidCommandType = "thermocycler/closeLid" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py index d4851d91d99..a24706a54c3 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_block.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -26,7 +27,7 @@ class DeactivateBlockResult(BaseModel): class DeactivateBlockImpl( - AbstractCommandImpl[DeactivateBlockParams, DeactivateBlockResult] + AbstractCommandImpl[DeactivateBlockParams, SuccessData[DeactivateBlockResult, None]] ): """Execution implementation of a Thermocycler's deactivate block command.""" @@ -39,7 +40,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: DeactivateBlockParams) -> DeactivateBlockResult: + async def execute( + self, params: DeactivateBlockParams + ) -> SuccessData[DeactivateBlockResult, None]: """Unset a Thermocycler's target block temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -51,10 +54,12 @@ async def execute(self, params: DeactivateBlockParams) -> DeactivateBlockResult: if thermocycler_hardware is not None: await thermocycler_hardware.deactivate_block() - return DeactivateBlockResult() + return SuccessData(public=DeactivateBlockResult(), private=None) -class DeactivateBlock(BaseCommand[DeactivateBlockParams, DeactivateBlockResult]): +class DeactivateBlock( + BaseCommand[DeactivateBlockParams, DeactivateBlockResult, ErrorOccurrence] +): """A command to unset a Thermocycler's target block temperature.""" commandType: DeactivateBlockCommandType = "thermocycler/deactivateBlock" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py index 8116e0fa9f6..4f76d2c3d3e 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -25,7 +26,9 @@ class DeactivateLidResult(BaseModel): """Result data from unsetting a Thermocycler's target lid temperature.""" -class DeactivateLidImpl(AbstractCommandImpl[DeactivateLidParams, DeactivateLidResult]): +class DeactivateLidImpl( + AbstractCommandImpl[DeactivateLidParams, SuccessData[DeactivateLidResult, None]] +): """Execution implementation of a Thermocycler's deactivate lid command.""" def __init__( @@ -37,7 +40,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: DeactivateLidParams) -> DeactivateLidResult: + async def execute( + self, params: DeactivateLidParams + ) -> SuccessData[DeactivateLidResult, None]: """Unset a Thermocycler's target lid temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -49,10 +54,12 @@ async def execute(self, params: DeactivateLidParams) -> DeactivateLidResult: if thermocycler_hardware is not None: await thermocycler_hardware.deactivate_lid() - return DeactivateLidResult() + return SuccessData(public=DeactivateLidResult(), private=None) -class DeactivateLid(BaseCommand[DeactivateLidParams, DeactivateLidResult]): +class DeactivateLid( + BaseCommand[DeactivateLidParams, DeactivateLidResult, ErrorOccurrence] +): """A command to unset a Thermocycler's target lid temperature.""" commandType: DeactivateLidCommandType = "thermocycler/deactivateLid" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py index b556a17cb13..0facf0d4ec3 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/open_lid.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.types import MotorAxis if TYPE_CHECKING: @@ -26,7 +27,7 @@ class OpenLidResult(BaseModel): """Result data from opening a Thermocycler's lid.""" -class OpenLidImpl(AbstractCommandImpl[OpenLidParams, OpenLidResult]): +class OpenLidImpl(AbstractCommandImpl[OpenLidParams, SuccessData[OpenLidResult, None]]): """Execution implementation of a Thermocycler's open lid command.""" def __init__( @@ -40,7 +41,7 @@ def __init__( self._equipment = equipment self._movement = movement - async def execute(self, params: OpenLidParams) -> OpenLidResult: + async def execute(self, params: OpenLidParams) -> SuccessData[OpenLidResult, None]: """Open a Thermocycler's lid.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -60,10 +61,10 @@ async def execute(self, params: OpenLidParams) -> OpenLidResult: if thermocycler_hardware is not None: await thermocycler_hardware.open() - return OpenLidResult() + return SuccessData(public=OpenLidResult(), private=None) -class OpenLid(BaseCommand[OpenLidParams, OpenLidResult]): +class OpenLid(BaseCommand[OpenLidParams, OpenLidResult, ErrorOccurrence]): """A command to open a Thermocycler's lid.""" commandType: OpenLidCommandType = "thermocycler/openLid" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py index 76ad974eb6e..af387e3324e 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py @@ -7,7 +7,8 @@ from opentrons.hardware_control.modules.types import ThermocyclerStep -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -45,7 +46,9 @@ class RunProfileResult(BaseModel): """Result data from running a Thermocycler profile.""" -class RunProfileImpl(AbstractCommandImpl[RunProfileParams, RunProfileResult]): +class RunProfileImpl( + AbstractCommandImpl[RunProfileParams, SuccessData[RunProfileResult, None]] +): """Execution implementation of a Thermocycler's run profile command.""" def __init__( @@ -57,7 +60,9 @@ def __init__( self._state_view = state_view self._equipment = equipment - async def execute(self, params: RunProfileParams) -> RunProfileResult: + async def execute( + self, params: RunProfileParams + ) -> SuccessData[RunProfileResult, None]: """Run a Thermocycler profile.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -91,10 +96,10 @@ async def execute(self, params: RunProfileParams) -> RunProfileResult: steps=steps, repetitions=1, volume=target_volume ) - return RunProfileResult() + return SuccessData(public=RunProfileResult(), private=None) -class RunProfile(BaseCommand[RunProfileParams, RunProfileResult]): +class RunProfile(BaseCommand[RunProfileParams, RunProfileResult, ErrorOccurrence]): """A command to execute a Thermocycler profile run.""" commandType: RunProfileCommandType = "thermocycler/runProfile" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py index 61f13bd2dc2..796fb15c024 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -45,7 +46,7 @@ class SetTargetBlockTemperatureResult(BaseModel): class SetTargetBlockTemperatureImpl( AbstractCommandImpl[ SetTargetBlockTemperatureParams, - SetTargetBlockTemperatureResult, + SuccessData[SetTargetBlockTemperatureResult, None], ] ): """Execution implementation of a Thermocycler's set block temperature command.""" @@ -62,7 +63,7 @@ def __init__( async def execute( self, params: SetTargetBlockTemperatureParams, - ) -> SetTargetBlockTemperatureResult: + ) -> SuccessData[SetTargetBlockTemperatureResult, None]: """Set a Thermocycler's target block temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -92,13 +93,20 @@ async def execute( target_temperature, volume=target_volume, hold_time_seconds=hold_time ) - return SetTargetBlockTemperatureResult( - targetBlockTemperature=target_temperature + return SuccessData( + public=SetTargetBlockTemperatureResult( + targetBlockTemperature=target_temperature + ), + private=None, ) class SetTargetBlockTemperature( - BaseCommand[SetTargetBlockTemperatureParams, SetTargetBlockTemperatureResult] + BaseCommand[ + SetTargetBlockTemperatureParams, + SetTargetBlockTemperatureResult, + ErrorOccurrence, + ] ): """A command to set a Thermocycler's target block temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py index aaa5699b1d4..a819d6a3759 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -32,7 +33,9 @@ class SetTargetLidTemperatureResult(BaseModel): class SetTargetLidTemperatureImpl( - AbstractCommandImpl[SetTargetLidTemperatureParams, SetTargetLidTemperatureResult] + AbstractCommandImpl[ + SetTargetLidTemperatureParams, SuccessData[SetTargetLidTemperatureResult, None] + ] ): """Execution implementation of a Thermocycler's set lid temperature command.""" @@ -48,7 +51,7 @@ def __init__( async def execute( self, params: SetTargetLidTemperatureParams, - ) -> SetTargetLidTemperatureResult: + ) -> SuccessData[SetTargetLidTemperatureResult, None]: """Set a Thermocycler's target lid temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -63,11 +66,18 @@ async def execute( if thermocycler_hardware is not None: await thermocycler_hardware.set_target_lid_temperature(target_temperature) - return SetTargetLidTemperatureResult(targetLidTemperature=target_temperature) + return SuccessData( + public=SetTargetLidTemperatureResult( + targetLidTemperature=target_temperature + ), + private=None, + ) class SetTargetLidTemperature( - BaseCommand[SetTargetLidTemperatureParams, SetTargetLidTemperatureResult] + BaseCommand[ + SetTargetLidTemperatureParams, SetTargetLidTemperatureResult, ErrorOccurrence + ] ): """A command to set a Thermocycler's target lid temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py index 41d2ef0e60e..40a8241adaa 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -27,8 +28,7 @@ class WaitForBlockTemperatureResult(BaseModel): class WaitForBlockTemperatureImpl( AbstractCommandImpl[ - WaitForBlockTemperatureParams, - WaitForBlockTemperatureResult, + WaitForBlockTemperatureParams, SuccessData[WaitForBlockTemperatureResult, None] ] ): """Execution implementation of Thermocycler's wait for block temperature command.""" @@ -45,7 +45,7 @@ def __init__( async def execute( self, params: WaitForBlockTemperatureParams, - ) -> WaitForBlockTemperatureResult: + ) -> SuccessData[WaitForBlockTemperatureResult, None]: """Wait for a Thermocycler's target block temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -61,11 +61,13 @@ async def execute( if thermocycler_hardware is not None: await thermocycler_hardware.wait_for_block_target() - return WaitForBlockTemperatureResult() + return SuccessData(public=WaitForBlockTemperatureResult(), private=None) class WaitForBlockTemperature( - BaseCommand[WaitForBlockTemperatureParams, WaitForBlockTemperatureResult] + BaseCommand[ + WaitForBlockTemperatureParams, WaitForBlockTemperatureResult, ErrorOccurrence + ] ): """A command to wait for a Thermocycler's target block temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py index 75a652b79e7..026aed14ad6 100644 --- a/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +++ b/api/src/opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py @@ -5,7 +5,8 @@ from pydantic import BaseModel, Field -from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from opentrons.protocol_engine.state import StateView @@ -27,8 +28,7 @@ class WaitForLidTemperatureResult(BaseModel): class WaitForLidTemperatureImpl( AbstractCommandImpl[ - WaitForLidTemperatureParams, - WaitForLidTemperatureResult, + WaitForLidTemperatureParams, SuccessData[WaitForLidTemperatureResult, None] ] ): """Execution implementation of Thermocycler's wait for lid temperature command.""" @@ -45,7 +45,7 @@ def __init__( async def execute( self, params: WaitForLidTemperatureParams, - ) -> WaitForLidTemperatureResult: + ) -> SuccessData[WaitForLidTemperatureResult, None]: """Wait for a Thermocycler's lid temperature.""" thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( params.moduleId @@ -61,11 +61,13 @@ async def execute( if thermocycler_hardware is not None: await thermocycler_hardware.wait_for_lid_target() - return WaitForLidTemperatureResult() + return SuccessData(public=WaitForLidTemperatureResult(), private=None) class WaitForLidTemperature( - BaseCommand[WaitForLidTemperatureParams, WaitForLidTemperatureResult] + BaseCommand[ + WaitForLidTemperatureParams, WaitForLidTemperatureResult, ErrorOccurrence + ] ): """A command to wait for a Thermocycler's lid temperature.""" diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index 7da13136e14..858be81842c 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -6,7 +6,8 @@ from ..errors import TouchTipDisabledError, LabwareIsTipRackError from ..types import DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from .pipetting_common import ( PipetteIdMixin, WellLocationMixin, @@ -46,7 +47,9 @@ class TouchTipResult(DestinationPositionResult): pass -class TouchTipImplementation(AbstractCommandImpl[TouchTipParams, TouchTipResult]): +class TouchTipImplementation( + AbstractCommandImpl[TouchTipParams, SuccessData[TouchTipResult, None]] +): """Touch tip command implementation.""" def __init__( @@ -60,7 +63,9 @@ def __init__( self._movement = movement self._gantry_mover = gantry_mover - async def execute(self, params: TouchTipParams) -> TouchTipResult: + async def execute( + self, params: TouchTipParams + ) -> SuccessData[TouchTipResult, None]: """Touch tip to sides of a well using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -99,10 +104,12 @@ async def execute(self, params: TouchTipParams) -> TouchTipResult: speed=touch_speed, ) - return TouchTipResult(position=DeckPoint(x=x, y=y, z=z)) + return SuccessData( + public=TouchTipResult(position=DeckPoint(x=x, y=y, z=z)), private=None + ) -class TouchTip(BaseCommand[TouchTipParams, TouchTipResult]): +class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, ErrorOccurrence]): """Touch up tip command model.""" commandType: TouchTipCommandType = "touchTip" diff --git a/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py b/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py index 67aa5d1dc34..9816e03cf33 100644 --- a/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py +++ b/api/src/opentrons/protocol_engine/commands/verify_tip_presence.py @@ -6,7 +6,8 @@ from typing_extensions import Literal from .pipetting_common import PipetteIdMixin -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence from ..types import TipPresenceStatus, InstrumentSensorId @@ -35,7 +36,9 @@ class VerifyTipPresenceResult(BaseModel): class VerifyTipPresenceImplementation( - AbstractCommandImpl[VerifyTipPresenceParams, VerifyTipPresenceResult] + AbstractCommandImpl[ + VerifyTipPresenceParams, SuccessData[VerifyTipPresenceResult, None] + ] ): """VerifyTipPresence command implementation.""" @@ -46,7 +49,9 @@ def __init__( ) -> None: self._tip_handler = tip_handler - async def execute(self, params: VerifyTipPresenceParams) -> VerifyTipPresenceResult: + async def execute( + self, params: VerifyTipPresenceParams + ) -> SuccessData[VerifyTipPresenceResult, None]: """Verify if tip presence is as expected for the requested pipette.""" pipette_id = params.pipetteId expected_state = params.expectedState @@ -62,10 +67,12 @@ async def execute(self, params: VerifyTipPresenceParams) -> VerifyTipPresenceRes follow_singular_sensor=follow_singular_sensor, ) - return VerifyTipPresenceResult() + return SuccessData(public=VerifyTipPresenceResult(), private=None) -class VerifyTipPresence(BaseCommand[VerifyTipPresenceParams, VerifyTipPresenceResult]): +class VerifyTipPresence( + BaseCommand[VerifyTipPresenceParams, VerifyTipPresenceResult, ErrorOccurrence] +): """VerifyTipPresence command model.""" commandType: VerifyTipPresenceCommandType = "verifyTipPresence" diff --git a/api/src/opentrons/protocol_engine/commands/wait_for_duration.py b/api/src/opentrons/protocol_engine/commands/wait_for_duration.py index 7c8018c237c..df1eae28aa4 100644 --- a/api/src/opentrons/protocol_engine/commands/wait_for_duration.py +++ b/api/src/opentrons/protocol_engine/commands/wait_for_duration.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import RunControlHandler @@ -28,20 +29,24 @@ class WaitForDurationResult(BaseModel): class WaitForDurationImplementation( - AbstractCommandImpl[WaitForDurationParams, WaitForDurationResult] + AbstractCommandImpl[WaitForDurationParams, SuccessData[WaitForDurationResult, None]] ): """Wait for duration command implementation.""" def __init__(self, run_control: RunControlHandler, **kwargs: object) -> None: self._run_control = run_control - async def execute(self, params: WaitForDurationParams) -> WaitForDurationResult: + async def execute( + self, params: WaitForDurationParams + ) -> SuccessData[WaitForDurationResult, None]: """Wait for a duration of time.""" await self._run_control.wait_for_duration(params.seconds) - return WaitForDurationResult() + return SuccessData(public=WaitForDurationResult(), private=None) -class WaitForDuration(BaseCommand[WaitForDurationParams, WaitForDurationResult]): +class WaitForDuration( + BaseCommand[WaitForDurationParams, WaitForDurationResult, ErrorOccurrence] +): """Wait for duration command model.""" commandType: WaitForDurationCommandType = "waitForDuration" diff --git a/api/src/opentrons/protocol_engine/commands/wait_for_resume.py b/api/src/opentrons/protocol_engine/commands/wait_for_resume.py index 6917621a7ac..c6036f852e2 100644 --- a/api/src/opentrons/protocol_engine/commands/wait_for_resume.py +++ b/api/src/opentrons/protocol_engine/commands/wait_for_resume.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import RunControlHandler @@ -29,20 +30,24 @@ class WaitForResumeResult(BaseModel): class WaitForResumeImplementation( - AbstractCommandImpl[WaitForResumeParams, WaitForResumeResult] + AbstractCommandImpl[WaitForResumeParams, SuccessData[WaitForResumeResult, None]] ): """Wait for resume command implementation.""" def __init__(self, run_control: RunControlHandler, **kwargs: object) -> None: self._run_control = run_control - async def execute(self, params: WaitForResumeParams) -> WaitForResumeResult: + async def execute( + self, params: WaitForResumeParams + ) -> SuccessData[WaitForResumeResult, None]: """Dispatch a PauseAction to the store to pause the protocol.""" await self._run_control.wait_for_resume() - return WaitForResumeResult() + return SuccessData(public=WaitForResumeResult(), private=None) -class WaitForResume(BaseCommand[WaitForResumeParams, WaitForResumeResult]): +class WaitForResume( + BaseCommand[WaitForResumeParams, WaitForResumeResult, ErrorOccurrence] +): """Wait for resume command model.""" commandType: WaitForResumeCommandType = "waitForResume" diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index d00b5c0a96d..ca397baec02 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -2,6 +2,7 @@ import asyncio from logging import getLogger from typing import Optional, List, Protocol +from typing_extensions import assert_never from opentrons.hardware_control import HardwareControlAPI @@ -11,16 +12,12 @@ PythonException, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy from ..state import StateStore from ..resources import ModelUtils -from ..commands import ( - CommandStatus, - AbstractCommandImpl, - CommandResult, - CommandPrivateResult, -) +from ..commands import CommandStatus from ..actions import ( ActionDispatcher, RunCommandAction, @@ -142,17 +139,17 @@ async def execute(self, command_id: str) -> None: ) running_command = self._state_store.commands.get(queued_command.id) + log.debug( + f"Executing {running_command.id}, {running_command.commandType}, {running_command.params}" + ) try: - log.debug( - f"Executing {running_command.id}, {running_command.commandType}, {running_command.params}" + result = await command_impl.execute( + running_command.params # type: ignore[arg-type] ) - if isinstance(command_impl, AbstractCommandImpl): - result: CommandResult = await command_impl.execute(running_command.params) # type: ignore[arg-type] - private_result: Optional[CommandPrivateResult] = None - else: - result, private_result = await command_impl.execute(running_command.params) # type: ignore[arg-type] except (Exception, asyncio.CancelledError) as error: + # The command encountered an undefined error. + log.warning(f"Execution of {running_command.id} failed", exc_info=error) # TODO(mc, 2022-11-14): mark command as stopped rather than failed # https://opentrons.atlassian.net/browse/RCORE-390 @@ -184,16 +181,23 @@ async def execute(self, command_id: str) -> None: ), ) ) + else: - update = { - "result": result, - "status": CommandStatus.SUCCEEDED, - "completedAt": self._model_utils.get_timestamp(), - "notes": note_tracker.get_notes(), - } - succeeded_command = running_command.copy(update=update) - self._action_dispatcher.dispatch( - SucceedCommandAction( - command=succeeded_command, private_result=private_result - ), - ) + if isinstance(result, SuccessData): + update = { + "result": result.public, + "status": CommandStatus.SUCCEEDED, + "completedAt": self._model_utils.get_timestamp(), + "notes": note_tracker.get_notes(), + } + succeeded_command = running_command.copy(update=update) + self._action_dispatcher.dispatch( + SucceedCommandAction( + command=succeeded_command, private_result=result.private + ), + ) + else: + # The command encountered a defined error. + # TODO(mm, 2024-05-10): Once commands start returning DefinedErrorData, + # handle it here by dispatching a FailCommandAction. + assert_never(result) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py index ed639444b3d..fd2d0469954 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -95,6 +95,7 @@ EStopNotPresentError, FirmwareUpdateRequiredError, FailedGripperPickupError, + LiquidNotFoundError, ) from opentrons_hardware.hardware_control.move_group_runner import MoveGroupRunner @@ -715,14 +716,19 @@ async def test_liquid_probe( mock_move_group_run: mock.AsyncMock, mock_send_stop_threshold: mock.AsyncMock, ) -> None: - await controller.liquid_probe( - mount=mount, - max_z_distance=fake_liquid_settings.max_z_distance, - mount_speed=fake_liquid_settings.mount_speed, - plunger_speed=fake_liquid_settings.plunger_speed, - threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, - output_option=fake_liquid_settings.output_option, - ) + try: + await controller.liquid_probe( + mount=mount, + max_z_distance=fake_liquid_settings.max_z_distance, + mount_speed=fake_liquid_settings.mount_speed, + plunger_speed=fake_liquid_settings.plunger_speed, + threshold_pascals=fake_liquid_settings.sensor_threshold_pascals, + output_option=fake_liquid_settings.output_option, + ) + except LiquidNotFoundError: + # the move raises a liquid not found now since we don't call the move group and it doesn't + # get any positions back + pass move_groups = (mock_move_group_run.call_args_list[0][0][0]._move_groups)[0][0] head_node = axis_to_node(Axis.by_mount(mount)) tool_node = sensor_node_for_mount(mount) diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py index 895bd7e3665..6ecf768c4eb 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_gripper.py @@ -26,6 +26,7 @@ CalibrateGripperParams, CalibrateGripperParamsJaw, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.errors import HardwareNotSupportedError from opentrons.protocol_engine.types import Vec3f @@ -69,7 +70,10 @@ async def test_calibrate_gripper( ).then_return(Point(1.1, 2.2, 3.3)) result = await subject.execute(params) - assert result == CalibrateGripperResult(jawOffset=Vec3f(x=1.1, y=2.2, z=3.3)) + assert result == SuccessData( + public=CalibrateGripperResult(jawOffset=Vec3f(x=1.1, y=2.2, z=3.3)), + private=None, + ) @pytest.mark.ot3_only @@ -101,8 +105,8 @@ async def test_calibrate_gripper_saves_calibration( ) ).then_return(expected_calibration_data) result = await subject.execute(params) - assert result.jawOffset == Vec3f(x=1.1, y=2.2, z=3.3) - assert result.savedCalibration == expected_calibration_data + assert result.public.jawOffset == Vec3f(x=1.1, y=2.2, z=3.3) + assert result.public.savedCalibration == expected_calibration_data @pytest.mark.ot3_only diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py index a7821bd80e0..0226453c72e 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_module.py @@ -12,6 +12,7 @@ CalibrateModuleImplementation, CalibrateModuleParams, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.errors.exceptions import HardwareNotSupportedError from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import ( @@ -85,13 +86,16 @@ async def test_calibrate_module_implementation( result = await subject.execute(params) - assert result == CalibrateModuleResult( - moduleOffset=ModuleOffsetVector( - x=3, - y=4, - z=6, + assert result == SuccessData( + public=CalibrateModuleResult( + moduleOffset=ModuleOffsetVector( + x=3, + y=4, + z=6, + ), + location=location, ), - location=location, + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py index 65545c24f51..ba949f0e2df 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_calibrate_pipette.py @@ -11,6 +11,7 @@ CalibratePipetteImplementation, CalibratePipetteParams, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.errors.exceptions import HardwareNotSupportedError from opentrons.protocol_engine.types import InstrumentOffsetVector @@ -59,8 +60,11 @@ async def test_calibrate_pipette_implementation( times=1, ) - assert result == CalibratePipetteResult( - pipetteOffset=InstrumentOffsetVector(x=3, y=4, z=6) + assert result == SuccessData( + public=CalibratePipetteResult( + pipetteOffset=InstrumentOffsetVector(x=3, y=4, z=6) + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py b/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py index df58ab7dbc0..dd057d1cf8a 100644 --- a/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py +++ b/api/tests/opentrons/protocol_engine/commands/calibration/test_move_to_maintenance_position.py @@ -5,12 +5,14 @@ import pytest from decoy import Decoy + from opentrons.protocol_engine.commands.calibration.move_to_maintenance_position import ( MoveToMaintenancePositionParams, MoveToMaintenancePositionImplementation, MoveToMaintenancePositionResult, MaintenancePosition, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.state import StateView from opentrons.types import MountType, Mount, Point @@ -54,7 +56,7 @@ async def test_calibration_move_to_location_implementatio_for_attach_instrument( decoy.when(ot3_hardware_api.get_instrument_max_height(Mount.LEFT)).then_return(300) result = await subject.execute(params=params) - assert result == MoveToMaintenancePositionResult() + assert result == SuccessData(public=MoveToMaintenancePositionResult(), private=None) hw_mount = mount_type.to_hw_mount() decoy.verify( @@ -98,7 +100,7 @@ async def test_calibration_move_to_location_implementatio_for_attach_plate( decoy.when(ot3_hardware_api.get_instrument_max_height(Mount.LEFT)).then_return(300) result = await subject.execute(params=params) - assert result == MoveToMaintenancePositionResult() + assert result == SuccessData(public=MoveToMaintenancePositionResult(), private=None) decoy.verify( await ot3_hardware_api.prepare_for_mount_movement(Mount.LEFT), @@ -141,7 +143,7 @@ async def test_calibration_move_to_location_implementation_for_gripper( decoy.when(ot3_hardware_api.get_instrument_max_height(Mount.LEFT)).then_return(300) result = await subject.execute(params=params) - assert result == MoveToMaintenancePositionResult() + assert result == SuccessData(public=MoveToMaintenancePositionResult(), private=None) decoy.verify( await ot3_hardware_api.prepare_for_mount_movement(Mount.LEFT), diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py index 3d0f4988e9f..d728b97cb4d 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_close_labware_latch.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.close_labware_latch import ( CloseLabwareLatchImpl, ) @@ -43,7 +44,9 @@ async def test_close_labware_latch( result = await subject.execute(data) decoy.verify(await heater_shaker_hardware.close_labware_latch(), times=1) - assert result == heater_shaker.CloseLabwareLatchResult() + assert result == SuccessData( + public=heater_shaker.CloseLabwareLatchResult(), private=None + ) async def test_close_labware_latch_virtual( @@ -73,4 +76,6 @@ async def test_close_labware_latch_virtual( result = await subject.execute(data) - assert result == heater_shaker.CloseLabwareLatchResult() + assert result == SuccessData( + public=heater_shaker.CloseLabwareLatchResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py index 0dbd7a6862d..0da296f71d6 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_heater.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.deactivate_heater import ( DeactivateHeaterImpl, ) @@ -44,4 +45,6 @@ async def test_deactivate_heater( result = await subject.execute(data) decoy.verify(await hs_hardware.deactivate_heater(), times=1) - assert result == heater_shaker.DeactivateHeaterResult() + assert result == SuccessData( + public=heater_shaker.DeactivateHeaterResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py index 24457182f45..3ab339f97e7 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_deactivate_shaker.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.deactivate_shaker import ( DeactivateShakerImpl, ) @@ -44,7 +45,9 @@ async def test_deactivate_shaker( result = await subject.execute(data) decoy.verify(await hs_hardware.deactivate_shaker(), times=1) - assert result == heater_shaker.DeactivateShakerResult() + assert result == SuccessData( + public=heater_shaker.DeactivateShakerResult(), private=None + ) async def test_deactivate_shaker_virtual( @@ -74,4 +77,6 @@ async def test_deactivate_shaker_virtual( ).then_return(None) result = await subject.execute(data) - assert result == heater_shaker.DeactivateShakerResult() + assert result == SuccessData( + public=heater_shaker.DeactivateShakerResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py index 6232fe27981..6894c1d7e80 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_open_labware_latch.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.open_labware_latch import ( OpenLabwareLatchImpl, ) @@ -62,4 +63,6 @@ async def test_open_labware_latch( ), await hs_hardware.open_labware_latch(), ) - assert result == heater_shaker.OpenLabwareLatchResult(pipetteRetracted=True) + assert result == SuccessData( + public=heater_shaker.OpenLabwareLatchResult(pipetteRetracted=True), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py index 389fd3ff53f..85e92ffd5b0 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_and_wait_for_shake_speed.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.set_and_wait_for_shake_speed import ( SetAndWaitForShakeSpeedImpl, ) @@ -68,4 +69,7 @@ async def test_set_and_wait_for_shake_speed( ), await hs_hardware.set_speed(rpm=1234), ) - assert result == heater_shaker.SetAndWaitForShakeSpeedResult(pipetteRetracted=True) + assert result == SuccessData( + public=heater_shaker.SetAndWaitForShakeSpeedResult(pipetteRetracted=True), + private=None, + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py index 0004a6da7ab..b220c15ebef 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_set_target_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.set_target_temperature import ( SetTargetTemperatureImpl, ) @@ -53,4 +54,6 @@ async def test_set_target_temperature( result = await subject.execute(data) decoy.verify(await hs_hardware.start_set_temperature(celsius=45.6), times=1) - assert result == heater_shaker.SetTargetTemperatureResult() + assert result == SuccessData( + public=heater_shaker.SetTargetTemperatureResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py index 6f7f517af19..a575e8d4795 100644 --- a/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/heater_shaker/test_wait_for_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import heater_shaker +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.heater_shaker.wait_for_temperature import ( WaitForTemperatureImpl, ) @@ -48,4 +49,6 @@ async def test_wait_for_temperature( decoy.verify( await hs_hardware.await_temperature(awaiting_temperature=123.45), times=1 ) - assert result == heater_shaker.WaitForTemperatureResult() + assert result == SuccessData( + public=heater_shaker.WaitForTemperatureResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py index b294f21cecb..b87cd5d3f3b 100644 --- a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py +++ b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_disengage.py @@ -9,6 +9,7 @@ MagneticModuleSubState, MagneticModuleId, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.magnetic_module import ( DisengageParams, DisengageResult, @@ -45,4 +46,4 @@ async def test_magnetic_module_disengage_implementation( result = await subject.execute(params=params) decoy.verify(await magnetic_module_hw.deactivate(), times=1) - assert result == DisengageResult() + assert result == SuccessData(public=DisengageResult(), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py index 2bfea51d887..6563371345e 100644 --- a/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py +++ b/api/tests/opentrons/protocol_engine/commands/magnetic_module/test_engage.py @@ -9,6 +9,7 @@ MagneticModuleSubState, ) from opentrons.protocol_engine.execution import EquipmentHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.magnetic_module import ( EngageParams, EngageResult, @@ -50,4 +51,4 @@ async def test_magnetic_module_engage_implementation( result = await subject.execute(params=params) decoy.verify(await magnetic_module_hw.engage(9001), times=1) - assert result == EngageResult() + assert result == SuccessData(public=EngageResult(), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py index 0aa28cf525c..7e73ec94dc6 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_deactivate.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import temperature_module +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.temperature_module.deactivate import ( DeactivateTemperatureImpl, ) @@ -43,5 +44,6 @@ async def test_await_temperature( result = await subject.execute(data) decoy.verify(await tempdeck_hardware.deactivate(), times=1) - assert result == temperature_module.DeactivateTemperatureResult() - assert isinstance(result, temperature_module.DeactivateTemperatureResult) + assert result == SuccessData( + public=temperature_module.DeactivateTemperatureResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py index e3f137e2166..cd57f86a4c6 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_set_target_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import temperature_module +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.temperature_module.set_target_temperature import ( SetTargetTemperatureImpl, ) @@ -49,4 +50,7 @@ async def test_set_target_temperature( result = await subject.execute(data) decoy.verify(await tempdeck_hardware.start_set_temperature(celsius=1), times=1) - assert result == temperature_module.SetTargetTemperatureResult(targetTemperature=1) + assert result == SuccessData( + public=temperature_module.SetTargetTemperatureResult(targetTemperature=1), + private=None, + ) diff --git a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py index 7a1a423d906..df18e8a144c 100644 --- a/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/temperature_module/test_wait_for_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import temperature_module +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.temperature_module.wait_for_temperature import ( WaitForTemperatureImpl, ) @@ -46,7 +47,9 @@ async def test_wait_for_temperature( decoy.verify( await tempdeck_hardware.await_temperature(awaiting_temperature=123), times=1 ) - assert result == temperature_module.WaitForTemperatureResult() + assert result == SuccessData( + public=temperature_module.WaitForTemperatureResult(), private=None + ) async def test_wait_for_temperature_requested_celsius( @@ -86,4 +89,6 @@ async def test_wait_for_temperature_requested_celsius( decoy.verify( await tempdeck_hardware.await_temperature(awaiting_temperature=12), times=1 ) - assert result == temperature_module.WaitForTemperatureResult() + assert result == SuccessData( + public=temperature_module.WaitForTemperatureResult(), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index f625c19f93f..4cb820eb0ea 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -10,6 +10,7 @@ AspirateResult, AspirateImplementation, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.state import StateView @@ -84,7 +85,10 @@ async def test_aspirate_implementation_no_prep( result = await subject.execute(data) - assert result == AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)) + assert result == SuccessData( + public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), + private=None, + ) async def test_aspirate_implementation_with_prep( @@ -140,7 +144,10 @@ async def test_aspirate_implementation_with_prep( result = await subject.execute(data) - assert result == AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)) + assert result == SuccessData( + public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), + private=None, + ) decoy.verify( await movement.move_to_well( diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 3d09c029bcd..c6197f2d26f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -10,6 +10,7 @@ AspirateInPlaceResult, AspirateInPlaceImplementation, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.errors.exceptions import PipetteNotReadyToAspirateError from opentrons.protocol_engine.notes import CommandNoteAdder @@ -84,7 +85,7 @@ async def test_aspirate_in_place_implementation( result = await subject.execute(params=data) - assert result == AspirateInPlaceResult(volume=123) + assert result == SuccessData(public=AspirateInPlaceResult(volume=123), private=None) async def test_handle_aspirate_in_place_request_not_ready_to_aspirate( diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 2088e83fc97..919d37e9a76 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -9,6 +9,7 @@ BlowOutImplementation, BlowOutParams, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.execution import ( MovementHandler, PipettingHandler, @@ -52,7 +53,9 @@ async def test_blow_out_implementation( result = await subject.execute(data) - assert result == BlowOutResult(position=DeckPoint(x=1, y=2, z=3)) + assert result == SuccessData( + public=BlowOutResult(position=DeckPoint(x=1, y=2, z=3)), private=None + ) decoy.verify( await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234), diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py index 00deb7c640c..a14bcdc8019 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py @@ -7,7 +7,7 @@ BlowOutInPlaceResult, BlowOutInPlaceImplementation, ) - +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.execution import ( MovementHandler, PipettingHandler, @@ -36,7 +36,7 @@ async def test_blow_out_in_place_implementation( result = await subject.execute(data) - assert result == BlowOutInPlaceResult() + assert result == SuccessData(public=BlowOutInPlaceResult(), private=None) decoy.verify( await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234) diff --git a/api/tests/opentrons/protocol_engine/commands/test_comment.py b/api/tests/opentrons/protocol_engine/commands/test_comment.py index 3aa088e0a37..4010f2ec56c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_comment.py +++ b/api/tests/opentrons/protocol_engine/commands/test_comment.py @@ -4,6 +4,7 @@ CommentResult, CommentImplementation, ) +from opentrons.protocol_engine.commands.command import SuccessData async def test_comment_implementation() -> None: @@ -14,4 +15,4 @@ async def test_comment_implementation() -> None: result = await subject.execute(data) - assert result == CommentResult() + assert result == SuccessData(public=CommentResult(), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index 333662d4bcf..0bd683fc1fe 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -10,6 +10,7 @@ LoadedStaticPipetteData, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.configure_for_volume import ( ConfigureForVolumeParams, ConfigureForVolumeResult, @@ -65,9 +66,11 @@ async def test_configure_for_volume_implementation( ) ) - result, private_result = await subject.execute(data) + result = await subject.execute(data) - assert result == ConfigureForVolumeResult() - assert private_result == ConfigureForVolumePrivateResult( - pipette_id="pipette-id", serial_number="some number", config=config + assert result == SuccessData( + public=ConfigureForVolumeResult(), + private=ConfigureForVolumePrivateResult( + pipette_id="pipette-id", serial_number="some number", config=config + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py index 23cdddd98be..67b4294c1be 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py @@ -11,7 +11,7 @@ from opentrons.types import Point from opentrons.hardware_control.nozzle_manager import NozzleMap - +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.configure_nozzle_layout import ( ConfigureNozzleLayoutParams, ConfigureNozzleLayoutResult, @@ -124,10 +124,12 @@ async def test_configure_nozzle_layout_implementation( ) ).then_return(expected_nozzlemap) - result, private_result = await subject.execute(requested_nozzle_layout) + result = await subject.execute(requested_nozzle_layout) - assert result == ConfigureNozzleLayoutResult() - assert private_result == ConfigureNozzleLayoutPrivateResult( - pipette_id="pipette-id", - nozzle_map=expected_nozzlemap, + assert result == SuccessData( + public=ConfigureNozzleLayoutResult(), + private=ConfigureNozzleLayoutPrivateResult( + pipette_id="pipette-id", + nozzle_map=expected_nozzlemap, + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index cb6737f535f..4df18a19152 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -5,6 +5,7 @@ from opentrons.protocol_engine.execution import MovementHandler, PipettingHandler from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.dispense import ( DispenseParams, DispenseResult, @@ -50,4 +51,7 @@ async def test_dispense_implementation( result = await subject.execute(data) - assert result == DispenseResult(volume=42, position=DeckPoint(x=1, y=2, z=3)) + assert result == SuccessData( + public=DispenseResult(volume=42, position=DeckPoint(x=1, y=2, z=3)), + private=None, + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index 025d863d45b..e1bb654613c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine.execution import PipettingHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.dispense_in_place import ( DispenseInPlaceParams, DispenseInPlaceResult, @@ -31,4 +32,4 @@ async def test_dispense_in_place_implementation( result = await subject.execute(data) - assert result == DispenseInPlaceResult(volume=42) + assert result == SuccessData(public=DispenseInPlaceResult(volume=42), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 4a3c547c07a..9690dcc2461 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -13,6 +13,7 @@ from opentrons.protocol_engine.execution import MovementHandler, TipHandler from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.drop_tip import ( DropTipParams, DropTipResult, @@ -110,7 +111,9 @@ async def test_drop_tip_implementation( result = await subject.execute(params) - assert result == DropTipResult(position=DeckPoint(x=111, y=222, z=333)) + assert result == SuccessData( + public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), private=None + ) decoy.verify( await mock_tip_handler.drop_tip(pipette_id="abc", home_after=True), @@ -170,4 +173,6 @@ async def test_drop_tip_with_alternating_locations( ).then_return(Point(x=111, y=222, z=333)) result = await subject.execute(params) - assert result == DropTipResult(position=DeckPoint(x=111, y=222, z=333)) + assert result == SuccessData( + public=DropTipResult(position=DeckPoint(x=111, y=222, z=333)), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py index fa47b5b7da3..4bfefe4fdbe 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py @@ -4,6 +4,7 @@ from opentrons.protocol_engine.execution import TipHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.drop_tip_in_place import ( DropTipInPlaceParams, DropTipInPlaceResult, @@ -28,7 +29,7 @@ async def test_drop_tip_implementation( result = await subject.execute(params) - assert result == DropTipInPlaceResult() + assert result == SuccessData(public=DropTipInPlaceResult(), private=None) decoy.verify( await mock_tip_handler.drop_tip(pipette_id="abc", home_after=False), diff --git a/api/tests/opentrons/protocol_engine/commands/test_get_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_get_tip_presence.py index 94fe8caadf3..a1d0230f74a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_get_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_get_tip_presence.py @@ -5,6 +5,7 @@ from opentrons.protocol_engine.execution import TipHandler from opentrons.protocol_engine.types import TipPresenceStatus +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.get_tip_presence import ( GetTipPresenceParams, GetTipPresenceResult, @@ -39,4 +40,6 @@ async def test_get_tip_presence_implementation( result = await subject.execute(data) - assert result == GetTipPresenceResult(status=status) + assert result == SuccessData( + public=GetTipPresenceResult(status=status), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_home.py b/api/tests/opentrons/protocol_engine/commands/test_home.py index b23e6e11dc3..f68c1b6de27 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_home.py +++ b/api/tests/opentrons/protocol_engine/commands/test_home.py @@ -5,6 +5,7 @@ from opentrons.types import MountType from opentrons.protocol_engine.execution import MovementHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.home import ( HomeParams, HomeResult, @@ -20,7 +21,7 @@ async def test_home_implementation(decoy: Decoy, movement: MovementHandler) -> N result = await subject.execute(data) - assert result == HomeResult() + assert result == SuccessData(public=HomeResult(), private=None) decoy.verify(await movement.home(axes=[MotorAxis.X, MotorAxis.Y])) @@ -32,7 +33,7 @@ async def test_home_all_implementation(decoy: Decoy, movement: MovementHandler) result = await subject.execute(data) - assert result == HomeResult() + assert result == SuccessData(public=HomeResult(), private=None) decoy.verify(await movement.home(axes=None)) @@ -51,7 +52,7 @@ async def test_home_with_invalid_position( ) result = await subject.execute(data) - assert result == HomeResult() + assert result == SuccessData(public=HomeResult(), private=None) decoy.verify(await movement.home(axes=[MotorAxis.X, MotorAxis.Y]), times=1) decoy.reset() @@ -60,6 +61,6 @@ async def test_home_with_invalid_position( await movement.check_for_valid_position(mount=MountType.LEFT) ).then_return(True) result = await subject.execute(data) - assert result == HomeResult() + assert result == SuccessData(public=HomeResult(), private=None) decoy.verify(await movement.home(axes=[MotorAxis.X, MotorAxis.Y]), times=0) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py index 7ca9d112e27..867e8555386 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_labware.py @@ -20,6 +20,7 @@ from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_labware import ( LoadLabwareParams, LoadLabwareResult, @@ -80,10 +81,13 @@ async def test_load_labware_implementation( result = await subject.execute(data) - assert result == LoadLabwareResult( - labwareId="labware-id", - definition=well_plate_def, - offsetId="labware-offset-id", + assert result == SuccessData( + public=LoadLabwareResult( + labwareId="labware-id", + definition=well_plate_def, + offsetId="labware-offset-id", + ), + private=None, ) @@ -153,10 +157,13 @@ async def test_load_labware_on_labware( result = await subject.execute(data) - assert result == LoadLabwareResult( - labwareId="labware-id", - definition=well_plate_def, - offsetId="labware-offset-id", + assert result == SuccessData( + public=LoadLabwareResult( + labwareId="labware-id", + definition=well_plate_def, + offsetId="labware-offset-id", + ), + private=None, ) decoy.verify( diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py index 2c7a4bb7c93..f1f998b85e7 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py @@ -2,6 +2,7 @@ import pytest from decoy import Decoy +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands import ( LoadLiquidResult, LoadLiquidImplementation, @@ -35,7 +36,7 @@ async def test_load_liquid_implementation( ) result = await subject.execute(data) - assert result == LoadLiquidResult() + assert result == SuccessData(public=LoadLiquidResult(), private=None) decoy.verify(mock_state_view.liquid.validate_liquid_id("liquid-id")) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index 65306f34adc..88a43d6e557 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -16,6 +16,7 @@ from opentrons.protocol_engine import ModuleModel as EngineModuleModel from opentrons.hardware_control.modules import ModuleType +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_module import ( LoadModuleParams, LoadModuleResult, @@ -84,11 +85,14 @@ async def test_load_module_implementation( ) result = await subject.execute(data) - assert result == LoadModuleResult( - moduleId="module-id", - serialNumber="mod-serial", - model=ModuleModel.TEMPERATURE_MODULE_V2, - definition=tempdeck_v2_def, + assert result == SuccessData( + public=LoadModuleResult( + moduleId="module-id", + serialNumber="mod-serial", + model=ModuleModel.TEMPERATURE_MODULE_V2, + definition=tempdeck_v2_def, + ), + private=None, ) @@ -137,11 +141,14 @@ async def test_load_module_implementation_mag_block( ) result = await subject.execute(data) - assert result == LoadModuleResult( - moduleId="module-id", - serialNumber=None, - model=ModuleModel.MAGNETIC_BLOCK_V1, - definition=mag_block_v1_def, + assert result == SuccessData( + public=LoadModuleResult( + moduleId="module-id", + serialNumber=None, + model=ModuleModel.MAGNETIC_BLOCK_V1, + definition=mag_block_v1_def, + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index 91e86ad1376..cb1913da0bb 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -13,6 +13,7 @@ LoadedStaticPipetteData, ) from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.load_pipette import ( LoadPipetteParams, LoadPipetteResult, @@ -66,11 +67,13 @@ async def test_load_pipette_implementation( ) ) - result, private_result = await subject.execute(data) + result = await subject.execute(data) - assert result == LoadPipetteResult(pipetteId="some id") - assert private_result == LoadPipettePrivateResult( - pipette_id="some id", serial_number="some-serial-number", config=config_data + assert result == SuccessData( + public=LoadPipetteResult(pipetteId="some id"), + private=LoadPipettePrivateResult( + pipette_id="some id", serial_number="some-serial-number", config=config_data + ), ) @@ -117,11 +120,13 @@ async def test_load_pipette_implementation_96_channel( ) ) - result, private_result = await subject.execute(data) + result = await subject.execute(data) - assert result == LoadPipetteResult(pipetteId="pipette-id") - assert private_result == LoadPipettePrivateResult( - pipette_id="pipette-id", serial_number="some id", config=config_data + assert result == SuccessData( + public=LoadPipetteResult(pipetteId="pipette-id"), + private=LoadPipettePrivateResult( + pipette_id="pipette-id", serial_number="some id", config=config_data + ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index beb9e14c11d..0872525faf0 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -22,6 +22,7 @@ AddressableAreaLocation, ) from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_labware import ( MoveLabwareParams, MoveLabwareResult, @@ -99,8 +100,11 @@ async def test_manual_move_labware_implementation( decoy.verify( state_view.labware.raise_if_labware_has_labware_on_top("my-cool-labware-id") ) - assert result == MoveLabwareResult( - offsetId="wowzers-a-new-offset-id", + assert result == SuccessData( + public=MoveLabwareResult( + offsetId="wowzers-a-new-offset-id", + ), + private=None, ) @@ -162,8 +166,11 @@ async def test_move_labware_implementation_on_labware( "my-even-cooler-labware-id", ), ) - assert result == MoveLabwareResult( - offsetId="wowzers-a-new-offset-id", + assert result == SuccessData( + public=MoveLabwareResult( + offsetId="wowzers-a-new-offset-id", + ), + private=None, ) @@ -246,8 +253,11 @@ async def test_gripper_move_labware_implementation( post_drop_slide_offset=None, ), ) - assert result == MoveLabwareResult( - offsetId="wowzers-a-new-offset-id", + assert result == SuccessData( + public=MoveLabwareResult( + offsetId="wowzers-a-new-offset-id", + ), + private=None, ) @@ -333,8 +343,11 @@ async def test_gripper_move_to_waste_chute_implementation( post_drop_slide_offset=expected_slide_offset, ), ) - assert result == MoveLabwareResult( - offsetId="wowzers-a-new-offset-id", + assert result == SuccessData( + public=MoveLabwareResult( + offsetId="wowzers-a-new-offset-id", + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py index 9ac321cbb78..f8f49956721 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py @@ -5,6 +5,7 @@ from opentrons.protocol_engine.execution import MovementHandler from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_relative import ( MoveRelativeParams, MoveRelativeResult, @@ -34,4 +35,6 @@ async def test_move_relative_implementation( result = await subject.execute(data) - assert result == MoveRelativeResult(position=DeckPoint(x=1, y=2, z=3)) + assert result == SuccessData( + public=MoveRelativeResult(position=DeckPoint(x=1, y=2, z=3)), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 20515bc12c4..20d944b6f87 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -6,6 +6,7 @@ from opentrons.protocol_engine.state import StateView from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_to_addressable_area import ( MoveToAddressableAreaParams, MoveToAddressableAreaResult, @@ -47,4 +48,7 @@ async def test_move_to_addressable_area_implementation( result = await subject.execute(data) - assert result == MoveToAddressableAreaResult(position=DeckPoint(x=9, y=8, z=7)) + assert result == SuccessData( + public=MoveToAddressableAreaResult(position=DeckPoint(x=9, y=8, z=7)), + private=None, + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index 73478ccafd5..5576b662566 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -6,6 +6,7 @@ from opentrons.protocol_engine.state import StateView from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_to_addressable_area_for_drop_tip import ( MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, @@ -54,6 +55,7 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( result = await subject.execute(data) - assert result == MoveToAddressableAreaForDropTipResult( - position=DeckPoint(x=9, y=8, z=7) + assert result == SuccessData( + public=MoveToAddressableAreaForDropTipResult(position=DeckPoint(x=9, y=8, z=7)), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py index 4f2c32b965b..c630c913480 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py @@ -7,6 +7,7 @@ from opentrons.protocol_engine.types import DeckPoint from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_to_coordinates import ( MoveToCoordinatesParams, MoveToCoordinatesResult, @@ -54,4 +55,7 @@ async def test_move_to_coordinates_implementation( result = await subject.execute(params=params) - assert result == MoveToCoordinatesResult(position=DeckPoint(x=4.44, y=5.55, z=6.66)) + assert result == SuccessData( + public=MoveToCoordinatesResult(position=DeckPoint(x=4.44, y=5.55, z=6.66)), + private=None, + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index aebe8318737..ddd6cf51a21 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -5,6 +5,7 @@ from opentrons.protocol_engine.execution import MovementHandler from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.move_to_well import ( MoveToWellParams, MoveToWellResult, @@ -43,4 +44,6 @@ async def test_move_to_well_implementation( result = await subject.execute(data) - assert result == MoveToWellResult(position=DeckPoint(x=9, y=8, z=7)) + assert result == SuccessData( + public=MoveToWellResult(position=DeckPoint(x=9, y=8, z=7)), private=None + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 087ac0a493e..d44f769ab76 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -9,6 +9,7 @@ from opentrons.protocol_engine.state import StateView from opentrons.protocol_engine.execution import MovementHandler, TipHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.pick_up_tip import ( PickUpTipParams, PickUpTipResult, @@ -77,9 +78,12 @@ async def test_pick_up_tip_implementation( ) ) - assert result == PickUpTipResult( - tipLength=42, - tipVolume=300, - tipDiameter=5, - position=DeckPoint(x=111, y=222, z=333), + assert result == SuccessData( + public=PickUpTipResult( + tipLength=42, + tipVolume=300, + tipDiameter=5, + position=DeckPoint(x=111, y=222, z=333), + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py index d3f09d6685f..b11254af481 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py @@ -6,6 +6,7 @@ PipettingHandler, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.prepare_to_aspirate import ( PrepareToAspirateParams, PrepareToAspirateImplementation, @@ -26,4 +27,4 @@ async def test_prepare_to_aspirate_implmenetation( ) result = await subject.execute(data) - assert isinstance(result, PrepareToAspirateResult) + assert result == SuccessData(public=PrepareToAspirateResult(), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py index 556d4975786..8bafa40d47e 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_reload_labware.py @@ -18,6 +18,7 @@ from opentrons.protocol_engine.resources import labware_validation from opentrons.protocol_engine.state import StateView +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.reload_labware import ( ReloadLabwareParams, ReloadLabwareResult, @@ -56,9 +57,12 @@ async def test_reload_labware_implementation( result = await subject.execute(data) - assert result == ReloadLabwareResult( - labwareId="my-labware-id", - offsetId="labware-offset-id", + assert result == SuccessData( + public=ReloadLabwareResult( + labwareId="my-labware-id", + offsetId="labware-offset-id", + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_retract_axis.py b/api/tests/opentrons/protocol_engine/commands/test_retract_axis.py index 09ca68bd69b..a580875d779 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_retract_axis.py +++ b/api/tests/opentrons/protocol_engine/commands/test_retract_axis.py @@ -4,6 +4,7 @@ from opentrons.protocol_engine.types import MotorAxis from opentrons.protocol_engine.execution import MovementHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.retract_axis import ( RetractAxisParams, RetractAxisResult, @@ -21,5 +22,5 @@ async def test_retract_axis_implementation( data = RetractAxisParams(axis=MotorAxis.Y) result = await subject.execute(data) - assert result == RetractAxisResult() + assert result == SuccessData(public=RetractAxisResult(), private=None) decoy.verify(await movement.retract_axis(axis=MotorAxis.Y)) diff --git a/api/tests/opentrons/protocol_engine/commands/test_save_position.py b/api/tests/opentrons/protocol_engine/commands/test_save_position.py index 99b52a4cd42..c0f5e091e30 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_save_position.py +++ b/api/tests/opentrons/protocol_engine/commands/test_save_position.py @@ -7,6 +7,7 @@ from opentrons.protocol_engine.resources import ModelUtils from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.save_position import ( SavePositionParams, SavePositionResult, @@ -45,7 +46,10 @@ async def test_save_position_implementation( result = await subject.execute(params) - assert result == SavePositionResult( - positionId="456", - position=DeckPoint(x=1, y=2, z=3), + assert result == SuccessData( + public=SavePositionResult( + positionId="456", + position=DeckPoint(x=1, y=2, z=3), + ), + private=None, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_set_rail_lights.py b/api/tests/opentrons/protocol_engine/commands/test_set_rail_lights.py index 473a685e068..161fb2d3fcf 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_set_rail_lights.py +++ b/api/tests/opentrons/protocol_engine/commands/test_set_rail_lights.py @@ -5,6 +5,7 @@ RailLightsHandler, ) +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.set_rail_lights import ( SetRailLightsParams, SetRailLightsResult, @@ -25,6 +26,6 @@ async def test_set_rail_lights_implementation( result = await subject.execute(data) - assert result == SetRailLightsResult() + assert result == SuccessData(public=SetRailLightsResult(), private=None) decoy.verify(await rail_lights.set_rail_lights(True), times=1) diff --git a/api/tests/opentrons/protocol_engine/commands/test_set_status_bar.py b/api/tests/opentrons/protocol_engine/commands/test_set_status_bar.py index 2ec4cc696f6..53652ce6b87 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_set_status_bar.py +++ b/api/tests/opentrons/protocol_engine/commands/test_set_status_bar.py @@ -3,6 +3,7 @@ import pytest from decoy import Decoy +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.set_status_bar import ( SetStatusBarParams, SetStatusBarResult, @@ -34,7 +35,7 @@ async def test_status_bar_busy( result = await subject.execute(params=data) - assert result == SetStatusBarResult() + assert result == SuccessData(public=SetStatusBarResult(), private=None) decoy.verify(await status_bar.set_status_bar(status=StatusBarState.OFF), times=0) @@ -62,6 +63,6 @@ async def test_set_status_bar_animation( data = SetStatusBarParams(animation=animation) result = await subject.execute(params=data) - assert result == SetStatusBarResult() + assert result == SuccessData(public=SetStatusBarResult(), private=None) decoy.verify(await status_bar.set_status_bar(status=expected_state), times=1) diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index 492f1a6751b..2f440c96f13 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -9,6 +9,7 @@ from opentrons.protocol_engine.state import StateView from opentrons.types import Point +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.touch_tip import ( TouchTipParams, TouchTipResult, @@ -120,7 +121,9 @@ async def test_touch_tip_implementation( result = await subject.execute(params) - assert result == TouchTipResult(position=DeckPoint(x=4, y=5, z=6)) + assert result == SuccessData( + public=TouchTipResult(position=DeckPoint(x=4, y=5, z=6)), private=None + ) async def test_touch_tip_disabled( diff --git a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py index 160ee056ae8..087d924f0d2 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py @@ -4,6 +4,7 @@ from opentrons.protocol_engine.execution import TipHandler from opentrons.protocol_engine.types import TipPresenceStatus +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.verify_tip_presence import ( VerifyTipPresenceParams, VerifyTipPresenceResult, @@ -31,4 +32,4 @@ async def test_verify_tip_presence_implementation( result = await subject.execute(data) - assert isinstance(result, VerifyTipPresenceResult) + assert result == SuccessData(public=VerifyTipPresenceResult(), private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/test_wait_for_duration.py b/api/tests/opentrons/protocol_engine/commands/test_wait_for_duration.py index 17c3f2e09d7..9d351ce00d3 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_wait_for_duration.py +++ b/api/tests/opentrons/protocol_engine/commands/test_wait_for_duration.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine.execution import RunControlHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.wait_for_duration import ( WaitForDurationParams, WaitForDurationResult, @@ -21,5 +22,5 @@ async def test_pause_implementation( result = await subject.execute(data) - assert result == WaitForDurationResult() + assert result == SuccessData(public=WaitForDurationResult(), private=None) decoy.verify(await run_control.wait_for_duration(42.0), times=1) diff --git a/api/tests/opentrons/protocol_engine/commands/test_wait_for_resume.py b/api/tests/opentrons/protocol_engine/commands/test_wait_for_resume.py index fcfb6119697..752b85d3446 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_wait_for_resume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_wait_for_resume.py @@ -3,6 +3,7 @@ from opentrons.protocol_engine.execution import RunControlHandler +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.wait_for_resume import ( WaitForResumeCreate, WaitForResumeParams, @@ -22,7 +23,7 @@ async def test_wait_for_resume_implementation( result = await subject.execute(data) - assert result == WaitForResumeResult() + assert result == SuccessData(public=WaitForResumeResult(), private=None) decoy.verify(await run_control.wait_for_resume(), times=1) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py index 4f6ca6f0ba1..a569c18c970 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_close_lid.py @@ -11,6 +11,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.close_lid import ( CloseLidImpl, ) @@ -54,4 +55,4 @@ async def test_close_lid( await tc_hardware.close(), times=1, ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py index 0886a56748d..75627b93014 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_block.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.deactivate_block import ( DeactivateBlockImpl, ) @@ -44,4 +45,4 @@ async def test_deactivate_block( result = await subject.execute(data) decoy.verify(await tc_hardware.deactivate_block(), times=1) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py index 235b5ed16d5..11d6e292370 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_deactivate_lid.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.deactivate_lid import ( DeactivateLidImpl, ) @@ -44,4 +45,4 @@ async def test_deactivate_lid( result = await subject.execute(data) decoy.verify(await tc_hardware.deactivate_lid(), times=1) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py index c6b0e980d9e..8be2cd89c2d 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_open_lid.py @@ -11,6 +11,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.open_lid import ( OpenLidImpl, ) @@ -52,4 +53,4 @@ async def test_open_lid( await tc_hardware.open(), times=1, ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py index 277856444f7..d97bacf7c85 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.run_profile import ( RunProfileImpl, ) @@ -74,4 +75,4 @@ async def test_run_profile( ), times=1, ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py index ee7b0e99830..89e00592510 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.set_target_block_temperature import ( SetTargetBlockTemperatureImpl, ) @@ -66,4 +67,4 @@ async def test_set_target_block_temperature( ), times=1, ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py index 7d6201c65d5..aa558561ac8 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_lid_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.set_target_lid_temperature import ( SetTargetLidTemperatureImpl, ) @@ -55,4 +56,4 @@ async def test_set_target_lid_temperature( result = await subject.execute(data) decoy.verify(await tc_hardware.set_target_lid_temperature(celsius=45.6), times=1) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py index 6ea966e5201..060cc34f2c2 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_block_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.wait_for_block_temperature import ( WaitForBlockTemperatureImpl, ) @@ -50,4 +51,4 @@ async def test_set_target_block_temperature( tc_module_substate.get_target_block_temperature(), await tc_hardware.wait_for_block_target(), ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py index c2a9a8a72ea..08ad7db94a9 100644 --- a/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py +++ b/api/tests/opentrons/protocol_engine/commands/thermocycler/test_wait_for_lid_temperature.py @@ -10,6 +10,7 @@ ) from opentrons.protocol_engine.execution import EquipmentHandler from opentrons.protocol_engine.commands import thermocycler as tc_commands +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.thermocycler.wait_for_lid_temperature import ( WaitForLidTemperatureImpl, ) @@ -50,4 +51,4 @@ async def test_set_target_block_temperature( tc_module_substate.get_target_lid_temperature(), await tc_hardware.wait_for_lid_target(), ) - assert result == expected_result + assert result == SuccessData(public=expected_result, private=None) diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 8f4433a9ebe..57d370eef8f 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -10,10 +10,12 @@ from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI from opentrons.protocol_engine import errors +from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.error_recovery_policy import ( ErrorRecoveryPolicy, ErrorRecoveryType, ) +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence from opentrons.protocol_engine.errors.exceptions import ( EStopActivatedError as PE_EStopActivatedError, ) @@ -210,8 +212,12 @@ class _TestCommandResult(BaseModel): bar: str = "bar" -class _TestCommandImpl(AbstractCommandImpl[_TestCommandParams, _TestCommandResult]): - async def execute(self, params: _TestCommandParams) -> _TestCommandResult: +class _TestCommandImpl( + AbstractCommandImpl[_TestCommandParams, SuccessData[_TestCommandResult, None]] +): + async def execute( + self, params: _TestCommandParams + ) -> SuccessData[_TestCommandResult, None]: raise NotImplementedError() @@ -237,7 +243,9 @@ async def test_execute( TestCommandImplCls = decoy.mock(func=_TestCommandImpl) command_impl = decoy.mock(cls=_TestCommandImpl) - class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + class _TestCommand( + BaseCommand[_TestCommandParams, _TestCommandResult, ErrorOccurrence] + ): commandType: str = "testCommand" params: _TestCommandParams result: Optional[_TestCommandResult] @@ -245,7 +253,7 @@ class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): _ImplementationCls: Type[_TestCommandImpl] = TestCommandImplCls command_params = _TestCommandParams() - command_result = _TestCommandResult() + command_result = SuccessData(public=_TestCommandResult(), private=None) queued_command = cast( Command, @@ -289,7 +297,7 @@ class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): completedAt=datetime(year=2023, month=3, day=3), status=CommandStatus.SUCCEEDED, params=command_params, - result=command_result, + result=command_result.public, notes=command_notes, ), ) @@ -400,7 +408,9 @@ async def test_execute_raises_protocol_engine_error( TestCommandImplCls = decoy.mock(func=_TestCommandImpl) command_impl = decoy.mock(cls=_TestCommandImpl) - class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + class _TestCommand( + BaseCommand[_TestCommandParams, _TestCommandResult, ErrorOccurrence] + ): commandType: str = "testCommand" params: _TestCommandParams result: Optional[_TestCommandResult] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index 8c12e3d059c..471419d2827 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -15,7 +15,7 @@ "errorInfo": { "args": "('Module Type magneticModuleType does not have a related fixture ID.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 150, in execute\n result: CommandResult = await command_impl.execute(running_command.params) # type: ignore[arg-type]\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 116, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 167, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 80, in to_module_fixture_id\n raise ValueError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 146, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 121, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 175, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 80, in to_module_fixture_id\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] @@ -56,7 +56,7 @@ "errorInfo": { "args": "('Module Type magneticModuleType does not have a related fixture ID.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 150, in execute\n result: CommandResult = await command_impl.execute(running_command.params) # type: ignore[arg-type]\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 116, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 167, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 80, in to_module_fixture_id\n raise ValueError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 146, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 121, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 175, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 80, in to_module_fixture_id\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json index df53cf0907c..75501aa1ccd 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json @@ -17,16 +17,16 @@ }, "errors": [ { - "detail": "ParameterValueError [line 48]: Parameter must be set to one of the allowed values of {'flex_1channel_50', 'flex_8channel_50'}.", + "detail": "ParameterValueError [line 48]: Parameter must be set to one of the allowed values of {'flex_8channel_50', 'flex_1channel_50'}.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "opentrons.protocols.parameters.types.ParameterValueError: Parameter must be set to one of the allowed values of {'flex_1channel_50', 'flex_8channel_50'}.", + "detail": "opentrons.protocols.parameters.types.ParameterValueError: Parameter must be set to one of the allowed values of {'flex_8channel_50', 'flex_1channel_50'}.", "errorCode": "4000", "errorInfo": { - "args": "(\"Parameter must be set to one of the allowed values of {'flex_1channel_50', 'flex_8channel_50'}.\",)", + "args": "(\"Parameter must be set to one of the allowed values of {'flex_8channel_50', 'flex_1channel_50'}.\",)", "class": "ParameterValueError", "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 80, in _parse_and_set_parameters\n exec(\"add_parameters(__param_context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices.py\", line 48, in add_parameters\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/_parameter_context.py\", line 152, in add_str\n parameter = parameter_definition.create_str_parameter(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/parameters/parameter_definition.py\", line 241, in create_str_parameter\n return ParameterDefinition(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/parameters/parameter_definition.py\", line 84, in __init__\n self.value: ParamType = default\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/parameters/parameter_definition.py\", line 95, in value\n raise ParameterValueError(\n" }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 507c5d11bee..f31be30f943 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -15,7 +15,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 150, in execute\n result: CommandResult = await command_impl.execute(running_command.params) # type: ignore[arg-type]\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 116, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 176, in _ensure_module_location\n raise ValueError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 146, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 121, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 184, in _ensure_module_location\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] @@ -56,7 +56,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 150, in execute\n result: CommandResult = await command_impl.execute(running_command.params) # type: ignore[arg-type]\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 116, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 176, in _ensure_module_location\n raise ValueError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/execution/command_executor.py\", line 146, in execute\n result = await command_impl.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 121, in execute\n self._ensure_module_location(params.location.slotName, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/commands/load_module.py\", line 184, in _ensure_module_location\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx index c436bc04960..939ee157b80 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRunOverflowMenu.test.tsx @@ -5,10 +5,7 @@ import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { when } from 'vitest-when' import { MemoryRouter } from 'react-router-dom' -import { - useAllCommandsQuery, - useDeleteRunMutation, -} from '@opentrons/react-api-client' +import { useDeleteRunMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import runRecord from '../../../organisms/RunDetails/__fixtures__/runRecord.json' import { useDownloadRunLog, useTrackProtocolRunEvent, useRobot } from '../hooks' @@ -21,6 +18,7 @@ import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { HistoricalProtocolRunOverflowMenu } from '../HistoricalProtocolRunOverflowMenu' +import { useNotifyAllCommandsQuery } from '../../../resources/runs' import type { UseQueryResult } from 'react-query' import type { CommandsData } from '@opentrons/api-client' @@ -32,6 +30,7 @@ vi.mock('../../RunTimeControl/hooks') vi.mock('../../../redux/analytics') vi.mock('../../../redux/config') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') +vi.mock('../../../resources/runs') vi.mock('@opentrons/react-api-client') const render = ( @@ -87,7 +86,7 @@ describe('HistoricalProtocolRunOverflowMenu', () => { isStopRunActionLoading: false, isResetRunLoading: false, }) - when(useAllCommandsQuery) + when(useNotifyAllCommandsQuery) .calledWith( RUN_ID, { diff --git a/app/src/organisms/Devices/hooks/useLastRunCommand.ts b/app/src/organisms/Devices/hooks/useLastRunCommand.ts index 347532abd36..b0840b888ae 100644 --- a/app/src/organisms/Devices/hooks/useLastRunCommand.ts +++ b/app/src/organisms/Devices/hooks/useLastRunCommand.ts @@ -1,5 +1,3 @@ -import { useAllCommandsQuery } from '@opentrons/react-api-client' -import { useRunStatus } from '../../RunTimeControl/hooks' import { RUN_STATUS_AWAITING_RECOVERY, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, @@ -11,6 +9,9 @@ import { RUN_STATUS_STOP_REQUESTED, } from '@opentrons/api-client' +import { useNotifyAllCommandsQuery } from '../../../resources/runs' +import { useRunStatus } from '../../RunTimeControl/hooks' + import type { UseQueryOptions } from 'react-query' import type { CommandsData, RunCommandSummary } from '@opentrons/api-client' @@ -31,7 +32,7 @@ export function useLastRunCommand( options: UseQueryOptions = {} ): RunCommandSummary | null { const runStatus = useRunStatus(runId) - const { data: commandsData } = useAllCommandsQuery( + const { data: commandsData } = useNotifyAllCommandsQuery( runId, { cursor: null, pageLength: 1 }, { diff --git a/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts b/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts index 33976bb6fd0..394c8a3eac0 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useRunCommands.ts @@ -1,4 +1,5 @@ -import { useAllCommandsQuery } from '@opentrons/react-api-client' +import { useNotifyAllCommandsQuery } from '../../../resources/runs' + import type { UseQueryOptions } from 'react-query' import type { CommandsData, @@ -13,7 +14,7 @@ export function useRunCommands( params?: GetCommandsParams, options?: UseQueryOptions ): RunCommandSummary[] | null { - const { data: commandsData } = useAllCommandsQuery(runId, params, { + const { data: commandsData } = useNotifyAllCommandsQuery(runId, params, { refetchInterval: REFETCH_INTERVAL, ...options, }) diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index 61abab2fc00..9c7b535fe38 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -21,7 +21,6 @@ import { import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { - useNotifyLastRunCommand, useNotifyAllCommandsAsPreSerializedList, useNotifyRunQuery, } from '../../resources/runs' @@ -31,6 +30,7 @@ import { NAV_BAR_WIDTH } from '../../App/constants' import { CommandIcon } from './CommandIcon' import { useRunStatus } from '../RunTimeControl/hooks' import { getCommandTextData } from '../CommandText/utils/getCommandTextData' +import { useLastRunCommand } from '../Devices/hooks/useLastRunCommand' import type { ViewportListRef } from 'react-viewport-list' import type { RunStatus } from '@opentrons/api-client' @@ -72,7 +72,7 @@ export const RunPreviewComponent = ( const nullCheckedCommandsFromQuery = commandsFromQuery == null ? robotSideAnalysis?.commands : commandsFromQuery const viewPortRef = React.useRef(null) - const currentRunCommandKey = useNotifyLastRunCommand(runId, { + const currentRunCommandKey = useLastRunCommand(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, })?.key const [ diff --git a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx index 3ab555a22be..77eeada15ba 100644 --- a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx +++ b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx @@ -3,10 +3,8 @@ import { when } from 'vitest-when' import { screen } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { - useAllCommandsQuery, - useCommandQuery, -} from '@opentrons/react-api-client' + +import { useCommandQuery } from '@opentrons/react-api-client' import { RUN_STATUS_IDLE, RUN_STATUS_RUNNING, @@ -19,8 +17,8 @@ import { ProgressBar } from '../../../atoms/ProgressBar' import { useRunStatus } from '../../RunTimeControl/hooks' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { - useNotifyLastRunCommand, useNotifyRunQuery, + useNotifyAllCommandsQuery, } from '../../../resources/runs' import { useDownloadRunLog } from '../../Devices/hooks' import { @@ -35,6 +33,8 @@ import { } from '../../InterventionModal/__fixtures__' import { RunProgressMeter } from '..' import { renderWithProviders } from '../../../__testing-utils__' +import { useLastRunCommand } from '../../Devices/hooks/useLastRunCommand' + import type { RunCommandSummary } from '@opentrons/api-client' import type * as ApiClient from '@opentrons/react-api-client' @@ -42,7 +42,6 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { const actual = await importOriginal() return { ...actual, - useAllCommandsQuery: vi.fn(), useCommandQuery: vi.fn(), } }) @@ -52,6 +51,7 @@ vi.mock('../../../resources/runs') vi.mock('../../Devices/hooks') vi.mock('../../../atoms/ProgressBar') vi.mock('../../InterventionModal') +vi.mock('../../Devices/hooks/useLastRunCommand') const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -73,7 +73,7 @@ describe('RunProgressMeter', () => { when(useMostRecentCompletedAnalysis) .calledWith(NON_DETERMINISTIC_RUN_ID) .thenReturn(null) - when(useAllCommandsQuery) + when(useNotifyAllCommandsQuery) .calledWith(NON_DETERMINISTIC_RUN_ID, { cursor: null, pageLength: 1 }) .thenReturn(mockUseAllCommandsResponseNonDeterministic) when(useCommandQuery) @@ -83,7 +83,7 @@ describe('RunProgressMeter', () => { downloadRunLog: vi.fn(), isRunLogLoading: false, }) - when(useNotifyLastRunCommand) + when(useLastRunCommand) .calledWith(NON_DETERMINISTIC_RUN_ID, { refetchInterval: 1000 }) .thenReturn({ key: NON_DETERMINISTIC_COMMAND_KEY } as RunCommandSummary) @@ -112,7 +112,7 @@ describe('RunProgressMeter', () => { screen.getByText('Download run log') }) it('should render an intervention modal when lastRunCommand is a pause command', () => { - vi.mocked(useAllCommandsQuery).mockReturnValue({ + vi.mocked(useNotifyAllCommandsQuery).mockReturnValue({ data: { data: [mockPauseCommandWithStartTime], meta: { totalLength: 1 } }, } as any) vi.mocked(useNotifyRunQuery).mockReturnValue({ @@ -124,7 +124,7 @@ describe('RunProgressMeter', () => { }) it('should render an intervention modal when lastRunCommand is a move labware command', () => { - vi.mocked(useAllCommandsQuery).mockReturnValue({ + vi.mocked(useNotifyAllCommandsQuery).mockReturnValue({ data: { data: [mockMoveLabwareCommandFromSlot], meta: { totalLength: 1 }, diff --git a/app/src/organisms/RunProgressMeter/index.tsx b/app/src/organisms/RunProgressMeter/index.tsx index f747de4f9b6..d230636956c 100644 --- a/app/src/organisms/RunProgressMeter/index.tsx +++ b/app/src/organisms/RunProgressMeter/index.tsx @@ -27,10 +27,7 @@ import { RUN_STATUS_RUNNING, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, } from '@opentrons/api-client' -import { - useAllCommandsQuery, - useCommandQuery, -} from '@opentrons/react-api-client' +import { useCommandQuery } from '@opentrons/react-api-client' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getTopPortalEl } from '../../App/portal' @@ -42,7 +39,10 @@ import { ProgressBar } from '../../atoms/ProgressBar' import { useDownloadRunLog, useRobotType } from '../Devices/hooks' import { InterventionTicks } from './InterventionTicks' import { isInterventionCommand } from '../InterventionModal/utils' -import { useNotifyRunQuery } from '../../resources/runs' +import { + useNotifyRunQuery, + useNotifyAllCommandsQuery, +} from '../../resources/runs' import { getCommandTextData } from '../CommandText/utils/getCommandTextData' import type { RunStatus } from '@opentrons/api-client' @@ -75,7 +75,7 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element { const { data: runRecord } = useNotifyRunQuery(runId) const runData = runRecord?.data ?? null const analysis = useMostRecentCompletedAnalysis(runId) - const { data: allCommandsQueryData } = useAllCommandsQuery(runId, { + const { data: allCommandsQueryData } = useNotifyAllCommandsQuery(runId, { cursor: null, pageLength: 1, }) diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index acf08a15d77..bbb1e0eca60 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -10,7 +10,6 @@ import { RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' import { - useAllCommandsQuery, useProtocolAnalysesQuery, useProtocolQuery, useRunActionMutations, @@ -35,10 +34,11 @@ import { RunPausedSplash } from '../../../organisms/OnDeviceDisplay/RunningProto import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' import { - useNotifyLastRunCommand, useNotifyRunQuery, + useNotifyAllCommandsQuery, } from '../../../resources/runs' import { useFeatureFlag } from '../../../redux/config' +import { useLastRunCommand } from '../../../organisms/Devices/hooks/useLastRunCommand' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses, RunCommandSummary } from '@opentrons/api-client' @@ -58,6 +58,7 @@ vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') vi.mock('../../../organisms/OpenDoorAlertModal') vi.mock('../../../resources/runs') vi.mock('../../../redux/config') +vi.mock('../../../organisms/Devices/hooks/useLastRunCommand') const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' @@ -134,10 +135,10 @@ describe('RunningProtocol', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn(mockRobotSideAnalysis) - when(vi.mocked(useAllCommandsQuery)) + when(vi.mocked(useNotifyAllCommandsQuery)) .calledWith(RUN_ID, { cursor: null, pageLength: 1 }) .thenReturn(mockUseAllCommandsResponseNonDeterministic) - vi.mocked(useNotifyLastRunCommand).mockReturnValue({ + vi.mocked(useLastRunCommand).mockReturnValue({ key: 'FAKE_COMMAND_KEY', } as RunCommandSummary) when(vi.mocked(useFeatureFlag)) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 0bd793acc8f..0101f92ab17 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -30,10 +30,7 @@ import { import { useFeatureFlag } from '../../redux/config' import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { - useNotifyLastRunCommand, - useNotifyRunQuery, -} from '../../resources/runs' +import { useNotifyRunQuery } from '../../resources/runs' import { InterventionModal } from '../../organisms/InterventionModal' import { isInterventionCommand } from '../../organisms/InterventionModal/utils' import { @@ -56,6 +53,7 @@ import { RunPausedSplash } from '../../organisms/OnDeviceDisplay/RunningProtocol import { getLocalRobot } from '../../redux/discovery' import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' import { ErrorRecoveryFlows } from '../../organisms/ErrorRecoveryFlows' +import { useLastRunCommand } from '../../organisms/Devices/hooks/useLastRunCommand' import type { OnDeviceRouteParams } from '../../App/types' @@ -95,7 +93,7 @@ export function RunningProtocol(): JSX.Element { const lastAnimatedCommand = React.useRef(null) const swipe = useSwipe() const robotSideAnalysis = useMostRecentCompletedAnalysis(runId) - const lastRunCommand = useNotifyLastRunCommand(runId, { + const lastRunCommand = useLastRunCommand(runId, { refetchInterval: LIVE_RUN_COMMANDS_POLL_MS, }) diff --git a/app/src/resources/runs/index.ts b/app/src/resources/runs/index.ts index 91595d17f08..a69aba067aa 100644 --- a/app/src/resources/runs/index.ts +++ b/app/src/resources/runs/index.ts @@ -2,5 +2,5 @@ export * from './hooks' export * from './utils' export * from './useNotifyAllRunsQuery' export * from './useNotifyRunQuery' -export * from './useNotifyLastRunCommand' +export * from './useNotifyAllCommandsQuery' export * from './useNotifyAllCommandsAsPreSerializedList' diff --git a/app/src/resources/runs/useNotifyAllCommandsQuery.ts b/app/src/resources/runs/useNotifyAllCommandsQuery.ts new file mode 100644 index 00000000000..c4624313d00 --- /dev/null +++ b/app/src/resources/runs/useNotifyAllCommandsQuery.ts @@ -0,0 +1,26 @@ +import { useAllCommandsQuery } from '@opentrons/react-api-client' + +import { useNotifyService } from '../useNotifyService' + +import type { UseQueryResult } from 'react-query' +import type { CommandsData, GetCommandsParams } from '@opentrons/api-client' +import type { QueryOptionsWithPolling } from '../useNotifyService' + +export function useNotifyAllCommandsQuery( + runId: string | null, + params?: GetCommandsParams | null, + options: QueryOptionsWithPolling = {} +): UseQueryResult { + const { notifyOnSettled, isNotifyEnabled } = useNotifyService({ + topic: 'robot-server/runs/current_command', // only updates to the current command cause all commands to change + options, + }) + + const httpResponse = useAllCommandsQuery(runId, params, { + ...options, + enabled: options?.enabled !== false && isNotifyEnabled, + onSettled: notifyOnSettled, + }) + + return httpResponse +} diff --git a/app/src/resources/runs/useNotifyLastRunCommand.ts b/app/src/resources/runs/useNotifyLastRunCommand.ts deleted file mode 100644 index b7c2289ffc8..00000000000 --- a/app/src/resources/runs/useNotifyLastRunCommand.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useNotifyService } from '../useNotifyService' -import { useLastRunCommand } from '../../organisms/Devices/hooks/useLastRunCommand' - -import type { CommandsData, RunCommandSummary } from '@opentrons/api-client' -import type { QueryOptionsWithPolling } from '../useNotifyService' - -export function useNotifyLastRunCommand( - runId: string, - options: QueryOptionsWithPolling = {} -): RunCommandSummary | null { - const { notifyOnSettled, isNotifyEnabled } = useNotifyService({ - topic: 'robot-server/runs/current_command', - options, - }) - - const httpResponse = useLastRunCommand(runId, { - ...options, - enabled: options?.enabled !== false && isNotifyEnabled, - onSettled: notifyOnSettled, - }) - - return httpResponse -} diff --git a/components/src/forms/DeprecatedCheckboxField.tsx b/components/src/forms/DeprecatedCheckboxField.tsx index a520d4da7d1..68452eb90e4 100644 --- a/components/src/forms/DeprecatedCheckboxField.tsx +++ b/components/src/forms/DeprecatedCheckboxField.tsx @@ -62,7 +62,7 @@ export function DeprecatedCheckboxField( props.isIndeterminate ? 'minus-box' : props.value - ? 'checkbox-marked' + ? 'ot-checkbox' : 'checkbox-blank-outline' } width="100%" diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 135eced1f5d..842788bfd5b 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -19,13 +19,13 @@ from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocols.types import APIVersion from opentrons.hardware_control.thread_manager import ThreadManager -from opentrons.hardware_control.types import OT3Mount, Axis +from opentrons.hardware_control.types import OT3Mount, Axis, HardwareFeatureFlags from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control.instruments.ot3.pipette import Pipette from opentrons import execute, simulate -from opentrons.types import Point, Location - +from opentrons.types import Point, Location, Mount +from opentrons.config.types import OT3Config, RobotConfig from opentrons_shared_data.labware.dev_types import LabwareDefinition from hardware_testing.opentrons_api import helpers_ot3 @@ -81,7 +81,17 @@ def get_api_context( """Get api context.""" async def _thread_manager_build_hw_api( - *args: Any, loop: asyncio.AbstractEventLoop, **kwargs: Any + attached_instruments: Optional[ + Dict[Union[Mount, OT3Mount], Dict[str, Optional[str]]] + ] = None, + attached_modules: Optional[List[str]] = None, + config: Union[OT3Config, RobotConfig, None] = None, + loop: Optional[asyncio.AbstractEventLoop] = None, + strict_attached_instruments: bool = True, + use_usb_bus: bool = False, + update_firmware: bool = True, + status_bar_enabled: bool = True, + feature_flags: Optional[HardwareFeatureFlags] = None, ) -> OT3API: return await helpers_ot3.build_async_ot3_hardware_api( is_simulating=is_simulating, diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py index af83c031436..eb118c2edff 100644 --- a/hardware-testing/hardware_testing/liquid_sense/__main__.py +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -94,8 +94,9 @@ class RunArgs: start_height_offset: float aspirate: bool dial_indicator: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] - plunger_speed: bool + plunger_speed: float trials_before_jog: int + multi_passes: int @classmethod def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: @@ -236,6 +237,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": dial_indicator=dial, plunger_speed=args.plunger_speed, trials_before_jog=args.trials_before_jog, + multi_passes=args.multi_passes, ) @@ -266,6 +268,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": parser.add_argument("--ignore-env", action="store_true") parser.add_argument("--ignore-dial", action="store_true") parser.add_argument("--trials-before-jog", type=int, default=10) + parser.add_argument("--multi-passes", type=int, default=1) args = parser.parse_args() run_args = RunArgs.build_run_args(args) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py index 46368568bf2..9a61c172c8e 100644 --- a/hardware-testing/hardware_testing/liquid_sense/execute.py +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -26,6 +26,15 @@ from opentrons.protocol_api import ProtocolContext, Well, Labware +from opentrons_shared_data.errors.exceptions import LiquidNotFoundError + + +PROBE_MAX_TIME: Dict[int, float] = { + 1: 2.75, + 8: 1.75, + 96: 0.85, +} + def _load_tipracks( ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int @@ -280,6 +289,41 @@ def _get_target_height() -> float: store_tip_results(run_args.test_report, tip, results, adjusted_results) +def get_plunger_travel(run_args: RunArgs) -> float: + """Get the travel distance for the pipette.""" + hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT + hw_api = get_sync_hw_api(run_args.ctx) + plunger_positions = hw_api._pipette_handler.get_pipette(hw_mount).plunger_positions + plunger_travel = plunger_positions.bottom - plunger_positions.top + return plunger_travel + + +def find_max_z_distances( + run_args: RunArgs, tip: int, well: Well, p_speed: float +) -> List[float]: + """Returns a list of max z distances for each probe. + + Each element is the max travel for the z mount for a particular call + to hw_api.liquid_probe, it is the limit of z distance the pipette can + move with the combination of z speed and plunger speed, + if the distance would exceed the well depth then the number is + truncated to avoid collisions. + """ + z_speed = run_args.z_speed + max_z_distance = well.depth + run_args.start_height_offset + plunger_travel = get_plunger_travel(run_args) + p_travel_time = min( + plunger_travel / p_speed, PROBE_MAX_TIME[run_args.pipette_channels] + ) + + z_travels: List[float] = [] + while max_z_distance > 0: + next_travel = min(p_travel_time * z_speed, max_z_distance) + z_travels.append(next_travel) + max_z_distance -= next_travel + return z_travels + + def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: hw_api = get_sync_hw_api(run_args.ctx) lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ @@ -303,25 +347,37 @@ def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: if run_args.plunger_speed == -1 else run_args.plunger_speed ) - lps = LiquidProbeSettings( - starting_mount_height=well.top().point.z + run_args.start_height_offset, - max_z_distance=min(well.depth, lqid_cfg["max_z_distance"]), - min_z_distance=lqid_cfg["min_z_distance"], - mount_speed=run_args.z_speed, - plunger_speed=plunger_speed, - sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], - expected_liquid_height=110, - output_option=OutputOptions.sync_buffer_to_csv, - aspirate_while_sensing=run_args.aspirate, - auto_zero_sensor=True, - num_baseline_reads=10, - data_files=data_files, - ) - hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT - run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") - # TODO add in stuff for secondary probe - height = hw_api.liquid_probe(hw_mount, lps, probe_target) + z_distances: List[float] = find_max_z_distances(run_args, tip, well, plunger_speed) + z_distances = z_distances[: run_args.multi_passes] + start_height = well.top().point.z + run_args.start_height_offset + for z_dist in z_distances: + lps = LiquidProbeSettings( + starting_mount_height=start_height, + max_z_distance=z_dist, + min_z_distance=lqid_cfg["min_z_distance"], + mount_speed=run_args.z_speed, + plunger_speed=plunger_speed, + sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], + expected_liquid_height=110, + output_option=OutputOptions.sync_buffer_to_csv, + aspirate_while_sensing=run_args.aspirate, + auto_zero_sensor=True, + num_baseline_reads=10, + data_files=data_files, + ) + + hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT + run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") + # TODO add in stuff for secondary probe + try: + height = hw_api.liquid_probe(hw_mount, lps, probe_target) + except LiquidNotFoundError as lnf: + ui.print_info(f"Liquid not found current position {lnf.detail}") + start_height -= z_dist + else: + break + run_args.recorder.clear_sample_tag() + ui.print_info(f"Trial {trial} complete") - run_args.recorder.clear_sample_tag() return height diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index c2dcac25502..0676a29967a 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -72,7 +72,8 @@ # FIXME we should restrict some of these functions by instrument type. -def _build_pass_step( +def _fix_pass_step_for_buffer( + move_group: MoveGroupStep, movers: List[NodeId], distance: Dict[NodeId, float], speed: Dict[NodeId, float], @@ -82,8 +83,7 @@ def _build_pass_step( pipette_nodes = [ i for i in movers if i in [NodeId.pipette_left, NodeId.pipette_right] ] - - move_group = create_step( + pipette_move = create_step( distance={ax: float64(abs(distance[ax])) for ax in movers}, velocity={ ax: float64(speed[ax] * copysign(1.0, distance[ax])) for ax in movers @@ -92,10 +92,23 @@ def _build_pass_step( # use any node present to calculate duration of the move, assuming the durations # will be the same duration=float64(abs(distance[movers[0]] / speed[movers[0]])), - present_nodes=movers, - stop_condition=stop_condition, + present_nodes=pipette_nodes, + stop_condition=MoveStopCondition.sensor_report, + sensor_to_use=sensor_to_use, ) - pipette_move = create_step( + for node in pipette_nodes: + move_group[node] = pipette_move[node] + return move_group + + +def _build_pass_step( + movers: List[NodeId], + distance: Dict[NodeId, float], + speed: Dict[NodeId, float], + stop_condition: MoveStopCondition = MoveStopCondition.sync_line, + sensor_to_use: Optional[SensorId] = None, +) -> MoveGroupStep: + move_group = create_step( distance={ax: float64(abs(distance[ax])) for ax in movers}, velocity={ ax: float64(speed[ax] * copysign(1.0, distance[ax])) for ax in movers @@ -104,12 +117,9 @@ def _build_pass_step( # use any node present to calculate duration of the move, assuming the durations # will be the same duration=float64(abs(distance[movers[0]] / speed[movers[0]])), - present_nodes=pipette_nodes, - stop_condition=MoveStopCondition.sensor_report, - sensor_to_use=sensor_to_use, + present_nodes=movers, + stop_condition=stop_condition, ) - for node in pipette_nodes: - move_group[node] = pipette_move[node] return move_group @@ -144,7 +154,7 @@ async def run_sync_buffer_to_csv( ) ), ) - await asyncio.sleep(10) + await sensor_capturer.wait_for_complete() messenger.remove_listener(sensor_capturer) await messenger.send( node_id=tool, @@ -323,6 +333,15 @@ async def liquid_probe( stop_condition=MoveStopCondition.sync_line, sensor_to_use=sensor_id, ) + if sync_buffer_output: + sensor_group = _fix_pass_step_for_buffer( + sensor_group, + movers=[head_node, tool], + distance={head_node: max_z_distance, tool: max_z_distance}, + speed={head_node: mount_speed, tool: plunger_speed}, + stop_condition=MoveStopCondition.sync_line, + sensor_to_use=sensor_id, + ) sensor_runner = MoveGroupRunner(move_groups=[[sensor_group]]) if csv_output: diff --git a/hardware/opentrons_hardware/sensors/sensor_driver.py b/hardware/opentrons_hardware/sensors/sensor_driver.py index 7e587d35b80..189560aecf8 100644 --- a/hardware/opentrons_hardware/sensors/sensor_driver.py +++ b/hardware/opentrons_hardware/sensors/sensor_driver.py @@ -235,6 +235,7 @@ def __init__( self.response_queue: asyncio.Queue[float] = asyncio.Queue() self.mount = mount self.start_time = 0.0 + self.event: Any = None async def __aenter__(self) -> None: """Create a csv heading for logging pressure readings.""" @@ -248,6 +249,16 @@ async def __aexit__(self, *args: Any) -> None: """Close csv file.""" self.data_file.close() + async def wait_for_complete(self, wait_time: float = 2.0) -> None: + """Wait for the data to stop, only use this with a send_accumulated_data_request.""" + self.event = asyncio.Event() + recieving = True + while recieving: + await asyncio.sleep(wait_time) + recieving = self.event.is_set() + self.event.clear() + self.event = None + def __call__( self, message: MessageDefinition, @@ -261,3 +272,5 @@ def __call__( self.response_queue.put_nowait(data) current_time = round((time.time() - self.start_time), 3) self.csv_writer.writerow([current_time, data]) # type: ignore + if self.event is not None: + self.event.set() diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py index ba391da2c14..c88de67f089 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_tool_sensors.py @@ -1,10 +1,12 @@ """Test the tool-sensor coordination code.""" import logging from mock import patch, ANY, AsyncMock, call +import os import pytest from contextlib import asynccontextmanager from typing import Iterator, List, Tuple, AsyncIterator, Any, Dict from opentrons_hardware.firmware_bindings.messages.message_definitions import ( + AddLinearMoveRequest, ExecuteMoveGroupRequest, MoveCompleted, ReadFromSensorResponse, @@ -48,6 +50,7 @@ SensorType, SensorThresholdMode, SensorOutputBinding, + MoveStopCondition, ) from opentrons_hardware.sensors.scheduler import SensorScheduler from opentrons_hardware.sensors.sensor_driver import SensorDriver @@ -195,6 +198,109 @@ def move_responder( ) +@pytest.mark.parametrize( + "csv_output, sync_buffer_output, can_bus_only_output, move_stop_condition", + [ + (True, False, False, MoveStopCondition.sync_line), + (True, True, False, MoveStopCondition.sensor_report), + (False, False, True, MoveStopCondition.sync_line), + ], +) +async def test_liquid_probe_output_options( + mock_messenger: AsyncMock, + mock_bind_output: AsyncMock, + message_send_loopback: CanLoopback, + mock_sensor_threshold: AsyncMock, + csv_output: bool, + sync_buffer_output: bool, + can_bus_only_output: bool, + move_stop_condition: MoveStopCondition, +) -> None: + """Test that liquid_probe targets the right nodes.""" + sensor_info = SensorInformation( + sensor_type=SensorType.pressure, + sensor_id=SensorId.S0, + node_id=NodeId.pipette_left, + ) + test_csv_file: str = os.path.join(os.getcwd(), "test.csv") + + def move_responder( + node_id: NodeId, message: MessageDefinition + ) -> List[Tuple[NodeId, MessageDefinition, NodeId]]: + message.payload.serialize() + if isinstance(message, ExecuteMoveGroupRequest): + ack_payload = EmptyPayload() + ack_payload.message_index = message.payload.message_index + return [ + ( + NodeId.host, + MoveCompleted( + payload=MoveCompletedPayload( + group_id=UInt8Field(0), + seq_id=UInt8Field(0), + current_position_um=UInt32Field(14000), + encoder_position_um=Int32Field(14000), + position_flags=MotorPositionFlagsField(0), + ack_id=UInt8Field(2), + ) + ), + NodeId.head_l, + ), + ( + NodeId.host, + MoveCompleted( + payload=MoveCompletedPayload( + group_id=UInt8Field(0), + seq_id=UInt8Field(0), + current_position_um=UInt32Field(14000), + encoder_position_um=Int32Field(14000), + position_flags=MotorPositionFlagsField(0), + ack_id=UInt8Field(2), + ) + ), + NodeId.pipette_left, + ), + ] + else: + if ( + isinstance(message, AddLinearMoveRequest) + and node_id == NodeId.pipette_left + ): + assert ( + message.payload.request_stop_condition.value == move_stop_condition + ) + return [] + + message_send_loopback.add_responder(move_responder) + try: + position = await liquid_probe( + messenger=mock_messenger, + tool=NodeId.pipette_left, + head_node=NodeId.head_l, + max_z_distance=40, + mount_speed=10, + plunger_speed=8, + threshold_pascals=14, + csv_output=csv_output, + sync_buffer_output=sync_buffer_output, + can_bus_only_output=can_bus_only_output, + data_files={SensorId.S0: test_csv_file}, + auto_zero_sensor=True, + num_baseline_reads=8, + sensor_id=SensorId.S0, + ) + finally: + if os.path.isfile(test_csv_file): + # clean up the test file this creates if it exists + os.remove(test_csv_file) + assert position[NodeId.head_l].positions_only()[0] == 14 + assert mock_sensor_threshold.call_args_list[0][0][0] == SensorThresholdInformation( + sensor=sensor_info, + data=SensorDataType.build(14 * 65536, sensor_info.sensor_type), + mode=SensorThresholdMode.absolute, + ) + + @pytest.mark.parametrize( "target_node,motor_node,distance,speed,", [ diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index 04509609800..739611e853e 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -2,6 +2,7 @@ "api": "API: An API level is 2.15", "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.", + "copy_code": "Copy code", "disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.", "got_feedback": "Got feedback? We love to hear it.", "make_sure_your_prompt": "Make sure your prompt includes the following:", diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 3b5c54ebbc6..a8b844af08c 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -1,12 +1,18 @@ import React from 'react' -// import { css } from 'styled-components' +import styled from 'styled-components' import { useTranslation } from 'react-i18next' import Markdown from 'react-markdown' import { + ALIGN_CENTER, BORDERS, COLORS, DIRECTION_COLUMN, Flex, + Icon, + JUSTIFY_CENTER, + POSITION_ABSOLUTE, + POSITION_RELATIVE, + PrimaryButton, SPACING, StyledText, } from '@opentrons/components' @@ -15,12 +21,26 @@ import type { ChatData } from '../../resources/types' interface ChatDisplayProps { chat: ChatData + chatId: string } -export function ChatDisplay({ chat }: ChatDisplayProps): JSX.Element { +export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') + const [isCopied, setIsCopied] = React.useState(false) const { role, content } = chat const isUser = role === 'user' + + const handleClickCopy = async (): Promise => { + const lastCodeBlock = document.querySelector(`#${chatId}`) + const code = lastCodeBlock?.textContent ?? '' + await navigator.clipboard.writeText(code) + setIsCopied(true) + } + + function CodeText(props: JSX.IntrinsicAttributes): JSX.Element { + return + } + return ( - {/* ToDo (kk:05/02/2024) This part is waiting for Mel's design */} - {/* {content} - */} - {content} + + {role === 'assistant' ? ( + + + + + + ) : null} ) } -// ToDo (kk:05/02/2024) This part is waiting for Mel's design -// function ExternalLink(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } - -// function ParagraphText(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } +// Note (05/08/2024) the following styles are temp +function ExternalLink(props: JSX.IntrinsicAttributes): JSX.Element { + return +} -// function HeaderText(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } +function ParagraphText(props: JSX.IntrinsicAttributes): JSX.Element { + return +} -// function ListItemText(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } +function HeaderText(props: JSX.IntrinsicAttributes): JSX.Element { + return +} -// function UnnumberedListText(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } +function ListItemText(props: JSX.IntrinsicAttributes): JSX.Element { + return +} -// const CODE_TEXT_STYLE = css` -// padding: ${SPACING.spacing16}; -// font-family: monospace; -// color: ${COLORS.white}; -// background-color: ${COLORS.black90}; -// ` +function UnnumberedListText(props: JSX.IntrinsicAttributes): JSX.Element { + return +} -// function CodeText(props: JSX.IntrinsicAttributes): JSX.Element { -// return -// } +const CodeWrapper = styled(Flex)` + font-family: monospace; + padding: ${SPACING.spacing16}; + color: ${COLORS.white}; + background-color: ${COLORS.black90}; + border-radius: ${BORDERS.borderRadius8}; + overflow: auto; +` diff --git a/opentrons-ai-client/src/molecules/PrimaryFloatingButton/index.tsx b/opentrons-ai-client/src/molecules/PrimaryFloatingButton/index.tsx index 0044355931f..056e4aee2b4 100644 --- a/opentrons-ai-client/src/molecules/PrimaryFloatingButton/index.tsx +++ b/opentrons-ai-client/src/molecules/PrimaryFloatingButton/index.tsx @@ -14,9 +14,9 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import type { IconName } from '@opentrons/components/src/icons' +import type { IconName, StyleProps } from '@opentrons/components' -interface PrimaryFloatingButtonProps { +interface PrimaryFloatingButtonProps extends StyleProps { buttonText: string iconName: IconName disabled?: boolean @@ -26,9 +26,10 @@ export function PrimaryFloatingButton({ buttonText, iconName, disabled = false, + ...buttonProps }: PrimaryFloatingButtonProps): JSX.Element { return ( - + 0 ? chatData.map((chat, index) => ( )) : null} diff --git a/opentrons-ai-server/.gitignore b/opentrons-ai-server/.gitignore index 106d3921319..78bcfc1a90b 100644 --- a/opentrons-ai-server/.gitignore +++ b/opentrons-ai-server/.gitignore @@ -3,3 +3,7 @@ results package function.zip requirements.txt +test.env +cached_token.txt +tests/helpers/cached_token.txt +tests/helpers/test.env diff --git a/opentrons-ai-server/Makefile b/opentrons-ai-server/Makefile index 4cf76860f97..62cb4446010 100644 --- a/opentrons-ai-server/Makefile +++ b/opentrons-ai-server/Makefile @@ -21,7 +21,7 @@ black-check: .PHONY: ruff ruff: - python -m pipenv run python -m ruff check . --fix + python -m pipenv run python -m ruff check . --fix --unsafe-fixes .PHONY: ruff-check ruff-check: @@ -29,7 +29,7 @@ ruff-check: .PHONY: mypy mypy: - python -m pipenv run python -m mypy aws_actions.py api + python -m pipenv run python -m mypy deploy.py api tests .PHONY: format-readme format-readme: @@ -56,7 +56,7 @@ pre-commit: fixup unit-test .PHONY: gen-env gen-env: - python -m pipenv run python api/settings.py + python -m pipenv run python -m api.settings .PHONY: unit-test unit-test: @@ -71,13 +71,13 @@ clean-package: .PHONY: gen-requirements gen-requirements: @echo "Generating requirements.txt from Pipfile.lock..." - python -m pipenv requirements --hash > requirements.txt + python -m pipenv requirements > requirements.txt .PHONY: install-deps install-deps: @echo "Installing dependencies to package/ directory..." mkdir -p package - python -m pipenv run pip install -r requirements.txt --target ./package --upgrade + docker run --rm -v "$$PWD":/var/task "public.ecr.aws/sam/build-python3.12" /bin/sh -c "pip install -r requirements.txt -t ./package" .PHONY: package-lambda package-lambda: @@ -96,9 +96,20 @@ ENV ?= sandbox .PHONY: deploy deploy: @echo "Deploying to environment: $(ENV)" - python -m pipenv run python aws_actions.py --env $(ENV) --action deploy + python -m pipenv run python deploy.py --env $(ENV) -.PHONY: test-lambda -test-lambda: - @echo "Invoking the latest version of the lambda: $(ENV)" - python -m pipenv run python aws_actions.py --env $(ENV) --action test \ No newline at end of file +.PHONY: direct-chat-completion +direct-chat-completion: + python -m pipenv run python -m api.domain.openai_predict + +.PHONY: print-client-settings-vars +print-client-settings-vars: + python -m pipenv run python -m tests.helpers.settings + +.PHONY: live-client +live-client: + python -m pipenv run python -m tests.helpers.client + +.PHONY: test-live +test-live: + python -m pipenv run python -m pytest tests -m live --env $(ENV) diff --git a/opentrons-ai-server/Pipfile b/opentrons-ai-server/Pipfile index 5e57f79f609..799dafad4e7 100644 --- a/opentrons-ai-server/Pipfile +++ b/opentrons-ai-server/Pipfile @@ -18,6 +18,8 @@ aws-lambda-powertools = {extras = ["all"], version = "==2.37.0"} boto3 = "==1.34.97" boto3-stubs = "==1.34.97" rich = "==13.7.1" +pyjwt = "==2.8.0" +cryptography = "==42.0.7" [requires] python_version = "3.12" diff --git a/opentrons-ai-server/Pipfile.lock b/opentrons-ai-server/Pipfile.lock index 3b68da2070c..622e8e712f7 100644 --- a/opentrons-ai-server/Pipfile.lock +++ b/opentrons-ai-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6ceabeaac3e8b1643a130540d4c963c7c24eff2a0619c278fc846c6b0d2c6db9" + "sha256": "877bcf8baeeaf2e289d894271882fdb23dc51fecc25fcd9d033500e416238188" }, "pipfile-spec": 6, "requires": { @@ -286,11 +286,11 @@ }, "botocore": { "hashes": [ - "sha256:4cee65df02f4b0be08ad1401965cc89efafebc50ef0727d2d17083c7f1ed2831", - "sha256:631c0031d8ce922b5752ab395ead896a0281b0dc74745a754d0351a27c5d83de" + "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431", + "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939" ], "markers": "python_version >= '3.8'", - "version": "==1.34.98" + "version": "==1.34.103" }, "botocore-stubs": { "hashes": [ @@ -300,6 +300,64 @@ "markers": "python_version >= '3.8' and python_version < '4.0'", "version": "==1.34.94" }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" + }, "click": { "hashes": [ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", @@ -308,6 +366,45 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "cryptography": { + "hashes": [ + "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", + "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", + "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", + "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", + "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", + "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", + "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", + "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", + "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", + "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", + "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", + "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", + "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", + "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", + "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", + "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", + "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", + "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", + "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", + "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", + "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", + "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", + "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", + "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", + "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", + "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", + "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", + "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", + "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", + "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", + "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", + "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==42.0.7" + }, "fastjsonschema": { "hashes": [ "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0", @@ -421,6 +518,14 @@ "markers": "python_version >= '3.8'", "version": "==1.5.0" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, "pydantic": { "hashes": [ "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5", @@ -437,6 +542,15 @@ "markers": "python_version >= '3.8'", "version": "==2.18.0" }, + "pyjwt": { + "hashes": [ + "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", + "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.8.0" + }, "pytest": { "hashes": [ "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", diff --git a/opentrons-ai-server/README.md b/opentrons-ai-server/README.md index a1f14e9e81a..9db5606ff40 100644 --- a/opentrons-ai-server/README.md +++ b/opentrons-ai-server/README.md @@ -18,7 +18,7 @@ The Opentrons AI application's server. 1. This allows formatting of of `.md` and `.json` files 1. select the python version `pyenv local 3.12.3` 1. This will create a `.python-version` file in this directory -1. select the node version `nvs` currently 18.19\* +1. select the node version with `nvs` or `nvm` currently 18.19\* 1. Install pipenv and python dependencies `make setup` ## Install a dev dependency @@ -31,11 +31,11 @@ The Opentrons AI application's server. ## Stack and structure -### Lambda Pattern +### Tools - [powertools](https://powertools.aws.dev/) -- [reinvent talk for the pattern](https://www.youtube.com/watch?v=52W3Qyg242Y) -- [for creating docs](https://www.ranthebuilder.cloud/post/serverless-open-api-documentation-with-aws-powertools) +- [pytest]: https://docs.pytest.org/en/ +- [openai python api library]: https://pypi.org/project/openai/ ### Lambda Code Organizations and Separation of Concerns @@ -46,10 +46,13 @@ The Opentrons AI application's server. - integration - the integration with other services -[pytest]: https://docs.pytest.org/en/ -[openai python api library]: https://pypi.org/project/openai/ +## Dev process -## Deploy +1. Make your changes +1. Fix what can be automatically then lent and unit test like CI will `make pre-commit` +1. `make pre-commit` passes +1. deploy to sandbox `make build deploy test-live ENV=sandbox AWS_PROFILE=the-profile` -1. build the package `make build` -1. deploy the package `make deploy ENV=sandbox AWS_PROFILE=robotics_ai_sandbox` +## TODO + +- llama-index is gigantic. Have to figure out how to get it in the lambda diff --git a/opentrons-ai-server/api/domain/openai_predict.py b/opentrons-ai-server/api/domain/openai_predict.py new file mode 100644 index 00000000000..9ae013e6912 --- /dev/null +++ b/opentrons-ai-server/api/domain/openai_predict.py @@ -0,0 +1,55 @@ +from typing import List + +from openai import OpenAI +from openai.types.chat import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageParam + +from api.domain.prompts import system_notes +from api.settings import Settings, is_running_on_lambda + + +class OpenAIPredict: + def __init__(self, settings: Settings) -> None: + self.settings: Settings = settings + self.client: OpenAI = OpenAI(api_key=settings.openai_api_key.get_secret_value()) + + def predict(self, prompt: str, chat_completion_message_params: List[ChatCompletionMessageParam] | None = None) -> None | str: + """The simplest chat completion from the OpenAI API""" + top_p = 0.0 + messages: List[ChatCompletionMessageParam] = [{"role": "system", "content": system_notes}] + if chat_completion_message_params: + messages += chat_completion_message_params + + user_message: ChatCompletionMessageParam = {"role": "user", "content": f"QUESTION/DESCRIPTION: \n{prompt}\n\n"} + messages.append(user_message) + + response: ChatCompletion = self.client.chat.completions.create( + model=self.settings.OPENAI_MODEL_NAME, + messages=messages, + stream=False, + temperature=0.005, + max_tokens=4000, + top_p=top_p, + frequency_penalty=0, + presence_penalty=0, + ) + + assistant_message: ChatCompletionMessage = response.choices[0].message + return assistant_message.content + + +def main() -> None: + """Intended for testing this class locally.""" + if is_running_on_lambda(): + return + from rich import print + from rich.prompt import Prompt + + settings = Settings.build() + openai = OpenAIPredict(settings) + prompt = Prompt.ask("Type a prompt to send to the OpenAI API:") + completion = openai.predict(prompt) + print(completion) + + +if __name__ == "__main__": + main() diff --git a/opentrons-ai-server/api/domain/prompts.py b/opentrons-ai-server/api/domain/prompts.py new file mode 100644 index 00000000000..f71fef0d188 --- /dev/null +++ b/opentrons-ai-server/api/domain/prompts.py @@ -0,0 +1,37 @@ +system_notes = """\ +You are an expert at generating a protocol based on Opentrons Python API v2. +You will be shown the user's question/description and information related to +the Opentrons Python API v2 documentation. And you respond the user's question/description +using only this information. + +INSTRUCTIONS: + +1) All types of protocols are based on apiLevel 2.15, + thus prepend the following code block +`metadata` and `requirements`: +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': '[protocol name by user]', + 'author': '[user name]', + 'description': "[what is the protocol about]" +} +requirements = {"robotType": "[Robot type]", "apiLevel": "2.15"} +``` + +2) See the transfer rules <> below. +3) Learn examples see <> + +4) Inside `run` function, according to the description generate the following in order: +- modules +- adapter +- labware +- pipettes +Note that sometimes API names is very long eg., +`Opentrons 96 Flat Bottom Adapter with NEST 96 Well Plate 200 uL Flat` + + +5) If the pipette is multi-channel eg., P20 Multi-Channel Gen2, please use `columns` method. +\n\n\ +""" diff --git a/opentrons-ai-server/api/domain/todo.py b/opentrons-ai-server/api/domain/todo.py deleted file mode 100644 index fb5da240a32..00000000000 --- a/opentrons-ai-server/api/domain/todo.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Any, List - -from httpx import Response - -from api.integration.jsonplaceholder import JSONPlaceholder -from api.models.todo import Todo, Todos - - -def handle_todos_response(response: Response) -> None | Any: - if response.status_code == 200: - return response.json() - return None - - -def map_todos(todos: List[dict[Any, Any]]) -> Todos: - # limit to 5 - return Todos([Todo(**todo) for todo in todos][:5]) - - -def retrieve_todos() -> Todos: - response = JSONPlaceholder().get_todos() - todos = handle_todos_response(response) - return map_todos(todos) if todos else Todos([]) diff --git a/opentrons-ai-server/api/handler/function.py b/opentrons-ai-server/api/handler/function.py index 9557f1314c7..f08a87b4f79 100644 --- a/opentrons-ai-server/api/handler/function.py +++ b/opentrons-ai-server/api/handler/function.py @@ -3,47 +3,71 @@ from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response, content_types -from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEventV2 from aws_lambda_powertools.utilities.typing import LambdaContext -from api.domain.todo import retrieve_todos -from api.models.todo import Todos +from api.domain.openai_predict import OpenAIPredict +from api.models.chat_request import ChatRequest +from api.models.chat_response import ChatResponse +from api.models.empty_request_error import EmptyRequestError +from api.models.internal_server_error import InternalServerError from api.settings import Settings -settings: Settings = Settings() -tracer: Tracer = Tracer(service=settings.service_name) -logger: Logger = Logger(service=settings.service_name) +# Shared resources of the function +service_name = Settings.get_service_name() +tracer: Tracer = Tracer(service=service_name) +logger: Logger = Logger(service=service_name) app: APIGatewayHttpResolver = APIGatewayHttpResolver() -@app.get("/todos") # type: ignore[misc] +# named in the same pattern as openai https://platform.openai.com/docs/api-reference/chat +@app.post("/chat/completion") # type: ignore[misc] @tracer.capture_method -def get_todos() -> Response[Todos] | Response[dict[str, str]]: - logger.info("GET todos") - logger.info(app.current_event) +def create_chat_completion() -> Response[ChatResponse] | Response[InternalServerError] | Response[EmptyRequestError]: + logger.info("POST /chat/completion app.current_event", extra=app.current_event) try: - todos: Todos = retrieve_todos() - if len(todos) == 0: - return Response(status_code=HTTPStatus.NOT_FOUND, content_type=content_types.APPLICATION_JSON, body=todos) - return Response(status_code=HTTPStatus.FOUND, content_type=content_types.APPLICATION_JSON, body=todos) + if app.current_event.body is None: + return Response( + status_code=HTTPStatus.BAD_REQUEST, + content_type=content_types.APPLICATION_JSON, + body=EmptyRequestError(message="Request body is empty"), + ) + body: ChatRequest = ChatRequest.parse_raw(app.current_event.body) + if body.fake: + return Response( + status_code=HTTPStatus.OK, + content_type=content_types.APPLICATION_JSON, + body=ChatResponse(reply="Fake response", fake=body.fake), + ) + settings: Settings = Settings.build() + openai: OpenAIPredict = OpenAIPredict(settings=settings) + response = openai.predict(prompt=body.message) + if response is None or response == "": + return Response( + status_code=HTTPStatus.NO_CONTENT, + content_type=content_types.APPLICATION_JSON, + body=ChatResponse(reply="No response was generated", fake=body.fake), + ) + return Response( + status_code=HTTPStatus.OK, + content_type=content_types.APPLICATION_JSON, + body=ChatResponse(reply=response, fake=body.fake), + ) except Exception as e: logger.exception(e) return Response( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content_type=content_types.APPLICATION_JSON, - body={"error": str(e), "message": "An error occurred while retrieving todos"}, + body=InternalServerError(exception_object=e), ) @app.get("/health") # type: ignore[misc] @tracer.capture_method def get_health() -> Response[Any]: - logger.info("GET health") + logger.info("GET /health app.current_event", extra=app.current_event) return Response(status_code=HTTPStatus.OK, content_type=content_types.APPLICATION_JSON, body={"version": "0.0.1"}) -# You can continue to use other utilities just as before -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP, log_event=True) -@tracer.capture_lambda_handler -def handler(event: dict[Any, Any], context: LambdaContext) -> dict[Any, Any]: +def handler(event: APIGatewayProxyEventV2, context: LambdaContext) -> dict[Any, Any]: return app.resolve(event, context) diff --git a/opentrons-ai-server/api/integration/aws_secrets_manager.py b/opentrons-ai-server/api/integration/aws_secrets_manager.py new file mode 100644 index 00000000000..a726eaeac0f --- /dev/null +++ b/opentrons-ai-server/api/integration/aws_secrets_manager.py @@ -0,0 +1,9 @@ +import boto3 +from pydantic import SecretStr + + +def fetch_secret(secret_name: str) -> SecretStr: + """Fetch a secret using Boto3.""" + client = boto3.client("secretsmanager") + response = client.get_secret_value(SecretId=secret_name) + return SecretStr(response["SecretString"]) diff --git a/opentrons-ai-server/api/integration/jsonplaceholder.py b/opentrons-ai-server/api/integration/jsonplaceholder.py deleted file mode 100644 index 201f99a5e98..00000000000 --- a/opentrons-ai-server/api/integration/jsonplaceholder.py +++ /dev/null @@ -1,12 +0,0 @@ -from httpx import Client, Response, Timeout - -from api.settings import Settings - - -class JSONPlaceholder: - def __init__(self) -> None: - self.settings: Settings = Settings() - self.client = Client(base_url=self.settings.typicode_base_url, timeout=Timeout(connect=5.0, read=10.0, write=10.0, pool=5.0)) - - def get_todos(self) -> Response: - return self.client.get("/todos") diff --git a/opentrons-ai-server/api/models/__init__.py b/opentrons-ai-server/api/models/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentrons-ai-server/api/models/chat_request.py b/opentrons-ai-server/api/models/chat_request.py new file mode 100644 index 00000000000..77d714b234d --- /dev/null +++ b/opentrons-ai-server/api/models/chat_request.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class ChatRequest(BaseModel): + message: str + fake: bool diff --git a/opentrons-ai-server/api/models/chat_response.py b/opentrons-ai-server/api/models/chat_response.py new file mode 100644 index 00000000000..d5a9609d0b1 --- /dev/null +++ b/opentrons-ai-server/api/models/chat_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class ChatResponse(BaseModel): + reply: str + fake: bool diff --git a/opentrons-ai-server/api/models/empty_request_error.py b/opentrons-ai-server/api/models/empty_request_error.py new file mode 100644 index 00000000000..783b82cdf3c --- /dev/null +++ b/opentrons-ai-server/api/models/empty_request_error.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class EmptyRequestError(BaseModel): + error: str = "Empty request" + message: str diff --git a/opentrons-ai-server/api/models/internal_server_error.py b/opentrons-ai-server/api/models/internal_server_error.py index c925f87afd7..d8ca81ada83 100644 --- a/opentrons-ai-server/api/models/internal_server_error.py +++ b/opentrons-ai-server/api/models/internal_server_error.py @@ -1,7 +1,9 @@ -from typing import Annotated +from pydantic import BaseModel -from pydantic import BaseModel, Field +class InternalServerError(BaseModel): + message: str = "Internal server error" + exception_object: Exception -class InternalServerErrorOutput(BaseModel): - error: Annotated[str, Field(description="Error description")] = "internal server error" + class Config: + arbitrary_types_allowed = True diff --git a/opentrons-ai-server/api/models/todo.py b/opentrons-ai-server/api/models/todo.py deleted file mode 100644 index 31b22b180c7..00000000000 --- a/opentrons-ai-server/api/models/todo.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import List - -from pydantic import BaseModel - - -class Todo(BaseModel): - userId: int - id: int - title: str - completed: bool - - -class Todos(List[Todo]): - pass diff --git a/opentrons-ai-server/api/settings.py b/opentrons-ai-server/api/settings.py index c1cf492d94a..ae6a28eb797 100644 --- a/opentrons-ai-server/api/settings.py +++ b/opentrons-ai-server/api/settings.py @@ -1,63 +1,78 @@ import os +from dataclasses import asdict, dataclass from pathlib import Path +from typing import Type -import boto3 from dotenv import load_dotenv -from pydantic import ( - SecretStr, -) +from pydantic import SecretStr + +from api.integration.aws_secrets_manager import fetch_secret ENV_PATH: Path = Path(Path(__file__).parent.parent, ".env") +def is_running_on_lambda() -> bool: + """Check if the script is running on AWS Lambda.""" + return "AWS_LAMBDA_FUNCTION_NAME" in os.environ + + +@dataclass(frozen=True) class Settings: - def __init__(self) -> None: + HUGGINGFACE_SIMULATE_ENDPOINT: str + LOG_LEVEL: str + SERVICE_NAME: str + ENVIRONMENT: str + OPENAI_MODEL_NAME: str + openai_api_key: SecretStr + huggingface_api_key: SecretStr + + @classmethod + def build(cls: Type["Settings"]) -> "Settings": # Load environment variables from .env file if it exists - # These map to the the environment variables defined and set in terraform - # These may also be set with some future need during lambda version creation load_dotenv(ENV_PATH) - self.typicode_base_url: str = os.getenv("TYPICODE_BASE_URL", "https://jsonplaceholder.typicode.com") - self.openai_base_url: str = os.getenv("OPENAI_BASE_URL", "https://api.openai.com") - self.huggingface_base_url: str = os.getenv("HUGGINGFACE_BASE_URL", "https://api-inference.huggingface.co") - self.log_level: str = os.getenv("LOG_LEVEL", "debug") - self.service_name: str = os.getenv("SERVICE_NAME", "local-ai-api") - self.environment: str = os.getenv("ENVIRONMENT", "local") + + environment = os.getenv("ENVIRONMENT", "local") + service_name = os.getenv("SERVICE_NAME", "local-ai-api") + openai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4-1106-preview") + huggingface_simulate_endpoint = os.getenv("HUGGINGFACE_SIMULATE_ENDPOINT", "https://Opentrons-simulator.hf.space/protocol") + log_level = os.getenv("LOG_LEVEL", "debug") + if is_running_on_lambda(): - # Fetch secrets from AWS Secrets manager using AWS Lambda Powertools - self.openai_api_key: SecretStr = self.fetch_secret(f"{self.environment}-openai-api-key") - self.huggingface_api_key: SecretStr = self.fetch_secret(f"{self.environment}-huggingface-api-key") + openai_api_key = fetch_secret(f"{environment}-openai-api-key") + huggingface_api_key = fetch_secret(f"{environment}-huggingface-api-key") else: - # Use values from .env or defaults if not set - self.openai_api_key = SecretStr(os.getenv("OPENAI_API_KEY", "default-openai-secret")) # can change to throw - self.huggingface_api_key = SecretStr(os.getenv("HUGGINGFACE_API_KEY", "default-huggingface-secret")) # can change to throw + openai_api_key = SecretStr(os.getenv("OPENAI_API_KEY", "")) + huggingface_api_key = SecretStr(os.getenv("HUGGINGFACE_API_KEY", "")) + + return cls( + HUGGINGFACE_SIMULATE_ENDPOINT=huggingface_simulate_endpoint, + LOG_LEVEL=log_level, + SERVICE_NAME=service_name, + ENVIRONMENT=environment, + OPENAI_MODEL_NAME=openai_model_name, + openai_api_key=openai_api_key, + huggingface_api_key=huggingface_api_key, + ) @staticmethod - def fetch_secret(secret_name: str) -> SecretStr: - """Fetch a secret using Boto3.""" - client = boto3.client("secretsmanager") - response = client.get_secret_value(SecretId=secret_name) - return SecretStr(response["SecretString"]) + def get_service_name() -> str: + return os.getenv("SERVICE_NAME", "local-ai-api") def generate_env_file(settings: Settings) -> None: """ Generates a .env file from the current settings including defaults. """ + with open(ENV_PATH, "w") as file: - for field, value in vars(settings).items(): - # Ensure we handle secret types appropriately - value = value.get_secret_value() if isinstance(value, SecretStr) else value + for field, value in asdict(settings).items(): if value is not None: file.write(f"{field.upper()}={value}\n") - print(f".env file generated at {str(ENV_PATH)}") - -def is_running_on_lambda() -> bool: - """Check if the script is running on AWS Lambda.""" - return "AWS_LAMBDA_FUNCTION_NAME" in os.environ + print(f".env file generated at {str(ENV_PATH)}") # Example usage if __name__ == "__main__": - config = Settings() + config: Settings = Settings.build() generate_env_file(config) diff --git a/opentrons-ai-server/aws_actions.py b/opentrons-ai-server/deploy.py similarity index 72% rename from opentrons-ai-server/aws_actions.py rename to opentrons-ai-server/deploy.py index 78ff96fc91d..6676186bca3 100644 --- a/opentrons-ai-server/aws_actions.py +++ b/opentrons-ai-server/deploy.py @@ -1,5 +1,4 @@ import argparse -import json import time from dataclasses import dataclass from pathlib import Path @@ -13,7 +12,6 @@ install() ENVIRONMENTS = ["sandbox", "dev"] -ACTIONS = ["deploy", "test"] @dataclass(frozen=True) @@ -38,7 +36,7 @@ class DevDeploymentConfig(BaseDeploymentConfig): FUNCTION_NAME: str = "dev-api-function" -class AWSActions: +class Deploy: def __init__(self, config: SandboxDeploymentConfig | DevDeploymentConfig) -> None: self.config: SandboxDeploymentConfig | DevDeploymentConfig = config self.lambda_client = boto3.client("lambda") @@ -92,52 +90,27 @@ def wait_for_lambda_status(self, version: str) -> None: print("Status still 'Pending'. Checking again in 3 seconds...") time.sleep(3) # Wait for 3 seconds before checking again - def invoke_lambda(self, version: str) -> None: - """Invoke the updated Lambda function.""" - with open(self.config.HEALTH_EVENT, "r") as f: - event = json.load(f) - function_with_version = f"{self.config.FUNCTION_NAME}:{version}" - print(f"Invoking Lambda function: {function_with_version}") - response = self.lambda_client.invoke(FunctionName=function_with_version, Payload=json.dumps(event)) - print("Invoked Lambda function response:") - print(response) - print("Payload:") - print(response["Payload"].read().decode()) - - def invoke_latest_lambda(self) -> None: - """Invoke the latest version of the Lambda function.""" - with open(self.config.HEALTH_EVENT, "r") as f: - event = json.load(f) - response = self.lambda_client.invoke(FunctionName=self.config.FUNCTION_NAME, Payload=json.dumps(event)) - print("Invoked Lambda function response:") - print(response) - print("Payload:") - print(response["Payload"].read().decode()) - def deploy(self) -> None: self.upload_to_s3() version = self.update_lambda() if version: self.wait_for_lambda_status(version) - self.invoke_lambda(version) def main() -> None: - parser = argparse.ArgumentParser(description="Manage Lambda deployment or testing.") + parser = argparse.ArgumentParser(description="Manage Lambda deployment.") parser.add_argument("--env", type=str, help=f"Deployment environment {ENVIRONMENTS}") - parser.add_argument( - "--action", type=str, choices=ACTIONS, default="test", help=f"Choose action to perform: {ACTIONS} (default is test)" - ) args = parser.parse_args() # Determine if the script was called with command-line arguments - if args.env and args.action: + if args.env: + if args.env.lower() not in ENVIRONMENTS: + print(f"[red]Invalid environment specified: {args.env}[/red]") + exit(1) env = args.env.lower() - action = args.action.lower() else: # Interactive prompts if no command-line arguments env = Prompt.ask("[bold magenta]Enter the deployment environment[/]", choices=ENVIRONMENTS, default="sandbox") - action = Prompt.ask("[bold cyan]Choose the action[/]", choices=ACTIONS, default="test") # Validate environment config: SandboxDeploymentConfig | DevDeploymentConfig @@ -148,16 +121,8 @@ def main() -> None: else: print(f"[red]Invalid environment specified: {env}[/red]") exit(1) - - print(f"[green]Environment: {env}[/]") - print(f"[green]Action: {action}[/]") - - if action == "deploy": - aws_actions = AWSActions(config) - aws_actions.deploy() - elif action == "test": - aws_actions = AWSActions(config) - aws_actions.invoke_latest_lambda() + aws_actions = Deploy(config) + aws_actions.deploy() if __name__ == "__main__": diff --git a/opentrons-ai-server/pytest.ini b/opentrons-ai-server/pytest.ini index 110114c9c5c..bca574b8aff 100644 --- a/opentrons-ai-server/pytest.ini +++ b/opentrons-ai-server/pytest.ini @@ -1,4 +1,5 @@ [pytest] addopts = -s -vv --log-cli-level info markers = - unit: marks tests as unit tests (select with '-m unit') \ No newline at end of file + unit: marks tests as unit tests (select with '-m unit') + live: marks tests as live tests (select with '-m live') diff --git a/opentrons-ai-server/test_events/health.json b/opentrons-ai-server/test_events/health.json deleted file mode 100644 index e464bc1f6b1..00000000000 --- a/opentrons-ai-server/test_events/health.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "version": "2.0", - "routeKey": "$default", - "rawPath": "/health", - "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", - "cookies": ["cookie1", "cookie2"], - "headers": { - "Header1": "value1", - "Header2": "value1,value2" - }, - "queryStringParameters": { - "parameter1": "value1,value2", - "parameter2": "value" - }, - "requestContext": { - "accountId": "123456789012", - "apiId": "api-id", - "authentication": { - "clientCert": { - "clientCertPem": "CERT_CONTENT", - "subjectDN": "www.example.com", - "issuerDN": "Example issuer", - "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", - "validity": { - "notBefore": "May 28 12:30:02 2019 GMT", - "notAfter": "Aug 5 09:36:04 2021 GMT" - } - } - }, - "authorizer": { - "jwt": { - "claims": { - "claim1": "value1", - "claim2": "value2" - }, - "scopes": ["scope1", "scope2"] - } - }, - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "http": { - "method": "GET", - "path": "/health", - "protocol": "HTTP/1.1", - "sourceIp": "192.168.0.1/32", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 - }, - "body": "eyJ0ZXN0IjoiYm9keSJ9", - "pathParameters": { - "parameter1": "value1" - }, - "isBase64Encoded": true, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } -} diff --git a/opentrons-ai-server/tests/conftest.py b/opentrons-ai-server/tests/conftest.py new file mode 100644 index 00000000000..74d594f911f --- /dev/null +++ b/opentrons-ai-server/tests/conftest.py @@ -0,0 +1,26 @@ +from typing import Generator + +import pytest + +from tests.helpers.client import Client +from tests.helpers.settings import get_settings + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Add an option to pytest command-line parser to specify the environment.""" + parser.addoption("--env", action="store", default="sandbox", help="Set the environment for the tests (dev, sandbox, staging, prod)") + + +@pytest.fixture(scope="session") +def env(request: pytest.FixtureRequest) -> str: + """A fixture to access the environment argument value.""" + return str(request.config.getoption("--env")) + + +@pytest.fixture(scope="session") +def client(env: str) -> Generator[Client, None, None]: + """Fixture to initialize and tear down the client for API interaction.""" + settings = get_settings(env=env) + client = Client(settings) + yield client + client.close() diff --git a/opentrons-ai-server/tests/helpers/__init__.py b/opentrons-ai-server/tests/helpers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentrons-ai-server/tests/helpers/client.py b/opentrons-ai-server/tests/helpers/client.py new file mode 100644 index 00000000000..1ba22df9b1a --- /dev/null +++ b/opentrons-ai-server/tests/helpers/client.py @@ -0,0 +1,90 @@ +from api.models.chat_request import ChatRequest +from httpx import Client as HttpxClient +from httpx import Response, Timeout +from rich import inspect +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Prompt + +from tests.helpers.settings import Settings, get_settings +from tests.helpers.token import Token + + +class Client: + def __init__(self, settings: Settings): + self.settings = settings + self.token = Token(self.settings) + self.auth_headers = self.get_auth_headers() + self.invalid_auth_headers = self.get_auth_headers("bad_token") + self.type_headers = {"Content-Type": "application/json"} + self.standard_headers = { + **self.type_headers, + **self.auth_headers, + } + self.timeout = Timeout(connect=5.0, read=120.0, write=120.0, pool=5.0) + self.httpx = HttpxClient(base_url=self.settings.BASE_URL, timeout=self.timeout) + + def close(self) -> None: + """Closes the HTTPX client instance.""" + self.httpx.close() + + def get_auth_headers(self, token_override: str | None = None) -> dict[str, str]: + if token_override: + return {"Authorization": f"Bearer {token_override}"} + return {"Authorization": f"Bearer {self.token.value}"} + + def get_health(self) -> Response: + """Call the /health endpoint and return the response.""" + return self.httpx.get("/health", headers=self.type_headers) + + def get_chat_completion(self, message: str, fake: bool = True, bad_auth: bool = False) -> Response: + """Call the /chat/completion endpoint and return the response.""" + request = ChatRequest(message=message, fake=fake) + headers = self.standard_headers if not bad_auth else self.invalid_auth_headers + return self.httpx.post("/chat/completion", headers=headers, json=request.model_dump()) + + def get_bad_endpoint(self, bad_auth: bool = False) -> Response: + """Call nonexistent endpoint and return the response.""" + headers = self.standard_headers if not bad_auth else self.invalid_auth_headers + return self.httpx.get( + "/chat/idontexist", + headers=headers, + ) + + +def print_response(response: Response) -> None: + """Prints the HTTP response using rich.""" + console = Console() + console.print(Panel("Response", expand=False)) + inspect(response) + + +def main() -> None: + console = Console() + env = Prompt.ask("Select environment", choices=["dev", "sandbox"], default="sandbox") + settings = get_settings(env=env) + client = Client(settings) + try: + console.print(Panel("Getting health endpoint", expand=False)) + response = client.get_health() + print_response(response) + + console.print(Panel("Getting chat completion with fake=True and good auth (won't call OpenAI)", expand=False)) + response = client.get_chat_completion("How do I load a pipette?") + print_response(response) + + console.print(Panel("Getting chat completion with fake=True and bad auth to show 401 error (won't call OpenAI)", expand=False)) + response = client.get_chat_completion("How do I load a pipette?", bad_auth=True) + print_response(response) + + real = Prompt.ask("Actually call OpenAI API?", choices=["y", "n"], default="n") + if real == "y": + message = Prompt.ask("Enter a message") + response = client.get_chat_completion(message, fake=False) + print_response(response) + finally: + client.close() + + +if __name__ == "__main__": + main() diff --git a/opentrons-ai-server/tests/helpers/settings.py b/opentrons-ai-server/tests/helpers/settings.py new file mode 100644 index 00000000000..b87947c570e --- /dev/null +++ b/opentrons-ai-server/tests/helpers/settings.py @@ -0,0 +1,94 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + + +class Settings: + # One env file for all environments + ENV_PATH: Path = Path(Path(__file__).parent, "test.env") + ENV_VARIABLE_MAP: dict[str, str] = {} + TOKEN_URL: str + BASE_URL: str + CLIENT_ID: str + SECRET: str + AUDIENCE: str + GRANT_TYPE: str + CACHED_TOKEN_PATH: str + # Dynamic properties hard coded or computed + excluded: list[str] = ["CACHED_TOKEN_PATH"] + + def _set_properties(self) -> None: + for key, env_var in self.ENV_VARIABLE_MAP.items(): + if key in self.excluded: + setattr(self, key, env_var) + continue + value = self._get_required_env(env_var) + setattr(self, key, value) + + def _get_required_env(self, var_name: str) -> str: + """Retrieve a required environment variable or raise an error if not found.""" + try: + return os.environ[var_name] + except KeyError as err: + raise EnvironmentError(f"Required environment variable '{var_name}' is not set.") from err + + +class DevSettings(Settings): + ENV_VARIABLE_MAP = { + "TOKEN_URL": "DEV_TOKEN_URL", + "BASE_URL": "DEV_BASE_URL", + "CLIENT_ID": "DEV_CLIENT_ID", + "SECRET": "DEV_SECRET", + "AUDIENCE": "DEV_AUDIENCE", + "GRANT_TYPE": "DEV_GRANT_TYPE", + "CACHED_TOKEN_PATH": str(Path(Path(__file__).parent, "cached_token.txt")), + } + + def __init__(self) -> None: + super().__init__() + load_dotenv(self.ENV_PATH) + self._set_properties() + + +class SandboxSettings(Settings): + ENV_VARIABLE_MAP = { + "TOKEN_URL": "SANDBOX_TOKEN_URL", + "BASE_URL": "SANDBOX_BASE_URL", + "CLIENT_ID": "SANDBOX_CLIENT_ID", + "SECRET": "SANDBOX_SECRET", + "AUDIENCE": "SANDBOX_AUDIENCE", + "GRANT_TYPE": "SANDBOX_GRANT_TYPE", + "CACHED_TOKEN_PATH": str(Path(Path(__file__).parent, "cached_token.txt")), + } + + def __init__(self) -> None: + super().__init__() + load_dotenv(self.ENV_PATH) + self._set_properties() + + +# TODO:y3rsh:2024-05-11: Add staging and prod + + +def get_settings(env: str) -> Settings: + if env.lower() == "dev": + return DevSettings() + elif env.lower() == "sandbox": + return SandboxSettings() + elif env.lower() == "staging": + raise NotImplementedError("Staging environment not implemented.") + elif env.lower() == "prod": + raise NotImplementedError("Production environment not implemented.") + else: + raise ValueError(f"Unsupported environment: {env}") + + +# Print the environment variable skeleton +# This is what you print when building the secret +if __name__ == "__main__": + for env in [SandboxSettings, DevSettings]: + for _var, name in env.ENV_VARIABLE_MAP.items(): + if _var in env.excluded: + continue + print(f"{name}=") diff --git a/opentrons-ai-server/tests/helpers/token.py b/opentrons-ai-server/tests/helpers/token.py new file mode 100644 index 00000000000..e47efa7313e --- /dev/null +++ b/opentrons-ai-server/tests/helpers/token.py @@ -0,0 +1,45 @@ +import os + +import httpx + +from tests.helpers.settings import Settings +from tests.helpers.token_verifier import TokenVerifier + + +class Token: + def __init__(self, settings: Settings, refresh: bool = False) -> None: + self.refresh: bool = refresh + self.settings: Settings = settings + self.value: str | None = None + self.token_verifier = TokenVerifier(self.settings) + self._set_token() + + def _read_secret(self) -> str: + """Read the client secret from a file.""" + with open(self.settings.CACHED_TOKEN_PATH, "r") as file: + return file.read().strip() + + def _set_token(self) -> None: + """Retrieve or refresh the authentication token.""" + if self._is_token_cached(): + self.value = self._read_secret() + if not self.value or self.refresh or not self.token_verifier.is_valid_token(self.value): + headers = {"Content-Type": "application/json"} + data = { + "client_id": self.settings.CLIENT_ID, + "client_secret": self.settings.SECRET, + "audience": self.settings.AUDIENCE, + "grant_type": self.settings.GRANT_TYPE, + } + with httpx.Client() as client: + response = client.post(self.settings.TOKEN_URL, headers=headers, json=data) + response.raise_for_status() # Raises exception for 4XX/5XX responses + token = response.json()["access_token"] + # cache the token + with open(self.settings.CACHED_TOKEN_PATH, "w") as file: + file.write(token) + self.value = token + + def _is_token_cached(self) -> bool: + """Check if the token is cached.""" + return os.path.exists(self.settings.CACHED_TOKEN_PATH) diff --git a/opentrons-ai-server/tests/helpers/token_verifier.py b/opentrons-ai-server/tests/helpers/token_verifier.py new file mode 100644 index 00000000000..c1b4e4aac54 --- /dev/null +++ b/opentrons-ai-server/tests/helpers/token_verifier.py @@ -0,0 +1,75 @@ +from base64 import urlsafe_b64decode +from typing import Any, Optional +from urllib.parse import urlparse + +import httpx +import jwt +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers +from jwt.exceptions import DecodeError, ExpiredSignatureError, InvalidTokenError +from rich import inspect, print + +from tests.helpers.settings import Settings + + +class TokenVerifier: + def __init__(self, settings: Settings) -> None: + self.settings: Settings = settings + + def _get_issuer(self) -> str: + parsed_url = urlparse(self.settings.TOKEN_URL) + return f"{parsed_url.scheme}://{parsed_url.netloc}/" + + def _ensure_bytes(self, value: str) -> str: + """Ensures the decoded Base64 values are correctly padded.""" + return value + "=" * (-len(value) % 4) + + def _fetch_jwks(self, jwks_url: str) -> Any: + """Fetches the JWKS using HTTPX.""" + with httpx.Client() as client: + response = client.get(jwks_url) + response.raise_for_status() + return response.json() + + def _decode_key(self, jwk: Any) -> str: + """Converts a JWK to a PEM formatted public key.""" + e = urlsafe_b64decode(self._ensure_bytes(jwk["e"])) + n = urlsafe_b64decode(self._ensure_bytes(jwk["n"])) + + public_numbers = RSAPublicNumbers(int.from_bytes(e, "big"), int.from_bytes(n, "big")) + public_key = public_numbers.public_key(default_backend()) + pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) + return pem.decode("utf-8") + + def _get_kid_from_jwt(self, token: str) -> Optional[str]: + """Extract the 'kid' from JWT header without verifying the token.""" + unverified_header = jwt.get_unverified_header(token) + return unverified_header.get("kid") + + def is_valid_token(self, token: str) -> bool: + """Check if the token is valid using the JWKS endpoint.""" + if not token: + return False + jwks_url = f"{self._get_issuer()}.well-known/jwks.json" + kid = self._get_kid_from_jwt(token) + jwks = self._fetch_jwks(jwks_url) + key = next((key for key in jwks["keys"] if key["kid"] == kid), None) + if key is None: + return False + + try: + decoded_token = jwt.decode( + token, + key=self._decode_key(key), + algorithms=["RS256"], + issuer=self._get_issuer(), + audience=self.settings.AUDIENCE, + options={"verify_signature": True}, + ) + print("Decoded token:") + inspect(decoded_token) + return True + except (DecodeError, ExpiredSignatureError, InvalidTokenError) as e: + print(f"JWT validation error: {str(e)}") + return False diff --git a/opentrons-ai-server/tests/test_chat_models.py b/opentrons-ai-server/tests/test_chat_models.py new file mode 100644 index 00000000000..4c5141cf13e --- /dev/null +++ b/opentrons-ai-server/tests/test_chat_models.py @@ -0,0 +1,32 @@ +import pytest +from api.models.chat_request import ChatRequest +from api.models.chat_response import ChatResponse +from pydantic import ValidationError + + +@pytest.mark.unit +def test_chat_request_model() -> None: + # Test valid data + request_data = {"message": "Hello", "fake": False} + request = ChatRequest(**request_data) + assert request.message == "Hello" + assert request.fake is False + + # Test invalid data + with pytest.raises(ValidationError): + invalid_request_data = {"message": 123, "fake": "true"} + ChatRequest(**invalid_request_data) + + +@pytest.mark.unit +def test_chat_response_model() -> None: + # Test valid data + response_data = {"reply": "Hi", "fake": True} + response = ChatResponse(**response_data) + assert response.reply == "Hi" + assert response.fake is True + + # Test invalid data + with pytest.raises(ValidationError): + invalid_response_data = {"reply": 123, "fake": "false"} + ChatResponse(**invalid_response_data) diff --git a/opentrons-ai-server/tests/test_live.py b/opentrons-ai-server/tests/test_live.py new file mode 100644 index 00000000000..e2e3930aaa7 --- /dev/null +++ b/opentrons-ai-server/tests/test_live.py @@ -0,0 +1,39 @@ +import pytest + +from tests.helpers.client import Client + + +@pytest.mark.live +def test_get_health(client: Client) -> None: + """Test to verify the health endpoint of the API.""" + response = client.get_health() + assert response.status_code == 200, "Health endpoint should return HTTP 200" + + +@pytest.mark.live +def test_get_chat_completion_good_auth(client: Client) -> None: + """Test the chat completion endpoint with good authentication.""" + response = client.get_chat_completion("How do I load tipracks for my 8 channel pipette on an OT2?", fake=True) + assert response.status_code == 200, "Chat completion with good auth should return HTTP 200" + + +@pytest.mark.live +def test_get_chat_completion_bad_auth(client: Client) -> None: + """Test the chat completion endpoint with bad authentication.""" + # This call never reaches the lambda function, the API Gateway rejects it + response = client.get_chat_completion("How do I load a pipette?", bad_auth=True) + assert response.status_code == 401, "Chat completion with bad auth should return HTTP 401" + + +@pytest.mark.live +def test_get_bad_endpoint_with_good_auth(client: Client) -> None: + """Test a nonexistent endpoint with good authentication.""" + response = client.get_bad_endpoint() + assert response.status_code == 404, "nonexistent endpoint with good auth should return HTTP 404" + + +@pytest.mark.live +def test_get_bad_endpoint_with_bad_auth(client: Client) -> None: + """Test a nonexistent endpoint with bad authentication.""" + response = client.get_bad_endpoint(bad_auth=True) + assert response.status_code == 401, "nonexistent endpoint with bad auth should return HTTP 401" diff --git a/opentrons-ai-server/tests/test_todos.py b/opentrons-ai-server/tests/test_todos.py deleted file mode 100644 index 4de564f6dca..00000000000 --- a/opentrons-ai-server/tests/test_todos.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest -from api.domain.todo import map_todos -from api.models.todo import Todo, Todos -from pydantic import ValidationError - - -@pytest.mark.parametrize( - "data", - [ - {"userId": "not-an-int", "id": 1, "title": "Test Todo", "completed": False}, # Invalid userId - {"userId": 1, "id": 1, "title": None, "completed": False}, # Null title - {"userId": 1, "id": 1, "title": "Test Todo", "completed": "nope"}, # Invalid completed - ], -) -@pytest.mark.unit -def test_todo_item_validation_errors(data) -> None: - with pytest.raises(ValidationError): - Todo(**data) - - -@pytest.mark.parametrize( - "todos, expected", - [ - # Test case 1: Empty list - ([], []), - # Test case 2: Single todo - ([{"id": 1, "title": "Todo 1", "completed": False, "userId": 101}], [Todo(id=1, title="Todo 1", completed=False, userId=101)]), - # Test case 3: Multiple todos - ( - [ - {"id": 1, "title": "Todo 1", "completed": False, "userId": 101}, - {"id": 2, "title": "Todo 2", "completed": True, "userId": 102}, - {"id": 3, "title": "Todo 3", "completed": False, "userId": 103}, - ], - [ - Todo(id=1, title="Todo 1", completed=False, userId=101), - Todo(id=2, title="Todo 2", completed=True, userId=102), - Todo(id=3, title="Todo 3", completed=False, userId=103), - ], - ), - ], -) -@pytest.mark.unit -def test_map_todos_parameterized(todos, expected) -> None: - todos: Todos = map_todos(todos) - assert todos == expected diff --git a/package.json b/package.json index 328a4a12e5e..d440ede1742 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "discovery-client", "labware-designer", "labware-library", + "opentrons-ai-client", "protocol-designer", "shared-data", "step-generation", @@ -106,7 +107,6 @@ "handlebars-loader": "^1.7.1", "html-webpack-plugin": "^3.2.0", "identity-obj-proxy": "^3.0.0", - "jotai": "2.8.0", "jsdom": "^16.4.0", "lost": "^8.3.1", "madge": "^3.6.0", diff --git a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx index 2cbb465c1fc..294960d7cde 100644 --- a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx @@ -1,8 +1,12 @@ import * as React from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { getModuleDisplayName } from '@opentrons/shared-data' import { + WASTE_CHUTE_CUTOUT, + getModuleDisplayName, +} from '@opentrons/shared-data' +import { + getAdditionalEquipmentEntities, getLabwareEntities, getModuleEntities, } from '../../../../step-forms/selectors' @@ -11,6 +15,7 @@ import { getUnoccupiedLabwareLocationOptions, } from '../../../../top-selectors/labware-locations' import { StepFormDropdown } from '../StepFormDropdownField' +import { getHasWasteChute } from '../../../labware' export function LabwareLocationField( props: Omit, 'options'> & { @@ -19,6 +24,9 @@ export function LabwareLocationField( ): JSX.Element { const { t } = useTranslation('form') const { labware, useGripper, value } = props + const additionalEquipmentEntities = useSelector( + getAdditionalEquipmentEntities + ) const labwareEntities = useSelector(getLabwareEntities) const robotState = useSelector(getRobotStateAtActiveItem) const moduleEntities = useSelector(getModuleEntities) @@ -34,6 +42,12 @@ export function LabwareLocationField( ) } + if (!useGripper && getHasWasteChute(additionalEquipmentEntities)) { + unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter( + option => option.value !== WASTE_CHUTE_CUTOUT + ) + } + const location: string = value as string const bothFieldsSelected = labware != null && value != null diff --git a/protocol-designer/src/components/lists/TitledStepList.tsx b/protocol-designer/src/components/lists/TitledStepList.tsx index bfcbc3987f2..3d66d0c03cc 100644 --- a/protocol-designer/src/components/lists/TitledStepList.tsx +++ b/protocol-designer/src/components/lists/TitledStepList.tsx @@ -88,7 +88,7 @@ export function TitledStepList(props: Props): JSX.Element { ) const multiSelectIconName = props.selected - ? 'checkbox-marked' + ? 'ot-checkbox' : 'checkbox-blank-outline' return ( diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 29210f5c52f..bcd2fd954a9 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -128,11 +128,11 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { iconInfo = ( ) } else if (showCheckbox && disabled) { @@ -163,7 +163,7 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { {...tempTargetProps} data-testid="EquipmentOption_upArrow" onClick={ - numMultiples === 7 + isDisabled || numMultiples === 7 ? undefined : () => { multiples.setValue(numMultiples + 1) @@ -176,7 +176,7 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { { multiples.setValue(numMultiples - 1) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx index af122437b84..ad73d6edb74 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx @@ -43,7 +43,10 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { // and a cutoutFixtureId so that we don't have to string parse here to generate them equipment.includes('stagingArea') ) - const unoccupiedStagingAreaSlots = getUnoccupiedStagingAreaSlots(modules) + const unoccupiedStagingAreaSlots = getUnoccupiedStagingAreaSlots( + modules, + additionalEquipment + ) const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( item => { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx index 1c367f59e4d..e2ec8a55ed1 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx @@ -67,9 +67,9 @@ describe('EquipmentOption', () => { } render(props) screen.getByText('mockText') - expect( - screen.getByLabelText('EquipmentOption_checkbox-marked') - ).toHaveStyle(`color: ${COLORS.blue50}`) + expect(screen.getByLabelText('EquipmentOption_ot-checkbox')).toHaveStyle( + `color: ${COLORS.blue50}` + ) expect(screen.getByLabelText('EquipmentOption_flex_mockText')).toHaveStyle( `border: ${BORDERS.activeLineBorder}` ) @@ -91,4 +91,35 @@ describe('EquipmentOption', () => { expect(props.multiples?.setValue).toHaveBeenCalled() screen.getByTestId('EquipmentOption_downArrow') }) + it('renders the equipment option with multiples allowed cta disabled from isDisabled', () => { + props = { + ...props, + multiples: { + numItems: 1, + maxItems: 4, + setValue: vi.fn(), + isDisabled: true, + }, + } + render(props) + fireEvent.click(screen.getByTestId('EquipmentOption_upArrow')) + expect(props.multiples?.setValue).not.toHaveBeenCalled() + }) + it('renders the equipment option with multiples allowed cta disabled from hitting max number', () => { + props = { + ...props, + multiples: { + numItems: 1, + maxItems: 7, + setValue: vi.fn(), + isDisabled: false, + }, + } + render(props) + screen.getByText('1') + for (let i = 1; i < 7; i++) { + fireEvent.click(screen.getByTestId('EquipmentOption_upArrow')) + } + expect(props.multiples?.setValue).toHaveBeenCalledTimes(6) + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index a8d59634e0b..7f4a2a7bf0d 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -31,25 +31,47 @@ let MOCK_FORM_STATE = { describe('getUnoccupiedStagingAreaSlots', () => { it('should return all staging area slots when there are no modules', () => { - const result = getUnoccupiedStagingAreaSlots(null) + const result = getUnoccupiedStagingAreaSlots(null, []) expect(result).toStrictEqual(STANDARD_EMPTY_SLOTS) }) - it('should return one staging area slot when there are modules in the way of the other slots', () => { - const result = getUnoccupiedStagingAreaSlots({ - 0: { model: 'magneticBlockV1', type: 'magneticBlockType', slot: 'A3' }, - 1: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'B3', - }, - 2: { - model: 'temperatureModuleV2', - type: 'temperatureModuleType', - slot: 'C3', + it('should return one staging area slot when there are only 1 num slots available', () => { + const result = getUnoccupiedStagingAreaSlots( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'D3', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B3', + }, + 4: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + 5: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A3', + }, }, - }) + [] + ) expect(result).toStrictEqual([ - { cutoutId: 'cutoutD3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, + { cutoutId: 'cutoutA3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, ]) }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index eb3f0985c20..56102e1e3c2 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -64,26 +64,6 @@ export const MOVABLE_TRASH_CUTOUTS = [ }, ] -export const getUnoccupiedStagingAreaSlots = ( - modules: FormState['modules'] -): DeckConfiguration => { - let unoccupiedSlots = STANDARD_EMPTY_SLOTS - const moduleCutoutIds = - modules != null - ? Object.values(modules).flatMap(module => - module.type === THERMOCYCLER_MODULE_TYPE - ? [`cutout${module.slot}`, 'cutoutA1'] - : `cutout${module.slot}` - ) - : [] - - unoccupiedSlots = unoccupiedSlots.filter(emptySlot => { - return !moduleCutoutIds.includes(emptySlot.cutoutId) - }) - - return unoccupiedSlots -} - const TOTAL_MODULE_SLOTS = 8 export const getNumSlotsAvailable = ( @@ -128,6 +108,19 @@ export const getNumSlotsAvailable = ( ) } +export const getUnoccupiedStagingAreaSlots = ( + modules: FormState['modules'], + additionalEquipment: FormState['additionalEquipment'] +): DeckConfiguration => { + const numSlotsAvailable = getNumSlotsAvailable(modules, additionalEquipment) + let unoccupiedSlots = STANDARD_EMPTY_SLOTS + + if (numSlotsAvailable < 4) { + unoccupiedSlots = STANDARD_EMPTY_SLOTS.slice(0, numSlotsAvailable) + } + return unoccupiedSlots +} + interface TrashOptionDisabledProps { trashType: 'trashBin' | 'wasteChute' additionalEquipment: AdditionalEquipment[] diff --git a/protocol-designer/src/components/steplist/MultiSelectToolbar/index.tsx b/protocol-designer/src/components/steplist/MultiSelectToolbar/index.tsx index bdadc543a53..90985278a80 100644 --- a/protocol-designer/src/components/steplist/MultiSelectToolbar/index.tsx +++ b/protocol-designer/src/components/steplist/MultiSelectToolbar/index.tsx @@ -159,7 +159,7 @@ export const MultiSelectToolbar = (props: Props): JSX.Element => { } = useConditionalConfirm(onDeleteClickAction, true) const selectProps: ClickableIconProps = { - iconName: isAllStepsSelected ? 'checkbox-marked' : 'minus-box', + iconName: isAllStepsSelected ? 'ot-checkbox' : 'minus-box', tooltipText: isAllStepsSelected ? 'Deselect All' : 'Select All', onClick: confirmSelect, } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 957decc8bd3..e07431bf188 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -96,6 +96,10 @@ }, "timeline": { "error": { + "LABWARE_DISCARDED_IN_WASTE_CHUTE": { + "title": "The labware has been previously discarded into the waste chute", + "body": "Please select a different labware to move." + }, "LABWARE_ON_ANOTHER_ENTITY": { "title": "Attempting to move a labware on top of another entity", "body": "Please reselect which slot your labware should move to." diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 20b04ecd0b5..1f321526c76 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -87,13 +87,24 @@ export const getMoveLabwareOptions: Selector = createSelector( stepFormSelectors.getInitialDeckSetup, stepFormSelectors.getSavedStepForms, stepFormSelectors.getAdditionalEquipmentEntities, + stepFormSelectors.getUnsavedForm, ( labwareEntities, nicknamesById, initialDeckSetup, savedStepForms, - additionalEquipmentEntities + additionalEquipmentEntities, + unsavedForm ) => { + const savedFormKeys = Object.keys(savedStepForms) + const previouslySavedFormDataIndex = unsavedForm + ? savedFormKeys.indexOf(unsavedForm.id) + : -1 + const filteredSavedStepFormIds = + previouslySavedFormDataIndex !== -1 + ? savedFormKeys.slice(0, previouslySavedFormDataIndex) + : savedFormKeys + const wasteChuteLocation = Object.values(additionalEquipmentEntities).find( aE => aE.name === 'wasteChute' )?.location @@ -104,12 +115,13 @@ export const getMoveLabwareOptions: Selector = createSelector( labwareEntity: LabwareEntity, labwareId: string ): Options => { - const isLabwareInWasteChute = Object.values(savedStepForms).find( - form => - form.stepType === 'moveLabware' && - form.labware === labwareId && - form.newLocation === wasteChuteLocation - ) + const isLabwareInWasteChute = + filteredSavedStepFormIds.find( + id => + savedStepForms[id].stepType === 'moveLabware' && + savedStepForms[id].labware === labwareId && + savedStepForms[id].newLocation === wasteChuteLocation + ) != null const isAdapter = labwareEntity.def.allowedRoles?.includes('adapter') ?? false diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 07c9a489a25..d711e4667e8 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -122,6 +122,10 @@ "detail": "Motor Driver Error", "category": "roboticsControlError" }, + "2017": { + "detail": "Liquid Not Found", + "category": "roboticsControlError" + }, "3000": { "detail": "A robotics interaction error occurred.", "category": "roboticsInteractionError" diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 61013b57e8f..e386cba455e 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -60,6 +60,7 @@ class ErrorCodes(Enum): EXECUTION_CANCELLED = _code_from_dict_entry("2014") FAILED_GRIPPER_PICKUP_ERROR = _code_from_dict_entry("2015") MOTOR_DRIVER_ERROR = _code_from_dict_entry("2016") + LIQUID_NOT_FOUND = _code_from_dict_entry("2017") ROBOTICS_INTERACTION_ERROR = _code_from_dict_entry("3000") LABWARE_DROPPED = _code_from_dict_entry("3001") LABWARE_NOT_PICKED_UP = _code_from_dict_entry("3002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 779c33464e7..499fcf3ac2d 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -611,6 +611,24 @@ def __init__( super().__init__(ErrorCodes.MOTOR_DRIVER_ERROR, message, detail, wrapping) +class LiquidNotFoundError(RoboticsControlError): + """Error raised if liquid sensing move completes without detecting liquid.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Initialize LiquidNotFoundError.""" + super().__init__( + ErrorCodes.LIQUID_NOT_FOUND, + message, + detail, + wrapping, + ) + + class LabwareDroppedError(RoboticsInteractionError): """An error indicating that the gripper dropped labware it was holding.""" diff --git a/step-generation/src/__tests__/moveLabware.test.ts b/step-generation/src/__tests__/moveLabware.test.ts index 0e380e49791..ddc8e6008de 100644 --- a/step-generation/src/__tests__/moveLabware.test.ts +++ b/step-generation/src/__tests__/moveLabware.test.ts @@ -129,6 +129,35 @@ describe('moveLabware', () => { type: 'LABWARE_ON_ANOTHER_ENTITY', }) }) + it('should return an error for the labware already being discarded in previous step', () => { + const wasteChuteInvariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, + mockWasteChuteId: { + name: 'wasteChute', + id: mockWasteChuteId, + location: WASTE_CHUTE_CUTOUT, + }, + }, + } as InvariantContext + + robotState.labware = { + [SOURCE_LABWARE]: { slot: 'gripperWasteChute' }, + } + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: true, + newLocation: { slotName: 'A1' }, + } as MoveLabwareArgs + + const result = moveLabware(params, wasteChuteInvariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'LABWARE_DISCARDED_IN_WASTE_CHUTE', + }) + }) it('should return an error for trying to move the labware off deck with a gripper', () => { const params = { commandCreatorFnName: 'moveLabware', diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 8f0b956cb32..904f95fb569 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -103,6 +103,10 @@ export const moveLabware: CommandCreator = ( } const initialLabwareSlot = prevRobotState.labware[labware]?.slot + + if (hasWasteChute && initialLabwareSlot === 'gripperWasteChute') { + errors.push(errorCreators.labwareDiscarded()) + } const initialAdapterSlot = prevRobotState.labware[initialLabwareSlot]?.slot const initialSlot = initialAdapterSlot != null ? initialAdapterSlot : initialLabwareSlot diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index 051b2b091b4..a541c65e4ea 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -269,3 +269,10 @@ export const noTipSelected = (): CommandCreatorError => { message: 'No tips were selected for this step', } } + +export const labwareDiscarded = (): CommandCreatorError => { + return { + type: 'LABWARE_DISCARDED_IN_WASTE_CHUTE', + message: 'The labware was discarded in waste chute in a previous step.', + } +} diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 153ef73f97f..ad1ea16df14 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -524,6 +524,7 @@ export type ErrorType = | 'HEATER_SHAKER_NORTH_SOUTH_EAST_WEST_SHAKING' | 'INSUFFICIENT_TIPS' | 'INVALID_SLOT' + | 'LABWARE_DISCARDED_IN_WASTE_CHUTE' | 'LABWARE_DOES_NOT_EXIST' | 'LABWARE_OFF_DECK' | 'LABWARE_ON_ANOTHER_ENTITY' diff --git a/test-data-generation/Makefile b/test-data-generation/Makefile index a4818b00ab1..1ce4889ab91 100644 --- a/test-data-generation/Makefile +++ b/test-data-generation/Makefile @@ -27,11 +27,18 @@ wheel: $(python) setup.py $(wheel_opts) bdist_wheel rm -rf build -.PHONY: test -test: - $(pytest) tests \ +.PHONY: debug-test +debug-test: + $(pytest) ./tests \ + -vvv \ -s \ --hypothesis-show-statistics \ - --hypothesis-verbosity=normal \ --hypothesis-explain \ - -vvv \ No newline at end of file + --hypothesis-profile=dev + + +.PHONY: test +test: + $(pytest) ./tests \ + --hypothesis-explain \ + --hypothesis-profile=ci \ No newline at end of file diff --git a/test-data-generation/Pipfile b/test-data-generation/Pipfile index 758bcddacb7..70da23f28f5 100644 --- a/test-data-generation/Pipfile +++ b/test-data-generation/Pipfile @@ -4,7 +4,8 @@ url = "https://pypi.org/simple" verify_ssl = true [packages] -pytest = "==7.4.3" +pytest = "==7.4.4" +pytest-asyncio = "~=0.23.0" black = "==23.11.0" mypy = "==1.7.1" flake8 = "==7.0.0" @@ -13,8 +14,9 @@ flake8-docstrings = "~=1.7.0" flake8-noqa = "~=1.4.0" hypothesis = "==6.96.1" opentrons-shared-data = {file = "../shared-data/python", editable = true} +opentrons = { editable = true, path = "../api"} test-data-generation = {file = ".", editable = true} - +astor = "0.8.1" [requires] python_version = "3.10" diff --git a/test-data-generation/Pipfile.lock b/test-data-generation/Pipfile.lock index 1b223033d61..f43daa84809 100644 --- a/test-data-generation/Pipfile.lock +++ b/test-data-generation/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1df89f797a19f2c0febc582e7452a52858511cece041f9f612a59d35628226c2" + "sha256": "149f388d38898e580ae235ebf800a3959e1018e27ceef1d12612efc5f6bad328" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,30 @@ ] }, "default": { + "aionotify": { + "hashes": [ + "sha256:385e1becfaac2d9f4326673033d53912ef9565b6febdedbec593ee966df392c6", + "sha256:64b702ad0eb115034533f9f62730a9253b79f5ff0fbf3d100c392124cdf12507" + ], + "version": "==0.2.0" + }, + "anyio": { + "hashes": [ + "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", + "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" + ], + "markers": "python_version >= '3.7'", + "version": "==3.7.1" + }, + "astor": { + "hashes": [ + "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", + "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.8.1" + }, "attrs": { "hashes": [ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", @@ -110,6 +134,14 @@ "markers": "python_version >= '3.8'", "version": "==6.96.1" }, + "idna": { + "hashes": [ + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + ], + "markers": "python_version >= '3.5'", + "version": "==3.7" + }, "iniconfig": { "hashes": [ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", @@ -176,10 +208,57 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "numpy": { + "hashes": [ + "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", + "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", + "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", + "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", + "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", + "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", + "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", + "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", + "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", + "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", + "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", + "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", + "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", + "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", + "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", + "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", + "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", + "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", + "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", + "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", + "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", + "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", + "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", + "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", + "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", + "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", + "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", + "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", + "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", + "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", + "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", + "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", + "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", + "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", + "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", + "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f" + ], + "markers": "python_version >= '3.9'", + "version": "==1.26.4" + }, + "opentrons": { + "editable": true, + "markers": "python_version >= '3.10'", + "path": "../api" + }, "opentrons-shared-data": { "editable": true, "file": "../shared-data/python", - "markers": "python_version >= '3.8'" + "markers": "python_version >= '3.10'" }, "packaging": { "hashes": [ @@ -199,19 +278,19 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", + "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.1" }, "pluggy": { "hashes": [ - "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", - "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" ], "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "version": "==1.5.0" }, "pycodestyle": { "hashes": [ @@ -317,14 +396,38 @@ "markers": "python_version >= '3.8'", "version": "==0.20.0" }, + "pyserial": { + "hashes": [ + "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", + "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0" + ], + "version": "==3.5" + }, "pytest": { "hashes": [ - "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==7.4.3" + "version": "==7.4.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.23.6" + }, + "sniffio": { + "hashes": [ + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" }, "snowballstemmer": { "hashes": [ @@ -357,7 +460,7 @@ "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.8'", "version": "==4.11.0" } }, diff --git a/test-data-generation/src/test_data_generation/deck_configuration/datashapes.py b/test-data-generation/src/test_data_generation/deck_configuration/datashapes.py index 94cf907e308..2bf2fbb110e 100644 --- a/test-data-generation/src/test_data_generation/deck_configuration/datashapes.py +++ b/test-data-generation/src/test_data_generation/deck_configuration/datashapes.py @@ -111,14 +111,14 @@ def __str__(self) -> str: return f"{(self.row + self.col).center(self.contents.longest_string())}{self.contents}" @property - def __label(self) -> SlotName: + def label(self) -> SlotName: """Return the slot label.""" return typing.cast(SlotName, f"{self.row}{self.col}") @property def slot_label_string(self) -> str: """Return the slot label.""" - return f"{self.__label.center(self.contents.longest_string())}" + return f"{self.label.center(self.contents.longest_string())}" @property def contents_string(self) -> str: diff --git a/test-data-generation/src/test_data_generation/deck_configuration/strategy/deck_configuration_strategies.py b/test-data-generation/src/test_data_generation/deck_configuration/strategy/deck_configuration_strategies.py new file mode 100644 index 00000000000..088676399ed --- /dev/null +++ b/test-data-generation/src/test_data_generation/deck_configuration/strategy/deck_configuration_strategies.py @@ -0,0 +1,53 @@ +"""Test data generation for deck configuration tests.""" +import typing +from hypothesis import assume, strategies as st +from test_data_generation.deck_configuration.datashapes import ( + DeckConfiguration, + PossibleSlotContents as PSC, +) + +from test_data_generation.deck_configuration.strategy.helper_strategies import ( + a_deck_by_columns, +) + +DeckConfigurationStrategy = typing.Callable[..., st.SearchStrategy[DeckConfiguration]] + + +@st.composite +def a_deck_configuration_with_invalid_fixture_in_col_2( + draw: st.DrawFn, +) -> DeckConfiguration: + """Generate a deck with an invalid fixture in column 2.""" + POSSIBLE_FIXTURES = [ + PSC.LABWARE_SLOT, + PSC.TEMPERATURE_MODULE, + PSC.HEATER_SHAKER_MODULE, + PSC.TRASH_BIN, + PSC.MAGNETIC_BLOCK_MODULE, + ] + INVALID_FIXTURES = [ + PSC.HEATER_SHAKER_MODULE, + PSC.TRASH_BIN, + PSC.TEMPERATURE_MODULE, + ] + + deck = draw(a_deck_by_columns(col_2_contents=POSSIBLE_FIXTURES)) + + num_invalid_fixtures = len( + [ + True + for slot in deck.column_by_number("2").slots + if slot.contents.is_one_of(INVALID_FIXTURES) + ] + ) + assume(num_invalid_fixtures > 0) + + return deck + + +DECK_CONFIGURATION_STRATEGIES: typing.Dict[str, DeckConfigurationStrategy] = { + f.__name__: f + for f in [ + a_deck_configuration_with_invalid_fixture_in_col_2, + ] +} diff --git a/test-data-generation/src/test_data_generation/deck_configuration/strategy/final_strategies.py b/test-data-generation/src/test_data_generation/deck_configuration/strategy/final_strategies.py deleted file mode 100644 index 9bf70180f96..00000000000 --- a/test-data-generation/src/test_data_generation/deck_configuration/strategy/final_strategies.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test data generation for deck configuration tests.""" -from hypothesis import assume, strategies as st -from test_data_generation.deck_configuration.datashapes import ( - Column, - DeckConfiguration, - Slot, - PossibleSlotContents as PSC, -) - -from test_data_generation.deck_configuration.strategy.helper_strategies import a_column - - -def _above_or_below_is_module_or_trash(col: Column, slot: Slot) -> bool: - """Return True if the deck has a module above or below the specified slot.""" - above = col.slot_above(slot) - below = col.slot_below(slot) - - return (above is not None and above.contents.is_module_or_trash_bin()) or ( - below is not None and below.contents.is_module_or_trash_bin() - ) - - -@st.composite -def a_deck_configuration_with_a_module_or_trash_slot_above_or_below_a_heater_shaker( - draw: st.DrawFn, -) -> DeckConfiguration: - """Generate a deck with a module or trash bin fixture above or below a heater shaker.""" - deck = draw( - st.builds( - DeckConfiguration.from_cols, - col1=a_column("1"), - col2=a_column( - "2", content_options=[PSC.LABWARE_SLOT, PSC.MAGNETIC_BLOCK_MODULE] - ), - col3=a_column("3"), - ) - ) - column = deck.column_by_number(draw(st.sampled_from(["1", "3"]))) - - assume(column.number_of(PSC.HEATER_SHAKER_MODULE) in [1, 2]) - for slot in column.slots: - if slot.contents is PSC.HEATER_SHAKER_MODULE: - assume(_above_or_below_is_module_or_trash(column, slot)) - deck.override_with_column(column) - - return deck - - -@st.composite -def a_deck_configuration_with_invalid_fixture_in_col_2( - draw: st.DrawFn, -) -> DeckConfiguration: - """Generate a deck with an invalid fixture in column 2.""" - POSSIBLE_FIXTURES = [ - PSC.LABWARE_SLOT, - PSC.TEMPERATURE_MODULE, - PSC.HEATER_SHAKER_MODULE, - PSC.TRASH_BIN, - PSC.MAGNETIC_BLOCK_MODULE, - ] - INVALID_FIXTURES = [ - PSC.HEATER_SHAKER_MODULE, - PSC.TRASH_BIN, - PSC.TEMPERATURE_MODULE, - ] - column2 = draw(a_column("2", content_options=POSSIBLE_FIXTURES)) - num_invalid_fixtures = len( - [True for slot in column2.slots if slot.contents.is_one_of(INVALID_FIXTURES)] - ) - assume(num_invalid_fixtures > 0) - - deck = draw( - st.builds( - DeckConfiguration.from_cols, - col1=a_column("1"), - col2=st.just(column2), - col3=a_column("3"), - ) - ) - - return deck diff --git a/test-data-generation/src/test_data_generation/deck_configuration/strategy/helper_strategies.py b/test-data-generation/src/test_data_generation/deck_configuration/strategy/helper_strategies.py index 17950f63a39..59bc433d7b7 100644 --- a/test-data-generation/src/test_data_generation/deck_configuration/strategy/helper_strategies.py +++ b/test-data-generation/src/test_data_generation/deck_configuration/strategy/helper_strategies.py @@ -1,20 +1,24 @@ """Test data generation for deck configuration tests.""" -from typing import List +import typing from hypothesis import strategies as st from test_data_generation.deck_configuration.datashapes import ( Column, Row, Slot, PossibleSlotContents as PSC, + DeckConfiguration, + RowName, + ColumnName, ) @st.composite def a_slot( draw: st.DrawFn, - row: str, - col: str, - content_options: List[PSC] = PSC.all(), + row: RowName, + col: ColumnName, + thermocycler_on_deck: bool, + content_options: typing.List[PSC] = PSC.all(), ) -> Slot: """Generate a slot with a random content. @@ -24,12 +28,6 @@ def a_slot( no_thermocycler = [ content for content in content_options if content is not PSC.THERMOCYCLER_MODULE ] - no_waste_chute_or_staging_area = [ - content - for content in content_options - if not content.is_a_waste_chute() and not content.is_a_staging_area() - ] - no_waste_chute_or_thermocycler = [ content for content in no_thermocycler if not content.is_a_waste_chute() ] @@ -39,17 +37,28 @@ def a_slot( if not content.is_a_staging_area() ] - if col == "1" and (row == "A" or row == "B"): + # If the deck is configured a with a thermocycler, we must ensure that no other fixture + # occupies slot a1 or b1. + # This is for 2 reasons: + # 1) The way Deck Configuration works under the hood, is that the thermocycler fixture spans the 2 slots. + # 2) When go to generate a protocol, we don't want to have to be doing a ton of checks to make sure that + # the thermocycler exists, and that there is no other fixture in the same slot. The logic is simpler just to filter + # out loading a thermocycler twice. + + in_one_of_the_slots_the_thermocycler_occupies: bool = col == "1" and ( + row == "a" or row == "b" + ) + if thermocycler_on_deck and in_one_of_the_slots_the_thermocycler_occupies: return draw( st.builds( Slot, row=st.just(row), col=st.just(col), - contents=st.sampled_from(no_waste_chute_or_staging_area), + contents=st.just(PSC.THERMOCYCLER_MODULE), ) ) elif col == "3": - if row == "D": + if row == "d": return draw( st.builds( Slot, @@ -83,17 +92,33 @@ def a_slot( @st.composite def a_row( draw: st.DrawFn, - row: str, - content_options: List[PSC] = PSC.all(), + row: RowName, + thermocycler_on_deck: bool, + content_options: typing.List[PSC] = PSC.all(), ) -> Row: """Generate a row with random slots.""" return draw( st.builds( Row, row=st.just(row), - col1=a_slot(row=row, col="1", content_options=content_options), - col2=a_slot(row=row, col="2", content_options=content_options), - col3=a_slot(row=row, col="3", content_options=content_options), + col1=a_slot( + row=row, + col="1", + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + col2=a_slot( + row=row, + col="2", + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + col3=a_slot( + row=row, + col="3", + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), ) ) @@ -101,17 +126,74 @@ def a_row( @st.composite def a_column( draw: st.DrawFn, - col: str, - content_options: List[PSC] = PSC.all(), + col: ColumnName, + thermocycler_on_deck: bool, + content_options: typing.List[PSC] = PSC.all(), ) -> Column: """Generate a column with random slots.""" return draw( st.builds( Column, col=st.just(col), - a=a_slot(row="a", col=col, content_options=content_options), - b=a_slot(row="b", col=col, content_options=content_options), - c=a_slot(row="c", col=col, content_options=content_options), - d=a_slot(row="d", col=col, content_options=content_options), + a=a_slot( + row="a", + col=col, + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + b=a_slot( + row="b", + col=col, + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + c=a_slot( + row="c", + col=col, + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + d=a_slot( + row="d", + col=col, + thermocycler_on_deck=thermocycler_on_deck, + content_options=content_options, + ), + ) + ) + + +@st.composite +def a_deck_by_columns( + draw: st.DrawFn, + thermocycler_on_deck: bool | None = None, + col_1_contents: typing.List[PSC] = PSC.all(), + col_2_contents: typing.List[PSC] = PSC.all(), + col_3_contents: typing.List[PSC] = PSC.all(), +) -> DeckConfiguration: + """Generate a deck by columns.""" + # Let the thermocycler existence be another generated value if + # not specified. + if thermocycler_on_deck is None: + thermocycler_on_deck = draw(st.booleans()) + + return draw( + st.builds( + DeckConfiguration.from_cols, + a_column( + "1", + thermocycler_on_deck=thermocycler_on_deck, + content_options=col_1_contents, + ), + a_column( + "2", + thermocycler_on_deck=thermocycler_on_deck, + content_options=col_2_contents, + ), + a_column( + "3", + thermocycler_on_deck=thermocycler_on_deck, + content_options=col_3_contents, + ), ) ) diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/__init__.py b/test-data-generation/src/test_data_generation/python_protocol_generation/__init__.py new file mode 100644 index 00000000000..45f2dcce037 --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/__init__.py @@ -0,0 +1 @@ +"""Test data generation.""" diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/ast_helpers.py b/test-data-generation/src/test_data_generation/python_protocol_generation/ast_helpers.py new file mode 100644 index 00000000000..691903e04b4 --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/ast_helpers.py @@ -0,0 +1,137 @@ +"""Abstract layer for generating AST nodes. + +Provide primitive data structures that can be used to generate AST nodes. +""" + +import typing +import ast +from dataclasses import dataclass +from test_data_generation.python_protocol_generation.util import ProtocolContextMethods + + +class CanGenerateAST(typing.Protocol): + """Protocol for objects that can generate an AST node.""" + + def generate_ast(self) -> ast.AST: + """Generate an AST node.""" + ... + + +@dataclass +class ImportStatement: + """Class to represent from some.module import a_thing statement.""" + + module: str + names: typing.List[str] + + def generate_ast(self) -> ast.ImportFrom: + """Generate an AST node for the import statement.""" + return ast.ImportFrom( + module=self.module, + names=[ast.alias(name=name, asname=None) for name in self.names], + level=0, + ) + + +@dataclass +class BaseCall: + """Class to represent a method or function call.""" + + call_on: str + what_to_call: ProtocolContextMethods | str + + def _evaluate_what_to_call(self) -> str: + """Evaluate the value of what_to_call.""" + if isinstance(self.what_to_call, ProtocolContextMethods): + return self.what_to_call.value + else: + return self.what_to_call + + def generate_ast(self) -> ast.AST: + """Generate an AST node for the call.""" + raise NotImplementedError + + +@dataclass +class CallFunction(BaseCall): + """Class to represent a method or function call.""" + + args: typing.List[str] + + def generate_ast(self) -> ast.Call: + """Generate an AST node for the call.""" + return ast.Call( + func=ast.Attribute( + value=ast.Name(id=self.call_on, ctx=ast.Load()), + attr=self._evaluate_what_to_call(), + ctx=ast.Load(), + ), + args=[ast.Constant(str_arg) for str_arg in self.args], + keywords=[], + ) + + +@dataclass +class CallAttribute(BaseCall): + """Class to represent a method or function call.""" + + def generate_ast(self) -> ast.Expr: + """Generate an AST node for the call.""" + return ast.Expr( + value=ast.Attribute( + value=ast.Name(id=self.call_on, ctx=ast.Load()), + attr=self._evaluate_what_to_call(), + ctx=ast.Load(), + ) + ) + + +@dataclass +class AssignStatement: + """Class to represent an assignment statement.""" + + var_name: str + value: CallFunction | str | ast.AST + + def generate_ast(self) -> ast.Assign: + """Generate an AST node for the assignment statement.""" + if isinstance(self.value, CallFunction): + return ast.Assign( + targets=[ast.Name(id=self.var_name, ctx=ast.Store())], + value=self.value.generate_ast(), + ) + else: + return ast.Assign( + targets=[ast.Name(id=self.var_name, ctx=ast.Store())], + value=self.value, + ) + + +@dataclass +class FunctionDefinition: + """Class to represent a function definition.""" + + name: str + args: typing.List[str] + + def generate_ast(self) -> ast.FunctionDef: + """Generate an AST node for the function definition.""" + return ast.FunctionDef( + name=self.name, + args=ast.arguments( + posonlyargs=[], + args=[ + ast.arg( + arg=arg, + ) + for arg in self.args + ], + vararg=None, + kwonlyargs=[], + kw_defaults=[], + kwarg=None, + defaults=[], + ), + body=[], + decorator_list=[], + ) diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/call_phase.py b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/call_phase.py new file mode 100644 index 00000000000..24a35efc099 --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/call_phase.py @@ -0,0 +1,38 @@ +"""This module contains functions that make calls against the various load statements in a protocol. + +Example load statements: load_module, load_labware, load_waste_chute, load_pipette, etc. +Example calls: module.labware, waste_chute.top, etc. +This is required to ensure that the loaded entities are recognized by the analysis engine. +""" +import typing +from test_data_generation.python_protocol_generation import ast_helpers as ast_h +from test_data_generation.python_protocol_generation.util import ProtocolContextMethods + + +def create_call_to_attribute_on_loaded_entity( + load_statement: ast_h.AssignStatement, +) -> ast_h.CallAttribute: + """Create a call statement from a load statement.""" + assert isinstance(load_statement.value, ast_h.CallFunction) + + if load_statement.value.what_to_call in [ + ProtocolContextMethods.LOAD_WASTE_CHUTE, + ProtocolContextMethods.LOAD_TRASH_BIN, + ]: + what_to_call = "location" + else: + what_to_call = "api_version" + + return ast_h.CallAttribute( + call_on=load_statement.var_name, + what_to_call=what_to_call, + ) + + +def create_calls_to_loaded_entities( + load_statements: typing.List[ast_h.AssignStatement], +) -> typing.List[ast_h.CallAttribute]: + """Create calls to loaded entity from .""" + return [ + create_call_to_attribute_on_loaded_entity(entity) for entity in load_statements + ] diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/load_phase.py b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/load_phase.py new file mode 100644 index 00000000000..636bbf4c2f4 --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/load_phase.py @@ -0,0 +1,248 @@ +"""This module contains the functions that generate the various load statements of a protocol. + +For example, load_module, load_labware, load_waste_chute, etc. +""" + +import typing +from test_data_generation.deck_configuration.datashapes import ( + PossibleSlotContents as PSC, + Slot, + SlotName, + RowName, +) +from test_data_generation.python_protocol_generation import ast_helpers as ast_h +from test_data_generation.python_protocol_generation.util import PipetteConfiguration +from test_data_generation.python_protocol_generation.util import ( + ModuleNames, + ProtocolContextMethods, + PROTOCOL_CONTEXT_VAR_NAME, +) + + +def _staging_area(row: RowName) -> ast_h.AssignStatement: + """Create a staging area in a specified row. + + This is done implicitly by loading a 96-well plate in column 4 of the specified row. + """ + labware_name = "nest_96_wellplate_100ul_pcr_full_skirt" + labware_location = f"{row.upper()}4" + + return ast_h.AssignStatement( + var_name=f"well_plate_{row}4", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_LABWARE, + args=[labware_name, labware_location], + ), + ) + + +def _waste_chute(has_staging_area: bool) -> typing.List[ast_h.AssignStatement]: + """Create a waste chute. + + If has_staging_area is True, a staging area is created in row D. + """ + entries = [ + ast_h.AssignStatement( + var_name="waste_chute", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_WASTE_CHUTE, + args=[], + ), + ) + ] + + if has_staging_area: + entries.append(_staging_area("d")) + + return entries + + +def _magnetic_block_on_staging_area(row: RowName) -> typing.List[ast_h.AssignStatement]: + """Create a magnetic block on a staging area in a specified row.""" + slot = typing.cast(SlotName, f"{row}3") + entries = [ + _magnetic_block(slot), + _staging_area(row), + ] + return entries + + # Call module.labware to make sure it is included as part of the analysis + + +def _trash_bin(slot: SlotName) -> ast_h.AssignStatement: + """Create a trash bin in a specified slot.""" + location = slot.upper() + + return ast_h.AssignStatement( + var_name=f"trash_bin_{slot}", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_TRASH_BIN, + args=[location], + ), + ) + + # Call trash_bin.top() to make sure it is included as part of the analysis + + +def _thermocycler_module() -> ast_h.AssignStatement: + """Create a thermocycler module.""" + return ast_h.AssignStatement( + var_name="thermocycler_module", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_MODULE, + args=[ModuleNames.THERMOCYCLER_MODULE.value], + ), + ) + + # Call module.labware to make sure it is included as part of the analysis + + +def _temperature_module(slot: SlotName) -> ast_h.AssignStatement: + """Create a temperature module in a specified slot.""" + module_location = slot.upper() + return ast_h.AssignStatement( + var_name=f"temperature_module_{slot}", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_MODULE, + args=[ModuleNames.TEMPERATURE_MODULE.value, module_location], + ), + ) + + # Call module.labware to make sure it is included as part of the analysis + + +def _magnetic_block(slot_name: SlotName) -> ast_h.AssignStatement: + """Create a magnetic block in a specified slot.""" + module_location = slot_name.upper() + return ast_h.AssignStatement( + var_name=f"mag_block_{slot_name}", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_MODULE, + args=[ModuleNames.MAGNETIC_BLOCK_MODULE.value, module_location], + ), + ) + # Call module.labware to make sure it is included as part of the analysis + + +def _heater_shaker_module(slot_name: SlotName) -> ast_h.AssignStatement: + """Create a heater shaker module in a specified slot.""" + module_location = slot_name.upper() + + return ast_h.AssignStatement( + var_name=f"heater_shaker_{slot_name}", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_MODULE, + args=[ModuleNames.HEATER_SHAKER_MODULE.value, module_location], + ), + ) + # Call module.labware to make sure it is included as part of the analysis + + +def _labware_slot(slot_name: SlotName) -> ast_h.AssignStatement: + """Create a labware slot in a specified slot.""" + labware_name = "nest_96_wellplate_100ul_pcr_full_skirt" + labware_location = slot_name.upper() + + return ast_h.AssignStatement( + var_name=f"well_plate_{slot_name}", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_LABWARE, + args=[labware_name, labware_location], + ), + ) + # well_plate_{slot}.is_tiprack + + +def create_deck_slot_load_statement( + slot: Slot, +) -> ast_h.AssignStatement | typing.List[ast_h.AssignStatement]: + """Maps the contents of a slot to the correct assign statement.""" + match slot.contents: + case PSC.WASTE_CHUTE | PSC.WASTE_CHUTE_NO_COVER: + return _waste_chute(False) + + case PSC.STAGING_AREA_WITH_WASTE_CHUTE | PSC.STAGING_AREA_WITH_WASTE_CHUTE_NO_COVER: + return _waste_chute(True) + + case PSC.STAGING_AREA_WITH_MAGNETIC_BLOCK: + return _magnetic_block_on_staging_area(slot.row) + + case PSC.TRASH_BIN: + return _trash_bin(slot.label) + + case PSC.THERMOCYCLER_MODULE: + return _thermocycler_module() + + case PSC.TEMPERATURE_MODULE: + return _temperature_module(slot.label) + + case PSC.MAGNETIC_BLOCK_MODULE: + return _magnetic_block(slot.label) + + case PSC.HEATER_SHAKER_MODULE: + return _heater_shaker_module(slot.label) + + case PSC.STAGING_AREA: + return _staging_area(slot.row) + + case PSC.LABWARE_SLOT: + return _labware_slot(slot.label) + + case _: + raise (ValueError(f"Unknown slot contents: {slot.contents}")) + + +def create_deck_slot_load_statements( + slots: typing.List[Slot], +) -> typing.List[ast_h.AssignStatement]: + """Iterates over a list of slots and creates the corresponding load statements.""" + entries: typing.List[ast_h.AssignStatement] = [] + for slot in slots: + if slot.contents == PSC.THERMOCYCLER_MODULE and slot.label == "b1": + continue + + load_statement = create_deck_slot_load_statement(slot) + if isinstance(load_statement, typing.List): + entries.extend(load_statement) + else: + entries.append(load_statement) + return entries + + +def create_pipette_load_statements( + pipette_config: PipetteConfiguration, +) -> typing.List[ast_h.AssignStatement]: + """Create the load statements for a pipette configuration.""" + entries: typing.List[ast_h.AssignStatement] = [] + if pipette_config.left is not None: + entries.append( + ast_h.AssignStatement( + var_name="left_pipette", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_INSTRUMENT, + args=[pipette_config.left.value, "left"], + ), + ) + ) + if pipette_config.right is not None: + entries.append( + ast_h.AssignStatement( + var_name="right_pipette", + value=ast_h.CallFunction( + call_on=PROTOCOL_CONTEXT_VAR_NAME, + what_to_call=ProtocolContextMethods.LOAD_INSTRUMENT, + args=[pipette_config.right.value, "right"], + ), + ) + ) + + return entries diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/setup_phase.py b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/setup_phase.py new file mode 100644 index 00000000000..f18047f975a --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/generation_phases/setup_phase.py @@ -0,0 +1,37 @@ +"""This module provides function to generate the initial setup of an Opentrons protocol.""" +import ast +import typing +from test_data_generation.python_protocol_generation import ast_helpers as ast_h +from test_data_generation.python_protocol_generation.util import ( + PROTOCOL_CONTEXT_VAR_NAME, +) + + +def create_requirements_dict( + robot_type: typing.Literal["OT-2", "OT-3"], api_version: str +) -> ast_h.AssignStatement: + """Create an assignment statement for the requirements dictionary.""" + return ast_h.AssignStatement( + var_name="requirements", + value=ast.Expression( + body=ast.Dict( + keys=[ast.Constant("robotType"), ast.Constant("apiLevel")], + values=[ast.Constant(robot_type), ast.Constant(api_version)], + ), + ), + ) + + +def import_protocol_context() -> ast_h.ImportStatement: + """Create an import statement for the ProtocolContext class.""" + return ast_h.ImportStatement( + module="opentrons.protocol_api", names=["ProtocolContext"] + ) + + +def create_protocol_context_run_function() -> ast_h.FunctionDefinition: + """Create a function definition for the run function of a protocol.""" + return ast_h.FunctionDefinition( + name="run", + args=[PROTOCOL_CONTEXT_VAR_NAME], + ) diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/python_protocol_generator.py b/test-data-generation/src/test_data_generation/python_protocol_generation/python_protocol_generator.py new file mode 100644 index 00000000000..07b433a6d8d --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/python_protocol_generator.py @@ -0,0 +1,87 @@ +"""Module for generating Python protocol code from a deck configuration.""" + +import ast +import astor # type: ignore +import typing +from .ast_helpers import CanGenerateAST +from test_data_generation.deck_configuration.datashapes import ( + DeckConfiguration, + PossibleSlotContents as PSC, +) + +from .generation_phases.setup_phase import ( + create_protocol_context_run_function, + import_protocol_context, + create_requirements_dict, +) +from .generation_phases.load_phase import ( + create_deck_slot_load_statements, + create_pipette_load_statements, +) +from .generation_phases.call_phase import create_calls_to_loaded_entities +from .util import PipetteConfiguration, PipetteNames + + +class PythonProtocolGenerator: + """Class for generating Python protocol code from a deck configuration.""" + + def __init__( + self, + deck_configuration: DeckConfiguration, + api_version: str, + ) -> None: + """Initialize the PythonProtocolGenerator. + + Call boilerplate functions to set up the protocol. + """ + self._top_level_statements: typing.List[CanGenerateAST] = [] + self._deck_configuration = deck_configuration + + self._top_level_statements.extend( + [ + import_protocol_context(), + create_requirements_dict("OT-3", api_version), + ] + ) + + self._pipettes = self._choose_pipettes() + + def _choose_pipettes(self) -> PipetteConfiguration: + """Choose the pipettes to use based on the deck configuration.""" + if self._deck_configuration.d.col3.contents.is_one_of( + [PSC.WASTE_CHUTE_NO_COVER, PSC.STAGING_AREA_WITH_WASTE_CHUTE_NO_COVER] + ): + return PipetteConfiguration( + left=PipetteNames.NINETY_SIX_CHANNEL, right=None + ) + else: + return PipetteConfiguration( + left=PipetteNames.SINGLE_CHANNEL, right=PipetteNames.MULTI_CHANNEL + ) + + def generate_protocol(self) -> str: + """Generate the Python protocol code.""" + module = ast.Module( + body=[statement.generate_ast() for statement in self._top_level_statements] + ) + run_function = create_protocol_context_run_function().generate_ast() + pipette_load_statements = create_pipette_load_statements(self._pipettes) + deck_slot_load_statements = create_deck_slot_load_statements( + self._deck_configuration.slots + ) + + calls_to_loaded_entities = create_calls_to_loaded_entities( + pipette_load_statements + deck_slot_load_statements + ) + + statements_to_make = ( + pipette_load_statements + + deck_slot_load_statements + + calls_to_loaded_entities + ) + + for statement in statements_to_make: + run_function.body.append(statement.generate_ast()) + module.body.append(run_function) + + return str(astor.to_source(module)) diff --git a/test-data-generation/src/test_data_generation/python_protocol_generation/util.py b/test-data-generation/src/test_data_generation/python_protocol_generation/util.py new file mode 100644 index 00000000000..e0516d0f1dc --- /dev/null +++ b/test-data-generation/src/test_data_generation/python_protocol_generation/util.py @@ -0,0 +1,42 @@ +"""Constants and datashapes used in the protocol generation.""" + +import dataclasses +import typing +import enum + +PROTOCOL_CONTEXT_VAR_NAME: typing.Final[str] = "protocol_context" + + +class PipetteNames(str, enum.Enum): + """Names of the pipettes used in the protocol.""" + + SINGLE_CHANNEL = "flex_1channel_1000" + MULTI_CHANNEL = "flex_8channel_1000" + NINETY_SIX_CHANNEL = "flex_96channel_1000" + + +@dataclasses.dataclass +class PipetteConfiguration: + """Configuration for a pipette.""" + + left: PipetteNames | None + right: PipetteNames | None + + +class ModuleNames(str, enum.Enum): + """Names of the modules used in the protocol.""" + + MAGNETIC_BLOCK_MODULE = "magneticBlockV1" + THERMOCYCLER_MODULE = "thermocyclerModuleV2" + TEMPERATURE_MODULE = "temperatureModuleV2" + HEATER_SHAKER_MODULE = "heaterShakerModuleV1" + + +class ProtocolContextMethods(str, enum.Enum): + """Methods available on the protocol context.""" + + LOAD_MODULE = "load_module" + LOAD_LABWARE = "load_labware" + LOAD_INSTRUMENT = "load_instrument" + LOAD_WASTE_CHUTE = "load_waste_chute" + LOAD_TRASH_BIN = "load_trash_bin" diff --git a/test-data-generation/tests/conftest.py b/test-data-generation/tests/conftest.py new file mode 100644 index 00000000000..39e39ae66e9 --- /dev/null +++ b/test-data-generation/tests/conftest.py @@ -0,0 +1,22 @@ +"""Pytest configuration file. + +Contains hypothesis settings profiles. +""" + +from hypothesis import settings, Verbosity, Phase + + +settings.register_profile( + "dev", + max_examples=10, + verbosity=Verbosity.normal, + deadline=None, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), +) + +settings.register_profile( + "ci", + max_examples=1000, + verbosity=Verbosity.verbose, + deadline=None, +) diff --git a/test-data-generation/tests/test_data_generation/deck_configuration/test_deck_configuration.py b/test-data-generation/tests/test_data_generation/deck_configuration/test_deck_configuration.py index 02c4f125187..5d68e51015c 100644 --- a/test-data-generation/tests/test_data_generation/deck_configuration/test_deck_configuration.py +++ b/test-data-generation/tests/test_data_generation/deck_configuration/test_deck_configuration.py @@ -1,41 +1,21 @@ """Tests to ensure that the deck configuration is generated correctly.""" +from pathlib import Path from hypothesis import given, settings, HealthCheck from test_data_generation.deck_configuration.datashapes import DeckConfiguration -from test_data_generation.deck_configuration.strategy.final_strategies import ( - a_deck_configuration_with_a_module_or_trash_slot_above_or_below_a_heater_shaker, +from test_data_generation.deck_configuration.strategy.deck_configuration_strategies import ( a_deck_configuration_with_invalid_fixture_in_col_2, ) - -NUM_EXAMPLES = 100 - - -@given( - deck_config=a_deck_configuration_with_a_module_or_trash_slot_above_or_below_a_heater_shaker() -) -@settings( - max_examples=NUM_EXAMPLES, - suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], +from test_data_generation.python_protocol_generation.python_protocol_generator import ( + PythonProtocolGenerator, ) -def test_above_below_heater_shaker(deck_config: DeckConfiguration) -> None: - """I hypothesize, that any deck configuration with a non-labware slot fixture above or below a heater-shaker is invalid.""" - print(deck_config) - - # TODO: create protocol and run analysis - - # protocol = create_protocol(deck) - # with pytest.assertRaises as e: - # analyze(protocol) - # assert e.exception == "Some statement about the deck configuration being invalid because of the labware above or below the Heater-Shaker" @given(deck_config=a_deck_configuration_with_invalid_fixture_in_col_2()) -@settings( - max_examples=NUM_EXAMPLES, - suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], -) -def test_invalid_fixture_in_col_2(deck_config: DeckConfiguration) -> None: +@settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) +def test_invalid_fixture_in_col_2( + deck_config: DeckConfiguration, tmp_path: Path +) -> None: """I hypothesize, that any deck configuration that contains at least one, Heater-Shaker, Trash Bin, or Temperature module, in column 2 is invalid.""" - print(deck_config) - - # TODO: Same as above + protocol_content = PythonProtocolGenerator(deck_config, "2.18").generate_protocol() + print(protocol_content)