opentrons 8.6.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/__init__.py +150 -0
- opentrons/_version.py +34 -0
- opentrons/calibration_storage/__init__.py +54 -0
- opentrons/calibration_storage/deck_configuration.py +62 -0
- opentrons/calibration_storage/encoder_decoder.py +31 -0
- opentrons/calibration_storage/file_operators.py +142 -0
- opentrons/calibration_storage/helpers.py +103 -0
- opentrons/calibration_storage/ot2/__init__.py +34 -0
- opentrons/calibration_storage/ot2/deck_attitude.py +85 -0
- opentrons/calibration_storage/ot2/mark_bad_calibration.py +27 -0
- opentrons/calibration_storage/ot2/models/__init__.py +0 -0
- opentrons/calibration_storage/ot2/models/v1.py +149 -0
- opentrons/calibration_storage/ot2/pipette_offset.py +129 -0
- opentrons/calibration_storage/ot2/tip_length.py +281 -0
- opentrons/calibration_storage/ot3/__init__.py +31 -0
- opentrons/calibration_storage/ot3/deck_attitude.py +83 -0
- opentrons/calibration_storage/ot3/gripper_offset.py +156 -0
- opentrons/calibration_storage/ot3/models/__init__.py +0 -0
- opentrons/calibration_storage/ot3/models/v1.py +122 -0
- opentrons/calibration_storage/ot3/module_offset.py +138 -0
- opentrons/calibration_storage/ot3/pipette_offset.py +95 -0
- opentrons/calibration_storage/types.py +45 -0
- opentrons/cli/__init__.py +21 -0
- opentrons/cli/__main__.py +5 -0
- opentrons/cli/analyze.py +501 -0
- opentrons/config/__init__.py +631 -0
- opentrons/config/advanced_settings.py +871 -0
- opentrons/config/defaults_ot2.py +214 -0
- opentrons/config/defaults_ot3.py +499 -0
- opentrons/config/feature_flags.py +86 -0
- opentrons/config/gripper_config.py +55 -0
- opentrons/config/reset.py +203 -0
- opentrons/config/robot_configs.py +187 -0
- opentrons/config/types.py +183 -0
- opentrons/drivers/__init__.py +0 -0
- opentrons/drivers/absorbance_reader/__init__.py +11 -0
- opentrons/drivers/absorbance_reader/abstract.py +72 -0
- opentrons/drivers/absorbance_reader/async_byonoy.py +352 -0
- opentrons/drivers/absorbance_reader/driver.py +81 -0
- opentrons/drivers/absorbance_reader/hid_protocol.py +161 -0
- opentrons/drivers/absorbance_reader/simulator.py +84 -0
- opentrons/drivers/asyncio/__init__.py +0 -0
- opentrons/drivers/asyncio/communication/__init__.py +22 -0
- opentrons/drivers/asyncio/communication/async_serial.py +183 -0
- opentrons/drivers/asyncio/communication/errors.py +88 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +552 -0
- opentrons/drivers/command_builder.py +102 -0
- opentrons/drivers/flex_stacker/__init__.py +13 -0
- opentrons/drivers/flex_stacker/abstract.py +214 -0
- opentrons/drivers/flex_stacker/driver.py +768 -0
- opentrons/drivers/flex_stacker/errors.py +68 -0
- opentrons/drivers/flex_stacker/simulator.py +309 -0
- opentrons/drivers/flex_stacker/types.py +367 -0
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/drivers/heater_shaker/__init__.py +5 -0
- opentrons/drivers/heater_shaker/abstract.py +76 -0
- opentrons/drivers/heater_shaker/driver.py +204 -0
- opentrons/drivers/heater_shaker/simulator.py +94 -0
- opentrons/drivers/mag_deck/__init__.py +6 -0
- opentrons/drivers/mag_deck/abstract.py +44 -0
- opentrons/drivers/mag_deck/driver.py +208 -0
- opentrons/drivers/mag_deck/simulator.py +63 -0
- opentrons/drivers/rpi_drivers/__init__.py +33 -0
- opentrons/drivers/rpi_drivers/dev_types.py +94 -0
- opentrons/drivers/rpi_drivers/gpio.py +282 -0
- opentrons/drivers/rpi_drivers/gpio_simulator.py +127 -0
- opentrons/drivers/rpi_drivers/interfaces.py +15 -0
- opentrons/drivers/rpi_drivers/types.py +364 -0
- opentrons/drivers/rpi_drivers/usb.py +102 -0
- opentrons/drivers/rpi_drivers/usb_simulator.py +22 -0
- opentrons/drivers/serial_communication.py +151 -0
- opentrons/drivers/smoothie_drivers/__init__.py +4 -0
- opentrons/drivers/smoothie_drivers/connection.py +51 -0
- opentrons/drivers/smoothie_drivers/constants.py +121 -0
- opentrons/drivers/smoothie_drivers/driver_3_0.py +1933 -0
- opentrons/drivers/smoothie_drivers/errors.py +49 -0
- opentrons/drivers/smoothie_drivers/parse_utils.py +143 -0
- opentrons/drivers/smoothie_drivers/simulator.py +99 -0
- opentrons/drivers/smoothie_drivers/types.py +16 -0
- opentrons/drivers/temp_deck/__init__.py +10 -0
- opentrons/drivers/temp_deck/abstract.py +54 -0
- opentrons/drivers/temp_deck/driver.py +197 -0
- opentrons/drivers/temp_deck/simulator.py +57 -0
- opentrons/drivers/thermocycler/__init__.py +12 -0
- opentrons/drivers/thermocycler/abstract.py +99 -0
- opentrons/drivers/thermocycler/driver.py +395 -0
- opentrons/drivers/thermocycler/simulator.py +126 -0
- opentrons/drivers/types.py +107 -0
- opentrons/drivers/utils.py +222 -0
- opentrons/execute.py +742 -0
- opentrons/hardware_control/__init__.py +65 -0
- opentrons/hardware_control/__main__.py +77 -0
- opentrons/hardware_control/adapters.py +98 -0
- opentrons/hardware_control/api.py +1347 -0
- opentrons/hardware_control/backends/__init__.py +7 -0
- opentrons/hardware_control/backends/controller.py +400 -0
- opentrons/hardware_control/backends/errors.py +9 -0
- opentrons/hardware_control/backends/estop_state.py +164 -0
- opentrons/hardware_control/backends/flex_protocol.py +497 -0
- opentrons/hardware_control/backends/ot3controller.py +1930 -0
- opentrons/hardware_control/backends/ot3simulator.py +900 -0
- opentrons/hardware_control/backends/ot3utils.py +664 -0
- opentrons/hardware_control/backends/simulator.py +442 -0
- opentrons/hardware_control/backends/status_bar_state.py +240 -0
- opentrons/hardware_control/backends/subsystem_manager.py +431 -0
- opentrons/hardware_control/backends/tip_presence_manager.py +173 -0
- opentrons/hardware_control/backends/types.py +14 -0
- opentrons/hardware_control/constants.py +6 -0
- opentrons/hardware_control/dev_types.py +125 -0
- opentrons/hardware_control/emulation/__init__.py +0 -0
- opentrons/hardware_control/emulation/abstract_emulator.py +21 -0
- opentrons/hardware_control/emulation/app.py +56 -0
- opentrons/hardware_control/emulation/connection_handler.py +38 -0
- opentrons/hardware_control/emulation/heater_shaker.py +150 -0
- opentrons/hardware_control/emulation/magdeck.py +60 -0
- opentrons/hardware_control/emulation/module_server/__init__.py +8 -0
- opentrons/hardware_control/emulation/module_server/client.py +78 -0
- opentrons/hardware_control/emulation/module_server/helpers.py +130 -0
- opentrons/hardware_control/emulation/module_server/models.py +31 -0
- opentrons/hardware_control/emulation/module_server/server.py +110 -0
- opentrons/hardware_control/emulation/parser.py +74 -0
- opentrons/hardware_control/emulation/proxy.py +241 -0
- opentrons/hardware_control/emulation/run_emulator.py +68 -0
- opentrons/hardware_control/emulation/scripts/__init__.py +0 -0
- opentrons/hardware_control/emulation/scripts/run_app.py +54 -0
- opentrons/hardware_control/emulation/scripts/run_module_emulator.py +72 -0
- opentrons/hardware_control/emulation/scripts/run_smoothie.py +37 -0
- opentrons/hardware_control/emulation/settings.py +119 -0
- opentrons/hardware_control/emulation/simulations.py +133 -0
- opentrons/hardware_control/emulation/smoothie.py +192 -0
- opentrons/hardware_control/emulation/tempdeck.py +69 -0
- opentrons/hardware_control/emulation/thermocycler.py +128 -0
- opentrons/hardware_control/emulation/types.py +10 -0
- opentrons/hardware_control/emulation/util.py +38 -0
- opentrons/hardware_control/errors.py +43 -0
- opentrons/hardware_control/execution_manager.py +164 -0
- opentrons/hardware_control/instruments/__init__.py +5 -0
- opentrons/hardware_control/instruments/instrument_abc.py +39 -0
- opentrons/hardware_control/instruments/ot2/__init__.py +0 -0
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +152 -0
- opentrons/hardware_control/instruments/ot2/pipette.py +777 -0
- opentrons/hardware_control/instruments/ot2/pipette_handler.py +995 -0
- opentrons/hardware_control/instruments/ot3/__init__.py +0 -0
- opentrons/hardware_control/instruments/ot3/gripper.py +420 -0
- opentrons/hardware_control/instruments/ot3/gripper_handler.py +173 -0
- opentrons/hardware_control/instruments/ot3/instrument_calibration.py +214 -0
- opentrons/hardware_control/instruments/ot3/pipette.py +858 -0
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +1030 -0
- opentrons/hardware_control/module_control.py +332 -0
- opentrons/hardware_control/modules/__init__.py +69 -0
- opentrons/hardware_control/modules/absorbance_reader.py +373 -0
- opentrons/hardware_control/modules/errors.py +7 -0
- opentrons/hardware_control/modules/flex_stacker.py +948 -0
- opentrons/hardware_control/modules/heater_shaker.py +426 -0
- opentrons/hardware_control/modules/lid_temp_status.py +35 -0
- opentrons/hardware_control/modules/magdeck.py +233 -0
- opentrons/hardware_control/modules/mod_abc.py +245 -0
- opentrons/hardware_control/modules/module_calibration.py +93 -0
- opentrons/hardware_control/modules/plate_temp_status.py +61 -0
- opentrons/hardware_control/modules/tempdeck.py +299 -0
- opentrons/hardware_control/modules/thermocycler.py +731 -0
- opentrons/hardware_control/modules/types.py +417 -0
- opentrons/hardware_control/modules/update.py +255 -0
- opentrons/hardware_control/modules/utils.py +73 -0
- opentrons/hardware_control/motion_utilities.py +318 -0
- opentrons/hardware_control/nozzle_manager.py +422 -0
- opentrons/hardware_control/ot3_calibration.py +1171 -0
- opentrons/hardware_control/ot3api.py +3227 -0
- opentrons/hardware_control/pause_manager.py +31 -0
- opentrons/hardware_control/poller.py +112 -0
- opentrons/hardware_control/protocols/__init__.py +106 -0
- opentrons/hardware_control/protocols/asyncio_configurable.py +11 -0
- opentrons/hardware_control/protocols/calibratable.py +45 -0
- opentrons/hardware_control/protocols/chassis_accessory_manager.py +90 -0
- opentrons/hardware_control/protocols/configurable.py +48 -0
- opentrons/hardware_control/protocols/event_sourcer.py +18 -0
- opentrons/hardware_control/protocols/execution_controllable.py +33 -0
- opentrons/hardware_control/protocols/flex_calibratable.py +96 -0
- opentrons/hardware_control/protocols/flex_instrument_configurer.py +52 -0
- opentrons/hardware_control/protocols/gripper_controller.py +55 -0
- opentrons/hardware_control/protocols/hardware_manager.py +51 -0
- opentrons/hardware_control/protocols/identifiable.py +16 -0
- opentrons/hardware_control/protocols/instrument_configurer.py +206 -0
- opentrons/hardware_control/protocols/liquid_handler.py +266 -0
- opentrons/hardware_control/protocols/module_provider.py +16 -0
- opentrons/hardware_control/protocols/motion_controller.py +243 -0
- opentrons/hardware_control/protocols/position_estimator.py +45 -0
- opentrons/hardware_control/protocols/simulatable.py +10 -0
- opentrons/hardware_control/protocols/stoppable.py +9 -0
- opentrons/hardware_control/protocols/types.py +27 -0
- opentrons/hardware_control/robot_calibration.py +224 -0
- opentrons/hardware_control/scripts/README.md +28 -0
- opentrons/hardware_control/scripts/__init__.py +1 -0
- opentrons/hardware_control/scripts/gripper_control.py +208 -0
- opentrons/hardware_control/scripts/ot3gripper +7 -0
- opentrons/hardware_control/scripts/ot3repl +7 -0
- opentrons/hardware_control/scripts/repl.py +187 -0
- opentrons/hardware_control/scripts/tc_control.py +97 -0
- opentrons/hardware_control/simulator_setup.py +260 -0
- opentrons/hardware_control/thread_manager.py +431 -0
- opentrons/hardware_control/threaded_async_lock.py +97 -0
- opentrons/hardware_control/types.py +792 -0
- opentrons/hardware_control/util.py +234 -0
- opentrons/legacy_broker.py +53 -0
- opentrons/legacy_commands/__init__.py +1 -0
- opentrons/legacy_commands/commands.py +483 -0
- opentrons/legacy_commands/helpers.py +153 -0
- opentrons/legacy_commands/module_commands.py +215 -0
- opentrons/legacy_commands/protocol_commands.py +54 -0
- opentrons/legacy_commands/publisher.py +155 -0
- opentrons/legacy_commands/robot_commands.py +51 -0
- opentrons/legacy_commands/types.py +1115 -0
- opentrons/motion_planning/__init__.py +32 -0
- opentrons/motion_planning/adjacent_slots_getters.py +168 -0
- opentrons/motion_planning/deck_conflict.py +396 -0
- opentrons/motion_planning/errors.py +35 -0
- opentrons/motion_planning/types.py +42 -0
- opentrons/motion_planning/waypoints.py +218 -0
- opentrons/ordered_set.py +138 -0
- opentrons/protocol_api/__init__.py +105 -0
- opentrons/protocol_api/_liquid.py +157 -0
- opentrons/protocol_api/_liquid_properties.py +814 -0
- opentrons/protocol_api/_nozzle_layout.py +31 -0
- opentrons/protocol_api/_parameter_context.py +300 -0
- opentrons/protocol_api/_parameters.py +31 -0
- opentrons/protocol_api/_transfer_liquid_validation.py +108 -0
- opentrons/protocol_api/_types.py +43 -0
- opentrons/protocol_api/config.py +23 -0
- opentrons/protocol_api/core/__init__.py +23 -0
- opentrons/protocol_api/core/common.py +33 -0
- opentrons/protocol_api/core/core_map.py +74 -0
- opentrons/protocol_api/core/engine/__init__.py +22 -0
- opentrons/protocol_api/core/engine/_default_labware_versions.py +179 -0
- opentrons/protocol_api/core/engine/deck_conflict.py +348 -0
- opentrons/protocol_api/core/engine/exceptions.py +19 -0
- opentrons/protocol_api/core/engine/instrument.py +2391 -0
- opentrons/protocol_api/core/engine/labware.py +238 -0
- opentrons/protocol_api/core/engine/load_labware_params.py +73 -0
- opentrons/protocol_api/core/engine/module_core.py +1025 -0
- opentrons/protocol_api/core/engine/overlap_versions.py +20 -0
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +358 -0
- opentrons/protocol_api/core/engine/point_calculations.py +64 -0
- opentrons/protocol_api/core/engine/protocol.py +1153 -0
- opentrons/protocol_api/core/engine/robot.py +139 -0
- opentrons/protocol_api/core/engine/stringify.py +74 -0
- opentrons/protocol_api/core/engine/transfer_components_executor.py +990 -0
- opentrons/protocol_api/core/engine/well.py +241 -0
- opentrons/protocol_api/core/instrument.py +459 -0
- opentrons/protocol_api/core/labware.py +151 -0
- opentrons/protocol_api/core/legacy/__init__.py +11 -0
- opentrons/protocol_api/core/legacy/_labware_geometry.py +37 -0
- opentrons/protocol_api/core/legacy/deck.py +369 -0
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +108 -0
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +709 -0
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +235 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +592 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +612 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +162 -0
- opentrons/protocol_api/core/legacy/load_info.py +67 -0
- opentrons/protocol_api/core/legacy/module_geometry.py +547 -0
- opentrons/protocol_api/core/legacy/well_geometry.py +148 -0
- opentrons/protocol_api/core/legacy_simulator/__init__.py +16 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +624 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +85 -0
- opentrons/protocol_api/core/module.py +484 -0
- opentrons/protocol_api/core/protocol.py +311 -0
- opentrons/protocol_api/core/robot.py +51 -0
- opentrons/protocol_api/core/well.py +116 -0
- opentrons/protocol_api/core/well_grid.py +45 -0
- opentrons/protocol_api/create_protocol_context.py +177 -0
- opentrons/protocol_api/deck.py +223 -0
- opentrons/protocol_api/disposal_locations.py +244 -0
- opentrons/protocol_api/instrument_context.py +3212 -0
- opentrons/protocol_api/labware.py +1579 -0
- opentrons/protocol_api/module_contexts.py +1425 -0
- opentrons/protocol_api/module_validation_and_errors.py +61 -0
- opentrons/protocol_api/protocol_context.py +1688 -0
- opentrons/protocol_api/robot_context.py +303 -0
- opentrons/protocol_api/validation.py +761 -0
- opentrons/protocol_engine/__init__.py +155 -0
- opentrons/protocol_engine/actions/__init__.py +65 -0
- opentrons/protocol_engine/actions/action_dispatcher.py +30 -0
- opentrons/protocol_engine/actions/action_handler.py +13 -0
- opentrons/protocol_engine/actions/actions.py +302 -0
- opentrons/protocol_engine/actions/get_state_update.py +38 -0
- opentrons/protocol_engine/clients/__init__.py +5 -0
- opentrons/protocol_engine/clients/sync_client.py +174 -0
- opentrons/protocol_engine/clients/transports.py +197 -0
- opentrons/protocol_engine/commands/__init__.py +757 -0
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +61 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +154 -0
- opentrons/protocol_engine/commands/absorbance_reader/common.py +6 -0
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +151 -0
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +154 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +226 -0
- opentrons/protocol_engine/commands/air_gap_in_place.py +162 -0
- opentrons/protocol_engine/commands/aspirate.py +244 -0
- opentrons/protocol_engine/commands/aspirate_in_place.py +184 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +211 -0
- opentrons/protocol_engine/commands/blow_out.py +146 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +119 -0
- opentrons/protocol_engine/commands/calibration/__init__.py +60 -0
- opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +166 -0
- opentrons/protocol_engine/commands/calibration/calibrate_module.py +117 -0
- opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +96 -0
- opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +156 -0
- opentrons/protocol_engine/commands/command.py +308 -0
- opentrons/protocol_engine/commands/command_unions.py +974 -0
- opentrons/protocol_engine/commands/comment.py +57 -0
- opentrons/protocol_engine/commands/configure_for_volume.py +108 -0
- opentrons/protocol_engine/commands/configure_nozzle_layout.py +115 -0
- opentrons/protocol_engine/commands/custom.py +67 -0
- opentrons/protocol_engine/commands/dispense.py +194 -0
- opentrons/protocol_engine/commands/dispense_in_place.py +179 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
- opentrons/protocol_engine/commands/drop_tip.py +232 -0
- opentrons/protocol_engine/commands/drop_tip_in_place.py +205 -0
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +64 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +900 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +293 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +281 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +339 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +328 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +326 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +61 -0
- opentrons/protocol_engine/commands/get_next_tip.py +134 -0
- opentrons/protocol_engine/commands/get_tip_presence.py +87 -0
- opentrons/protocol_engine/commands/hash_command_params.py +38 -0
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +102 -0
- opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +83 -0
- opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +82 -0
- opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +84 -0
- opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +110 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +125 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +90 -0
- opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +102 -0
- opentrons/protocol_engine/commands/home.py +100 -0
- opentrons/protocol_engine/commands/identify_module.py +86 -0
- opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
- opentrons/protocol_engine/commands/liquid_probe.py +464 -0
- opentrons/protocol_engine/commands/load_labware.py +210 -0
- opentrons/protocol_engine/commands/load_lid.py +154 -0
- opentrons/protocol_engine/commands/load_lid_stack.py +272 -0
- opentrons/protocol_engine/commands/load_liquid.py +95 -0
- opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
- opentrons/protocol_engine/commands/load_module.py +223 -0
- opentrons/protocol_engine/commands/load_pipette.py +167 -0
- opentrons/protocol_engine/commands/magnetic_module/__init__.py +32 -0
- opentrons/protocol_engine/commands/magnetic_module/disengage.py +97 -0
- opentrons/protocol_engine/commands/magnetic_module/engage.py +119 -0
- opentrons/protocol_engine/commands/move_labware.py +546 -0
- opentrons/protocol_engine/commands/move_relative.py +102 -0
- opentrons/protocol_engine/commands/move_to_addressable_area.py +176 -0
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +198 -0
- opentrons/protocol_engine/commands/move_to_coordinates.py +107 -0
- opentrons/protocol_engine/commands/move_to_well.py +119 -0
- opentrons/protocol_engine/commands/movement_common.py +338 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +241 -0
- opentrons/protocol_engine/commands/pipetting_common.py +443 -0
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +121 -0
- opentrons/protocol_engine/commands/pressure_dispense.py +155 -0
- opentrons/protocol_engine/commands/reload_labware.py +90 -0
- opentrons/protocol_engine/commands/retract_axis.py +75 -0
- opentrons/protocol_engine/commands/robot/__init__.py +70 -0
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +96 -0
- opentrons/protocol_engine/commands/robot/common.py +18 -0
- opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
- opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
- opentrons/protocol_engine/commands/robot/move_to.py +94 -0
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +86 -0
- opentrons/protocol_engine/commands/save_position.py +109 -0
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +353 -0
- opentrons/protocol_engine/commands/set_rail_lights.py +67 -0
- opentrons/protocol_engine/commands/set_status_bar.py +89 -0
- opentrons/protocol_engine/commands/temperature_module/__init__.py +46 -0
- opentrons/protocol_engine/commands/temperature_module/deactivate.py +86 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +104 -0
- opentrons/protocol_engine/commands/thermocycler/__init__.py +152 -0
- opentrons/protocol_engine/commands/thermocycler/close_lid.py +87 -0
- opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +80 -0
- opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +80 -0
- opentrons/protocol_engine/commands/thermocycler/open_lid.py +87 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +171 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +124 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +140 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +100 -0
- opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +93 -0
- opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +89 -0
- opentrons/protocol_engine/commands/touch_tip.py +189 -0
- opentrons/protocol_engine/commands/unsafe/__init__.py +161 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +100 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +121 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +82 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_close_latch.py +94 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_manual_retrieve.py +295 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_open_latch.py +91 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_prepare_shuttle.py +136 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +90 -0
- opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +153 -0
- opentrons/protocol_engine/commands/verify_tip_presence.py +100 -0
- opentrons/protocol_engine/commands/wait_for_duration.py +76 -0
- opentrons/protocol_engine/commands/wait_for_resume.py +75 -0
- opentrons/protocol_engine/create_protocol_engine.py +193 -0
- opentrons/protocol_engine/engine_support.py +28 -0
- opentrons/protocol_engine/error_recovery_policy.py +81 -0
- opentrons/protocol_engine/errors/__init__.py +191 -0
- opentrons/protocol_engine/errors/error_occurrence.py +182 -0
- opentrons/protocol_engine/errors/exceptions.py +1308 -0
- opentrons/protocol_engine/execution/__init__.py +50 -0
- opentrons/protocol_engine/execution/command_executor.py +216 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +102 -0
- opentrons/protocol_engine/execution/door_watcher.py +119 -0
- opentrons/protocol_engine/execution/equipment.py +819 -0
- opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
- opentrons/protocol_engine/execution/gantry_mover.py +686 -0
- opentrons/protocol_engine/execution/hardware_stopper.py +147 -0
- opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +207 -0
- opentrons/protocol_engine/execution/labware_movement.py +297 -0
- opentrons/protocol_engine/execution/movement.py +349 -0
- opentrons/protocol_engine/execution/pipetting.py +607 -0
- opentrons/protocol_engine/execution/queue_worker.py +86 -0
- opentrons/protocol_engine/execution/rail_lights.py +25 -0
- opentrons/protocol_engine/execution/run_control.py +33 -0
- opentrons/protocol_engine/execution/status_bar.py +34 -0
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +188 -0
- opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +81 -0
- opentrons/protocol_engine/execution/tip_handler.py +550 -0
- opentrons/protocol_engine/labware_offset_standardization.py +194 -0
- opentrons/protocol_engine/notes/__init__.py +17 -0
- opentrons/protocol_engine/notes/notes.py +59 -0
- opentrons/protocol_engine/plugins.py +104 -0
- opentrons/protocol_engine/protocol_engine.py +683 -0
- opentrons/protocol_engine/resources/__init__.py +26 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +232 -0
- opentrons/protocol_engine/resources/deck_data_provider.py +94 -0
- opentrons/protocol_engine/resources/file_provider.py +161 -0
- opentrons/protocol_engine/resources/fixture_validation.py +58 -0
- opentrons/protocol_engine/resources/labware_data_provider.py +106 -0
- opentrons/protocol_engine/resources/labware_validation.py +73 -0
- opentrons/protocol_engine/resources/model_utils.py +32 -0
- opentrons/protocol_engine/resources/module_data_provider.py +44 -0
- opentrons/protocol_engine/resources/ot3_validation.py +21 -0
- opentrons/protocol_engine/resources/pipette_data_provider.py +379 -0
- opentrons/protocol_engine/slot_standardization.py +128 -0
- opentrons/protocol_engine/state/__init__.py +1 -0
- opentrons/protocol_engine/state/_abstract_store.py +27 -0
- opentrons/protocol_engine/state/_axis_aligned_bounding_box.py +50 -0
- opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
- opentrons/protocol_engine/state/_move_types.py +83 -0
- opentrons/protocol_engine/state/_well_math.py +193 -0
- opentrons/protocol_engine/state/addressable_areas.py +699 -0
- opentrons/protocol_engine/state/command_history.py +309 -0
- opentrons/protocol_engine/state/commands.py +1158 -0
- opentrons/protocol_engine/state/config.py +39 -0
- opentrons/protocol_engine/state/files.py +57 -0
- opentrons/protocol_engine/state/fluid_stack.py +138 -0
- opentrons/protocol_engine/state/geometry.py +2359 -0
- opentrons/protocol_engine/state/inner_well_math_utils.py +548 -0
- opentrons/protocol_engine/state/labware.py +1459 -0
- opentrons/protocol_engine/state/liquid_classes.py +82 -0
- opentrons/protocol_engine/state/liquids.py +73 -0
- opentrons/protocol_engine/state/module_substates/__init__.py +45 -0
- opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +35 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +112 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +115 -0
- opentrons/protocol_engine/state/module_substates/magnetic_block_substate.py +17 -0
- opentrons/protocol_engine/state/module_substates/magnetic_module_substate.py +65 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +67 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +163 -0
- opentrons/protocol_engine/state/modules.py +1500 -0
- opentrons/protocol_engine/state/motion.py +373 -0
- opentrons/protocol_engine/state/pipettes.py +905 -0
- opentrons/protocol_engine/state/state.py +421 -0
- opentrons/protocol_engine/state/state_summary.py +36 -0
- opentrons/protocol_engine/state/tips.py +420 -0
- opentrons/protocol_engine/state/update_types.py +904 -0
- opentrons/protocol_engine/state/wells.py +290 -0
- opentrons/protocol_engine/types/__init__.py +308 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +81 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +131 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +111 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +16 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +191 -0
- opentrons/protocol_engine/types/location.py +194 -0
- opentrons/protocol_engine/types/module.py +303 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +124 -0
- opentrons/protocol_reader/__init__.py +37 -0
- opentrons/protocol_reader/extract_labware_definitions.py +66 -0
- opentrons/protocol_reader/file_format_validator.py +152 -0
- opentrons/protocol_reader/file_hasher.py +27 -0
- opentrons/protocol_reader/file_identifier.py +284 -0
- opentrons/protocol_reader/file_reader_writer.py +90 -0
- opentrons/protocol_reader/input_file.py +16 -0
- opentrons/protocol_reader/protocol_files_invalid_error.py +6 -0
- opentrons/protocol_reader/protocol_reader.py +188 -0
- opentrons/protocol_reader/protocol_source.py +124 -0
- opentrons/protocol_reader/role_analyzer.py +86 -0
- opentrons/protocol_runner/__init__.py +26 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +118 -0
- opentrons/protocol_runner/json_file_reader.py +55 -0
- opentrons/protocol_runner/json_translator.py +314 -0
- opentrons/protocol_runner/legacy_command_mapper.py +848 -0
- opentrons/protocol_runner/legacy_context_plugin.py +116 -0
- opentrons/protocol_runner/protocol_runner.py +530 -0
- opentrons/protocol_runner/python_protocol_wrappers.py +179 -0
- opentrons/protocol_runner/run_orchestrator.py +496 -0
- opentrons/protocol_runner/task_queue.py +95 -0
- opentrons/protocols/__init__.py +6 -0
- opentrons/protocols/advanced_control/__init__.py +0 -0
- opentrons/protocols/advanced_control/common.py +38 -0
- opentrons/protocols/advanced_control/mix.py +60 -0
- opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
- opentrons/protocols/advanced_control/transfers/common.py +180 -0
- opentrons/protocols/advanced_control/transfers/transfer.py +972 -0
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +231 -0
- opentrons/protocols/api_support/__init__.py +0 -0
- opentrons/protocols/api_support/constants.py +8 -0
- opentrons/protocols/api_support/deck_type.py +110 -0
- opentrons/protocols/api_support/definitions.py +18 -0
- opentrons/protocols/api_support/instrument.py +151 -0
- opentrons/protocols/api_support/labware_like.py +233 -0
- opentrons/protocols/api_support/tip_tracker.py +175 -0
- opentrons/protocols/api_support/types.py +32 -0
- opentrons/protocols/api_support/util.py +403 -0
- opentrons/protocols/bundle.py +89 -0
- opentrons/protocols/duration/__init__.py +4 -0
- opentrons/protocols/duration/errors.py +5 -0
- opentrons/protocols/duration/estimator.py +628 -0
- opentrons/protocols/execution/__init__.py +0 -0
- opentrons/protocols/execution/dev_types.py +181 -0
- opentrons/protocols/execution/errors.py +40 -0
- opentrons/protocols/execution/execute.py +84 -0
- opentrons/protocols/execution/execute_json_v3.py +275 -0
- opentrons/protocols/execution/execute_json_v4.py +359 -0
- opentrons/protocols/execution/execute_json_v5.py +28 -0
- opentrons/protocols/execution/execute_python.py +169 -0
- opentrons/protocols/execution/json_dispatchers.py +87 -0
- opentrons/protocols/execution/types.py +7 -0
- opentrons/protocols/geometry/__init__.py +0 -0
- opentrons/protocols/geometry/planning.py +297 -0
- opentrons/protocols/labware.py +312 -0
- opentrons/protocols/models/__init__.py +0 -0
- opentrons/protocols/models/json_protocol.py +679 -0
- opentrons/protocols/parameters/__init__.py +0 -0
- opentrons/protocols/parameters/csv_parameter_definition.py +77 -0
- opentrons/protocols/parameters/csv_parameter_interface.py +96 -0
- opentrons/protocols/parameters/exceptions.py +34 -0
- opentrons/protocols/parameters/parameter_definition.py +272 -0
- opentrons/protocols/parameters/types.py +17 -0
- opentrons/protocols/parameters/validation.py +267 -0
- opentrons/protocols/parse.py +671 -0
- opentrons/protocols/types.py +159 -0
- opentrons/py.typed +0 -0
- opentrons/resources/scripts/lpc21isp +0 -0
- opentrons/resources/smoothie-edge-8414642.hex +23010 -0
- opentrons/simulate.py +1065 -0
- opentrons/system/__init__.py +6 -0
- opentrons/system/camera.py +51 -0
- opentrons/system/log_control.py +59 -0
- opentrons/system/nmcli.py +856 -0
- opentrons/system/resin.py +24 -0
- opentrons/system/smoothie_update.py +15 -0
- opentrons/system/wifi.py +204 -0
- opentrons/tools/__init__.py +0 -0
- opentrons/tools/args_handler.py +22 -0
- opentrons/tools/write_pipette_memory.py +157 -0
- opentrons/types.py +618 -0
- opentrons/util/__init__.py +1 -0
- opentrons/util/async_helpers.py +166 -0
- opentrons/util/broker.py +84 -0
- opentrons/util/change_notifier.py +47 -0
- opentrons/util/entrypoint_util.py +278 -0
- opentrons/util/get_union_elements.py +26 -0
- opentrons/util/helpers.py +6 -0
- opentrons/util/linal.py +178 -0
- opentrons/util/logging_config.py +265 -0
- opentrons/util/logging_queue_handler.py +61 -0
- opentrons/util/performance_helpers.py +157 -0
- opentrons-8.6.0a1.dist-info/METADATA +37 -0
- opentrons-8.6.0a1.dist-info/RECORD +600 -0
- opentrons-8.6.0a1.dist-info/WHEEL +4 -0
- opentrons-8.6.0a1.dist-info/entry_points.txt +3 -0
- opentrons-8.6.0a1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"""
|
|
2
|
+
opentrons.protocols.parse: functions and state for parsing protocols
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import enum
|
|
7
|
+
import functools
|
|
8
|
+
import itertools
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import traceback
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from zipfile import ZipFile
|
|
15
|
+
from typing import Any, Dict, Optional, Union, Tuple, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
import jsonschema # type: ignore
|
|
18
|
+
|
|
19
|
+
from opentrons_shared_data.labware import load_schema as load_labware_schema
|
|
20
|
+
from opentrons_shared_data.protocol import (
|
|
21
|
+
Schema as JSONProtocolSchema,
|
|
22
|
+
load_schema as load_protocol_schema,
|
|
23
|
+
)
|
|
24
|
+
from opentrons_shared_data.robot.types import RobotType
|
|
25
|
+
|
|
26
|
+
from opentrons.ordered_set import OrderedSet
|
|
27
|
+
|
|
28
|
+
from .api_support.definitions import MIN_SUPPORTED_VERSION_FOR_FLEX
|
|
29
|
+
from .api_support.types import APIVersion
|
|
30
|
+
from .types import (
|
|
31
|
+
RUN_FUNCTION_MESSAGE,
|
|
32
|
+
Protocol,
|
|
33
|
+
PythonProtocol,
|
|
34
|
+
JsonProtocol,
|
|
35
|
+
StaticPythonInfo,
|
|
36
|
+
PythonProtocolMetadata,
|
|
37
|
+
PythonProtocolRequirements,
|
|
38
|
+
MalformedPythonProtocolError,
|
|
39
|
+
ApiDeprecationError,
|
|
40
|
+
)
|
|
41
|
+
from .bundle import extract_bundle
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from opentrons_shared_data.labware.types import LabwareDefinition
|
|
45
|
+
from opentrons_shared_data.protocol.types import JsonProtocol as JsonProtocolDef
|
|
46
|
+
|
|
47
|
+
MODULE_LOG = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# match e.g. "2.0" but not "hi", "2", "2.0.1"
|
|
50
|
+
API_VERSION_RE = re.compile(r"^(\d+)\.(\d+)$")
|
|
51
|
+
MAX_SUPPORTED_JSON_SCHEMA_VERSION = 5
|
|
52
|
+
API_VERSION_FOR_JSON_V5_AND_BELOW = APIVersion(2, 8)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class JSONSchemaVersionTooNewError(RuntimeError):
|
|
56
|
+
def __init__(self, attempted_schema_version: int) -> None:
|
|
57
|
+
super().__init__(attempted_schema_version)
|
|
58
|
+
self.attempted_schema_version = attempted_schema_version
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
return (
|
|
62
|
+
f"The protocol you are trying to open is a"
|
|
63
|
+
f" JSONv{self.attempted_schema_version} protocol,"
|
|
64
|
+
f" which is not supported by this software version."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PythonParseMode(enum.Enum):
|
|
69
|
+
"""Configure optional rules for when `opentrons.protocols.parse.parse()` parses Python files.
|
|
70
|
+
|
|
71
|
+
This is an August 2023 temporary measure to let us add more validation to Python files without
|
|
72
|
+
disrupting our many internal users testing the Flex.
|
|
73
|
+
https://opentrons.atlassian.net/browse/RSS-306
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
NORMAL = enum.auto()
|
|
77
|
+
"""Enforce the normal, strict, officially customer-facing rules.
|
|
78
|
+
|
|
79
|
+
You should use this mode when handling protocol files that are not already on a robot.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
ALLOW_LEGACY_METADATA_AND_REQUIREMENTS = enum.auto()
|
|
83
|
+
"""Disable enforcement of certain rules, allowing more questionable protocol files.
|
|
84
|
+
|
|
85
|
+
You should use this mode when handling protocol files that are already stored on a robot.
|
|
86
|
+
|
|
87
|
+
Certain rules were added late in Flex development, after protocol files that disobey them
|
|
88
|
+
were already put on internal testing robots. robot-server and the app generally do not
|
|
89
|
+
gracefully handle it when files that are already on a robot suddenly fail to parse; it currently
|
|
90
|
+
requires a disruptive factory-reset to get the robot usable again. To avoid making all our
|
|
91
|
+
internal users do that, we have this mode, as a temporary measure.
|
|
92
|
+
|
|
93
|
+
The specific differences from normal mode are:
|
|
94
|
+
|
|
95
|
+
1. Normally, if a protocol is for the Flex, it's an error to specify `apiLevel` 2.14 or older.
|
|
96
|
+
In this mode, the parser will allow those older `apiLevel`s. Actually running one of those
|
|
97
|
+
protocols may happen to work, or it may have obscure problems.
|
|
98
|
+
|
|
99
|
+
2. Normally, it's an error to specify unrecognized fields in the `requirements` dict.
|
|
100
|
+
In this mode, the parser ignores unrecognized fields.
|
|
101
|
+
|
|
102
|
+
3. Normally, it's an error to specify `apiLevel` in both the `metadata` and `requirements`
|
|
103
|
+
dicts simultaneously. You need to choose just one. In this mode, it's allowed, and
|
|
104
|
+
`requirements` will override `metadata` if they're different.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_v2_ast(protocol_ast: ast.Module) -> None:
|
|
109
|
+
defs = [fdef for fdef in protocol_ast.body if isinstance(fdef, ast.FunctionDef)]
|
|
110
|
+
rundefs = [fdef for fdef in defs if fdef.name == "run"]
|
|
111
|
+
# There must be precisely 1 one run function
|
|
112
|
+
if len(rundefs) > 1:
|
|
113
|
+
lines = [str(d.lineno) for d in rundefs]
|
|
114
|
+
linestr = ", ".join(lines)
|
|
115
|
+
raise MalformedPythonProtocolError(
|
|
116
|
+
short_message=f"More than one run function is defined (lines {linestr}).",
|
|
117
|
+
long_additional_message=RUN_FUNCTION_MESSAGE,
|
|
118
|
+
)
|
|
119
|
+
if not rundefs:
|
|
120
|
+
raise MalformedPythonProtocolError(
|
|
121
|
+
short_message="No function 'run(ctx)' defined",
|
|
122
|
+
long_additional_message=RUN_FUNCTION_MESSAGE,
|
|
123
|
+
)
|
|
124
|
+
if _has_api_v1_imports(protocol_ast):
|
|
125
|
+
raise MalformedPythonProtocolError(
|
|
126
|
+
short_message=(
|
|
127
|
+
"Protocol API v1 modules such as robot, instruments, and labware "
|
|
128
|
+
"may not be imported in Protocol API V2 protocols"
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_v2_static_info(static_info: StaticPythonInfo) -> None:
|
|
134
|
+
# Unlike the metadata dict, in the requirements dict, we only allow you to specify
|
|
135
|
+
# officially known keys. This lets us add new keys in the future without having to worry about
|
|
136
|
+
# conflicting with other random junk that people might have put there, and it prevents silly
|
|
137
|
+
# typos from causing confusing downstream problems.
|
|
138
|
+
allowed_requirements_keys = {
|
|
139
|
+
"apiLevel",
|
|
140
|
+
"robotType",
|
|
141
|
+
# NOTE(mm, 2023-08-08): If we add new allowed keys to this dict in the future,
|
|
142
|
+
# we should probably gate them behind new apiLevels.
|
|
143
|
+
}
|
|
144
|
+
# OrderedSet just to make the error message deterministic and easy to test.
|
|
145
|
+
actual_requirements_keys = OrderedSet((static_info.requirements or {}).keys())
|
|
146
|
+
unexpected_requirements_keys = actual_requirements_keys - allowed_requirements_keys
|
|
147
|
+
if unexpected_requirements_keys:
|
|
148
|
+
raise MalformedPythonProtocolError(
|
|
149
|
+
f"Unrecognized {'key' if len(unexpected_requirements_keys) == 1 else 'keys'}"
|
|
150
|
+
f" in requirements dict:"
|
|
151
|
+
f" {', '.join(repr(k) for k in unexpected_requirements_keys)}."
|
|
152
|
+
f" Allowed keys:"
|
|
153
|
+
f" {', '.join(repr(k) for k in allowed_requirements_keys)}."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
api_level_in_metadata = "apiLevel" in (static_info.metadata or {})
|
|
157
|
+
api_level_in_requirements = "apiLevel" in (static_info.requirements or {})
|
|
158
|
+
if api_level_in_metadata and api_level_in_requirements:
|
|
159
|
+
# If a user does this, it's almost certainly a mistake. Forbid it to avoid complexity in
|
|
160
|
+
# which dict takes precedence, and in what happens when you upload to an old software
|
|
161
|
+
# version that only knows about the metadata dict, not the requirements dict.
|
|
162
|
+
raise MalformedPythonProtocolError(
|
|
163
|
+
"You may only put apiLevel in the metadata dict or the requirements dict, not both."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validate_robot_type_at_version(robot_type: RobotType, version: APIVersion) -> None:
|
|
168
|
+
if robot_type == "OT-3 Standard" and version < MIN_SUPPORTED_VERSION_FOR_FLEX:
|
|
169
|
+
raise MalformedPythonProtocolError(
|
|
170
|
+
short_message=(
|
|
171
|
+
f"The Opentrons Flex only supports apiLevel"
|
|
172
|
+
f" {MIN_SUPPORTED_VERSION_FOR_FLEX} or newer."
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def version_from_string(vstr: str) -> APIVersion:
|
|
178
|
+
"""Parse an API version from a string
|
|
179
|
+
|
|
180
|
+
:param str vstr: The version string to parse
|
|
181
|
+
:returns APIVersion: The parsed version
|
|
182
|
+
:raises ValueError: if the version string is the wrong format
|
|
183
|
+
"""
|
|
184
|
+
matches = API_VERSION_RE.match(vstr)
|
|
185
|
+
if not matches:
|
|
186
|
+
raise MalformedPythonProtocolError(
|
|
187
|
+
short_message=(
|
|
188
|
+
f"apiLevel {vstr} is incorrectly formatted. It should be "
|
|
189
|
+
"major.minor, where both major and minor are numbers."
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
return APIVersion(major=int(matches.group(1)), minor=int(matches.group(2)))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _parse_json(
|
|
196
|
+
protocol_contents: Union[str, bytes], filename: Optional[str] = None
|
|
197
|
+
) -> JsonProtocol:
|
|
198
|
+
"""Parse a protocol known or at least suspected to be json"""
|
|
199
|
+
protocol_json = json.loads(protocol_contents)
|
|
200
|
+
version, validated = validate_json(protocol_json)
|
|
201
|
+
return JsonProtocol(
|
|
202
|
+
text=protocol_contents,
|
|
203
|
+
filename=filename,
|
|
204
|
+
contents=validated,
|
|
205
|
+
schema_version=version,
|
|
206
|
+
api_level=API_VERSION_FOR_JSON_V5_AND_BELOW,
|
|
207
|
+
metadata=validated["metadata"],
|
|
208
|
+
robot_type=validated["robot"]["model"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _parse_python(
|
|
213
|
+
protocol_contents: Union[str, bytes],
|
|
214
|
+
python_parse_mode: PythonParseMode,
|
|
215
|
+
filename: Optional[str] = None,
|
|
216
|
+
bundled_labware: Optional[Dict[str, "LabwareDefinition"]] = None,
|
|
217
|
+
bundled_data: Optional[Dict[str, bytes]] = None,
|
|
218
|
+
bundled_python: Optional[Dict[str, str]] = None,
|
|
219
|
+
extra_labware: Optional[Dict[str, "LabwareDefinition"]] = None,
|
|
220
|
+
) -> PythonProtocol:
|
|
221
|
+
"""Parse a protocol known or at least suspected to be python"""
|
|
222
|
+
if filename is None:
|
|
223
|
+
# The fallback "<protocol>" needs to match what opentrons.protocols.execution.execute_python
|
|
224
|
+
# looks for when it extracts tracebacks.
|
|
225
|
+
ast_filename = "<protocol>"
|
|
226
|
+
elif filename.endswith(".zip"):
|
|
227
|
+
# The extension ".zip" and the fallback "protocol.ot2.py" need to match what
|
|
228
|
+
# opentrons.protocols.execution.execute_python looks for when it extracts tracebacks.
|
|
229
|
+
ast_filename = "protocol.ot2.py"
|
|
230
|
+
else:
|
|
231
|
+
ast_filename = filename
|
|
232
|
+
|
|
233
|
+
# todo(mm, 2021-09-13): By default, ast.parse will inherit compiler options
|
|
234
|
+
# and future features from this module. This may not be appropriate.
|
|
235
|
+
# Investigate switching to compile() with dont_inherit=True.
|
|
236
|
+
try:
|
|
237
|
+
parsed = ast.parse(protocol_contents, filename=ast_filename)
|
|
238
|
+
except SyntaxError as syntax_error:
|
|
239
|
+
raise MalformedPythonProtocolError(
|
|
240
|
+
short_message=str(syntax_error),
|
|
241
|
+
# Get Python's nice syntax error message with carets pointing to where in the line
|
|
242
|
+
# had the problem.
|
|
243
|
+
long_additional_message="\n".join(
|
|
244
|
+
traceback.format_exception_only(type(syntax_error), syntax_error)
|
|
245
|
+
),
|
|
246
|
+
) from syntax_error
|
|
247
|
+
except ValueError as null_bytes_error:
|
|
248
|
+
# ast.parse() raises SyntaxError for most errors,
|
|
249
|
+
# but ValueError if the source contains null bytes.
|
|
250
|
+
raise MalformedPythonProtocolError(short_message=str(null_bytes_error))
|
|
251
|
+
|
|
252
|
+
static_info = _extract_static_python_info(parsed)
|
|
253
|
+
protocol = compile(parsed, filename=ast_filename, mode="exec")
|
|
254
|
+
version = _get_version(static_info, parsed, ast_filename)
|
|
255
|
+
robot_type = _robot_type_from_static_python_info(static_info)
|
|
256
|
+
|
|
257
|
+
if version >= APIVersion(2, 0):
|
|
258
|
+
_validate_v2_ast(parsed)
|
|
259
|
+
if python_parse_mode != PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS:
|
|
260
|
+
_validate_v2_static_info(static_info)
|
|
261
|
+
_validate_robot_type_at_version(robot_type, version)
|
|
262
|
+
else:
|
|
263
|
+
raise ApiDeprecationError(version)
|
|
264
|
+
|
|
265
|
+
result = PythonProtocol(
|
|
266
|
+
text=protocol_contents,
|
|
267
|
+
filename=filename,
|
|
268
|
+
contents=protocol,
|
|
269
|
+
metadata=static_info.metadata,
|
|
270
|
+
api_level=version,
|
|
271
|
+
robot_type=robot_type,
|
|
272
|
+
bundled_labware=bundled_labware,
|
|
273
|
+
bundled_data=bundled_data,
|
|
274
|
+
bundled_python=bundled_python,
|
|
275
|
+
extra_labware=extra_labware,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_bundle(
|
|
282
|
+
bundle: ZipFile, python_parse_mode: PythonParseMode, filename: Optional[str] = None
|
|
283
|
+
) -> PythonProtocol:
|
|
284
|
+
"""Parse a bundled Python protocol"""
|
|
285
|
+
contents = extract_bundle(bundle)
|
|
286
|
+
|
|
287
|
+
result = _parse_python(
|
|
288
|
+
protocol_contents=contents.protocol,
|
|
289
|
+
python_parse_mode=python_parse_mode,
|
|
290
|
+
filename=filename,
|
|
291
|
+
bundled_labware=contents.bundled_labware,
|
|
292
|
+
bundled_data=contents.bundled_data,
|
|
293
|
+
bundled_python=contents.bundled_python,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if result.api_level < APIVersion(2, 0):
|
|
297
|
+
raise MalformedPythonProtocolError(
|
|
298
|
+
short_message=f"Bundled protocols must use Protocol API v2, got {result.api_level}."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def parse(
|
|
305
|
+
protocol_file: Union[str, bytes],
|
|
306
|
+
filename: Optional[str] = None,
|
|
307
|
+
extra_labware: Optional[Dict[str, "LabwareDefinition"]] = None,
|
|
308
|
+
extra_data: Optional[Dict[str, bytes]] = None,
|
|
309
|
+
# TODO(mm, 2023-08-10): Remove python_parse_mode after the Flex launch, when the malformed
|
|
310
|
+
# protocols are no longer on any robots. https://opentrons.atlassian.net/browse/RSS-306
|
|
311
|
+
python_parse_mode: PythonParseMode = PythonParseMode.NORMAL,
|
|
312
|
+
) -> Protocol:
|
|
313
|
+
"""Parse a protocol from text.
|
|
314
|
+
|
|
315
|
+
:param protocol_file: The protocol file, or for single-file protocols, a
|
|
316
|
+
string of the protocol contents.
|
|
317
|
+
:param filename: The name of the protocol. Optional, but helps with
|
|
318
|
+
deducing the kind of protocol (e.g. if it ends with
|
|
319
|
+
'.json' we can treat it like json)
|
|
320
|
+
:param extra_labware: Any extra labware defs that should be given to the
|
|
321
|
+
protocol. Ignored if the protocol is json or zipped
|
|
322
|
+
python.
|
|
323
|
+
:param extra_data: Any extra data files that should be provided to the
|
|
324
|
+
protocol. Ignored if the protocol is json or zipped
|
|
325
|
+
python.
|
|
326
|
+
:param python_parse_mode: See `PythonParseMode`.
|
|
327
|
+
:return types.Protocol: The protocol holder, a named tuple that stores the
|
|
328
|
+
data in the protocol for later simulation or
|
|
329
|
+
execution.
|
|
330
|
+
"""
|
|
331
|
+
if filename and filename.endswith(".zip"):
|
|
332
|
+
if not isinstance(protocol_file, bytes):
|
|
333
|
+
raise RuntimeError(
|
|
334
|
+
"Please update your Run App version to support uploading a .zip file"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
with ZipFile(BytesIO(protocol_file)) as bundle:
|
|
338
|
+
result = _parse_bundle(
|
|
339
|
+
bundle=bundle, python_parse_mode=python_parse_mode, filename=filename
|
|
340
|
+
)
|
|
341
|
+
return result
|
|
342
|
+
else:
|
|
343
|
+
if filename and filename.endswith(".json"):
|
|
344
|
+
return _parse_json(protocol_file, filename)
|
|
345
|
+
elif filename and filename.endswith(".py"):
|
|
346
|
+
return _parse_python(
|
|
347
|
+
protocol_contents=protocol_file,
|
|
348
|
+
python_parse_mode=python_parse_mode,
|
|
349
|
+
filename=filename,
|
|
350
|
+
extra_labware=extra_labware,
|
|
351
|
+
bundled_data=extra_data,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# our jsonschema says the top level json kind is object so we can
|
|
355
|
+
# rely on it starting with a { if it's valid. that could either be
|
|
356
|
+
# a string or bytes.
|
|
357
|
+
#
|
|
358
|
+
# if it's a string, then if the protocol file starts with a { and
|
|
359
|
+
# we do protocol_file[0] then we get the string "{".
|
|
360
|
+
#
|
|
361
|
+
# if it's a bytes, then if the protocol file starts with the ascii or
|
|
362
|
+
# utf-8 representation of { and we do protocol_file[0] we get 123,
|
|
363
|
+
# because while single elements of strings are strings, single elements
|
|
364
|
+
# of bytes are the byte value as a number.
|
|
365
|
+
#
|
|
366
|
+
# to get that number we could either use ord() or do what we do here
|
|
367
|
+
# which I think is a little nicer, if any of the above can be called
|
|
368
|
+
# "nice".
|
|
369
|
+
if protocol_file and protocol_file[0] in ("{", b"{"[0]):
|
|
370
|
+
return _parse_json(protocol_file, filename)
|
|
371
|
+
else:
|
|
372
|
+
return _parse_python(
|
|
373
|
+
protocol_contents=protocol_file,
|
|
374
|
+
python_parse_mode=python_parse_mode,
|
|
375
|
+
filename=filename,
|
|
376
|
+
extra_labware=extra_labware,
|
|
377
|
+
bundled_data=extra_data,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _extract_static_python_info(parsed: ast.Module) -> StaticPythonInfo:
|
|
382
|
+
"""Extract statically analyzable info from a Python protocol, like its metadata.
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
ValueError: If the places that we expect to be statically analyzable are
|
|
386
|
+
actually not, or if they contain unsupported types.
|
|
387
|
+
"""
|
|
388
|
+
extracted_metadata: PythonProtocolMetadata = None
|
|
389
|
+
extracted_requirements: PythonProtocolRequirements = None
|
|
390
|
+
|
|
391
|
+
assignments = (obj for obj in parsed.body if isinstance(obj, ast.Assign))
|
|
392
|
+
for assignment in assignments:
|
|
393
|
+
target = assignment.targets[0]
|
|
394
|
+
assigned_value = assignment.value
|
|
395
|
+
|
|
396
|
+
if isinstance(target, ast.Name) and isinstance(assigned_value, ast.Dict):
|
|
397
|
+
target_name = target.id
|
|
398
|
+
if target_name == "metadata":
|
|
399
|
+
extracted_metadata = _extract_static_dict(
|
|
400
|
+
static_dict=assigned_value, name="metadata"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# `requirements` was added later. This is technically a breaking change if
|
|
404
|
+
# anyone happened to declare a module-level `requirements` variable in their
|
|
405
|
+
# protocol, since we'll now raise an error if it isn't statically parseable
|
|
406
|
+
# or if it contains unsupported types.
|
|
407
|
+
elif target_name == "requirements":
|
|
408
|
+
extracted_requirements = _extract_static_dict(
|
|
409
|
+
static_dict=assigned_value, name="requirements"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return StaticPythonInfo(
|
|
413
|
+
metadata=extracted_metadata, requirements=extracted_requirements
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _extract_static_dict(static_dict: ast.Dict, name: str) -> Dict[str, str]:
|
|
418
|
+
"""Statically read a `metadata`-like dict from a Python Protocol API file.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
static_dict: The AST node representing the dict.
|
|
422
|
+
name: The name of the dict in the user's Python source code,
|
|
423
|
+
for error reporting.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
MalformedPythonError: If the dict is too complex for this function to understand
|
|
427
|
+
statically, or if it contains unsupported types.
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
evaluated_literal = ast.literal_eval(static_dict)
|
|
431
|
+
except ValueError as exception:
|
|
432
|
+
# Undocumented, but ast.literal_eval() seems to raise ValueError for
|
|
433
|
+
# expressions that aren't statically or "safely" evaluable, like
|
|
434
|
+
# `{"o": object()}` or `{"s": "abc"[0]}`.
|
|
435
|
+
raise MalformedPythonProtocolError(
|
|
436
|
+
short_message=(
|
|
437
|
+
f"Could not read the contents of the {name} dict."
|
|
438
|
+
f" Make sure it doesn't contain any complex expressions, such as"
|
|
439
|
+
f" function calls or array indexings."
|
|
440
|
+
)
|
|
441
|
+
) from exception
|
|
442
|
+
|
|
443
|
+
# ast.literal_eval() is typed as returning Any, but we're pretty sure it
|
|
444
|
+
# should return a dict in this case because we passed it an ast.Dict.
|
|
445
|
+
assert isinstance(evaluated_literal, dict)
|
|
446
|
+
|
|
447
|
+
# Make sure we don't return anything outside of our declared return type.
|
|
448
|
+
for key, value in evaluated_literal.items():
|
|
449
|
+
if not isinstance(key, str):
|
|
450
|
+
raise MalformedPythonProtocolError(
|
|
451
|
+
short_message=(
|
|
452
|
+
f'Keys in the {name} dict must be strings, but key "{key}"'
|
|
453
|
+
f' has type "{type(key).__name__}".'
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
if not isinstance(value, str):
|
|
457
|
+
raise MalformedPythonProtocolError(
|
|
458
|
+
short_message=(
|
|
459
|
+
f'Values in the {name} dict must be strings, but value "{value}"'
|
|
460
|
+
f' has type "{type(value).__name__}".'
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return evaluated_literal
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@functools.lru_cache(1)
|
|
468
|
+
def _has_api_v1_imports(parsed: ast.Module) -> bool:
|
|
469
|
+
"""Return whether a Python protocol has import statements specific to PAPIv1."""
|
|
470
|
+
# Imports in the form of `import opentrons.robot` will have an entry in
|
|
471
|
+
# parsed.body[i].names[j].name in the form "opentrons.robot". Find those
|
|
472
|
+
# imports and transform them to strip away the 'opentrons.' part.
|
|
473
|
+
ot_imports = [
|
|
474
|
+
".".join(name.name.split(".")[1:])
|
|
475
|
+
for name in itertools.chain.from_iterable(
|
|
476
|
+
[obj.names for obj in parsed.body if isinstance(obj, ast.Import)]
|
|
477
|
+
)
|
|
478
|
+
if "opentrons" in name.name
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
# Imports in the form of `from opentrons import robot` (with or without an
|
|
482
|
+
# `as ___` statement) will have an entry in parsed.body[i].module
|
|
483
|
+
# containing "opentrons"
|
|
484
|
+
ot_from_imports = [
|
|
485
|
+
name.name
|
|
486
|
+
for name in itertools.chain.from_iterable(
|
|
487
|
+
[
|
|
488
|
+
obj.names
|
|
489
|
+
for obj in parsed.body
|
|
490
|
+
if isinstance(obj, ast.ImportFrom)
|
|
491
|
+
and obj.module
|
|
492
|
+
and "opentrons" in obj.module
|
|
493
|
+
]
|
|
494
|
+
)
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
# If any of these are populated, filter for entries with v1-specific terms
|
|
498
|
+
opentrons_imports = set(ot_imports + ot_from_imports)
|
|
499
|
+
v1_markers = set(("robot", "instruments", "modules", "containers"))
|
|
500
|
+
return bool(v1_markers.intersection(opentrons_imports))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _version_from_static_python_info(
|
|
504
|
+
static_python_info: StaticPythonInfo,
|
|
505
|
+
) -> Optional[APIVersion]:
|
|
506
|
+
"""Get an explicitly specified apiLevel from static info, if we can.
|
|
507
|
+
|
|
508
|
+
If the protocol doesn't declare apiLevel at all, return None.
|
|
509
|
+
If the protocol declares apiLevel incorrectly, raise a ValueError.
|
|
510
|
+
"""
|
|
511
|
+
from_requirements = (static_python_info.requirements or {}).get("apiLevel", None)
|
|
512
|
+
from_metadata = (static_python_info.metadata or {}).get("apiLevel", None)
|
|
513
|
+
|
|
514
|
+
requested_level = from_requirements or from_metadata
|
|
515
|
+
if requested_level is None:
|
|
516
|
+
return None
|
|
517
|
+
elif requested_level == "1":
|
|
518
|
+
# TODO(mm, 2022-10-21): Can we safely move this special case to
|
|
519
|
+
# version_from_string()?
|
|
520
|
+
return APIVersion(1, 0)
|
|
521
|
+
else:
|
|
522
|
+
return version_from_string(requested_level)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def robot_type_from_python_identifier(python_robot_type: str) -> RobotType:
|
|
526
|
+
if python_robot_type == "OT-2":
|
|
527
|
+
return "OT-2 Standard"
|
|
528
|
+
# Allow "OT-3" as a deprecated alias of "Flex" to support internal-to-Opentrons Python protocols
|
|
529
|
+
# that were written before the "Flex" name existed.
|
|
530
|
+
elif python_robot_type in ("Flex", "OT-3"):
|
|
531
|
+
return "OT-3 Standard"
|
|
532
|
+
else:
|
|
533
|
+
raise MalformedPythonProtocolError(
|
|
534
|
+
short_message=f"robotType must be 'OT-2' or 'Flex', not {repr(python_robot_type)}."
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _robot_type_from_static_python_info(
|
|
539
|
+
static_python_info: StaticPythonInfo,
|
|
540
|
+
) -> RobotType:
|
|
541
|
+
python_robot_type = (static_python_info.requirements or {}).get("robotType", None)
|
|
542
|
+
if python_robot_type is None:
|
|
543
|
+
return "OT-2 Standard"
|
|
544
|
+
else:
|
|
545
|
+
return robot_type_from_python_identifier(python_robot_type)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _get_version(
|
|
549
|
+
static_python_info: StaticPythonInfo, parsed: ast.Module, filename: str
|
|
550
|
+
) -> APIVersion:
|
|
551
|
+
"""
|
|
552
|
+
Infer protocol API version based on a combination of metadata and imports.
|
|
553
|
+
|
|
554
|
+
If a protocol specifies its API version using the 'apiLevel' key of a top-
|
|
555
|
+
level dict variable named `metadata`, the value for that key will be
|
|
556
|
+
returned as the version.
|
|
557
|
+
|
|
558
|
+
If that variable does not exist or if it does not contain the 'apiLevel'
|
|
559
|
+
key, the API version will be inferred from the imports. A script with an
|
|
560
|
+
import containing 'robot', 'instruments', or 'modules' will be assumed to
|
|
561
|
+
be an APIv1 protocol. If none of these are present, it is assumed to be an
|
|
562
|
+
APIv2 protocol (note that 'labware' is not in this list, as there is a
|
|
563
|
+
valid APIv2 import named 'labware').
|
|
564
|
+
"""
|
|
565
|
+
declared_version = _version_from_static_python_info(static_python_info)
|
|
566
|
+
if declared_version:
|
|
567
|
+
return declared_version
|
|
568
|
+
else:
|
|
569
|
+
# No apiLevel key, may be apiv1
|
|
570
|
+
if not _has_api_v1_imports(parsed):
|
|
571
|
+
raise MalformedPythonProtocolError(
|
|
572
|
+
short_message=(
|
|
573
|
+
f"apiLevel not declared in {filename}. "
|
|
574
|
+
f"You must specify the target API version "
|
|
575
|
+
f"in the apiLevel key of the metadata dict. For instance, "
|
|
576
|
+
f'metadata={{"apiLevel": "2.0"}}'
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
return APIVersion(1, 0)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _get_protocol_schema_version(protocol_json: Dict[Any, Any]) -> int:
|
|
583
|
+
# v3 and above uses `schemaVersion: integer`
|
|
584
|
+
version = protocol_json.get("schemaVersion")
|
|
585
|
+
if version:
|
|
586
|
+
return int(version)
|
|
587
|
+
# v1 uses 1.x.x and v2 uses 2.x.x
|
|
588
|
+
legacyKebabVersion = protocol_json.get("protocol-schema")
|
|
589
|
+
# No minor/patch schemas ever were released,
|
|
590
|
+
# do not permit protocols with nonexistent schema versions to load
|
|
591
|
+
if legacyKebabVersion == "1.0.0":
|
|
592
|
+
return 1
|
|
593
|
+
elif legacyKebabVersion == "2.0.0":
|
|
594
|
+
return 2
|
|
595
|
+
elif legacyKebabVersion:
|
|
596
|
+
raise RuntimeError(
|
|
597
|
+
f'No such schema version: "{legacyKebabVersion}". Did you mean '
|
|
598
|
+
+ '"1.0.0" or "2.0.0"?'
|
|
599
|
+
)
|
|
600
|
+
# no truthy value for schemaVersion or protocol-schema
|
|
601
|
+
raise RuntimeError(
|
|
602
|
+
"Could not determine schema version for protocol. "
|
|
603
|
+
+ 'Make sure there is a version number under "schemaVersion"'
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _get_schema_for_protocol(version_num: int) -> JSONProtocolSchema:
|
|
608
|
+
"""Retrieve the json schema for a protocol schema version"""
|
|
609
|
+
# TODO(IL, 2020/03/05): use $otSharedSchema, but maybe wait until
|
|
610
|
+
# deprecating v1/v2 JSON protocols?
|
|
611
|
+
if version_num > MAX_SUPPORTED_JSON_SCHEMA_VERSION:
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
f"JSON Protocol version {version_num} is not yet "
|
|
614
|
+
+ "supported in this version of the API"
|
|
615
|
+
)
|
|
616
|
+
try:
|
|
617
|
+
return load_protocol_schema(version=version_num)
|
|
618
|
+
except FileNotFoundError:
|
|
619
|
+
raise RuntimeError(
|
|
620
|
+
'JSON Protocol schema "{}" does not exist'.format(version_num)
|
|
621
|
+
) from None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def validate_json(protocol_json: Dict[Any, Any]) -> Tuple[int, "JsonProtocolDef"]:
|
|
625
|
+
"""Validates a json protocol and returns its schema version"""
|
|
626
|
+
# Check if this is actually a labware
|
|
627
|
+
labware_schema_v2 = load_labware_schema()
|
|
628
|
+
try:
|
|
629
|
+
jsonschema.validate(protocol_json, labware_schema_v2)
|
|
630
|
+
except jsonschema.ValidationError:
|
|
631
|
+
pass
|
|
632
|
+
else:
|
|
633
|
+
MODULE_LOG.error("labware uploaded instead of protocol")
|
|
634
|
+
raise RuntimeError(
|
|
635
|
+
"The file you are trying to open is a JSON labware definition, "
|
|
636
|
+
"and therefore can not be opened here. Please try "
|
|
637
|
+
"uploading a JSON protocol file instead."
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# this is now either a protocol or something corrupt
|
|
641
|
+
version_num = _get_protocol_schema_version(protocol_json)
|
|
642
|
+
if version_num <= 2:
|
|
643
|
+
raise RuntimeError(
|
|
644
|
+
f"JSON protocol version {version_num} is "
|
|
645
|
+
"deprecated. Please upload your protocol into Protocol "
|
|
646
|
+
"Designer and save it to migrate the protocol to a later "
|
|
647
|
+
"version. This error might mean a labware "
|
|
648
|
+
"definition was specified instead of a protocol."
|
|
649
|
+
)
|
|
650
|
+
if version_num > MAX_SUPPORTED_JSON_SCHEMA_VERSION:
|
|
651
|
+
raise JSONSchemaVersionTooNewError(attempted_schema_version=version_num)
|
|
652
|
+
protocol_schema = _get_schema_for_protocol(version_num)
|
|
653
|
+
|
|
654
|
+
# instruct schema how to resolve all $ref's used in protocol schemas
|
|
655
|
+
resolver = jsonschema.RefResolver(
|
|
656
|
+
protocol_schema.get("$id", ""),
|
|
657
|
+
protocol_schema,
|
|
658
|
+
store={"opentronsLabwareSchemaV2": labware_schema_v2},
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# do the validation
|
|
662
|
+
try:
|
|
663
|
+
jsonschema.validate(protocol_json, protocol_schema, resolver=resolver)
|
|
664
|
+
except jsonschema.ValidationError:
|
|
665
|
+
MODULE_LOG.exception("JSON protocol validation failed")
|
|
666
|
+
raise RuntimeError(
|
|
667
|
+
"This may be a corrupted file or a JSON file that is not an "
|
|
668
|
+
"Opentrons JSON protocol."
|
|
669
|
+
)
|
|
670
|
+
else:
|
|
671
|
+
return version_num, protocol_json # type: ignore
|